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