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