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.kernel.impl; 017 018import static com.google.common.base.Predicates.not; 019import static com.google.common.base.Throwables.propagate; 020import static com.google.common.collect.Iterators.concat; 021import static com.google.common.collect.Iterators.filter; 022import static com.google.common.collect.Iterators.singletonIterator; 023import static com.google.common.collect.Iterators.transform; 024import static com.google.common.collect.Lists.newArrayList; 025import static com.hp.hpl.jena.update.UpdateAction.execute; 026import static com.hp.hpl.jena.update.UpdateFactory.create; 027import static org.apache.commons.codec.digest.DigestUtils.shaHex; 028import static org.fcrepo.kernel.impl.identifiers.NodeResourceConverter.nodeConverter; 029import static org.fcrepo.kernel.impl.utils.FedoraTypesUtils.isFrozenNode; 030import static org.fcrepo.kernel.impl.utils.FedoraTypesUtils.isInternalNode; 031import static org.fcrepo.kernel.services.functions.JcrPropertyFunctions.isFrozen; 032import static org.fcrepo.kernel.services.functions.JcrPropertyFunctions.property2values; 033import static org.fcrepo.kernel.services.functions.JcrPropertyFunctions.value2string; 034import static org.modeshape.jcr.api.JcrConstants.JCR_CONTENT; 035import static org.slf4j.LoggerFactory.getLogger; 036 037import java.lang.reflect.Constructor; 038import java.lang.reflect.InvocationTargetException; 039import java.util.Collections; 040import java.util.Date; 041import java.util.Iterator; 042import java.util.List; 043 044 045import javax.jcr.ItemNotFoundException; 046import javax.jcr.Node; 047import javax.jcr.PathNotFoundException; 048import javax.jcr.Property; 049import javax.jcr.RepositoryException; 050import javax.jcr.Session; 051import javax.jcr.version.LabelExistsVersionException; 052import javax.jcr.version.Version; 053import javax.jcr.version.VersionHistory; 054 055import com.google.common.base.Converter; 056import com.google.common.base.Function; 057import com.google.common.base.Predicate; 058import com.google.common.collect.Iterators; 059import com.hp.hpl.jena.rdf.model.Resource; 060import org.fcrepo.kernel.FedoraJcrTypes; 061import org.fcrepo.kernel.models.NonRdfSourceDescription; 062import org.fcrepo.kernel.models.FedoraBinary; 063import org.fcrepo.kernel.models.FedoraResource; 064import org.fcrepo.kernel.exception.MalformedRdfException; 065import org.fcrepo.kernel.exception.PathNotFoundRuntimeException; 066import org.fcrepo.kernel.exception.RepositoryRuntimeException; 067import org.fcrepo.kernel.identifiers.IdentifierConverter; 068import org.fcrepo.kernel.impl.utils.JcrPropertyStatementListener; 069import org.fcrepo.kernel.utils.iterators.GraphDifferencingIterator; 070import org.fcrepo.kernel.impl.utils.iterators.RdfAdder; 071import org.fcrepo.kernel.impl.utils.iterators.RdfRemover; 072import org.fcrepo.kernel.utils.iterators.NodeIterator; 073import org.fcrepo.kernel.utils.iterators.PropertyIterator; 074import org.fcrepo.kernel.utils.iterators.RdfStream; 075import org.modeshape.jcr.api.JcrTools; 076import org.slf4j.Logger; 077 078import com.hp.hpl.jena.rdf.model.Model; 079import com.hp.hpl.jena.update.UpdateRequest; 080 081/** 082 * Common behaviors across FedoraObject and Datastream types; also used 083 * when the exact type of an object is irrelevant 084 * 085 * @author ajs6f 086 */ 087public class FedoraResourceImpl extends JcrTools implements FedoraJcrTypes, FedoraResource { 088 089 private static final Logger LOGGER = getLogger(FedoraResourceImpl.class); 090 091 protected Node node; 092 093 /** 094 * Construct a FedoraObject from an existing JCR Node 095 * @param node an existing JCR node to treat as an fcrepo object 096 */ 097 public FedoraResourceImpl(final Node node) { 098 this.node = node; 099 } 100 101 /* (non-Javadoc) 102 * @see org.fcrepo.kernel.models.FedoraResource#getNode() 103 */ 104 @Override 105 public Node getNode() { 106 return node; 107 } 108 109 /* (non-Javadoc) 110 * @see org.fcrepo.kernel.models.FedoraResource#getPath() 111 */ 112 @Override 113 public String getPath() { 114 try { 115 return node.getPath(); 116 } catch (final RepositoryException e) { 117 throw new RepositoryRuntimeException(e); 118 } 119 } 120 121 /* (non-Javadoc) 122 * @see org.fcrepo.kernel.models.FedoraResource#getChildren() 123 */ 124 @Override 125 public Iterator<FedoraResource> getChildren() { 126 try { 127 return concat(nodeToGoodChildren(node)); 128 } catch (final RepositoryException e) { 129 throw new RepositoryRuntimeException(e); 130 } 131 } 132 133 /** 134 * Get the "good" children for a node by skipping all pairtree nodes in the way. 135 * @param input 136 * @return 137 * @throws RepositoryException 138 */ 139 private Iterator<Iterator<FedoraResource>> nodeToGoodChildren(final Node input) throws RepositoryException { 140 final Iterator<Node> children = filter(new NodeIterator(input.getNodes()), not(nastyChildren)); 141 return transform(children, new Function<Node, Iterator<FedoraResource>>() { 142 143 @Override 144 public Iterator<FedoraResource> apply(final Node input) { 145 try { 146 if (input.isNodeType(FEDORA_PAIRTREE)) { 147 return concat(nodeToGoodChildren(input)); 148 } 149 return singletonIterator(nodeToObjectBinaryConverter.convert(input)); 150 } catch (final RepositoryException e) { 151 throw new RepositoryRuntimeException(e); 152 } 153 } 154 }); 155 } 156 /** 157 * Children for whom we will not generate triples. 158 */ 159 private static Predicate<Node> nastyChildren = 160 new Predicate<Node>() { 161 162 @Override 163 public boolean apply(final Node n) { 164 LOGGER.trace("Testing child node {}", n); 165 try { 166 return isInternalNode.apply(n) 167 || n.getName().equals(JCR_CONTENT) 168 || TombstoneImpl.hasMixin(n) 169 || n.getName().equals("#"); 170 } catch (final RepositoryException e) { 171 throw new RepositoryRuntimeException(e); 172 } 173 } 174 }; 175 176 177 private static final Converter<FedoraResource, FedoraResource> datastreamToBinary 178 = new Converter<FedoraResource, FedoraResource>() { 179 180 @Override 181 protected FedoraResource doForward(final FedoraResource fedoraResource) { 182 if (fedoraResource instanceof NonRdfSourceDescription) { 183 return ((NonRdfSourceDescription) fedoraResource).getDescribedResource(); 184 } 185 return fedoraResource; 186 } 187 188 @Override 189 protected FedoraResource doBackward(final FedoraResource fedoraResource) { 190 if (fedoraResource instanceof FedoraBinary) { 191 return ((FedoraBinary) fedoraResource).getDescription(); 192 } 193 return fedoraResource; 194 } 195 }; 196 197 private static final Converter<Node, FedoraResource> nodeToObjectBinaryConverter 198 = nodeConverter.andThen(datastreamToBinary); 199 200 @Override 201 public FedoraResource getContainer() { 202 try { 203 204 if (getNode().getDepth() == 0) { 205 return null; 206 } 207 208 Node container = getNode().getParent(); 209 while (container.getDepth() > 0) { 210 if (container.isNodeType(FEDORA_PAIRTREE) 211 || container.isNodeType(FEDORA_NON_RDF_SOURCE_DESCRIPTION)) { 212 container = container.getParent(); 213 } else { 214 return nodeConverter.convert(container); 215 } 216 } 217 218 return nodeConverter.convert(container); 219 } catch (final RepositoryException e) { 220 throw new RepositoryRuntimeException(e); 221 } 222 } 223 224 @Override 225 public FedoraResource getChild(final String relPath) { 226 try { 227 return nodeConverter.convert(getNode().getNode(relPath)); 228 } catch (final RepositoryException e) { 229 throw new RepositoryRuntimeException(e); 230 } 231 } 232 233 @Override 234 public boolean hasProperty(final String relPath) { 235 try { 236 return getNode().hasProperty(relPath); 237 } catch (final RepositoryException e) { 238 throw new RepositoryRuntimeException(e); 239 } 240 } 241 242 @Override 243 public Property getProperty(final String relPath) { 244 try { 245 return getNode().getProperty(relPath); 246 } catch (final RepositoryException e) { 247 throw new RepositoryRuntimeException(e); 248 } 249 } 250 251 @Override 252 public void delete() { 253 try { 254 final Iterator<Property> inboundProperties = Iterators.concat( 255 new PropertyIterator(node.getReferences()), 256 new PropertyIterator(node.getWeakReferences())); 257 258 while (inboundProperties.hasNext()) { 259 inboundProperties.next().remove(); 260 } 261 262 final Node parent; 263 264 if (getNode().getDepth() > 0) { 265 parent = getNode().getParent(); 266 } else { 267 parent = null; 268 } 269 final String name = getNode().getName(); 270 271 node.remove(); 272 273 if (parent != null) { 274 createTombstone(parent, name); 275 } 276 277 } catch (final RepositoryException e) { 278 throw new RepositoryRuntimeException(e); 279 } 280 } 281 282 private void createTombstone(final Node parent, final String path) throws RepositoryException { 283 findOrCreateChild(parent, path, FEDORA_TOMBSTONE); 284 } 285 286 /* (non-Javadoc) 287 * @see org.fcrepo.kernel.models.FedoraResource#getCreatedDate() 288 */ 289 @Override 290 public Date getCreatedDate() { 291 try { 292 if (hasProperty(JCR_CREATED)) { 293 return new Date(getProperty(JCR_CREATED).getDate().getTimeInMillis()); 294 } 295 } catch (final PathNotFoundException e) { 296 throw new PathNotFoundRuntimeException(e); 297 } catch (final RepositoryException e) { 298 throw new RepositoryRuntimeException(e); 299 } 300 LOGGER.debug("Node {} does not have a createdDate", node); 301 return null; 302 } 303 304 /* (non-Javadoc) 305 * @see org.fcrepo.kernel.models.FedoraResource#getLastModifiedDate() 306 */ 307 @Override 308 public Date getLastModifiedDate() { 309 310 try { 311 if (hasProperty(JCR_LASTMODIFIED)) { 312 return new Date(getProperty(JCR_LASTMODIFIED).getDate().getTimeInMillis()); 313 } 314 } catch (final PathNotFoundException e) { 315 throw new PathNotFoundRuntimeException(e); 316 } catch (final RepositoryException e) { 317 throw new RepositoryRuntimeException(e); 318 } 319 LOGGER.debug("Could not get last modified date property for node {}", node); 320 321 final Date createdDate = getCreatedDate(); 322 if (createdDate != null) { 323 LOGGER.trace("Using created date for last modified date for node {}", node); 324 return createdDate; 325 } 326 327 return null; 328 } 329 330 331 @Override 332 public boolean hasType(final String type) { 333 try { 334 if (isFrozen.apply(node) && hasProperty(FROZEN_MIXIN_TYPES)) { 335 final List<String> types = newArrayList( 336 transform(property2values.apply(getProperty(FROZEN_MIXIN_TYPES)), value2string) 337 ); 338 return types.contains(type); 339 } 340 return node.isNodeType(type); 341 } catch (final PathNotFoundException e) { 342 throw new PathNotFoundRuntimeException(e); 343 } catch (final RepositoryException e) { 344 throw new RepositoryRuntimeException(e); 345 } 346 } 347 348 /* (non-Javadoc) 349 * @see org.fcrepo.kernel.models.FedoraResource#updateProperties 350 * (org.fcrepo.kernel.identifiers.IdentifierConverter, java.lang.String, RdfStream) 351 */ 352 @Override 353 public void updateProperties(final IdentifierConverter<Resource, FedoraResource> idTranslator, 354 final String sparqlUpdateStatement, final RdfStream originalTriples) 355 throws MalformedRdfException { 356 357 final Model model = originalTriples.asModel(); 358 359 final JcrPropertyStatementListener listener = 360 new JcrPropertyStatementListener(idTranslator, getSession()); 361 362 model.register(listener); 363 364 final UpdateRequest request = create(sparqlUpdateStatement, idTranslator.reverse().convert(this).toString()); 365 model.setNsPrefixes(request.getPrefixMapping()); 366 execute(request, model); 367 368 listener.assertNoExceptions(); 369 } 370 371 @Override 372 public RdfStream getTriples(final IdentifierConverter<Resource, FedoraResource> idTranslator, 373 final Class<? extends RdfStream> context) { 374 return getTriples(idTranslator, Collections.singleton(context)); 375 } 376 377 @Override 378 public RdfStream getTriples(final IdentifierConverter<Resource, FedoraResource> idTranslator, 379 final Iterable<? extends Class<? extends RdfStream>> contexts) { 380 final RdfStream stream = new RdfStream(); 381 382 for (final Class<? extends RdfStream> context : contexts) { 383 try { 384 final Constructor<? extends RdfStream> declaredConstructor 385 = context.getDeclaredConstructor(FedoraResource.class, IdentifierConverter.class); 386 387 final RdfStream rdfStream = declaredConstructor.newInstance(this, idTranslator); 388 389 stream.concat(rdfStream); 390 } catch (final NoSuchMethodException | 391 InstantiationException | 392 IllegalAccessException e) { 393 // Shouldn't happen. 394 throw propagate(e); 395 } catch (final InvocationTargetException e) { 396 final Throwable cause = e.getCause(); 397 if (cause instanceof RepositoryException) { 398 throw new RepositoryRuntimeException(cause); 399 } 400 throw propagate(cause); 401 } 402 } 403 404 return stream; 405 } 406 407 /* (non-Javadoc) 408 * @see org.fcrepo.kernel.models.FedoraResource#addVersionLabel(java.lang.String) 409 */ 410 @Override 411 public void addVersionLabel(final String label) { 412 try { 413 final VersionHistory versionHistory = getVersionHistory(); 414 if (versionHistory.hasVersionLabel(label)) { 415 // ModeShape should do this, but it just throws a VersionException 416 // the bug has been reported here: https://issues.jboss.org/browse/MODE-2372 417 throw new LabelExistsVersionException("The specified label \"" + label 418 + "\" is already assigned to another version of this resource!"); 419 } 420 versionHistory.addVersionLabel(getBaseVersion().getName(), label, false); 421 } catch (final RepositoryException e) { 422 throw new RepositoryRuntimeException(e); 423 } 424 } 425 426 /* 427 * (non-Javadoc) 428 * @see org.fcrepo.kernel.models.FedoraResource#getBaseVersion() 429 */ 430 @Override 431 public Version getBaseVersion() { 432 try { 433 return getSession().getWorkspace().getVersionManager().getBaseVersion(getPath()); 434 } catch (final RepositoryException e) { 435 throw new RepositoryRuntimeException(e); 436 } 437 } 438 439 /* 440 * (non-Javadoc) 441 * @see org.fcrepo.kernel.models.FedoraResource#getVersionHistory() 442 */ 443 @Override 444 public VersionHistory getVersionHistory() { 445 try { 446 return getSession().getWorkspace().getVersionManager().getVersionHistory(getPath()); 447 } catch (final RepositoryException e) { 448 throw new RepositoryRuntimeException(e); 449 } 450 } 451 452 /* (non-Javadoc) 453 * @see org.fcrepo.kernel.models.FedoraResource#isNew() 454 */ 455 @Override 456 public Boolean isNew() { 457 return node.isNew(); 458 } 459 460 /* (non-Javadoc) 461 * @see org.fcrepo.kernel.models.FedoraResource#replaceProperties 462 * (org.fcrepo.kernel.identifiers.IdentifierConverter, com.hp.hpl.jena.rdf.model.Model) 463 */ 464 @Override 465 public void replaceProperties(final IdentifierConverter<Resource, FedoraResource> idTranslator, 466 final Model inputModel, final RdfStream originalTriples) throws MalformedRdfException { 467 468 final RdfStream replacementStream = new RdfStream().namespaces(inputModel.getNsPrefixMap()); 469 470 final GraphDifferencingIterator differencer = 471 new GraphDifferencingIterator(inputModel, originalTriples); 472 473 final StringBuilder exceptions = new StringBuilder(); 474 try { 475 new RdfRemover(idTranslator, getSession(), replacementStream 476 .withThisContext(differencer)).consume(); 477 } catch (final MalformedRdfException e) { 478 exceptions.append(e.getMessage()); 479 exceptions.append("\n"); 480 } 481 482 try { 483 new RdfAdder(idTranslator, getSession(), replacementStream 484 .withThisContext(differencer.notCommon())).consume(); 485 } catch (final MalformedRdfException e) { 486 exceptions.append(e.getMessage()); 487 } 488 489 if (exceptions.length() > 0) { 490 throw new MalformedRdfException(exceptions.toString()); 491 } 492 } 493 494 /* (non-Javadoc) 495 * @see org.fcrepo.kernel.models.FedoraResource#getEtagValue() 496 */ 497 @Override 498 public String getEtagValue() { 499 final Date lastModifiedDate = getLastModifiedDate(); 500 501 if (lastModifiedDate != null) { 502 return shaHex(getPath() + lastModifiedDate.getTime()); 503 } 504 return ""; 505 } 506 507 @Override 508 public void enableVersioning() { 509 try { 510 node.addMixin("mix:versionable"); 511 } catch (final RepositoryException e) { 512 throw new RepositoryRuntimeException(e); 513 } 514 } 515 516 @Override 517 public void disableVersioning() { 518 try { 519 node.removeMixin("mix:versionable"); 520 } catch (final RepositoryException e) { 521 throw new RepositoryRuntimeException(e); 522 } 523 524 } 525 526 @Override 527 public boolean isVersioned() { 528 try { 529 return node.isNodeType("mix:versionable"); 530 } catch (final RepositoryException e) { 531 throw new RepositoryRuntimeException(e); 532 } 533 } 534 535 @Override 536 public boolean isFrozenResource() { 537 return isFrozenNode.apply(this); 538 } 539 540 @Override 541 public FedoraResource getVersionedAncestor() { 542 543 try { 544 if (!isFrozenResource()) { 545 return null; 546 } 547 548 Node versionableFrozenNode = getNode(); 549 FedoraResource unfrozenResource = getUnfrozenResource(); 550 551 // traverse the frozen tree looking for a node whose unfrozen equivalent is versioned 552 while (!unfrozenResource.isVersioned()) { 553 554 if (versionableFrozenNode.getDepth() == 0) { 555 return null; 556 } 557 558 // node in the frozen tree 559 versionableFrozenNode = versionableFrozenNode.getParent(); 560 561 // unfrozen equivalent 562 unfrozenResource = new FedoraResourceImpl(versionableFrozenNode).getUnfrozenResource(); 563 } 564 565 return new FedoraResourceImpl(versionableFrozenNode); 566 } catch (final RepositoryException e) { 567 throw new RepositoryRuntimeException(e); 568 } 569 570 } 571 572 @Override 573 public FedoraResource getUnfrozenResource() { 574 if (!isFrozenResource()) { 575 return this; 576 } 577 578 try { 579 return new FedoraResourceImpl(getSession().getNodeByIdentifier(getProperty("jcr:frozenUuid").getString())); 580 } catch (final RepositoryException e) { 581 throw new RepositoryRuntimeException(e); 582 } 583 } 584 585 @Override 586 public Node getNodeVersion(final String label) { 587 try { 588 final Session session = getSession(); 589 try { 590 591 final Node frozenNode = session.getNodeByIdentifier(label); 592 593 final String baseUUID = getNode().getIdentifier(); 594 595 /* 596 * We found a node whose identifier is the "label" for the version. Now 597 * we must do due dilligence to make sure it's a frozen node representing 598 * a version of the subject node. 599 */ 600 final Property p = frozenNode.getProperty("jcr:frozenUuid"); 601 if (p != null) { 602 if (p.getString().equals(baseUUID)) { 603 return frozenNode; 604 } 605 } 606 /* 607 * Though a node with an id of the label was found, it wasn't the 608 * node we were looking for, so fall through and look for a labeled 609 * node. 610 */ 611 } catch (final ItemNotFoundException ex) { 612 /* 613 * the label wasn't a uuid of a frozen node but 614 * instead possibly a version label. 615 */ 616 } 617 618 if (isVersioned()) { 619 final VersionHistory hist = 620 session.getWorkspace().getVersionManager().getVersionHistory(getPath()); 621 622 if (hist.hasVersionLabel(label)) { 623 LOGGER.debug("Found version for {} by label {}.", this, label); 624 return hist.getVersionByLabel(label).getFrozenNode(); 625 } 626 } 627 628 LOGGER.warn("Unknown version {} with label or uuid {}!", this, label); 629 return null; 630 } catch (final RepositoryException e) { 631 throw new RepositoryRuntimeException(e); 632 } 633 634 } 635 636 @Override 637 public boolean equals(final Object object) { 638 if (object instanceof FedoraResourceImpl) { 639 return ((FedoraResourceImpl) object).getNode().equals(this.getNode()); 640 } 641 return false; 642 } 643 644 @Override 645 public int hashCode() { 646 return getNode().hashCode(); 647 } 648 649 protected Session getSession() { 650 try { 651 return getNode().getSession(); 652 } catch (final RepositoryException e) { 653 throw new RepositoryRuntimeException(e); 654 } 655 } 656 657 @Override 658 public String toString() { 659 return getNode().toString(); 660 } 661}