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 com.hp.hpl.jena.graph.Node.ANY;
019import static javax.ws.rs.core.MediaType.APPLICATION_XHTML_XML;
020import static javax.ws.rs.core.MediaType.APPLICATION_XHTML_XML_TYPE;
021import static javax.ws.rs.core.MediaType.TEXT_HTML;
022import static javax.ws.rs.core.MediaType.TEXT_HTML_TYPE;
023import static org.apache.commons.lang.StringUtils.isBlank;
024import static org.fcrepo.http.commons.responses.RdfSerializationUtils.getAllValuesForPredicate;
025import static org.fcrepo.http.commons.responses.RdfSerializationUtils.getFirstValueForPredicate;
026import static org.fcrepo.http.commons.responses.RdfSerializationUtils.mixinTypesPredicate;
027import static org.fcrepo.http.commons.responses.RdfSerializationUtils.primaryTypePredicate;
028import static com.google.common.collect.ImmutableList.copyOf;
029import static com.google.common.collect.ImmutableMap.builder;
030import static org.slf4j.LoggerFactory.getLogger;
031
032import java.io.IOException;
033import java.io.InputStream;
034import java.io.OutputStream;
035import java.io.OutputStreamWriter;
036import java.io.Writer;
037import java.lang.annotation.Annotation;
038import java.lang.reflect.Type;
039import java.net.URL;
040import java.util.ArrayList;
041import java.util.Arrays;
042import java.util.Iterator;
043import java.util.List;
044import java.util.Map;
045import java.util.Properties;
046
047import javax.annotation.PostConstruct;
048import javax.jcr.RepositoryException;
049import javax.jcr.Session;
050import javax.jcr.nodetype.NodeType;
051import javax.jcr.nodetype.NodeTypeIterator;
052import javax.ws.rs.Produces;
053import javax.ws.rs.WebApplicationException;
054import javax.ws.rs.core.MediaType;
055import javax.ws.rs.core.MultivaluedMap;
056import javax.ws.rs.core.UriInfo;
057import javax.ws.rs.ext.MessageBodyWriter;
058import javax.ws.rs.ext.Provider;
059
060import com.google.common.base.Predicate;
061import com.google.common.collect.ImmutableList;
062import com.google.common.collect.ImmutableMap;
063import com.google.common.collect.Iterables;
064import com.hp.hpl.jena.graph.Node;
065import com.hp.hpl.jena.rdf.model.Model;
066import org.apache.velocity.Template;
067import org.apache.velocity.VelocityContext;
068import org.apache.velocity.app.VelocityEngine;
069import org.apache.velocity.context.Context;
070import org.apache.velocity.tools.generic.EscapeTool;
071import org.apache.velocity.tools.generic.FieldTool;
072import org.fcrepo.http.commons.responses.HtmlTemplate;
073import org.fcrepo.http.commons.responses.ViewHelpers;
074import org.fcrepo.http.commons.session.SessionFactory;
075import org.fcrepo.kernel.RdfLexicon;
076import org.fcrepo.kernel.exception.RepositoryRuntimeException;
077import org.fcrepo.kernel.impl.rdf.impl.NamespaceRdfContext;
078import org.fcrepo.kernel.utils.iterators.RdfStream;
079import org.slf4j.Logger;
080import org.springframework.beans.factory.annotation.Autowired;
081
082/**
083 * Simple HTML provider for RdfStreams
084 *
085 * @author ajs6f
086 * @since Nov 19, 2013
087 */
088@Provider
089@Produces({TEXT_HTML, APPLICATION_XHTML_XML})
090public class StreamingBaseHtmlProvider implements MessageBodyWriter<RdfStream> {
091
092
093    @Autowired
094    SessionFactory sessionFactory;
095
096    @javax.ws.rs.core.Context
097    UriInfo uriInfo;
098
099    private static EscapeTool escapeTool = new EscapeTool();
100
101    protected VelocityEngine velocity = new VelocityEngine();
102
103    /**
104     * Location in the classpath where Velocity templates are to be found.
105     */
106    public static final String templatesLocation = "/views";
107
108    /**
109     * Location in the classpath where the common css file is to be found.
110     */
111    public static final String commonCssLocation = "/views/common.css";
112
113    /**
114     * Location in the classpath where the common javascript file is to be found.
115     */
116    public static final String commonJsLocation = "/views/common.js";
117
118    /**
119     * A map from String names for primary node types to the Velocity templates
120     * that should be used for those node types.
121     */
122    protected Map<String, Template> templatesMap;
123
124    public static final String templateFilenameExtension = ".vsl";
125
126    public static final String velocityPropertiesLocation =
127            "/velocity.properties";
128
129    private static final Logger LOGGER =
130        getLogger(StreamingBaseHtmlProvider.class);
131
132    private final Predicate<NodeType> acceptWhenTemplateExists = new Predicate<NodeType>() {
133        @Override
134        public boolean apply(final NodeType nodeType) {
135            final String nodeTypeName = nodeType.getName();
136            if (isBlank(nodeTypeName)) {
137                return false;
138            }
139            return velocity.resourceExists(getTemplateLocation(nodeTypeName));
140        }
141    };
142
143    private final Predicate<String> acceptWhenTemplateMapContainsKey = new Predicate<String>() {
144        @Override
145        public boolean apply(final String key) {
146            if (isBlank(key)) {
147                return false;
148            }
149            return templatesMap.containsKey(key);
150        }
151    };
152
153    @PostConstruct
154    void init() throws IOException {
155
156        LOGGER.trace("Velocity engine initializing...");
157        final Properties properties = new Properties();
158        final URL propertiesUrl =
159                getClass().getResource(velocityPropertiesLocation);
160        LOGGER.debug("Using Velocity configuration from {}", propertiesUrl);
161        try (final InputStream propertiesStream = propertiesUrl.openStream()) {
162            properties.load(propertiesStream);
163        }
164        velocity.init(properties);
165        LOGGER.trace("Velocity engine initialized.");
166
167        LOGGER.trace("Assembling a map of node primary types -> templates...");
168        final ImmutableMap.Builder<String, Template> templatesMapBuilder = builder();
169        final Session session = sessionFactory.getInternalSession();
170        try {
171            // we search all of the possible node primary types and mixins
172            for (final NodeTypeIterator primaryNodeTypes =
173                         session.getWorkspace().getNodeTypeManager()
174                                 .getPrimaryNodeTypes(); primaryNodeTypes.hasNext();) {
175                final NodeType primaryNodeType =
176                    primaryNodeTypes.nextNodeType();
177                final String primaryNodeTypeName =
178                    primaryNodeType.getName();
179
180                // Create a list of the primary type and all its parents
181                final List<NodeType> nodeTypesList = new ArrayList<>();
182                nodeTypesList.add(primaryNodeType);
183                nodeTypesList.addAll(Arrays.asList(primaryNodeType.getSupertypes()));
184
185                // Find a template that matches the primary type or one of its parents
186                final NodeType templateMatch = Iterables.find(nodeTypesList, acceptWhenTemplateExists, null);
187                if (templateMatch != null) {
188                    addTemplate(primaryNodeTypeName, templateMatch.getName(), templatesMapBuilder);
189                } else {
190                    // No HTML representation available for that kind of node
191                    LOGGER.debug("Didn't find template for nodes with primary type or its parents: {} in location: {}",
192                            primaryNodeTypeName, templatesLocation);
193                }
194            }
195
196            final List<String> otherTemplates =
197                    ImmutableList.of("jcr:nodetypes", "node", "fcr:versions", "fcr:fixity");
198
199            for (final String key : otherTemplates) {
200                final Template template =
201                    velocity.getTemplate(getTemplateLocation(key));
202                templatesMapBuilder.put(key, template);
203            }
204
205            templatesMap = templatesMapBuilder.build();
206
207        } catch (final RepositoryException e) {
208            throw new RepositoryRuntimeException(e);
209        }
210        LOGGER.trace("Assembled template map.");
211        LOGGER.trace("HtmlProvider initialization complete.");
212    }
213
214    @Override
215    public void writeTo(final RdfStream rdfStream, final Class<?> type,
216                        final Type genericType, final Annotation[] annotations,
217                        final MediaType mediaType,
218                        final MultivaluedMap<String, Object> httpHeaders,
219                        final OutputStream entityStream) throws IOException {
220
221        try {
222            final RdfStream nsRdfStream = new NamespaceRdfContext(rdfStream.session());
223
224            rdfStream.namespaces(nsRdfStream.namespaces());
225
226            final Node subject = rdfStream.topic();
227
228            final Model model = rdfStream.asModel();
229
230            final Template nodeTypeTemplate = getTemplate(model, subject, annotations);
231
232            final Context context = getContext(model, subject);
233
234            // the contract of MessageBodyWriter<T> is _not_ to close the stream
235            // after writing to it
236            final Writer outWriter = new OutputStreamWriter(entityStream);
237            nodeTypeTemplate.merge(context, outWriter);
238            outWriter.flush();
239
240        } catch (final RepositoryException e) {
241            throw new WebApplicationException(e);
242        }
243
244    }
245
246    protected Context getContext(final Model model, final Node subject) {
247        final FieldTool fieldTool = new FieldTool();
248
249        final Context context = new VelocityContext();
250        context.put("rdfLexicon", fieldTool.in(RdfLexicon.class));
251        context.put("helpers", ViewHelpers.getInstance());
252        context.put("esc", escapeTool);
253        context.put("rdf", model.getGraph());
254
255        context.put("model", model);
256        context.put("subjects", model.listSubjects());
257        context.put("nodeany", ANY);
258        context.put("topic", subject);
259        context.put("uriInfo", uriInfo);
260        return context;
261    }
262
263    private Template getTemplate(final Model rdf, final Node subject,
264                                 final Annotation[] annotations) {
265        Template template = null;
266
267        for (final Annotation a : annotations) {
268            if (a instanceof HtmlTemplate) {
269                final String value = ((HtmlTemplate) a).value();
270                LOGGER.debug("Found an HtmlTemplate annotation {}", value);
271                template = templatesMap.get(value);
272                break;
273            }
274        }
275
276        if (template == null) {
277            LOGGER.trace("Attempting to discover the mixin types of the node for the resource in question...");
278            final Iterator<String> mixinTypes =
279                getAllValuesForPredicate(rdf, subject,
280                                         mixinTypesPredicate);
281
282            LOGGER.debug("Found mixins: {}", mixinTypes);
283            if (mixinTypes != null) {
284                final ImmutableList<String> copy = copyOf(mixinTypes);
285                final String mixin = Iterables.find(copy, acceptWhenTemplateMapContainsKey, null);
286                if (mixin != null) {
287                    LOGGER.debug("Matched mixin type: {}", mixin);
288                    template = templatesMap.get(mixin);
289                }
290            }
291        }
292
293        if (template == null) {
294            LOGGER.trace("Attempting to discover the primary type of the node for the resource in question...");
295            final String nodeType =
296                    getFirstValueForPredicate(rdf, subject, primaryTypePredicate);
297
298            LOGGER.debug("Found primary node type: {}", nodeType);
299            template = templatesMap.get(nodeType);
300        }
301
302        if (template == null) {
303            LOGGER.debug("Falling back on default node template");
304            template = templatesMap.get("node");
305        }
306
307        LOGGER.debug("Choosing template: {}", template.getName());
308        return template;
309    }
310
311    @Override
312    public boolean isWriteable(final Class<?> type, final Type genericType,
313                               final Annotation[] annotations, final MediaType mediaType) {
314        LOGGER.debug(
315                "Checking to see if type: {} is serializable to mimeType: {}",
316                type.getName(), mediaType);
317        return (mediaType.equals(TEXT_HTML_TYPE) || mediaType
318                .equals(APPLICATION_XHTML_XML_TYPE))
319                && RdfStream.class.isAssignableFrom(type);
320    }
321
322    @Override
323    public long getSize(final RdfStream t, final Class<?> type,
324                        final Type genericType, final Annotation[] annotations,
325                        final MediaType mediaType) {
326        // we don't know in advance how large the result might be
327        return -1;
328    }
329
330    private void addTemplate(final String primaryNodeTypeName, final String templateNodeTypeName,
331                             final ImmutableMap.Builder<String, Template> templatesMapBuilder) {
332        final String templateLocation = getTemplateLocation(templateNodeTypeName);
333        final Template template =
334            velocity.getTemplate(templateLocation);
335        template.setName(templateLocation);
336        LOGGER.debug("Found template: {}", templateLocation);
337        templatesMapBuilder.put(primaryNodeTypeName, template);
338        LOGGER.debug("which we will use for nodes with primary type: {}",
339                     primaryNodeTypeName);
340    }
341
342    private static String getTemplateLocation(final String nodeTypeName) {
343        return templatesLocation + "/" +
344            nodeTypeName.replace(':', '-') + templateFilenameExtension;
345    }
346}