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