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 092 * @param subject 093 * @param 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 105 * @param 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 118 * @param subject 119 * @return iterator 120 */ 121 public Iterator<Node> getOrderedVersions(final Graph graph, 122 final Node subject, final Resource predicate) { 123 final Iterator<Triple> versions = getObjects(graph, subject, predicate); 124 final Map<String, Node> map = new TreeMap<>(); 125 final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); 126 Triple triple; 127 String date; 128 while (versions.hasNext()) { 129 triple = versions.next(); 130 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 */ 146 public String getVersionSubjectUrl(final UriInfo uriInfo, final Node subject) { 147 final Map<String, String> breadcrumbs = getNodeBreadcrumbs(uriInfo, subject); 148 String lastUrl = null; 149 for (final Map.Entry<String, String> entry : breadcrumbs.entrySet()) { 150 if (entry.getValue().equals("fcr:versions")) { 151 return lastUrl; 152 } 153 lastUrl = entry.getKey(); 154 } 155 return null; 156 } 157 158 /** 159 * Gets a version label of a subject from the graph 160 * 161 * @param graph 162 * @param subject 163 * @param defaultValue a value to be returned if no label is present in the 164 * graph 165 * @return the label of the version if one has been provided; otherwise 166 * the default is returned 167 */ 168 public String getVersionLabel(final Graph graph, 169 final Node subject, final String defaultValue) { 170 final Iterator<Triple> objects = getObjects(graph, subject, HAS_VERSION_LABEL); 171 if (objects.hasNext()) { 172 return objects.next().getObject().getLiteralValue().toString(); 173 } 174 return defaultValue; 175 } 176 177 /** 178 * Gets a modification date of a subject from the graph 179 * 180 * @param graph 181 * @param subject 182 * @return the modification date or null if none exists 183 */ 184 public String getVersionDate(final Graph graph, 185 final Node subject) { 186 final Iterator<Triple> objects = getObjects(graph, subject, CREATED_DATE); 187 if (objects.hasNext()) { 188 return objects.next().getObject().getLiteralValue().toString(); 189 } 190 return ""; 191 } 192 193 /** 194 * Get the canonical title of a subject from the graph 195 * 196 * @param graph 197 * @param subject 198 * @return canonical title of the subject in the graph 199 */ 200 public String getObjectTitle(final Graph graph, final Node subject) { 201 202 if (subject == null) { 203 return ""; 204 } 205 206 final Property[] properties = new Property[] {RDFS_LABEL, DC_TITLE, DCTERMS_TITLE, SKOS_PREFLABEL}; 207 208 for (final Property p : properties) { 209 final Iterator<Triple> objects = getObjects(graph, subject, p); 210 211 if (objects.hasNext()) { 212 return objects.next().getObject().getLiteral().getLexicalForm(); 213 } 214 } 215 216 if (subject.isURI()) { 217 return subject.getURI(); 218 } else if (subject.isBlank()) { 219 return subject.getBlankNodeLabel(); 220 } else { 221 return subject.toString(); 222 } 223 224 } 225 226 /** 227 * Take a HAS_SERIALIZATION node and find the RDFS_LABEL for the format it is associated with 228 * 229 * @param graph 230 * @param subject 231 * @return the label for the serialization format 232 */ 233 public String getSerializationTitle(final Graph graph, final Node subject) { 234 final Property dcFormat = createProperty(DC_NAMESPACE + "format"); 235 final Iterator<Triple> formatRDFs = getObjects(graph, subject, dcFormat); 236 if (formatRDFs.hasNext()) { 237 return getObjectTitle(graph, formatRDFs.next().getObject()); 238 } 239 return ""; 240 } 241 242 /** 243 * Determines whether the subject is writable 244 * true if node is writable 245 */ 246 public boolean isWritable(final Graph graph, final Node subject) { 247 final Iterator<Triple> it = getObjects(graph, subject, RdfLexicon.WRITABLE); 248 return it.hasNext() && it.next().getObject().getLiteralValue().toString().equals("true"); 249 } 250 251 /** 252 * Determines whether the subject is of type nt:frozenNode. 253 * true if node has type nt:frozen 254 */ 255 public boolean isFrozenNode(final Graph graph, final Node subject) { 256 final Iterator<Triple> objects = getObjects(graph, subject, RdfLexicon.HAS_PRIMARY_TYPE); 257 return objects.hasNext() 258 && objects.next().getObject() 259 .getLiteralValue().toString().equals("nt:frozenNode"); 260 } 261 262 /** 263 * Get the string version of the object that matches the given subject and 264 * predicate 265 * 266 * @param graph 267 * @param subject 268 * @param predicate 269 * @return string version of the object 270 */ 271 public String getObjectsAsString(final Graph graph, 272 final Node subject, final Resource predicate, final boolean uriAsLink) { 273 final Iterator<Triple> iterator = getObjects(graph, subject, predicate); 274 275 if (iterator.hasNext()) { 276 final Node object = iterator.next().getObject(); 277 278 if (object.isLiteral()) { 279 final String s = object.getLiteralValue().toString(); 280 if (s.isEmpty()) { 281 return "<empty>"; 282 } 283 return s; 284 } 285 if (uriAsLink) { 286 return "<<a href=\"" + object.getURI() + "\">" + 287 object.getURI() + "</a>>"; 288 } 289 return object.getURI(); 290 } 291 return ""; 292 } 293 294 /** 295 * Generate url to local name breadcrumbs for a given node's tree 296 * 297 * @param uriInfo 298 * @param subject 299 * @return breadcrumbs 300 */ 301 public Map<String, String> getNodeBreadcrumbs(final UriInfo uriInfo, 302 final Node subject) { 303 final String topic = subject.getURI(); 304 305 LOGGER.trace("Generating breadcrumbs for subject {}", subject); 306 final ImmutableMap.Builder<String, String> builder = 307 ImmutableMap.builder(); 308 309 final String baseUri = uriInfo.getBaseUri().toString(); 310 311 if (!topic.startsWith(baseUri)) { 312 LOGGER.trace("Topic wasn't part of our base URI {}", baseUri); 313 return builder.build(); 314 } 315 316 final String salientPath = topic.substring(baseUri.length()); 317 318 final String[] split = salientPath.split("/"); 319 320 final StringBuilder cumulativePath = new StringBuilder(); 321 322 for (final String path : split) { 323 324 if (path.isEmpty()) { 325 continue; 326 } 327 328 cumulativePath.append(path); 329 330 final String uri = 331 uriInfo.getBaseUriBuilder().path(cumulativePath.toString()) 332 .build().toString(); 333 334 LOGGER.trace("Adding breadcrumb for path segment {} => {}", path, 335 uri); 336 337 builder.put(uri, path); 338 339 cumulativePath.append("/"); 340 341 } 342 343 return builder.build(); 344 345 } 346 347 /** 348 * Sort a Iterator of Triples alphabetically by its subject, predicate, and 349 * object 350 * 351 * @param model 352 * @param it 353 * @return iterator of alphabetized triples 354 */ 355 public List<Triple> getSortedTriples(final Model model, final Iterator<Triple> it) { 356 return Ordering.from(new TripleOrdering(model)).sortedCopy(ImmutableList.copyOf(it)); 357 } 358 359 /** 360 * Get the namespace prefix (or the namespace URI itself, if no prefix is 361 * available) from a prefix mapping 362 * 363 * @param mapping 364 * @param namespace 365 * @return namespace prefix 366 */ 367 public String getNamespacePrefix(final PrefixMapping mapping, 368 final String namespace, final boolean compact) { 369 final String nsURIPrefix = mapping.getNsURIPrefix(namespace); 370 371 if (nsURIPrefix == null) { 372 if (compact) { 373 final int hashIdx = namespace.lastIndexOf('#'); 374 375 final int split; 376 377 if (hashIdx > 0) { 378 split = namespace.substring(0, hashIdx).lastIndexOf('/'); 379 } else { 380 split = namespace.lastIndexOf('/'); 381 } 382 383 if (split > 0) { 384 return "..." + namespace.substring(split); 385 } 386 return namespace; 387 } 388 return namespace; 389 } 390 return nsURIPrefix + ":"; 391 } 392 393 /** 394 * Get a prefix preamble appropriate for a SPARQL-UPDATE query from a prefix 395 * mapping object 396 * 397 * @param mapping 398 * @return prefix preamble 399 */ 400 public String getPrefixPreamble(final PrefixMapping mapping) { 401 final StringBuilder sb = new StringBuilder(); 402 403 final Map<String, String> nsPrefixMap = mapping.getNsPrefixMap(); 404 405 for (final Map.Entry<String, String> entry : nsPrefixMap.entrySet()) { 406 sb.append("PREFIX " + entry.getKey() + ": <" + entry.getValue() + 407 ">\n"); 408 } 409 410 sb.append("\n"); 411 return sb.toString(); 412 } 413 414 /** 415 * Determines whether the subject is kind of RDF resource 416 */ 417 public boolean isRdfResource(final Graph graph, 418 final Node subject, 419 final String namespace, 420 final String resource) { 421 final Iterator<Triple> it = graph.find(subject, 422 createResource(RDF_NAMESPACE + "type").asNode(), 423 createResource(namespace + resource).asNode()); 424 return it.hasNext(); 425 } 426 427 /** 428 * Convert an RDF resource to an RDF node 429 * 430 * @param r 431 * @return RDF node representation of the given RDF resource 432 */ 433 public Node asNode(final Resource r) { 434 return r.asNode(); 435 } 436 437 /** 438 * Convert a URI string to an RDF node 439 * 440 * @param r 441 * @return RDF node representation of the given string 442 */ 443 public Node asLiteralStringNode(final String r) { 444 return ResourceFactory.createPlainLiteral(r).asNode(); 445 } 446 447 /** 448 * Yes, we really did create a method to increment 449 * a given int. You can't do math in a velocity template. 450 * 451 * @param i 452 * @return maths 453 */ 454 public int addOne(final int i) { 455 return i + 1; 456 } 457 458 /** 459 * Proxying access to the RDF type static property 460 * @return RDF type property 461 */ 462 public Property rdfType() { 463 return RDF.type; 464 } 465 466 /** 467 * Proxying access to the RDFS domain static property 468 * @return RDFS domain property 469 */ 470 public Property rdfsDomain() { 471 return RDFS.domain; 472 } 473 474 /** 475 * Proxying access to the RDFS class static property 476 * @return RDFS class resource 477 */ 478 public Resource rdfsClass() { 479 return RDFS.Class; 480 } 481 482 /** 483 * Get the content-bearing node for the given subject 484 * @param subject 485 * @return content-bearing node for the given subject 486 */ 487 public Node getContentNode(final Node subject) { 488 return NodeFactory.createURI(subject.getURI().replace(FCR_METADATA, "")); 489 } 490 491 /** 492 * Transform a source string to something appropriate for HTML ids 493 * @param source 494 * @return transformed source string 495 */ 496 public String parameterize(final String source) { 497 return source.toLowerCase().replaceAll("[^a-z0-9\\-_]+", "_"); 498 } 499}