/*
* Licensed to DuraSpace under one or more contributor license agreements.
* See the NOTICE file distributed with this work for additional information
* regarding copyright ownership.
*
* DuraSpace licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.fcrepo.http.api.responses;
import static java.lang.System.getProperty;
import static java.util.stream.Stream.of;
import static javax.ws.rs.core.MediaType.TEXT_HTML_TYPE;
import static com.google.common.collect.ImmutableMap.builder;
import static org.apache.jena.graph.Node.ANY;
import static org.apache.jena.sparql.util.graph.GraphUtils.multiValueURI;
import static org.apache.jena.vocabulary.RDF.type;
import static org.fcrepo.http.commons.domain.RDFMediaType.TEXT_HTML_WITH_CHARSET;
import static org.fcrepo.kernel.api.RdfLexicon.LDP_NAMESPACE;
import static org.fcrepo.kernel.api.RdfLexicon.REPOSITORY_NAMESPACE;
import static org.fcrepo.kernel.api.RdfCollectors.toModel;
import static org.slf4j.LoggerFactory.getLogger;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.net.URL;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import javax.annotation.PostConstruct;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.UriInfo;
import javax.ws.rs.ext.MessageBodyWriter;
import javax.ws.rs.ext.Provider;
import com.google.common.collect.ImmutableMap;
import org.apache.jena.graph.Node;
import org.apache.jena.rdf.model.Model;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.context.Context;
import org.apache.velocity.tools.generic.EscapeTool;
import org.apache.velocity.tools.generic.FieldTool;
import org.fcrepo.http.commons.responses.HtmlTemplate;
import org.fcrepo.http.commons.responses.RdfNamespacedStream;
import org.fcrepo.http.commons.responses.ViewHelpers;
import org.fcrepo.kernel.api.RdfLexicon;
import org.slf4j.Logger;
/**
* Simple HTML provider for RdfNamespacedStreams
*
* @author ajs6f
* @since Nov 19, 2013
*/
@Provider
@Produces({TEXT_HTML_WITH_CHARSET})
public class StreamingBaseHtmlProvider implements MessageBodyWriter<RdfNamespacedStream> {
@javax.ws.rs.core.Context
UriInfo uriInfo;
private static EscapeTool escapeTool = new EscapeTool();
protected VelocityEngine velocity = new VelocityEngine();
/**
* Location in the classpath where Velocity templates are to be found.
*/
public static final String templatesLocation = "/views";
/**
* Location in the classpath where the common css file is to be found.
*/
public static final String commonCssLocation = "/views/common.css";
/**
* Location in the classpath where the common javascript file is to be found.
*/
public static final String commonJsLocation = "/views/common.js";
/**
* A map from String names for primary node types to the Velocity templates
* that should be used for those node types.
*/
protected Map<String, Template> templatesMap;
public static final String templateFilenameExtension = ".vsl";
public static final String velocityPropertiesLocation =
"/velocity.properties";
private static final ViewHelpers VIEW_HELPERS = ViewHelpers.getInstance();
private static final Logger LOGGER =
getLogger(StreamingBaseHtmlProvider.class);
@PostConstruct
void init() throws IOException {
LOGGER.trace("Velocity engine initializing...");
final Properties properties = new Properties();
final String fcrepoHome = getProperty("fcrepo.home");
final String velocityLog = getProperty("fcrepo.velocity.runtime.log");
if (velocityLog != null) {
LOGGER.debug("Setting Velocity runtime log: {}", velocityLog);
properties.setProperty("runtime.log", velocityLog);
} else if (fcrepoHome != null) {
LOGGER.debug("Using fcrepo.home directory for the velocity log");
properties.setProperty("runtime.log", fcrepoHome + getProperty("file.separator") + "velocity.log");
}
final URL propertiesUrl =
getClass().getResource(velocityPropertiesLocation);
LOGGER.debug("Using Velocity configuration from {}", propertiesUrl);
try (final InputStream propertiesStream = propertiesUrl.openStream()) {
properties.load(propertiesStream);
}
velocity.init(properties);
LOGGER.trace("Velocity engine initialized.");
LOGGER.trace("Assembling a map of node primary types -> templates...");
final ImmutableMap.Builder<String, Template> templatesMapBuilder = builder();
of("fcr:versions", "fcr:fixity", "default")
.forEach(key -> templatesMapBuilder.put(key, velocity.getTemplate(getTemplateLocation(key))));
templatesMap = templatesMapBuilder
.put(REPOSITORY_NAMESPACE + "RepositoryRoot", velocity.getTemplate(getTemplateLocation("root")))
.put(REPOSITORY_NAMESPACE + "Binary", velocity.getTemplate(getTemplateLocation("binary")))
.put(REPOSITORY_NAMESPACE + "Version", velocity.getTemplate(getTemplateLocation("resource")))
.put(REPOSITORY_NAMESPACE + "Pairtree", velocity.getTemplate(getTemplateLocation("resource")))
.put(REPOSITORY_NAMESPACE + "Container", velocity.getTemplate(getTemplateLocation("resource")))
.put(LDP_NAMESPACE + "NonRdfSource", velocity.getTemplate(getTemplateLocation("binary")))
.put(LDP_NAMESPACE + "RdfSource", velocity.getTemplate(getTemplateLocation("resource"))).build();
LOGGER.trace("Assembled template map.");
LOGGER.trace("HtmlProvider initialization complete.");
}
@Override
public void writeTo(final RdfNamespacedStream nsStream, final Class<?> type,
final Type genericType, final Annotation[] annotations,
final MediaType mediaType,
final MultivaluedMap<String, Object> httpHeaders,
final OutputStream entityStream) throws IOException {
final Node subject = ViewHelpers.getContentNode(nsStream.stream.topic());
final Model model = nsStream.stream.collect(toModel());
model.setNsPrefixes(nsStream.namespaces);
final Template nodeTypeTemplate = getTemplate(model, subject, Arrays.asList(annotations));
final Context context = getContext(model, subject);
// the contract of MessageBodyWriter<T> is _not_ to close the stream
// after writing to it
final Writer outWriter = new OutputStreamWriter(entityStream);
nodeTypeTemplate.merge(context, outWriter);
outWriter.flush();
}
protected Context getContext(final Model model, final Node subject) {
final FieldTool fieldTool = new FieldTool();
final Context context = new VelocityContext();
context.put("rdfLexicon", fieldTool.in(RdfLexicon.class));
context.put("helpers", VIEW_HELPERS);
context.put("esc", escapeTool);
context.put("rdf", model.getGraph());
context.put("model", model);
context.put("subjects", model.listSubjects());
context.put("nodeany", ANY);
context.put("topic", subject);
context.put("uriInfo", uriInfo);
return context;
}
private Template getTemplate(final Model rdf, final Node subject,
final List<Annotation> annotations) {
final String tplName = annotations.stream().filter(x -> x instanceof HtmlTemplate)
.map(x -> ((HtmlTemplate) x).value()).filter(templatesMap::containsKey).findFirst()
.orElseGet(() -> {
final List<String> types = multiValueURI(rdf.getResource(subject.getURI()), type);
if (types.contains(REPOSITORY_NAMESPACE + "RepositoryRoot")) {
return REPOSITORY_NAMESPACE + "RepositoryRoot";
}
return types.stream().filter(templatesMap::containsKey).findFirst().orElse("default");
});
LOGGER.debug("Using template: {}", tplName);
return templatesMap.get(tplName);
}
@Override
public boolean isWriteable(final Class<?> type, final Type genericType,
final Annotation[] annotations, final MediaType mediaType) {
LOGGER.debug(
"Checking to see if type: {} is serializable to mimeType: {}",
type.getName(), mediaType);
return isTextHtml(mediaType) && RdfNamespacedStream.class.isAssignableFrom(type);
}
@Override
public long getSize(final RdfNamespacedStream t, final Class<?> type,
final Type genericType, final Annotation[] annotations,
final MediaType mediaType) {
// we don't know in advance how large the result might be
return -1;
}
private static String getTemplateLocation(final String nodeTypeName) {
return templatesLocation + "/" +
nodeTypeName.replace(':', '-') + templateFilenameExtension;
}
private static boolean isTextHtml(final MediaType mediaType) {
return mediaType.getType().equals(TEXT_HTML_TYPE.getType()) &&
mediaType.getSubtype().equals(TEXT_HTML_TYPE.getSubtype());
}
}