001/* 002 * The contents of this file are subject to the license and copyright 003 * detailed in the LICENSE and NOTICE files at the root of the source 004 * tree. 005 */ 006package org.fcrepo.http.commons.responses; 007 008import static com.google.common.collect.Lists.newArrayList; 009import static org.apache.jena.atlas.iterator.Iter.asStream; 010import static org.apache.jena.graph.GraphUtil.listObjects; 011import static org.apache.jena.rdf.model.ResourceFactory.createProperty; 012import static org.apache.jena.rdf.model.ResourceFactory.createResource; 013import static org.apache.jena.vocabulary.DC.title; 014import static org.apache.jena.vocabulary.RDF.type; 015import static org.apache.jena.vocabulary.RDFS.label; 016import static org.apache.jena.vocabulary.SKOS.prefLabel; 017import static java.util.Arrays.asList; 018import static java.util.Arrays.stream; 019import static java.util.Collections.emptyMap; 020import static java.util.stream.Collectors.joining; 021import static java.util.stream.Collectors.toMap; 022import static org.fcrepo.kernel.api.FedoraTypes.FCR_METADATA; 023import static org.fcrepo.kernel.api.FedoraTypes.FCR_VERSIONS; 024import static org.fcrepo.kernel.api.RdfLexicon.CONTAINS; 025import static org.fcrepo.kernel.api.RdfLexicon.MEMENTO_TYPE; 026import static org.fcrepo.kernel.api.RdfLexicon.REPOSITORY_ROOT; 027import static org.fcrepo.kernel.api.RdfLexicon.isManagedPredicate; 028import static org.fcrepo.kernel.api.services.VersionService.MEMENTO_LABEL_FORMATTER; 029import static org.fcrepo.kernel.api.services.VersionService.MEMENTO_RFC_1123_FORMATTER; 030import static org.slf4j.LoggerFactory.getLogger; 031 032import java.time.Instant; 033import java.util.Comparator; 034import java.util.Iterator; 035import java.util.LinkedHashMap; 036import java.util.List; 037import java.util.Map; 038import java.util.Optional; 039import java.util.StringJoiner; 040 041import javax.ws.rs.core.UriInfo; 042 043import org.apache.jena.graph.Graph; 044import org.apache.jena.graph.NodeFactory; 045import org.apache.jena.graph.Triple; 046import org.apache.jena.graph.impl.LiteralLabel; 047import org.apache.jena.vocabulary.DCTerms; 048 049import org.fcrepo.http.commons.api.rdf.TripleOrdering; 050import org.slf4j.Logger; 051 052import org.apache.jena.graph.Node; 053import org.apache.jena.rdf.model.Model; 054import org.apache.jena.rdf.model.Property; 055import org.apache.jena.rdf.model.Resource; 056import org.apache.jena.rdf.model.ResourceFactory; 057import org.apache.jena.shared.PrefixMapping; 058import org.apache.jena.vocabulary.RDF; 059import org.apache.jena.vocabulary.RDFS; 060 061/** 062 * General view helpers for rendering HTML responses 063 * 064 * @author awoods 065 * @author ajs6f 066 */ 067public class ViewHelpers { 068 069 private static final Logger LOGGER = getLogger(ViewHelpers.class); 070 071 private static ViewHelpers instance = null; 072 073 private static final List<Property> TITLE_PROPERTIES = asList(label, title, DCTerms.title, prefLabel); 074 075 private ViewHelpers() { 076 // Exists only to defeat instantiation. 077 } 078 079 /** 080 * ViewHelpers is a singleton. Initialize or return the existing object 081 * @return an instance of ViewHelpers 082 */ 083 public static ViewHelpers getInstance() { 084 return instance == null ? instance = new ViewHelpers() : instance; 085 } 086 087 /** 088 * Return an iterator of Triples for versions. 089 * 090 * @param graph the graph 091 * @param subject the subject 092 * @return iterator 093 */ 094 public Iterator<Node> getVersions(final Graph graph, 095 final Node subject) { 096 // Mementos should be ordered by date so use the getOrderedVersions. 097 return getOrderedVersions(graph, subject, CONTAINS.asResource()); 098 } 099 100 /** 101 * Return an iterator of Triples for versions in order that 102 * they were created. 103 * 104 * @param g the graph 105 * @param subject the subject 106 * @param predicate the predicate 107 * @return iterator 108 */ 109 public Iterator<Node> getOrderedVersions(final Graph g, final Node subject, final Resource predicate) { 110 final List<Node> vs = listObjects(g, subject, predicate.asNode()).toList(); 111 vs.sort(Comparator.comparing(v -> getVersionDate(g, v))); 112 return vs.iterator(); 113 } 114 115 /** 116 * Gets the URL of the node whose version is represented by the 117 * current node. The current implementation assumes the URI 118 * of that node will be the same as the breadcrumb entry that 119 * precedes one with the path "fcr:versions". 120 * @param uriInfo the uri info 121 * @param subject the subject 122 * @return the URL of the node 123 */ 124 public String getVersionSubjectUrl(final UriInfo uriInfo, final Node subject) { 125 final Map<String, String> breadcrumbs = getNodeBreadcrumbs(uriInfo, subject); 126 String lastUrl = null; 127 for (final Map.Entry<String, String> entry : breadcrumbs.entrySet()) { 128 if (entry.getValue().equals("fcr:versions")) { 129 return lastUrl; 130 } 131 lastUrl = entry.getKey(); 132 } 133 return null; 134 } 135 136 /** 137 * Get the date time as the version label. 138 * 139 * @param graph the graph 140 * @param subject the subject 141 * @return the datetime in RFC 1123 format. 142 */ 143 public String getVersionLabel(final Graph graph, final Node subject) { 144 final Instant datetime = getVersionDate(graph, subject); 145 return MEMENTO_RFC_1123_FORMATTER.format(datetime); 146 } 147 148 /** 149 * Gets a modification date of a subject from the graph 150 * 151 * @param graph the graph 152 * @param subject the subject 153 * @return the modification date if it exists 154 */ 155 public Instant getVersionDate(final Graph graph, final Node subject) { 156 final String[] pathParts = subject.getURI().split("/"); 157 return MEMENTO_LABEL_FORMATTER.parse(pathParts[pathParts.length - 1], Instant::from); 158 } 159 160 private static Optional<String> getValue(final Graph graph, final Node subject, final Node predicate) { 161 final Iterator<Node> objects = listObjects(graph, subject, predicate); 162 return Optional.ofNullable(objects.hasNext() ? objects.next().getLiteralValue().toString() : null); 163 } 164 165 /** 166 * Get the canonical title of a subject from the graph 167 * 168 * @param graph the graph 169 * @param sub the subject 170 * @return canonical title of the subject in the graph 171 */ 172 public String getObjectTitle(final Graph graph, final Node sub) { 173 if (sub == null) { 174 return ""; 175 } 176 final Optional<String> title = TITLE_PROPERTIES.stream().map(Property::asNode).flatMap(p -> listObjects( 177 graph, sub, p).toList().stream()).filter(Node::isLiteral).map(Node::getLiteral).map( 178 LiteralLabel::toString).findFirst(); 179 return title.orElse(sub.isURI() ? sub.getURI() : sub.isBlank() ? sub.getBlankNodeLabel() : sub.toString()); 180 } 181 182 /** 183 * Get the ID of a resource, this is the URI, blank node ID or just the local name. Used in resource.vsl 184 * 185 * @param res 186 * The resource to get the ID for 187 * @return 188 * The ID as a string. 189 */ 190 public String getResourceId(final Resource res) { 191 if (res == null) { 192 return ""; 193 } else if (res.isURIResource()) { 194 return res.getURI(); 195 } else if (res.isAnon()) { 196 return res.getId().getLabelString(); 197 } else { 198 return res.getLocalName(); 199 } 200 } 201 202 /** 203 * Determines whether the subject is writable 204 * true if node is writable 205 * @param graph the graph 206 * @param subject the subject 207 * @return whether the subject is writable 208 */ 209 public boolean isWritable(final Graph graph, final Node subject) { 210 // XXX: always return true until we can determine a better way to control the HTML UI 211 return true; 212 } 213 214 /** 215 * Determines whether the subject is of type memento:Memento. 216 * 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((MEMENTO_TYPE)::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 ? "<<a href=\"" + obj.getURI() + "\">" + obj.getURI() + "</a>>" : obj.getURI(); 247 } 248 return ""; 249 } 250 251 /** 252 * Returns the original resource as a URI Node if 253 * the subject represents a memento uri; otherwise it 254 * returns the subject parameter. 255 * @param subject the subject 256 * @return a URI node of the original resource. 257 */ 258 public Node getOriginalResource(final Node subject) { 259 if (!subject.isURI()) { 260 return subject; 261 } 262 263 final String subjectUri = subject.getURI(); 264 final int index = subjectUri.indexOf(FCR_VERSIONS); 265 if (index > 0) { 266 return NodeFactory.createURI(subjectUri.substring(0, index - 1)); 267 } else { 268 return subject; 269 } 270 } 271 272 /** 273 * Same as above but takes a string. 274 * NB: This method is currently used in fcrepo-http-api/src/main/resources/views/default.vsl 275 * @param subjectUri the URI 276 * @return a node 277 */ 278 public Node getOriginalResource(final String subjectUri) { 279 return getOriginalResource(createURI(subjectUri)); 280 } 281 282 /** 283 * Get the number of child resources associated with the arg 'subject' as specified by the triple found in the arg 284 * 'graph' with the predicate RdfLexicon.HAS_CHILD_COUNT. 285 * 286 * @param graph of triples 287 * @param subject for which child resources is sought 288 * @return number of child resources 289 */ 290 public int getNumChildren(final Graph graph, final Node subject) { 291 LOGGER.trace("Getting number of children: s:{}, g:{}", subject, graph); 292 return (int) asStream(listObjects(graph, subject, CONTAINS.asNode())).count(); 293 } 294 295 /** 296 * Generate url to local name breadcrumbs for a given node's tree 297 * 298 * @param uriInfo the uri info 299 * @param subject the subject 300 * @return breadcrumbs 301 */ 302 public Map<String, String> getNodeBreadcrumbs(final UriInfo uriInfo, 303 final Node subject) { 304 final String topic = subject.getURI(); 305 306 LOGGER.trace("Generating breadcrumbs for subject {}", subject); 307 final String baseUri = uriInfo.getBaseUri().toString(); 308 309 if (!topic.startsWith(baseUri)) { 310 LOGGER.trace("Topic wasn't part of our base URI {}", baseUri); 311 return emptyMap(); 312 } 313 314 final String salientPath = topic.substring(baseUri.length()); 315 final StringJoiner cumulativePath = new StringJoiner("/"); 316 return stream(salientPath.split("/")).filter(seg -> !seg.isEmpty()).collect(toMap(seg -> uriInfo 317 .getBaseUriBuilder().path(cumulativePath.add(seg).toString()) 318 .build().toString(), seg -> seg, (u, v) -> null, LinkedHashMap::new)); 319 } 320 321 /** 322 * Sort a Iterator of Triples alphabetically by its subject, predicate, and 323 * object 324 * 325 * @param model the model 326 * @param it the iterator of triples 327 * @return iterator of alphabetized triples 328 */ 329 public List<Triple> getSortedTriples(final Model model, final Iterator<Triple> it) { 330 final List<Triple> triples = newArrayList(it); 331 triples.sort(new TripleOrdering(model)); 332 return triples; 333 } 334 335 /** 336 * Get the namespace prefix (or the namespace URI itself, if no prefix is 337 * available) from a prefix mapping 338 * 339 * @param mapping the prefix mapping 340 * @param ns the namespace 341 * @param compact the boolean value of compact 342 * @return namespace prefix 343 */ 344 public String getNamespacePrefix(final PrefixMapping mapping, 345 final String ns, final boolean compact) { 346 final String nsURIPrefix = mapping.getNsURIPrefix(ns); 347 if (nsURIPrefix == null) { 348 if (compact) { 349 final int hashIdx = ns.lastIndexOf('#'); 350 final int split = hashIdx > 0 ? ns.substring(0, hashIdx).lastIndexOf('/') : ns.lastIndexOf('/'); 351 return split > 0 ? "..." + ns.substring(split) : ns; 352 } 353 return ns; 354 } 355 return nsURIPrefix + ":"; 356 } 357 358 /** 359 * Get a prefix preamble appropriate for a SPARQL-UPDATE query from a prefix 360 * mapping object 361 * 362 * @param mapping the prefix mapping 363 * @return prefix preamble 364 */ 365 public String getPrefixPreamble(final PrefixMapping mapping) { 366 return mapping.getNsPrefixMap().entrySet().stream() 367 .map(e -> "PREFIX " + e.getKey() + ": <" + e.getValue() + ">").collect(joining("\n", "", "\n\n")); 368 } 369 370 /** 371 * Determines whether the subject is kind of RDF resource 372 * @param graph the graph 373 * @param subject the subject 374 * @param namespace the namespace 375 * @param resource the resource 376 * @return whether the subject is kind of RDF resource 377 */ 378 public boolean isRdfResource(final Graph graph, 379 final Node subject, 380 final String namespace, 381 final String resource) { 382 LOGGER.trace("Is RDF Resource? s:{}, ns:{}, r:{}, g:{}", subject, namespace, resource, graph); 383 return graph.find(subject, type.asNode(), 384 createResource(namespace + resource).asNode()).hasNext(); 385 } 386 387 /** 388 * Is the subject the repository root resource. 389 * 390 * @param graph The graph 391 * @param subject The current subject 392 * @return true if has rdf:type http://fedora.info/definitions/v4/repository#RepositoryRoot 393 */ 394 public boolean isRootResource(final Graph graph, final Node subject) { 395 return graph.contains(subject, rdfType().asNode(), REPOSITORY_ROOT.asNode()); 396 } 397 398 /** 399 * Convert a URI string to an RDF node 400 * 401 * @param r the uri string 402 * @return RDF node representation of the given string 403 */ 404 public Node asLiteralStringNode(final String r) { 405 return ResourceFactory.createPlainLiteral(r).asNode(); 406 } 407 408 /** 409 * Yes, we really did create a method to increment 410 * a given int. You can't do math in a velocity template. 411 * 412 * @param i the given integer 413 * @return maths 414 */ 415 public int addOne(final int i) { 416 return i + 1; 417 } 418 419 /** 420 * Proxying access to the RDF type static property 421 * @return RDF type property 422 */ 423 public Property rdfType() { 424 return RDF.type; 425 } 426 427 /** 428 * Proxying access to the RDFS domain static property 429 * @return RDFS domain property 430 */ 431 public Property rdfsDomain() { 432 return RDFS.domain; 433 } 434 435 /** 436 * Proxying access to the RDFS class static property 437 * @return RDFS class resource 438 */ 439 public Resource rdfsClass() { 440 return RDFS.Class; 441 } 442 443 /** 444 * Get the content-bearing node for the given subject 445 * @param subject the subject 446 * @return content-bearing node for the given subject 447 */ 448 public static Node getContentNode(final Node subject) { 449 return subject == null ? null : NodeFactory.createURI(subject.getURI().replace("/" + FCR_METADATA, "")); 450 } 451 452 /** 453 * Create a URI Node from the provided String 454 * 455 * @param uri from which a URI Node will be created 456 * @return URI Node 457 */ 458 public static Node createURI(final String uri) { 459 return NodeFactory.createURI(uri); 460 } 461 462 /** 463 * Transform a source string to something appropriate for HTML ids 464 * @param source the source string 465 * @return transformed source string 466 */ 467 public static String parameterize(final String source) { 468 return source.toLowerCase().replaceAll("[^a-z0-9\\-_]+", "_"); 469 } 470 471 /** 472 * Test if a Predicate is managed 473 * @param property the property 474 * @return whether the property is managed 475 */ 476 public static boolean isManagedProperty(final Node property) { 477 return property.isURI() && isManagedPredicate.test(createProperty(property.getURI())); 478 } 479 480 /** 481 * Find a key in a map and format it as a string 482 * @param input map of objects. 483 * @param key the key to locate in the map. 484 * @return the result string. 485 */ 486 public static String getString(final Map<String, Object> input, final String key) { 487 if (input.get(key) == null) { 488 return ""; 489 } 490 final var value = input.get(key); 491 final var clazz = value.getClass(); 492 final String output; 493 if (clazz == String.class) { 494 output = formatAsString((String) value); 495 } else if (clazz == String[].class) { 496 output = formatAsString((String[]) value); 497 } else if (clazz == Long.class) { 498 output = formatAsString((Long) value); 499 } else { 500 output = ""; 501 } 502 return output; 503 } 504 505 /** 506 * Format to a string and check for null values 507 * @param input a string array or null 508 * @return a string. 509 */ 510 private static String formatAsString(final String[] input) { 511 return (input == null || input.length == 0 ? "" : String.join(", ", input)); 512 } 513 514 /** 515 * Format a string to check for null values 516 * @param input a string or null 517 * @return a string. 518 */ 519 private static String formatAsString(final String input) { 520 return (input == null ? "" : input); 521 } 522 523 private static String formatAsString(final Long input) { 524 return (input == null ? "" : input.toString()); 525 } 526}