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.hp.hpl.jena.graph.Node.ANY;
020import static com.hp.hpl.jena.rdf.model.ResourceFactory.createResource;
021import static com.hp.hpl.jena.rdf.model.ResourceFactory.createProperty;
022import static org.fcrepo.kernel.RdfLexicon.CREATED_DATE;
023import static org.fcrepo.kernel.FedoraJcrTypes.FCR_METADATA;
024import static org.fcrepo.kernel.RdfLexicon.DC_TITLE;
025import static org.fcrepo.kernel.RdfLexicon.DCTERMS_TITLE;
026import static org.fcrepo.kernel.RdfLexicon.HAS_VERSION_LABEL;
027import static org.fcrepo.kernel.RdfLexicon.RDFS_LABEL;
028import static org.fcrepo.kernel.RdfLexicon.SKOS_PREFLABEL;
029import static org.fcrepo.kernel.RdfLexicon.HAS_VERSION;
030import static org.fcrepo.kernel.RdfLexicon.RDF_NAMESPACE;
031import static org.fcrepo.kernel.RdfLexicon.DC_NAMESPACE;
032import static org.slf4j.LoggerFactory.getLogger;
033
034import java.text.SimpleDateFormat;
035import java.util.Date;
036import java.util.Iterator;
037import java.util.List;
038import java.util.Map;
039import java.util.TreeMap;
040
041import javax.ws.rs.core.UriInfo;
042
043import com.hp.hpl.jena.graph.Graph;
044import com.hp.hpl.jena.graph.Triple;
045import org.fcrepo.http.commons.api.rdf.TripleOrdering;
046import org.fcrepo.kernel.RdfLexicon;
047import org.slf4j.Logger;
048
049import com.google.common.collect.ImmutableList;
050import com.google.common.collect.ImmutableMap;
051import com.google.common.collect.Ordering;
052import com.hp.hpl.jena.graph.Node;
053import com.hp.hpl.jena.graph.NodeFactory;
054import com.hp.hpl.jena.rdf.model.Model;
055import com.hp.hpl.jena.rdf.model.Property;
056import com.hp.hpl.jena.rdf.model.Resource;
057import com.hp.hpl.jena.rdf.model.ResourceFactory;
058import com.hp.hpl.jena.shared.PrefixMapping;
059import com.hp.hpl.jena.vocabulary.RDF;
060import com.hp.hpl.jena.vocabulary.RDFS;
061
062/**
063 * General view helpers for rendering HTML responses
064 *
065 * @author awoods
066 */
067public class ViewHelpers {
068
069    private static final Logger LOGGER = getLogger(ViewHelpers.class);
070
071    private static ViewHelpers instance = null;
072
073    protected ViewHelpers() {
074        // Exists only to defeat instantiation.
075    }
076
077    /**
078     * ViewHelpers are singletons. Initialize or return the existing object
079     * @return an instance of ViewHelpers
080     */
081    public static ViewHelpers getInstance() {
082        if (instance == null) {
083            instance = new ViewHelpers();
084        }
085        return instance;
086    }
087
088    /**
089     * Return an iterator of Triples that match the given subject and predicate
090     *
091     * @param graph the graph
092     * @param subject the subject
093     * @param predicate the predicate
094     * @return iterator
095     */
096    public Iterator<Triple> getObjects(final Graph graph,
097        final Node subject, final Resource predicate) {
098        return graph.find(subject, predicate.asNode(), ANY);
099    }
100
101    /**
102     * Return an iterator of Triples for versions.
103     *
104     * @param graph the graph
105     * @param subject the subject
106     * @return iterator
107     */
108    public Iterator<Node> getVersions(final Graph graph,
109        final Node subject) {
110        return getOrderedVersions(graph, subject, HAS_VERSION);
111    }
112
113    /**
114     * Return an iterator of Triples for versions in order that
115     * they were created.
116     *
117     * @param graph the graph
118     * @param subject the subject
119     * @param predicate the predicate
120     * @return iterator
121     */
122    public Iterator<Node> getOrderedVersions(final Graph graph,
123        final Node subject, final Resource predicate) {
124        final Iterator<Triple> versions = getObjects(graph, subject, predicate);
125        final Map<String, Node> map = new TreeMap<>();
126        final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
127        Triple triple;
128        String date;
129        while (versions.hasNext()) {
130            triple = versions.next();
131            date = getVersionDate(graph, triple.getObject());
132            String key = isNullOrEmpty(date) ? format.format(new Date()) : date;
133            while (map.containsKey(key)) {
134                key = key + "1";
135            }
136            map.put(key, triple.getObject());
137        }
138        return map.values().iterator();
139    }
140
141    /**
142     * Gets the URL of the node whose version is represented by the
143     * current node.  The current implementation assumes the URI
144     * of that node will be the same as the breadcrumb entry that
145     * precedes one with the path "fcr:versions".
146     * @param uriInfo the uri info
147     * @param subject the subject
148     * @return the URL of the node
149     */
150     public String getVersionSubjectUrl(final UriInfo uriInfo, final Node subject) {
151        final Map<String, String> breadcrumbs = getNodeBreadcrumbs(uriInfo, subject);
152        String lastUrl = null;
153        for (final Map.Entry<String, String> entry : breadcrumbs.entrySet()) {
154            if (entry.getValue().equals("fcr:versions")) {
155                return lastUrl;
156            }
157            lastUrl = entry.getKey();
158        }
159        return null;
160     }
161
162    /**
163     * Gets a version label of a subject from the graph
164     *
165     * @param graph the graph
166     * @param subject the subject
167     * @param defaultValue a value to be returned if no label is present in the
168     *                     graph
169     * @return the label of the version if one has been provided; otherwise
170     * the default is returned
171     */
172    public String getVersionLabel(final Graph graph,
173                                 final Node subject, final String defaultValue) {
174        final Iterator<Triple> objects = getObjects(graph, subject, HAS_VERSION_LABEL);
175        if (objects.hasNext()) {
176            return objects.next().getObject().getLiteralValue().toString();
177        }
178        return defaultValue;
179    }
180
181    /**
182     * Gets a modification date of a subject from the graph
183     *
184     * @param graph the graph
185     * @param subject the subject
186     * @return the modification date or null if none exists
187     */
188    public String getVersionDate(final Graph graph,
189                                 final Node subject) {
190        final Iterator<Triple> objects = getObjects(graph, subject, CREATED_DATE);
191        if (objects.hasNext()) {
192            return objects.next().getObject().getLiteralValue().toString();
193        }
194        return "";
195    }
196
197    /**
198     * Get the canonical title of a subject from the graph
199     *
200     * @param graph the graph
201     * @param subject the subject
202     * @return canonical title of the subject in the graph
203     */
204    public String getObjectTitle(final Graph graph, final Node subject) {
205
206        if (subject == null) {
207            return "";
208        }
209
210        final Property[] properties = new Property[] {RDFS_LABEL, DC_TITLE, DCTERMS_TITLE, SKOS_PREFLABEL};
211
212        for (final Property p : properties) {
213            final Iterator<Triple> objects = getObjects(graph, subject, p);
214
215            if (objects.hasNext()) {
216                return objects.next().getObject().getLiteral().getLexicalForm();
217            }
218        }
219
220        if (subject.isURI()) {
221            return subject.getURI();
222        } else if (subject.isBlank()) {
223            return subject.getBlankNodeLabel();
224        } else {
225            return subject.toString();
226        }
227
228    }
229
230    /**
231     * Take a HAS_SERIALIZATION node and find the RDFS_LABEL for the format it is associated with
232     *
233     * @param graph the graph
234     * @param subject the subject
235     * @return the label for the serialization format
236     */
237    public String getSerializationTitle(final Graph graph, final Node subject) {
238        final Property dcFormat = createProperty(DC_NAMESPACE + "format");
239        final Iterator<Triple> formatRDFs = getObjects(graph, subject, dcFormat);
240        if (formatRDFs.hasNext()) {
241            return getObjectTitle(graph, formatRDFs.next().getObject());
242        }
243        return "";
244    }
245
246    /**
247     * Determines whether the subject is writable
248     * true if node is writable
249     * @param graph the graph
250     * @param subject the subject
251     * @return whether the subject is writable
252     */
253    public boolean isWritable(final Graph graph, final Node subject) {
254        final Iterator<Triple> it = getObjects(graph, subject, RdfLexicon.WRITABLE);
255        return it.hasNext() && it.next().getObject().getLiteralValue().toString().equals("true");
256    }
257
258    /**
259     * Determines whether the subject is of type nt:frozenNode.
260     * true if node has type nt:frozen
261     * @param graph the graph
262     * @param subject the subject
263     * @return whether the subject is frozen node
264     */
265    public boolean isFrozenNode(final Graph graph, final Node subject) {
266        final Iterator<Triple> objects = getObjects(graph, subject, RdfLexicon.HAS_PRIMARY_TYPE);
267        return objects.hasNext()
268                && objects.next().getObject()
269                .getLiteralValue().toString().equals("nt:frozenNode");
270    }
271
272    /**
273     * Get the string version of the object that matches the given subject and
274     * predicate
275     *
276     * @param graph the graph
277     * @param subject the subject
278     * @param predicate the predicate
279     * @param uriAsLink the boolean value of uri as link
280     * @return string version of the object
281     */
282    public String getObjectsAsString(final Graph graph,
283            final Node subject, final Resource predicate, final boolean uriAsLink) {
284        final Iterator<Triple> iterator = getObjects(graph, subject, predicate);
285
286        if (iterator.hasNext()) {
287            final Node object = iterator.next().getObject();
288
289            if (object.isLiteral()) {
290                final String s = object.getLiteralValue().toString();
291                if (s.isEmpty()) {
292                    return "<empty>";
293                }
294                return s;
295            }
296            if (uriAsLink) {
297                return "&lt;<a href=\"" + object.getURI() + "\">" +
298                           object.getURI() + "</a>&gt;";
299            }
300            return object.getURI();
301        }
302        return "";
303    }
304
305    /**
306     * Generate url to local name breadcrumbs for a given node's tree
307     *
308     * @param uriInfo the uri info
309     * @param subject the subject
310     * @return breadcrumbs
311     */
312    public Map<String, String> getNodeBreadcrumbs(final UriInfo uriInfo,
313            final Node subject) {
314        final String topic = subject.getURI();
315
316        LOGGER.trace("Generating breadcrumbs for subject {}", subject);
317        final ImmutableMap.Builder<String, String> builder =
318                ImmutableMap.builder();
319
320        final String baseUri = uriInfo.getBaseUri().toString();
321
322        if (!topic.startsWith(baseUri)) {
323            LOGGER.trace("Topic wasn't part of our base URI {}", baseUri);
324            return builder.build();
325        }
326
327        final String salientPath = topic.substring(baseUri.length());
328
329        final String[] split = salientPath.split("/");
330
331        final StringBuilder cumulativePath = new StringBuilder();
332
333        for (final String path : split) {
334
335            if (path.isEmpty()) {
336                continue;
337            }
338
339            cumulativePath.append(path);
340
341            final String uri =
342                    uriInfo.getBaseUriBuilder().path(cumulativePath.toString())
343                            .build().toString();
344
345            LOGGER.trace("Adding breadcrumb for path segment {} => {}", path,
346                    uri);
347
348            builder.put(uri, path);
349
350            cumulativePath.append("/");
351
352        }
353
354        return builder.build();
355
356    }
357
358    /**
359     * Sort a Iterator of Triples alphabetically by its subject, predicate, and
360     * object
361     *
362     * @param model the model
363     * @param it the iterator of triples
364     * @return iterator of alphabetized triples
365     */
366    public List<Triple> getSortedTriples(final Model model, final Iterator<Triple> it) {
367        return Ordering.from(new TripleOrdering(model)).sortedCopy(ImmutableList.copyOf(it));
368    }
369
370    /**
371     * Get the namespace prefix (or the namespace URI itself, if no prefix is
372     * available) from a prefix mapping
373     *
374     * @param mapping the prefix mapping
375     * @param namespace the namespace
376     * @param compact the boolean value of compact
377     * @return namespace prefix
378     */
379    public String getNamespacePrefix(final PrefixMapping mapping,
380            final String namespace, final boolean compact) {
381        final String nsURIPrefix = mapping.getNsURIPrefix(namespace);
382
383        if (nsURIPrefix == null) {
384            if (compact) {
385                final int hashIdx = namespace.lastIndexOf('#');
386
387                final int split;
388
389                if (hashIdx > 0) {
390                    split = namespace.substring(0, hashIdx).lastIndexOf('/');
391                } else {
392                    split = namespace.lastIndexOf('/');
393                }
394
395                if (split > 0) {
396                    return "..." + namespace.substring(split);
397                }
398                return namespace;
399            }
400            return namespace;
401        }
402        return nsURIPrefix + ":";
403    }
404
405    /**
406     * Get a prefix preamble appropriate for a SPARQL-UPDATE query from a prefix
407     * mapping object
408     *
409     * @param mapping the prefix mapping
410     * @return prefix preamble
411     */
412    public String getPrefixPreamble(final PrefixMapping mapping) {
413        final StringBuilder sb = new StringBuilder();
414
415        final Map<String, String> nsPrefixMap = mapping.getNsPrefixMap();
416
417        for (final Map.Entry<String, String> entry : nsPrefixMap.entrySet()) {
418            sb.append("PREFIX " + entry.getKey() + ": <" + entry.getValue() +
419                    ">\n");
420        }
421
422        sb.append("\n");
423        return sb.toString();
424    }
425
426    /**
427     * Determines whether the subject is kind of RDF resource
428     * @param graph the graph
429     * @param subject the subject
430     * @param namespace the namespace
431     * @param resource the resource
432     * @return whether the subject is kind of RDF resource
433     */
434    public boolean isRdfResource(final Graph graph,
435                                 final Node subject,
436                                 final String namespace,
437                                 final String resource) {
438        final Iterator<Triple> it = graph.find(subject,
439                                               createResource(RDF_NAMESPACE + "type").asNode(),
440                                               createResource(namespace + resource).asNode());
441        return it.hasNext();
442    }
443
444    /**
445     * Convert an RDF resource to an RDF node
446     *
447     * @param r the resource
448     * @return RDF node representation of the given RDF resource
449     */
450    public Node asNode(final Resource r) {
451        return r.asNode();
452    }
453
454    /**
455     * Convert a URI string to an RDF node
456     *
457     * @param r the uri string
458     * @return RDF node representation of the given string
459     */
460    public Node asLiteralStringNode(final String r) {
461        return ResourceFactory.createPlainLiteral(r).asNode();
462    }
463
464    /**
465     * Yes, we really did create a method to increment
466     * a given int. You can't do math in a velocity template.
467     *
468     * @param i the given integer
469     * @return maths
470     */
471    public int addOne(final int i) {
472        return i + 1;
473    }
474
475    /**
476     * Proxying access to the RDF type static property
477     * @return RDF type property
478     */
479    public Property rdfType() {
480        return RDF.type;
481    }
482
483    /**
484     * Proxying access to the RDFS domain static property
485     * @return RDFS domain property
486     */
487    public Property rdfsDomain() {
488        return RDFS.domain;
489    }
490
491    /**
492     * Proxying access to the RDFS class static property
493     * @return RDFS class resource
494     */
495    public Resource rdfsClass() {
496        return RDFS.Class;
497    }
498
499    /**
500     * Get the content-bearing node for the given subject
501     * @param subject the subject
502     * @return content-bearing node for the given subject
503     */
504    public Node getContentNode(final Node subject) {
505        return NodeFactory.createURI(subject.getURI().replace(FCR_METADATA, ""));
506    }
507
508    /**
509     * Transform a source string to something appropriate for HTML ids
510     * @param source the source string
511     * @return transformed source string
512     */
513    public String parameterize(final String source) {
514        return source.toLowerCase().replaceAll("[^a-z0-9\\-_]+", "_");
515    }
516}