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