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.graph.GraphUtil.listObjects; 022import static org.apache.jena.rdf.model.ResourceFactory.createResource; 023import static org.apache.jena.rdf.model.ResourceFactory.createProperty; 024import static java.util.Arrays.asList; 025import static java.util.Arrays.stream; 026import static java.util.Collections.emptyMap; 027import static java.util.stream.Collectors.joining; 028import static java.util.stream.Collectors.toMap; 029import static org.fcrepo.kernel.api.FedoraTypes.FCR_METADATA; 030import static org.fcrepo.kernel.api.RdfLexicon.CONTAINS; 031import static org.fcrepo.kernel.api.RdfLexicon.CREATED_DATE; 032import static org.fcrepo.kernel.api.RdfLexicon.DC_TITLE; 033import static org.fcrepo.kernel.api.RdfLexicon.DCTERMS_TITLE; 034import static org.fcrepo.kernel.api.RdfLexicon.HAS_VERSION_LABEL; 035import static org.fcrepo.kernel.api.RdfLexicon.RDFS_LABEL; 036import static org.fcrepo.kernel.api.RdfLexicon.REPOSITORY_NAMESPACE; 037import static org.fcrepo.kernel.api.RdfLexicon.SKOS_PREFLABEL; 038import static org.fcrepo.kernel.api.RdfLexicon.WRITABLE; 039import static org.fcrepo.kernel.api.RdfLexicon.HAS_VERSION; 040import static org.fcrepo.kernel.api.RdfLexicon.RDF_NAMESPACE; 041import static org.fcrepo.kernel.api.RdfLexicon.DC_NAMESPACE; 042import static org.slf4j.LoggerFactory.getLogger; 043 044import java.text.SimpleDateFormat; 045import java.util.Date; 046import java.util.Iterator; 047import java.util.LinkedHashMap; 048import java.util.List; 049import java.util.Map; 050import java.util.Optional; 051import java.util.StringJoiner; 052import javax.ws.rs.core.UriInfo; 053 054import org.apache.jena.graph.Graph; 055import org.apache.jena.graph.NodeFactory; 056import org.apache.jena.graph.Triple; 057import org.apache.jena.graph.impl.LiteralLabel; 058 059import org.fcrepo.http.commons.api.rdf.TripleOrdering; 060import org.fcrepo.kernel.modeshape.utils.StreamUtils; 061import org.slf4j.Logger; 062 063import org.apache.jena.graph.Node; 064import org.apache.jena.rdf.model.Model; 065import org.apache.jena.rdf.model.Property; 066import org.apache.jena.rdf.model.Resource; 067import org.apache.jena.rdf.model.ResourceFactory; 068import org.apache.jena.shared.PrefixMapping; 069import org.apache.jena.vocabulary.RDF; 070import org.apache.jena.vocabulary.RDFS; 071 072/** 073 * General view helpers for rendering HTML responses 074 * 075 * @author awoods 076 * @author ajs6f 077 */ 078public class ViewHelpers { 079 080 private static final Logger LOGGER = getLogger(ViewHelpers.class); 081 082 private static ViewHelpers instance = null; 083 084 private static final List<Property> TITLE_PROPERTIES = asList(RDFS_LABEL, DC_TITLE, DCTERMS_TITLE, SKOS_PREFLABEL); 085 086 private static final Property DC_FORMAT = createProperty(DC_NAMESPACE + "format"); 087 088 private static final String DEFAULT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'").format(new Date()); 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 final Iterator<Node> iterator = listObjects(graph, subject, CONTAINS.asNode()); 253 if (iterator.hasNext()) { 254 return (int) StreamUtils.iteratorToStream(iterator).count(); 255 } 256 return 0; 257 } 258 259 /** 260 * Generate url to local name breadcrumbs for a given node's tree 261 * 262 * @param uriInfo the uri info 263 * @param subject the subject 264 * @return breadcrumbs 265 */ 266 public Map<String, String> getNodeBreadcrumbs(final UriInfo uriInfo, 267 final Node subject) { 268 final String topic = subject.getURI(); 269 270 LOGGER.trace("Generating breadcrumbs for subject {}", subject); 271 final String baseUri = uriInfo.getBaseUri().toString(); 272 273 if (!topic.startsWith(baseUri)) { 274 LOGGER.trace("Topic wasn't part of our base URI {}", baseUri); 275 return emptyMap(); 276 } 277 278 final String salientPath = topic.substring(baseUri.length()); 279 final StringJoiner cumulativePath = new StringJoiner("/"); 280 return stream(salientPath.split("/")).filter(seg -> !seg.isEmpty()).collect(toMap(seg -> uriInfo 281 .getBaseUriBuilder().path(cumulativePath.add(seg).toString()) 282 .build().toString(), seg -> seg, (u, v) -> null, LinkedHashMap::new)); 283 } 284 285 /** 286 * Sort a Iterator of Triples alphabetically by its subject, predicate, and 287 * object 288 * 289 * @param model the model 290 * @param it the iterator of triples 291 * @return iterator of alphabetized triples 292 */ 293 public List<Triple> getSortedTriples(final Model model, final Iterator<Triple> it) { 294 final List<Triple> triples = newArrayList(it); 295 triples.sort(new TripleOrdering(model)); 296 return triples; 297 } 298 299 /** 300 * Get the namespace prefix (or the namespace URI itself, if no prefix is 301 * available) from a prefix mapping 302 * 303 * @param mapping the prefix mapping 304 * @param ns the namespace 305 * @param compact the boolean value of compact 306 * @return namespace prefix 307 */ 308 public String getNamespacePrefix(final PrefixMapping mapping, 309 final String ns, final boolean compact) { 310 final String nsURIPrefix = mapping.getNsURIPrefix(ns); 311 if (nsURIPrefix == null) { 312 if (compact) { 313 final int hashIdx = ns.lastIndexOf('#'); 314 final int split = hashIdx > 0 ? ns.substring(0, hashIdx).lastIndexOf('/') : ns.lastIndexOf('/'); 315 return split > 0 ? "..." + ns.substring(split) : ns; 316 } 317 return ns; 318 } 319 return nsURIPrefix + ":"; 320 } 321 322 /** 323 * Get a prefix preamble appropriate for a SPARQL-UPDATE query from a prefix 324 * mapping object 325 * 326 * @param mapping the prefix mapping 327 * @return prefix preamble 328 */ 329 public String getPrefixPreamble(final PrefixMapping mapping) { 330 return mapping.getNsPrefixMap().entrySet().stream() 331 .map(e -> "PREFIX " + e.getKey() + ": <" + e.getValue() + ">").collect(joining("\n", "", "\n\n")); 332 } 333 334 /** 335 * Determines whether the subject is kind of RDF resource 336 * @param graph the graph 337 * @param subject the subject 338 * @param namespace the namespace 339 * @param resource the resource 340 * @return whether the subject is kind of RDF resource 341 */ 342 public boolean isRdfResource(final Graph graph, 343 final Node subject, 344 final String namespace, 345 final String resource) { 346 LOGGER.trace("Is RDF Resource? s:{}, ns:{}, r:{}, g:{}", subject, namespace, resource, graph); 347 return graph.find(subject, createResource(RDF_NAMESPACE + "type").asNode(), 348 createResource(namespace + resource).asNode()).hasNext(); 349 } 350 351 /** 352 * Convert a URI string to an RDF node 353 * 354 * @param r the uri string 355 * @return RDF node representation of the given string 356 */ 357 public Node asLiteralStringNode(final String r) { 358 return ResourceFactory.createPlainLiteral(r).asNode(); 359 } 360 361 /** 362 * Yes, we really did create a method to increment 363 * a given int. You can't do math in a velocity template. 364 * 365 * @param i the given integer 366 * @return maths 367 */ 368 public int addOne(final int i) { 369 return i + 1; 370 } 371 372 /** 373 * Proxying access to the RDF type static property 374 * @return RDF type property 375 */ 376 public Property rdfType() { 377 return RDF.type; 378 } 379 380 /** 381 * Proxying access to the RDFS domain static property 382 * @return RDFS domain property 383 */ 384 public Property rdfsDomain() { 385 return RDFS.domain; 386 } 387 388 /** 389 * Proxying access to the RDFS class static property 390 * @return RDFS class resource 391 */ 392 public Resource rdfsClass() { 393 return RDFS.Class; 394 } 395 396 /** 397 * Get the content-bearing node for the given subject 398 * @param subject the subject 399 * @return content-bearing node for the given subject 400 */ 401 public static Node getContentNode(final Node subject) { 402 return subject == null ? null : NodeFactory.createURI(subject.getURI().replace("/" + FCR_METADATA, "")); 403 } 404 405 /** 406 * Create a URI Node from the provided String 407 * 408 * @param uri from which a URI Node will be created 409 * @return URI Node 410 */ 411 public static Node createURI(final String uri) { 412 return NodeFactory.createURI(uri); 413 } 414 415 /** 416 * Transform a source string to something appropriate for HTML ids 417 * @param source the source string 418 * @return transformed source string 419 */ 420 public static String parameterize(final String source) { 421 return source.toLowerCase().replaceAll("[^a-z0-9\\-_]+", "_"); 422 } 423}