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
092     * @param subject
093     * @param 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
105     * @param 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
118     * @param subject
119     * @return iterator
120     */
121    public Iterator<Node> getOrderedVersions(final Graph graph,
122        final Node subject, final Resource predicate) {
123        final Iterator<Triple> versions = getObjects(graph, subject, predicate);
124        final Map<String, Node> map = new TreeMap<>();
125        final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
126        Triple triple;
127        String date;
128        while (versions.hasNext()) {
129            triple = versions.next();
130            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     */
146     public String getVersionSubjectUrl(final UriInfo uriInfo, final Node subject) {
147        final Map<String, String> breadcrumbs = getNodeBreadcrumbs(uriInfo, subject);
148        String lastUrl = null;
149        for (final Map.Entry<String, String> entry : breadcrumbs.entrySet()) {
150            if (entry.getValue().equals("fcr:versions")) {
151                return lastUrl;
152            }
153            lastUrl = entry.getKey();
154        }
155        return null;
156     }
157
158    /**
159     * Gets a version label of a subject from the graph
160     *
161     * @param graph
162     * @param subject
163     * @param defaultValue a value to be returned if no label is present in the
164     *                     graph
165     * @return the label of the version if one has been provided; otherwise
166     * the default is returned
167     */
168    public String getVersionLabel(final Graph graph,
169                                 final Node subject, final String defaultValue) {
170        final Iterator<Triple> objects = getObjects(graph, subject, HAS_VERSION_LABEL);
171        if (objects.hasNext()) {
172            return objects.next().getObject().getLiteralValue().toString();
173        }
174        return defaultValue;
175    }
176
177    /**
178     * Gets a modification date of a subject from the graph
179     *
180     * @param graph
181     * @param subject
182     * @return the modification date or null if none exists
183     */
184    public String getVersionDate(final Graph graph,
185                                 final Node subject) {
186        final Iterator<Triple> objects = getObjects(graph, subject, CREATED_DATE);
187        if (objects.hasNext()) {
188            return objects.next().getObject().getLiteralValue().toString();
189        }
190        return "";
191    }
192
193    /**
194     * Get the canonical title of a subject from the graph
195     *
196     * @param graph
197     * @param subject
198     * @return canonical title of the subject in the graph
199     */
200    public String getObjectTitle(final Graph graph, final Node subject) {
201
202        if (subject == null) {
203            return "";
204        }
205
206        final Property[] properties = new Property[] {RDFS_LABEL, DC_TITLE, DCTERMS_TITLE, SKOS_PREFLABEL};
207
208        for (final Property p : properties) {
209            final Iterator<Triple> objects = getObjects(graph, subject, p);
210
211            if (objects.hasNext()) {
212                return objects.next().getObject().getLiteral().getLexicalForm();
213            }
214        }
215
216        if (subject.isURI()) {
217            return subject.getURI();
218        } else if (subject.isBlank()) {
219            return subject.getBlankNodeLabel();
220        } else {
221            return subject.toString();
222        }
223
224    }
225
226    /**
227     * Take a HAS_SERIALIZATION node and find the RDFS_LABEL for the format it is associated with
228     *
229     * @param graph
230     * @param subject
231     * @return the label for the serialization format
232     */
233    public String getSerializationTitle(final Graph graph, final Node subject) {
234        final Property dcFormat = createProperty(DC_NAMESPACE + "format");
235        final Iterator<Triple> formatRDFs = getObjects(graph, subject, dcFormat);
236        if (formatRDFs.hasNext()) {
237            return getObjectTitle(graph, formatRDFs.next().getObject());
238        }
239        return "";
240    }
241
242    /**
243     * Determines whether the subject is writable
244     * true if node is writable
245     */
246    public boolean isWritable(final Graph graph, final Node subject) {
247        final Iterator<Triple> it = getObjects(graph, subject, RdfLexicon.WRITABLE);
248        return it.hasNext() && it.next().getObject().getLiteralValue().toString().equals("true");
249    }
250
251    /**
252     * Determines whether the subject is of type nt:frozenNode.
253     * true if node has type nt:frozen
254     */
255    public boolean isFrozenNode(final Graph graph, final Node subject) {
256        final Iterator<Triple> objects = getObjects(graph, subject, RdfLexicon.HAS_PRIMARY_TYPE);
257        return objects.hasNext()
258                && objects.next().getObject()
259                .getLiteralValue().toString().equals("nt:frozenNode");
260    }
261
262    /**
263     * Get the string version of the object that matches the given subject and
264     * predicate
265     *
266     * @param graph
267     * @param subject
268     * @param predicate
269     * @return string version of the object
270     */
271    public String getObjectsAsString(final Graph graph,
272            final Node subject, final Resource predicate, final boolean uriAsLink) {
273        final Iterator<Triple> iterator = getObjects(graph, subject, predicate);
274
275        if (iterator.hasNext()) {
276            final Node object = iterator.next().getObject();
277
278            if (object.isLiteral()) {
279                final String s = object.getLiteralValue().toString();
280                if (s.isEmpty()) {
281                    return "<empty>";
282                }
283                return s;
284            }
285            if (uriAsLink) {
286                return "&lt;<a href=\"" + object.getURI() + "\">" +
287                           object.getURI() + "</a>&gt;";
288            }
289            return object.getURI();
290        }
291        return "";
292    }
293
294    /**
295     * Generate url to local name breadcrumbs for a given node's tree
296     *
297     * @param uriInfo
298     * @param subject
299     * @return breadcrumbs
300     */
301    public Map<String, String> getNodeBreadcrumbs(final UriInfo uriInfo,
302            final Node subject) {
303        final String topic = subject.getURI();
304
305        LOGGER.trace("Generating breadcrumbs for subject {}", subject);
306        final ImmutableMap.Builder<String, String> builder =
307                ImmutableMap.builder();
308
309        final String baseUri = uriInfo.getBaseUri().toString();
310
311        if (!topic.startsWith(baseUri)) {
312            LOGGER.trace("Topic wasn't part of our base URI {}", baseUri);
313            return builder.build();
314        }
315
316        final String salientPath = topic.substring(baseUri.length());
317
318        final String[] split = salientPath.split("/");
319
320        final StringBuilder cumulativePath = new StringBuilder();
321
322        for (final String path : split) {
323
324            if (path.isEmpty()) {
325                continue;
326            }
327
328            cumulativePath.append(path);
329
330            final String uri =
331                    uriInfo.getBaseUriBuilder().path(cumulativePath.toString())
332                            .build().toString();
333
334            LOGGER.trace("Adding breadcrumb for path segment {} => {}", path,
335                    uri);
336
337            builder.put(uri, path);
338
339            cumulativePath.append("/");
340
341        }
342
343        return builder.build();
344
345    }
346
347    /**
348     * Sort a Iterator of Triples alphabetically by its subject, predicate, and
349     * object
350     *
351     * @param model
352     * @param it
353     * @return iterator of alphabetized triples
354     */
355    public List<Triple> getSortedTriples(final Model model, final Iterator<Triple> it) {
356        return Ordering.from(new TripleOrdering(model)).sortedCopy(ImmutableList.copyOf(it));
357    }
358
359    /**
360     * Get the namespace prefix (or the namespace URI itself, if no prefix is
361     * available) from a prefix mapping
362     *
363     * @param mapping
364     * @param namespace
365     * @return namespace prefix
366     */
367    public String getNamespacePrefix(final PrefixMapping mapping,
368            final String namespace, final boolean compact) {
369        final String nsURIPrefix = mapping.getNsURIPrefix(namespace);
370
371        if (nsURIPrefix == null) {
372            if (compact) {
373                final int hashIdx = namespace.lastIndexOf('#');
374
375                final int split;
376
377                if (hashIdx > 0) {
378                    split = namespace.substring(0, hashIdx).lastIndexOf('/');
379                } else {
380                    split = namespace.lastIndexOf('/');
381                }
382
383                if (split > 0) {
384                    return "..." + namespace.substring(split);
385                }
386                return namespace;
387            }
388            return namespace;
389        }
390        return nsURIPrefix + ":";
391    }
392
393    /**
394     * Get a prefix preamble appropriate for a SPARQL-UPDATE query from a prefix
395     * mapping object
396     *
397     * @param mapping
398     * @return prefix preamble
399     */
400    public String getPrefixPreamble(final PrefixMapping mapping) {
401        final StringBuilder sb = new StringBuilder();
402
403        final Map<String, String> nsPrefixMap = mapping.getNsPrefixMap();
404
405        for (final Map.Entry<String, String> entry : nsPrefixMap.entrySet()) {
406            sb.append("PREFIX " + entry.getKey() + ": <" + entry.getValue() +
407                    ">\n");
408        }
409
410        sb.append("\n");
411        return sb.toString();
412    }
413
414    /**
415     * Determines whether the subject is kind of RDF resource
416     */
417    public boolean isRdfResource(final Graph graph,
418                                 final Node subject,
419                                 final String namespace,
420                                 final String resource) {
421        final Iterator<Triple> it = graph.find(subject,
422                                               createResource(RDF_NAMESPACE + "type").asNode(),
423                                               createResource(namespace + resource).asNode());
424        return it.hasNext();
425    }
426
427    /**
428     * Convert an RDF resource to an RDF node
429     *
430     * @param r
431     * @return RDF node representation of the given RDF resource
432     */
433    public Node asNode(final Resource r) {
434        return r.asNode();
435    }
436
437    /**
438     * Convert a URI string to an RDF node
439     *
440     * @param r
441     * @return RDF node representation of the given string
442     */
443    public Node asLiteralStringNode(final String r) {
444        return ResourceFactory.createPlainLiteral(r).asNode();
445    }
446
447    /**
448     * Yes, we really did create a method to increment
449     * a given int. You can't do math in a velocity template.
450     *
451     * @param i
452     * @return maths
453     */
454    public int addOne(final int i) {
455        return i + 1;
456    }
457
458    /**
459     * Proxying access to the RDF type static property
460     * @return RDF type property
461     */
462    public Property rdfType() {
463        return RDF.type;
464    }
465
466    /**
467     * Proxying access to the RDFS domain static property
468     * @return RDFS domain property
469     */
470    public Property rdfsDomain() {
471        return RDFS.domain;
472    }
473
474    /**
475     * Proxying access to the RDFS class static property
476     * @return RDFS class resource
477     */
478    public Resource rdfsClass() {
479        return RDFS.Class;
480    }
481
482    /**
483     * Get the content-bearing node for the given subject
484     * @param subject
485     * @return content-bearing node for the given subject
486     */
487    public Node getContentNode(final Node subject) {
488        return NodeFactory.createURI(subject.getURI().replace(FCR_METADATA, ""));
489    }
490
491    /**
492     * Transform a source string to something appropriate for HTML ids
493     * @param source
494     * @return transformed source string
495     */
496    public String parameterize(final String source) {
497        return source.toLowerCase().replaceAll("[^a-z0-9\\-_]+", "_");
498    }
499}