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.graph.NodeFactory.createURI; 022import static com.hp.hpl.jena.rdf.model.ResourceFactory.createResource; 023import static com.hp.hpl.jena.rdf.model.ResourceFactory.createProperty; 024import static java.util.stream.Collectors.joining; 025import static org.fcrepo.kernel.api.RdfLexicon.CREATED_DATE; 026import static org.fcrepo.kernel.api.FedoraJcrTypes.FCR_METADATA; 027import static org.fcrepo.kernel.api.RdfLexicon.DC_TITLE; 028import static org.fcrepo.kernel.api.RdfLexicon.DCTERMS_TITLE; 029import static org.fcrepo.kernel.api.RdfLexicon.HAS_VERSION_LABEL; 030import static org.fcrepo.kernel.api.RdfLexicon.RDFS_LABEL; 031import static org.fcrepo.kernel.api.RdfLexicon.SKOS_PREFLABEL; 032import static org.fcrepo.kernel.api.RdfLexicon.HAS_VERSION; 033import static org.fcrepo.kernel.api.RdfLexicon.RDF_NAMESPACE; 034import static org.fcrepo.kernel.api.RdfLexicon.DC_NAMESPACE; 035import static org.slf4j.LoggerFactory.getLogger; 036 037import java.text.SimpleDateFormat; 038import java.util.Date; 039import java.util.Iterator; 040import java.util.List; 041import java.util.Map; 042import java.util.TreeMap; 043import javax.ws.rs.core.UriInfo; 044 045import com.hp.hpl.jena.graph.Graph; 046import com.hp.hpl.jena.graph.Triple; 047 048import org.fcrepo.http.commons.api.rdf.TripleOrdering; 049import org.fcrepo.kernel.api.RdfLexicon; 050 051import org.slf4j.Logger; 052 053import com.google.common.collect.ImmutableMap; 054import com.hp.hpl.jena.graph.Node; 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 LOGGER.trace("Getting Objects as String: s:{}, p:{}, g:{}", subject, predicate, graph); 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 final List<Triple> triples = newArrayList(it); 368 triples.sort(new TripleOrdering(model)); 369 return triples; 370 } 371 372 /** 373 * Get the namespace prefix (or the namespace URI itself, if no prefix is 374 * available) from a prefix mapping 375 * 376 * @param mapping the prefix mapping 377 * @param namespace the namespace 378 * @param compact the boolean value of compact 379 * @return namespace prefix 380 */ 381 public String getNamespacePrefix(final PrefixMapping mapping, 382 final String namespace, final boolean compact) { 383 final String nsURIPrefix = mapping.getNsURIPrefix(namespace); 384 385 if (nsURIPrefix == null) { 386 if (compact) { 387 final int hashIdx = namespace.lastIndexOf('#'); 388 389 final int split; 390 391 if (hashIdx > 0) { 392 split = namespace.substring(0, hashIdx).lastIndexOf('/'); 393 } else { 394 split = namespace.lastIndexOf('/'); 395 } 396 397 if (split > 0) { 398 return "..." + namespace.substring(split); 399 } 400 return namespace; 401 } 402 return namespace; 403 } 404 return nsURIPrefix + ":"; 405 } 406 407 /** 408 * Get a prefix preamble appropriate for a SPARQL-UPDATE query from a prefix 409 * mapping object 410 * 411 * @param mapping the prefix mapping 412 * @return prefix preamble 413 */ 414 public String getPrefixPreamble(final PrefixMapping mapping) { 415 return mapping.getNsPrefixMap().entrySet().stream() 416 .map(e -> "PREFIX " + e.getKey() + ": <" + e.getValue() + ">").collect(joining("\n", "", "\n\n")); 417 } 418 419 /** 420 * Determines whether the subject is kind of RDF resource 421 * @param graph the graph 422 * @param subject the subject 423 * @param namespace the namespace 424 * @param resource the resource 425 * @return whether the subject is kind of RDF resource 426 */ 427 public boolean isRdfResource(final Graph graph, 428 final Node subject, 429 final String namespace, 430 final String resource) { 431 LOGGER.trace("Is RDF Resource? s:{}, ns:{}, r:{}, g:{}", subject, namespace, resource, graph); 432 return graph.find(subject, createResource(RDF_NAMESPACE + "type").asNode(), 433 createResource(namespace + resource).asNode()).hasNext(); 434 } 435 436 /** 437 * Convert an RDF resource to an RDF node 438 * 439 * @param r the resource 440 * @return RDF node representation of the given RDF resource 441 */ 442 public Node asNode(final Resource r) { 443 return r.asNode(); 444 } 445 446 /** 447 * Convert a URI string to an RDF node 448 * 449 * @param r the uri string 450 * @return RDF node representation of the given string 451 */ 452 public Node asLiteralStringNode(final String r) { 453 return ResourceFactory.createPlainLiteral(r).asNode(); 454 } 455 456 /** 457 * Yes, we really did create a method to increment 458 * a given int. You can't do math in a velocity template. 459 * 460 * @param i the given integer 461 * @return maths 462 */ 463 public int addOne(final int i) { 464 return i + 1; 465 } 466 467 /** 468 * Proxying access to the RDF type static property 469 * @return RDF type property 470 */ 471 public Property rdfType() { 472 return RDF.type; 473 } 474 475 /** 476 * Proxying access to the RDFS domain static property 477 * @return RDFS domain property 478 */ 479 public Property rdfsDomain() { 480 return RDFS.domain; 481 } 482 483 /** 484 * Proxying access to the RDFS class static property 485 * @return RDFS class resource 486 */ 487 public Resource rdfsClass() { 488 return RDFS.Class; 489 } 490 491 /** 492 * Get the content-bearing node for the given subject 493 * @param subject the subject 494 * @return content-bearing node for the given subject 495 */ 496 public Node getContentNode(final Node subject) { 497 return (null == subject) ? null : createURI(subject.getURI().replace("/" + FCR_METADATA, "")); 498 } 499 500 /** 501 * Transform a source string to something appropriate for HTML ids 502 * @param source the source string 503 * @return transformed source string 504 */ 505 public String parameterize(final String source) { 506 return source.toLowerCase().replaceAll("[^a-z0-9\\-_]+", "_"); 507 } 508}