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.util.stream.Stream.of; 021import static javax.ws.rs.core.MediaType.TEXT_HTML_TYPE; 022import static com.google.common.collect.ImmutableMap.builder; 023import static org.apache.jena.graph.Node.ANY; 024import static org.apache.jena.sparql.util.graph.GraphUtils.multiValueURI; 025import static org.apache.jena.vocabulary.RDF.type; 026import static org.fcrepo.http.commons.domain.RDFMediaType.TEXT_HTML_WITH_CHARSET; 027import static org.fcrepo.http.commons.session.TransactionConstants.ATOMIC_ID_HEADER; 028import static org.fcrepo.kernel.api.RdfLexicon.ARCHIVAL_GROUP; 029import static org.fcrepo.kernel.api.RdfLexicon.NON_RDF_SOURCE; 030import static org.fcrepo.kernel.api.RdfLexicon.RDF_SOURCE; 031import static org.fcrepo.kernel.api.RdfCollectors.toModel; 032import static org.fcrepo.kernel.api.RdfLexicon.REPOSITORY_ROOT; 033import static org.slf4j.LoggerFactory.getLogger; 034 035import java.io.IOException; 036import java.io.InputStream; 037import java.io.OutputStream; 038import java.io.OutputStreamWriter; 039import java.io.Writer; 040import java.lang.annotation.Annotation; 041import java.lang.reflect.Type; 042import java.net.URI; 043import java.net.URL; 044import java.nio.charset.StandardCharsets; 045import java.util.Arrays; 046import java.util.List; 047import java.util.Map; 048import java.util.Properties; 049import javax.annotation.PostConstruct; 050import javax.inject.Inject; 051import javax.servlet.http.HttpServletRequest; 052import javax.ws.rs.Produces; 053import javax.ws.rs.core.MediaType; 054import javax.ws.rs.core.MultivaluedMap; 055import javax.ws.rs.core.UriBuilder; 056import javax.ws.rs.core.UriInfo; 057import javax.ws.rs.ext.MessageBodyWriter; 058import javax.ws.rs.ext.Provider; 059 060import com.google.common.collect.ImmutableMap; 061import org.apache.jena.graph.Node; 062import org.apache.jena.rdf.model.Model; 063import org.apache.velocity.Template; 064import org.apache.velocity.VelocityContext; 065import org.apache.velocity.app.VelocityEngine; 066import org.apache.velocity.context.Context; 067import org.apache.velocity.tools.generic.EscapeTool; 068import org.apache.velocity.tools.generic.FieldTool; 069import org.fcrepo.config.FedoraPropsConfig; 070import org.fcrepo.config.OcflPropsConfig; 071import org.fcrepo.http.api.FedoraLdp; 072import org.fcrepo.http.commons.api.rdf.HttpIdentifierConverter; 073import org.fcrepo.http.commons.responses.HtmlTemplate; 074import org.fcrepo.http.commons.responses.RdfNamespacedStream; 075import org.fcrepo.http.commons.responses.ViewHelpers; 076import org.fcrepo.kernel.api.ReadOnlyTransaction; 077import org.fcrepo.kernel.api.Transaction; 078import org.fcrepo.kernel.api.RdfLexicon; 079import org.fcrepo.kernel.api.TransactionManager; 080import org.fcrepo.kernel.api.exception.PathNotFoundException; 081import org.fcrepo.kernel.api.exception.RepositoryRuntimeException; 082import org.fcrepo.kernel.api.identifiers.FedoraId; 083import org.fcrepo.kernel.api.models.Binary; 084import org.fcrepo.kernel.api.models.FedoraResource; 085import org.fcrepo.kernel.api.models.ResourceFactory; 086import org.slf4j.Logger; 087 088/** 089 * Simple HTML provider for RdfNamespacedStreams 090 * 091 * @author ajs6f 092 * @author whikloj 093 * @since Nov 19, 2013 094 */ 095@Provider 096@Produces({TEXT_HTML_WITH_CHARSET}) 097public class StreamingBaseHtmlProvider implements MessageBodyWriter<RdfNamespacedStream> { 098 099 100 @Inject 101 UriInfo uriInfo; 102 103 @Inject 104 TransactionManager transactionManager; 105 106 @Inject 107 HttpServletRequest request; 108 109 @Inject 110 private ResourceFactory resourceFactory; 111 112 @Inject 113 private OcflPropsConfig ocflPropsConfig; 114 115 @Inject 116 private FedoraPropsConfig fedoraPropsConfig; 117 118 private boolean autoVersioningEnabled; 119 120 private HttpIdentifierConverter identifierConverter; 121 122 private Transaction readOnlyTx; 123 124 private Transaction transaction() { 125 if (request.getHeader(ATOMIC_ID_HEADER) != null) { 126 return transactionManager.get(request.getHeader(ATOMIC_ID_HEADER)); 127 } 128 return readOnlyTx; 129 } 130 131 private HttpIdentifierConverter identifierConverter() { 132 if (identifierConverter == null) { 133 final UriBuilder uriBuilder = 134 uriInfo.getBaseUriBuilder().clone().path(FedoraLdp.class); 135 identifierConverter = new HttpIdentifierConverter(uriBuilder); 136 } 137 return identifierConverter; 138 } 139 140 private static final EscapeTool escapeTool = new EscapeTool(); 141 142 private final VelocityEngine velocity = new VelocityEngine(); 143 144 /** 145 * Location in the classpath where Velocity templates are to be found. 146 */ 147 private static final String templatesLocation = "/views"; 148 149 /** 150 * A map from String names for primary node types to the Velocity templates 151 * that should be used for those node types. 152 */ 153 private Map<String, Template> templatesMap; 154 155 private static final String templateFilenameExtension = ".vsl"; 156 157 private static final String velocityPropertiesLocation = 158 "/velocity.properties"; 159 160 private static final ViewHelpers VIEW_HELPERS = ViewHelpers.getInstance(); 161 162 private static final Logger LOGGER = 163 getLogger(StreamingBaseHtmlProvider.class); 164 165 @PostConstruct 166 void init() throws IOException { 167 LOGGER.trace("Velocity engine initializing..."); 168 final Properties properties = new Properties(); 169 final var velocityLog = fedoraPropsConfig.getVelocityLog().toString(); 170 autoVersioningEnabled = ocflPropsConfig.isAutoVersioningEnabled(); 171 LOGGER.debug("Setting Velocity runtime log: {}", velocityLog); 172 properties.setProperty("runtime.log", velocityLog); 173 174 final URL propertiesUrl = 175 getClass().getResource(velocityPropertiesLocation); 176 LOGGER.debug("Using Velocity configuration from {}", propertiesUrl); 177 try (final InputStream propertiesStream = propertiesUrl.openStream()) { 178 properties.load(propertiesStream); 179 } 180 velocity.init(properties); 181 LOGGER.trace("Velocity engine initialized."); 182 183 LOGGER.trace("Assembling a map of node primary types -> templates..."); 184 final ImmutableMap.Builder<String, Template> templatesMapBuilder = builder(); 185 186 of("fcr:versions", "fcr:fixity", "default") 187 .forEach(key -> templatesMapBuilder.put(key, velocity.getTemplate(getTemplateLocation(key)))); 188 189 templatesMap = templatesMapBuilder 190 .put(REPOSITORY_ROOT.toString(), velocity.getTemplate(getTemplateLocation("root"))) 191 .put(NON_RDF_SOURCE.toString(), velocity.getTemplate(getTemplateLocation("binary"))) 192 .put(RDF_SOURCE.toString(), velocity.getTemplate(getTemplateLocation("resource"))).build(); 193 194 LOGGER.trace("Assembled template map."); 195 196 readOnlyTx = ReadOnlyTransaction.INSTANCE; 197 198 LOGGER.trace("HtmlProvider initialization complete."); 199 } 200 201 @Override 202 public void writeTo(final RdfNamespacedStream nsStream, final Class<?> type, 203 final Type genericType, final Annotation[] annotations, 204 final MediaType mediaType, 205 final MultivaluedMap<String, Object> httpHeaders, 206 final OutputStream entityStream) throws IOException { 207 208 final Node subject = ViewHelpers.getContentNode(nsStream.stream.topic()); 209 210 final Model model = nsStream.stream.collect(toModel()); 211 model.setNsPrefixes(nsStream.namespaces); 212 213 final Template nodeTypeTemplate = getTemplate(model, subject, Arrays.asList(annotations)); 214 215 final Context context = getContext(model, subject); 216 217 final FedoraId fedoraID = FedoraId.create(identifierConverter().toInternalId(subject.toString())); 218 try { 219 final FedoraResource resource = getResourceFromSubject(fedoraID); 220 context.put("isOriginalResource", (resource != null && resource.isOriginalResource())); 221 context.put("isArchivalGroup", (resource != null && resource.getSystemTypes(false) 222 .contains(URI.create(ARCHIVAL_GROUP.getURI())))); 223 context.put("isVersion", (resource != null && resource.isMemento())); 224 context.put("isLDPNR", (resource != null && (resource instanceof Binary || !resource 225 .getDescribedResource().equals(resource)))); 226 } catch (final PathNotFoundException e) { 227 final var baseId = FedoraId.create(fedoraID.getBaseId()); 228 if (fedoraID.isRepositoryRoot() || baseId.isRepositoryRoot()) { 229 // We have requested the root resource or default ACL. 230 context.put("isOriginalResource", true); 231 context.put("isArchivalGroup", false); 232 context.put("isVersion", false); 233 context.put("isLDPNR", false); 234 } else { 235 throw new RepositoryRuntimeException(e.getMessage(), e); 236 } 237 } 238 context.put("autoVersioningEnabled", autoVersioningEnabled); 239 240 // the contract of MessageBodyWriter<T> is _not_ to close the stream 241 // after writing to it 242 final Writer outWriter = new OutputStreamWriter(entityStream, StandardCharsets.UTF_8); 243 nodeTypeTemplate.merge(context, outWriter); 244 outWriter.flush(); 245 } 246 247 /** 248 * Get a FedoraResource for the subject of the graph, if it exists. 249 * 250 * @param resourceId the FedoraId of the subject 251 * @return FedoraResource if exists or null 252 * @throws PathNotFoundException if the OCFL mapping is not found. 253 */ 254 private FedoraResource getResourceFromSubject(final FedoraId resourceId) throws PathNotFoundException { 255 return resourceFactory.getResource(transaction(), resourceId); 256 } 257 258 private Context getContext(final Model model, final Node subject) { 259 final FieldTool fieldTool = new FieldTool(); 260 261 final Context context = new VelocityContext(); 262 final String[] baseUrl = uriInfo.getBaseUri().getPath().split("/"); 263 if (baseUrl.length > 0) { 264 final String staticBaseUrl = String.join("/", Arrays.copyOf(baseUrl, baseUrl.length - 1)); 265 context.put("staticBaseUrl", staticBaseUrl); 266 } else { 267 context.put("staticBaseUrl", "/"); 268 } 269 context.put("rdfLexicon", fieldTool.in(RdfLexicon.class)); 270 context.put("helpers", VIEW_HELPERS); 271 context.put("esc", escapeTool); 272 context.put("rdf", model.getGraph()); 273 274 context.put("model", model); 275 context.put("subjects", model.listSubjects()); 276 context.put("nodeany", ANY); 277 context.put("topic", subject); 278 context.put("originalResource", VIEW_HELPERS.getOriginalResource(subject)); 279 context.put("uriInfo", uriInfo); 280 return context; 281 } 282 283 private Template getTemplate(final Model rdf, final Node subject, 284 final List<Annotation> annotations) { 285 286 final String tplName = annotations.stream().filter(x -> x instanceof HtmlTemplate) 287 .map(x -> ((HtmlTemplate) x).value()).filter(templatesMap::containsKey).findFirst() 288 .orElseGet(() -> { 289 final List<String> types = multiValueURI(rdf.getResource(subject.getURI()), type); 290 if (types.contains(REPOSITORY_ROOT.toString())) { 291 return REPOSITORY_ROOT.toString(); 292 } 293 return types.stream().filter(templatesMap::containsKey).findFirst().orElse("default"); 294 }); 295 LOGGER.debug("Using template: {}", tplName); 296 return templatesMap.get(tplName); 297 } 298 299 @Override 300 public boolean isWriteable(final Class<?> type, final Type genericType, 301 final Annotation[] annotations, final MediaType mediaType) { 302 LOGGER.debug( 303 "Checking to see if type: {} is serializable to mimeType: {}", 304 type.getName(), mediaType); 305 return isTextHtml(mediaType) && RdfNamespacedStream.class.isAssignableFrom(type); 306 } 307 308 @Override 309 public long getSize(final RdfNamespacedStream t, final Class<?> type, 310 final Type genericType, final Annotation[] annotations, 311 final MediaType mediaType) { 312 // we don't know in advance how large the result might be 313 return -1; 314 } 315 316 private static String getTemplateLocation(final String nodeTypeName) { 317 return templatesLocation + "/" + 318 nodeTypeName.replace(':', '-') + templateFilenameExtension; 319 } 320 321 private static boolean isTextHtml(final MediaType mediaType) { 322 return mediaType.getType().equals(TEXT_HTML_TYPE.getType()) && 323 mediaType.getSubtype().equals(TEXT_HTML_TYPE.getSubtype()); 324 } 325 326}