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