001/*
002 * Licensed to DuraSpace under one or more contributor license agreements.
003 * See the NOTICE file distributed with this work for additional information
004 * regarding copyright ownership.
005 *
006 * DuraSpace licenses this file to you under the Apache License,
007 * Version 2.0 (the "License"); you may not use this file except in
008 * compliance with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.fcrepo.kernel.modeshape;
019
020import static org.apache.jena.rdf.model.ResourceFactory.createTypedLiteral;
021import static org.apache.jena.update.UpdateAction.execute;
022import static org.apache.jena.update.UpdateFactory.create;
023import static java.time.Instant.ofEpochMilli;
024import static java.util.Arrays.asList;
025import static java.util.Collections.singleton;
026import static java.util.stream.Collectors.joining;
027import static java.util.stream.Collectors.toList;
028import static java.util.stream.Stream.concat;
029import static java.util.stream.Stream.empty;
030import static java.util.stream.Stream.of;
031import static org.apache.commons.codec.digest.DigestUtils.sha1Hex;
032import static org.fcrepo.kernel.api.RdfLexicon.LAST_MODIFIED_DATE;
033import static org.fcrepo.kernel.api.RdfLexicon.REPOSITORY_NAMESPACE;
034import static org.fcrepo.kernel.api.RdfLexicon.isManagedNamespace;
035import static org.fcrepo.kernel.api.RdfLexicon.isManagedPredicate;
036import static org.fcrepo.kernel.api.RdfCollectors.toModel;
037import static org.fcrepo.kernel.api.RequiredRdfContext.EMBED_RESOURCES;
038import static org.fcrepo.kernel.api.RequiredRdfContext.INBOUND_REFERENCES;
039import static org.fcrepo.kernel.api.RequiredRdfContext.LDP_CONTAINMENT;
040import static org.fcrepo.kernel.api.RequiredRdfContext.LDP_MEMBERSHIP;
041import static org.fcrepo.kernel.api.RequiredRdfContext.MINIMAL;
042import static org.fcrepo.kernel.api.RequiredRdfContext.PROPERTIES;
043import static org.fcrepo.kernel.api.RequiredRdfContext.SERVER_MANAGED;
044import static org.fcrepo.kernel.api.RequiredRdfContext.VERSIONS;
045import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.JCR_CREATED;
046import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.JCR_LASTMODIFIED;
047import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.FROZEN_MIXIN_TYPES;
048import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.ROOT;
049import static org.fcrepo.kernel.modeshape.RdfJcrLexicon.jcrProperties;
050import static org.fcrepo.kernel.modeshape.identifiers.NodeResourceConverter.nodeConverter;
051import static org.fcrepo.kernel.modeshape.rdf.JcrRdfTools.getRDFNamespaceForJcrNamespace;
052import static org.fcrepo.kernel.modeshape.services.functions.JcrPropertyFunctions.isFrozen;
053import static org.fcrepo.kernel.modeshape.services.functions.JcrPropertyFunctions.property2values;
054import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.getContainingNode;
055import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.getJcrNode;
056import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.hasInternalNamespace;
057import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.isFrozenNode;
058import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.isInternalNode;
059import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.ldpInsertedContentProperty;
060import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.resourceToProperty;
061import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.touchLdpMembershipResource;
062import static org.fcrepo.kernel.modeshape.utils.NamespaceTools.getNamespaceRegistry;
063import static org.fcrepo.kernel.modeshape.utils.StreamUtils.iteratorToStream;
064import static org.fcrepo.kernel.modeshape.utils.UncheckedFunction.uncheck;
065import static org.modeshape.jcr.api.JcrConstants.JCR_CONTENT;
066import static org.slf4j.LoggerFactory.getLogger;
067
068import java.net.URI;
069import java.util.ArrayList;
070import java.util.Arrays;
071import java.util.Calendar;
072import java.util.Collection;
073import java.time.Instant;
074import java.util.Iterator;
075import java.util.List;
076import java.util.Map;
077import java.util.Optional;
078import java.util.Set;
079import java.util.concurrent.atomic.AtomicBoolean;
080import java.util.function.Function;
081import java.util.function.Predicate;
082import java.util.stream.Collectors;
083import java.util.stream.Stream;
084
085import javax.jcr.ItemNotFoundException;
086import javax.jcr.Node;
087import javax.jcr.PathNotFoundException;
088import javax.jcr.Property;
089import javax.jcr.RepositoryException;
090import javax.jcr.Session;
091import javax.jcr.Value;
092import javax.jcr.nodetype.NodeType;
093import javax.jcr.version.Version;
094import javax.jcr.version.VersionHistory;
095import javax.jcr.NamespaceRegistry;
096import javax.jcr.version.VersionManager;
097
098import com.google.common.base.Converter;
099import com.google.common.collect.ImmutableMap;
100import org.apache.jena.rdf.model.Resource;
101import org.apache.jena.graph.Triple;
102
103import org.fcrepo.kernel.api.FedoraTypes;
104import org.fcrepo.kernel.api.FedoraVersion;
105import org.fcrepo.kernel.api.models.FedoraResource;
106import org.fcrepo.kernel.api.exception.AccessDeniedException;
107import org.fcrepo.kernel.api.exception.ConstraintViolationException;
108import org.fcrepo.kernel.api.exception.InvalidPrefixException;
109import org.fcrepo.kernel.api.exception.MalformedRdfException;
110import org.fcrepo.kernel.api.exception.PathNotFoundRuntimeException;
111import org.fcrepo.kernel.api.exception.RepositoryRuntimeException;
112import org.fcrepo.kernel.api.identifiers.IdentifierConverter;
113import org.fcrepo.kernel.modeshape.rdf.converters.PropertyConverter;
114import org.fcrepo.kernel.api.TripleCategory;
115import org.fcrepo.kernel.api.RdfStream;
116import org.fcrepo.kernel.api.rdf.DefaultRdfStream;
117import org.fcrepo.kernel.api.utils.GraphDifferencer;
118import org.fcrepo.kernel.modeshape.rdf.impl.AclRdfContext;
119import org.fcrepo.kernel.modeshape.rdf.impl.ChildrenRdfContext;
120import org.fcrepo.kernel.modeshape.rdf.impl.ContentRdfContext;
121import org.fcrepo.kernel.modeshape.rdf.impl.HashRdfContext;
122import org.fcrepo.kernel.modeshape.rdf.impl.LdpContainerRdfContext;
123import org.fcrepo.kernel.modeshape.rdf.impl.LdpIsMemberOfRdfContext;
124import org.fcrepo.kernel.modeshape.rdf.impl.LdpRdfContext;
125import org.fcrepo.kernel.modeshape.rdf.impl.ParentRdfContext;
126import org.fcrepo.kernel.modeshape.rdf.impl.PropertiesRdfContext;
127import org.fcrepo.kernel.modeshape.rdf.impl.TypeRdfContext;
128import org.fcrepo.kernel.modeshape.rdf.impl.ReferencesRdfContext;
129import org.fcrepo.kernel.modeshape.rdf.impl.RootRdfContext;
130import org.fcrepo.kernel.modeshape.rdf.impl.SkolemNodeRdfContext;
131import org.fcrepo.kernel.modeshape.rdf.impl.VersionsRdfContext;
132import org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils;
133import org.fcrepo.kernel.modeshape.utils.JcrPropertyStatementListener;
134import org.fcrepo.kernel.modeshape.utils.PropertyChangedListener;
135import org.fcrepo.kernel.modeshape.utils.UncheckedPredicate;
136import org.fcrepo.kernel.modeshape.utils.iterators.RdfAdder;
137import org.fcrepo.kernel.modeshape.utils.iterators.RdfRemover;
138
139import org.modeshape.jcr.api.JcrTools;
140import org.slf4j.Logger;
141
142import org.apache.jena.rdf.model.Model;
143import org.apache.jena.sparql.modify.request.UpdateData;
144import org.apache.jena.sparql.modify.request.UpdateDeleteWhere;
145import org.apache.jena.sparql.modify.request.UpdateModify;
146import org.apache.jena.update.UpdateRequest;
147
148/**
149 * Common behaviors across {@link org.fcrepo.kernel.api.models.Container} and
150 * {@link org.fcrepo.kernel.api.models.NonRdfSourceDescription} types; also used
151 * when the exact type of an object is irrelevant
152 *
153 * @author ajs6f
154 */
155public class FedoraResourceImpl extends JcrTools implements FedoraTypes, FedoraResource {
156
157    private static final Logger LOGGER = getLogger(FedoraResourceImpl.class);
158
159    private static final long NO_TIME = 0L;
160    private static final String JCR_CHILD_VERSION_HISTORY = "jcr:childVersionHistory";
161    private static final String JCR_VERSIONABLE_UUID = "jcr:versionableUuid";
162    private static final String JCR_FROZEN_UUID = "jcr:frozenUuid";
163    private static final String JCR_VERSION_STORAGE = "jcr:versionStorage";
164
165    private static final PropertyConverter propertyConverter = new PropertyConverter();
166
167    // A curried type accepting resource, translator, and "minimality", returning triples.
168    private static interface RdfGenerator extends Function<FedoraResource,
169    Function<IdentifierConverter<Resource, FedoraResource>, Function<Boolean, Stream<Triple>>>> {}
170
171    @SuppressWarnings("resource")
172    private static RdfGenerator getDefaultTriples = resource -> translator -> uncheck(minimal -> {
173        final Stream<Stream<Triple>> min = of(
174            new TypeRdfContext(resource, translator),
175            new PropertiesRdfContext(resource, translator));
176        if (!minimal) {
177            final Stream<Stream<Triple>> extra = of(
178                new HashRdfContext(resource, translator),
179                new SkolemNodeRdfContext(resource, translator));
180            return concat(min, extra).reduce(empty(), Stream::concat);
181        }
182        return min.reduce(empty(), Stream::concat);
183    });
184
185    private static RdfGenerator getEmbeddedResourceTriples = resource -> translator -> uncheck(minimal ->
186            resource.getChildren().flatMap(child -> child.getTriples(translator, PROPERTIES)));
187
188    private static RdfGenerator getInboundTriples = resource -> translator -> uncheck(_minimal -> {
189        return new ReferencesRdfContext(resource, translator);
190    });
191
192    private static RdfGenerator getLdpContainsTriples = resource -> translator -> uncheck(_minimal -> {
193        return new ChildrenRdfContext(resource, translator);
194    });
195
196    private static RdfGenerator getVersioningTriples = resource -> translator -> uncheck(_minimal -> {
197        return new VersionsRdfContext(resource, translator);
198    });
199
200    @SuppressWarnings("resource")
201    private static RdfGenerator getServerManagedTriples = resource -> translator -> uncheck(minimal -> {
202        if (minimal) {
203            return new LdpRdfContext(resource, translator);
204        }
205        final Stream<Stream<Triple>> streams = of(
206            new LdpRdfContext(resource, translator),
207            new AclRdfContext(resource, translator),
208            new RootRdfContext(resource, translator),
209            new ContentRdfContext(resource, translator),
210            new ParentRdfContext(resource, translator));
211        return streams.reduce(empty(), Stream::concat);
212    });
213
214    @SuppressWarnings("resource")
215    private static RdfGenerator getLdpMembershipTriples = resource -> translator -> uncheck(_minimal -> {
216        final Stream<Stream<Triple>> streams = of(
217            new LdpContainerRdfContext(resource, translator),
218            new LdpIsMemberOfRdfContext(resource, translator));
219        return streams.reduce(empty(), Stream::concat);
220    });
221
222    private static final Map<TripleCategory, RdfGenerator> contextMap =
223            ImmutableMap.<TripleCategory, RdfGenerator>builder()
224                    .put(PROPERTIES, getDefaultTriples)
225                    .put(VERSIONS, getVersioningTriples)
226                    .put(EMBED_RESOURCES, getEmbeddedResourceTriples)
227                    .put(INBOUND_REFERENCES, getInboundTriples)
228                    .put(SERVER_MANAGED, getServerManagedTriples)
229                    .put(LDP_MEMBERSHIP, getLdpMembershipTriples)
230                    .put(LDP_CONTAINMENT, getLdpContainsTriples)
231                    .build();
232
233    protected Node node;
234
235    /**
236     * Construct a {@link org.fcrepo.kernel.api.models.FedoraResource} from an existing JCR Node
237     * @param node an existing JCR node to treat as an fcrepo object
238     */
239    public FedoraResourceImpl(final Node node) {
240        this.node = node;
241    }
242
243    /**
244     * Return the underlying JCR Node for this resource
245     *
246     * @return the JCR Node
247     */
248    public Node getNode() {
249        return node;
250    }
251
252    /* (non-Javadoc)
253     * @see org.fcrepo.kernel.api.models.FedoraResource#getPath()
254     */
255    @Override
256    public String getPath() {
257        try {
258            final String path = node.getPath();
259            return path.endsWith("/" + JCR_CONTENT) ? path.substring(0, path.length() - JCR_CONTENT.length() - 1)
260                : path;
261        } catch (final RepositoryException e) {
262            throw new RepositoryRuntimeException(e);
263        }
264    }
265
266    /* (non-Javadoc)
267     * @see org.fcrepo.kernel.api.models.FedoraResource#getChildren(Boolean recursive)
268     */
269    @Override
270    public Stream<FedoraResource> getChildren(final Boolean recursive) {
271        try {
272            if (recursive) {
273                return nodeToGoodChildren(node).flatMap(FedoraResourceImpl::getAllChildren);
274            }
275            return nodeToGoodChildren(node);
276        } catch (final RepositoryException e) {
277            throw new RepositoryRuntimeException(e);
278        }
279    }
280
281    /* (non-Javadoc)
282     * @see org.fcrepo.kernel.api.models.FedoraResource#getDescription()
283     */
284    @Override
285    public FedoraResource getDescription() {
286        return this;
287    }
288
289    /* (non-Javadoc)
290     * @see org.fcrepo.kernel.api.models.FedoraResource#getDescribedResource()
291     */
292    @Override
293    public FedoraResource getDescribedResource() {
294        return this;
295    }
296
297    /**
298     * Get the "good" children for a node by skipping all pairtree nodes in the way.
299     * @param input
300     * @return
301     * @throws RepositoryException
302     */
303    @SuppressWarnings("unchecked")
304    private Stream<FedoraResource> nodeToGoodChildren(final Node input) throws RepositoryException {
305        return iteratorToStream(input.getNodes()).filter(nastyChildren.negate())
306            .flatMap(uncheck((final Node child) -> child.isNodeType(FEDORA_PAIRTREE) ? nodeToGoodChildren(child) :
307                        of(nodeToObjectBinaryConverter.convert(child))));
308    }
309
310    /**
311     * Get all children recursively, and flatten into a single Stream.
312     */
313    private static Stream<FedoraResource> getAllChildren(final FedoraResource resource) {
314        return concat(of(resource), resource.getChildren().flatMap(FedoraResourceImpl::getAllChildren));
315    }
316
317    /**
318     * Children for whom we will not generate triples.
319     */
320    private static Predicate<Node> nastyChildren = isInternalNode
321                    .or(TombstoneImpl::hasMixin)
322                    .or(UncheckedPredicate.uncheck(p -> p.getName().equals(JCR_CONTENT)))
323                    .or(UncheckedPredicate.uncheck(p -> p.getName().equals("#")));
324
325    private static final Converter<FedoraResource, FedoraResource> datastreamToBinary
326            = new Converter<FedoraResource, FedoraResource>() {
327
328        @Override
329        protected FedoraResource doForward(final FedoraResource fedoraResource) {
330            return fedoraResource.getDescribedResource();
331        }
332
333        @Override
334        protected FedoraResource doBackward(final FedoraResource fedoraResource) {
335            return fedoraResource.getDescription();
336        }
337    };
338
339    private static final Converter<Node, FedoraResource> nodeToObjectBinaryConverter
340            = nodeConverter.andThen(datastreamToBinary);
341
342    @Override
343    public FedoraResource getContainer() {
344        return getContainingNode(getNode()).map(nodeConverter::convert).orElse(null);
345    }
346
347    @Override
348    public FedoraResource getChild(final String relPath) {
349        try {
350            return nodeConverter.convert(getNode().getNode(relPath));
351        } catch (final RepositoryException e) {
352            throw new RepositoryRuntimeException(e);
353        }
354    }
355
356    @Override
357    public boolean hasProperty(final String relPath) {
358        try {
359            return getNode().hasProperty(relPath);
360        } catch (final RepositoryException e) {
361            throw new RepositoryRuntimeException(e);
362        }
363    }
364
365    @Override
366    public void delete() {
367        try {
368            // Remove inbound references to this resource and, recursively, any of its children
369            removeReferences(node);
370
371            final Node parent = getNode().getDepth() > 0 ? getNode().getParent() : null;
372
373            final String name = getNode().getName();
374
375            // This is resolved immediately b/c we delete the node before updating an indirect container's target
376            final boolean shouldUpdateIndirectResource = ldpInsertedContentProperty(node)
377                .flatMap(resourceToProperty(getSession())).filter(this::hasProperty).isPresent();
378
379            final Optional<Node> containingNode = getContainingNode(getNode());
380
381            node.remove();
382
383            if (parent != null) {
384                createTombstone(parent, name);
385
386                // also update membershipResources for Direct/Indirect Containers
387                containingNode.filter(UncheckedPredicate.uncheck((final Node ancestor) ->
388                            ancestor.hasProperty(LDP_MEMBER_RESOURCE) && (ancestor.isNodeType(LDP_DIRECT_CONTAINER) ||
389                            shouldUpdateIndirectResource)))
390                    .ifPresent(ancestor -> {
391                        try {
392                            FedoraTypesUtils.touch(ancestor.getProperty(LDP_MEMBER_RESOURCE).getNode());
393                        } catch (final RepositoryException ex) {
394                            throw new RepositoryRuntimeException(ex);
395                        }
396                    });
397
398                // update the lastModified date on the parent node
399                containingNode.ifPresent(ancestor -> {
400                    FedoraTypesUtils.touch(ancestor);
401                });
402            }
403        } catch (final javax.jcr.AccessDeniedException e) {
404            throw new AccessDeniedException(e);
405        } catch (final RepositoryException e) {
406            throw new RepositoryRuntimeException(e);
407        }
408    }
409
410    private void removeReferences(final Node n) {
411        try {
412            // Remove references to this resource
413            doRemoveReferences(n);
414
415            // Recurse over children of this resource
416            if (n.hasNodes()) {
417                @SuppressWarnings("unchecked")
418                final Iterator<Node> nodes = n.getNodes();
419                nodes.forEachRemaining(this::removeReferences);
420            }
421        } catch (RepositoryException e) {
422            throw new RepositoryRuntimeException(e);
423        }
424    }
425
426    private void doRemoveReferences(final Node n) throws RepositoryException {
427        @SuppressWarnings("unchecked")
428        final Iterator<Property> references = n.getReferences();
429        @SuppressWarnings("unchecked")
430        final Iterator<Property> weakReferences = n.getWeakReferences();
431        concat(iteratorToStream(references), iteratorToStream(weakReferences)).forEach(prop -> {
432            try {
433                final List<Value> newVals = property2values.apply(prop).filter(
434                        UncheckedPredicate.uncheck(value ->
435                                !n.equals(getSession().getNodeByIdentifier(value.getString()))))
436                        .collect(toList());
437
438                if (newVals.size() == 0) {
439                    prop.remove();
440                } else {
441                    prop.setValue(newVals.toArray(new Value[newVals.size()]));
442                }
443            } catch (final RepositoryException ex) {
444                // Ignore error from trying to update properties on versioned resources
445                if (ex instanceof javax.jcr.nodetype.ConstraintViolationException &&
446                        ex.getMessage().contains(JCR_VERSION_STORAGE)) {
447                    LOGGER.debug("Ignoring exception trying to remove property from versioned resource: {}",
448                            ex.getMessage());
449                } else {
450                    throw new RepositoryRuntimeException(ex);
451                }
452            }
453        });
454    }
455
456    private void createTombstone(final Node parent, final String path) throws RepositoryException {
457        findOrCreateChild(parent, path, FEDORA_TOMBSTONE);
458    }
459
460    /* (non-Javadoc)
461     * @see org.fcrepo.kernel.api.models.FedoraResource#getCreatedDate()
462     */
463    @Override
464    public Instant getCreatedDate() {
465        try {
466            if (hasProperty(JCR_CREATED)) {
467                return ofEpochMilli(getTimestamp(JCR_CREATED, NO_TIME));
468            }
469        } catch (final PathNotFoundException e) {
470            throw new PathNotFoundRuntimeException(e);
471        } catch (final RepositoryException e) {
472            throw new RepositoryRuntimeException(e);
473        }
474        LOGGER.debug("Node {} does not have a createdDate", node);
475        return null;
476    }
477
478    /* (non-Javadoc)
479     * @see org.fcrepo.kernel.api.models.FedoraResource#getLastModifiedDate()
480     */
481
482    /**
483     * This method gets the last modified date for this FedoraResource.  Because
484     * the last modified date is managed by fcrepo (not ModeShape) while the created
485     * date *is* managed by ModeShape in the current implementation it's possible that
486     * the last modified date will be before the created date.  Instead of making
487     * a second update to correct the modified date, in cases where the modified
488     * date is ealier than the created date, this class presents the created date instead.
489     *
490     * Any method that exposes the last modified date must maintain this illusion so
491     * that that external callers are presented with a sensible and consistent
492     * representation of this resource.
493     * @return the last modified Instant (or the created Instant if it was after the last
494     *         modified date)
495     */
496    @Override
497    public Instant getLastModifiedDate() {
498
499        final Instant createdDate = getCreatedDate();
500        try {
501            final long created = createdDate == null ? NO_TIME : createdDate.toEpochMilli();
502            if (hasProperty(FEDORA_LASTMODIFIED)) {
503                return ofEpochMilli(getTimestamp(FEDORA_LASTMODIFIED, created));
504            } else if (hasProperty(JCR_LASTMODIFIED)) {
505                return ofEpochMilli(getTimestamp(JCR_LASTMODIFIED, created));
506            }
507        } catch (final PathNotFoundException e) {
508            throw new PathNotFoundRuntimeException(e);
509        } catch (final RepositoryException e) {
510            throw new RepositoryRuntimeException(e);
511        }
512        LOGGER.debug("Could not get last modified date property for node {}", node);
513
514        if (createdDate != null) {
515            LOGGER.trace("Using created date for last modified date for node {}", node);
516            return createdDate;
517        }
518
519        return null;
520    }
521
522    private long getTimestamp(final String property, final long created) throws RepositoryException {
523        LOGGER.trace("Using {} date", property);
524        final long timestamp = getProperty(property).getDate().getTimeInMillis();
525        if (timestamp < created && created > NO_TIME) {
526            LOGGER.trace("Returning the later created date ({} > {}) for {}", created, timestamp, property);
527            return created;
528        }
529        return timestamp;
530    }
531
532    /**
533     * Set the last-modified date to the current date.
534     */
535    public void touch() {
536        FedoraTypesUtils.touch(getNode());
537    }
538
539    @Override
540    public boolean hasType(final String type) {
541        try {
542            if (type.equals(FEDORA_REPOSITORY_ROOT)) {
543                return node.isNodeType(ROOT);
544            } else if (isFrozen.test(node) && hasProperty(FROZEN_MIXIN_TYPES)) {
545                return property2values.apply(getProperty(FROZEN_MIXIN_TYPES)).map(uncheck(Value::getString))
546                    .anyMatch(type::equals);
547            }
548            return node.isNodeType(type);
549        } catch (final PathNotFoundException e) {
550            throw new PathNotFoundRuntimeException(e);
551        } catch (final RepositoryException e) {
552            throw new RepositoryRuntimeException(e);
553        }
554    }
555
556    @Override
557    public List<URI> getTypes() {
558        try {
559            final List<NodeType> nodeTypes = new ArrayList<>();
560            final NodeType primaryNodeType = node.getPrimaryNodeType();
561            nodeTypes.add(primaryNodeType);
562            nodeTypes.addAll(asList(primaryNodeType.getSupertypes()));
563            final List<NodeType> mixinTypes = asList(node.getMixinNodeTypes());
564
565            nodeTypes.addAll(mixinTypes);
566            mixinTypes.stream()
567                .map(NodeType::getSupertypes)
568                .flatMap(Arrays::stream)
569                .forEach(nodeTypes::add);
570
571            final List<URI> types = nodeTypes.stream()
572                .map(uncheck(NodeType::getName))
573                .filter(hasInternalNamespace.negate())
574                .distinct()
575                .map(nodeTypeNameToURI)
576                .peek(x -> LOGGER.debug("node has rdf:type {}", x))
577                .collect(Collectors.toList());
578
579            if (isFrozenResource()) {
580                types.add(URI.create(REPOSITORY_NAMESPACE + "Version"));
581            }
582
583            return types;
584
585        } catch (final PathNotFoundException e) {
586            throw new PathNotFoundRuntimeException(e);
587        } catch (final RepositoryException e) {
588            throw new RepositoryRuntimeException(e);
589        }
590    }
591
592    private final Function<String, URI> nodeTypeNameToURI = uncheck(name -> {
593        final String prefix = name.split(":")[0];
594        final String typeName = name.split(":")[1];
595        final String namespace = getSession().getWorkspace().getNamespaceRegistry().getURI(prefix);
596        return URI.create(getRDFNamespaceForJcrNamespace(namespace) + typeName);
597    });
598
599    /* (non-Javadoc)
600     * @see org.fcrepo.kernel.api.models.FedoraResource#updateProperties
601     *     (org.fcrepo.kernel.api.identifiers.IdentifierConverter, java.lang.String, RdfStream)
602     */
603    @Override
604    public void updateProperties(final IdentifierConverter<Resource, FedoraResource> idTranslator,
605                                 final String sparqlUpdateStatement, final RdfStream originalTriples)
606            throws MalformedRdfException, AccessDeniedException {
607
608        final Model model = originalTriples.collect(toModel());
609
610        final UpdateRequest request = create(sparqlUpdateStatement,
611                idTranslator.reverse().convert(this).toString());
612
613        final Collection<IllegalArgumentException> errors = checkInvalidPredicates(request);
614
615        final NamespaceRegistry namespaceRegistry = getNamespaceRegistry(getSession());
616
617        request.getPrefixMapping().getNsPrefixMap().forEach(
618            (k,v) -> {
619                try {
620                    LOGGER.debug("Prefix mapping is key:{} -> value:{}", k, v);
621                    if (Arrays.asList(namespaceRegistry.getPrefixes()).contains(k)
622                        &&  !v.equals(namespaceRegistry.getURI(k))) {
623
624                        final String namespaceURI = namespaceRegistry.getURI(k);
625                        LOGGER.debug("Prefix has already been defined: {}:{}", k, namespaceURI);
626                        throw new InvalidPrefixException("Prefix already exists as: " + k + " -> " + namespaceURI);
627                   }
628
629                } catch (final RepositoryException e) {
630                    throw new RepositoryRuntimeException(e);
631                }
632           });
633
634        if (!errors.isEmpty()) {
635            throw new IllegalArgumentException(errors.stream().map(Exception::getMessage).collect(joining(",\n")));
636        }
637
638        final JcrPropertyStatementListener listener = new JcrPropertyStatementListener(
639                idTranslator, getSession(), idTranslator.reverse().convert(this).asNode());
640
641        model.register(listener);
642
643        // If this resource's structural parent is an IndirectContainer, check whether the
644        // ldp:insertedContentRelation property is present in the stream of changed triples.
645        // If so, set the propertyChanged value to true.
646        final AtomicBoolean propertyChanged = new AtomicBoolean();
647        ldpInsertedContentProperty(getNode()).ifPresent(resource -> {
648            model.register(new PropertyChangedListener(resource, propertyChanged));
649        });
650
651        model.setNsPrefixes(request.getPrefixMapping());
652        execute(request, model);
653
654        removeEmptyFragments();
655
656        listener.assertNoExceptions();
657
658        // Update the fedora:lastModified property
659        touch();
660
661        // Update the fedora:lastModified property of the ldp:memberResource
662        // resource, if necessary.
663        if (propertyChanged.get()) {
664            touchLdpMembershipResource(getNode());
665        }
666    }
667
668    @Override
669    public RdfStream getTriples(final IdentifierConverter<Resource, FedoraResource> idTranslator,
670                                final TripleCategory context) {
671        return getTriples(idTranslator, singleton(context));
672    }
673
674    @Override
675    public RdfStream getTriples(final IdentifierConverter<Resource, FedoraResource> idTranslator,
676                                final Set<? extends TripleCategory> contexts) {
677
678        return new DefaultRdfStream(idTranslator.reverse().convert(this).asNode(), contexts.stream()
679                .filter(contextMap::containsKey)
680                .map(x -> contextMap.get(x).apply(this).apply(idTranslator).apply(contexts.contains(MINIMAL)))
681                .reduce(empty(), Stream::concat));
682    }
683
684    /*
685     * (non-Javadoc)
686     * @see org.fcrepo.kernel.api.models.FedoraResource#getBaseVersion()
687     */
688    @Override
689    public FedoraResource getBaseVersion() {
690        try {
691            return new FedoraResourceImpl(getVersionManager().getBaseVersion(getPath()).getFrozenNode());
692        } catch (final RepositoryException e) {
693            throw new RepositoryRuntimeException(e);
694        }
695    }
696
697    /* (non-Javadoc)
698     * @see org.fcrepo.kernel.api.models.FedoraResource#isNew()
699     */
700    @Override
701    public Boolean isNew() {
702        return node.isNew();
703    }
704
705    /* (non-Javadoc)
706     * @see org.fcrepo.kernel.api.models.FedoraResource#replaceProperties
707     *     (org.fcrepo.kernel.api.identifiers.IdentifierConverter, org.apache.jena.rdf.model.Model)
708     */
709    @Override
710    public void replaceProperties(final IdentifierConverter<Resource, FedoraResource> idTranslator,
711        final Model inputModel, final RdfStream originalTriples) throws MalformedRdfException {
712
713        try (final RdfStream replacementStream =
714                new DefaultRdfStream(idTranslator.reverse().convert(this).asNode())) {
715
716            final GraphDifferencer differencer =
717                new GraphDifferencer(inputModel, originalTriples);
718
719            final StringBuilder exceptions = new StringBuilder();
720            try (final DefaultRdfStream diffStream =
721                    new DefaultRdfStream(replacementStream.topic(), differencer.difference())) {
722                new RdfRemover(idTranslator, getSession(), diffStream).consume();
723            } catch (final ConstraintViolationException e) {
724                throw e;
725            } catch (final MalformedRdfException e) {
726                exceptions.append(e.getMessage());
727                exceptions.append("\n");
728            }
729
730            try (final DefaultRdfStream notCommonStream =
731                    new DefaultRdfStream(replacementStream.topic(), differencer.notCommon())) {
732                new RdfAdder(idTranslator, getSession(), notCommonStream).consume();
733            } catch (final ConstraintViolationException e) {
734                throw e;
735            } catch (final MalformedRdfException e) {
736                exceptions.append(e.getMessage());
737            }
738
739            // If this resource's structural parent is an IndirectContainer, check whether the
740            // ldp:insertedContentRelation property is present in the stream of changed triples.
741            // If so, set the propertyChanged value to true.
742            final AtomicBoolean propertyChanged = new AtomicBoolean();
743            ldpInsertedContentProperty(getNode()).ifPresent(resource -> {
744                propertyChanged.set(differencer.notCommon().map(Triple::getPredicate).anyMatch(resource::equals));
745            });
746
747            removeEmptyFragments();
748
749            if (exceptions.length() > 0) {
750                throw new MalformedRdfException(exceptions.toString());
751            }
752
753            // Update the fedora:lastModified property
754            touch();
755
756            // If the ldp:insertedContentRelation property was changed, update the
757            // ldp:membershipResource resource.
758            if (propertyChanged.get()) {
759                touchLdpMembershipResource(getNode());
760            }
761        }
762    }
763
764    private void removeEmptyFragments() {
765        try {
766            if (node.hasNode("#")) {
767                @SuppressWarnings("unchecked")
768                final Iterator<Node> nodes = node.getNode("#").getNodes();
769                nodes.forEachRemaining(n -> {
770                    try {
771                        @SuppressWarnings("unchecked")
772                        final Iterator<Property> properties = n.getProperties();
773                        final boolean hasUserProps = iteratorToStream(properties).map(propertyConverter::convert)
774                            .filter(p -> !jcrProperties.contains(p))
775                            .anyMatch(isManagedPredicate.negate());
776
777                        final boolean hasUserTypes = Arrays.stream(n.getMixinNodeTypes())
778                            .map(uncheck(NodeType::getName)).filter(hasInternalNamespace.negate())
779                            .map(uncheck(type ->
780                                getSession().getWorkspace().getNamespaceRegistry().getURI(type.split(":")[0])))
781                            .anyMatch(isManagedNamespace.negate());
782
783                        if (!hasUserProps && !hasUserTypes && !n.getWeakReferences().hasNext() &&
784                                !n.getReferences().hasNext()) {
785                            LOGGER.debug("Removing empty hash URI node: {}", n.getName());
786                            n.remove();
787                        }
788                    } catch (final RepositoryException ex) {
789                        throw new RepositoryRuntimeException("Error removing empty fragments", ex);
790                    }
791                });
792            }
793        } catch (final RepositoryException ex) {
794            throw new RepositoryRuntimeException("Error removing empty fragments", ex);
795        }
796    }
797
798    /* (non-Javadoc)
799     * @see org.fcrepo.kernel.api.models.FedoraResource#getEtagValue()
800     */
801    @Override
802    public String getEtagValue() {
803        final Instant lastModifiedDate = getLastModifiedDate();
804
805        if (lastModifiedDate != null) {
806            return sha1Hex(getPath() + lastModifiedDate.toEpochMilli());
807        }
808        return "";
809    }
810
811    @Override
812    public void enableVersioning() {
813        try {
814            node.addMixin("mix:versionable");
815        } catch (final RepositoryException e) {
816            throw new RepositoryRuntimeException(e);
817        }
818    }
819
820    @Override
821    public void disableVersioning() {
822        try {
823            node.removeMixin("mix:versionable");
824        } catch (final RepositoryException e) {
825            throw new RepositoryRuntimeException(e);
826        }
827
828    }
829
830    @Override
831    public boolean isVersioned() {
832        try {
833            return node.isNodeType("mix:versionable");
834        } catch (final RepositoryException e) {
835            throw new RepositoryRuntimeException(e);
836        }
837    }
838
839    @Override
840    public boolean isFrozenResource() {
841        return isFrozenNode.test(this);
842    }
843
844    @Override
845    public FedoraResource getVersionedAncestor() {
846
847        try {
848            if (!isFrozenResource()) {
849                return null;
850            }
851
852            Node versionableFrozenNode = getNode();
853            FedoraResource unfrozenResource = getUnfrozenResource();
854
855            // traverse the frozen tree looking for a node whose unfrozen equivalent is versioned
856            while (!unfrozenResource.isVersioned()) {
857
858                if (versionableFrozenNode.getDepth() == 0) {
859                    return null;
860                }
861
862                // node in the frozen tree
863                versionableFrozenNode = versionableFrozenNode.getParent();
864
865                // unfrozen equivalent
866                unfrozenResource = new FedoraResourceImpl(versionableFrozenNode).getUnfrozenResource();
867            }
868
869            return new FedoraResourceImpl(versionableFrozenNode);
870        } catch (final RepositoryException e) {
871            throw new RepositoryRuntimeException(e);
872        }
873
874    }
875
876    @Override
877    public FedoraResource getUnfrozenResource() {
878        if (!isFrozenResource()) {
879            return this;
880        }
881
882        try {
883            // Either this resource is frozen
884            if (hasProperty(JCR_FROZEN_UUID)) {
885                try {
886                    return new FedoraResourceImpl(getNodeByProperty(getProperty(JCR_FROZEN_UUID)));
887                } catch (final ItemNotFoundException e) {
888                    // The unfrozen resource has been deleted, return the tombstone.
889                    return new TombstoneImpl(getNode());
890                }
891
892                // ..Or it is a child-version-history on a frozen path
893            } else if (hasProperty(JCR_CHILD_VERSION_HISTORY)) {
894                final Node childVersionHistory = getNodeByProperty(getProperty(JCR_CHILD_VERSION_HISTORY));
895                try {
896                    final Node childNode = getNodeByProperty(childVersionHistory.getProperty(JCR_VERSIONABLE_UUID));
897                    return new FedoraResourceImpl(childNode);
898                } catch (final ItemNotFoundException e) {
899                    // The unfrozen resource has been deleted, return the tombstone.
900                    return new TombstoneImpl(childVersionHistory);
901                }
902
903            } else {
904                throw new RepositoryRuntimeException("Resource must be frozen or a child-history!");
905            }
906        } catch (final RepositoryException e) {
907            throw new RepositoryRuntimeException(e);
908        }
909    }
910
911    @Override
912    public FedoraResource getVersion(final String label) {
913        try {
914            final Node n = getFrozenNode(label);
915
916            if (n != null) {
917                return new FedoraResourceImpl(n);
918            }
919
920            if (isVersioned()) {
921                final VersionHistory hist = getVersionManager().getVersionHistory(getPath());
922
923                if (hist.hasVersionLabel(label)) {
924                    LOGGER.debug("Found version for {} by label {}.", this, label);
925                    return new FedoraResourceImpl(hist.getVersionByLabel(label).getFrozenNode());
926                }
927            }
928
929            LOGGER.warn("Unknown version {} with label {}!", getPath(), label);
930            return null;
931        } catch (final RepositoryException e) {
932            throw new RepositoryRuntimeException(e);
933        }
934
935    }
936
937    @Override
938    public Stream<FedoraVersion> getVersions() {
939        try {
940            final VersionHistory history = getVersionManager().getVersionHistory(getPath());
941            @SuppressWarnings("unchecked")
942            final Iterator<Version> versions = history.getAllVersions();
943            return iteratorToStream(versions)
944                /* discard jcr:rootVersion */
945                .filter(UncheckedPredicate.uncheck(version ->
946                            !version.getName().equals(history.getRootVersion().getName())))
947                /* omit unlabelled versions */
948                .filter(UncheckedPredicate.uncheck(version -> {
949                    final String[] labels = history.getVersionLabels(version);
950                    if (labels.length == 0) {
951                        LOGGER.warn("An unlabelled version for {} was found! Omitting from version listing!",
952                                getPath());
953                    } else if (labels.length > 1) {
954                        LOGGER.warn("Multiple version labels found for {}! Using first label, \"{}\".", getPath(),
955                                labels[0]);
956                    }
957                    return labels.length > 0;
958                }))
959                .map(uncheck(version ->
960                        new FedoraVersionImpl(history.getVersionLabels(version)[0], version.getCreated().toInstant())));
961        } catch (final RepositoryException ex) {
962            throw new RepositoryRuntimeException(ex);
963        }
964    }
965
966    @Override
967    public String getVersionLabelOfFrozenResource() {
968        if (!isFrozenResource()) {
969            return null;
970        }
971
972        // Frozen node is required to find associated version label
973        final Node frozenResource;
974        try {
975            // Version History associated with this resource
976            final VersionHistory history = getVersionManager().getVersionHistory(getUnfrozenResource().getPath());
977
978            // Possibly the frozen node is nested inside of current child-version-history
979            if (getNode().hasProperty(JCR_CHILD_VERSION_HISTORY)) {
980                final Node childVersionHistory = getNodeByProperty(getProperty(JCR_CHILD_VERSION_HISTORY));
981                final Node childNode = getNodeByProperty(childVersionHistory.getProperty(JCR_VERSIONABLE_UUID));
982                final Version childVersion = getVersionManager().getBaseVersion(childNode.getPath());
983                frozenResource = childVersion.getFrozenNode();
984
985            } else {
986                frozenResource = getNode();
987            }
988
989            // Loop versions
990            @SuppressWarnings("unchecked")
991            final Stream<Version> versions = iteratorToStream(history.getAllVersions());
992            return versions
993                .filter(UncheckedPredicate.uncheck(version -> version.getFrozenNode().equals(frozenResource)))
994                .map(uncheck(history::getVersionLabels))
995                .flatMap(Arrays::stream)
996                .findFirst().orElse(null);
997        } catch (final RepositoryException e) {
998            throw new RepositoryRuntimeException(e);
999        }
1000    }
1001
1002    private Node getNodeByProperty(final Property property) throws RepositoryException {
1003        return getSession().getNodeByIdentifier(property.getString());
1004    }
1005
1006    protected VersionManager getVersionManager() {
1007        try {
1008            return getSession().getWorkspace().getVersionManager();
1009        } catch (final RepositoryException e) {
1010            throw new RepositoryRuntimeException(e);
1011        }
1012    }
1013
1014    /**
1015     * Helps ensure that there are no terminating slashes in the predicate.
1016     * A terminating slash means ModeShape has trouble extracting the localName, e.g., for
1017     * http://myurl.org/.
1018     *
1019     * @see <a href="https://jira.duraspace.org/browse/FCREPO-1409"> FCREPO-1409 </a> for details.
1020     */
1021    private static Collection<IllegalArgumentException> checkInvalidPredicates(final UpdateRequest request) {
1022        return request.getOperations().stream()
1023                .flatMap(x -> {
1024                    if (x instanceof UpdateModify) {
1025                        final UpdateModify y = (UpdateModify)x;
1026                        return concat(y.getInsertQuads().stream(), y.getDeleteQuads().stream());
1027                    } else if (x instanceof UpdateData) {
1028                        return ((UpdateData)x).getQuads().stream();
1029                    } else if (x instanceof UpdateDeleteWhere) {
1030                        return ((UpdateDeleteWhere)x).getQuads().stream();
1031                    } else {
1032                        return empty();
1033                    }
1034                })
1035                .filter(x -> x.getPredicate().isURI() && x.getPredicate().getURI().endsWith("/"))
1036                .map(x -> new IllegalArgumentException("Invalid predicate ends with '/': " + x.getPredicate().getURI()))
1037                .collect(Collectors.toList());
1038    }
1039
1040    private Node getFrozenNode(final String label) throws RepositoryException {
1041        try {
1042            final Session session = getSession();
1043
1044            final Node frozenNode = session.getNodeByIdentifier(label);
1045
1046            final String baseUUID = getNode().getIdentifier();
1047
1048            /*
1049             * We found a node whose identifier is the "label" for the version.  Now
1050             * we must do due dilligence to make sure it's a frozen node representing
1051             * a version of the subject node.
1052             */
1053            final Property p = frozenNode.getProperty(JCR_FROZEN_UUID);
1054            if (p != null) {
1055                if (p.getString().equals(baseUUID)) {
1056                    return frozenNode;
1057                }
1058            }
1059            /*
1060             * Though a node with an id of the label was found, it wasn't the
1061             * node we were looking for, so fall through and look for a labeled
1062             * node.
1063             */
1064        } catch (final ItemNotFoundException ex) {
1065            /*
1066             * the label wasn't a uuid of a frozen node but
1067             * instead possibly a version label.
1068             */
1069        }
1070        return null;
1071    }
1072
1073    @Override
1074    public boolean equals(final Object object) {
1075        if (object instanceof FedoraResourceImpl) {
1076            return ((FedoraResourceImpl) object).getNode().equals(this.getNode());
1077        }
1078        return false;
1079    }
1080
1081    @Override
1082    public int hashCode() {
1083        return getNode().hashCode();
1084    }
1085
1086    protected Session getSession() {
1087        try {
1088            return getNode().getSession();
1089        } catch (final RepositoryException e) {
1090            throw new RepositoryRuntimeException(e);
1091        }
1092    }
1093
1094    @Override
1095    public String toString() {
1096        return getNode().toString();
1097    }
1098
1099    protected Property getProperty(final String relPath) {
1100        try {
1101            return getNode().getProperty(relPath);
1102        } catch (final RepositoryException e) {
1103            throw new RepositoryRuntimeException(e);
1104        }
1105    }
1106
1107    /**
1108     * A method that takes a Triple and returns a Triple that is the correct representation of
1109     * that triple for the given resource.  The current implementation of this method is used by
1110     * {@link PropertiesRdfContext} to replace the reported {@link org.fcrepo.kernel.api.RdfLexicon#LAST_MODIFIED_DATE}
1111     * with the one produced by {@link #getLastModifiedDate}.
1112     * @param r the Fedora resource
1113     * @param translator a converter to get the external identifier from a jcr node
1114     * @return a function to convert triples
1115     */
1116    public static Function<Triple, Triple> fixDatesIfNecessary(final FedoraResource r,
1117                                                      final Converter<Node, Resource> translator) {
1118        return t -> {
1119            if (t.getPredicate().toString().equals(LAST_MODIFIED_DATE.toString())
1120                    && t.getSubject().equals(translator.convert(getJcrNode(r)).asNode())) {
1121                final Calendar c = new Calendar.Builder().setInstant(r.getLastModifiedDate().toEpochMilli()).build();
1122                return new Triple(t.getSubject(), t.getPredicate(), createTypedLiteral(c).asNode());
1123            }
1124            return t;
1125        };
1126    }
1127
1128}