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.google.common.net.MediaType.parse;
021import static java.time.Instant.ofEpochMilli;
022import static java.util.Arrays.asList;
023import static java.util.Collections.singleton;
024import static java.util.stream.Collectors.joining;
025import static java.util.stream.Collectors.toList;
026import static java.util.stream.Stream.concat;
027import static java.util.stream.Stream.empty;
028import static java.util.stream.Stream.of;
029import static org.apache.commons.codec.digest.DigestUtils.sha1Hex;
030import static org.apache.jena.graph.NodeFactory.createURI;
031import static org.apache.jena.rdf.model.ResourceFactory.createProperty;
032import static org.apache.jena.rdf.model.ResourceFactory.createResource;
033import static org.apache.jena.rdf.model.ResourceFactory.createTypedLiteral;
034import static org.apache.jena.update.UpdateAction.execute;
035import static org.apache.jena.update.UpdateFactory.create;
036import static org.fcrepo.kernel.api.RdfCollectors.toModel;
037import static org.fcrepo.kernel.api.RdfLexicon.DIRECT_CONTAINER;
038import static org.fcrepo.kernel.api.RdfLexicon.HAS_MEMBER_RELATION;
039import static org.fcrepo.kernel.api.RdfLexicon.INDIRECT_CONTAINER;
040import static org.fcrepo.kernel.api.RdfLexicon.INSERTED_CONTENT_RELATION;
041import static org.fcrepo.kernel.api.RdfLexicon.INTERACTION_MODELS;
042import static org.fcrepo.kernel.api.RdfLexicon.LAST_MODIFIED_DATE;
043import static org.fcrepo.kernel.api.RdfLexicon.LDP_NAMESPACE;
044import static org.fcrepo.kernel.api.RdfLexicon.MEMBERSHIP_RESOURCE;
045import static org.fcrepo.kernel.api.RdfLexicon.MEMBER_SUBJECT;
046import static org.fcrepo.kernel.api.RdfLexicon.RDF_NAMESPACE;
047import static org.fcrepo.kernel.api.RdfLexicon.isManagedNamespace;
048import static org.fcrepo.kernel.api.RdfLexicon.isManagedPredicate;
049import static org.fcrepo.kernel.api.RdfLexicon.isRelaxed;
050import static org.fcrepo.kernel.api.RequiredRdfContext.EMBED_RESOURCES;
051import static org.fcrepo.kernel.api.RequiredRdfContext.INBOUND_REFERENCES;
052import static org.fcrepo.kernel.api.RequiredRdfContext.LDP_CONTAINMENT;
053import static org.fcrepo.kernel.api.RequiredRdfContext.LDP_MEMBERSHIP;
054import static org.fcrepo.kernel.api.RequiredRdfContext.MINIMAL;
055import static org.fcrepo.kernel.api.RequiredRdfContext.PROPERTIES;
056import static org.fcrepo.kernel.api.RequiredRdfContext.SERVER_MANAGED;
057import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.JCR_CREATED;
058import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.JCR_LASTMODIFIED;
059import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.ROOT;
060import static org.fcrepo.kernel.modeshape.RdfJcrLexicon.jcrProperties;
061import static org.fcrepo.kernel.modeshape.identifiers.NodeResourceConverter.nodeConverter;
062import static org.fcrepo.kernel.modeshape.rdf.JcrRdfTools.getRDFNamespaceForJcrNamespace;
063import static org.fcrepo.kernel.modeshape.services.functions.JcrPropertyFunctions.property2values;
064import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.getContainingNode;
065import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.getJcrNode;
066import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.hasInternalNamespace;
067import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.isAcl;
068import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.isInternalNode;
069import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.isMemento;
070import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.isTimeMap;
071import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.ldpInsertedContentProperty;
072import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.resourceToProperty;
073import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.touchLdpMembershipResource;
074import static org.fcrepo.kernel.modeshape.utils.NamespaceTools.getNamespaceRegistry;
075import static org.fcrepo.kernel.modeshape.utils.StreamUtils.iteratorToStream;
076import static org.fcrepo.kernel.modeshape.utils.UncheckedFunction.uncheck;
077import static org.fcrepo.kernel.api.RdfLexicon.LDPCV_TIME_MAP;
078import static org.fcrepo.kernel.api.RdfLexicon.LDP_MEMBER;
079import static org.modeshape.jcr.api.JcrConstants.JCR_CONTENT;
080import static org.modeshape.jcr.api.JcrConstants.NT_FOLDER;
081import static org.slf4j.LoggerFactory.getLogger;
082
083import java.net.URI;
084import java.time.Instant;
085import java.time.temporal.ChronoUnit;
086import java.time.temporal.Temporal;
087import java.util.ArrayList;
088import java.util.Arrays;
089import java.util.Calendar;
090import java.util.Collection;
091import java.util.Comparator;
092import java.util.Iterator;
093import java.util.List;
094import java.util.Map;
095import java.util.Optional;
096import java.util.Set;
097import java.util.concurrent.atomic.AtomicBoolean;
098import java.util.function.Function;
099import java.util.function.Predicate;
100import java.util.stream.Collectors;
101import java.util.stream.Stream;
102import javax.jcr.NamespaceRegistry;
103import javax.jcr.Node;
104import javax.jcr.PathNotFoundException;
105import javax.jcr.Property;
106import javax.jcr.RepositoryException;
107import javax.jcr.Session;
108import javax.jcr.Value;
109import javax.jcr.nodetype.NodeType;
110
111import com.google.common.annotations.VisibleForTesting;
112import com.google.common.base.Converter;
113import com.google.common.collect.ImmutableList;
114import com.google.common.collect.ImmutableMap;
115import org.apache.commons.lang3.StringUtils;
116import org.apache.jena.graph.Triple;
117import org.apache.jena.rdf.model.Model;
118import org.apache.jena.rdf.model.Resource;
119import org.apache.jena.rdf.model.Statement;
120import org.apache.jena.rdf.model.StmtIterator;
121import org.apache.jena.sparql.core.Quad;
122import org.apache.jena.sparql.modify.request.UpdateData;
123import org.apache.jena.sparql.modify.request.UpdateDeleteWhere;
124import org.apache.jena.sparql.modify.request.UpdateModify;
125import org.apache.jena.update.Update;
126import org.apache.jena.update.UpdateRequest;
127import org.apache.jena.vocabulary.RDF;
128import org.fcrepo.kernel.api.FedoraTypes;
129import org.fcrepo.kernel.api.RdfLexicon;
130import org.fcrepo.kernel.api.RdfStream;
131import org.fcrepo.kernel.api.TripleCategory;
132import org.fcrepo.kernel.api.exception.AccessDeniedException;
133import org.fcrepo.kernel.api.exception.ConstraintViolationException;
134import org.fcrepo.kernel.api.exception.InteractionModelViolationException;
135import org.fcrepo.kernel.api.exception.InvalidPrefixException;
136import org.fcrepo.kernel.api.exception.MalformedRdfException;
137import org.fcrepo.kernel.api.exception.PathNotFoundRuntimeException;
138import org.fcrepo.kernel.api.exception.RepositoryRuntimeException;
139import org.fcrepo.kernel.api.exception.ServerManagedPropertyException;
140import org.fcrepo.kernel.api.exception.ServerManagedTypeException;
141import org.fcrepo.kernel.api.identifiers.IdentifierConverter;
142import org.fcrepo.kernel.api.models.FedoraResource;
143import org.fcrepo.kernel.api.models.FedoraTimeMap;
144import org.fcrepo.kernel.api.models.NonRdfSourceDescription;
145import org.fcrepo.kernel.api.rdf.DefaultRdfStream;
146import org.fcrepo.kernel.api.utils.GraphDifferencer;
147import org.fcrepo.kernel.api.utils.RelaxedPropertiesHelper;
148import org.fcrepo.kernel.modeshape.rdf.converters.PropertyConverter;
149import org.fcrepo.kernel.modeshape.rdf.impl.ChildrenRdfContext;
150import org.fcrepo.kernel.modeshape.rdf.impl.ContentRdfContext;
151import org.fcrepo.kernel.modeshape.rdf.impl.HashRdfContext;
152import org.fcrepo.kernel.modeshape.rdf.impl.InternalIdentifierTranslator;
153import org.fcrepo.kernel.modeshape.rdf.impl.LdpContainerRdfContext;
154import org.fcrepo.kernel.modeshape.rdf.impl.LdpIsMemberOfRdfContext;
155import org.fcrepo.kernel.modeshape.rdf.impl.LdpRdfContext;
156import org.fcrepo.kernel.modeshape.rdf.impl.PropertiesRdfContext;
157import org.fcrepo.kernel.modeshape.rdf.impl.ReferencesRdfContext;
158import org.fcrepo.kernel.modeshape.rdf.impl.RootRdfContext;
159import org.fcrepo.kernel.modeshape.rdf.impl.SkolemNodeRdfContext;
160import org.fcrepo.kernel.modeshape.rdf.impl.TypeRdfContext;
161import org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils;
162import org.fcrepo.kernel.modeshape.utils.FilteringJcrPropertyStatementListener;
163import org.fcrepo.kernel.modeshape.utils.PropertyChangedListener;
164import org.fcrepo.kernel.modeshape.utils.UncheckedPredicate;
165import org.fcrepo.kernel.modeshape.utils.iterators.RdfAdder;
166import org.fcrepo.kernel.modeshape.utils.iterators.RdfRemover;
167import org.modeshape.jcr.api.JcrTools;
168import org.slf4j.Logger;
169
170/**
171 * Common behaviors across {@link org.fcrepo.kernel.api.models.Container} and
172 * {@link org.fcrepo.kernel.api.models.NonRdfSourceDescription} types; also used
173 * when the exact type of an object is irrelevant
174 *
175 * @author ajs6f
176 */
177public class FedoraResourceImpl extends JcrTools implements FedoraTypes, FedoraResource {
178
179    private static final Logger LOGGER = getLogger(FedoraResourceImpl.class);
180
181    private static final long NO_TIME = 0L;
182
183    private static final PropertyConverter propertyConverter = new PropertyConverter();
184
185    public static final String CONTAINER_WEBAC_ACL = "fedora:acl";
186
187    private static final String RDF_TYPE_URI = RDF_NAMESPACE + "type";
188
189    // A curried type accepting resource, translator, and "minimality", returning triples.
190    protected interface RdfGenerator extends Function<FedoraResource,
191    Function<IdentifierConverter<Resource, FedoraResource>, Function<Boolean, Stream<Triple>>>> {}
192
193    private static final RdfGenerator getDefaultTriples = resource -> translator -> uncheck(minimal -> {
194        final Stream<Stream<Triple>> min = of(
195            new TypeRdfContext(resource, translator),
196            new PropertiesRdfContext(resource, translator));
197        if (!minimal) {
198            final Stream<Stream<Triple>> extra = of(
199                new HashRdfContext(resource, translator),
200                new SkolemNodeRdfContext(resource, translator));
201            return concat(min, extra).reduce(empty(), Stream::concat);
202        }
203        return min.reduce(empty(), Stream::concat);
204    });
205
206    private static final RdfGenerator getEmbeddedResourceTriples = resource -> translator -> uncheck(minimal ->
207            resource.getChildren().flatMap(child -> child.getTriples(translator, PROPERTIES)));
208
209    private static final RdfGenerator getInboundTriples = resource -> translator -> uncheck(_minimal -> {
210        return new ReferencesRdfContext(resource, translator);
211    });
212
213    private static final RdfGenerator getLdpContainsTriples = resource -> translator -> uncheck(_minimal -> {
214        return new ChildrenRdfContext(resource, translator);
215    });
216
217    private static final RdfGenerator getServerManagedTriples = resource -> translator -> uncheck(minimal -> {
218        if (minimal) {
219            return new LdpRdfContext(resource, translator);
220        }
221        final Stream<Stream<Triple>> streams = of(
222            new LdpRdfContext(resource, translator),
223            new RootRdfContext(resource, translator),
224            new ContentRdfContext(resource, translator));
225        return streams.reduce(empty(), Stream::concat);
226    });
227
228    private static final RdfGenerator getLdpMembershipTriples = resource -> translator -> uncheck(_minimal -> {
229        final Stream<Stream<Triple>> streams = of(
230            new LdpContainerRdfContext(resource, translator),
231            new LdpIsMemberOfRdfContext(resource, translator));
232        return streams.reduce(empty(), Stream::concat);
233    });
234
235    protected static final Map<TripleCategory, RdfGenerator> contextMap =
236            ImmutableMap.<TripleCategory, RdfGenerator>builder()
237                    .put(PROPERTIES, getDefaultTriples)
238                    .put(EMBED_RESOURCES, getEmbeddedResourceTriples)
239                    .put(INBOUND_REFERENCES, getInboundTriples)
240                    .put(SERVER_MANAGED, getServerManagedTriples)
241                    .put(LDP_MEMBERSHIP, getLdpMembershipTriples)
242                    .put(LDP_CONTAINMENT, getLdpContainsTriples)
243                    .build();
244
245    protected final Node node;
246
247    /*
248     * A terminating slash means ModeShape has trouble extracting the localName, e.g., for http://myurl.org/.
249     *
250     * @see <a href="https://jira.duraspace.org/browse/FCREPO-1409"> FCREPO-1409 </a> for details.
251     */
252    private static final Function<Triple, ConstraintViolationException> validatePredicateEndsWithSlash = uncheck(x -> {
253        if (x.getPredicate().isURI() && x.getPredicate().getURI().endsWith("/")) {
254            return new MalformedRdfException("Invalid predicate ends with '/': " + x.getPredicate().getURI());
255        }
256        return null;
257    });
258
259    /*
260     * Ensures the object URI is valid
261     */
262    private static final Function<Triple, ConstraintViolationException> validateObjectUrl = uncheck(x -> {
263        if (x.getObject().isURI()) {
264            final String uri = x.getObject().toString();
265            try {
266                new URI(uri);
267            } catch (final Exception ex) {
268                return new MalformedRdfException("Invalid object URI (" + uri + " ) : " + ex.getMessage());
269            }
270        }
271        return null;
272    });
273
274    private static final Function<Triple, ConstraintViolationException> validateMimeTypeTriple = uncheck(x -> {
275        /* only look at the mime type if it's not a sparql variable */
276        if (x.getPredicate().toString().equals(RdfLexicon.HAS_MIME_TYPE.toString()) &&
277                !x.getObject().toString(false).startsWith("?")) {
278            try {
279                parse(x.getObject().toString(false));
280            } catch (final Exception ex) {
281                return new MalformedRdfException("Invalid value for '" + RdfLexicon.HAS_MIME_TYPE +
282                        "' encountered : " + x.getObject().toString());
283            }
284        }
285        return null;
286    });
287
288
289    private static final Function<Triple, ConstraintViolationException> validateNoManagedTypes = uncheck(x ->  {
290        final org.apache.jena.graph.Node object = x.getObject();
291        final String predicateUri = x.getPredicate().getURI();
292        if (object.isURI() && RDF_TYPE_URI.equals(predicateUri) &&
293            isManagedNamespace.test(object.getNameSpace())) {
294            return new ServerManagedTypeException(
295                "The " + predicateUri + " predicate may not take an object in the server managed namespaces (" +
296                object.getNameSpace() + ").");
297        }
298        return null;
299    });
300
301    private static final Function<Triple, ConstraintViolationException> validateNoManagedPredicates  = uncheck(x ->  {
302        final String predicateUri = x.getPredicate().getURI();
303        final org.apache.jena.rdf.model.Property predicateProperty = createProperty(predicateUri);
304        if (isManagedPredicate.test(predicateProperty) && !isRelaxed.test(predicateProperty)) {
305            return new ServerManagedPropertyException(
306                "The server managed predicates (" + predicateUri + ") cannot be modified by the client.");
307        }
308
309        return null;
310    });
311
312    private static final Function<Triple, ConstraintViolationException> validateMemberRelation = uncheck(x -> {
313        final org.apache.jena.graph.Node object = x.getObject();
314        if (object.isURI() && x.getPredicate().getURI().equals(HAS_MEMBER_RELATION.toString()) &&
315            isManagedPredicate.test(createProperty(object.getURI()))) {
316            return new ServerManagedPropertyException(
317                "The " + HAS_MEMBER_RELATION + " predicate may not take the server managed type. (" +
318                object.getURI() + ").");
319        }
320
321        return null;
322    });
323
324    private static final List<Function<Triple, ConstraintViolationException>> tripleValidators =
325            ImmutableList.<Function<Triple, ConstraintViolationException>>builder()
326                    .add(validatePredicateEndsWithSlash)
327                    .add(validateObjectUrl)
328                    .add(validateMimeTypeTriple)
329                    .add(validateNoManagedTypes)
330                    .add(validateNoManagedPredicates)
331                    .add(validateMemberRelation).build();
332
333    /**
334     * Construct a {@link org.fcrepo.kernel.api.models.FedoraResource} from an existing JCR Node
335     * @param node an existing JCR node to treat as an fcrepo object
336     */
337    public FedoraResourceImpl(final Node node) {
338        this.node = node;
339    }
340
341    /**
342     * Return the underlying JCR Node for this resource
343     *
344     * @return the JCR Node
345     */
346    public Node getNode() {
347        return node;
348    }
349
350    /* (non-Javadoc)
351     * @see org.fcrepo.kernel.api.models.FedoraResource#getPath()
352     */
353    @Override
354    public String getPath() {
355        try {
356            final String path = node.getPath();
357            return path.endsWith("/" + JCR_CONTENT) ? path.substring(0, path.length() - JCR_CONTENT.length() - 1)
358                : path;
359        } catch (final RepositoryException e) {
360            throw new RepositoryRuntimeException(e);
361        }
362    }
363
364    /* (non-Javadoc)
365     * @see org.fcrepo.kernel.api.models.FedoraResource#getChildren(Boolean recursive)
366     */
367    @Override
368    public Stream<FedoraResource> getChildren(final Boolean recursive) {
369        try {
370            if (recursive) {
371                return nodeToGoodChildren(node).flatMap(FedoraResourceImpl::getAllChildren);
372            }
373            return nodeToGoodChildren(node);
374        } catch (final RepositoryException e) {
375            throw new RepositoryRuntimeException(e);
376        }
377    }
378
379    /* (non-Javadoc)
380     * @see org.fcrepo.kernel.api.models.FedoraResource#getDescription()
381     */
382    @Override
383    public FedoraResource getDescription() {
384        return this;
385    }
386
387    protected Node getDescriptionNode() {
388        return getNode();
389    }
390
391    /* (non-Javadoc)
392     * @see org.fcrepo.kernel.api.models.FedoraResource#getDescribedResource()
393     */
394    @Override
395    public FedoraResource getDescribedResource() {
396        return this;
397    }
398
399    /**
400     * Get the "good" children for a node by skipping all pairtree nodes in the way.
401     * @param input Node containing children
402     * @return Stream of good children
403     * @throws RepositoryException on error
404     */
405    @SuppressWarnings("unchecked")
406    private Stream<FedoraResource> nodeToGoodChildren(final Node input) throws RepositoryException {
407        return iteratorToStream(input.getNodes()).filter(nastyChildren.negate())
408            .flatMap(uncheck((final Node child) -> child.isNodeType(FEDORA_PAIRTREE) ? nodeToGoodChildren(child) :
409                        of(nodeConverter.convert(child))));
410    }
411
412    /**
413     * Get all children recursively, and flatten into a single Stream.
414     */
415    private static Stream<FedoraResource> getAllChildren(final FedoraResource resource) {
416        return concat(of(resource), resource.getChildren().flatMap(FedoraResourceImpl::getAllChildren));
417    }
418
419    /**
420     * Children for whom we will not generate triples.
421     */
422    private static final Predicate<Node> nastyChildren = isInternalNode
423                    .or(TombstoneImpl::hasMixin)
424                    .or(FedoraTimeMapImpl::hasMixin)
425                    .or(FedoraWebacAclImpl::hasMixin)
426                    .or(UncheckedPredicate.uncheck(p -> p.getName().equals(JCR_CONTENT)))
427                    .or(UncheckedPredicate.uncheck(p -> p.getName().equals("#")));
428
429    @Override
430    public FedoraResource getContainer() {
431        return getContainingNode(getNode()).map(nodeConverter::convert).orElse(null);
432    }
433
434    @Override
435    public FedoraResource getOriginalResource() {
436        if (!isMemento()) {
437            return this;
438        }
439
440        try {
441            return nodeConverter.convert(node.getParent().getParent());
442        } catch (final RepositoryException e) {
443            throw new RepositoryRuntimeException(e);
444        }
445    }
446
447    @Override
448    public FedoraResource getTimeMap() {
449        if (this instanceof FedoraTimeMap) {
450            return this;
451        }
452
453        try {
454            if (isOriginalResource()) {
455                return Optional.of(node.getNode(LDPCV_TIME_MAP)).map(nodeConverter::convert).orElse(null);
456            } else if (isMemento()) {
457                return Optional.of(node.getParent()).map(nodeConverter::convert).orElse(null);
458            } else {
459                throw new PathNotFoundException(
460                    "getTimeMap() is not supported for this node: " + node.getPath());
461            }
462        } catch (final PathNotFoundException e) {
463            throw new PathNotFoundRuntimeException(e);
464        } catch (final RepositoryException e) {
465            throw new RepositoryRuntimeException(e);
466        }
467    }
468
469    @Override
470    public Instant getMementoDatetime() {
471        try {
472            final Node node = getNode();
473            if (!isMemento() || !node.hasProperty(MEMENTO_DATETIME)) {
474                return null;
475            }
476
477            final Calendar calDate = node.getProperty(MEMENTO_DATETIME).getDate();
478            return calDate.toInstant();
479        } catch (final RepositoryException e) {
480            throw new RepositoryRuntimeException(e);
481        }
482    }
483
484    @Override
485    public boolean isOriginalResource() {
486        return !isMemento();
487    }
488
489    @Override
490    public boolean isTimeMap() {
491        return isTimeMap.test(getNode());
492    }
493
494    @Override
495    public boolean isMemento() {
496        return isMemento.test(getNode());
497    }
498
499    @Override
500    public boolean isAcl() {
501        return isAcl.test(getNode());
502    }
503
504    @Override
505    public FedoraResource getAcl() {
506        final Node parentNode;
507
508        try {
509            if (this instanceof NonRdfSourceDescription) {
510                parentNode = getNode().getParent();
511            } else {
512                parentNode = getNode();
513            }
514
515            if (!parentNode.hasNode(CONTAINER_WEBAC_ACL)) {
516                return null;
517            }
518
519            final Node aclNode = parentNode.getNode(CONTAINER_WEBAC_ACL);
520            return Optional.of(aclNode).map(nodeConverter::convert).orElse(null);
521        } catch (final RepositoryException e) {
522            throw new RepositoryRuntimeException(e);
523        }
524    }
525
526    @Override
527    public FedoraResource findOrCreateAcl() {
528        final Node aclNode;
529        try {
530            final Node parentNode;
531            if (this instanceof NonRdfSourceDescription) {
532                parentNode = getNode().getParent();
533            } else {
534                parentNode = getNode();
535            }
536
537            aclNode = findOrCreateChild(parentNode, CONTAINER_WEBAC_ACL, NT_FOLDER);
538            if (aclNode.isNew()) {
539                LOGGER.debug("Created Webac ACL {}", aclNode.getPath());
540
541                // add mixin type fedora:Resource
542                if (aclNode.canAddMixin(FEDORA_RESOURCE)) {
543                    aclNode.addMixin(FEDORA_RESOURCE);
544                }
545
546                // add mixin type webac:Acl
547                if (aclNode.canAddMixin(FEDORA_WEBAC_ACL)) {
548                    aclNode.addMixin(FEDORA_WEBAC_ACL);
549                }
550            }
551        } catch (final RepositoryException e) {
552            throw new RepositoryRuntimeException(e);
553        }
554        return Optional.of(aclNode).map(nodeConverter::convert).orElse(null);
555    }
556
557    @Override
558    public FedoraResource getChild(final String relPath) {
559        try {
560            return nodeConverter.convert(getNode().getNode(relPath));
561        } catch (final RepositoryException e) {
562            throw new RepositoryRuntimeException(e);
563        }
564    }
565
566    @Override
567    public boolean hasProperty(final String relPath) {
568        try {
569            return getNode().hasProperty(relPath);
570        } catch (final RepositoryException e) {
571            throw new RepositoryRuntimeException(e);
572        }
573    }
574
575    @Override
576    public void delete() {
577        try {
578            // Precalculate before node is removed
579            final boolean isMemento = isMemento();
580            final boolean isAcl = isAcl();
581
582            // Remove inbound references to this resource and, recursively, any of its children
583            removeReferences(node);
584
585            final Node parent = getNode().getDepth() > 0 ? getNode().getParent() : null;
586
587            final String name = getNode().getName();
588
589            // This is resolved immediately b/c we delete the node before updating an indirect container's target
590            final boolean shouldUpdateIndirectResource = ldpInsertedContentProperty(node)
591                .flatMap(resourceToProperty(getSession())).filter(this::hasProperty).isPresent();
592
593            final Optional<Node> containingNode = getContainingNode(getNode());
594
595            node.remove();
596
597            if (parent != null) {
598                if (!isMemento && !isAcl) {
599                    createTombstone(parent, name);
600                }
601
602                // also update membershipResources for Direct/Indirect Containers
603                containingNode.filter(UncheckedPredicate.uncheck((final Node ancestor) ->
604                            ancestor.hasProperty(LDP_MEMBER_RESOURCE) && (ancestor.isNodeType(LDP_DIRECT_CONTAINER) ||
605                            shouldUpdateIndirectResource)))
606                    .ifPresent(ancestor -> {
607                        try {
608                            FedoraTypesUtils.touch(ancestor.getProperty(LDP_MEMBER_RESOURCE).getNode());
609                        } catch (final RepositoryException ex) {
610                            throw new RepositoryRuntimeException(ex);
611                        }
612                    });
613
614                // update the lastModified date on the parent node
615                containingNode.ifPresent(ancestor -> {
616                    FedoraTypesUtils.touch(ancestor);
617                });
618            }
619        } catch (final javax.jcr.AccessDeniedException e) {
620            throw new AccessDeniedException(e);
621        } catch (final RepositoryException e) {
622            throw new RepositoryRuntimeException(e);
623        }
624    }
625
626    protected void removeReferences(final Node n) {
627        try {
628            // Remove references to this resource
629            doRemoveReferences(n);
630
631            // Recurse over children of this resource
632            if (n.hasNodes()) {
633                @SuppressWarnings("unchecked")
634                final Iterator<Node> nodes = n.getNodes();
635                nodes.forEachRemaining(this::removeReferences);
636            }
637        } catch (final RepositoryException e) {
638            throw new RepositoryRuntimeException(e);
639        }
640    }
641
642    private void doRemoveReferences(final Node n) throws RepositoryException {
643        @SuppressWarnings("unchecked")
644        final Iterator<Property> references = n.getReferences();
645        @SuppressWarnings("unchecked")
646        final Iterator<Property> weakReferences = n.getWeakReferences();
647        concat(iteratorToStream(references), iteratorToStream(weakReferences)).forEach(prop -> {
648            try {
649                final List<Value> newVals = property2values.apply(prop).filter(
650                        UncheckedPredicate.uncheck(value ->
651                                !n.equals(getSession().getNodeByIdentifier(value.getString()))))
652                        .collect(toList());
653
654                if (newVals.size() == 0) {
655                    prop.remove();
656                } else {
657                    prop.setValue(newVals.toArray(new Value[newVals.size()]));
658                }
659            } catch (final RepositoryException ex) {
660                throw new RepositoryRuntimeException(ex);
661            }
662        });
663    }
664
665    private void createTombstone(final Node parent, final String path) throws RepositoryException {
666        findOrCreateChild(parent, path, FEDORA_TOMBSTONE);
667    }
668
669    /* (non-Javadoc)
670     * @see org.fcrepo.kernel.api.models.FedoraResource#getCreatedDate()
671     */
672    @Override
673    public Instant getCreatedDate() {
674        try {
675            if (hasProperty(FEDORA_CREATED)) {
676                return ofEpochMilli(getTimestamp(FEDORA_CREATED, NO_TIME));
677            }
678            if (hasProperty(JCR_CREATED)) {
679                return ofEpochMilli(getTimestamp(JCR_CREATED, NO_TIME));
680            }
681        } catch (final PathNotFoundException e) {
682            throw new PathNotFoundRuntimeException(e);
683        } catch (final RepositoryException e) {
684            throw new RepositoryRuntimeException(e);
685        }
686        LOGGER.debug("Node {} does not have a createdDate", node);
687        return null;
688    }
689
690    /* (non-Javadoc)
691     * @see org.fcrepo.kernel.api.models.FedoraResource#getLastModifiedDate()
692     */
693
694    /**
695     * This method gets the last modified date for this FedoraResource.  Because
696     * the last modified date is managed by fcrepo (not ModeShape) while the created
697     * date *is* sometimes managed by ModeShape in the current implementation it's
698     * possible that the last modified date will be before the created date.  Instead
699     * of making a second update to correct the modified date, in cases where the modified
700     * date is ealier than the created date, this class presents the created date instead.
701     *
702     * Any method that exposes the last modified date must maintain this illusion so
703     * that that external callers are presented with a sensible and consistent
704     * representation of this resource.
705     * @return the last modified Instant (or the created Instant if it was after the last
706     *         modified date)
707     */
708    @Override
709    public Instant getLastModifiedDate() {
710
711        final Instant createdDate = getCreatedDate();
712        try {
713            final long created = createdDate == null ? NO_TIME : createdDate.toEpochMilli();
714            if (hasProperty(FEDORA_LASTMODIFIED)) {
715                return ofEpochMilli(getTimestamp(FEDORA_LASTMODIFIED, created));
716            } else if (hasProperty(JCR_LASTMODIFIED)) {
717                return ofEpochMilli(getTimestamp(JCR_LASTMODIFIED, created));
718            }
719        } catch (final PathNotFoundException e) {
720            throw new PathNotFoundRuntimeException(e);
721        } catch (final RepositoryException e) {
722            throw new RepositoryRuntimeException(e);
723        }
724        LOGGER.debug("Could not get last modified date property for node {}", node);
725
726        if (createdDate != null) {
727            LOGGER.trace("Using created date for last modified date for node {}", node);
728            return createdDate;
729        }
730
731        return null;
732    }
733
734    private long getTimestamp(final String property, final long created) throws RepositoryException {
735        LOGGER.trace("Using {} date", property);
736        final long timestamp = getProperty(property).getDate().getTimeInMillis();
737        if (timestamp < created && created > NO_TIME) {
738            LOGGER.trace("Returning the later created date ({} > {}) for {}", created, timestamp, property);
739            return created;
740        }
741        return timestamp;
742    }
743
744    @Override
745    public boolean hasType(final String type) {
746        try {
747            if (type.equals(FEDORA_REPOSITORY_ROOT)) {
748                return node.isNodeType(ROOT);
749            }
750            return node.isNodeType(type);
751        } catch (final PathNotFoundException e) {
752            throw new PathNotFoundRuntimeException(e);
753        } catch (final RepositoryException e) {
754            throw new RepositoryRuntimeException(e);
755        }
756    }
757
758    @Override
759    public List<URI> getTypes() {
760        try {
761            final List<NodeType> nodeTypes = new ArrayList<>();
762            final NodeType primaryNodeType = node.getPrimaryNodeType();
763            nodeTypes.add(primaryNodeType);
764            nodeTypes.addAll(asList(primaryNodeType.getSupertypes()));
765            final List<NodeType> mixinTypes = asList(node.getMixinNodeTypes());
766
767            nodeTypes.addAll(mixinTypes);
768            mixinTypes.stream()
769                .map(NodeType::getSupertypes)
770                .flatMap(Arrays::stream)
771                .forEach(nodeTypes::add);
772
773            final List<URI> types = nodeTypes.stream()
774                .map(uncheck(NodeType::getName))
775                .filter(hasInternalNamespace.negate())
776                .distinct()
777                .map(nodeTypeNameToURI)
778                .peek(x -> LOGGER.debug("node has rdf:type {}", x))
779                .collect(Collectors.toList());
780
781            return types;
782
783        } catch (final PathNotFoundException e) {
784            throw new PathNotFoundRuntimeException(e);
785        } catch (final RepositoryException e) {
786            throw new RepositoryRuntimeException(e);
787        }
788    }
789
790    private final Function<String, URI> nodeTypeNameToURI = uncheck(name -> {
791        final String prefix = name.split(":")[0];
792        final String typeName = name.split(":")[1];
793        final String namespace = getSession().getWorkspace().getNamespaceRegistry().getURI(prefix);
794        return URI.create(getRDFNamespaceForJcrNamespace(namespace) + typeName);
795    });
796
797    /* (non-Javadoc)
798     * @see org.fcrepo.kernel.api.models.FedoraResource#updateProperties
799     *     (org.fcrepo.kernel.api.identifiers.IdentifierConverter, java.lang.String, RdfStream)
800     */
801    @Override
802    public void updateProperties(final IdentifierConverter<Resource, FedoraResource> idTranslator,
803                                 final String sparqlUpdateStatement, final RdfStream originalTriples)
804            throws MalformedRdfException, AccessDeniedException {
805
806        final Model model = originalTriples.collect(toModel());
807
808        final FedoraResource described = getDescribedResource();
809        final String describedURI = idTranslator.reverse().convert(described).toString();
810
811        final UpdateRequest request = create(sparqlUpdateStatement, describedURI);
812
813        final Collection<ConstraintViolationException> errors = validateUpdateRequest(request);
814
815        final NamespaceRegistry namespaceRegistry = getNamespaceRegistry(getSession());
816
817        request.getPrefixMapping().getNsPrefixMap().forEach(
818            (k,v) -> {
819                try {
820                    LOGGER.debug("Prefix mapping is key:{} -> value:{}", k, v);
821                    if (Arrays.asList(namespaceRegistry.getPrefixes()).contains(k)
822                        &&  !v.equals(namespaceRegistry.getURI(k))) {
823
824                        final String namespaceURI = namespaceRegistry.getURI(k);
825                        LOGGER.debug("Prefix has already been defined: {}:{}", k, namespaceURI);
826                        throw new InvalidPrefixException("Prefix already exists as: " + k + " -> " + namespaceURI);
827                   }
828
829                } catch (final RepositoryException e) {
830                    throw new RepositoryRuntimeException(e);
831                }
832           });
833
834        throwConstraintErrorsIfPresent(errors);
835
836        checkInteractionModel(request);
837
838        final FilteringJcrPropertyStatementListener listener = new FilteringJcrPropertyStatementListener(
839                idTranslator, getSession(), idTranslator.reverse().convert(described).asNode());
840
841        model.register(listener);
842
843        // If this resource's structural parent is an IndirectContainer, check whether the
844        // ldp:insertedContentRelation property is present in the stream of changed triples.
845        // If so, set the propertyChanged value to true.
846        final AtomicBoolean propertyChanged = new AtomicBoolean();
847        ldpInsertedContentProperty(getNode()).ifPresent(resource -> {
848            model.register(new PropertyChangedListener(resource, propertyChanged));
849        });
850
851        model.setNsPrefixes(request.getPrefixMapping());
852        execute(request, model);
853
854        removeEmptyFragments();
855
856        ensureInteractionModelDefaults(describedURI, model);
857
858        listener.assertNoExceptions();
859
860        try {
861            touch(propertyChanged.get(), listener.getAddedCreatedDate(), listener.getAddedCreatedBy(),
862                    listener.getAddedModifiedDate(), listener.getAddedModifiedBy());
863        } catch (final RepositoryException e) {
864            throw new RuntimeException(e);
865        }
866    }
867
868    private void ensureInteractionModelDefaults(final String uri, final Model model) {
869        final Resource resc = model.getResource(uri);
870        final boolean isIndirect = resc.hasProperty(RDF.type, INDIRECT_CONTAINER);
871        final boolean isDirect = !isIndirect && resc.hasProperty(RDF.type, DIRECT_CONTAINER);
872
873        if (isIndirect || isDirect) {
874            if (!resc.hasProperty(MEMBERSHIP_RESOURCE)) {
875                resc.addProperty(MEMBERSHIP_RESOURCE, resc);
876            }
877            if (!resc.hasProperty(HAS_MEMBER_RELATION)) {
878                resc.addProperty(HAS_MEMBER_RELATION, LDP_MEMBER);
879            }
880        }
881        if (isIndirect) {
882            if (!resc.hasProperty(INSERTED_CONTENT_RELATION)) {
883                resc.addProperty(INSERTED_CONTENT_RELATION, MEMBER_SUBJECT);
884            }
885        }
886    }
887
888    private Optional<String> getResourceInteraction() {
889        return INTERACTION_MODELS.stream().filter(x -> hasType(x)).findFirst();
890    }
891
892    private void checkInteractionModel(final Triple triple, final Optional<String> resourceInteractionModel) {
893        // check for interaction model change violation
894        final String interactionModel = getInteractionModel.apply(triple);
895        if (StringUtils.isNotBlank(interactionModel) &&
896            !interactionModel.equals(resourceInteractionModel.get())) {
897            throw new InteractionModelViolationException("Changing the resource's interaction model from "
898                                                         + resourceInteractionModel.get() + " to " + interactionModel +
899                                                         " is not allowed!");
900        }
901    }
902
903    /*
904     * Check the SPARQLUpdate statements for the invalid interaction model changes.
905     * @param request the UpdateRequest
906     * @throws InteractionModelViolationException when attempting to change the interaction model
907     */
908    private void checkInteractionModel(final UpdateRequest request)  {
909        final List<Quad> deleteQuads = new ArrayList<>();
910        final List<Quad> updateQuads = new ArrayList<>();
911
912        for (final Update operation : request.getOperations()) {
913            if (operation instanceof UpdateModify) {
914                final UpdateModify op = (UpdateModify) operation;
915                deleteQuads.addAll(op.getDeleteQuads());
916                updateQuads.addAll(op.getInsertQuads());
917            } else if (operation instanceof UpdateData) {
918                final UpdateData op = (UpdateData) operation;
919                updateQuads.addAll(op.getQuads());
920            } else if (operation instanceof UpdateDeleteWhere) {
921                final UpdateDeleteWhere op = (UpdateDeleteWhere) operation;
922                deleteQuads.addAll(op.getQuads());
923            }
924
925            final Optional<String> resourceInteractionModel = getResourceInteraction();
926            if (resourceInteractionModel.isPresent()) {
927                updateQuads.forEach(e -> {
928                    // check for interaction model change violation
929                    checkInteractionModel(e.asTriple(), resourceInteractionModel);
930                });
931            }
932
933            deleteQuads.forEach(e -> {
934                final String interactionModel = getInteractionModel.apply(e.asTriple());
935                if (StringUtils.isNotBlank(interactionModel)) {
936                    throw new InteractionModelViolationException("Deleting the interaction model "
937                            + interactionModel + " is not allowed!");
938                }
939            });
940        }
941    }
942
943    /*
944     * Dynamic function to extract the interaction model from Triple.
945     */
946    private static final Function<Triple, String> getInteractionModel =
947            uncheck( x -> {
948                if (x.getPredicate().hasURI(RDF_NAMESPACE + "type") && x.getObject().isURI()
949                        && INTERACTION_MODELS.contains((x.getObject().getURI().replace(LDP_NAMESPACE, "ldp:")))) {
950                return x.getObject().getURI().replace(LDP_NAMESPACE, "ldp:");
951            }
952            return null;
953    });
954
955    @Override
956    public RdfStream getTriples(final IdentifierConverter<Resource, FedoraResource> idTranslator,
957                                final TripleCategory context) {
958        return getTriples(idTranslator, singleton(context));
959    }
960
961    @Override
962    public RdfStream getTriples(final IdentifierConverter<Resource, FedoraResource> idTranslator,
963                                final Set<? extends TripleCategory> contexts) {
964
965        Stream<Triple> triples = contexts.stream()
966                .filter(contextMap::containsKey)
967                .map(x -> contextMap.get(x).apply(this).apply(idTranslator).apply(contexts.contains(MINIMAL)))
968                .reduce(empty(), Stream::concat);
969
970        // if a memento, convert subjects to original resource and object references from referential integrity
971        // ignoring internal URL back the original external URL.
972        if (isMemento()) {
973            final IdentifierConverter<Resource, FedoraResource> internalIdTranslator
974                    = new InternalIdentifierTranslator(getSession());
975            triples = triples.map(convertMementoReferences(idTranslator, internalIdTranslator));
976        }
977
978        return new DefaultRdfStream(idTranslator.reverse().convert(this).asNode(), triples);
979    }
980
981    /* (non-Javadoc)
982     * @see org.fcrepo.kernel.api.models.FedoraResource#isNew()
983     */
984    @Override
985    public Boolean isNew() {
986        return node.isNew();
987    }
988
989    /* (non-Javadoc)
990     * @see org.fcrepo.kernel.api.models.FedoraResource#replaceProperties
991     *     (org.fcrepo.kernel.api.identifiers.IdentifierConverter, org.apache.jena.rdf.model.Model,
992     *     org.fcrepo.kernel.api.RdfStream)
993     */
994    @Override
995    public void replaceProperties(final IdentifierConverter<Resource, FedoraResource> idTranslator,
996        final Model inputModel, final RdfStream originalTriples) throws MalformedRdfException {
997
998        final Resource selfResource = idTranslator.reverse().convert(this);
999        ensureInteractionModelDefaults(selfResource.toString(), inputModel);
1000
1001        // remove any statements that update "relaxed" server-managed triples so they can be updated separately
1002        final List<Statement> filteredStatements = new ArrayList<>();
1003        final StmtIterator it = inputModel.listStatements();
1004        final Optional<String> resourceInteractionModel = getResourceInteraction();
1005        final boolean hasInteractionModel = resourceInteractionModel.isPresent();
1006        while (it.hasNext()) {
1007            final Statement next = it.next();
1008            if (RdfLexicon.isRelaxed.test(next.getPredicate())) {
1009                filteredStatements.add(next);
1010                it.remove();
1011            } else {
1012                if (hasInteractionModel) {
1013                    checkInteractionModel(next.asTriple(), resourceInteractionModel);
1014                }
1015            }
1016        }
1017        // remove any "relaxed" server-managed triples from the existing triples
1018        final RdfStream filteredTriples = new DefaultRdfStream(originalTriples.topic(),
1019                originalTriples.filter(triple -> !isRelaxed.test(createProperty(triple.getPredicate().getURI()))));
1020
1021
1022
1023        try (final RdfStream replacementStream =
1024                new DefaultRdfStream(selfResource.asNode())) {
1025
1026            final GraphDifferencer differencer =
1027                new GraphDifferencer(inputModel, filteredTriples);
1028
1029            final StringBuilder exceptions = new StringBuilder();
1030            try (final DefaultRdfStream diffStream =
1031                    new DefaultRdfStream(replacementStream.topic(), differencer.difference())) {
1032                new RdfRemover(idTranslator, getSession(), diffStream).consume();
1033            } catch (final MalformedRdfException e) {
1034                exceptions.append(e.getMessage());
1035                exceptions.append("\n");
1036            } catch (final ConstraintViolationException e) {
1037                throw e;
1038            }
1039
1040            try (
1041                final DefaultRdfStream notCommonStream =
1042                        new DefaultRdfStream(replacementStream.topic(), differencer.notCommon());
1043                final DefaultRdfStream testStream =
1044                        new DefaultRdfStream(replacementStream.topic(), differencer.notCommon())) {
1045
1046                // do some very basic validation to catch invalid RDF
1047                // this uses the same checks that updateProperties() uses
1048                final Collection<ConstraintViolationException> errors = testStream
1049                        .flatMap(FedoraResourceImpl::validateTriple)
1050                        .filter(x -> x != null)
1051                        .collect(Collectors.toList());
1052
1053                throwConstraintErrorsIfPresent(errors);
1054
1055                new RdfAdder(idTranslator, getSession(), notCommonStream, inputModel.getNsPrefixMap()).consume();
1056            } catch (final MalformedRdfException e) {
1057                exceptions.append(e.getMessage());
1058            } catch (final ConstraintViolationException e) {
1059                throw e;
1060            }
1061
1062            // If this resource's structural parent is an IndirectContainer, check whether the
1063            // ldp:insertedContentRelation property is present in the stream of changed triples.
1064            // If so, set the propertyChanged value to true.
1065            final AtomicBoolean propertyChanged = new AtomicBoolean();
1066            ldpInsertedContentProperty(getNode()).ifPresent(resource -> {
1067                propertyChanged.set(differencer.notCommon().map(Triple::getPredicate).anyMatch(resource::equals));
1068            });
1069
1070            removeEmptyFragments();
1071
1072            if (exceptions.length() > 0) {
1073                throw new MalformedRdfException(exceptions.toString());
1074            }
1075
1076            try {
1077                touch(propertyChanged.get(), RelaxedPropertiesHelper.getCreatedDate(filteredStatements),
1078                        RelaxedPropertiesHelper.getCreatedBy(filteredStatements),
1079                        RelaxedPropertiesHelper.getModifiedDate(filteredStatements),
1080                        RelaxedPropertiesHelper.getModifiedBy(filteredStatements));
1081            } catch (final RepositoryException e) {
1082                throw new RuntimeException(e);
1083            }
1084        }
1085    }
1086
1087    private void throwConstraintErrorsIfPresent(final Collection<ConstraintViolationException> errors) {
1088        if (!errors.isEmpty()) {
1089            if (errors.size() == 1) {
1090                //throw the original constraint error if there
1091                //is only one so that the constraints document that
1092                //is returned to the user is as accurate as possible.
1093                throw errors.stream().findFirst().get();
1094            } else {
1095                throw new ConstraintViolationException(
1096                    errors.stream().map(Exception::getMessage).collect(joining(",\n")));
1097            }
1098        }
1099    }
1100
1101    /**
1102     * Touches a resource to ensure that the implicitly updated properties are updated if
1103     * not explicitly set.
1104     * @param includeMembershipResource true if this touch should propagate through to
1105     *                                  ldp membership resources
1106     * @param createdDate the date to which the created date should be set or null to leave it unchanged
1107     * @param createdUser the user to which the created by should be set or null to leave it unchanged
1108     * @param modifiedDate the date to which the modified date should be set or null to use now
1109     * @param modifyingUser the user making the modification or null to use the current user
1110     * @throws RepositoryException an error occurs while updating the repository
1111     */
1112    @VisibleForTesting
1113    public void touch(final boolean includeMembershipResource, final Calendar createdDate, final String createdUser,
1114                      final Calendar modifiedDate, final String modifyingUser) throws RepositoryException {
1115        FedoraTypesUtils.touch(getNode(), createdDate, createdUser, modifiedDate, modifyingUser);
1116
1117        // If the ldp:insertedContentRelation property was changed, update the
1118        // ldp:membershipResource resource.
1119        if (includeMembershipResource) {
1120            touchLdpMembershipResource(getNode(), modifiedDate, modifyingUser);
1121        }
1122    }
1123
1124    private void removeEmptyFragments() {
1125        try {
1126            if (node.hasNode("#")) {
1127                @SuppressWarnings("unchecked")
1128                final Iterator<Node> nodes = node.getNode("#").getNodes();
1129                nodes.forEachRemaining(n -> {
1130                    try {
1131                        @SuppressWarnings("unchecked")
1132                        final Iterator<Property> properties = n.getProperties();
1133                        final boolean hasUserProps = iteratorToStream(properties).map(propertyConverter::convert)
1134                            .filter(p -> !jcrProperties.contains(p))
1135                            .anyMatch(isManagedPredicate.negate());
1136
1137                        final boolean hasUserTypes = Arrays.stream(n.getMixinNodeTypes())
1138                            .map(uncheck(NodeType::getName)).filter(hasInternalNamespace.negate())
1139                            .map(uncheck(type ->
1140                                getSession().getWorkspace().getNamespaceRegistry().getURI(type.split(":")[0])))
1141                            .anyMatch(isManagedNamespace.negate());
1142
1143                        if (!hasUserProps && !hasUserTypes && !n.getWeakReferences().hasNext() &&
1144                                !n.getReferences().hasNext()) {
1145                            LOGGER.debug("Removing empty hash URI node: {}", n.getName());
1146                            n.remove();
1147                        }
1148                    } catch (final RepositoryException ex) {
1149                        throw new RepositoryRuntimeException("Error removing empty fragments", ex);
1150                    }
1151                });
1152            }
1153        } catch (final RepositoryException ex) {
1154            throw new RepositoryRuntimeException("Error removing empty fragments", ex);
1155        }
1156    }
1157
1158    /* (non-Javadoc)
1159     * @see org.fcrepo.kernel.api.models.FedoraResource#getEtagValue()
1160     */
1161    @Override
1162    public String getEtagValue() {
1163        final Instant lastModifiedDate = getLastModifiedDate();
1164
1165        if (lastModifiedDate != null) {
1166            return sha1Hex(getPath() + lastModifiedDate.toEpochMilli());
1167        }
1168        return "";
1169    }
1170
1171    /* (non-Javadoc)
1172     * @see org.fcrepo.kernel.api.models.FedoraResource#getStateToken()
1173     */
1174    @Override
1175    public String getStateToken() {
1176        return getEtagValue();
1177    }
1178
1179    /**
1180     * Returns a function that converts the subject to the original URI and the object of a triple from an
1181     * undereferenceable internal identifier back to it's original external resource path.
1182     * If the object is not an internal identifier, the object is returned.
1183     *
1184     * @param translator a converter to get the external resource identifier from a path
1185     * @param internalTranslator a converter to get the path from an internal identifier
1186     * @return a function to convert triples
1187     */
1188     protected static Function<Triple, Triple> convertMementoReferences(
1189            final IdentifierConverter<Resource, FedoraResource> translator,
1190            final IdentifierConverter<Resource, FedoraResource> internalTranslator) {
1191
1192         return t -> {
1193            final String subjectURI = t.getSubject().getURI();
1194            // Remove any hash components from the subject while locating the original resource
1195            final String subjectPath;
1196            final int hashIndex = subjectURI.indexOf("#");
1197            if (hashIndex != -1) {
1198                subjectPath = subjectURI.substring(0, hashIndex);
1199            } else {
1200                subjectPath = subjectURI;
1201            }
1202            final Resource subject = createResource(subjectPath);
1203             final FedoraResource subjResc = translator.convert(subject);
1204            org.apache.jena.graph.Node subjectNode =
1205                 translator.reverse().convert(subjResc.getOriginalResource()).asNode();
1206
1207            // Add the hash component back into the subject uri. Note: we cannot convert the memento hash URI
1208            // to the original as a jcr node, as the hash may not exist for the original at this point.
1209            if (hashIndex != -1) {
1210                subjectNode = createURI(subjectNode.getURI() + subjectURI.substring(hashIndex));
1211            }
1212
1213             org.apache.jena.graph.Node objectNode = t.getObject();
1214             if (t.getObject().isURI()) {
1215                 final Resource object = createResource(t.getObject().getURI());
1216                 if (internalTranslator.inDomain(object)) {
1217                     final FedoraResource objResc = internalTranslator.convert(object);
1218                     final Resource newObject = translator.reverse().convert(objResc);
1219                     objectNode = newObject.asNode();
1220                 }
1221             }
1222
1223             return new Triple(subjectNode, t.getPredicate(), objectNode);
1224         };
1225    }
1226
1227    private static Collection<ConstraintViolationException> validateUpdateRequest(final UpdateRequest request) {
1228        return request.getOperations().stream()
1229                .flatMap(x -> {
1230                    if (x instanceof UpdateModify) {
1231                        final UpdateModify y = (UpdateModify) x;
1232                        return concat(y.getInsertQuads().stream(), y.getDeleteQuads().stream());
1233                    } else if (x instanceof UpdateData) {
1234                        return ((UpdateData) x).getQuads().stream();
1235                    } else if (x instanceof UpdateDeleteWhere) {
1236                        return ((UpdateDeleteWhere) x).getQuads().stream();
1237                    } else {
1238                        return empty();
1239                    }
1240                })
1241                .map(x -> x.asTriple())
1242                .flatMap(FedoraResourceImpl::validateTriple)
1243                .filter(x -> x != null)
1244                .collect(Collectors.toList());
1245    }
1246
1247    private static Stream<ConstraintViolationException> validateTriple(final Triple triple) {
1248        return tripleValidators.stream().map(x -> x.apply(triple));
1249    }
1250
1251    @Override
1252    public boolean equals(final Object object) {
1253        if (object instanceof FedoraResourceImpl) {
1254            return ((FedoraResourceImpl) object).getNode().equals(this.getNode());
1255        }
1256        return false;
1257    }
1258
1259    @Override
1260    public int hashCode() {
1261        return getNode().hashCode();
1262    }
1263
1264    protected Session getSession() {
1265        try {
1266            return getNode().getSession();
1267        } catch (final RepositoryException e) {
1268            throw new RepositoryRuntimeException(e);
1269        }
1270    }
1271
1272    @Override
1273    public String toString() {
1274        return getNode().toString();
1275    }
1276
1277    @Override
1278    public void addType(final String type) {
1279        try {
1280            if (node.canAddMixin(type)) {
1281                node.addMixin(type);
1282            }
1283        } catch (final RepositoryException e) {
1284            throw new RepositoryRuntimeException(e);
1285        }
1286    }
1287
1288    protected Property getProperty(final String relPath) {
1289        try {
1290            return getNode().getProperty(relPath);
1291        } catch (final RepositoryException e) {
1292            throw new RepositoryRuntimeException(e);
1293        }
1294    }
1295
1296    /**
1297     * A method that takes a Triple and returns a Triple that is the correct representation of
1298     * that triple for the given resource.  The current implementation of this method is used by
1299     * {@link PropertiesRdfContext} to replace the reported {@link org.fcrepo.kernel.api.RdfLexicon#LAST_MODIFIED_DATE}
1300     * with the one produced by {@link #getLastModifiedDate}.
1301     * @param r the Fedora resource
1302     * @param translator a converter to get the external identifier from a jcr node
1303     * @return a function to convert triples
1304     */
1305    public static Function<Triple, Triple> fixDatesIfNecessary(final FedoraResource r,
1306                                                      final Converter<Node, Resource> translator) {
1307        return t -> {
1308            if (t.getPredicate().toString().equals(LAST_MODIFIED_DATE.toString())
1309                    && t.getSubject().equals(translator.convert(getJcrNode(r)).asNode())) {
1310                final Calendar c = new Calendar.Builder().setInstant(r.getLastModifiedDate().toEpochMilli()).build();
1311                return new Triple(t.getSubject(), t.getPredicate(), createTypedLiteral(c).asNode());
1312            }
1313            return t;
1314        };
1315    }
1316
1317  @Override
1318  public FedoraResource findMementoByDatetime(final Instant mementoDatetime) {
1319      if (isOriginalResource()) {
1320            final FedoraResource timemap = this.getTimeMap();
1321            if (timemap != null) {
1322                final Stream<FedoraResource> mementos = timemap.getChildren();
1323                // Filter to mementos prior to mementoDatetime, then reduce to the nearest one
1324                final Optional<FedoraResource> closest = mementos
1325                        .filter(t -> dateTimeDifference(mementoDatetime, t.getMementoDatetime()) <= 0)
1326                        .reduce((a, b) ->
1327                                dateTimeDifference(a.getMementoDatetime(), mementoDatetime)
1328                                <= dateTimeDifference(b.getMementoDatetime(), mementoDatetime) ?
1329                                        a : b);
1330                if (closest.isPresent()) {
1331                    // Return the closest version older than the requested date.
1332                    return closest.get();
1333                } else {
1334                    // Otherwise you requested before the first version, so return the first version if it exists.
1335                    // If there are no Mementos return null.
1336                    final Optional<FedoraResource> earliest =  timemap.getChildren().min(
1337                            Comparator.comparing(FedoraResource::getMementoDatetime));
1338                    return earliest.orElse(null);
1339                }
1340            }
1341      }
1342      return null;
1343  }
1344
1345    /**
1346     * Calculate the difference between two datetime to the unit.
1347     *
1348     * @param d1 first datetime
1349     * @param d2 second datetime
1350     * @return the difference
1351     */
1352  private static long dateTimeDifference(final Temporal d1, final Temporal d2) {
1353      return ChronoUnit.SECONDS.between(d1, d2);
1354  }
1355
1356}