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