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.time.Instant.now;
030import static java.time.ZoneId.of;
031import static java.util.Arrays.asList;
032import static java.util.Arrays.stream;
033import static java.util.Collections.emptyMap;
034import static java.util.stream.Collectors.joining;
035import static java.util.stream.Collectors.toMap;
036import static org.fcrepo.kernel.api.FedoraTypes.FCR_METADATA;
037import static org.fcrepo.kernel.api.RdfLexicon.CONTAINS;
038import static org.fcrepo.kernel.api.RdfLexicon.CREATED_DATE;
039import static org.fcrepo.kernel.api.RdfLexicon.HAS_VERSION;
040import static org.fcrepo.kernel.api.RdfLexicon.HAS_VERSION_LABEL;
041import static org.fcrepo.kernel.api.RdfLexicon.REPOSITORY_NAMESPACE;
042import static org.fcrepo.kernel.api.RdfLexicon.WRITABLE;
043import static org.fcrepo.kernel.api.RdfLexicon.isManagedPredicate;
044import static org.slf4j.LoggerFactory.getLogger;
045
046import java.time.format.DateTimeFormatter;
047import java.util.Iterator;
048import java.util.LinkedHashMap;
049import java.util.List;
050import java.util.Map;
051import java.util.Optional;
052import java.util.StringJoiner;
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 static final String DEFAULT =
088        DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'").withZone(of("GMT")).format(now());
089
090    private ViewHelpers() {
091        // Exists only to defeat instantiation.
092    }
093
094    /**
095     * ViewHelpers is a singleton. Initialize or return the existing object
096     * @return an instance of ViewHelpers
097     */
098    public static ViewHelpers getInstance() {
099        return instance == null ? instance = new ViewHelpers() : instance;
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 g the graph
119     * @param subject the subject
120     * @param predicate the predicate
121     * @return iterator
122     */
123    public Iterator<Node> getOrderedVersions(final Graph g, final Node subject, final Resource predicate) {
124        final List<Node> vs = listObjects(g, subject, predicate.asNode()).toList();
125        vs.sort((v1, v2) -> getVersionDate(g, v1).orElse(DEFAULT).compareTo(getVersionDate(g, v2).orElse(DEFAULT)));
126        return vs.iterator();
127    }
128
129    /**
130     * Gets the URL of the node whose version is represented by the
131     * current node.  The current implementation assumes the URI
132     * of that node will be the same as the breadcrumb entry that
133     * precedes one with the path "fcr:versions".
134     * @param uriInfo the uri info
135     * @param subject the subject
136     * @return the URL of the node
137     */
138     public String getVersionSubjectUrl(final UriInfo uriInfo, final Node subject) {
139        final Map<String, String> breadcrumbs = getNodeBreadcrumbs(uriInfo, subject);
140        String lastUrl = null;
141        for (final Map.Entry<String, String> entry : breadcrumbs.entrySet()) {
142            if (entry.getValue().equals("fcr:versions")) {
143                return lastUrl;
144            }
145            lastUrl = entry.getKey();
146        }
147        return null;
148     }
149
150    /**
151     * Gets a version label of a subject from the graph
152     *
153     * @param graph the graph
154     * @param subject the subject
155     * @return the label of the version if one has been provided
156     */
157    public Optional<String> getVersionLabel(final Graph graph, final Node subject) {
158        return getValue(graph, subject, HAS_VERSION_LABEL.asNode());
159    }
160
161    /**
162     * Gets a modification date of a subject from the graph
163     *
164     * @param graph the graph
165     * @param subject the subject
166     * @return the modification date if it exists
167     */
168    public Optional<String> getVersionDate(final Graph graph, final Node subject) {
169        return getValue(graph, subject, CREATED_DATE.asNode());
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        return getValue(graph, subject, WRITABLE.asNode()).filter("true"::equals).isPresent();
203    }
204
205    /**
206     * Determines whether the subject is of type fedora:Version.
207     * true if node has type fedora:Version
208     * @param graph the graph
209     * @param subject the subject
210     * @return whether the subject is a versioned node
211     */
212    public boolean isVersionedNode(final Graph graph, final Node subject) {
213        return listObjects(graph, subject, RDF.type.asNode()).toList().stream().map(Node::getURI)
214            .anyMatch((REPOSITORY_NAMESPACE + "Version")::equals);
215    }
216
217    /**
218     * Get the string version of the object that matches the given subject and
219     * predicate
220     *
221     * @param graph the graph
222     * @param subject the subject
223     * @param predicate the predicate
224     * @param uriAsLink the boolean value of uri as link
225     * @return string version of the object
226     */
227    public String getObjectsAsString(final Graph graph,
228            final Node subject, final Resource predicate, final boolean uriAsLink) {
229        LOGGER.trace("Getting Objects as String: s:{}, p:{}, g:{}", subject, predicate, graph);
230        final Iterator<Node> iterator = listObjects(graph, subject, predicate.asNode());
231        if (iterator.hasNext()) {
232            final Node obj = iterator.next();
233            if (obj.isLiteral()) {
234                final String lit = obj.getLiteralValue().toString();
235                return lit.isEmpty() ? "<empty>" : lit;
236            }
237            return uriAsLink ? "&lt;<a href=\"" + obj.getURI() + "\">" + obj.getURI() + "</a>&gt;" : obj.getURI();
238        }
239        return "";
240    }
241
242    /**
243     * Get the number of child resources associated with the arg 'subject' as specified by the triple found in the arg
244     * 'graph' with the predicate RdfLexicon.HAS_CHILD_COUNT.
245     *
246     * @param graph   of triples
247     * @param subject for which child resources is sought
248     * @return number of child resources
249     */
250    public int getNumChildren(final Graph graph, final Node subject) {
251        LOGGER.trace("Getting number of children: s:{}, g:{}", subject, graph);
252        return (int) asStream(listObjects(graph, subject, CONTAINS.asNode())).count();
253    }
254
255    /**
256     * Generate url to local name breadcrumbs for a given node's tree
257     *
258     * @param uriInfo the uri info
259     * @param subject the subject
260     * @return breadcrumbs
261     */
262    public Map<String, String> getNodeBreadcrumbs(final UriInfo uriInfo,
263            final Node subject) {
264        final String topic = subject.getURI();
265
266        LOGGER.trace("Generating breadcrumbs for subject {}", subject);
267        final String baseUri = uriInfo.getBaseUri().toString();
268
269        if (!topic.startsWith(baseUri)) {
270            LOGGER.trace("Topic wasn't part of our base URI {}", baseUri);
271            return emptyMap();
272        }
273
274        final String salientPath = topic.substring(baseUri.length());
275        final StringJoiner cumulativePath = new StringJoiner("/");
276        return stream(salientPath.split("/")).filter(seg -> !seg.isEmpty()).collect(toMap(seg -> uriInfo
277                .getBaseUriBuilder().path(cumulativePath.add(seg).toString())
278                .build().toString(), seg -> seg, (u, v) -> null, LinkedHashMap::new));
279    }
280
281    /**
282     * Sort a Iterator of Triples alphabetically by its subject, predicate, and
283     * object
284     *
285     * @param model the model
286     * @param it the iterator of triples
287     * @return iterator of alphabetized triples
288     */
289    public List<Triple> getSortedTriples(final Model model, final Iterator<Triple> it) {
290        final List<Triple> triples = newArrayList(it);
291        triples.sort(new TripleOrdering(model));
292        return triples;
293    }
294
295    /**
296     * Get the namespace prefix (or the namespace URI itself, if no prefix is
297     * available) from a prefix mapping
298     *
299     * @param mapping the prefix mapping
300     * @param ns the namespace
301     * @param compact the boolean value of compact
302     * @return namespace prefix
303     */
304    public String getNamespacePrefix(final PrefixMapping mapping,
305            final String ns, final boolean compact) {
306        final String nsURIPrefix = mapping.getNsURIPrefix(ns);
307        if (nsURIPrefix == null) {
308            if (compact) {
309                final int hashIdx = ns.lastIndexOf('#');
310                final int split = hashIdx > 0 ? ns.substring(0, hashIdx).lastIndexOf('/') : ns.lastIndexOf('/');
311                return split > 0 ? "..." + ns.substring(split) : ns;
312            }
313            return ns;
314        }
315        return nsURIPrefix + ":";
316    }
317
318    /**
319     * Get a prefix preamble appropriate for a SPARQL-UPDATE query from a prefix
320     * mapping object
321     *
322     * @param mapping the prefix mapping
323     * @return prefix preamble
324     */
325    public String getPrefixPreamble(final PrefixMapping mapping) {
326        return mapping.getNsPrefixMap().entrySet().stream()
327                .map(e -> "PREFIX " + e.getKey() + ": <" + e.getValue() + ">").collect(joining("\n", "", "\n\n"));
328    }
329
330    /**
331     * Determines whether the subject is kind of RDF resource
332     * @param graph the graph
333     * @param subject the subject
334     * @param namespace the namespace
335     * @param resource the resource
336     * @return whether the subject is kind of RDF resource
337     */
338    public boolean isRdfResource(final Graph graph,
339                                 final Node subject,
340                                 final String namespace,
341                                 final String resource) {
342        LOGGER.trace("Is RDF Resource? s:{}, ns:{}, r:{}, g:{}", subject, namespace, resource, graph);
343        return graph.find(subject, type.asNode(),
344                createResource(namespace + resource).asNode()).hasNext();
345    }
346
347    /**
348     * Convert a URI string to an RDF node
349     *
350     * @param r the uri string
351     * @return RDF node representation of the given string
352     */
353    public Node asLiteralStringNode(final String r) {
354        return ResourceFactory.createPlainLiteral(r).asNode();
355    }
356
357    /**
358     * Yes, we really did create a method to increment
359     * a given int. You can't do math in a velocity template.
360     *
361     * @param i the given integer
362     * @return maths
363     */
364    public int addOne(final int i) {
365        return i + 1;
366    }
367
368    /**
369     * Proxying access to the RDF type static property
370     * @return RDF type property
371     */
372    public Property rdfType() {
373        return RDF.type;
374    }
375
376    /**
377     * Proxying access to the RDFS domain static property
378     * @return RDFS domain property
379     */
380    public Property rdfsDomain() {
381        return RDFS.domain;
382    }
383
384    /**
385     * Proxying access to the RDFS class static property
386     * @return RDFS class resource
387     */
388    public Resource rdfsClass() {
389        return RDFS.Class;
390    }
391
392    /**
393     * Get the content-bearing node for the given subject
394     * @param subject the subject
395     * @return content-bearing node for the given subject
396     */
397    public static Node getContentNode(final Node subject) {
398        return subject == null ? null : NodeFactory.createURI(subject.getURI().replace("/" + FCR_METADATA, ""));
399    }
400
401    /**
402     * Create a URI Node from the provided String
403     *
404     * @param uri from which a URI Node will be created
405     * @return URI Node
406     */
407    public static Node createURI(final String uri) {
408        return NodeFactory.createURI(uri);
409    }
410
411    /**
412     * Transform a source string to something appropriate for HTML ids
413     * @param source the source string
414     * @return transformed source string
415     */
416    public static String parameterize(final String source) {
417        return source.toLowerCase().replaceAll("[^a-z0-9\\-_]+", "_");
418    }
419
420    /**
421     * Test if a Predicate is managed
422     * @param property the property
423     * @return whether the property is managed
424     */
425    public static boolean isManagedProperty(final Node property) {
426        return property.isURI() && isManagedPredicate.test(createProperty(property.getURI()));
427    }
428}