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.collect.Lists.newArrayList;
019import static com.hp.hpl.jena.graph.GraphUtil.listObjects;
020import static com.hp.hpl.jena.graph.NodeFactory.createURI;
021import static com.hp.hpl.jena.rdf.model.ResourceFactory.createResource;
022import static com.hp.hpl.jena.rdf.model.ResourceFactory.createProperty;
023import static java.util.Arrays.asList;
024import static java.util.Arrays.stream;
025import static java.util.Collections.emptyMap;
026import static java.util.stream.Collectors.joining;
027import static java.util.stream.Collectors.toMap;
028import static org.fcrepo.kernel.api.FedoraTypes.FCR_METADATA;
029import static org.fcrepo.kernel.api.RdfLexicon.CREATED_DATE;
030import static org.fcrepo.kernel.api.RdfLexicon.DC_TITLE;
031import static org.fcrepo.kernel.api.RdfLexicon.DCTERMS_TITLE;
032import static org.fcrepo.kernel.api.RdfLexicon.HAS_CHILD_COUNT;
033import static org.fcrepo.kernel.api.RdfLexicon.HAS_VERSION_LABEL;
034import static org.fcrepo.kernel.api.RdfLexicon.RDFS_LABEL;
035import static org.fcrepo.kernel.api.RdfLexicon.REPOSITORY_NAMESPACE;
036import static org.fcrepo.kernel.api.RdfLexicon.SKOS_PREFLABEL;
037import static org.fcrepo.kernel.api.RdfLexicon.WRITABLE;
038import static org.fcrepo.kernel.api.RdfLexicon.HAS_VERSION;
039import static org.fcrepo.kernel.api.RdfLexicon.RDF_NAMESPACE;
040import static org.fcrepo.kernel.api.RdfLexicon.DC_NAMESPACE;
041import static org.slf4j.LoggerFactory.getLogger;
042
043import java.text.SimpleDateFormat;
044import java.util.Date;
045import java.util.Iterator;
046import java.util.LinkedHashMap;
047import java.util.List;
048import java.util.Map;
049import java.util.Optional;
050import java.util.StringJoiner;
051import javax.ws.rs.core.UriInfo;
052
053import com.hp.hpl.jena.graph.Graph;
054import com.hp.hpl.jena.graph.Triple;
055import com.hp.hpl.jena.graph.impl.LiteralLabel;
056
057import org.fcrepo.http.commons.api.rdf.TripleOrdering;
058import org.slf4j.Logger;
059
060import com.hp.hpl.jena.graph.Node;
061import com.hp.hpl.jena.rdf.model.Model;
062import com.hp.hpl.jena.rdf.model.Property;
063import com.hp.hpl.jena.rdf.model.Resource;
064import com.hp.hpl.jena.rdf.model.ResourceFactory;
065import com.hp.hpl.jena.shared.PrefixMapping;
066import com.hp.hpl.jena.vocabulary.RDF;
067import com.hp.hpl.jena.vocabulary.RDFS;
068
069/**
070 * General view helpers for rendering HTML responses
071 *
072 * @author awoods
073 * @author ajs6f
074 */
075public class ViewHelpers {
076
077    private static final Logger LOGGER = getLogger(ViewHelpers.class);
078
079    private static ViewHelpers instance = null;
080
081    private static final List<Property>  TITLE_PROPERTIES = asList(RDFS_LABEL, DC_TITLE, DCTERMS_TITLE, SKOS_PREFLABEL);
082
083    private static final Property DC_FORMAT = createProperty(DC_NAMESPACE + "format");
084
085    private static final String DEFAULT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'").format(new Date());
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        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 g the graph
116     * @param subject the subject
117     * @param predicate the predicate
118     * @return iterator
119     */
120    public Iterator<Node> getOrderedVersions(final Graph g, final Node subject, final Resource predicate) {
121        final List<Node> vs = listObjects(g, subject, predicate.asNode()).toList();
122        vs.sort((v1, v2) -> getVersionDate(g, v1).orElse(DEFAULT).compareTo(getVersionDate(g, v2).orElse(DEFAULT)));
123        return vs.iterator();
124    }
125
126    /**
127     * Gets the URL of the node whose version is represented by the
128     * current node.  The current implementation assumes the URI
129     * of that node will be the same as the breadcrumb entry that
130     * precedes one with the path "fcr:versions".
131     * @param uriInfo the uri info
132     * @param subject the subject
133     * @return the URL of the node
134     */
135     public String getVersionSubjectUrl(final UriInfo uriInfo, final Node subject) {
136        final Map<String, String> breadcrumbs = getNodeBreadcrumbs(uriInfo, subject);
137        String lastUrl = null;
138        for (final Map.Entry<String, String> entry : breadcrumbs.entrySet()) {
139            if (entry.getValue().equals("fcr:versions")) {
140                return lastUrl;
141            }
142            lastUrl = entry.getKey();
143        }
144        return null;
145     }
146
147    /**
148     * Gets a version label of a subject from the graph
149     *
150     * @param graph the graph
151     * @param subject the subject
152     * @return the label of the version if one has been provided
153     */
154    public Optional<String> getVersionLabel(final Graph graph, final Node subject) {
155        return getValue(graph, subject, HAS_VERSION_LABEL.asNode());
156    }
157
158    /**
159     * Gets a modification date of a subject from the graph
160     *
161     * @param graph the graph
162     * @param subject the subject
163     * @return the modification date if it exists
164     */
165    public Optional<String> getVersionDate(final Graph graph, final Node subject) {
166        return getValue(graph, subject, CREATED_DATE.asNode());
167    }
168
169    private static Optional<String> getValue(final Graph graph, final Node subject, final Node predicate) {
170        final Iterator<Node> objects = listObjects(graph, subject, predicate);
171        return Optional.ofNullable(objects.hasNext() ? objects.next().getLiteralValue().toString() : null);
172    }
173
174    /**
175     * Get the canonical title of a subject from the graph
176     *
177     * @param graph the graph
178     * @param sub the subject
179     * @return canonical title of the subject in the graph
180     */
181    public String getObjectTitle(final Graph graph, final Node sub) {
182        if (sub == null) {
183            return "";
184        }
185        final Optional<String> title = TITLE_PROPERTIES.stream().map(Property::asNode).flatMap(p -> listObjects(
186                graph, sub, p).toList().stream()).filter(Node::isLiteral).map(Node::getLiteral).map(
187                        LiteralLabel::toString).findFirst();
188        return title.orElse(sub.isURI() ? sub.getURI() : sub.isBlank() ? sub.getBlankNodeLabel() : sub.toString());
189    }
190
191    /**
192     * Take a HAS_SERIALIZATION node and find the RDFS_LABEL for the format it is associated with
193     *
194     * @param graph the graph
195     * @param subject the subject
196     * @return the label for the serialization format
197     */
198    public Optional<String> getSerializationTitle(final Graph graph, final Node subject) {
199        final Iterator<Node> formats = listObjects(graph, subject, DC_FORMAT.asNode());
200        return Optional.ofNullable(formats.hasNext() ? getObjectTitle(graph, formats.next()) : null);
201    }
202
203    /**
204     * Determines whether the subject is writable
205     * true if node is writable
206     * @param graph the graph
207     * @param subject the subject
208     * @return whether the subject is writable
209     */
210    public boolean isWritable(final Graph graph, final Node subject) {
211        return getValue(graph, subject, WRITABLE.asNode()).filter("true"::equals).isPresent();
212    }
213
214    /**
215     * Determines whether the subject is of type fedora:Version.
216     * true if node has type fedora:Version
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((REPOSITORY_NAMESPACE + "Version")::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     * Get the number of child resources associated with the arg 'subject' as specified by the triple found in the arg
253     * 'graph' with the predicate RdfLexicon.HAS_CHILD_COUNT.
254     *
255     * @param graph   of triples
256     * @param subject for which child resources is sought
257     * @return number of child resources
258     */
259    public int getNumChildren(final Graph graph, final Node subject) {
260        LOGGER.trace("Getting number of children: s:{}, g:{}", subject, graph);
261        final Iterator<Node> iterator = listObjects(graph, subject, HAS_CHILD_COUNT.asNode());
262        if (iterator.hasNext()) {
263            final Node obj = iterator.next();
264            if (obj.isLiteral()) {
265                final String lit = obj.getLiteralValue().toString();
266                return lit.isEmpty() ? 0 : Integer.parseInt(lit);
267            }
268        }
269        return 0;
270    }
271
272    /**
273     * Generate url to local name breadcrumbs for a given node's tree
274     *
275     * @param uriInfo the uri info
276     * @param subject the subject
277     * @return breadcrumbs
278     */
279    public Map<String, String> getNodeBreadcrumbs(final UriInfo uriInfo,
280            final Node subject) {
281        final String topic = subject.getURI();
282
283        LOGGER.trace("Generating breadcrumbs for subject {}", subject);
284        final String baseUri = uriInfo.getBaseUri().toString();
285
286        if (!topic.startsWith(baseUri)) {
287            LOGGER.trace("Topic wasn't part of our base URI {}", baseUri);
288            return emptyMap();
289        }
290
291        final String salientPath = topic.substring(baseUri.length());
292        final StringJoiner cumulativePath = new StringJoiner("/");
293        return stream(salientPath.split("/")).filter(seg -> !seg.isEmpty()).collect(toMap(seg -> uriInfo
294                .getBaseUriBuilder().path(cumulativePath.add(seg).toString())
295                .build().toString(), seg -> seg, (u, v) -> null, LinkedHashMap::new));
296    }
297
298    /**
299     * Sort a Iterator of Triples alphabetically by its subject, predicate, and
300     * object
301     *
302     * @param model the model
303     * @param it the iterator of triples
304     * @return iterator of alphabetized triples
305     */
306    public List<Triple> getSortedTriples(final Model model, final Iterator<Triple> it) {
307        final List<Triple> triples = newArrayList(it);
308        triples.sort(new TripleOrdering(model));
309        return triples;
310    }
311
312    /**
313     * Get the namespace prefix (or the namespace URI itself, if no prefix is
314     * available) from a prefix mapping
315     *
316     * @param mapping the prefix mapping
317     * @param ns the namespace
318     * @param compact the boolean value of compact
319     * @return namespace prefix
320     */
321    public String getNamespacePrefix(final PrefixMapping mapping,
322            final String ns, final boolean compact) {
323        final String nsURIPrefix = mapping.getNsURIPrefix(ns);
324        if (nsURIPrefix == null) {
325            if (compact) {
326                final int hashIdx = ns.lastIndexOf('#');
327                final int split = hashIdx > 0 ? ns.substring(0, hashIdx).lastIndexOf('/') : ns.lastIndexOf('/');
328                return split > 0 ? "..." + ns.substring(split) : ns;
329            }
330            return ns;
331        }
332        return nsURIPrefix + ":";
333    }
334
335    /**
336     * Get a prefix preamble appropriate for a SPARQL-UPDATE query from a prefix
337     * mapping object
338     *
339     * @param mapping the prefix mapping
340     * @return prefix preamble
341     */
342    public String getPrefixPreamble(final PrefixMapping mapping) {
343        return mapping.getNsPrefixMap().entrySet().stream()
344                .map(e -> "PREFIX " + e.getKey() + ": <" + e.getValue() + ">").collect(joining("\n", "", "\n\n"));
345    }
346
347    /**
348     * Determines whether the subject is kind of RDF resource
349     * @param graph the graph
350     * @param subject the subject
351     * @param namespace the namespace
352     * @param resource the resource
353     * @return whether the subject is kind of RDF resource
354     */
355    public boolean isRdfResource(final Graph graph,
356                                 final Node subject,
357                                 final String namespace,
358                                 final String resource) {
359        LOGGER.trace("Is RDF Resource? s:{}, ns:{}, r:{}, g:{}", subject, namespace, resource, graph);
360        return graph.find(subject, createResource(RDF_NAMESPACE + "type").asNode(),
361                createResource(namespace + resource).asNode()).hasNext();
362    }
363
364    /**
365     * Convert a URI string to an RDF node
366     *
367     * @param r the uri string
368     * @return RDF node representation of the given string
369     */
370    public Node asLiteralStringNode(final String r) {
371        return ResourceFactory.createPlainLiteral(r).asNode();
372    }
373
374    /**
375     * Yes, we really did create a method to increment
376     * a given int. You can't do math in a velocity template.
377     *
378     * @param i the given integer
379     * @return maths
380     */
381    public int addOne(final int i) {
382        return i + 1;
383    }
384
385    /**
386     * Proxying access to the RDF type static property
387     * @return RDF type property
388     */
389    public Property rdfType() {
390        return RDF.type;
391    }
392
393    /**
394     * Proxying access to the RDFS domain static property
395     * @return RDFS domain property
396     */
397    public Property rdfsDomain() {
398        return RDFS.domain;
399    }
400
401    /**
402     * Proxying access to the RDFS class static property
403     * @return RDFS class resource
404     */
405    public Resource rdfsClass() {
406        return RDFS.Class;
407    }
408
409    /**
410     * Get the content-bearing node for the given subject
411     * @param subject the subject
412     * @return content-bearing node for the given subject
413     */
414    public static Node getContentNode(final Node subject) {
415        return subject == null ? null : createURI(subject.getURI().replace("/" + FCR_METADATA, ""));
416    }
417
418    /**
419     * Transform a source string to something appropriate for HTML ids
420     * @param source the source string
421     * @return transformed source string
422     */
423    public static String parameterize(final String source) {
424        return source.toLowerCase().replaceAll("[^a-z0-9\\-_]+", "_");
425    }
426}