001/**
002 * Copyright 2015 DuraSpace, Inc.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.fcrepo.http.api.responses;
017
018import static javax.ws.rs.core.MediaType.APPLICATION_XHTML_XML;
019import static javax.ws.rs.core.MediaType.APPLICATION_XHTML_XML_TYPE;
020import static javax.ws.rs.core.MediaType.TEXT_HTML;
021import static javax.ws.rs.core.MediaType.TEXT_HTML_TYPE;
022import static com.google.common.collect.ImmutableMap.builder;
023import static com.hp.hpl.jena.graph.Node.ANY;
024import static com.hp.hpl.jena.rdf.model.ResourceFactory.createProperty;
025import static com.hp.hpl.jena.rdf.model.ResourceFactory.createResource;
026import static org.apache.commons.lang3.StringUtils.isBlank;
027import static org.fcrepo.kernel.api.RdfLexicon.JCR_NAMESPACE;
028import static org.fcrepo.kernel.modeshape.rdf.JcrRdfTools.getRDFNamespaceForJcrNamespace;
029import static org.slf4j.LoggerFactory.getLogger;
030
031import java.io.IOException;
032import java.io.InputStream;
033import java.io.OutputStream;
034import java.io.OutputStreamWriter;
035import java.io.Writer;
036import java.lang.annotation.Annotation;
037import java.lang.reflect.Type;
038import java.net.URL;
039import java.util.ArrayList;
040import java.util.Arrays;
041import java.util.List;
042import java.util.Map;
043import java.util.Optional;
044import java.util.Properties;
045
046import javax.annotation.PostConstruct;
047import javax.jcr.RepositoryException;
048import javax.jcr.Session;
049import javax.jcr.nodetype.NodeType;
050import javax.jcr.nodetype.NodeTypeIterator;
051import javax.ws.rs.Produces;
052import javax.ws.rs.WebApplicationException;
053import javax.ws.rs.core.MediaType;
054import javax.ws.rs.core.MultivaluedMap;
055import javax.ws.rs.core.UriInfo;
056import javax.ws.rs.ext.MessageBodyWriter;
057import javax.ws.rs.ext.Provider;
058
059import com.google.common.collect.ImmutableList;
060import com.google.common.collect.ImmutableMap;
061import com.hp.hpl.jena.graph.Node;
062import com.hp.hpl.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.http.commons.responses.HtmlTemplate;
070import org.fcrepo.http.commons.responses.ViewHelpers;
071import org.fcrepo.http.commons.session.SessionFactory;
072import org.fcrepo.kernel.api.RdfLexicon;
073import org.fcrepo.kernel.api.exception.RepositoryRuntimeException;
074import org.fcrepo.kernel.api.utils.iterators.RdfStream;
075import org.fcrepo.kernel.modeshape.rdf.impl.NamespaceRdfContext;
076import org.slf4j.Logger;
077import org.springframework.beans.factory.annotation.Autowired;
078
079/**
080 * Simple HTML provider for RdfStreams
081 *
082 * @author ajs6f
083 * @since Nov 19, 2013
084 */
085@Provider
086@Produces({TEXT_HTML, APPLICATION_XHTML_XML})
087public class StreamingBaseHtmlProvider implements MessageBodyWriter<RdfStream> {
088
089
090    @Autowired
091    SessionFactory sessionFactory;
092
093    @javax.ws.rs.core.Context
094    UriInfo uriInfo;
095
096    private static EscapeTool escapeTool = new EscapeTool();
097
098    protected VelocityEngine velocity = new VelocityEngine();
099
100    /**
101     * Location in the classpath where Velocity templates are to be found.
102     */
103    public static final String templatesLocation = "/views";
104
105    /**
106     * Location in the classpath where the common css file is to be found.
107     */
108    public static final String commonCssLocation = "/views/common.css";
109
110    /**
111     * Location in the classpath where the common javascript file is to be found.
112     */
113    public static final String commonJsLocation = "/views/common.js";
114
115    /**
116     * A map from String names for primary node types to the Velocity templates
117     * that should be used for those node types.
118     */
119    protected Map<String, Template> templatesMap;
120
121    public static final String templateFilenameExtension = ".vsl";
122
123    public static final String velocityPropertiesLocation =
124            "/velocity.properties";
125
126    private static final ViewHelpers VIEW_HELPERS = ViewHelpers.getInstance();
127
128    private static final Logger LOGGER =
129        getLogger(StreamingBaseHtmlProvider.class);
130
131    @PostConstruct
132    void init() throws IOException {
133
134        LOGGER.trace("Velocity engine initializing...");
135        final Properties properties = new Properties();
136        final URL propertiesUrl =
137                getClass().getResource(velocityPropertiesLocation);
138        LOGGER.debug("Using Velocity configuration from {}", propertiesUrl);
139        try (final InputStream propertiesStream = propertiesUrl.openStream()) {
140            properties.load(propertiesStream);
141        }
142        velocity.init(properties);
143        LOGGER.trace("Velocity engine initialized.");
144
145        LOGGER.trace("Assembling a map of node primary types -> templates...");
146        final ImmutableMap.Builder<String, Template> templatesMapBuilder = builder();
147        final Session session = sessionFactory.getInternalSession();
148        try {
149            // we search all of the possible node primary types and mixins
150            for (final NodeTypeIterator primaryNodeTypes =
151                         session.getWorkspace().getNodeTypeManager()
152                                 .getPrimaryNodeTypes(); primaryNodeTypes.hasNext();) {
153                final NodeType primaryNodeType =
154                    primaryNodeTypes.nextNodeType();
155                final String primaryNodeTypeName =
156                    primaryNodeType.getName();
157
158                // Create a list of the primary type and all its parents
159                final List<NodeType> nodeTypesList = new ArrayList<>();
160                nodeTypesList.add(primaryNodeType);
161                nodeTypesList.addAll(Arrays.asList(primaryNodeType.getSupertypes()));
162
163                // Find a template that matches the primary type or one of its parents
164                nodeTypesList.stream()
165                             .map(NodeType::getName)
166                             .filter(x -> !isBlank(x) && velocity.resourceExists(getTemplateLocation(x)))
167                             .findFirst()
168                             .ifPresent(x -> addTemplate(primaryNodeTypeName, x, templatesMapBuilder));
169            }
170
171            final List<String> otherTemplates =
172                    ImmutableList.of("jcr:nodetypes", "node", "fcr:versions", "fcr:fixity");
173
174            for (final String key : otherTemplates) {
175                final Template template =
176                    velocity.getTemplate(getTemplateLocation(key));
177                templatesMapBuilder.put(key, template);
178            }
179
180            templatesMap = templatesMapBuilder.build();
181
182        } catch (final RepositoryException e) {
183            throw new RepositoryRuntimeException(e);
184        }
185        LOGGER.trace("Assembled template map.");
186        LOGGER.trace("HtmlProvider initialization complete.");
187    }
188
189    @Override
190    public void writeTo(final RdfStream rdfStream, final Class<?> type,
191                        final Type genericType, final Annotation[] annotations,
192                        final MediaType mediaType,
193                        final MultivaluedMap<String, Object> httpHeaders,
194                        final OutputStream entityStream) throws IOException {
195
196        try {
197            final RdfStream nsRdfStream = new NamespaceRdfContext(rdfStream.session());
198
199            rdfStream.namespaces(nsRdfStream.namespaces());
200
201            final Node subject = VIEW_HELPERS.getContentNode(rdfStream.topic());
202
203            final Model model = rdfStream.asModel();
204
205            final Template nodeTypeTemplate = getTemplate(model, subject, Arrays.asList(annotations));
206
207            final Context context = getContext(model, subject);
208
209            // the contract of MessageBodyWriter<T> is _not_ to close the stream
210            // after writing to it
211            final Writer outWriter = new OutputStreamWriter(entityStream);
212            nodeTypeTemplate.merge(context, outWriter);
213            outWriter.flush();
214
215        } catch (final RepositoryException e) {
216            throw new WebApplicationException(e);
217        }
218
219    }
220
221    protected Context getContext(final Model model, final Node subject) {
222        final FieldTool fieldTool = new FieldTool();
223
224        final Context context = new VelocityContext();
225        context.put("rdfLexicon", fieldTool.in(RdfLexicon.class));
226        context.put("helpers", VIEW_HELPERS);
227        context.put("esc", escapeTool);
228        context.put("rdf", model.getGraph());
229
230        context.put("model", model);
231        context.put("subjects", model.listSubjects());
232        context.put("nodeany", ANY);
233        context.put("topic", subject);
234        context.put("uriInfo", uriInfo);
235        return context;
236    }
237
238    private Template getTemplate(final Model rdf, final Node subject,
239                                 final List<Annotation> annotations) {
240
241        Optional<Template> template = annotations.stream()
242                                  .filter(x -> x instanceof HtmlTemplate)
243                                  .map(x -> ((HtmlTemplate) x).value())
244                                  .filter(templatesMap::containsKey)
245                                  .map(templatesMap::get)
246                                  .findFirst();
247
248        if (!template.isPresent()) {
249            LOGGER.trace("Attempting to discover mixin types of node for resource in question: {}", subject);
250            template = rdf.listObjectsOfProperty(createResource(subject.getURI()),
251                                                 createProperty(getRDFNamespaceForJcrNamespace(JCR_NAMESPACE) +
252                                                     "mixinTypes"))
253                          .toList().stream()
254                          .map(x -> x.asLiteral().getLexicalForm())
255                          .filter(templatesMap::containsKey)
256                          .map(templatesMap::get)
257                          .findFirst();
258        }
259
260        if (template.isPresent()) {
261            LOGGER.debug("Choosing template: {}", template.get().getName());
262            return template.get();
263        } else {
264            LOGGER.trace("Attempting to discover primary type of node for resource in question: {}", subject);
265            return rdf.listObjectsOfProperty(createResource(subject.getURI()),
266                                             createProperty(getRDFNamespaceForJcrNamespace(JCR_NAMESPACE) +
267                                                     "primaryType"))
268                          .toList().stream()
269                          .map(x -> x.asLiteral().getLexicalForm())
270                          .filter(templatesMap::containsKey)
271                          .map(templatesMap::get)
272                          .findFirst()
273                          .orElse(templatesMap.get("node"));
274        }
275    }
276
277    @Override
278    public boolean isWriteable(final Class<?> type, final Type genericType,
279                               final Annotation[] annotations, final MediaType mediaType) {
280        LOGGER.debug(
281                "Checking to see if type: {} is serializable to mimeType: {}",
282                type.getName(), mediaType);
283        return (mediaType.equals(TEXT_HTML_TYPE) || mediaType
284                .equals(APPLICATION_XHTML_XML_TYPE))
285                && RdfStream.class.isAssignableFrom(type);
286    }
287
288    @Override
289    public long getSize(final RdfStream t, final Class<?> type,
290                        final Type genericType, final Annotation[] annotations,
291                        final MediaType mediaType) {
292        // we don't know in advance how large the result might be
293        return -1;
294    }
295
296    private void addTemplate(final String primaryNodeTypeName, final String templateNodeTypeName,
297                             final ImmutableMap.Builder<String, Template> templatesMapBuilder) {
298        final String templateLocation = getTemplateLocation(templateNodeTypeName);
299        final Template template =
300            velocity.getTemplate(templateLocation);
301        template.setName(templateLocation);
302        LOGGER.debug("Found template: {}", templateLocation);
303        templatesMapBuilder.put(primaryNodeTypeName, template);
304        LOGGER.debug("which we will use for nodes with primary type: {}",
305                     primaryNodeTypeName);
306    }
307
308    private static String getTemplateLocation(final String nodeTypeName) {
309        return templatesLocation + "/" +
310            nodeTypeName.replace(':', '-') + templateFilenameExtension;
311    }
312}