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.HashMap; 043import java.util.List; 044import java.util.Map; 045import java.util.Properties; 046import javax.annotation.PostConstruct; 047import javax.jcr.PathNotFoundException; 048import javax.servlet.http.HttpServletRequest; 049import javax.ws.rs.Produces; 050import javax.ws.rs.core.MediaType; 051import javax.ws.rs.core.MultivaluedMap; 052import javax.ws.rs.core.UriInfo; 053import javax.ws.rs.ext.MessageBodyWriter; 054import javax.ws.rs.ext.Provider; 055 056import com.google.common.collect.ImmutableMap; 057import org.apache.jena.graph.Node; 058import org.apache.jena.rdf.model.Model; 059import org.apache.jena.rdf.model.Resource; 060import org.apache.velocity.Template; 061import org.apache.velocity.VelocityContext; 062import org.apache.velocity.app.VelocityEngine; 063import org.apache.velocity.context.Context; 064import org.apache.velocity.tools.generic.EscapeTool; 065import org.apache.velocity.tools.generic.FieldTool; 066import org.fcrepo.http.api.FedoraLdp; 067import org.fcrepo.http.commons.api.rdf.HttpResourceConverter; 068import org.fcrepo.http.commons.responses.HtmlTemplate; 069import org.fcrepo.http.commons.responses.RdfNamespacedStream; 070import org.fcrepo.http.commons.responses.ViewHelpers; 071import org.fcrepo.http.commons.session.HttpSession; 072import org.fcrepo.http.commons.session.SessionFactory; 073import org.fcrepo.kernel.api.RdfLexicon; 074import org.fcrepo.kernel.api.identifiers.IdentifierConverter; 075import org.fcrepo.kernel.api.models.FedoraBinary; 076import org.fcrepo.kernel.api.models.FedoraResource; 077import org.glassfish.jersey.uri.UriTemplate; 078import org.slf4j.Logger; 079 080/** 081 * Simple HTML provider for RdfNamespacedStreams 082 * 083 * @author ajs6f 084 * @author whikloj 085 * @since Nov 19, 2013 086 */ 087@Provider 088@Produces({TEXT_HTML_WITH_CHARSET}) 089public class StreamingBaseHtmlProvider implements MessageBodyWriter<RdfNamespacedStream> { 090 091 092 @javax.ws.rs.core.Context 093 UriInfo uriInfo; 094 095 @javax.ws.rs.core.Context 096 SessionFactory sessionFactory; 097 098 @javax.ws.rs.core.Context 099 HttpServletRequest request; 100 101 private HttpSession session() { 102 return sessionFactory.getSession(request); 103 } 104 105 private IdentifierConverter<Resource, FedoraResource> translator() { 106 return new HttpResourceConverter(session(), 107 uriInfo.getBaseUriBuilder().clone().path(FedoraLdp.class)); 108 } 109 110 private static final EscapeTool escapeTool = new EscapeTool(); 111 112 private final VelocityEngine velocity = new VelocityEngine(); 113 114 /** 115 * Location in the classpath where Velocity templates are to be found. 116 */ 117 private static final String templatesLocation = "/views"; 118 119 /** 120 * A map from String names for primary node types to the Velocity templates 121 * that should be used for those node types. 122 */ 123 private Map<String, Template> templatesMap; 124 125 private static final String templateFilenameExtension = ".vsl"; 126 127 private static final String velocityPropertiesLocation = 128 "/velocity.properties"; 129 130 private static final ViewHelpers VIEW_HELPERS = ViewHelpers.getInstance(); 131 132 private static final Logger LOGGER = 133 getLogger(StreamingBaseHtmlProvider.class); 134 135 @PostConstruct 136 void init() throws IOException { 137 LOGGER.trace("Velocity engine initializing..."); 138 final Properties properties = new Properties(); 139 final String fcrepoHome = getProperty("fcrepo.home"); 140 final String velocityLog = getProperty("fcrepo.velocity.runtime.log"); 141 if (velocityLog != null) { 142 LOGGER.debug("Setting Velocity runtime log: {}", velocityLog); 143 properties.setProperty("runtime.log", velocityLog); 144 } else if (fcrepoHome != null) { 145 LOGGER.debug("Using fcrepo.home directory for the velocity log"); 146 properties.setProperty("runtime.log", fcrepoHome + getProperty("file.separator") + "velocity.log"); 147 } 148 final URL propertiesUrl = 149 getClass().getResource(velocityPropertiesLocation); 150 LOGGER.debug("Using Velocity configuration from {}", propertiesUrl); 151 try (final InputStream propertiesStream = propertiesUrl.openStream()) { 152 properties.load(propertiesStream); 153 } 154 velocity.init(properties); 155 LOGGER.trace("Velocity engine initialized."); 156 157 LOGGER.trace("Assembling a map of node primary types -> templates..."); 158 final ImmutableMap.Builder<String, Template> templatesMapBuilder = builder(); 159 160 of("fcr:versions", "fcr:fixity", "default") 161 .forEach(key -> templatesMapBuilder.put(key, velocity.getTemplate(getTemplateLocation(key)))); 162 163 templatesMap = templatesMapBuilder 164 .put(REPOSITORY_NAMESPACE + "RepositoryRoot", velocity.getTemplate(getTemplateLocation("root"))) 165 .put(REPOSITORY_NAMESPACE + "Binary", velocity.getTemplate(getTemplateLocation("binary"))) 166 .put(REPOSITORY_NAMESPACE + "Version", velocity.getTemplate(getTemplateLocation("resource"))) 167 .put(REPOSITORY_NAMESPACE + "Pairtree", velocity.getTemplate(getTemplateLocation("resource"))) 168 .put(REPOSITORY_NAMESPACE + "Container", velocity.getTemplate(getTemplateLocation("resource"))) 169 .put(LDP_NAMESPACE + "NonRdfSource", velocity.getTemplate(getTemplateLocation("binary"))) 170 .put(LDP_NAMESPACE + "RdfSource", velocity.getTemplate(getTemplateLocation("resource"))).build(); 171 172 LOGGER.trace("Assembled template map."); 173 LOGGER.trace("HtmlProvider initialization complete."); 174 } 175 176 @Override 177 public void writeTo(final RdfNamespacedStream nsStream, final Class<?> type, 178 final Type genericType, final Annotation[] annotations, 179 final MediaType mediaType, 180 final MultivaluedMap<String, Object> httpHeaders, 181 final OutputStream entityStream) throws IOException { 182 183 final Node subject = ViewHelpers.getContentNode(nsStream.stream.topic()); 184 185 final Model model = nsStream.stream.collect(toModel()); 186 model.setNsPrefixes(nsStream.namespaces); 187 188 final Template nodeTypeTemplate = getTemplate(model, subject, Arrays.asList(annotations)); 189 190 final Context context = getContext(model, subject); 191 192 final FedoraResource resource = getResourceFromSubject(subject.toString()); 193 context.put("isOriginalResource", (resource != null && resource.isOriginalResource())); 194 context.put("isVersion", (resource != null && resource.isMemento())); 195 context.put("isLDPNR", (resource != null && 196 (resource instanceof FedoraBinary || !resource.getDescribedResource().equals(resource)))); 197 198 // the contract of MessageBodyWriter<T> is _not_ to close the stream 199 // after writing to it 200 final Writer outWriter = new OutputStreamWriter(entityStream); 201 nodeTypeTemplate.merge(context, outWriter); 202 outWriter.flush(); 203 } 204 205 /** 206 * Get a FedoraResource for the subject of the graph, if it exists. 207 * 208 * @param subjectUri the uri of the subject 209 * @return FedoraResource if exists or null 210 */ 211 private FedoraResource getResourceFromSubject(final String subjectUri) { 212 final UriTemplate uriTemplate = 213 new UriTemplate(uriInfo.getBaseUriBuilder().clone().path(FedoraLdp.class).toTemplate()); 214 final Map<String, String> values = new HashMap<>(); 215 uriTemplate.match(subjectUri, values); 216 if (values.containsKey("path")) { 217 try { 218 return translator().convert(translator().toDomain(values.get("path"))); 219 } catch (final RuntimeException e) { 220 if (e.getCause() instanceof PathNotFoundException) { 221 //there is at least one case (ie Time Map endpoints - aka /fcr:versions) where the underlying jcr 222 // node will not exist while the end point is visible to the user. 223 return null; 224 } else { 225 throw e; 226 } 227 } 228 } 229 return null; 230 } 231 232 private Context getContext(final Model model, final Node subject) { 233 final FieldTool fieldTool = new FieldTool(); 234 235 final Context context = new VelocityContext(); 236 final String[] baseUrl = uriInfo.getBaseUri().getPath().split("/"); 237 if (baseUrl.length > 0) { 238 final String staticBaseUrl = String.join("/", Arrays.copyOf(baseUrl, baseUrl.length - 1)); 239 context.put("staticBaseUrl", staticBaseUrl); 240 } else { 241 context.put("staticBaseUrl", "/"); 242 } 243 context.put("rdfLexicon", fieldTool.in(RdfLexicon.class)); 244 context.put("helpers", VIEW_HELPERS); 245 context.put("esc", escapeTool); 246 context.put("rdf", model.getGraph()); 247 248 context.put("model", model); 249 context.put("subjects", model.listSubjects()); 250 context.put("nodeany", ANY); 251 context.put("topic", subject); 252 context.put("originalResource", VIEW_HELPERS.getOriginalResource(subject)); 253 context.put("uriInfo", uriInfo); 254 return context; 255 } 256 257 private Template getTemplate(final Model rdf, final Node subject, 258 final List<Annotation> annotations) { 259 260 final String tplName = annotations.stream().filter(x -> x instanceof HtmlTemplate) 261 .map(x -> ((HtmlTemplate) x).value()).filter(templatesMap::containsKey).findFirst() 262 .orElseGet(() -> { 263 final List<String> types = multiValueURI(rdf.getResource(subject.getURI()), type); 264 if (types.contains(REPOSITORY_NAMESPACE + "RepositoryRoot")) { 265 return REPOSITORY_NAMESPACE + "RepositoryRoot"; 266 } 267 return types.stream().filter(templatesMap::containsKey).findFirst().orElse("default"); 268 }); 269 LOGGER.debug("Using template: {}", tplName); 270 return templatesMap.get(tplName); 271 } 272 273 @Override 274 public boolean isWriteable(final Class<?> type, final Type genericType, 275 final Annotation[] annotations, final MediaType mediaType) { 276 LOGGER.debug( 277 "Checking to see if type: {} is serializable to mimeType: {}", 278 type.getName(), mediaType); 279 return isTextHtml(mediaType) && RdfNamespacedStream.class.isAssignableFrom(type); 280 } 281 282 @Override 283 public long getSize(final RdfNamespacedStream t, final Class<?> type, 284 final Type genericType, final Annotation[] annotations, 285 final MediaType mediaType) { 286 // we don't know in advance how large the result might be 287 return -1; 288 } 289 290 private static String getTemplateLocation(final String nodeTypeName) { 291 return templatesLocation + "/" + 292 nodeTypeName.replace(':', '-') + templateFilenameExtension; 293 } 294 295 private static boolean isTextHtml(final MediaType mediaType) { 296 return mediaType.getType().equals(TEXT_HTML_TYPE.getType()) && 297 mediaType.getSubtype().equals(TEXT_HTML_TYPE.getSubtype()); 298 } 299 300}