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.time.Instant.now; 030import static java.time.ZoneId.of; 031import static java.util.Arrays.asList; 032import static java.util.Arrays.stream; 033import static java.util.Collections.emptyMap; 034import static java.util.stream.Collectors.joining; 035import static java.util.stream.Collectors.toMap; 036import static org.fcrepo.kernel.api.FedoraTypes.FCR_METADATA; 037import static org.fcrepo.kernel.api.RdfLexicon.CONTAINS; 038import static org.fcrepo.kernel.api.RdfLexicon.CREATED_DATE; 039import static org.fcrepo.kernel.api.RdfLexicon.HAS_VERSION; 040import static org.fcrepo.kernel.api.RdfLexicon.HAS_VERSION_LABEL; 041import static org.fcrepo.kernel.api.RdfLexicon.REPOSITORY_NAMESPACE; 042import static org.fcrepo.kernel.api.RdfLexicon.WRITABLE; 043import static org.fcrepo.kernel.api.RdfLexicon.isManagedPredicate; 044import static org.slf4j.LoggerFactory.getLogger; 045 046import java.time.format.DateTimeFormatter; 047import java.util.Iterator; 048import java.util.LinkedHashMap; 049import java.util.List; 050import java.util.Map; 051import java.util.Optional; 052import java.util.StringJoiner; 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 static final String DEFAULT = 088 DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'").withZone(of("GMT")).format(now()); 089 090 private ViewHelpers() { 091 // Exists only to defeat instantiation. 092 } 093 094 /** 095 * ViewHelpers is a singleton. Initialize or return the existing object 096 * @return an instance of ViewHelpers 097 */ 098 public static ViewHelpers getInstance() { 099 return instance == null ? instance = new ViewHelpers() : instance; 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 g the graph 119 * @param subject the subject 120 * @param predicate the predicate 121 * @return iterator 122 */ 123 public Iterator<Node> getOrderedVersions(final Graph g, final Node subject, final Resource predicate) { 124 final List<Node> vs = listObjects(g, subject, predicate.asNode()).toList(); 125 vs.sort((v1, v2) -> getVersionDate(g, v1).orElse(DEFAULT).compareTo(getVersionDate(g, v2).orElse(DEFAULT))); 126 return vs.iterator(); 127 } 128 129 /** 130 * Gets the URL of the node whose version is represented by the 131 * current node. The current implementation assumes the URI 132 * of that node will be the same as the breadcrumb entry that 133 * precedes one with the path "fcr:versions". 134 * @param uriInfo the uri info 135 * @param subject the subject 136 * @return the URL of the node 137 */ 138 public String getVersionSubjectUrl(final UriInfo uriInfo, final Node subject) { 139 final Map<String, String> breadcrumbs = getNodeBreadcrumbs(uriInfo, subject); 140 String lastUrl = null; 141 for (final Map.Entry<String, String> entry : breadcrumbs.entrySet()) { 142 if (entry.getValue().equals("fcr:versions")) { 143 return lastUrl; 144 } 145 lastUrl = entry.getKey(); 146 } 147 return null; 148 } 149 150 /** 151 * Gets a version label of a subject from the graph 152 * 153 * @param graph the graph 154 * @param subject the subject 155 * @return the label of the version if one has been provided 156 */ 157 public Optional<String> getVersionLabel(final Graph graph, final Node subject) { 158 return getValue(graph, subject, HAS_VERSION_LABEL.asNode()); 159 } 160 161 /** 162 * Gets a modification date of a subject from the graph 163 * 164 * @param graph the graph 165 * @param subject the subject 166 * @return the modification date if it exists 167 */ 168 public Optional<String> getVersionDate(final Graph graph, final Node subject) { 169 return getValue(graph, subject, CREATED_DATE.asNode()); 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 return getValue(graph, subject, WRITABLE.asNode()).filter("true"::equals).isPresent(); 203 } 204 205 /** 206 * Determines whether the subject is of type fedora:Version. 207 * true if node has type fedora:Version 208 * @param graph the graph 209 * @param subject the subject 210 * @return whether the subject is a versioned node 211 */ 212 public boolean isVersionedNode(final Graph graph, final Node subject) { 213 return listObjects(graph, subject, RDF.type.asNode()).toList().stream().map(Node::getURI) 214 .anyMatch((REPOSITORY_NAMESPACE + "Version")::equals); 215 } 216 217 /** 218 * Get the string version of the object that matches the given subject and 219 * predicate 220 * 221 * @param graph the graph 222 * @param subject the subject 223 * @param predicate the predicate 224 * @param uriAsLink the boolean value of uri as link 225 * @return string version of the object 226 */ 227 public String getObjectsAsString(final Graph graph, 228 final Node subject, final Resource predicate, final boolean uriAsLink) { 229 LOGGER.trace("Getting Objects as String: s:{}, p:{}, g:{}", subject, predicate, graph); 230 final Iterator<Node> iterator = listObjects(graph, subject, predicate.asNode()); 231 if (iterator.hasNext()) { 232 final Node obj = iterator.next(); 233 if (obj.isLiteral()) { 234 final String lit = obj.getLiteralValue().toString(); 235 return lit.isEmpty() ? "<empty>" : lit; 236 } 237 return uriAsLink ? "<<a href=\"" + obj.getURI() + "\">" + obj.getURI() + "</a>>" : obj.getURI(); 238 } 239 return ""; 240 } 241 242 /** 243 * Get the number of child resources associated with the arg 'subject' as specified by the triple found in the arg 244 * 'graph' with the predicate RdfLexicon.HAS_CHILD_COUNT. 245 * 246 * @param graph of triples 247 * @param subject for which child resources is sought 248 * @return number of child resources 249 */ 250 public int getNumChildren(final Graph graph, final Node subject) { 251 LOGGER.trace("Getting number of children: s:{}, g:{}", subject, graph); 252 return (int) asStream(listObjects(graph, subject, CONTAINS.asNode())).count(); 253 } 254 255 /** 256 * Generate url to local name breadcrumbs for a given node's tree 257 * 258 * @param uriInfo the uri info 259 * @param subject the subject 260 * @return breadcrumbs 261 */ 262 public Map<String, String> getNodeBreadcrumbs(final UriInfo uriInfo, 263 final Node subject) { 264 final String topic = subject.getURI(); 265 266 LOGGER.trace("Generating breadcrumbs for subject {}", subject); 267 final String baseUri = uriInfo.getBaseUri().toString(); 268 269 if (!topic.startsWith(baseUri)) { 270 LOGGER.trace("Topic wasn't part of our base URI {}", baseUri); 271 return emptyMap(); 272 } 273 274 final String salientPath = topic.substring(baseUri.length()); 275 final StringJoiner cumulativePath = new StringJoiner("/"); 276 return stream(salientPath.split("/")).filter(seg -> !seg.isEmpty()).collect(toMap(seg -> uriInfo 277 .getBaseUriBuilder().path(cumulativePath.add(seg).toString()) 278 .build().toString(), seg -> seg, (u, v) -> null, LinkedHashMap::new)); 279 } 280 281 /** 282 * Sort a Iterator of Triples alphabetically by its subject, predicate, and 283 * object 284 * 285 * @param model the model 286 * @param it the iterator of triples 287 * @return iterator of alphabetized triples 288 */ 289 public List<Triple> getSortedTriples(final Model model, final Iterator<Triple> it) { 290 final List<Triple> triples = newArrayList(it); 291 triples.sort(new TripleOrdering(model)); 292 return triples; 293 } 294 295 /** 296 * Get the namespace prefix (or the namespace URI itself, if no prefix is 297 * available) from a prefix mapping 298 * 299 * @param mapping the prefix mapping 300 * @param ns the namespace 301 * @param compact the boolean value of compact 302 * @return namespace prefix 303 */ 304 public String getNamespacePrefix(final PrefixMapping mapping, 305 final String ns, final boolean compact) { 306 final String nsURIPrefix = mapping.getNsURIPrefix(ns); 307 if (nsURIPrefix == null) { 308 if (compact) { 309 final int hashIdx = ns.lastIndexOf('#'); 310 final int split = hashIdx > 0 ? ns.substring(0, hashIdx).lastIndexOf('/') : ns.lastIndexOf('/'); 311 return split > 0 ? "..." + ns.substring(split) : ns; 312 } 313 return ns; 314 } 315 return nsURIPrefix + ":"; 316 } 317 318 /** 319 * Get a prefix preamble appropriate for a SPARQL-UPDATE query from a prefix 320 * mapping object 321 * 322 * @param mapping the prefix mapping 323 * @return prefix preamble 324 */ 325 public String getPrefixPreamble(final PrefixMapping mapping) { 326 return mapping.getNsPrefixMap().entrySet().stream() 327 .map(e -> "PREFIX " + e.getKey() + ": <" + e.getValue() + ">").collect(joining("\n", "", "\n\n")); 328 } 329 330 /** 331 * Determines whether the subject is kind of RDF resource 332 * @param graph the graph 333 * @param subject the subject 334 * @param namespace the namespace 335 * @param resource the resource 336 * @return whether the subject is kind of RDF resource 337 */ 338 public boolean isRdfResource(final Graph graph, 339 final Node subject, 340 final String namespace, 341 final String resource) { 342 LOGGER.trace("Is RDF Resource? s:{}, ns:{}, r:{}, g:{}", subject, namespace, resource, graph); 343 return graph.find(subject, type.asNode(), 344 createResource(namespace + resource).asNode()).hasNext(); 345 } 346 347 /** 348 * Convert a URI string to an RDF node 349 * 350 * @param r the uri string 351 * @return RDF node representation of the given string 352 */ 353 public Node asLiteralStringNode(final String r) { 354 return ResourceFactory.createPlainLiteral(r).asNode(); 355 } 356 357 /** 358 * Yes, we really did create a method to increment 359 * a given int. You can't do math in a velocity template. 360 * 361 * @param i the given integer 362 * @return maths 363 */ 364 public int addOne(final int i) { 365 return i + 1; 366 } 367 368 /** 369 * Proxying access to the RDF type static property 370 * @return RDF type property 371 */ 372 public Property rdfType() { 373 return RDF.type; 374 } 375 376 /** 377 * Proxying access to the RDFS domain static property 378 * @return RDFS domain property 379 */ 380 public Property rdfsDomain() { 381 return RDFS.domain; 382 } 383 384 /** 385 * Proxying access to the RDFS class static property 386 * @return RDFS class resource 387 */ 388 public Resource rdfsClass() { 389 return RDFS.Class; 390 } 391 392 /** 393 * Get the content-bearing node for the given subject 394 * @param subject the subject 395 * @return content-bearing node for the given subject 396 */ 397 public static Node getContentNode(final Node subject) { 398 return subject == null ? null : NodeFactory.createURI(subject.getURI().replace("/" + FCR_METADATA, "")); 399 } 400 401 /** 402 * Create a URI Node from the provided String 403 * 404 * @param uri from which a URI Node will be created 405 * @return URI Node 406 */ 407 public static Node createURI(final String uri) { 408 return NodeFactory.createURI(uri); 409 } 410 411 /** 412 * Transform a source string to something appropriate for HTML ids 413 * @param source the source string 414 * @return transformed source string 415 */ 416 public static String parameterize(final String source) { 417 return source.toLowerCase().replaceAll("[^a-z0-9\\-_]+", "_"); 418 } 419 420 /** 421 * Test if a Predicate is managed 422 * @param property the property 423 * @return whether the property is managed 424 */ 425 public static boolean isManagedProperty(final Node property) { 426 return property.isURI() && isManagedPredicate.test(createProperty(property.getURI())); 427 } 428}