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            @SuppressWarnings("unchecked")
366            final Iterator<Property> references = node.getReferences();
367            @SuppressWarnings("unchecked")
368            final Iterator<Property> weakReferences = node.getWeakReferences();
369            concat(iteratorToStream(references), iteratorToStream(weakReferences)).forEach(prop -> {
370                try {
371                    final List<Value> newVals = property2values.apply(prop).filter(
372                            UncheckedPredicate.uncheck(value ->
373                                !node.equals(getSession().getNodeByIdentifier(value.getString()))))
374                        .collect(toList());
375
376                    if (newVals.size() == 0) {
377                        prop.remove();
378                    } else {
379                        prop.setValue(newVals.toArray(new Value[newVals.size()]));
380                    }
381                } catch (final RepositoryException ex) {
382                    // Ignore error from trying to update properties on versioned resources
383                    if (ex instanceof javax.jcr.nodetype.ConstraintViolationException &&
384                            ex.getMessage().contains(JCR_VERSION_STORAGE)) {
385                        LOGGER.debug("Ignoring exception trying to remove property from versioned resource: {}",
386                                ex.getMessage());
387                    } else {
388                        throw new RepositoryRuntimeException(ex);
389                    }
390                }
391            });
392
393            final Node parent = getNode().getDepth() > 0 ? getNode().getParent() : null;
394
395            final String name = getNode().getName();
396
397            // This is resolved immediately b/c we delete the node before updating an indirect container's target
398            final boolean shouldUpdateIndirectResource = ldpInsertedContentProperty(node)
399                .flatMap(resourceToProperty(getSession())).filter(this::hasProperty).isPresent();
400
401            final Optional<Node> containingNode = getContainingNode(getNode());
402
403            node.remove();
404
405            if (parent != null) {
406                createTombstone(parent, name);
407
408                // also update membershipResources for Direct/Indirect Containers
409                containingNode.filter(UncheckedPredicate.uncheck((final Node ancestor) ->
410                            ancestor.hasProperty(LDP_MEMBER_RESOURCE) && (ancestor.isNodeType(LDP_DIRECT_CONTAINER) ||
411                            shouldUpdateIndirectResource)))
412                    .ifPresent(ancestor -> {
413                        try {
414                            FedoraTypesUtils.touch(ancestor.getProperty(LDP_MEMBER_RESOURCE).getNode());
415                        } catch (final RepositoryException ex) {
416                            throw new RepositoryRuntimeException(ex);
417                        }
418                    });
419            }
420        } catch (final javax.jcr.AccessDeniedException e) {
421            throw new AccessDeniedException(e);
422        } catch (final RepositoryException e) {
423            throw new RepositoryRuntimeException(e);
424        }
425    }
426
427    private void createTombstone(final Node parent, final String path) throws RepositoryException {
428        findOrCreateChild(parent, path, FEDORA_TOMBSTONE);
429    }
430
431    /* (non-Javadoc)
432     * @see org.fcrepo.kernel.api.models.FedoraResource#getCreatedDate()
433     */
434    @Override
435    public Date getCreatedDate() {
436        try {
437            if (hasProperty(JCR_CREATED)) {
438                return new Date(getTimestamp(JCR_CREATED, NO_TIME));
439            }
440        } catch (final PathNotFoundException e) {
441            throw new PathNotFoundRuntimeException(e);
442        } catch (final RepositoryException e) {
443            throw new RepositoryRuntimeException(e);
444        }
445        LOGGER.debug("Node {} does not have a createdDate", node);
446        return null;
447    }
448
449    /* (non-Javadoc)
450     * @see org.fcrepo.kernel.api.models.FedoraResource#getLastModifiedDate()
451     */
452
453    /**
454     * This method gets the last modified date for this FedoraResource.  Because
455     * the last modified date is managed by fcrepo (not ModeShape) while the created
456     * date *is* managed by ModeShape in the current implementation it's possible that
457     * the last modified date will be before the created date.  Instead of making
458     * a second update to correct the modified date, in cases where the modified
459     * date is ealier than the created date, this class presents the created date instead.
460     *
461     * Any method that exposes the last modified date must maintain this illusion so
462     * that that external callers are presented with a sensible and consistent
463     * representation of this resource.
464     * @return the last modified Date (or the created date if it was after the last
465     *         modified date)
466     */
467    @Override
468    public Date getLastModifiedDate() {
469
470        final Date createdDate = getCreatedDate();
471        try {
472            final long created = createdDate == null ? NO_TIME : createdDate.getTime();
473            if (hasProperty(FEDORA_LASTMODIFIED)) {
474                return new Date(getTimestamp(FEDORA_LASTMODIFIED, created));
475            } else if (hasProperty(JCR_LASTMODIFIED)) {
476                return new Date(getTimestamp(JCR_LASTMODIFIED, created));
477            }
478        } catch (final PathNotFoundException e) {
479            throw new PathNotFoundRuntimeException(e);
480        } catch (final RepositoryException e) {
481            throw new RepositoryRuntimeException(e);
482        }
483        LOGGER.debug("Could not get last modified date property for node {}", node);
484
485        if (createdDate != null) {
486            LOGGER.trace("Using created date for last modified date for node {}", node);
487            return createdDate;
488        }
489
490        return null;
491    }
492
493    private long getTimestamp(final String property, final long created) throws RepositoryException {
494        LOGGER.trace("Using {} date", property);
495        final long timestamp = getProperty(property).getDate().getTimeInMillis();
496        if (timestamp < created && created > NO_TIME) {
497            LOGGER.trace("Returning the later created date ({} > {}) for {}", created, timestamp, property);
498            return created;
499        }
500        return timestamp;
501    }
502
503    /**
504     * Set the last-modified date to the current date.
505     */
506    public void touch() {
507        FedoraTypesUtils.touch(getNode());
508    }
509
510    @Override
511    public boolean hasType(final String type) {
512        try {
513            if (type.equals(FEDORA_REPOSITORY_ROOT)) {
514                return node.isNodeType(ROOT);
515            } else if (isFrozen.test(node) && hasProperty(FROZEN_MIXIN_TYPES)) {
516                return property2values.apply(getProperty(FROZEN_MIXIN_TYPES)).map(uncheck(Value::getString))
517                    .anyMatch(type::equals);
518            }
519            return node.isNodeType(type);
520        } catch (final PathNotFoundException e) {
521            throw new PathNotFoundRuntimeException(e);
522        } catch (final RepositoryException e) {
523            throw new RepositoryRuntimeException(e);
524        }
525    }
526
527    @Override
528    public List<URI> getTypes() {
529        try {
530            final List<NodeType> nodeTypes = new ArrayList<>();
531            final NodeType primaryNodeType = node.getPrimaryNodeType();
532            nodeTypes.add(primaryNodeType);
533            nodeTypes.addAll(asList(primaryNodeType.getSupertypes()));
534            final List<NodeType> mixinTypes = asList(node.getMixinNodeTypes());
535
536            nodeTypes.addAll(mixinTypes);
537            mixinTypes.stream()
538                .map(NodeType::getSupertypes)
539                .flatMap(Arrays::stream)
540                .forEach(nodeTypes::add);
541
542            final List<URI> types = nodeTypes.stream()
543                .map(uncheck(NodeType::getName))
544                .filter(hasInternalNamespace.negate())
545                .distinct()
546                .map(nodeTypeNameToURI)
547                .peek(x -> LOGGER.debug("node has rdf:type {}", x))
548                .collect(Collectors.toList());
549
550            if (isFrozenResource()) {
551                types.add(URI.create(REPOSITORY_NAMESPACE + "Version"));
552            }
553
554            return types;
555
556        } catch (final PathNotFoundException e) {
557            throw new PathNotFoundRuntimeException(e);
558        } catch (final RepositoryException e) {
559            throw new RepositoryRuntimeException(e);
560        }
561    }
562
563    private final Function<String, URI> nodeTypeNameToURI = uncheck(name -> {
564        final String prefix = name.split(":")[0];
565        final String typeName = name.split(":")[1];
566        final String namespace = getSession().getWorkspace().getNamespaceRegistry().getURI(prefix);
567        return URI.create(getRDFNamespaceForJcrNamespace(namespace) + typeName);
568    });
569
570    /* (non-Javadoc)
571     * @see org.fcrepo.kernel.api.models.FedoraResource#updateProperties
572     *     (org.fcrepo.kernel.api.identifiers.IdentifierConverter, java.lang.String, RdfStream)
573     */
574    @Override
575    public void updateProperties(final IdentifierConverter<Resource, FedoraResource> idTranslator,
576                                 final String sparqlUpdateStatement, final RdfStream originalTriples)
577            throws MalformedRdfException, AccessDeniedException {
578
579        final Model model = originalTriples.collect(toModel());
580
581        final UpdateRequest request = create(sparqlUpdateStatement,
582                idTranslator.reverse().convert(this).toString());
583
584        final Collection<IllegalArgumentException> errors = checkInvalidPredicates(request);
585
586        final NamespaceRegistry namespaceRegistry = getNamespaceRegistry(getSession());
587
588        request.getPrefixMapping().getNsPrefixMap().forEach(
589            (k,v) -> {
590                try {
591                    LOGGER.debug("Prefix mapping is key:{} -> value:{}", k, v);
592                    if (Arrays.asList(namespaceRegistry.getPrefixes()).contains(k)
593                        &&  !v.equals(namespaceRegistry.getURI(k))) {
594
595                        final String namespaceURI = namespaceRegistry.getURI(k);
596                        LOGGER.debug("Prefix has already been defined: {}:{}", k, namespaceURI);
597                        throw new InvalidPrefixException("Prefix already exists as: " + k + " -> " + namespaceURI);
598                   }
599
600                } catch (final RepositoryException e) {
601                    throw new RepositoryRuntimeException(e);
602                }
603           });
604
605        if (!errors.isEmpty()) {
606            throw new IllegalArgumentException(errors.stream().map(Exception::getMessage).collect(joining(",\n")));
607        }
608
609        final JcrPropertyStatementListener listener = new JcrPropertyStatementListener(
610                idTranslator, getSession(), idTranslator.reverse().convert(this).asNode());
611
612        model.register(listener);
613
614        // If this resource's structural parent is an IndirectContainer, check whether the
615        // ldp:insertedContentRelation property is present in the stream of changed triples.
616        // If so, set the propertyChanged value to true.
617        final AtomicBoolean propertyChanged = new AtomicBoolean();
618        ldpInsertedContentProperty(getNode()).ifPresent(resource -> {
619            model.register(new PropertyChangedListener(resource, propertyChanged));
620        });
621
622        model.setNsPrefixes(request.getPrefixMapping());
623        execute(request, model);
624
625        removeEmptyFragments();
626
627        listener.assertNoExceptions();
628
629        // Update the fedora:lastModified property
630        touch();
631
632        // Update the fedora:lastModified property of the ldp:memberResource
633        // resource, if necessary.
634        if (propertyChanged.get()) {
635            touchLdpMembershipResource(getNode());
636        }
637    }
638
639    @Override
640    public RdfStream getTriples(final IdentifierConverter<Resource, FedoraResource> idTranslator,
641                                final TripleCategory context) {
642        return getTriples(idTranslator, singleton(context));
643    }
644
645    @Override
646    public RdfStream getTriples(final IdentifierConverter<Resource, FedoraResource> idTranslator,
647                                final Set<? extends TripleCategory> contexts) {
648
649        return new DefaultRdfStream(idTranslator.reverse().convert(this).asNode(), contexts.stream()
650                .filter(contextMap::containsKey)
651                .map(x -> contextMap.get(x).apply(this).apply(idTranslator).apply(contexts.contains(MINIMAL)))
652                .reduce(empty(), Stream::concat));
653    }
654
655    /*
656     * (non-Javadoc)
657     * @see org.fcrepo.kernel.api.models.FedoraResource#getBaseVersion()
658     */
659    @Override
660    public Version getBaseVersion() {
661        try {
662            return getVersionManager().getBaseVersion(getPath());
663        } catch (final RepositoryException e) {
664            throw new RepositoryRuntimeException(e);
665        }
666    }
667
668    /*
669     * (non-Javadoc)
670     * @see org.fcrepo.kernel.api.models.FedoraResource#getVersionHistory()
671     */
672    @Override
673    public VersionHistory getVersionHistory() {
674        try {
675            return getVersionManager().getVersionHistory(getPath());
676        } catch (final RepositoryException e) {
677            throw new RepositoryRuntimeException(e);
678        }
679    }
680
681    /* (non-Javadoc)
682     * @see org.fcrepo.kernel.api.models.FedoraResource#isNew()
683     */
684    @Override
685    public Boolean isNew() {
686        return node.isNew();
687    }
688
689    /* (non-Javadoc)
690     * @see org.fcrepo.kernel.api.models.FedoraResource#replaceProperties
691     *     (org.fcrepo.kernel.api.identifiers.IdentifierConverter, com.hp.hpl.jena.rdf.model.Model)
692     */
693    @Override
694    public void replaceProperties(final IdentifierConverter<Resource, FedoraResource> idTranslator,
695        final Model inputModel, final RdfStream originalTriples) throws MalformedRdfException {
696
697        try (final RdfStream replacementStream =
698                new DefaultRdfStream(idTranslator.reverse().convert(this).asNode())) {
699
700            final GraphDifferencer differencer =
701                new GraphDifferencer(inputModel, originalTriples);
702
703            final StringBuilder exceptions = new StringBuilder();
704            try (final DefaultRdfStream diffStream =
705                    new DefaultRdfStream(replacementStream.topic(), differencer.difference())) {
706                new RdfRemover(idTranslator, getSession(), diffStream).consume();
707            } catch (final ConstraintViolationException e) {
708                throw e;
709            } catch (final MalformedRdfException e) {
710                exceptions.append(e.getMessage());
711                exceptions.append("\n");
712            }
713
714            try (final DefaultRdfStream notCommonStream =
715                    new DefaultRdfStream(replacementStream.topic(), differencer.notCommon())) {
716                new RdfAdder(idTranslator, getSession(), notCommonStream).consume();
717            } catch (final ConstraintViolationException e) {
718                throw e;
719            } catch (final MalformedRdfException e) {
720                exceptions.append(e.getMessage());
721            }
722
723            // If this resource's structural parent is an IndirectContainer, check whether the
724            // ldp:insertedContentRelation property is present in the stream of changed triples.
725            // If so, set the propertyChanged value to true.
726            final AtomicBoolean propertyChanged = new AtomicBoolean();
727            ldpInsertedContentProperty(getNode()).ifPresent(resource -> {
728                propertyChanged.set(differencer.notCommon().map(Triple::getPredicate).anyMatch(resource::equals));
729            });
730
731            removeEmptyFragments();
732
733            if (exceptions.length() > 0) {
734                throw new MalformedRdfException(exceptions.toString());
735            }
736
737            // Update the fedora:lastModified property
738            touch();
739
740            // If the ldp:insertedContentRelation property was changed, update the
741            // ldp:membershipResource resource.
742            if (propertyChanged.get()) {
743                touchLdpMembershipResource(getNode());
744            }
745        }
746    }
747
748    private void removeEmptyFragments() {
749        try {
750            if (node.hasNode("#")) {
751                @SuppressWarnings("unchecked")
752                final Iterator<Node> nodes = node.getNode("#").getNodes();
753                nodes.forEachRemaining(n -> {
754                    try {
755                        @SuppressWarnings("unchecked")
756                        final Iterator<Property> properties = n.getProperties();
757                        final boolean hasUserProps = iteratorToStream(properties).map(propertyConverter::convert)
758                            .anyMatch(isManagedPredicate.negate());
759
760                        final boolean hasUserTypes = Arrays.stream(n.getMixinNodeTypes())
761                            .map(uncheck(NodeType::getName)).filter(hasInternalNamespace.negate())
762                            .map(uncheck(type ->
763                                getSession().getWorkspace().getNamespaceRegistry().getURI(type.split(":")[0])))
764                            .anyMatch(isManagedNamespace.negate());
765
766                        if (!hasUserProps && !hasUserTypes && !n.getWeakReferences().hasNext() &&
767                                !n.getReferences().hasNext()) {
768                            LOGGER.debug("Removing empty hash URI node: {}", n.getName());
769                            n.remove();
770                        }
771                    } catch (final RepositoryException ex) {
772                        throw new RepositoryRuntimeException("Error removing empty fragments", ex);
773                    }
774                });
775            }
776        } catch (final RepositoryException ex) {
777            throw new RepositoryRuntimeException("Error removing empty fragments", ex);
778        }
779    }
780
781    /* (non-Javadoc)
782     * @see org.fcrepo.kernel.api.models.FedoraResource#getEtagValue()
783     */
784    @Override
785    public String getEtagValue() {
786        final Date lastModifiedDate = getLastModifiedDate();
787
788        if (lastModifiedDate != null) {
789            return shaHex(getPath() + lastModifiedDate.getTime());
790        }
791        return "";
792    }
793
794    @Override
795    public void enableVersioning() {
796        try {
797            node.addMixin("mix:versionable");
798        } catch (final RepositoryException e) {
799            throw new RepositoryRuntimeException(e);
800        }
801    }
802
803    @Override
804    public void disableVersioning() {
805        try {
806            node.removeMixin("mix:versionable");
807        } catch (final RepositoryException e) {
808            throw new RepositoryRuntimeException(e);
809        }
810
811    }
812
813    @Override
814    public boolean isVersioned() {
815        try {
816            return node.isNodeType("mix:versionable");
817        } catch (final RepositoryException e) {
818            throw new RepositoryRuntimeException(e);
819        }
820    }
821
822    @Override
823    public boolean isFrozenResource() {
824        return isFrozenNode.test(this);
825    }
826
827    @Override
828    public FedoraResource getVersionedAncestor() {
829
830        try {
831            if (!isFrozenResource()) {
832                return null;
833            }
834
835            Node versionableFrozenNode = getNode();
836            FedoraResource unfrozenResource = getUnfrozenResource();
837
838            // traverse the frozen tree looking for a node whose unfrozen equivalent is versioned
839            while (!unfrozenResource.isVersioned()) {
840
841                if (versionableFrozenNode.getDepth() == 0) {
842                    return null;
843                }
844
845                // node in the frozen tree
846                versionableFrozenNode = versionableFrozenNode.getParent();
847
848                // unfrozen equivalent
849                unfrozenResource = new FedoraResourceImpl(versionableFrozenNode).getUnfrozenResource();
850            }
851
852            return new FedoraResourceImpl(versionableFrozenNode);
853        } catch (final RepositoryException e) {
854            throw new RepositoryRuntimeException(e);
855        }
856
857    }
858
859    @Override
860    public FedoraResource getUnfrozenResource() {
861        if (!isFrozenResource()) {
862            return this;
863        }
864
865        try {
866            // Either this resource is frozen
867            if (hasProperty(JCR_FROZEN_UUID)) {
868                try {
869                    return new FedoraResourceImpl(getNodeByProperty(getProperty(JCR_FROZEN_UUID)));
870                } catch (final ItemNotFoundException e) {
871                    // The unfrozen resource has been deleted, return the tombstone.
872                    return new TombstoneImpl(getNode());
873                }
874
875                // ..Or it is a child-version-history on a frozen path
876            } else if (hasProperty(JCR_CHILD_VERSION_HISTORY)) {
877                final Node childVersionHistory = getNodeByProperty(getProperty(JCR_CHILD_VERSION_HISTORY));
878                try {
879                    final Node childNode = getNodeByProperty(childVersionHistory.getProperty(JCR_VERSIONABLE_UUID));
880                    return new FedoraResourceImpl(childNode);
881                } catch (final ItemNotFoundException e) {
882                    // The unfrozen resource has been deleted, return the tombstone.
883                    return new TombstoneImpl(childVersionHistory);
884                }
885
886            } else {
887                throw new RepositoryRuntimeException("Resource must be frozen or a child-history!");
888            }
889        } catch (final RepositoryException e) {
890            throw new RepositoryRuntimeException(e);
891        }
892    }
893
894    @Override
895    public FedoraResource getVersion(final String label) {
896        try {
897            final Node n = getFrozenNode(label);
898
899            if (n != null) {
900                return new FedoraResourceImpl(n);
901            }
902
903            if (isVersioned()) {
904                final VersionHistory hist = getVersionManager().getVersionHistory(getPath());
905
906                if (hist.hasVersionLabel(label)) {
907                    LOGGER.debug("Found version for {} by label {}.", this, label);
908                    return new FedoraResourceImpl(hist.getVersionByLabel(label).getFrozenNode());
909                }
910            }
911
912            LOGGER.warn("Unknown version {} with label {}!", getPath(), label);
913            return null;
914        } catch (final RepositoryException e) {
915            throw new RepositoryRuntimeException(e);
916        }
917
918    }
919
920    @Override
921    public String getVersionLabelOfFrozenResource() {
922        if (!isFrozenResource()) {
923            return null;
924        }
925
926        // Version History associated with this resource
927        final VersionHistory versionHistory = getUnfrozenResource().getVersionHistory();
928
929        // Frozen node is required to find associated version label
930        final Node frozenResource;
931        try {
932            // Possibly the frozen node is nested inside of current child-version-history
933            if (getNode().hasProperty(JCR_CHILD_VERSION_HISTORY)) {
934                final Node childVersionHistory = getNodeByProperty(getProperty(JCR_CHILD_VERSION_HISTORY));
935                final Node childNode = getNodeByProperty(childVersionHistory.getProperty(JCR_VERSIONABLE_UUID));
936                final Version childVersion = getVersionManager().getBaseVersion(childNode.getPath());
937                frozenResource = childVersion.getFrozenNode();
938
939            } else {
940                frozenResource = getNode();
941            }
942
943            // Loop versions
944            @SuppressWarnings("unchecked")
945            final Stream<Version> versions = iteratorToStream(versionHistory.getAllVersions());
946            return versions
947                .filter(UncheckedPredicate.uncheck(version -> version.getFrozenNode().equals(frozenResource)))
948                .map(uncheck(versionHistory::getVersionLabels))
949                .flatMap(Arrays::stream)
950                .findFirst().orElse(null);
951        } catch (final RepositoryException e) {
952            throw new RepositoryRuntimeException(e);
953        }
954    }
955
956    private Node getNodeByProperty(final Property property) throws RepositoryException {
957        return getSession().getNodeByIdentifier(property.getString());
958    }
959
960    protected VersionManager getVersionManager() {
961        try {
962            return getSession().getWorkspace().getVersionManager();
963        } catch (final RepositoryException e) {
964            throw new RepositoryRuntimeException(e);
965        }
966    }
967
968    /**
969     * Helps ensure that there are no terminating slashes in the predicate.
970     * A terminating slash means ModeShape has trouble extracting the localName, e.g., for
971     * http://myurl.org/.
972     *
973     * @see <a href="https://jira.duraspace.org/browse/FCREPO-1409"> FCREPO-1409 </a> for details.
974     */
975    private static Collection<IllegalArgumentException> checkInvalidPredicates(final UpdateRequest request) {
976        return request.getOperations().stream()
977                .flatMap(x -> {
978                    if (x instanceof UpdateModify) {
979                        final UpdateModify y = (UpdateModify)x;
980                        return concat(y.getInsertQuads().stream(), y.getDeleteQuads().stream());
981                    } else if (x instanceof UpdateData) {
982                        return ((UpdateData)x).getQuads().stream();
983                    } else if (x instanceof UpdateDeleteWhere) {
984                        return ((UpdateDeleteWhere)x).getQuads().stream();
985                    } else {
986                        return empty();
987                    }
988                })
989                .filter(x -> x.getPredicate().isURI() && x.getPredicate().getURI().endsWith("/"))
990                .map(x -> new IllegalArgumentException("Invalid predicate ends with '/': " + x.getPredicate().getURI()))
991                .collect(Collectors.toList());
992    }
993
994    private Node getFrozenNode(final String label) throws RepositoryException {
995        try {
996            final Session session = getSession();
997
998            final Node frozenNode = session.getNodeByIdentifier(label);
999
1000            final String baseUUID = getNode().getIdentifier();
1001
1002            /*
1003             * We found a node whose identifier is the "label" for the version.  Now
1004             * we must do due dilligence to make sure it's a frozen node representing
1005             * a version of the subject node.
1006             */
1007            final Property p = frozenNode.getProperty(JCR_FROZEN_UUID);
1008            if (p != null) {
1009                if (p.getString().equals(baseUUID)) {
1010                    return frozenNode;
1011                }
1012            }
1013            /*
1014             * Though a node with an id of the label was found, it wasn't the
1015             * node we were looking for, so fall through and look for a labeled
1016             * node.
1017             */
1018        } catch (final ItemNotFoundException ex) {
1019            /*
1020             * the label wasn't a uuid of a frozen node but
1021             * instead possibly a version label.
1022             */
1023        }
1024        return null;
1025    }
1026
1027    @Override
1028    public boolean equals(final Object object) {
1029        if (object instanceof FedoraResourceImpl) {
1030            return ((FedoraResourceImpl) object).getNode().equals(this.getNode());
1031        }
1032        return false;
1033    }
1034
1035    @Override
1036    public int hashCode() {
1037        return getNode().hashCode();
1038    }
1039
1040    protected Session getSession() {
1041        try {
1042            return getNode().getSession();
1043        } catch (final RepositoryException e) {
1044            throw new RepositoryRuntimeException(e);
1045        }
1046    }
1047
1048    @Override
1049    public String toString() {
1050        return getNode().toString();
1051    }
1052
1053    protected Property getProperty(final String relPath) {
1054        try {
1055            return getNode().getProperty(relPath);
1056        } catch (final RepositoryException e) {
1057            throw new RepositoryRuntimeException(e);
1058        }
1059    }
1060
1061    /**
1062     * A method that takes a Triple and returns a Triple that is the correct representation of
1063     * that triple for the given resource.  The current implementation of this method is used by
1064     * {@link PropertiesRdfContext} to replace the reported {@link org.fcrepo.kernel.api.RdfLexicon#LAST_MODIFIED_DATE}
1065     * with the one produced by {@link #getLastModifiedDate}.
1066     * @param r the Fedora resource
1067     * @param translator a converter to get the external identifier from a jcr node
1068     * @return a function to convert triples
1069     */
1070    public static Function<Triple, Triple> fixDatesIfNecessary(final FedoraResource r,
1071                                                      final Converter<Node, Resource> translator) {
1072        return t -> {
1073            if (t.getPredicate().toString().equals(LAST_MODIFIED_DATE.toString())
1074                    && t.getSubject().equals(translator.convert(getJcrNode(r)).asNode())) {
1075                final Calendar c = Calendar.getInstance();
1076                c.setTime(r.getLastModifiedDate());
1077                return new Triple(t.getSubject(), t.getPredicate(), createTypedLiteral(c).asNode());
1078            }
1079            return t;
1080            };
1081    }
1082
1083}