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