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