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