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