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