001/**
002 * Copyright 2014 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.commons.responses;
017
018import static com.google.common.base.Strings.isNullOrEmpty;
019import static com.hp.hpl.jena.graph.Node.ANY;
020import static com.hp.hpl.jena.rdf.model.ResourceFactory.createResource;
021import static com.hp.hpl.jena.rdf.model.ResourceFactory.createProperty;
022import static org.fcrepo.kernel.RdfLexicon.CREATED_DATE;
023import static org.fcrepo.kernel.FedoraJcrTypes.FCR_METADATA;
024import static org.fcrepo.kernel.RdfLexicon.DC_TITLE;
025import static org.fcrepo.kernel.RdfLexicon.HAS_VERSION_LABEL;
026import static org.fcrepo.kernel.RdfLexicon.RDFS_LABEL;
027import static org.fcrepo.kernel.RdfLexicon.HAS_VERSION;
028import static org.fcrepo.kernel.RdfLexicon.RDF_NAMESPACE;
029import static org.fcrepo.kernel.RdfLexicon.DC_NAMESPACE;
030import static org.slf4j.LoggerFactory.getLogger;
031
032import java.text.SimpleDateFormat;
033import java.util.Date;
034import java.util.Iterator;
035import java.util.List;
036import java.util.Map;
037import java.util.TreeMap;
038
039import javax.ws.rs.core.UriInfo;
040
041import com.hp.hpl.jena.graph.Graph;
042import com.hp.hpl.jena.graph.Triple;
043import org.fcrepo.http.commons.api.rdf.TripleOrdering;
044import org.fcrepo.kernel.RdfLexicon;
045import org.slf4j.Logger;
046
047import com.google.common.collect.ImmutableList;
048import com.google.common.collect.ImmutableMap;
049import com.google.common.collect.Ordering;
050import com.hp.hpl.jena.graph.Node;
051import com.hp.hpl.jena.graph.NodeFactory;
052import com.hp.hpl.jena.rdf.model.Model;
053import com.hp.hpl.jena.rdf.model.Property;
054import com.hp.hpl.jena.rdf.model.Resource;
055import com.hp.hpl.jena.rdf.model.ResourceFactory;
056import com.hp.hpl.jena.shared.PrefixMapping;
057import com.hp.hpl.jena.vocabulary.RDF;
058import com.hp.hpl.jena.vocabulary.RDFS;
059
060/**
061 * General view helpers for rendering HTML responses
062 *
063 * @author awoods
064 */
065public class ViewHelpers {
066
067    private static final Logger LOGGER = getLogger(ViewHelpers.class);
068
069    private static ViewHelpers instance = null;
070
071    protected ViewHelpers() {
072        // Exists only to defeat instantiation.
073    }
074
075    /**
076     * ViewHelpers are singletons. Initialize or return the existing object
077     * @return an instance of ViewHelpers
078     */
079    public static ViewHelpers getInstance() {
080        if (instance == null) {
081            instance = new ViewHelpers();
082        }
083        return instance;
084    }
085
086    /**
087     * Return an iterator of Triples that match the given subject and predicate
088     *
089     * @param graph
090     * @param subject
091     * @param predicate
092     * @return iterator
093     */
094    public Iterator<Triple> getObjects(final Graph graph,
095        final Node subject, final Resource predicate) {
096        return graph.find(subject, predicate.asNode(), ANY);
097    }
098
099    /**
100     * Return an iterator of Triples for versions.
101     *
102     * @param graph
103     * @param subject
104     * @return iterator
105     */
106    public Iterator<Node> getVersions(final Graph graph,
107        final Node subject) {
108        return getOrderedVersions(graph, subject, HAS_VERSION);
109    }
110
111    /**
112     * Return an iterator of Triples for versions in order that
113     * they were created.
114     *
115     * @param graph
116     * @param subject
117     * @return iterator
118     */
119    public Iterator<Node> getOrderedVersions(final Graph graph,
120        final Node subject, final Resource predicate) {
121        final Iterator<Triple> versions = getObjects(graph, subject, predicate);
122        final Map<String, Node> map = new TreeMap<>();
123        final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
124        Triple triple;
125        String date;
126        while (versions.hasNext()) {
127            triple = versions.next();
128            date = getVersionDate(graph, triple.getObject());
129            String key = isNullOrEmpty(date) ? format.format(new Date()) : date;
130            while (map.containsKey(key)) {
131                key = key + "1";
132            }
133            map.put(key, triple.getObject());
134        }
135        return map.values().iterator();
136    }
137
138    /**
139     * Gets the URL of the node whose version is represented by the
140     * current node.  The current implementation assumes the URI
141     * of that node will be the same as the breadcrumb entry that
142     * precedes one with the path "fcr:versions".
143     */
144     public String getVersionSubjectUrl(final UriInfo uriInfo, final Node subject) {
145        final Map<String, String> breadcrumbs = getNodeBreadcrumbs(uriInfo, subject);
146        String lastUrl = null;
147        for (final Map.Entry<String, String> entry : breadcrumbs.entrySet()) {
148            if (entry.getValue().equals("fcr:versions")) {
149                return lastUrl;
150            }
151            lastUrl = entry.getKey();
152        }
153        return null;
154     }
155
156    /**
157     * Gets a version label of a subject from the graph
158     *
159     * @param graph
160     * @param subject
161     * @param defaultValue a value to be returned if no label is present in the
162     *                     graph
163     * @return the label of the version if one has been provided; otherwise
164     * the default is returned
165     */
166    public String getVersionLabel(final Graph graph,
167                                 final Node subject, final String defaultValue) {
168        final Iterator<Triple> objects = getObjects(graph, subject, HAS_VERSION_LABEL);
169        if (objects.hasNext()) {
170            return objects.next().getObject().getLiteralValue().toString();
171        }
172        return defaultValue;
173    }
174
175    /**
176     * Gets a modification date of a subject from the graph
177     *
178     * @param graph
179     * @param subject
180     * @return the modification date or null if none exists
181     */
182    public String getVersionDate(final Graph graph,
183                                 final Node subject) {
184        final Iterator<Triple> objects = getObjects(graph, subject, CREATED_DATE);
185        if (objects.hasNext()) {
186            return objects.next().getObject().getLiteralValue().toString();
187        }
188        return "";
189    }
190
191    /**
192     * Get the canonical title of a subject from the graph
193     *
194     * @param graph
195     * @param subject
196     * @return canonical title of the subject in the graph
197     */
198    public String getObjectTitle(final Graph graph, final Node subject) {
199
200        if (subject == null) {
201            return "";
202        }
203
204        final Property[] properties = new Property[] {RDFS_LABEL, DC_TITLE};
205
206        for (final Property p : properties) {
207            final Iterator<Triple> objects = getObjects(graph, subject, p);
208
209            if (objects.hasNext()) {
210                return objects.next().getObject().getLiteral().getLexicalForm();
211            }
212        }
213
214        if (subject.isURI()) {
215            return subject.getURI();
216        } else if (subject.isBlank()) {
217            return subject.getBlankNodeLabel();
218        } else {
219            return subject.toString();
220        }
221
222    }
223
224    /**
225     * Take a HAS_SERIALIZATION node and find the RDFS_LABEL for the format it is associated with
226     *
227     * @param graph
228     * @param subject
229     * @return the label for the serialization format
230     */
231    public String getSerializationTitle(final Graph graph, final Node subject) {
232        final Property dcFormat = createProperty(DC_NAMESPACE + "format");
233        final Iterator<Triple> formatRDFs = getObjects(graph, subject, dcFormat);
234        if (formatRDFs.hasNext()) {
235            return getObjectTitle(graph, formatRDFs.next().getObject());
236        }
237        return "";
238    }
239
240    /**
241     * Determines whether the subject is writable
242     * true if node is writable
243     */
244    public boolean isWritable(final Graph graph, final Node subject) {
245        final Iterator<Triple> it = getObjects(graph, subject, RdfLexicon.WRITABLE);
246        return it.hasNext() && it.next().getObject().getLiteralValue().toString().equals("true");
247    }
248
249    /**
250     * Determines whether the subject is of type nt:frozenNode.
251     * true if node has type nt:frozen
252     */
253    public boolean isFrozenNode(final Graph graph, final Node subject) {
254        final Iterator<Triple> objects = getObjects(graph, subject, RdfLexicon.HAS_PRIMARY_TYPE);
255        return objects.hasNext()
256                && objects.next().getObject()
257                .getLiteralValue().toString().equals("nt:frozenNode");
258    }
259
260    /**
261     * Get the string version of the object that matches the given subject and
262     * predicate
263     *
264     * @param graph
265     * @param subject
266     * @param predicate
267     * @return string version of the object
268     */
269    public String getObjectsAsString(final Graph graph,
270            final Node subject, final Resource predicate, final boolean uriAsLink) {
271        final Iterator<Triple> iterator = getObjects(graph, subject, predicate);
272
273        if (iterator.hasNext()) {
274            final Node object = iterator.next().getObject();
275
276            if (object.isLiteral()) {
277                final String s = object.getLiteralValue().toString();
278                if (s.isEmpty()) {
279                    return "<empty>";
280                }
281                return s;
282            }
283            if (uriAsLink) {
284                return "&lt;<a href=\"" + object.getURI() + "\">" +
285                           object.getURI() + "</a>&gt;";
286            }
287            return object.getURI();
288        }
289        return "";
290    }
291
292    /**
293     * Generate url to local name breadcrumbs for a given node's tree
294     *
295     * @param uriInfo
296     * @param subject
297     * @return breadcrumbs
298     */
299    public Map<String, String> getNodeBreadcrumbs(final UriInfo uriInfo,
300            final Node subject) {
301        final String topic = subject.getURI();
302
303        LOGGER.trace("Generating breadcrumbs for subject {}", subject);
304        final ImmutableMap.Builder<String, String> builder =
305                ImmutableMap.builder();
306
307        final String baseUri = uriInfo.getBaseUri().toString();
308
309        if (!topic.startsWith(baseUri)) {
310            LOGGER.trace("Topic wasn't part of our base URI {}", baseUri);
311            return builder.build();
312        }
313
314        final String salientPath = topic.substring(baseUri.length());
315
316        final String[] split = salientPath.split("/");
317
318        final StringBuilder cumulativePath = new StringBuilder();
319
320        for (final String path : split) {
321
322            if (path.isEmpty()) {
323                continue;
324            }
325
326            cumulativePath.append(path);
327
328            final String uri =
329                    uriInfo.getBaseUriBuilder().path(cumulativePath.toString())
330                            .build().toString();
331
332            LOGGER.trace("Adding breadcrumb for path segment {} => {}", path,
333                    uri);
334
335            builder.put(uri, path);
336
337            cumulativePath.append("/");
338
339        }
340
341        return builder.build();
342
343    }
344
345    /**
346     * Sort a Iterator of Triples alphabetically by its subject, predicate, and
347     * object
348     *
349     * @param model
350     * @param it
351     * @return iterator of alphabetized triples
352     */
353    public List<Triple> getSortedTriples(final Model model, final Iterator<Triple> it) {
354        return Ordering.from(new TripleOrdering(model)).sortedCopy(ImmutableList.copyOf(it));
355    }
356
357    /**
358     * Get the namespace prefix (or the namespace URI itself, if no prefix is
359     * available) from a prefix mapping
360     *
361     * @param mapping
362     * @param namespace
363     * @return namespace prefix
364     */
365    public String getNamespacePrefix(final PrefixMapping mapping,
366            final String namespace, final boolean compact) {
367        final String nsURIPrefix = mapping.getNsURIPrefix(namespace);
368
369        if (nsURIPrefix == null) {
370            if (compact) {
371                final int hashIdx = namespace.lastIndexOf('#');
372
373                final int split;
374
375                if (hashIdx > 0) {
376                    split = namespace.substring(0, hashIdx).lastIndexOf('/');
377                } else {
378                    split = namespace.lastIndexOf('/');
379                }
380
381                if (split > 0) {
382                    return "..." + namespace.substring(split);
383                }
384                return namespace;
385            }
386            return namespace;
387        }
388        return nsURIPrefix + ":";
389    }
390
391    /**
392     * Get a prefix preamble appropriate for a SPARQL-UPDATE query from a prefix
393     * mapping object
394     *
395     * @param mapping
396     * @return prefix preamble
397     */
398    public String getPrefixPreamble(final PrefixMapping mapping) {
399        final StringBuilder sb = new StringBuilder();
400
401        final Map<String, String> nsPrefixMap = mapping.getNsPrefixMap();
402
403        for (final Map.Entry<String, String> entry : nsPrefixMap.entrySet()) {
404            sb.append("PREFIX " + entry.getKey() + ": <" + entry.getValue() +
405                    ">\n");
406        }
407
408        sb.append("\n");
409        return sb.toString();
410    }
411
412    /**
413     * Determines whether the subject is kind of RDF resource
414     */
415    public boolean isRdfResource(final Graph graph,
416                                 final Node subject,
417                                 final String namespace,
418                                 final String resource) {
419        final Iterator<Triple> it = graph.find(subject,
420                                               createResource(RDF_NAMESPACE + "type").asNode(),
421                                               createResource(namespace + resource).asNode());
422        return it.hasNext();
423    }
424
425    /**
426     * Convert an RDF resource to an RDF node
427     *
428     * @param r
429     * @return RDF node representation of the given RDF resource
430     */
431    public Node asNode(final Resource r) {
432        return r.asNode();
433    }
434
435    /**
436     * Convert a URI string to an RDF node
437     *
438     * @param r
439     * @return RDF node representation of the given string
440     */
441    public Node asLiteralStringNode(final String r) {
442        return ResourceFactory.createPlainLiteral(r).asNode();
443    }
444
445    /**
446     * Yes, we really did create a method to increment
447     * a given int. You can't do math in a velocity template.
448     *
449     * @param i
450     * @return maths
451     */
452    public int addOne(final int i) {
453        return i + 1;
454    }
455
456    /**
457     * Proxying access to the RDF type static property
458     * @return RDF type property
459     */
460    public Property rdfType() {
461        return RDF.type;
462    }
463
464    /**
465     * Proxying access to the RDFS domain static property
466     * @return RDFS domain property
467     */
468    public Property rdfsDomain() {
469        return RDFS.domain;
470    }
471
472    /**
473     * Proxying access to the RDFS class static property
474     * @return RDFS class resource
475     */
476    public Resource rdfsClass() {
477        return RDFS.Class;
478    }
479
480    /**
481     * Get the content-bearing node for the given subject
482     * @param subject
483     * @return content-bearing node for the given subject
484     */
485    public Node getContentNode(final Node subject) {
486        return NodeFactory.createURI(subject.getURI().replace(FCR_METADATA, ""));
487    }
488
489    /**
490     * Transform a source string to something appropriate for HTML ids
491     * @param source
492     * @return transformed source string
493     */
494    public String parameterize(final String source) {
495        return source.toLowerCase().replaceAll("[^a-z0-9\\-_]+", "_");
496    }
497}