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.commons.responses;
007
008import static com.google.common.collect.Lists.newArrayList;
009import static org.apache.jena.atlas.iterator.Iter.asStream;
010import static org.apache.jena.graph.GraphUtil.listObjects;
011import static org.apache.jena.rdf.model.ResourceFactory.createProperty;
012import static org.apache.jena.rdf.model.ResourceFactory.createResource;
013import static org.apache.jena.vocabulary.DC.title;
014import static org.apache.jena.vocabulary.RDF.type;
015import static org.apache.jena.vocabulary.RDFS.label;
016import static org.apache.jena.vocabulary.SKOS.prefLabel;
017import static java.util.Arrays.asList;
018import static java.util.Arrays.stream;
019import static java.util.Collections.emptyMap;
020import static java.util.stream.Collectors.joining;
021import static java.util.stream.Collectors.toMap;
022import static org.fcrepo.kernel.api.FedoraTypes.FCR_METADATA;
023import static org.fcrepo.kernel.api.FedoraTypes.FCR_VERSIONS;
024import static org.fcrepo.kernel.api.RdfLexicon.CONTAINS;
025import static org.fcrepo.kernel.api.RdfLexicon.MEMENTO_TYPE;
026import static org.fcrepo.kernel.api.RdfLexicon.REPOSITORY_ROOT;
027import static org.fcrepo.kernel.api.RdfLexicon.isManagedPredicate;
028import static org.fcrepo.kernel.api.services.VersionService.MEMENTO_LABEL_FORMATTER;
029import static org.fcrepo.kernel.api.services.VersionService.MEMENTO_RFC_1123_FORMATTER;
030import static org.slf4j.LoggerFactory.getLogger;
031
032import java.time.Instant;
033import java.util.Comparator;
034import java.util.Iterator;
035import java.util.LinkedHashMap;
036import java.util.List;
037import java.util.Map;
038import java.util.Optional;
039import java.util.StringJoiner;
040
041import javax.ws.rs.core.UriInfo;
042
043import org.apache.jena.graph.Graph;
044import org.apache.jena.graph.NodeFactory;
045import org.apache.jena.graph.Triple;
046import org.apache.jena.graph.impl.LiteralLabel;
047import org.apache.jena.vocabulary.DCTerms;
048
049import org.fcrepo.http.commons.api.rdf.TripleOrdering;
050import org.slf4j.Logger;
051
052import org.apache.jena.graph.Node;
053import org.apache.jena.rdf.model.Model;
054import org.apache.jena.rdf.model.Property;
055import org.apache.jena.rdf.model.Resource;
056import org.apache.jena.rdf.model.ResourceFactory;
057import org.apache.jena.shared.PrefixMapping;
058import org.apache.jena.vocabulary.RDF;
059import org.apache.jena.vocabulary.RDFS;
060
061/**
062 * General view helpers for rendering HTML responses
063 *
064 * @author awoods
065 * @author ajs6f
066 */
067public class ViewHelpers {
068
069    private static final Logger LOGGER = getLogger(ViewHelpers.class);
070
071    private static ViewHelpers instance = null;
072
073    private static final List<Property>  TITLE_PROPERTIES = asList(label, title, DCTerms.title, prefLabel);
074
075    private ViewHelpers() {
076        // Exists only to defeat instantiation.
077    }
078
079    /**
080     * ViewHelpers is a singleton. Initialize or return the existing object
081     * @return an instance of ViewHelpers
082     */
083    public static ViewHelpers getInstance() {
084        return instance == null ? instance = new ViewHelpers() : instance;
085    }
086
087    /**
088     * Return an iterator of Triples for versions.
089     *
090     * @param graph the graph
091     * @param subject the subject
092     * @return iterator
093     */
094    public Iterator<Node> getVersions(final Graph graph,
095        final Node subject) {
096        // Mementos should be ordered by date so use the getOrderedVersions.
097        return getOrderedVersions(graph, subject, CONTAINS.asResource());
098    }
099
100    /**
101     * Return an iterator of Triples for versions in order that
102     * they were created.
103     *
104     * @param g the graph
105     * @param subject the subject
106     * @param predicate the predicate
107     * @return iterator
108     */
109    public Iterator<Node> getOrderedVersions(final Graph g, final Node subject, final Resource predicate) {
110        final List<Node> vs = listObjects(g, subject, predicate.asNode()).toList();
111        vs.sort(Comparator.comparing(v -> getVersionDate(g, v)));
112        return vs.iterator();
113    }
114
115    /**
116     * Gets the URL of the node whose version is represented by the
117     * current node.  The current implementation assumes the URI
118     * of that node will be the same as the breadcrumb entry that
119     * precedes one with the path "fcr:versions".
120     * @param uriInfo the uri info
121     * @param subject the subject
122     * @return the URL of the node
123     */
124     public String getVersionSubjectUrl(final UriInfo uriInfo, final Node subject) {
125        final Map<String, String> breadcrumbs = getNodeBreadcrumbs(uriInfo, subject);
126        String lastUrl = null;
127        for (final Map.Entry<String, String> entry : breadcrumbs.entrySet()) {
128            if (entry.getValue().equals("fcr:versions")) {
129                return lastUrl;
130            }
131            lastUrl = entry.getKey();
132        }
133        return null;
134     }
135
136    /**
137     * Get the date time as the version label.
138     *
139     * @param graph the graph
140     * @param subject the subject
141     * @return the datetime in RFC 1123 format.
142     */
143    public String getVersionLabel(final Graph graph, final Node subject) {
144        final Instant datetime = getVersionDate(graph, subject);
145        return MEMENTO_RFC_1123_FORMATTER.format(datetime);
146    }
147
148    /**
149     * Gets a modification date of a subject from the graph
150     *
151     * @param graph the graph
152     * @param subject the subject
153     * @return the modification date if it exists
154     */
155    public Instant getVersionDate(final Graph graph, final Node subject) {
156        final String[] pathParts = subject.getURI().split("/");
157        return MEMENTO_LABEL_FORMATTER.parse(pathParts[pathParts.length - 1], Instant::from);
158    }
159
160    private static Optional<String> getValue(final Graph graph, final Node subject, final Node predicate) {
161        final Iterator<Node> objects = listObjects(graph, subject, predicate);
162        return Optional.ofNullable(objects.hasNext() ? objects.next().getLiteralValue().toString() : null);
163    }
164
165    /**
166     * Get the canonical title of a subject from the graph
167     *
168     * @param graph the graph
169     * @param sub the subject
170     * @return canonical title of the subject in the graph
171     */
172    public String getObjectTitle(final Graph graph, final Node sub) {
173        if (sub == null) {
174            return "";
175        }
176        final Optional<String> title = TITLE_PROPERTIES.stream().map(Property::asNode).flatMap(p -> listObjects(
177                graph, sub, p).toList().stream()).filter(Node::isLiteral).map(Node::getLiteral).map(
178                        LiteralLabel::toString).findFirst();
179        return title.orElse(sub.isURI() ? sub.getURI() : sub.isBlank() ? sub.getBlankNodeLabel() : sub.toString());
180    }
181
182    /**
183     * Get the ID of a resource, this is the URI, blank node ID or just the local name. Used in resource.vsl
184     *
185     * @param res
186     *   The resource to get the ID for
187     * @return
188     *   The ID as a string.
189     */
190     public String getResourceId(final Resource res) {
191         if (res == null) {
192            return "";
193         } else if (res.isURIResource()) {
194            return res.getURI();
195         } else if (res.isAnon()) {
196            return res.getId().getLabelString();
197         } else {
198            return res.getLocalName();
199         }
200     }
201
202    /**
203     * Determines whether the subject is writable
204     * true if node is writable
205     * @param graph the graph
206     * @param subject the subject
207     * @return whether the subject is writable
208     */
209    public boolean isWritable(final Graph graph, final Node subject) {
210        // XXX: always return true until we can determine a better way to control the HTML UI
211        return true;
212    }
213
214    /**
215     * Determines whether the subject is of type memento:Memento.
216     *
217     * @param graph the graph
218     * @param subject the subject
219     * @return whether the subject is a versioned node
220     */
221    public boolean isVersionedNode(final Graph graph, final Node subject) {
222        return listObjects(graph, subject, RDF.type.asNode()).toList().stream().map(Node::getURI)
223            .anyMatch((MEMENTO_TYPE)::equals);
224    }
225
226    /**
227     * Get the string version of the object that matches the given subject and
228     * predicate
229     *
230     * @param graph the graph
231     * @param subject the subject
232     * @param predicate the predicate
233     * @param uriAsLink the boolean value of uri as link
234     * @return string version of the object
235     */
236    public String getObjectsAsString(final Graph graph,
237            final Node subject, final Resource predicate, final boolean uriAsLink) {
238        LOGGER.trace("Getting Objects as String: s:{}, p:{}, g:{}", subject, predicate, graph);
239        final Iterator<Node> iterator = listObjects(graph, subject, predicate.asNode());
240        if (iterator.hasNext()) {
241            final Node obj = iterator.next();
242            if (obj.isLiteral()) {
243                final String lit = obj.getLiteralValue().toString();
244                return lit.isEmpty() ? "<empty>" : lit;
245            }
246            return uriAsLink ? "&lt;<a href=\"" + obj.getURI() + "\">" + obj.getURI() + "</a>&gt;" : obj.getURI();
247        }
248        return "";
249    }
250
251    /**
252     * Returns the original resource as a URI Node if
253     * the subject represents a memento uri; otherwise it
254     * returns the subject parameter.
255     * @param subject the subject
256     * @return a URI node of the original resource.
257     */
258    public Node getOriginalResource(final Node subject) {
259        if (!subject.isURI()) {
260            return subject;
261        }
262
263        final String subjectUri = subject.getURI();
264        final int index = subjectUri.indexOf(FCR_VERSIONS);
265        if (index > 0) {
266            return NodeFactory.createURI(subjectUri.substring(0, index - 1));
267        } else {
268            return subject;
269        }
270    }
271
272    /**
273     * Same as above but takes a string.
274     * NB: This method is currently used in fcrepo-http-api/src/main/resources/views/default.vsl
275     * @param subjectUri the URI
276     * @return a node
277     */
278    public Node getOriginalResource(final String subjectUri) {
279        return getOriginalResource(createURI(subjectUri));
280    }
281
282    /**
283     * Get the number of child resources associated with the arg 'subject' as specified by the triple found in the arg
284     * 'graph' with the predicate RdfLexicon.HAS_CHILD_COUNT.
285     *
286     * @param graph   of triples
287     * @param subject for which child resources is sought
288     * @return number of child resources
289     */
290    public int getNumChildren(final Graph graph, final Node subject) {
291        LOGGER.trace("Getting number of children: s:{}, g:{}", subject, graph);
292        return (int) asStream(listObjects(graph, subject, CONTAINS.asNode())).count();
293    }
294
295    /**
296     * Generate url to local name breadcrumbs for a given node's tree
297     *
298     * @param uriInfo the uri info
299     * @param subject the subject
300     * @return breadcrumbs
301     */
302    public Map<String, String> getNodeBreadcrumbs(final UriInfo uriInfo,
303            final Node subject) {
304        final String topic = subject.getURI();
305
306        LOGGER.trace("Generating breadcrumbs for subject {}", subject);
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 emptyMap();
312        }
313
314        final String salientPath = topic.substring(baseUri.length());
315        final StringJoiner cumulativePath = new StringJoiner("/");
316        return stream(salientPath.split("/")).filter(seg -> !seg.isEmpty()).collect(toMap(seg -> uriInfo
317                .getBaseUriBuilder().path(cumulativePath.add(seg).toString())
318                .build().toString(), seg -> seg, (u, v) -> null, LinkedHashMap::new));
319    }
320
321    /**
322     * Sort a Iterator of Triples alphabetically by its subject, predicate, and
323     * object
324     *
325     * @param model the model
326     * @param it the iterator of triples
327     * @return iterator of alphabetized triples
328     */
329    public List<Triple> getSortedTriples(final Model model, final Iterator<Triple> it) {
330        final List<Triple> triples = newArrayList(it);
331        triples.sort(new TripleOrdering(model));
332        return triples;
333    }
334
335    /**
336     * Get the namespace prefix (or the namespace URI itself, if no prefix is
337     * available) from a prefix mapping
338     *
339     * @param mapping the prefix mapping
340     * @param ns the namespace
341     * @param compact the boolean value of compact
342     * @return namespace prefix
343     */
344    public String getNamespacePrefix(final PrefixMapping mapping,
345            final String ns, final boolean compact) {
346        final String nsURIPrefix = mapping.getNsURIPrefix(ns);
347        if (nsURIPrefix == null) {
348            if (compact) {
349                final int hashIdx = ns.lastIndexOf('#');
350                final int split = hashIdx > 0 ? ns.substring(0, hashIdx).lastIndexOf('/') : ns.lastIndexOf('/');
351                return split > 0 ? "..." + ns.substring(split) : ns;
352            }
353            return ns;
354        }
355        return nsURIPrefix + ":";
356    }
357
358    /**
359     * Get a prefix preamble appropriate for a SPARQL-UPDATE query from a prefix
360     * mapping object
361     *
362     * @param mapping the prefix mapping
363     * @return prefix preamble
364     */
365    public String getPrefixPreamble(final PrefixMapping mapping) {
366        return mapping.getNsPrefixMap().entrySet().stream()
367                .map(e -> "PREFIX " + e.getKey() + ": <" + e.getValue() + ">").collect(joining("\n", "", "\n\n"));
368    }
369
370    /**
371     * Determines whether the subject is kind of RDF resource
372     * @param graph the graph
373     * @param subject the subject
374     * @param namespace the namespace
375     * @param resource the resource
376     * @return whether the subject is kind of RDF resource
377     */
378    public boolean isRdfResource(final Graph graph,
379                                 final Node subject,
380                                 final String namespace,
381                                 final String resource) {
382        LOGGER.trace("Is RDF Resource? s:{}, ns:{}, r:{}, g:{}", subject, namespace, resource, graph);
383        return graph.find(subject, type.asNode(),
384                createResource(namespace + resource).asNode()).hasNext();
385    }
386
387    /**
388     * Is the subject the repository root resource.
389     *
390     * @param graph The graph
391     * @param subject The current subject
392     * @return true if has rdf:type http://fedora.info/definitions/v4/repository#RepositoryRoot
393     */
394    public boolean isRootResource(final Graph graph, final Node subject) {
395        return graph.contains(subject, rdfType().asNode(), REPOSITORY_ROOT.asNode());
396    }
397
398    /**
399     * Convert a URI string to an RDF node
400     *
401     * @param r the uri string
402     * @return RDF node representation of the given string
403     */
404    public Node asLiteralStringNode(final String r) {
405        return ResourceFactory.createPlainLiteral(r).asNode();
406    }
407
408    /**
409     * Yes, we really did create a method to increment
410     * a given int. You can't do math in a velocity template.
411     *
412     * @param i the given integer
413     * @return maths
414     */
415    public int addOne(final int i) {
416        return i + 1;
417    }
418
419    /**
420     * Proxying access to the RDF type static property
421     * @return RDF type property
422     */
423    public Property rdfType() {
424        return RDF.type;
425    }
426
427    /**
428     * Proxying access to the RDFS domain static property
429     * @return RDFS domain property
430     */
431    public Property rdfsDomain() {
432        return RDFS.domain;
433    }
434
435    /**
436     * Proxying access to the RDFS class static property
437     * @return RDFS class resource
438     */
439    public Resource rdfsClass() {
440        return RDFS.Class;
441    }
442
443    /**
444     * Get the content-bearing node for the given subject
445     * @param subject the subject
446     * @return content-bearing node for the given subject
447     */
448    public static Node getContentNode(final Node subject) {
449        return subject == null ? null : NodeFactory.createURI(subject.getURI().replace("/" + FCR_METADATA, ""));
450    }
451
452    /**
453     * Create a URI Node from the provided String
454     *
455     * @param uri from which a URI Node will be created
456     * @return URI Node
457     */
458    public static Node createURI(final String uri) {
459        return NodeFactory.createURI(uri);
460    }
461
462    /**
463     * Transform a source string to something appropriate for HTML ids
464     * @param source the source string
465     * @return transformed source string
466     */
467    public static String parameterize(final String source) {
468        return source.toLowerCase().replaceAll("[^a-z0-9\\-_]+", "_");
469    }
470
471    /**
472     * Test if a Predicate is managed
473     * @param property the property
474     * @return whether the property is managed
475     */
476    public static boolean isManagedProperty(final Node property) {
477        return property.isURI() && isManagedPredicate.test(createProperty(property.getURI()));
478    }
479
480    /**
481     * Find a key in a map and format it as a string
482     * @param input map of objects.
483     * @param key the key to locate in the map.
484     * @return the result string.
485     */
486    public static String getString(final Map<String, Object> input, final String key) {
487        if (input.get(key) == null) {
488            return "";
489        }
490        final var value = input.get(key);
491        final var clazz = value.getClass();
492        final String output;
493        if (clazz == String.class) {
494            output = formatAsString((String) value);
495        } else if (clazz == String[].class) {
496            output = formatAsString((String[]) value);
497        } else if (clazz == Long.class) {
498            output = formatAsString((Long) value);
499        } else {
500            output = "";
501        }
502        return output;
503    }
504
505    /**
506     * Format to a string and check for null values
507     * @param input a string array or null
508     * @return a string.
509     */
510    private static String formatAsString(final String[] input) {
511        return (input == null || input.length == 0 ? "" :  String.join(", ", input));
512    }
513
514    /**
515     * Format a string to check for null values
516     * @param input a string or null
517     * @return a string.
518     */
519    private static String formatAsString(final String input) {
520        return (input == null ? "" : input);
521    }
522
523    private static String formatAsString(final Long input) {
524        return (input == null ? "" : input.toString());
525    }
526}