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.TEXT_HTML_TYPE;
022import static com.google.common.collect.ImmutableMap.builder;
023import static org.apache.jena.graph.Node.ANY;
024import static org.apache.jena.sparql.util.graph.GraphUtils.multiValueURI;
025import static org.apache.jena.vocabulary.RDF.type;
026import static org.fcrepo.http.commons.domain.RDFMediaType.TEXT_HTML_WITH_CHARSET;
027import static org.fcrepo.http.commons.session.TransactionConstants.ATOMIC_ID_HEADER;
028import static org.fcrepo.kernel.api.RdfLexicon.ARCHIVAL_GROUP;
029import static org.fcrepo.kernel.api.RdfLexicon.NON_RDF_SOURCE;
030import static org.fcrepo.kernel.api.RdfLexicon.RDF_SOURCE;
031import static org.fcrepo.kernel.api.RdfCollectors.toModel;
032import static org.fcrepo.kernel.api.RdfLexicon.REPOSITORY_ROOT;
033import static org.slf4j.LoggerFactory.getLogger;
034
035import java.io.IOException;
036import java.io.InputStream;
037import java.io.OutputStream;
038import java.io.OutputStreamWriter;
039import java.io.Writer;
040import java.lang.annotation.Annotation;
041import java.lang.reflect.Type;
042import java.net.URI;
043import java.net.URL;
044import java.nio.charset.StandardCharsets;
045import java.util.Arrays;
046import java.util.List;
047import java.util.Map;
048import java.util.Properties;
049import javax.annotation.PostConstruct;
050import javax.inject.Inject;
051import javax.servlet.http.HttpServletRequest;
052import javax.ws.rs.Produces;
053import javax.ws.rs.core.MediaType;
054import javax.ws.rs.core.MultivaluedMap;
055import javax.ws.rs.core.UriBuilder;
056import javax.ws.rs.core.UriInfo;
057import javax.ws.rs.ext.MessageBodyWriter;
058import javax.ws.rs.ext.Provider;
059
060import com.google.common.collect.ImmutableMap;
061import org.apache.jena.graph.Node;
062import org.apache.jena.rdf.model.Model;
063import org.apache.velocity.Template;
064import org.apache.velocity.VelocityContext;
065import org.apache.velocity.app.VelocityEngine;
066import org.apache.velocity.context.Context;
067import org.apache.velocity.tools.generic.EscapeTool;
068import org.apache.velocity.tools.generic.FieldTool;
069import org.fcrepo.config.FedoraPropsConfig;
070import org.fcrepo.config.OcflPropsConfig;
071import org.fcrepo.http.api.FedoraLdp;
072import org.fcrepo.http.commons.api.rdf.HttpIdentifierConverter;
073import org.fcrepo.http.commons.responses.HtmlTemplate;
074import org.fcrepo.http.commons.responses.RdfNamespacedStream;
075import org.fcrepo.http.commons.responses.ViewHelpers;
076import org.fcrepo.kernel.api.ReadOnlyTransaction;
077import org.fcrepo.kernel.api.Transaction;
078import org.fcrepo.kernel.api.RdfLexicon;
079import org.fcrepo.kernel.api.TransactionManager;
080import org.fcrepo.kernel.api.exception.PathNotFoundException;
081import org.fcrepo.kernel.api.exception.RepositoryRuntimeException;
082import org.fcrepo.kernel.api.identifiers.FedoraId;
083import org.fcrepo.kernel.api.models.Binary;
084import org.fcrepo.kernel.api.models.FedoraResource;
085import org.fcrepo.kernel.api.models.ResourceFactory;
086import org.slf4j.Logger;
087
088/**
089 * Simple HTML provider for RdfNamespacedStreams
090 *
091 * @author ajs6f
092 * @author whikloj
093 * @since Nov 19, 2013
094 */
095@Provider
096@Produces({TEXT_HTML_WITH_CHARSET})
097public class StreamingBaseHtmlProvider implements MessageBodyWriter<RdfNamespacedStream> {
098
099
100    @Inject
101    UriInfo uriInfo;
102
103    @Inject
104    TransactionManager transactionManager;
105
106    @Inject
107    HttpServletRequest request;
108
109    @Inject
110    private ResourceFactory resourceFactory;
111
112    @Inject
113    private OcflPropsConfig ocflPropsConfig;
114
115    @Inject
116    private FedoraPropsConfig fedoraPropsConfig;
117
118    private boolean autoVersioningEnabled;
119
120    private HttpIdentifierConverter identifierConverter;
121
122    private Transaction readOnlyTx;
123
124    private Transaction transaction() {
125        if (request.getHeader(ATOMIC_ID_HEADER) != null) {
126            return transactionManager.get(request.getHeader(ATOMIC_ID_HEADER));
127        }
128        return readOnlyTx;
129    }
130
131    private HttpIdentifierConverter identifierConverter() {
132        if (identifierConverter == null) {
133            final UriBuilder uriBuilder =
134                    uriInfo.getBaseUriBuilder().clone().path(FedoraLdp.class);
135            identifierConverter = new HttpIdentifierConverter(uriBuilder);
136        }
137        return identifierConverter;
138    }
139
140    private static final EscapeTool escapeTool = new EscapeTool();
141
142    private final VelocityEngine velocity = new VelocityEngine();
143
144    /**
145     * Location in the classpath where Velocity templates are to be found.
146     */
147    private static final String templatesLocation = "/views";
148
149    /**
150     * A map from String names for primary node types to the Velocity templates
151     * that should be used for those node types.
152     */
153    private Map<String, Template> templatesMap;
154
155    private static final String templateFilenameExtension = ".vsl";
156
157    private static final String velocityPropertiesLocation =
158            "/velocity.properties";
159
160    private static final ViewHelpers VIEW_HELPERS = ViewHelpers.getInstance();
161
162    private static final Logger LOGGER =
163        getLogger(StreamingBaseHtmlProvider.class);
164
165    @PostConstruct
166    void init() throws IOException {
167        LOGGER.trace("Velocity engine initializing...");
168        final Properties properties = new Properties();
169        final var velocityLog = fedoraPropsConfig.getVelocityLog().toString();
170        autoVersioningEnabled = ocflPropsConfig.isAutoVersioningEnabled();
171        LOGGER.debug("Setting Velocity runtime log: {}", velocityLog);
172        properties.setProperty("runtime.log", velocityLog);
173
174        final URL propertiesUrl =
175                getClass().getResource(velocityPropertiesLocation);
176        LOGGER.debug("Using Velocity configuration from {}", propertiesUrl);
177        try (final InputStream propertiesStream = propertiesUrl.openStream()) {
178            properties.load(propertiesStream);
179        }
180        velocity.init(properties);
181        LOGGER.trace("Velocity engine initialized.");
182
183        LOGGER.trace("Assembling a map of node primary types -> templates...");
184        final ImmutableMap.Builder<String, Template> templatesMapBuilder = builder();
185
186        of("fcr:versions", "fcr:fixity", "default")
187            .forEach(key -> templatesMapBuilder.put(key, velocity.getTemplate(getTemplateLocation(key))));
188
189        templatesMap = templatesMapBuilder
190            .put(REPOSITORY_ROOT.toString(), velocity.getTemplate(getTemplateLocation("root")))
191            .put(NON_RDF_SOURCE.toString(), velocity.getTemplate(getTemplateLocation("binary")))
192            .put(RDF_SOURCE.toString(), velocity.getTemplate(getTemplateLocation("resource"))).build();
193
194        LOGGER.trace("Assembled template map.");
195
196        readOnlyTx = ReadOnlyTransaction.INSTANCE;
197
198        LOGGER.trace("HtmlProvider initialization complete.");
199    }
200
201    @Override
202    public void writeTo(final RdfNamespacedStream nsStream, final Class<?> type,
203                        final Type genericType, final Annotation[] annotations,
204                        final MediaType mediaType,
205                        final MultivaluedMap<String, Object> httpHeaders,
206                        final OutputStream entityStream) throws IOException {
207
208        final Node subject = ViewHelpers.getContentNode(nsStream.stream.topic());
209
210        final Model model = nsStream.stream.collect(toModel());
211        model.setNsPrefixes(nsStream.namespaces);
212
213        final Template nodeTypeTemplate = getTemplate(model, subject, Arrays.asList(annotations));
214
215        final Context context = getContext(model, subject);
216
217        final FedoraId fedoraID = FedoraId.create(identifierConverter().toInternalId(subject.toString()));
218        try {
219            final FedoraResource resource = getResourceFromSubject(fedoraID);
220            context.put("isOriginalResource", (resource != null && resource.isOriginalResource()));
221            context.put("isArchivalGroup", (resource != null && resource.getSystemTypes(false)
222                    .contains(URI.create(ARCHIVAL_GROUP.getURI()))));
223            context.put("isVersion", (resource != null && resource.isMemento()));
224            context.put("isLDPNR", (resource != null && (resource instanceof Binary || !resource
225                    .getDescribedResource().equals(resource))));
226        } catch (final PathNotFoundException e) {
227            final var baseId = FedoraId.create(fedoraID.getBaseId());
228            if (fedoraID.isRepositoryRoot() || baseId.isRepositoryRoot()) {
229                // We have requested the root resource or default ACL.
230                context.put("isOriginalResource", true);
231                context.put("isArchivalGroup", false);
232                context.put("isVersion", false);
233                context.put("isLDPNR", false);
234            } else {
235                throw new RepositoryRuntimeException(e.getMessage(), e);
236            }
237        }
238        context.put("autoVersioningEnabled", autoVersioningEnabled);
239
240        // the contract of MessageBodyWriter<T> is _not_ to close the stream
241        // after writing to it
242        final Writer outWriter = new OutputStreamWriter(entityStream, StandardCharsets.UTF_8);
243        nodeTypeTemplate.merge(context, outWriter);
244        outWriter.flush();
245    }
246
247    /**
248     * Get a FedoraResource for the subject of the graph, if it exists.
249     *
250     * @param resourceId the FedoraId of the subject
251     * @return FedoraResource if exists or null
252     * @throws PathNotFoundException if the OCFL mapping is not found.
253     */
254    private FedoraResource getResourceFromSubject(final FedoraId resourceId) throws PathNotFoundException {
255        return resourceFactory.getResource(transaction(), resourceId);
256    }
257
258    private Context getContext(final Model model, final Node subject) {
259        final FieldTool fieldTool = new FieldTool();
260
261        final Context context = new VelocityContext();
262        final String[] baseUrl = uriInfo.getBaseUri().getPath().split("/");
263        if (baseUrl.length > 0) {
264            final String staticBaseUrl = String.join("/", Arrays.copyOf(baseUrl, baseUrl.length - 1));
265            context.put("staticBaseUrl", staticBaseUrl);
266        } else {
267            context.put("staticBaseUrl", "/");
268        }
269        context.put("rdfLexicon", fieldTool.in(RdfLexicon.class));
270        context.put("helpers", VIEW_HELPERS);
271        context.put("esc", escapeTool);
272        context.put("rdf", model.getGraph());
273
274        context.put("model", model);
275        context.put("subjects", model.listSubjects());
276        context.put("nodeany", ANY);
277        context.put("topic", subject);
278        context.put("originalResource", VIEW_HELPERS.getOriginalResource(subject));
279        context.put("uriInfo", uriInfo);
280        return context;
281    }
282
283    private Template getTemplate(final Model rdf, final Node subject,
284                                 final List<Annotation> annotations) {
285
286        final String tplName = annotations.stream().filter(x -> x instanceof HtmlTemplate)
287            .map(x -> ((HtmlTemplate) x).value()).filter(templatesMap::containsKey).findFirst()
288            .orElseGet(() -> {
289                final List<String> types = multiValueURI(rdf.getResource(subject.getURI()), type);
290                if (types.contains(REPOSITORY_ROOT.toString())) {
291                    return REPOSITORY_ROOT.toString();
292                }
293                return types.stream().filter(templatesMap::containsKey).findFirst().orElse("default");
294            });
295        LOGGER.debug("Using template: {}", tplName);
296        return templatesMap.get(tplName);
297    }
298
299    @Override
300    public boolean isWriteable(final Class<?> type, final Type genericType,
301                               final Annotation[] annotations, final MediaType mediaType) {
302        LOGGER.debug(
303                "Checking to see if type: {} is serializable to mimeType: {}",
304                type.getName(), mediaType);
305        return isTextHtml(mediaType) && RdfNamespacedStream.class.isAssignableFrom(type);
306    }
307
308    @Override
309    public long getSize(final RdfNamespacedStream t, final Class<?> type,
310                        final Type genericType, final Annotation[] annotations,
311                        final MediaType mediaType) {
312        // we don't know in advance how large the result might be
313        return -1;
314    }
315
316    private static String getTemplateLocation(final String nodeTypeName) {
317        return templatesLocation + "/" +
318            nodeTypeName.replace(':', '-') + templateFilenameExtension;
319    }
320
321    private static boolean isTextHtml(final MediaType mediaType) {
322        return mediaType.getType().equals(TEXT_HTML_TYPE.getType()) &&
323            mediaType.getSubtype().equals(TEXT_HTML_TYPE.getSubtype());
324    }
325
326}