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