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