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.util.Arrays.asList; 030import static java.util.Arrays.stream; 031import static java.util.Collections.emptyMap; 032import static java.util.stream.Collectors.joining; 033import static java.util.stream.Collectors.toMap; 034import static org.fcrepo.kernel.api.FedoraTypes.FCR_METADATA; 035import static org.fcrepo.kernel.api.FedoraTypes.FCR_VERSIONS; 036import static org.fcrepo.kernel.api.FedoraTypes.FEDORA_REPOSITORY_ROOT; 037import static org.fcrepo.kernel.api.RdfLexicon.CONTAINS; 038import static org.fcrepo.kernel.api.RdfLexicon.MEMENTO_TYPE; 039import static org.fcrepo.kernel.api.RdfLexicon.isManagedPredicate; 040import static org.fcrepo.kernel.api.services.VersionService.MEMENTO_LABEL_FORMATTER; 041import static org.fcrepo.kernel.api.services.VersionService.MEMENTO_RFC_1123_FORMATTER; 042import static org.slf4j.LoggerFactory.getLogger; 043 044import java.time.Instant; 045import java.util.Comparator; 046import java.util.Iterator; 047import java.util.LinkedHashMap; 048import java.util.List; 049import java.util.Map; 050import java.util.Optional; 051import java.util.StringJoiner; 052 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 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 // Mementos should be ordered by date so use the getOrderedVersions. 109 return getOrderedVersions(graph, subject, CONTAINS.asResource()); 110 } 111 112 /** 113 * Return an iterator of Triples for versions in order that 114 * they were created. 115 * 116 * @param g the graph 117 * @param subject the subject 118 * @param predicate the predicate 119 * @return iterator 120 */ 121 public Iterator<Node> getOrderedVersions(final Graph g, final Node subject, final Resource predicate) { 122 final List<Node> vs = listObjects(g, subject, predicate.asNode()).toList(); 123 vs.sort(Comparator.comparing(v -> getVersionDate(g, v))); 124 return vs.iterator(); 125 } 126 127 /** 128 * Gets the URL of the node whose version is represented by the 129 * current node. The current implementation assumes the URI 130 * of that node will be the same as the breadcrumb entry that 131 * precedes one with the path "fcr:versions". 132 * @param uriInfo the uri info 133 * @param subject the subject 134 * @return the URL of the node 135 */ 136 public String getVersionSubjectUrl(final UriInfo uriInfo, final Node subject) { 137 final Map<String, String> breadcrumbs = getNodeBreadcrumbs(uriInfo, subject); 138 String lastUrl = null; 139 for (final Map.Entry<String, String> entry : breadcrumbs.entrySet()) { 140 if (entry.getValue().equals("fcr:versions")) { 141 return lastUrl; 142 } 143 lastUrl = entry.getKey(); 144 } 145 return null; 146 } 147 148 /** 149 * Get the date time as the version label. 150 * 151 * @param graph the graph 152 * @param subject the subject 153 * @return the datetime in RFC 1123 format. 154 */ 155 public String getVersionLabel(final Graph graph, final Node subject) { 156 final Instant datetime = getVersionDate(graph, subject); 157 return MEMENTO_RFC_1123_FORMATTER.format(datetime); 158 } 159 160 /** 161 * Gets a modification date of a subject from the graph 162 * 163 * @param graph the graph 164 * @param subject the subject 165 * @return the modification date if it exists 166 */ 167 public Instant getVersionDate(final Graph graph, final Node subject) { 168 final String[] pathParts = subject.getURI().split("/"); 169 return MEMENTO_LABEL_FORMATTER.parse(pathParts[pathParts.length - 1], Instant::from); 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 // XXX: always return true until we can determine a better way to control the HTML UI 203 return true; 204 } 205 206 /** 207 * Determines whether the subject is of type memento:Memento. 208 * 209 * @param graph the graph 210 * @param subject the subject 211 * @return whether the subject is a versioned node 212 */ 213 public boolean isVersionedNode(final Graph graph, final Node subject) { 214 return listObjects(graph, subject, RDF.type.asNode()).toList().stream().map(Node::getURI) 215 .anyMatch((MEMENTO_TYPE)::equals); 216 } 217 218 /** 219 * Get the string version of the object that matches the given subject and 220 * predicate 221 * 222 * @param graph the graph 223 * @param subject the subject 224 * @param predicate the predicate 225 * @param uriAsLink the boolean value of uri as link 226 * @return string version of the object 227 */ 228 public String getObjectsAsString(final Graph graph, 229 final Node subject, final Resource predicate, final boolean uriAsLink) { 230 LOGGER.trace("Getting Objects as String: s:{}, p:{}, g:{}", subject, predicate, graph); 231 final Iterator<Node> iterator = listObjects(graph, subject, predicate.asNode()); 232 if (iterator.hasNext()) { 233 final Node obj = iterator.next(); 234 if (obj.isLiteral()) { 235 final String lit = obj.getLiteralValue().toString(); 236 return lit.isEmpty() ? "<empty>" : lit; 237 } 238 return uriAsLink ? "<<a href=\"" + obj.getURI() + "\">" + obj.getURI() + "</a>>" : obj.getURI(); 239 } 240 return ""; 241 } 242 243 /** 244 * Returns the original resource as a URI Node if 245 * the subject represents a memento uri; otherwise it 246 * returns the subject parameter. 247 * @param subject the subject 248 * @return a URI node of the original resource. 249 */ 250 public Node getOriginalResource(final Node subject) { 251 if (!subject.isURI()) { 252 return subject; 253 } 254 255 final String subjectUri = subject.getURI(); 256 final int index = subjectUri.indexOf(FCR_VERSIONS); 257 if (index > 0) { 258 return NodeFactory.createURI(subjectUri.substring(0, index - 1)); 259 } else { 260 return subject; 261 } 262 } 263 264 /** 265 * Same as above but takes a string. 266 * NB: This method is currently used in fcrepo-http-api/src/main/resources/views/default.vsl 267 * @param subjectUri the URI 268 * @return a node 269 */ 270 public Node getOriginalResource(final String subjectUri) { 271 return getOriginalResource(createURI(subjectUri)); 272 } 273 274 /** 275 * Get the number of child resources associated with the arg 'subject' as specified by the triple found in the arg 276 * 'graph' with the predicate RdfLexicon.HAS_CHILD_COUNT. 277 * 278 * @param graph of triples 279 * @param subject for which child resources is sought 280 * @return number of child resources 281 */ 282 public int getNumChildren(final Graph graph, final Node subject) { 283 LOGGER.trace("Getting number of children: s:{}, g:{}", subject, graph); 284 return (int) asStream(listObjects(graph, subject, CONTAINS.asNode())).count(); 285 } 286 287 /** 288 * Generate url to local name breadcrumbs for a given node's tree 289 * 290 * @param uriInfo the uri info 291 * @param subject the subject 292 * @return breadcrumbs 293 */ 294 public Map<String, String> getNodeBreadcrumbs(final UriInfo uriInfo, 295 final Node subject) { 296 final String topic = subject.getURI(); 297 298 LOGGER.trace("Generating breadcrumbs for subject {}", subject); 299 final String baseUri = uriInfo.getBaseUri().toString(); 300 301 if (!topic.startsWith(baseUri)) { 302 LOGGER.trace("Topic wasn't part of our base URI {}", baseUri); 303 return emptyMap(); 304 } 305 306 final String salientPath = topic.substring(baseUri.length()); 307 final StringJoiner cumulativePath = new StringJoiner("/"); 308 return stream(salientPath.split("/")).filter(seg -> !seg.isEmpty()).collect(toMap(seg -> uriInfo 309 .getBaseUriBuilder().path(cumulativePath.add(seg).toString()) 310 .build().toString(), seg -> seg, (u, v) -> null, LinkedHashMap::new)); 311 } 312 313 /** 314 * Sort a Iterator of Triples alphabetically by its subject, predicate, and 315 * object 316 * 317 * @param model the model 318 * @param it the iterator of triples 319 * @return iterator of alphabetized triples 320 */ 321 public List<Triple> getSortedTriples(final Model model, final Iterator<Triple> it) { 322 final List<Triple> triples = newArrayList(it); 323 triples.sort(new TripleOrdering(model)); 324 return triples; 325 } 326 327 /** 328 * Get the namespace prefix (or the namespace URI itself, if no prefix is 329 * available) from a prefix mapping 330 * 331 * @param mapping the prefix mapping 332 * @param ns the namespace 333 * @param compact the boolean value of compact 334 * @return namespace prefix 335 */ 336 public String getNamespacePrefix(final PrefixMapping mapping, 337 final String ns, final boolean compact) { 338 final String nsURIPrefix = mapping.getNsURIPrefix(ns); 339 if (nsURIPrefix == null) { 340 if (compact) { 341 final int hashIdx = ns.lastIndexOf('#'); 342 final int split = hashIdx > 0 ? ns.substring(0, hashIdx).lastIndexOf('/') : ns.lastIndexOf('/'); 343 return split > 0 ? "..." + ns.substring(split) : ns; 344 } 345 return ns; 346 } 347 return nsURIPrefix + ":"; 348 } 349 350 /** 351 * Get a prefix preamble appropriate for a SPARQL-UPDATE query from a prefix 352 * mapping object 353 * 354 * @param mapping the prefix mapping 355 * @return prefix preamble 356 */ 357 public String getPrefixPreamble(final PrefixMapping mapping) { 358 return mapping.getNsPrefixMap().entrySet().stream() 359 .map(e -> "PREFIX " + e.getKey() + ": <" + e.getValue() + ">").collect(joining("\n", "", "\n\n")); 360 } 361 362 /** 363 * Determines whether the subject is kind of RDF resource 364 * @param graph the graph 365 * @param subject the subject 366 * @param namespace the namespace 367 * @param resource the resource 368 * @return whether the subject is kind of RDF resource 369 */ 370 public boolean isRdfResource(final Graph graph, 371 final Node subject, 372 final String namespace, 373 final String resource) { 374 LOGGER.trace("Is RDF Resource? s:{}, ns:{}, r:{}, g:{}", subject, namespace, resource, graph); 375 return graph.find(subject, type.asNode(), 376 createResource(namespace + resource).asNode()).hasNext(); 377 } 378 379 /** 380 * Is the subject the repository root resource. 381 * 382 * @param graph The graph 383 * @param subject The current subject 384 * @return true if has rdf:type http://fedora.info/definitions/v4/repository#RepositoryRoot 385 */ 386 public boolean isRootResource(final Graph graph, final Node subject) { 387 final String rootRes = graph.getPrefixMapping().expandPrefix(FEDORA_REPOSITORY_ROOT); 388 final Node root = createResource(rootRes).asNode(); 389 return graph.contains(subject, rdfType().asNode(), root); 390 } 391 392 /** 393 * Convert a URI string to an RDF node 394 * 395 * @param r the uri string 396 * @return RDF node representation of the given string 397 */ 398 public Node asLiteralStringNode(final String r) { 399 return ResourceFactory.createPlainLiteral(r).asNode(); 400 } 401 402 /** 403 * Yes, we really did create a method to increment 404 * a given int. You can't do math in a velocity template. 405 * 406 * @param i the given integer 407 * @return maths 408 */ 409 public int addOne(final int i) { 410 return i + 1; 411 } 412 413 /** 414 * Proxying access to the RDF type static property 415 * @return RDF type property 416 */ 417 public Property rdfType() { 418 return RDF.type; 419 } 420 421 /** 422 * Proxying access to the RDFS domain static property 423 * @return RDFS domain property 424 */ 425 public Property rdfsDomain() { 426 return RDFS.domain; 427 } 428 429 /** 430 * Proxying access to the RDFS class static property 431 * @return RDFS class resource 432 */ 433 public Resource rdfsClass() { 434 return RDFS.Class; 435 } 436 437 /** 438 * Get the content-bearing node for the given subject 439 * @param subject the subject 440 * @return content-bearing node for the given subject 441 */ 442 public static Node getContentNode(final Node subject) { 443 return subject == null ? null : NodeFactory.createURI(subject.getURI().replace("/" + FCR_METADATA, "")); 444 } 445 446 /** 447 * Create a URI Node from the provided String 448 * 449 * @param uri from which a URI Node will be created 450 * @return URI Node 451 */ 452 public static Node createURI(final String uri) { 453 return NodeFactory.createURI(uri); 454 } 455 456 /** 457 * Transform a source string to something appropriate for HTML ids 458 * @param source the source string 459 * @return transformed source string 460 */ 461 public static String parameterize(final String source) { 462 return source.toLowerCase().replaceAll("[^a-z0-9\\-_]+", "_"); 463 } 464 465 /** 466 * Test if a Predicate is managed 467 * @param property the property 468 * @return whether the property is managed 469 */ 470 public static boolean isManagedProperty(final Node property) { 471 return property.isURI() && isManagedPredicate.test(createProperty(property.getURI())); 472 } 473}