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;
045import java.util.stream.Collectors;
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     * A map from String names for primary node types to the Velocity templates
094     * that should be used for those node types.
095     */
096    protected Map<String, Template> templatesMap;
097
098    public static final String templateFilenameExtension = ".vsl";
099
100    public static final String velocityPropertiesLocation =
101            "/velocity.properties";
102
103    private static final ViewHelpers VIEW_HELPERS = ViewHelpers.getInstance();
104
105    private static final Logger LOGGER =
106        getLogger(StreamingBaseHtmlProvider.class);
107
108    @PostConstruct
109    void init() throws IOException {
110        LOGGER.trace("Velocity engine initializing...");
111        final Properties properties = new Properties();
112        final String fcrepoHome = getProperty("fcrepo.home");
113        final String velocityLog = getProperty("fcrepo.velocity.runtime.log");
114        if (velocityLog != null) {
115            LOGGER.debug("Setting Velocity runtime log: {}", velocityLog);
116            properties.setProperty("runtime.log", velocityLog);
117        } else if (fcrepoHome != null) {
118            LOGGER.debug("Using fcrepo.home directory for the velocity log");
119            properties.setProperty("runtime.log", fcrepoHome + getProperty("file.separator") + "velocity.log");
120        }
121        final URL propertiesUrl =
122                getClass().getResource(velocityPropertiesLocation);
123        LOGGER.debug("Using Velocity configuration from {}", propertiesUrl);
124        try (final InputStream propertiesStream = propertiesUrl.openStream()) {
125            properties.load(propertiesStream);
126        }
127        velocity.init(properties);
128        LOGGER.trace("Velocity engine initialized.");
129
130        LOGGER.trace("Assembling a map of node primary types -> templates...");
131        final ImmutableMap.Builder<String, Template> templatesMapBuilder = builder();
132
133        of("fcr:versions", "fcr:fixity", "default")
134            .forEach(key -> templatesMapBuilder.put(key, velocity.getTemplate(getTemplateLocation(key))));
135
136        templatesMap = templatesMapBuilder
137            .put(REPOSITORY_NAMESPACE + "RepositoryRoot", velocity.getTemplate(getTemplateLocation("root")))
138            .put(REPOSITORY_NAMESPACE + "Binary", velocity.getTemplate(getTemplateLocation("binary")))
139            .put(REPOSITORY_NAMESPACE + "Version", velocity.getTemplate(getTemplateLocation("resource")))
140            .put(REPOSITORY_NAMESPACE + "Pairtree", velocity.getTemplate(getTemplateLocation("resource")))
141            .put(REPOSITORY_NAMESPACE + "Container", velocity.getTemplate(getTemplateLocation("resource")))
142            .put(LDP_NAMESPACE + "NonRdfSource", velocity.getTemplate(getTemplateLocation("binary")))
143            .put(LDP_NAMESPACE + "RdfSource", velocity.getTemplate(getTemplateLocation("resource"))).build();
144
145        LOGGER.trace("Assembled template map.");
146        LOGGER.trace("HtmlProvider initialization complete.");
147    }
148
149    @Override
150    public void writeTo(final RdfNamespacedStream nsStream, final Class<?> type,
151                        final Type genericType, final Annotation[] annotations,
152                        final MediaType mediaType,
153                        final MultivaluedMap<String, Object> httpHeaders,
154                        final OutputStream entityStream) throws IOException {
155
156        final Node subject = ViewHelpers.getContentNode(nsStream.stream.topic());
157        final Model model = nsStream.stream.collect(toModel());
158        model.setNsPrefixes(nsStream.namespaces);
159
160        final Template nodeTypeTemplate = getTemplate(model, subject, Arrays.asList(annotations));
161
162        final Context context = getContext(model, subject);
163
164        // the contract of MessageBodyWriter<T> is _not_ to close the stream
165        // after writing to it
166        final Writer outWriter = new OutputStreamWriter(entityStream);
167        nodeTypeTemplate.merge(context, outWriter);
168        outWriter.flush();
169    }
170
171    protected Context getContext(final Model model, final Node subject) {
172        final FieldTool fieldTool = new FieldTool();
173
174        final Context context = new VelocityContext();
175        final String[] baseUrl = uriInfo.getBaseUri().getPath().split("/");
176        if (baseUrl.length > 0) {
177            final String staticBaseUrl =
178                Arrays.asList(Arrays.copyOf(baseUrl, baseUrl.length - 1)).stream().collect(Collectors.joining("/"));
179            context.put("staticBaseUrl", staticBaseUrl);
180        } else {
181            context.put("staticBaseUrl", "/");
182        }
183        context.put("rdfLexicon", fieldTool.in(RdfLexicon.class));
184        context.put("helpers", VIEW_HELPERS);
185        context.put("esc", escapeTool);
186        context.put("rdf", model.getGraph());
187
188        context.put("model", model);
189        context.put("subjects", model.listSubjects());
190        context.put("nodeany", ANY);
191        context.put("topic", subject);
192        context.put("uriInfo", uriInfo);
193        return context;
194    }
195
196    private Template getTemplate(final Model rdf, final Node subject,
197                                 final List<Annotation> annotations) {
198
199        final String tplName = annotations.stream().filter(x -> x instanceof HtmlTemplate)
200            .map(x -> ((HtmlTemplate) x).value()).filter(templatesMap::containsKey).findFirst()
201            .orElseGet(() -> {
202                final List<String> types = multiValueURI(rdf.getResource(subject.getURI()), type);
203                if (types.contains(REPOSITORY_NAMESPACE + "RepositoryRoot")) {
204                    return REPOSITORY_NAMESPACE + "RepositoryRoot";
205                }
206                return types.stream().filter(templatesMap::containsKey).findFirst().orElse("default");
207            });
208        LOGGER.debug("Using template: {}", tplName);
209        return templatesMap.get(tplName);
210    }
211
212    @Override
213    public boolean isWriteable(final Class<?> type, final Type genericType,
214                               final Annotation[] annotations, final MediaType mediaType) {
215        LOGGER.debug(
216                "Checking to see if type: {} is serializable to mimeType: {}",
217                type.getName(), mediaType);
218        return isTextHtml(mediaType) && RdfNamespacedStream.class.isAssignableFrom(type);
219    }
220
221    @Override
222    public long getSize(final RdfNamespacedStream t, final Class<?> type,
223                        final Type genericType, final Annotation[] annotations,
224                        final MediaType mediaType) {
225        // we don't know in advance how large the result might be
226        return -1;
227    }
228
229    private static String getTemplateLocation(final String nodeTypeName) {
230        return templatesLocation + "/" +
231            nodeTypeName.replace(':', '-') + templateFilenameExtension;
232    }
233
234    private static boolean isTextHtml(final MediaType mediaType) {
235        return mediaType.getType().equals(TEXT_HTML_TYPE.getType()) &&
236            mediaType.getSubtype().equals(TEXT_HTML_TYPE.getSubtype());
237    }
238
239}