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