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.HashMap;
043import java.util.List;
044import java.util.Map;
045import java.util.Properties;
046import javax.annotation.PostConstruct;
047import javax.jcr.PathNotFoundException;
048import javax.servlet.http.HttpServletRequest;
049import javax.ws.rs.Produces;
050import javax.ws.rs.core.MediaType;
051import javax.ws.rs.core.MultivaluedMap;
052import javax.ws.rs.core.UriInfo;
053import javax.ws.rs.ext.MessageBodyWriter;
054import javax.ws.rs.ext.Provider;
055
056import com.google.common.collect.ImmutableMap;
057import org.apache.jena.graph.Node;
058import org.apache.jena.rdf.model.Model;
059import org.apache.jena.rdf.model.Resource;
060import org.apache.velocity.Template;
061import org.apache.velocity.VelocityContext;
062import org.apache.velocity.app.VelocityEngine;
063import org.apache.velocity.context.Context;
064import org.apache.velocity.tools.generic.EscapeTool;
065import org.apache.velocity.tools.generic.FieldTool;
066import org.fcrepo.http.api.FedoraLdp;
067import org.fcrepo.http.commons.api.rdf.HttpResourceConverter;
068import org.fcrepo.http.commons.responses.HtmlTemplate;
069import org.fcrepo.http.commons.responses.RdfNamespacedStream;
070import org.fcrepo.http.commons.responses.ViewHelpers;
071import org.fcrepo.http.commons.session.HttpSession;
072import org.fcrepo.http.commons.session.SessionFactory;
073import org.fcrepo.kernel.api.RdfLexicon;
074import org.fcrepo.kernel.api.identifiers.IdentifierConverter;
075import org.fcrepo.kernel.api.models.FedoraBinary;
076import org.fcrepo.kernel.api.models.FedoraResource;
077import org.glassfish.jersey.uri.UriTemplate;
078import org.slf4j.Logger;
079
080/**
081 * Simple HTML provider for RdfNamespacedStreams
082 *
083 * @author ajs6f
084 * @author whikloj
085 * @since Nov 19, 2013
086 */
087@Provider
088@Produces({TEXT_HTML_WITH_CHARSET})
089public class StreamingBaseHtmlProvider implements MessageBodyWriter<RdfNamespacedStream> {
090
091
092    @javax.ws.rs.core.Context
093    UriInfo uriInfo;
094
095    @javax.ws.rs.core.Context
096    SessionFactory sessionFactory;
097
098    @javax.ws.rs.core.Context
099    HttpServletRequest request;
100
101    private HttpSession session() {
102        return sessionFactory.getSession(request);
103    }
104
105    private IdentifierConverter<Resource, FedoraResource> translator() {
106        return new HttpResourceConverter(session(),
107                uriInfo.getBaseUriBuilder().clone().path(FedoraLdp.class));
108    }
109
110    private static final EscapeTool escapeTool = new EscapeTool();
111
112    private final VelocityEngine velocity = new VelocityEngine();
113
114    /**
115     * Location in the classpath where Velocity templates are to be found.
116     */
117    private static final String templatesLocation = "/views";
118
119    /**
120     * A map from String names for primary node types to the Velocity templates
121     * that should be used for those node types.
122     */
123    private Map<String, Template> templatesMap;
124
125    private static final String templateFilenameExtension = ".vsl";
126
127    private static final String velocityPropertiesLocation =
128            "/velocity.properties";
129
130    private static final ViewHelpers VIEW_HELPERS = ViewHelpers.getInstance();
131
132    private static final Logger LOGGER =
133        getLogger(StreamingBaseHtmlProvider.class);
134
135    @PostConstruct
136    void init() throws IOException {
137        LOGGER.trace("Velocity engine initializing...");
138        final Properties properties = new Properties();
139        final String fcrepoHome = getProperty("fcrepo.home");
140        final String velocityLog = getProperty("fcrepo.velocity.runtime.log");
141        if (velocityLog != null) {
142            LOGGER.debug("Setting Velocity runtime log: {}", velocityLog);
143            properties.setProperty("runtime.log", velocityLog);
144        } else if (fcrepoHome != null) {
145            LOGGER.debug("Using fcrepo.home directory for the velocity log");
146            properties.setProperty("runtime.log", fcrepoHome + getProperty("file.separator") + "velocity.log");
147        }
148        final URL propertiesUrl =
149                getClass().getResource(velocityPropertiesLocation);
150        LOGGER.debug("Using Velocity configuration from {}", propertiesUrl);
151        try (final InputStream propertiesStream = propertiesUrl.openStream()) {
152            properties.load(propertiesStream);
153        }
154        velocity.init(properties);
155        LOGGER.trace("Velocity engine initialized.");
156
157        LOGGER.trace("Assembling a map of node primary types -> templates...");
158        final ImmutableMap.Builder<String, Template> templatesMapBuilder = builder();
159
160        of("fcr:versions", "fcr:fixity", "default")
161            .forEach(key -> templatesMapBuilder.put(key, velocity.getTemplate(getTemplateLocation(key))));
162
163        templatesMap = templatesMapBuilder
164            .put(REPOSITORY_NAMESPACE + "RepositoryRoot", velocity.getTemplate(getTemplateLocation("root")))
165            .put(REPOSITORY_NAMESPACE + "Binary", velocity.getTemplate(getTemplateLocation("binary")))
166            .put(REPOSITORY_NAMESPACE + "Version", velocity.getTemplate(getTemplateLocation("resource")))
167            .put(REPOSITORY_NAMESPACE + "Pairtree", velocity.getTemplate(getTemplateLocation("resource")))
168            .put(REPOSITORY_NAMESPACE + "Container", velocity.getTemplate(getTemplateLocation("resource")))
169            .put(LDP_NAMESPACE + "NonRdfSource", velocity.getTemplate(getTemplateLocation("binary")))
170            .put(LDP_NAMESPACE + "RdfSource", velocity.getTemplate(getTemplateLocation("resource"))).build();
171
172        LOGGER.trace("Assembled template map.");
173        LOGGER.trace("HtmlProvider initialization complete.");
174    }
175
176    @Override
177    public void writeTo(final RdfNamespacedStream nsStream, final Class<?> type,
178                        final Type genericType, final Annotation[] annotations,
179                        final MediaType mediaType,
180                        final MultivaluedMap<String, Object> httpHeaders,
181                        final OutputStream entityStream) throws IOException {
182
183        final Node subject = ViewHelpers.getContentNode(nsStream.stream.topic());
184
185        final Model model = nsStream.stream.collect(toModel());
186        model.setNsPrefixes(nsStream.namespaces);
187
188        final Template nodeTypeTemplate = getTemplate(model, subject, Arrays.asList(annotations));
189
190        final Context context = getContext(model, subject);
191
192        final FedoraResource resource = getResourceFromSubject(subject.toString());
193        context.put("isOriginalResource", (resource != null && resource.isOriginalResource()));
194        context.put("isVersion", (resource != null && resource.isMemento()));
195        context.put("isLDPNR", (resource != null &&
196                (resource instanceof FedoraBinary || !resource.getDescribedResource().equals(resource))));
197
198        // the contract of MessageBodyWriter<T> is _not_ to close the stream
199        // after writing to it
200        final Writer outWriter = new OutputStreamWriter(entityStream);
201        nodeTypeTemplate.merge(context, outWriter);
202        outWriter.flush();
203    }
204
205    /**
206     * Get a FedoraResource for the subject of the graph, if it exists.
207     *
208     * @param subjectUri the uri of the subject
209     * @return FedoraResource if exists or null
210     */
211    private FedoraResource getResourceFromSubject(final String subjectUri) {
212        final UriTemplate uriTemplate =
213            new UriTemplate(uriInfo.getBaseUriBuilder().clone().path(FedoraLdp.class).toTemplate());
214        final Map<String, String> values = new HashMap<>();
215        uriTemplate.match(subjectUri, values);
216        if (values.containsKey("path")) {
217            try {
218                return translator().convert(translator().toDomain(values.get("path")));
219            } catch (final RuntimeException e) {
220                if (e.getCause() instanceof PathNotFoundException) {
221                    //there is at least one case (ie Time Map endpoints - aka /fcr:versions) where the underlying jcr
222                    // node will not exist while the end point is visible to the user.
223                    return null;
224                } else {
225                    throw e;
226                }
227            }
228        }
229        return null;
230    }
231
232    private Context getContext(final Model model, final Node subject) {
233        final FieldTool fieldTool = new FieldTool();
234
235        final Context context = new VelocityContext();
236        final String[] baseUrl = uriInfo.getBaseUri().getPath().split("/");
237        if (baseUrl.length > 0) {
238            final String staticBaseUrl = String.join("/", Arrays.copyOf(baseUrl, baseUrl.length - 1));
239            context.put("staticBaseUrl", staticBaseUrl);
240        } else {
241            context.put("staticBaseUrl", "/");
242        }
243        context.put("rdfLexicon", fieldTool.in(RdfLexicon.class));
244        context.put("helpers", VIEW_HELPERS);
245        context.put("esc", escapeTool);
246        context.put("rdf", model.getGraph());
247
248        context.put("model", model);
249        context.put("subjects", model.listSubjects());
250        context.put("nodeany", ANY);
251        context.put("topic", subject);
252        context.put("originalResource", VIEW_HELPERS.getOriginalResource(subject));
253        context.put("uriInfo", uriInfo);
254        return context;
255    }
256
257    private Template getTemplate(final Model rdf, final Node subject,
258                                 final List<Annotation> annotations) {
259
260        final String tplName = annotations.stream().filter(x -> x instanceof HtmlTemplate)
261            .map(x -> ((HtmlTemplate) x).value()).filter(templatesMap::containsKey).findFirst()
262            .orElseGet(() -> {
263                final List<String> types = multiValueURI(rdf.getResource(subject.getURI()), type);
264                if (types.contains(REPOSITORY_NAMESPACE + "RepositoryRoot")) {
265                    return REPOSITORY_NAMESPACE + "RepositoryRoot";
266                }
267                return types.stream().filter(templatesMap::containsKey).findFirst().orElse("default");
268            });
269        LOGGER.debug("Using template: {}", tplName);
270        return templatesMap.get(tplName);
271    }
272
273    @Override
274    public boolean isWriteable(final Class<?> type, final Type genericType,
275                               final Annotation[] annotations, final MediaType mediaType) {
276        LOGGER.debug(
277                "Checking to see if type: {} is serializable to mimeType: {}",
278                type.getName(), mediaType);
279        return isTextHtml(mediaType) && RdfNamespacedStream.class.isAssignableFrom(type);
280    }
281
282    @Override
283    public long getSize(final RdfNamespacedStream t, final Class<?> type,
284                        final Type genericType, final Annotation[] annotations,
285                        final MediaType mediaType) {
286        // we don't know in advance how large the result might be
287        return -1;
288    }
289
290    private static String getTemplateLocation(final String nodeTypeName) {
291        return templatesLocation + "/" +
292            nodeTypeName.replace(':', '-') + templateFilenameExtension;
293    }
294
295    private static boolean isTextHtml(final MediaType mediaType) {
296        return mediaType.getType().equals(TEXT_HTML_TYPE.getType()) &&
297            mediaType.getSubtype().equals(TEXT_HTML_TYPE.getSubtype());
298    }
299
300}