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.hp.hpl.jena.update.UpdateAction.execute;
019import static com.hp.hpl.jena.update.UpdateFactory.create;
020import static java.util.Arrays.asList;
021import static java.util.Collections.singleton;
022import static java.util.stream.Collectors.joining;
023import static java.util.stream.Collectors.toList;
024import static java.util.stream.Stream.concat;
025import static java.util.stream.Stream.empty;
026import static java.util.stream.Stream.of;
027import static org.apache.commons.codec.digest.DigestUtils.shaHex;
028import static org.fcrepo.kernel.api.RdfLexicon.REPOSITORY_NAMESPACE;
029import static org.fcrepo.kernel.api.RdfLexicon.isManagedNamespace;
030import static org.fcrepo.kernel.api.RdfLexicon.isManagedPredicate;
031import static org.fcrepo.kernel.api.RdfCollectors.toModel;
032import static org.fcrepo.kernel.api.RequiredRdfContext.EMBED_RESOURCES;
033import static org.fcrepo.kernel.api.RequiredRdfContext.INBOUND_REFERENCES;
034import static org.fcrepo.kernel.api.RequiredRdfContext.LDP_CONTAINMENT;
035import static org.fcrepo.kernel.api.RequiredRdfContext.LDP_MEMBERSHIP;
036import static org.fcrepo.kernel.api.RequiredRdfContext.MINIMAL;
037import static org.fcrepo.kernel.api.RequiredRdfContext.PROPERTIES;
038import static org.fcrepo.kernel.api.RequiredRdfContext.SERVER_MANAGED;
039import static org.fcrepo.kernel.api.RequiredRdfContext.VERSIONS;
040import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.JCR_CREATED;
041import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.JCR_LASTMODIFIED;
042import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.FROZEN_MIXIN_TYPES;
043import static org.fcrepo.kernel.modeshape.identifiers.NodeResourceConverter.nodeConverter;
044import static org.fcrepo.kernel.modeshape.rdf.JcrRdfTools.getRDFNamespaceForJcrNamespace;
045import static org.fcrepo.kernel.modeshape.services.functions.JcrPropertyFunctions.isFrozen;
046import static org.fcrepo.kernel.modeshape.services.functions.JcrPropertyFunctions.property2values;
047import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.hasInternalNamespace;
048import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.isFrozenNode;
049import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.isInternalNode;
050import static org.fcrepo.kernel.modeshape.utils.NamespaceTools.getNamespaceRegistry;
051import static org.fcrepo.kernel.modeshape.utils.StreamUtils.iteratorToStream;
052import static org.fcrepo.kernel.modeshape.utils.UncheckedFunction.uncheck;
053import static org.modeshape.jcr.api.JcrConstants.JCR_CONTENT;
054import static org.slf4j.LoggerFactory.getLogger;
055
056import java.net.URI;
057import java.util.ArrayList;
058import java.util.Arrays;
059import java.util.Collection;
060import java.util.Date;
061import java.util.Iterator;
062import java.util.List;
063import java.util.Map;
064import java.util.Set;
065import java.util.function.Function;
066import java.util.function.Predicate;
067import java.util.stream.Collectors;
068import java.util.stream.Stream;
069
070import javax.jcr.ItemNotFoundException;
071import javax.jcr.Node;
072import javax.jcr.PathNotFoundException;
073import javax.jcr.Property;
074import javax.jcr.RepositoryException;
075import javax.jcr.Session;
076import javax.jcr.Value;
077import javax.jcr.nodetype.NodeType;
078import javax.jcr.version.Version;
079import javax.jcr.version.VersionHistory;
080import javax.jcr.NamespaceRegistry;
081import javax.jcr.version.VersionManager;
082
083import com.google.common.base.Converter;
084import com.google.common.collect.ImmutableMap;
085import com.google.common.collect.Iterators;
086import com.hp.hpl.jena.rdf.model.Resource;
087import com.hp.hpl.jena.graph.Triple;
088
089import org.fcrepo.kernel.api.FedoraTypes;
090import org.fcrepo.kernel.api.models.NonRdfSourceDescription;
091import org.fcrepo.kernel.api.models.FedoraBinary;
092import org.fcrepo.kernel.api.models.FedoraResource;
093import org.fcrepo.kernel.api.exception.AccessDeniedException;
094import org.fcrepo.kernel.api.exception.ConstraintViolationException;
095import org.fcrepo.kernel.api.exception.InvalidPrefixException;
096import org.fcrepo.kernel.api.exception.MalformedRdfException;
097import org.fcrepo.kernel.api.exception.PathNotFoundRuntimeException;
098import org.fcrepo.kernel.api.exception.RepositoryRuntimeException;
099import org.fcrepo.kernel.api.identifiers.IdentifierConverter;
100import org.fcrepo.kernel.modeshape.rdf.converters.PropertyConverter;
101import org.fcrepo.kernel.api.TripleCategory;
102import org.fcrepo.kernel.api.RdfStream;
103import org.fcrepo.kernel.api.rdf.DefaultRdfStream;
104import org.fcrepo.kernel.api.utils.GraphDifferencer;
105import org.fcrepo.kernel.modeshape.rdf.impl.AclRdfContext;
106import org.fcrepo.kernel.modeshape.rdf.impl.ChildrenRdfContext;
107import org.fcrepo.kernel.modeshape.rdf.impl.ContentRdfContext;
108import org.fcrepo.kernel.modeshape.rdf.impl.HashRdfContext;
109import org.fcrepo.kernel.modeshape.rdf.impl.LdpContainerRdfContext;
110import org.fcrepo.kernel.modeshape.rdf.impl.LdpIsMemberOfRdfContext;
111import org.fcrepo.kernel.modeshape.rdf.impl.LdpRdfContext;
112import org.fcrepo.kernel.modeshape.rdf.impl.ParentRdfContext;
113import org.fcrepo.kernel.modeshape.rdf.impl.PropertiesRdfContext;
114import org.fcrepo.kernel.modeshape.rdf.impl.TypeRdfContext;
115import org.fcrepo.kernel.modeshape.rdf.impl.ReferencesRdfContext;
116import org.fcrepo.kernel.modeshape.rdf.impl.RootRdfContext;
117import org.fcrepo.kernel.modeshape.rdf.impl.SkolemNodeRdfContext;
118import org.fcrepo.kernel.modeshape.rdf.impl.VersionsRdfContext;
119import org.fcrepo.kernel.modeshape.utils.JcrPropertyStatementListener;
120import org.fcrepo.kernel.modeshape.utils.UncheckedPredicate;
121import org.fcrepo.kernel.modeshape.utils.iterators.RdfAdder;
122import org.fcrepo.kernel.modeshape.utils.iterators.RdfRemover;
123
124import org.modeshape.jcr.api.JcrTools;
125import org.slf4j.Logger;
126
127import com.hp.hpl.jena.rdf.model.Model;
128import com.hp.hpl.jena.sparql.modify.request.UpdateData;
129import com.hp.hpl.jena.sparql.modify.request.UpdateDeleteWhere;
130import com.hp.hpl.jena.sparql.modify.request.UpdateModify;
131import com.hp.hpl.jena.update.UpdateRequest;
132
133/**
134 * Common behaviors across {@link org.fcrepo.kernel.api.models.Container} and
135 * {@link org.fcrepo.kernel.api.models.NonRdfSourceDescription} types; also used
136 * when the exact type of an object is irrelevant
137 *
138 * @author ajs6f
139 */
140public class FedoraResourceImpl extends JcrTools implements FedoraTypes, FedoraResource {
141
142    private static final Logger LOGGER = getLogger(FedoraResourceImpl.class);
143
144    private static final String JCR_CHILD_VERSION_HISTORY = "jcr:childVersionHistory";
145    private static final String JCR_VERSIONABLE_UUID = "jcr:versionableUuid";
146    private static final String JCR_FROZEN_UUID = "jcr:frozenUuid";
147
148    private static final PropertyConverter propertyConverter = new PropertyConverter();
149
150    // A curried type accepting resource, translator, and "minimality", returning triples.
151    private static interface RdfGenerator extends Function<FedoraResource,
152    Function<IdentifierConverter<Resource, FedoraResource>, Function<Boolean, Stream<Triple>>>> {}
153
154    @SuppressWarnings("resource")
155    private static RdfGenerator getDefaultTriples = resource -> translator -> uncheck(minimal -> {
156        final Stream<Stream<Triple>> min = of(
157            new TypeRdfContext(resource, translator),
158            new PropertiesRdfContext(resource, translator));
159        if (!minimal) {
160            final Stream<Stream<Triple>> extra = of(
161                new HashRdfContext(resource, translator),
162                new SkolemNodeRdfContext(resource, translator));
163            return concat(min, extra).reduce(empty(), Stream::concat);
164        }
165        return min.reduce(empty(), Stream::concat);
166    });
167
168    private static RdfGenerator getEmbeddedResourceTriples = resource -> translator -> uncheck(minimal ->
169            resource.getChildren().flatMap(child -> child.getTriples(translator, PROPERTIES)));
170
171    private static RdfGenerator getInboundTriples = resource -> translator -> uncheck(_minimal -> {
172        return new ReferencesRdfContext(resource, translator);
173    });
174
175    private static RdfGenerator getLdpContainsTriples = resource -> translator -> uncheck(_minimal -> {
176        return new ChildrenRdfContext(resource, translator);
177    });
178
179    private static RdfGenerator getVersioningTriples = resource -> translator -> uncheck(_minimal -> {
180        return new VersionsRdfContext(resource, translator);
181    });
182
183    @SuppressWarnings("resource")
184    private static RdfGenerator getServerManagedTriples = resource -> translator -> uncheck(minimal -> {
185        if (minimal) {
186            return new LdpRdfContext(resource, translator);
187        }
188        final Stream<Stream<Triple>> streams = of(
189            new LdpRdfContext(resource, translator),
190            new AclRdfContext(resource, translator),
191            new RootRdfContext(resource, translator),
192            new ContentRdfContext(resource, translator),
193            new ParentRdfContext(resource, translator));
194        return streams.reduce(empty(), Stream::concat);
195    });
196
197    @SuppressWarnings("resource")
198    private static RdfGenerator getLdpMembershipTriples = resource -> translator -> uncheck(_minimal -> {
199        final Stream<Stream<Triple>> streams = of(
200            new LdpContainerRdfContext(resource, translator),
201            new LdpIsMemberOfRdfContext(resource, translator));
202        return streams.reduce(empty(), Stream::concat);
203    });
204
205    private static final Map<TripleCategory, RdfGenerator> contextMap =
206            ImmutableMap.<TripleCategory, RdfGenerator>builder()
207                    .put(PROPERTIES, getDefaultTriples)
208                    .put(VERSIONS, getVersioningTriples)
209                    .put(EMBED_RESOURCES, getEmbeddedResourceTriples)
210                    .put(INBOUND_REFERENCES, getInboundTriples)
211                    .put(SERVER_MANAGED, getServerManagedTriples)
212                    .put(LDP_MEMBERSHIP, getLdpMembershipTriples)
213                    .put(LDP_CONTAINMENT, getLdpContainsTriples)
214                    .build();
215
216    protected Node node;
217
218    /**
219     * Construct a {@link org.fcrepo.kernel.api.models.FedoraResource} from an existing JCR Node
220     * @param node an existing JCR node to treat as an fcrepo object
221     */
222    public FedoraResourceImpl(final Node node) {
223        this.node = node;
224    }
225
226    /* (non-Javadoc)
227     * @see org.fcrepo.kernel.api.models.FedoraResource#getNode()
228     */
229    @Override
230    public Node getNode() {
231        return node;
232    }
233
234    /* (non-Javadoc)
235     * @see org.fcrepo.kernel.api.models.FedoraResource#getPath()
236     */
237    @Override
238    public String getPath() {
239        try {
240            return node.getPath();
241        } catch (final RepositoryException e) {
242            throw new RepositoryRuntimeException(e);
243        }
244    }
245
246    /* (non-Javadoc)
247     * @see org.fcrepo.kernel.api.models.FedoraResource#getChildren(Boolean recursive)
248     */
249    @Override
250    public Stream<FedoraResource> getChildren(final Boolean recursive) {
251        try {
252            if (recursive) {
253                return nodeToGoodChildren(node).flatMap(FedoraResourceImpl::getAllChildren);
254            }
255            return nodeToGoodChildren(node);
256        } catch (final RepositoryException e) {
257            throw new RepositoryRuntimeException(e);
258        }
259    }
260
261    /**
262     * Get the "good" children for a node by skipping all pairtree nodes in the way.
263     * @param input
264     * @return
265     * @throws RepositoryException
266     */
267    @SuppressWarnings("unchecked")
268    private Stream<FedoraResource> nodeToGoodChildren(final Node input) throws RepositoryException {
269        return iteratorToStream(input.getNodes()).filter(nastyChildren.negate())
270            .flatMap(uncheck((final Node child) -> child.isNodeType(FEDORA_PAIRTREE) ? nodeToGoodChildren(child) :
271                        of(nodeToObjectBinaryConverter.convert(child))));
272    }
273
274    /**
275     * Get all children recursively, and flatten into a single Stream.
276     */
277    private static Stream<FedoraResource> getAllChildren(final FedoraResource resource) {
278        return concat(of(resource), resource.getChildren().flatMap(FedoraResourceImpl::getAllChildren));
279    }
280
281    /**
282     * Children for whom we will not generate triples.
283     */
284    private static Predicate<Node> nastyChildren = isInternalNode
285                    .or(TombstoneImpl::hasMixin)
286                    .or(UncheckedPredicate.uncheck(p -> p.getName().equals(JCR_CONTENT)))
287                    .or(UncheckedPredicate.uncheck(p -> p.getName().equals("#")));
288
289    private static final Converter<FedoraResource, FedoraResource> datastreamToBinary
290            = new Converter<FedoraResource, FedoraResource>() {
291
292        @Override
293        protected FedoraResource doForward(final FedoraResource fedoraResource) {
294            if (fedoraResource instanceof NonRdfSourceDescription) {
295                return ((NonRdfSourceDescription) fedoraResource).getDescribedResource();
296            }
297            return fedoraResource;
298        }
299
300        @Override
301        protected FedoraResource doBackward(final FedoraResource fedoraResource) {
302            if (fedoraResource instanceof FedoraBinary) {
303                return ((FedoraBinary) fedoraResource).getDescription();
304            }
305            return fedoraResource;
306        }
307    };
308
309    private static final Converter<Node, FedoraResource> nodeToObjectBinaryConverter
310            = nodeConverter.andThen(datastreamToBinary);
311
312    @Override
313    public FedoraResource getContainer() {
314        try {
315
316            if (getNode().getDepth() == 0) {
317                return null;
318            }
319
320            Node container = getNode().getParent();
321            while (container.getDepth() > 0) {
322                if (container.isNodeType(FEDORA_PAIRTREE)
323                        || container.isNodeType(FEDORA_NON_RDF_SOURCE_DESCRIPTION)) {
324                    container = container.getParent();
325                } else {
326                    return nodeConverter.convert(container);
327                }
328            }
329
330            return nodeConverter.convert(container);
331        } catch (final RepositoryException e) {
332            throw new RepositoryRuntimeException(e);
333        }
334    }
335
336    @Override
337    public FedoraResource getChild(final String relPath) {
338        try {
339            return nodeConverter.convert(getNode().getNode(relPath));
340        } catch (final RepositoryException e) {
341            throw new RepositoryRuntimeException(e);
342        }
343    }
344
345    @Override
346    public boolean hasProperty(final String relPath) {
347        try {
348            return getNode().hasProperty(relPath);
349        } catch (final RepositoryException e) {
350            throw new RepositoryRuntimeException(e);
351        }
352    }
353
354    @Override
355    public void delete() {
356        try {
357            final Iterator<Property> references = node.getReferences();
358            final Iterator<Property> weakReferences = node.getWeakReferences();
359            final Iterator<Property> inboundProperties = Iterators.concat(references, weakReferences);
360
361            while (inboundProperties.hasNext()) {
362                final Property prop = inboundProperties.next();
363                final List<Value> newVals = property2values.apply(prop).filter(
364                        UncheckedPredicate.uncheck(value ->
365                            !node.equals(getSession().getNodeByIdentifier(value.getString()))))
366                    .collect(toList());
367
368                if (newVals.size() == 0) {
369                    prop.remove();
370                } else {
371                    prop.setValue(newVals.toArray(new Value[newVals.size()]));
372                }
373            }
374
375            final Node parent;
376
377            if (getNode().getDepth() > 0) {
378                parent = getNode().getParent();
379            } else {
380                parent = null;
381            }
382            final String name = getNode().getName();
383
384            node.remove();
385
386            if (parent != null) {
387                createTombstone(parent, name);
388            }
389
390        } catch (final RepositoryException e) {
391            throw new RepositoryRuntimeException(e);
392        }
393    }
394
395    private void createTombstone(final Node parent, final String path) throws RepositoryException {
396        findOrCreateChild(parent, path, FEDORA_TOMBSTONE);
397    }
398
399    /* (non-Javadoc)
400     * @see org.fcrepo.kernel.api.models.FedoraResource#getCreatedDate()
401     */
402    @Override
403    public Date getCreatedDate() {
404        try {
405            if (hasProperty(JCR_CREATED)) {
406                return new Date(getProperty(JCR_CREATED).getDate().getTimeInMillis());
407            }
408        } catch (final PathNotFoundException e) {
409            throw new PathNotFoundRuntimeException(e);
410        } catch (final RepositoryException e) {
411            throw new RepositoryRuntimeException(e);
412        }
413        LOGGER.debug("Node {} does not have a createdDate", node);
414        return null;
415    }
416
417    /* (non-Javadoc)
418     * @see org.fcrepo.kernel.api.models.FedoraResource#getLastModifiedDate()
419     */
420    @Override
421    public Date getLastModifiedDate() {
422
423        try {
424            if (hasProperty(JCR_LASTMODIFIED)) {
425                return new Date(getProperty(JCR_LASTMODIFIED).getDate().getTimeInMillis());
426            }
427        } catch (final PathNotFoundException e) {
428            throw new PathNotFoundRuntimeException(e);
429        } catch (final RepositoryException e) {
430            throw new RepositoryRuntimeException(e);
431        }
432        LOGGER.debug("Could not get last modified date property for node {}", node);
433
434        final Date createdDate = getCreatedDate();
435        if (createdDate != null) {
436            LOGGER.trace("Using created date for last modified date for node {}", node);
437            return createdDate;
438        }
439
440        return null;
441    }
442
443
444    @Override
445    public boolean hasType(final String type) {
446        try {
447            if (isFrozen.test(node) && hasProperty(FROZEN_MIXIN_TYPES)) {
448                return property2values.apply(getProperty(FROZEN_MIXIN_TYPES)).map(uncheck(Value::getString))
449                    .anyMatch(type::equals);
450            }
451            return node.isNodeType(type);
452        } catch (final PathNotFoundException e) {
453            throw new PathNotFoundRuntimeException(e);
454        } catch (final RepositoryException e) {
455            throw new RepositoryRuntimeException(e);
456        }
457    }
458
459    @Override
460    public List<URI> getTypes() {
461        try {
462            final List<NodeType> nodeTypes = new ArrayList<>();
463            final NodeType primaryNodeType = node.getPrimaryNodeType();
464            nodeTypes.add(primaryNodeType);
465            nodeTypes.addAll(asList(primaryNodeType.getSupertypes()));
466            final List<NodeType> mixinTypes = asList(node.getMixinNodeTypes());
467
468            nodeTypes.addAll(mixinTypes);
469            mixinTypes.stream()
470                .map(NodeType::getSupertypes)
471                .flatMap(Arrays::stream)
472                .forEach(nodeTypes::add);
473
474            final List<URI> types = nodeTypes.stream()
475                .map(uncheck(NodeType::getName))
476                .filter(hasInternalNamespace.negate())
477                .distinct()
478                .map(nodeTypeNameToURI)
479                .peek(x -> LOGGER.debug("node has rdf:type {}", x))
480                .collect(Collectors.toList());
481
482            if (isFrozenResource()) {
483                types.add(URI.create(REPOSITORY_NAMESPACE + "Version"));
484            }
485
486            return types;
487
488        } catch (final PathNotFoundException e) {
489            throw new PathNotFoundRuntimeException(e);
490        } catch (final RepositoryException e) {
491            throw new RepositoryRuntimeException(e);
492        }
493    }
494
495    private final Function<String, URI> nodeTypeNameToURI = uncheck(name -> {
496        final String prefix = name.split(":")[0];
497        final String typeName = name.split(":")[1];
498        final String namespace = getSession().getWorkspace().getNamespaceRegistry().getURI(prefix);
499        return URI.create(getRDFNamespaceForJcrNamespace(namespace) + typeName);
500    });
501
502    /* (non-Javadoc)
503     * @see org.fcrepo.kernel.api.models.FedoraResource#updateProperties
504     *     (org.fcrepo.kernel.api.identifiers.IdentifierConverter, java.lang.String, RdfStream)
505     */
506    @Override
507    public void updateProperties(final IdentifierConverter<Resource, FedoraResource> idTranslator,
508                                 final String sparqlUpdateStatement, final RdfStream originalTriples)
509            throws MalformedRdfException, AccessDeniedException {
510
511        final Model model = originalTriples.collect(toModel());
512
513        final UpdateRequest request = create(sparqlUpdateStatement,
514                idTranslator.reverse().convert(this).toString());
515
516        final Collection<IllegalArgumentException> errors = checkInvalidPredicates(request);
517
518        final NamespaceRegistry namespaceRegistry = getNamespaceRegistry(getSession());
519
520        request.getPrefixMapping().getNsPrefixMap().forEach(
521            (k,v) -> {
522                try {
523                    LOGGER.debug("Prefix mapping is key:{} -> value:{}", k, v);
524                    if (Arrays.asList(namespaceRegistry.getPrefixes()).contains(k)
525                        &&  !v.equals(namespaceRegistry.getURI(k))) {
526
527                        final String namespaceURI = namespaceRegistry.getURI(k);
528                        LOGGER.debug("Prefix has already been defined: {}:{}", k, namespaceURI);
529                        throw new InvalidPrefixException("Prefix already exists as: " + k + " -> " + namespaceURI);
530                   }
531
532                } catch (final RepositoryException e) {
533                    throw new RepositoryRuntimeException(e);
534                }
535           });
536
537        if (!errors.isEmpty()) {
538            throw new IllegalArgumentException(errors.stream().map(Exception::getMessage).collect(joining(",\n")));
539        }
540
541        final JcrPropertyStatementListener listener = new JcrPropertyStatementListener(
542                idTranslator, getSession(), idTranslator.reverse().convert(this).asNode());
543
544        model.register(listener);
545
546        model.setNsPrefixes(request.getPrefixMapping());
547        execute(request, model);
548
549        removeEmptyFragments();
550
551        listener.assertNoExceptions();
552    }
553
554    @Override
555    public RdfStream getTriples(final IdentifierConverter<Resource, FedoraResource> idTranslator,
556                                final TripleCategory context) {
557        return getTriples(idTranslator, singleton(context));
558    }
559
560    @Override
561    public RdfStream getTriples(final IdentifierConverter<Resource, FedoraResource> idTranslator,
562                                final Set<? extends TripleCategory> contexts) {
563
564        return new DefaultRdfStream(idTranslator.reverse().convert(this).asNode(), contexts.stream()
565                .filter(contextMap::containsKey)
566                .map(x -> contextMap.get(x).apply(this).apply(idTranslator).apply(contexts.contains(MINIMAL)))
567                .reduce(empty(), Stream::concat));
568    }
569
570    /*
571     * (non-Javadoc)
572     * @see org.fcrepo.kernel.api.models.FedoraResource#getBaseVersion()
573     */
574    @Override
575    public Version getBaseVersion() {
576        try {
577            return getVersionManager().getBaseVersion(getPath());
578        } catch (final RepositoryException e) {
579            throw new RepositoryRuntimeException(e);
580        }
581    }
582
583    /*
584     * (non-Javadoc)
585     * @see org.fcrepo.kernel.api.models.FedoraResource#getVersionHistory()
586     */
587    @Override
588    public VersionHistory getVersionHistory() {
589        try {
590            return getVersionManager().getVersionHistory(getPath());
591        } catch (final RepositoryException e) {
592            throw new RepositoryRuntimeException(e);
593        }
594    }
595
596    /* (non-Javadoc)
597     * @see org.fcrepo.kernel.api.models.FedoraResource#isNew()
598     */
599    @Override
600    public Boolean isNew() {
601        return node.isNew();
602    }
603
604    /* (non-Javadoc)
605     * @see org.fcrepo.kernel.api.models.FedoraResource#replaceProperties
606     *     (org.fcrepo.kernel.api.identifiers.IdentifierConverter, com.hp.hpl.jena.rdf.model.Model)
607     */
608    @Override
609    public void replaceProperties(final IdentifierConverter<Resource, FedoraResource> idTranslator,
610        final Model inputModel, final RdfStream originalTriples) throws MalformedRdfException {
611
612        try (final RdfStream replacementStream =
613                new DefaultRdfStream(idTranslator.reverse().convert(this).asNode())) {
614
615            final GraphDifferencer differencer =
616                new GraphDifferencer(inputModel, originalTriples);
617
618            final StringBuilder exceptions = new StringBuilder();
619            try (final DefaultRdfStream diffStream =
620                    new DefaultRdfStream(replacementStream.topic(), differencer.difference())) {
621                new RdfRemover(idTranslator, getSession(), diffStream).consume();
622            } catch (final ConstraintViolationException e) {
623                throw e;
624            } catch (final MalformedRdfException e) {
625                exceptions.append(e.getMessage());
626                exceptions.append("\n");
627            }
628
629            try (final DefaultRdfStream notCommonStream =
630                    new DefaultRdfStream(replacementStream.topic(), differencer.notCommon())) {
631                new RdfAdder(idTranslator, getSession(), notCommonStream).consume();
632            } catch (final ConstraintViolationException e) {
633                throw e;
634            } catch (final MalformedRdfException e) {
635                exceptions.append(e.getMessage());
636            }
637
638            removeEmptyFragments();
639
640            if (exceptions.length() > 0) {
641                throw new MalformedRdfException(exceptions.toString());
642            }
643        }
644    }
645
646    private void removeEmptyFragments() {
647        try {
648            if (node.hasNode("#")) {
649                @SuppressWarnings("unchecked")
650                final Iterator<Node> nodes = node.getNode("#").getNodes();
651                nodes.forEachRemaining(n -> {
652                    try {
653                        @SuppressWarnings("unchecked")
654                        final Iterator<Property> properties = n.getProperties();
655                        final boolean hasUserProps = iteratorToStream(properties).map(propertyConverter::convert)
656                            .anyMatch(isManagedPredicate.negate());
657
658                        final boolean hasUserTypes = Arrays.stream(n.getMixinNodeTypes())
659                            .map(uncheck(NodeType::getName)).filter(hasInternalNamespace.negate())
660                            .map(uncheck(type ->
661                                getSession().getWorkspace().getNamespaceRegistry().getURI(type.split(":")[0])))
662                            .anyMatch(isManagedNamespace.negate());
663
664                        if (!hasUserProps && !hasUserTypes && !n.getWeakReferences().hasNext() &&
665                                !n.getReferences().hasNext()) {
666                            LOGGER.debug("Removing empty hash URI node: {}", n.getName());
667                            n.remove();
668                        }
669                    } catch (final RepositoryException ex) {
670                        throw new RepositoryRuntimeException("Error removing empty fragments", ex);
671                    }
672                });
673            }
674        } catch (final RepositoryException ex) {
675            throw new RepositoryRuntimeException("Error removing empty fragments", ex);
676        }
677    }
678
679    /* (non-Javadoc)
680     * @see org.fcrepo.kernel.api.models.FedoraResource#getEtagValue()
681     */
682    @Override
683    public String getEtagValue() {
684        final Date lastModifiedDate = getLastModifiedDate();
685
686        if (lastModifiedDate != null) {
687            return shaHex(getPath() + lastModifiedDate.getTime());
688        }
689        return "";
690    }
691
692    @Override
693    public void enableVersioning() {
694        try {
695            node.addMixin("mix:versionable");
696        } catch (final RepositoryException e) {
697            throw new RepositoryRuntimeException(e);
698        }
699    }
700
701    @Override
702    public void disableVersioning() {
703        try {
704            node.removeMixin("mix:versionable");
705        } catch (final RepositoryException e) {
706            throw new RepositoryRuntimeException(e);
707        }
708
709    }
710
711    @Override
712    public boolean isVersioned() {
713        try {
714            return node.isNodeType("mix:versionable");
715        } catch (final RepositoryException e) {
716            throw new RepositoryRuntimeException(e);
717        }
718    }
719
720    @Override
721    public boolean isFrozenResource() {
722        return isFrozenNode.test(this);
723    }
724
725    @Override
726    public FedoraResource getVersionedAncestor() {
727
728        try {
729            if (!isFrozenResource()) {
730                return null;
731            }
732
733            Node versionableFrozenNode = getNode();
734            FedoraResource unfrozenResource = getUnfrozenResource();
735
736            // traverse the frozen tree looking for a node whose unfrozen equivalent is versioned
737            while (!unfrozenResource.isVersioned()) {
738
739                if (versionableFrozenNode.getDepth() == 0) {
740                    return null;
741                }
742
743                // node in the frozen tree
744                versionableFrozenNode = versionableFrozenNode.getParent();
745
746                // unfrozen equivalent
747                unfrozenResource = new FedoraResourceImpl(versionableFrozenNode).getUnfrozenResource();
748            }
749
750            return new FedoraResourceImpl(versionableFrozenNode);
751        } catch (final RepositoryException e) {
752            throw new RepositoryRuntimeException(e);
753        }
754
755    }
756
757    @Override
758    public FedoraResource getUnfrozenResource() {
759        if (!isFrozenResource()) {
760            return this;
761        }
762
763        try {
764            // Either this resource is frozen
765            if (hasProperty(JCR_FROZEN_UUID)) {
766                try {
767                    return new FedoraResourceImpl(getNodeByProperty(getProperty(JCR_FROZEN_UUID)));
768                } catch (final ItemNotFoundException e) {
769                    // The unfrozen resource has been deleted, return the tombstone.
770                    return new TombstoneImpl(getNode());
771                }
772
773                // ..Or it is a child-version-history on a frozen path
774            } else if (hasProperty(JCR_CHILD_VERSION_HISTORY)) {
775                final Node childVersionHistory = getNodeByProperty(getProperty(JCR_CHILD_VERSION_HISTORY));
776                try {
777                    final Node childNode = getNodeByProperty(childVersionHistory.getProperty(JCR_VERSIONABLE_UUID));
778                    return new FedoraResourceImpl(childNode);
779                } catch (final ItemNotFoundException e) {
780                    // The unfrozen resource has been deleted, return the tombstone.
781                    return new TombstoneImpl(childVersionHistory);
782                }
783
784            } else {
785                throw new RepositoryRuntimeException("Resource must be frozen or a child-history!");
786            }
787        } catch (final RepositoryException e) {
788            throw new RepositoryRuntimeException(e);
789        }
790    }
791
792    @Override
793    public Node getNodeVersion(final String label) {
794        try {
795            final Node n = getFrozenNode(label);
796
797            if (n != null) {
798                return n;
799            }
800
801            if (isVersioned()) {
802                final VersionHistory hist = getVersionManager().getVersionHistory(getPath());
803
804                if (hist.hasVersionLabel(label)) {
805                    LOGGER.debug("Found version for {} by label {}.", this, label);
806                    return hist.getVersionByLabel(label).getFrozenNode();
807                }
808            }
809
810            LOGGER.warn("Unknown version {} with label or uuid {}!", this, label);
811            return null;
812        } catch (final RepositoryException e) {
813            throw new RepositoryRuntimeException(e);
814        }
815
816    }
817
818    @Override
819    public String getVersionLabelOfFrozenResource() {
820        if (!isFrozenResource()) {
821            return null;
822        }
823
824        // Version History associated with this resource
825        final VersionHistory versionHistory = getUnfrozenResource().getVersionHistory();
826
827        // Frozen node is required to find associated version label
828        final Node frozenResource;
829        try {
830            // Possibly the frozen node is nested inside of current child-version-history
831            if (getNode().hasProperty(JCR_CHILD_VERSION_HISTORY)) {
832                final Node childVersionHistory = getNodeByProperty(getProperty(JCR_CHILD_VERSION_HISTORY));
833                final Node childNode = getNodeByProperty(childVersionHistory.getProperty(JCR_VERSIONABLE_UUID));
834                final Version childVersion = getVersionManager().getBaseVersion(childNode.getPath());
835                frozenResource = childVersion.getFrozenNode();
836
837            } else {
838                frozenResource = getNode();
839            }
840
841            // Loop versions
842            @SuppressWarnings("unchecked")
843            final Stream<Version> versions = iteratorToStream(versionHistory.getAllVersions());
844            return versions
845                .filter(UncheckedPredicate.uncheck(version -> version.getFrozenNode().equals(frozenResource)))
846                .map(uncheck(versionHistory::getVersionLabels))
847                .flatMap(Arrays::stream)
848                .findFirst().orElse(null);
849        } catch (final RepositoryException e) {
850            throw new RepositoryRuntimeException(e);
851        }
852    }
853
854    private Node getNodeByProperty(final Property property) throws RepositoryException {
855        return getSession().getNodeByIdentifier(property.getString());
856    }
857
858    protected VersionManager getVersionManager() {
859        try {
860            return getSession().getWorkspace().getVersionManager();
861        } catch (final RepositoryException e) {
862            throw new RepositoryRuntimeException(e);
863        }
864    }
865
866    /**
867     * Helps ensure that there are no terminating slashes in the predicate.
868     * A terminating slash means ModeShape has trouble extracting the localName, e.g., for
869     * http://myurl.org/.
870     *
871     * @see <a href="https://jira.duraspace.org/browse/FCREPO-1409"> FCREPO-1409 </a> for details.
872     */
873    private static Collection<IllegalArgumentException> checkInvalidPredicates(final UpdateRequest request) {
874        return request.getOperations().stream()
875                .flatMap(x -> {
876                    if (x instanceof UpdateModify) {
877                        final UpdateModify y = (UpdateModify)x;
878                        return concat(y.getInsertQuads().stream(), y.getDeleteQuads().stream());
879                    } else if (x instanceof UpdateData) {
880                        return ((UpdateData)x).getQuads().stream();
881                    } else if (x instanceof UpdateDeleteWhere) {
882                        return ((UpdateDeleteWhere)x).getQuads().stream();
883                    } else {
884                        return empty();
885                    }
886                })
887                .filter(x -> x.getPredicate().isURI() && x.getPredicate().getURI().endsWith("/"))
888                .map(x -> new IllegalArgumentException("Invalid predicate ends with '/': " + x.getPredicate().getURI()))
889                .collect(Collectors.toList());
890    }
891
892    private Node getFrozenNode(final String label) throws RepositoryException {
893        try {
894            final Session session = getSession();
895
896            final Node frozenNode = session.getNodeByIdentifier(label);
897
898            final String baseUUID = getNode().getIdentifier();
899
900            /*
901             * We found a node whose identifier is the "label" for the version.  Now
902             * we must do due dilligence to make sure it's a frozen node representing
903             * a version of the subject node.
904             */
905            final Property p = frozenNode.getProperty(JCR_FROZEN_UUID);
906            if (p != null) {
907                if (p.getString().equals(baseUUID)) {
908                    return frozenNode;
909                }
910            }
911            /*
912             * Though a node with an id of the label was found, it wasn't the
913             * node we were looking for, so fall through and look for a labeled
914             * node.
915             */
916        } catch (final ItemNotFoundException ex) {
917            /*
918             * the label wasn't a uuid of a frozen node but
919             * instead possibly a version label.
920             */
921        }
922        return null;
923    }
924
925    @Override
926    public boolean equals(final Object object) {
927        if (object instanceof FedoraResourceImpl) {
928            return ((FedoraResourceImpl) object).getNode().equals(this.getNode());
929        }
930        return false;
931    }
932
933    @Override
934    public int hashCode() {
935        return getNode().hashCode();
936    }
937
938    protected Session getSession() {
939        try {
940            return getNode().getSession();
941        } catch (final RepositoryException e) {
942            throw new RepositoryRuntimeException(e);
943        }
944    }
945
946    @Override
947    public String toString() {
948        return getNode().toString();
949    }
950
951    protected Property getProperty(final String relPath) {
952        try {
953            return getNode().getProperty(relPath);
954        } catch (final RepositoryException e) {
955            throw new RepositoryRuntimeException(e);
956        }
957    }
958}