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