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.rdf.model.ResourceFactory.createResource;
022import static com.hp.hpl.jena.rdf.model.ResourceFactory.createProperty;
023import static java.util.stream.Collectors.joining;
024import static org.fcrepo.kernel.api.RdfLexicon.CREATED_DATE;
025import static org.fcrepo.kernel.api.FedoraJcrTypes.FCR_METADATA;
026import static org.fcrepo.kernel.api.RdfLexicon.DC_TITLE;
027import static org.fcrepo.kernel.api.RdfLexicon.DCTERMS_TITLE;
028import static org.fcrepo.kernel.api.RdfLexicon.HAS_VERSION_LABEL;
029import static org.fcrepo.kernel.api.RdfLexicon.RDFS_LABEL;
030import static org.fcrepo.kernel.api.RdfLexicon.SKOS_PREFLABEL;
031import static org.fcrepo.kernel.api.RdfLexicon.HAS_VERSION;
032import static org.fcrepo.kernel.api.RdfLexicon.RDF_NAMESPACE;
033import static org.fcrepo.kernel.api.RdfLexicon.DC_NAMESPACE;
034import static org.slf4j.LoggerFactory.getLogger;
035
036import java.text.SimpleDateFormat;
037import java.util.Date;
038import java.util.Iterator;
039import java.util.List;
040import java.util.Map;
041import java.util.TreeMap;
042import javax.ws.rs.core.UriInfo;
043
044import com.hp.hpl.jena.graph.Graph;
045import com.hp.hpl.jena.graph.Triple;
046
047import org.fcrepo.http.commons.api.rdf.TripleOrdering;
048import org.fcrepo.kernel.api.RdfLexicon;
049
050import org.slf4j.Logger;
051
052import com.google.common.collect.ImmutableMap;
053import com.hp.hpl.jena.graph.Node;
054import com.hp.hpl.jena.graph.NodeFactory;
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        final Iterator<Triple> iterator = getObjects(graph, subject, predicate);
284
285        if (iterator.hasNext()) {
286            final Node object = iterator.next().getObject();
287
288            if (object.isLiteral()) {
289                final String s = object.getLiteralValue().toString();
290                if (s.isEmpty()) {
291                    return "<empty>";
292                }
293                return s;
294            }
295            if (uriAsLink) {
296                return "&lt;<a href=\"" + object.getURI() + "\">" +
297                           object.getURI() + "</a>&gt;";
298            }
299            return object.getURI();
300        }
301        return "";
302    }
303
304    /**
305     * Generate url to local name breadcrumbs for a given node's tree
306     *
307     * @param uriInfo the uri info
308     * @param subject the subject
309     * @return breadcrumbs
310     */
311    public Map<String, String> getNodeBreadcrumbs(final UriInfo uriInfo,
312            final Node subject) {
313        final String topic = subject.getURI();
314
315        LOGGER.trace("Generating breadcrumbs for subject {}", subject);
316        final ImmutableMap.Builder<String, String> builder =
317                ImmutableMap.builder();
318
319        final String baseUri = uriInfo.getBaseUri().toString();
320
321        if (!topic.startsWith(baseUri)) {
322            LOGGER.trace("Topic wasn't part of our base URI {}", baseUri);
323            return builder.build();
324        }
325
326        final String salientPath = topic.substring(baseUri.length());
327
328        final String[] split = salientPath.split("/");
329
330        final StringBuilder cumulativePath = new StringBuilder();
331
332        for (final String path : split) {
333
334            if (path.isEmpty()) {
335                continue;
336            }
337
338            cumulativePath.append(path);
339
340            final String uri =
341                    uriInfo.getBaseUriBuilder().path(cumulativePath.toString())
342                            .build().toString();
343
344            LOGGER.trace("Adding breadcrumb for path segment {} => {}", path,
345                    uri);
346
347            builder.put(uri, path);
348
349            cumulativePath.append("/");
350
351        }
352
353        return builder.build();
354
355    }
356
357    /**
358     * Sort a Iterator of Triples alphabetically by its subject, predicate, and
359     * object
360     *
361     * @param model the model
362     * @param it the iterator of triples
363     * @return iterator of alphabetized triples
364     */
365    public List<Triple> getSortedTriples(final Model model, final Iterator<Triple> it) {
366        final List<Triple> triples = newArrayList(it);
367        triples.sort(new TripleOrdering(model));
368        return triples;
369    }
370
371    /**
372     * Get the namespace prefix (or the namespace URI itself, if no prefix is
373     * available) from a prefix mapping
374     *
375     * @param mapping the prefix mapping
376     * @param namespace the namespace
377     * @param compact the boolean value of compact
378     * @return namespace prefix
379     */
380    public String getNamespacePrefix(final PrefixMapping mapping,
381            final String namespace, final boolean compact) {
382        final String nsURIPrefix = mapping.getNsURIPrefix(namespace);
383
384        if (nsURIPrefix == null) {
385            if (compact) {
386                final int hashIdx = namespace.lastIndexOf('#');
387
388                final int split;
389
390                if (hashIdx > 0) {
391                    split = namespace.substring(0, hashIdx).lastIndexOf('/');
392                } else {
393                    split = namespace.lastIndexOf('/');
394                }
395
396                if (split > 0) {
397                    return "..." + namespace.substring(split);
398                }
399                return namespace;
400            }
401            return namespace;
402        }
403        return nsURIPrefix + ":";
404    }
405
406    /**
407     * Get a prefix preamble appropriate for a SPARQL-UPDATE query from a prefix
408     * mapping object
409     *
410     * @param mapping the prefix mapping
411     * @return prefix preamble
412     */
413    public String getPrefixPreamble(final PrefixMapping mapping) {
414        return mapping.getNsPrefixMap().entrySet().stream()
415                .map(e -> "PREFIX " + e.getKey() + ": <" + e.getValue() + ">").collect(joining("\n", "", "\n\n"));
416    }
417
418    /**
419     * Determines whether the subject is kind of RDF resource
420     * @param graph the graph
421     * @param subject the subject
422     * @param namespace the namespace
423     * @param resource the resource
424     * @return whether the subject is kind of RDF resource
425     */
426    public boolean isRdfResource(final Graph graph,
427                                 final Node subject,
428                                 final String namespace,
429                                 final String resource) {
430        return graph.find(subject, createResource(RDF_NAMESPACE + "type").asNode(),
431                createResource(namespace + resource).asNode()).hasNext();
432    }
433
434    /**
435     * Convert an RDF resource to an RDF node
436     *
437     * @param r the resource
438     * @return RDF node representation of the given RDF resource
439     */
440    public Node asNode(final Resource r) {
441        return r.asNode();
442    }
443
444    /**
445     * Convert a URI string to an RDF node
446     *
447     * @param r the uri string
448     * @return RDF node representation of the given string
449     */
450    public Node asLiteralStringNode(final String r) {
451        return ResourceFactory.createPlainLiteral(r).asNode();
452    }
453
454    /**
455     * Yes, we really did create a method to increment
456     * a given int. You can't do math in a velocity template.
457     *
458     * @param i the given integer
459     * @return maths
460     */
461    public int addOne(final int i) {
462        return i + 1;
463    }
464
465    /**
466     * Proxying access to the RDF type static property
467     * @return RDF type property
468     */
469    public Property rdfType() {
470        return RDF.type;
471    }
472
473    /**
474     * Proxying access to the RDFS domain static property
475     * @return RDFS domain property
476     */
477    public Property rdfsDomain() {
478        return RDFS.domain;
479    }
480
481    /**
482     * Proxying access to the RDFS class static property
483     * @return RDFS class resource
484     */
485    public Resource rdfsClass() {
486        return RDFS.Class;
487    }
488
489    /**
490     * Get the content-bearing node for the given subject
491     * @param subject the subject
492     * @return content-bearing node for the given subject
493     */
494    public Node getContentNode(final Node subject) {
495        return NodeFactory.createURI(subject.getURI().replace(FCR_METADATA, ""));
496    }
497
498    /**
499     * Transform a source string to something appropriate for HTML ids
500     * @param source the source string
501     * @return transformed source string
502     */
503    public String parameterize(final String source) {
504        return source.toLowerCase().replaceAll("[^a-z0-9\\-_]+", "_");
505    }
506}