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.base.Strings.isNullOrEmpty; 019import static com.google.common.collect.Lists.newArrayList; 020import static com.hp.hpl.jena.graph.Node.ANY; 021import static com.hp.hpl.jena.rdf.model.ResourceFactory.createResource; 022import static com.hp.hpl.jena.rdf.model.ResourceFactory.createProperty; 023import static java.util.stream.Collectors.joining; 024import static org.fcrepo.kernel.api.RdfLexicon.CREATED_DATE; 025import static org.fcrepo.kernel.api.FedoraJcrTypes.FCR_METADATA; 026import static org.fcrepo.kernel.api.RdfLexicon.DC_TITLE; 027import static org.fcrepo.kernel.api.RdfLexicon.DCTERMS_TITLE; 028import static org.fcrepo.kernel.api.RdfLexicon.HAS_VERSION_LABEL; 029import static org.fcrepo.kernel.api.RdfLexicon.RDFS_LABEL; 030import static org.fcrepo.kernel.api.RdfLexicon.SKOS_PREFLABEL; 031import static org.fcrepo.kernel.api.RdfLexicon.HAS_VERSION; 032import static org.fcrepo.kernel.api.RdfLexicon.RDF_NAMESPACE; 033import static org.fcrepo.kernel.api.RdfLexicon.DC_NAMESPACE; 034import static org.slf4j.LoggerFactory.getLogger; 035 036import java.text.SimpleDateFormat; 037import java.util.Date; 038import java.util.Iterator; 039import java.util.List; 040import java.util.Map; 041import java.util.TreeMap; 042import javax.ws.rs.core.UriInfo; 043 044import com.hp.hpl.jena.graph.Graph; 045import com.hp.hpl.jena.graph.Triple; 046 047import org.fcrepo.http.commons.api.rdf.TripleOrdering; 048import org.fcrepo.kernel.api.RdfLexicon; 049 050import org.slf4j.Logger; 051 052import com.google.common.collect.ImmutableMap; 053import com.hp.hpl.jena.graph.Node; 054import com.hp.hpl.jena.graph.NodeFactory; 055import com.hp.hpl.jena.rdf.model.Model; 056import com.hp.hpl.jena.rdf.model.Property; 057import com.hp.hpl.jena.rdf.model.Resource; 058import com.hp.hpl.jena.rdf.model.ResourceFactory; 059import com.hp.hpl.jena.shared.PrefixMapping; 060import com.hp.hpl.jena.vocabulary.RDF; 061import com.hp.hpl.jena.vocabulary.RDFS; 062 063/** 064 * General view helpers for rendering HTML responses 065 * 066 * @author awoods 067 */ 068public class ViewHelpers { 069 070 private static final Logger LOGGER = getLogger(ViewHelpers.class); 071 072 private static ViewHelpers instance = null; 073 074 protected ViewHelpers() { 075 // Exists only to defeat instantiation. 076 } 077 078 /** 079 * ViewHelpers are singletons. Initialize or return the existing object 080 * @return an instance of ViewHelpers 081 */ 082 public static ViewHelpers getInstance() { 083 if (instance == null) { 084 instance = new ViewHelpers(); 085 } 086 return instance; 087 } 088 089 /** 090 * Return an iterator of Triples that match the given subject and predicate 091 * 092 * @param graph the graph 093 * @param subject the subject 094 * @param predicate the predicate 095 * @return iterator 096 */ 097 public Iterator<Triple> getObjects(final Graph graph, 098 final Node subject, final Resource predicate) { 099 return graph.find(subject, predicate.asNode(), ANY); 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 graph the graph 119 * @param subject the subject 120 * @param predicate the predicate 121 * @return iterator 122 */ 123 public Iterator<Node> getOrderedVersions(final Graph graph, 124 final Node subject, final Resource predicate) { 125 final Iterator<Triple> versions = getObjects(graph, subject, predicate); 126 final Map<String, Node> map = new TreeMap<>(); 127 final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); 128 while (versions.hasNext()) { 129 final Triple triple = versions.next(); 130 final String date = getVersionDate(graph, triple.getObject()); 131 String key = isNullOrEmpty(date) ? format.format(new Date()) : date; 132 while (map.containsKey(key)) { 133 key = key + "1"; 134 } 135 map.put(key, triple.getObject()); 136 } 137 return map.values().iterator(); 138 } 139 140 /** 141 * Gets the URL of the node whose version is represented by the 142 * current node. The current implementation assumes the URI 143 * of that node will be the same as the breadcrumb entry that 144 * precedes one with the path "fcr:versions". 145 * @param uriInfo the uri info 146 * @param subject the subject 147 * @return the URL of the node 148 */ 149 public String getVersionSubjectUrl(final UriInfo uriInfo, final Node subject) { 150 final Map<String, String> breadcrumbs = getNodeBreadcrumbs(uriInfo, subject); 151 String lastUrl = null; 152 for (final Map.Entry<String, String> entry : breadcrumbs.entrySet()) { 153 if (entry.getValue().equals("fcr:versions")) { 154 return lastUrl; 155 } 156 lastUrl = entry.getKey(); 157 } 158 return null; 159 } 160 161 /** 162 * Gets a version label of a subject from the graph 163 * 164 * @param graph the graph 165 * @param subject the subject 166 * @param defaultValue a value to be returned if no label is present in the 167 * graph 168 * @return the label of the version if one has been provided; otherwise 169 * the default is returned 170 */ 171 public String getVersionLabel(final Graph graph, 172 final Node subject, final String defaultValue) { 173 final Iterator<Triple> objects = getObjects(graph, subject, HAS_VERSION_LABEL); 174 if (objects.hasNext()) { 175 return objects.next().getObject().getLiteralValue().toString(); 176 } 177 return defaultValue; 178 } 179 180 /** 181 * Gets a modification date of a subject from the graph 182 * 183 * @param graph the graph 184 * @param subject the subject 185 * @return the modification date or null if none exists 186 */ 187 public String getVersionDate(final Graph graph, 188 final Node subject) { 189 final Iterator<Triple> objects = getObjects(graph, subject, CREATED_DATE); 190 if (objects.hasNext()) { 191 return objects.next().getObject().getLiteralValue().toString(); 192 } 193 return ""; 194 } 195 196 /** 197 * Get the canonical title of a subject from the graph 198 * 199 * @param graph the graph 200 * @param subject the subject 201 * @return canonical title of the subject in the graph 202 */ 203 public String getObjectTitle(final Graph graph, final Node subject) { 204 205 if (subject == null) { 206 return ""; 207 } 208 209 final Property[] properties = new Property[] {RDFS_LABEL, DC_TITLE, DCTERMS_TITLE, SKOS_PREFLABEL}; 210 211 for (final Property p : properties) { 212 final Iterator<Triple> objects = getObjects(graph, subject, p); 213 214 if (objects.hasNext()) { 215 return objects.next().getObject().getLiteral().getLexicalForm(); 216 } 217 } 218 219 if (subject.isURI()) { 220 return subject.getURI(); 221 } else if (subject.isBlank()) { 222 return subject.getBlankNodeLabel(); 223 } else { 224 return subject.toString(); 225 } 226 227 } 228 229 /** 230 * Take a HAS_SERIALIZATION node and find the RDFS_LABEL for the format it is associated with 231 * 232 * @param graph the graph 233 * @param subject the subject 234 * @return the label for the serialization format 235 */ 236 public String getSerializationTitle(final Graph graph, final Node subject) { 237 final Property dcFormat = createProperty(DC_NAMESPACE + "format"); 238 final Iterator<Triple> formatRDFs = getObjects(graph, subject, dcFormat); 239 if (formatRDFs.hasNext()) { 240 return getObjectTitle(graph, formatRDFs.next().getObject()); 241 } 242 return ""; 243 } 244 245 /** 246 * Determines whether the subject is writable 247 * true if node is writable 248 * @param graph the graph 249 * @param subject the subject 250 * @return whether the subject is writable 251 */ 252 public boolean isWritable(final Graph graph, final Node subject) { 253 final Iterator<Triple> it = getObjects(graph, subject, RdfLexicon.WRITABLE); 254 return it.hasNext() && it.next().getObject().getLiteralValue().toString().equals("true"); 255 } 256 257 /** 258 * Determines whether the subject is of type nt:frozenNode. 259 * true if node has type nt:frozen 260 * @param graph the graph 261 * @param subject the subject 262 * @return whether the subject is frozen node 263 */ 264 public boolean isFrozenNode(final Graph graph, final Node subject) { 265 final Iterator<Triple> objects = getObjects(graph, subject, RdfLexicon.HAS_PRIMARY_TYPE); 266 return objects.hasNext() 267 && objects.next().getObject() 268 .getLiteralValue().toString().equals("nt:frozenNode"); 269 } 270 271 /** 272 * Get the string version of the object that matches the given subject and 273 * predicate 274 * 275 * @param graph the graph 276 * @param subject the subject 277 * @param predicate the predicate 278 * @param uriAsLink the boolean value of uri as link 279 * @return string version of the object 280 */ 281 public String getObjectsAsString(final Graph graph, 282 final Node subject, final Resource predicate, final boolean uriAsLink) { 283 final Iterator<Triple> iterator = getObjects(graph, subject, predicate); 284 285 if (iterator.hasNext()) { 286 final Node object = iterator.next().getObject(); 287 288 if (object.isLiteral()) { 289 final String s = object.getLiteralValue().toString(); 290 if (s.isEmpty()) { 291 return "<empty>"; 292 } 293 return s; 294 } 295 if (uriAsLink) { 296 return "<<a href=\"" + object.getURI() + "\">" + 297 object.getURI() + "</a>>"; 298 } 299 return object.getURI(); 300 } 301 return ""; 302 } 303 304 /** 305 * Generate url to local name breadcrumbs for a given node's tree 306 * 307 * @param uriInfo the uri info 308 * @param subject the subject 309 * @return breadcrumbs 310 */ 311 public Map<String, String> getNodeBreadcrumbs(final UriInfo uriInfo, 312 final Node subject) { 313 final String topic = subject.getURI(); 314 315 LOGGER.trace("Generating breadcrumbs for subject {}", subject); 316 final ImmutableMap.Builder<String, String> builder = 317 ImmutableMap.builder(); 318 319 final String baseUri = uriInfo.getBaseUri().toString(); 320 321 if (!topic.startsWith(baseUri)) { 322 LOGGER.trace("Topic wasn't part of our base URI {}", baseUri); 323 return builder.build(); 324 } 325 326 final String salientPath = topic.substring(baseUri.length()); 327 328 final String[] split = salientPath.split("/"); 329 330 final StringBuilder cumulativePath = new StringBuilder(); 331 332 for (final String path : split) { 333 334 if (path.isEmpty()) { 335 continue; 336 } 337 338 cumulativePath.append(path); 339 340 final String uri = 341 uriInfo.getBaseUriBuilder().path(cumulativePath.toString()) 342 .build().toString(); 343 344 LOGGER.trace("Adding breadcrumb for path segment {} => {}", path, 345 uri); 346 347 builder.put(uri, path); 348 349 cumulativePath.append("/"); 350 351 } 352 353 return builder.build(); 354 355 } 356 357 /** 358 * Sort a Iterator of Triples alphabetically by its subject, predicate, and 359 * object 360 * 361 * @param model the model 362 * @param it the iterator of triples 363 * @return iterator of alphabetized triples 364 */ 365 public List<Triple> getSortedTriples(final Model model, final Iterator<Triple> it) { 366 final List<Triple> triples = newArrayList(it); 367 triples.sort(new TripleOrdering(model)); 368 return triples; 369 } 370 371 /** 372 * Get the namespace prefix (or the namespace URI itself, if no prefix is 373 * available) from a prefix mapping 374 * 375 * @param mapping the prefix mapping 376 * @param namespace the namespace 377 * @param compact the boolean value of compact 378 * @return namespace prefix 379 */ 380 public String getNamespacePrefix(final PrefixMapping mapping, 381 final String namespace, final boolean compact) { 382 final String nsURIPrefix = mapping.getNsURIPrefix(namespace); 383 384 if (nsURIPrefix == null) { 385 if (compact) { 386 final int hashIdx = namespace.lastIndexOf('#'); 387 388 final int split; 389 390 if (hashIdx > 0) { 391 split = namespace.substring(0, hashIdx).lastIndexOf('/'); 392 } else { 393 split = namespace.lastIndexOf('/'); 394 } 395 396 if (split > 0) { 397 return "..." + namespace.substring(split); 398 } 399 return namespace; 400 } 401 return namespace; 402 } 403 return nsURIPrefix + ":"; 404 } 405 406 /** 407 * Get a prefix preamble appropriate for a SPARQL-UPDATE query from a prefix 408 * mapping object 409 * 410 * @param mapping the prefix mapping 411 * @return prefix preamble 412 */ 413 public String getPrefixPreamble(final PrefixMapping mapping) { 414 return mapping.getNsPrefixMap().entrySet().stream() 415 .map(e -> "PREFIX " + e.getKey() + ": <" + e.getValue() + ">").collect(joining("\n", "", "\n\n")); 416 } 417 418 /** 419 * Determines whether the subject is kind of RDF resource 420 * @param graph the graph 421 * @param subject the subject 422 * @param namespace the namespace 423 * @param resource the resource 424 * @return whether the subject is kind of RDF resource 425 */ 426 public boolean isRdfResource(final Graph graph, 427 final Node subject, 428 final String namespace, 429 final String resource) { 430 return graph.find(subject, createResource(RDF_NAMESPACE + "type").asNode(), 431 createResource(namespace + resource).asNode()).hasNext(); 432 } 433 434 /** 435 * Convert an RDF resource to an RDF node 436 * 437 * @param r the resource 438 * @return RDF node representation of the given RDF resource 439 */ 440 public Node asNode(final Resource r) { 441 return r.asNode(); 442 } 443 444 /** 445 * Convert a URI string to an RDF node 446 * 447 * @param r the uri string 448 * @return RDF node representation of the given string 449 */ 450 public Node asLiteralStringNode(final String r) { 451 return ResourceFactory.createPlainLiteral(r).asNode(); 452 } 453 454 /** 455 * Yes, we really did create a method to increment 456 * a given int. You can't do math in a velocity template. 457 * 458 * @param i the given integer 459 * @return maths 460 */ 461 public int addOne(final int i) { 462 return i + 1; 463 } 464 465 /** 466 * Proxying access to the RDF type static property 467 * @return RDF type property 468 */ 469 public Property rdfType() { 470 return RDF.type; 471 } 472 473 /** 474 * Proxying access to the RDFS domain static property 475 * @return RDFS domain property 476 */ 477 public Property rdfsDomain() { 478 return RDFS.domain; 479 } 480 481 /** 482 * Proxying access to the RDFS class static property 483 * @return RDFS class resource 484 */ 485 public Resource rdfsClass() { 486 return RDFS.Class; 487 } 488 489 /** 490 * Get the content-bearing node for the given subject 491 * @param subject the subject 492 * @return content-bearing node for the given subject 493 */ 494 public Node getContentNode(final Node subject) { 495 return NodeFactory.createURI(subject.getURI().replace(FCR_METADATA, "")); 496 } 497 498 /** 499 * Transform a source string to something appropriate for HTML ids 500 * @param source the source string 501 * @return transformed source string 502 */ 503 public String parameterize(final String source) { 504 return source.toLowerCase().replaceAll("[^a-z0-9\\-_]+", "_"); 505 } 506}