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.collect.Lists.newArrayList; 019import static com.hp.hpl.jena.graph.GraphUtil.listObjects; 020import static com.hp.hpl.jena.graph.NodeFactory.createURI; 021import static com.hp.hpl.jena.rdf.model.ResourceFactory.createResource; 022import static com.hp.hpl.jena.rdf.model.ResourceFactory.createProperty; 023import static java.util.Arrays.asList; 024import static java.util.Arrays.stream; 025import static java.util.Collections.emptyMap; 026import static java.util.stream.Collectors.joining; 027import static java.util.stream.Collectors.toMap; 028import static org.fcrepo.kernel.api.FedoraTypes.FCR_METADATA; 029import static org.fcrepo.kernel.api.RdfLexicon.CREATED_DATE; 030import static org.fcrepo.kernel.api.RdfLexicon.DC_TITLE; 031import static org.fcrepo.kernel.api.RdfLexicon.DCTERMS_TITLE; 032import static org.fcrepo.kernel.api.RdfLexicon.HAS_CHILD_COUNT; 033import static org.fcrepo.kernel.api.RdfLexicon.HAS_VERSION_LABEL; 034import static org.fcrepo.kernel.api.RdfLexicon.RDFS_LABEL; 035import static org.fcrepo.kernel.api.RdfLexicon.REPOSITORY_NAMESPACE; 036import static org.fcrepo.kernel.api.RdfLexicon.SKOS_PREFLABEL; 037import static org.fcrepo.kernel.api.RdfLexicon.WRITABLE; 038import static org.fcrepo.kernel.api.RdfLexicon.HAS_VERSION; 039import static org.fcrepo.kernel.api.RdfLexicon.RDF_NAMESPACE; 040import static org.fcrepo.kernel.api.RdfLexicon.DC_NAMESPACE; 041import static org.slf4j.LoggerFactory.getLogger; 042 043import java.text.SimpleDateFormat; 044import java.util.Date; 045import java.util.Iterator; 046import java.util.LinkedHashMap; 047import java.util.List; 048import java.util.Map; 049import java.util.Optional; 050import java.util.StringJoiner; 051import javax.ws.rs.core.UriInfo; 052 053import com.hp.hpl.jena.graph.Graph; 054import com.hp.hpl.jena.graph.Triple; 055import com.hp.hpl.jena.graph.impl.LiteralLabel; 056 057import org.fcrepo.http.commons.api.rdf.TripleOrdering; 058import org.slf4j.Logger; 059 060import com.hp.hpl.jena.graph.Node; 061import com.hp.hpl.jena.rdf.model.Model; 062import com.hp.hpl.jena.rdf.model.Property; 063import com.hp.hpl.jena.rdf.model.Resource; 064import com.hp.hpl.jena.rdf.model.ResourceFactory; 065import com.hp.hpl.jena.shared.PrefixMapping; 066import com.hp.hpl.jena.vocabulary.RDF; 067import com.hp.hpl.jena.vocabulary.RDFS; 068 069/** 070 * General view helpers for rendering HTML responses 071 * 072 * @author awoods 073 * @author ajs6f 074 */ 075public class ViewHelpers { 076 077 private static final Logger LOGGER = getLogger(ViewHelpers.class); 078 079 private static ViewHelpers instance = null; 080 081 private static final List<Property> TITLE_PROPERTIES = asList(RDFS_LABEL, DC_TITLE, DCTERMS_TITLE, SKOS_PREFLABEL); 082 083 private static final Property DC_FORMAT = createProperty(DC_NAMESPACE + "format"); 084 085 private static final String DEFAULT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'").format(new Date()); 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 return getOrderedVersions(graph, subject, HAS_VERSION); 109 } 110 111 /** 112 * Return an iterator of Triples for versions in order that 113 * they were created. 114 * 115 * @param g the graph 116 * @param subject the subject 117 * @param predicate the predicate 118 * @return iterator 119 */ 120 public Iterator<Node> getOrderedVersions(final Graph g, final Node subject, final Resource predicate) { 121 final List<Node> vs = listObjects(g, subject, predicate.asNode()).toList(); 122 vs.sort((v1, v2) -> getVersionDate(g, v1).orElse(DEFAULT).compareTo(getVersionDate(g, v2).orElse(DEFAULT))); 123 return vs.iterator(); 124 } 125 126 /** 127 * Gets the URL of the node whose version is represented by the 128 * current node. The current implementation assumes the URI 129 * of that node will be the same as the breadcrumb entry that 130 * precedes one with the path "fcr:versions". 131 * @param uriInfo the uri info 132 * @param subject the subject 133 * @return the URL of the node 134 */ 135 public String getVersionSubjectUrl(final UriInfo uriInfo, final Node subject) { 136 final Map<String, String> breadcrumbs = getNodeBreadcrumbs(uriInfo, subject); 137 String lastUrl = null; 138 for (final Map.Entry<String, String> entry : breadcrumbs.entrySet()) { 139 if (entry.getValue().equals("fcr:versions")) { 140 return lastUrl; 141 } 142 lastUrl = entry.getKey(); 143 } 144 return null; 145 } 146 147 /** 148 * Gets a version label of a subject from the graph 149 * 150 * @param graph the graph 151 * @param subject the subject 152 * @return the label of the version if one has been provided 153 */ 154 public Optional<String> getVersionLabel(final Graph graph, final Node subject) { 155 return getValue(graph, subject, HAS_VERSION_LABEL.asNode()); 156 } 157 158 /** 159 * Gets a modification date of a subject from the graph 160 * 161 * @param graph the graph 162 * @param subject the subject 163 * @return the modification date if it exists 164 */ 165 public Optional<String> getVersionDate(final Graph graph, final Node subject) { 166 return getValue(graph, subject, CREATED_DATE.asNode()); 167 } 168 169 private static Optional<String> getValue(final Graph graph, final Node subject, final Node predicate) { 170 final Iterator<Node> objects = listObjects(graph, subject, predicate); 171 return Optional.ofNullable(objects.hasNext() ? objects.next().getLiteralValue().toString() : null); 172 } 173 174 /** 175 * Get the canonical title of a subject from the graph 176 * 177 * @param graph the graph 178 * @param sub the subject 179 * @return canonical title of the subject in the graph 180 */ 181 public String getObjectTitle(final Graph graph, final Node sub) { 182 if (sub == null) { 183 return ""; 184 } 185 final Optional<String> title = TITLE_PROPERTIES.stream().map(Property::asNode).flatMap(p -> listObjects( 186 graph, sub, p).toList().stream()).filter(Node::isLiteral).map(Node::getLiteral).map( 187 LiteralLabel::toString).findFirst(); 188 return title.orElse(sub.isURI() ? sub.getURI() : sub.isBlank() ? sub.getBlankNodeLabel() : sub.toString()); 189 } 190 191 /** 192 * Take a HAS_SERIALIZATION node and find the RDFS_LABEL for the format it is associated with 193 * 194 * @param graph the graph 195 * @param subject the subject 196 * @return the label for the serialization format 197 */ 198 public Optional<String> getSerializationTitle(final Graph graph, final Node subject) { 199 final Iterator<Node> formats = listObjects(graph, subject, DC_FORMAT.asNode()); 200 return Optional.ofNullable(formats.hasNext() ? getObjectTitle(graph, formats.next()) : null); 201 } 202 203 /** 204 * Determines whether the subject is writable 205 * true if node is writable 206 * @param graph the graph 207 * @param subject the subject 208 * @return whether the subject is writable 209 */ 210 public boolean isWritable(final Graph graph, final Node subject) { 211 return getValue(graph, subject, WRITABLE.asNode()).filter("true"::equals).isPresent(); 212 } 213 214 /** 215 * Determines whether the subject is of type fedora:Version. 216 * true if node has type fedora:Version 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((REPOSITORY_NAMESPACE + "Version")::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 * Get the number of child resources associated with the arg 'subject' as specified by the triple found in the arg 253 * 'graph' with the predicate RdfLexicon.HAS_CHILD_COUNT. 254 * 255 * @param graph of triples 256 * @param subject for which child resources is sought 257 * @return number of child resources 258 */ 259 public int getNumChildren(final Graph graph, final Node subject) { 260 LOGGER.trace("Getting number of children: s:{}, g:{}", subject, graph); 261 final Iterator<Node> iterator = listObjects(graph, subject, HAS_CHILD_COUNT.asNode()); 262 if (iterator.hasNext()) { 263 final Node obj = iterator.next(); 264 if (obj.isLiteral()) { 265 final String lit = obj.getLiteralValue().toString(); 266 return lit.isEmpty() ? 0 : Integer.parseInt(lit); 267 } 268 } 269 return 0; 270 } 271 272 /** 273 * Generate url to local name breadcrumbs for a given node's tree 274 * 275 * @param uriInfo the uri info 276 * @param subject the subject 277 * @return breadcrumbs 278 */ 279 public Map<String, String> getNodeBreadcrumbs(final UriInfo uriInfo, 280 final Node subject) { 281 final String topic = subject.getURI(); 282 283 LOGGER.trace("Generating breadcrumbs for subject {}", subject); 284 final String baseUri = uriInfo.getBaseUri().toString(); 285 286 if (!topic.startsWith(baseUri)) { 287 LOGGER.trace("Topic wasn't part of our base URI {}", baseUri); 288 return emptyMap(); 289 } 290 291 final String salientPath = topic.substring(baseUri.length()); 292 final StringJoiner cumulativePath = new StringJoiner("/"); 293 return stream(salientPath.split("/")).filter(seg -> !seg.isEmpty()).collect(toMap(seg -> uriInfo 294 .getBaseUriBuilder().path(cumulativePath.add(seg).toString()) 295 .build().toString(), seg -> seg, (u, v) -> null, LinkedHashMap::new)); 296 } 297 298 /** 299 * Sort a Iterator of Triples alphabetically by its subject, predicate, and 300 * object 301 * 302 * @param model the model 303 * @param it the iterator of triples 304 * @return iterator of alphabetized triples 305 */ 306 public List<Triple> getSortedTriples(final Model model, final Iterator<Triple> it) { 307 final List<Triple> triples = newArrayList(it); 308 triples.sort(new TripleOrdering(model)); 309 return triples; 310 } 311 312 /** 313 * Get the namespace prefix (or the namespace URI itself, if no prefix is 314 * available) from a prefix mapping 315 * 316 * @param mapping the prefix mapping 317 * @param ns the namespace 318 * @param compact the boolean value of compact 319 * @return namespace prefix 320 */ 321 public String getNamespacePrefix(final PrefixMapping mapping, 322 final String ns, final boolean compact) { 323 final String nsURIPrefix = mapping.getNsURIPrefix(ns); 324 if (nsURIPrefix == null) { 325 if (compact) { 326 final int hashIdx = ns.lastIndexOf('#'); 327 final int split = hashIdx > 0 ? ns.substring(0, hashIdx).lastIndexOf('/') : ns.lastIndexOf('/'); 328 return split > 0 ? "..." + ns.substring(split) : ns; 329 } 330 return ns; 331 } 332 return nsURIPrefix + ":"; 333 } 334 335 /** 336 * Get a prefix preamble appropriate for a SPARQL-UPDATE query from a prefix 337 * mapping object 338 * 339 * @param mapping the prefix mapping 340 * @return prefix preamble 341 */ 342 public String getPrefixPreamble(final PrefixMapping mapping) { 343 return mapping.getNsPrefixMap().entrySet().stream() 344 .map(e -> "PREFIX " + e.getKey() + ": <" + e.getValue() + ">").collect(joining("\n", "", "\n\n")); 345 } 346 347 /** 348 * Determines whether the subject is kind of RDF resource 349 * @param graph the graph 350 * @param subject the subject 351 * @param namespace the namespace 352 * @param resource the resource 353 * @return whether the subject is kind of RDF resource 354 */ 355 public boolean isRdfResource(final Graph graph, 356 final Node subject, 357 final String namespace, 358 final String resource) { 359 LOGGER.trace("Is RDF Resource? s:{}, ns:{}, r:{}, g:{}", subject, namespace, resource, graph); 360 return graph.find(subject, createResource(RDF_NAMESPACE + "type").asNode(), 361 createResource(namespace + resource).asNode()).hasNext(); 362 } 363 364 /** 365 * Convert a URI string to an RDF node 366 * 367 * @param r the uri string 368 * @return RDF node representation of the given string 369 */ 370 public Node asLiteralStringNode(final String r) { 371 return ResourceFactory.createPlainLiteral(r).asNode(); 372 } 373 374 /** 375 * Yes, we really did create a method to increment 376 * a given int. You can't do math in a velocity template. 377 * 378 * @param i the given integer 379 * @return maths 380 */ 381 public int addOne(final int i) { 382 return i + 1; 383 } 384 385 /** 386 * Proxying access to the RDF type static property 387 * @return RDF type property 388 */ 389 public Property rdfType() { 390 return RDF.type; 391 } 392 393 /** 394 * Proxying access to the RDFS domain static property 395 * @return RDFS domain property 396 */ 397 public Property rdfsDomain() { 398 return RDFS.domain; 399 } 400 401 /** 402 * Proxying access to the RDFS class static property 403 * @return RDFS class resource 404 */ 405 public Resource rdfsClass() { 406 return RDFS.Class; 407 } 408 409 /** 410 * Get the content-bearing node for the given subject 411 * @param subject the subject 412 * @return content-bearing node for the given subject 413 */ 414 public static Node getContentNode(final Node subject) { 415 return subject == null ? null : createURI(subject.getURI().replace("/" + FCR_METADATA, "")); 416 } 417 418 /** 419 * Transform a source string to something appropriate for HTML ids 420 * @param source the source string 421 * @return transformed source string 422 */ 423 public static String parameterize(final String source) { 424 return source.toLowerCase().replaceAll("[^a-z0-9\\-_]+", "_"); 425 } 426}