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