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}