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