001/* 002 * Licensed to DuraSpace under one or more contributor license agreements. 003 * See the NOTICE file distributed with this work for additional information 004 * regarding copyright ownership. 005 * 006 * DuraSpace licenses this file to you under the Apache License, 007 * Version 2.0 (the "License"); you may not use this file except in 008 * compliance with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018package org.fcrepo.http.api.responses; 019 020import static java.lang.System.getProperty; 021import static java.util.stream.Stream.of; 022import static javax.ws.rs.core.MediaType.TEXT_HTML_TYPE; 023import static com.google.common.collect.ImmutableMap.builder; 024import static org.apache.jena.graph.Node.ANY; 025import static org.apache.jena.sparql.util.graph.GraphUtils.multiValueURI; 026import static org.apache.jena.vocabulary.RDF.type; 027import static org.fcrepo.http.commons.domain.RDFMediaType.TEXT_HTML_WITH_CHARSET; 028import static org.fcrepo.kernel.api.RdfLexicon.LDP_NAMESPACE; 029import static org.fcrepo.kernel.api.RdfLexicon.REPOSITORY_NAMESPACE; 030import static org.fcrepo.kernel.api.RdfCollectors.toModel; 031import static org.slf4j.LoggerFactory.getLogger; 032 033import java.io.IOException; 034import java.io.InputStream; 035import java.io.OutputStream; 036import java.io.OutputStreamWriter; 037import java.io.Writer; 038import java.lang.annotation.Annotation; 039import java.lang.reflect.Type; 040import java.net.URL; 041import java.util.Arrays; 042import java.util.List; 043import java.util.Map; 044import java.util.Properties; 045import java.util.stream.Collectors; 046import javax.annotation.PostConstruct; 047import javax.ws.rs.Produces; 048import javax.ws.rs.core.MediaType; 049import javax.ws.rs.core.MultivaluedMap; 050import javax.ws.rs.core.UriInfo; 051import javax.ws.rs.ext.MessageBodyWriter; 052import javax.ws.rs.ext.Provider; 053 054import com.google.common.collect.ImmutableMap; 055import org.apache.jena.graph.Node; 056import org.apache.jena.rdf.model.Model; 057import org.apache.velocity.Template; 058import org.apache.velocity.VelocityContext; 059import org.apache.velocity.app.VelocityEngine; 060import org.apache.velocity.context.Context; 061import org.apache.velocity.tools.generic.EscapeTool; 062import org.apache.velocity.tools.generic.FieldTool; 063import org.fcrepo.http.commons.responses.HtmlTemplate; 064import org.fcrepo.http.commons.responses.RdfNamespacedStream; 065import org.fcrepo.http.commons.responses.ViewHelpers; 066import org.fcrepo.kernel.api.RdfLexicon; 067import org.slf4j.Logger; 068 069/** 070 * Simple HTML provider for RdfNamespacedStreams 071 * 072 * @author ajs6f 073 * @since Nov 19, 2013 074 */ 075@Provider 076@Produces({TEXT_HTML_WITH_CHARSET}) 077public class StreamingBaseHtmlProvider implements MessageBodyWriter<RdfNamespacedStream> { 078 079 080 @javax.ws.rs.core.Context 081 UriInfo uriInfo; 082 083 private static EscapeTool escapeTool = new EscapeTool(); 084 085 protected VelocityEngine velocity = new VelocityEngine(); 086 087 /** 088 * Location in the classpath where Velocity templates are to be found. 089 */ 090 public static final String templatesLocation = "/views"; 091 092 /** 093 * A map from String names for primary node types to the Velocity templates 094 * that should be used for those node types. 095 */ 096 protected Map<String, Template> templatesMap; 097 098 public static final String templateFilenameExtension = ".vsl"; 099 100 public static final String velocityPropertiesLocation = 101 "/velocity.properties"; 102 103 private static final ViewHelpers VIEW_HELPERS = ViewHelpers.getInstance(); 104 105 private static final Logger LOGGER = 106 getLogger(StreamingBaseHtmlProvider.class); 107 108 @PostConstruct 109 void init() throws IOException { 110 LOGGER.trace("Velocity engine initializing..."); 111 final Properties properties = new Properties(); 112 final String fcrepoHome = getProperty("fcrepo.home"); 113 final String velocityLog = getProperty("fcrepo.velocity.runtime.log"); 114 if (velocityLog != null) { 115 LOGGER.debug("Setting Velocity runtime log: {}", velocityLog); 116 properties.setProperty("runtime.log", velocityLog); 117 } else if (fcrepoHome != null) { 118 LOGGER.debug("Using fcrepo.home directory for the velocity log"); 119 properties.setProperty("runtime.log", fcrepoHome + getProperty("file.separator") + "velocity.log"); 120 } 121 final URL propertiesUrl = 122 getClass().getResource(velocityPropertiesLocation); 123 LOGGER.debug("Using Velocity configuration from {}", propertiesUrl); 124 try (final InputStream propertiesStream = propertiesUrl.openStream()) { 125 properties.load(propertiesStream); 126 } 127 velocity.init(properties); 128 LOGGER.trace("Velocity engine initialized."); 129 130 LOGGER.trace("Assembling a map of node primary types -> templates..."); 131 final ImmutableMap.Builder<String, Template> templatesMapBuilder = builder(); 132 133 of("fcr:versions", "fcr:fixity", "default") 134 .forEach(key -> templatesMapBuilder.put(key, velocity.getTemplate(getTemplateLocation(key)))); 135 136 templatesMap = templatesMapBuilder 137 .put(REPOSITORY_NAMESPACE + "RepositoryRoot", velocity.getTemplate(getTemplateLocation("root"))) 138 .put(REPOSITORY_NAMESPACE + "Binary", velocity.getTemplate(getTemplateLocation("binary"))) 139 .put(REPOSITORY_NAMESPACE + "Version", velocity.getTemplate(getTemplateLocation("resource"))) 140 .put(REPOSITORY_NAMESPACE + "Pairtree", velocity.getTemplate(getTemplateLocation("resource"))) 141 .put(REPOSITORY_NAMESPACE + "Container", velocity.getTemplate(getTemplateLocation("resource"))) 142 .put(LDP_NAMESPACE + "NonRdfSource", velocity.getTemplate(getTemplateLocation("binary"))) 143 .put(LDP_NAMESPACE + "RdfSource", velocity.getTemplate(getTemplateLocation("resource"))).build(); 144 145 LOGGER.trace("Assembled template map."); 146 LOGGER.trace("HtmlProvider initialization complete."); 147 } 148 149 @Override 150 public void writeTo(final RdfNamespacedStream nsStream, final Class<?> type, 151 final Type genericType, final Annotation[] annotations, 152 final MediaType mediaType, 153 final MultivaluedMap<String, Object> httpHeaders, 154 final OutputStream entityStream) throws IOException { 155 156 final Node subject = ViewHelpers.getContentNode(nsStream.stream.topic()); 157 final Model model = nsStream.stream.collect(toModel()); 158 model.setNsPrefixes(nsStream.namespaces); 159 160 final Template nodeTypeTemplate = getTemplate(model, subject, Arrays.asList(annotations)); 161 162 final Context context = getContext(model, subject); 163 164 // the contract of MessageBodyWriter<T> is _not_ to close the stream 165 // after writing to it 166 final Writer outWriter = new OutputStreamWriter(entityStream); 167 nodeTypeTemplate.merge(context, outWriter); 168 outWriter.flush(); 169 } 170 171 protected Context getContext(final Model model, final Node subject) { 172 final FieldTool fieldTool = new FieldTool(); 173 174 final Context context = new VelocityContext(); 175 final String[] baseUrl = uriInfo.getBaseUri().getPath().split("/"); 176 if (baseUrl.length > 0) { 177 final String staticBaseUrl = 178 Arrays.asList(Arrays.copyOf(baseUrl, baseUrl.length - 1)).stream().collect(Collectors.joining("/")); 179 context.put("staticBaseUrl", staticBaseUrl); 180 } else { 181 context.put("staticBaseUrl", "/"); 182 } 183 context.put("rdfLexicon", fieldTool.in(RdfLexicon.class)); 184 context.put("helpers", VIEW_HELPERS); 185 context.put("esc", escapeTool); 186 context.put("rdf", model.getGraph()); 187 188 context.put("model", model); 189 context.put("subjects", model.listSubjects()); 190 context.put("nodeany", ANY); 191 context.put("topic", subject); 192 context.put("uriInfo", uriInfo); 193 return context; 194 } 195 196 private Template getTemplate(final Model rdf, final Node subject, 197 final List<Annotation> annotations) { 198 199 final String tplName = annotations.stream().filter(x -> x instanceof HtmlTemplate) 200 .map(x -> ((HtmlTemplate) x).value()).filter(templatesMap::containsKey).findFirst() 201 .orElseGet(() -> { 202 final List<String> types = multiValueURI(rdf.getResource(subject.getURI()), type); 203 if (types.contains(REPOSITORY_NAMESPACE + "RepositoryRoot")) { 204 return REPOSITORY_NAMESPACE + "RepositoryRoot"; 205 } 206 return types.stream().filter(templatesMap::containsKey).findFirst().orElse("default"); 207 }); 208 LOGGER.debug("Using template: {}", tplName); 209 return templatesMap.get(tplName); 210 } 211 212 @Override 213 public boolean isWriteable(final Class<?> type, final Type genericType, 214 final Annotation[] annotations, final MediaType mediaType) { 215 LOGGER.debug( 216 "Checking to see if type: {} is serializable to mimeType: {}", 217 type.getName(), mediaType); 218 return isTextHtml(mediaType) && RdfNamespacedStream.class.isAssignableFrom(type); 219 } 220 221 @Override 222 public long getSize(final RdfNamespacedStream t, final Class<?> type, 223 final Type genericType, final Annotation[] annotations, 224 final MediaType mediaType) { 225 // we don't know in advance how large the result might be 226 return -1; 227 } 228 229 private static String getTemplateLocation(final String nodeTypeName) { 230 return templatesLocation + "/" + 231 nodeTypeName.replace(':', '-') + templateFilenameExtension; 232 } 233 234 private static boolean isTextHtml(final MediaType mediaType) { 235 return mediaType.getType().equals(TEXT_HTML_TYPE.getType()) && 236 mediaType.getSubtype().equals(TEXT_HTML_TYPE.getSubtype()); 237 } 238 239}