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 java.time.Instant.ofEpochMilli;
021import static java.util.Arrays.asList;
022import static java.util.Collections.singleton;
023import static java.util.stream.Collectors.joining;
024import static java.util.stream.Collectors.toList;
025import static java.util.stream.Stream.concat;
026import static java.util.stream.Stream.empty;
027import static java.util.stream.Stream.of;
028import static org.apache.commons.codec.digest.DigestUtils.sha1Hex;
029import static org.apache.jena.rdf.model.ResourceFactory.createProperty;
030import static org.apache.jena.rdf.model.ResourceFactory.createTypedLiteral;
031import static org.apache.jena.update.UpdateAction.execute;
032import static org.apache.jena.update.UpdateFactory.create;
033import static org.fcrepo.kernel.api.RdfCollectors.toModel;
034import static org.fcrepo.kernel.api.RdfLexicon.LAST_MODIFIED_DATE;
035import static org.fcrepo.kernel.api.RdfLexicon.REPOSITORY_NAMESPACE;
036import static org.fcrepo.kernel.api.RdfLexicon.isManagedNamespace;
037import static org.fcrepo.kernel.api.RdfLexicon.isManagedPredicate;
038import static org.fcrepo.kernel.api.RdfLexicon.isRelaxed;
039import static org.fcrepo.kernel.api.RequiredRdfContext.EMBED_RESOURCES;
040import static org.fcrepo.kernel.api.RequiredRdfContext.INBOUND_REFERENCES;
041import static org.fcrepo.kernel.api.RequiredRdfContext.LDP_CONTAINMENT;
042import static org.fcrepo.kernel.api.RequiredRdfContext.LDP_MEMBERSHIP;
043import static org.fcrepo.kernel.api.RequiredRdfContext.MINIMAL;
044import static org.fcrepo.kernel.api.RequiredRdfContext.PROPERTIES;
045import static org.fcrepo.kernel.api.RequiredRdfContext.SERVER_MANAGED;
046import static org.fcrepo.kernel.api.RequiredRdfContext.VERSIONS;
047import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.FROZEN_MIXIN_TYPES;
048import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.JCR_CREATED;
049import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.JCR_LASTMODIFIED;
050import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.ROOT;
051import static org.fcrepo.kernel.modeshape.RdfJcrLexicon.jcrProperties;
052import static org.fcrepo.kernel.modeshape.identifiers.NodeResourceConverter.nodeConverter;
053import static org.fcrepo.kernel.modeshape.rdf.JcrRdfTools.getRDFNamespaceForJcrNamespace;
054import static org.fcrepo.kernel.modeshape.services.functions.JcrPropertyFunctions.isFrozen;
055import static org.fcrepo.kernel.modeshape.services.functions.JcrPropertyFunctions.property2values;
056import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.getContainingNode;
057import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.getJcrNode;
058import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.hasInternalNamespace;
059import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.isFrozenNode;
060import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.isInternalNode;
061import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.ldpInsertedContentProperty;
062import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.resourceToProperty;
063import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.touchLdpMembershipResource;
064import static org.fcrepo.kernel.modeshape.utils.NamespaceTools.getNamespaceRegistry;
065import static org.fcrepo.kernel.modeshape.utils.StreamUtils.iteratorToStream;
066import static org.fcrepo.kernel.modeshape.utils.UncheckedFunction.uncheck;
067import static org.modeshape.jcr.api.JcrConstants.JCR_CONTENT;
068import static org.slf4j.LoggerFactory.getLogger;
069
070import java.net.URI;
071import java.time.Instant;
072import java.util.ArrayList;
073import java.util.Arrays;
074import java.util.Calendar;
075import java.util.Collection;
076import java.util.Iterator;
077import java.util.List;
078import java.util.Map;
079import java.util.Optional;
080import java.util.Set;
081import java.util.concurrent.atomic.AtomicBoolean;
082import java.util.function.Function;
083import java.util.function.Predicate;
084import java.util.stream.Collectors;
085import java.util.stream.Stream;
086
087import javax.jcr.ItemNotFoundException;
088import javax.jcr.NamespaceRegistry;
089import javax.jcr.Node;
090import javax.jcr.PathNotFoundException;
091import javax.jcr.Property;
092import javax.jcr.RepositoryException;
093import javax.jcr.Session;
094import javax.jcr.Value;
095import javax.jcr.nodetype.NodeType;
096import javax.jcr.version.Version;
097import javax.jcr.version.VersionHistory;
098import javax.jcr.version.VersionManager;
099
100import com.google.common.annotations.VisibleForTesting;
101import org.apache.jena.rdf.model.Statement;
102import org.apache.jena.rdf.model.StmtIterator;
103import org.fcrepo.kernel.api.FedoraTypes;
104import org.fcrepo.kernel.api.FedoraVersion;
105import org.fcrepo.kernel.api.RdfLexicon;
106import org.fcrepo.kernel.api.RdfStream;
107import org.fcrepo.kernel.api.TripleCategory;
108import org.fcrepo.kernel.api.exception.AccessDeniedException;
109import org.fcrepo.kernel.api.exception.ConstraintViolationException;
110import org.fcrepo.kernel.api.exception.InvalidPrefixException;
111import org.fcrepo.kernel.api.exception.MalformedRdfException;
112import org.fcrepo.kernel.api.exception.PathNotFoundRuntimeException;
113import org.fcrepo.kernel.api.exception.RepositoryRuntimeException;
114import org.fcrepo.kernel.api.identifiers.IdentifierConverter;
115import org.fcrepo.kernel.api.models.FedoraResource;
116import org.fcrepo.kernel.api.rdf.DefaultRdfStream;
117import org.fcrepo.kernel.api.utils.GraphDifferencer;
118import org.fcrepo.kernel.api.utils.RelaxedPropertiesHelper;
119import org.fcrepo.kernel.modeshape.rdf.converters.PropertyConverter;
120import org.fcrepo.kernel.modeshape.rdf.impl.AclRdfContext;
121import org.fcrepo.kernel.modeshape.rdf.impl.ChildrenRdfContext;
122import org.fcrepo.kernel.modeshape.rdf.impl.ContentRdfContext;
123import org.fcrepo.kernel.modeshape.rdf.impl.HashRdfContext;
124import org.fcrepo.kernel.modeshape.rdf.impl.LdpContainerRdfContext;
125import org.fcrepo.kernel.modeshape.rdf.impl.LdpIsMemberOfRdfContext;
126import org.fcrepo.kernel.modeshape.rdf.impl.LdpRdfContext;
127import org.fcrepo.kernel.modeshape.rdf.impl.ParentRdfContext;
128import org.fcrepo.kernel.modeshape.rdf.impl.PropertiesRdfContext;
129import org.fcrepo.kernel.modeshape.rdf.impl.ReferencesRdfContext;
130import org.fcrepo.kernel.modeshape.rdf.impl.RootRdfContext;
131import org.fcrepo.kernel.modeshape.rdf.impl.SkolemNodeRdfContext;
132import org.fcrepo.kernel.modeshape.rdf.impl.TypeRdfContext;
133import org.fcrepo.kernel.modeshape.rdf.impl.VersionsRdfContext;
134import org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils;
135import org.fcrepo.kernel.modeshape.utils.FilteringJcrPropertyStatementListener;
136import org.fcrepo.kernel.modeshape.utils.PropertyChangedListener;
137import org.fcrepo.kernel.modeshape.utils.UncheckedPredicate;
138import org.fcrepo.kernel.modeshape.utils.iterators.RdfAdder;
139import org.fcrepo.kernel.modeshape.utils.iterators.RdfRemover;
140
141import org.apache.jena.graph.Triple;
142import org.apache.jena.rdf.model.Model;
143import org.apache.jena.rdf.model.Resource;
144import org.apache.jena.sparql.core.Quad;
145import org.apache.jena.sparql.modify.request.UpdateData;
146import org.apache.jena.sparql.modify.request.UpdateDeleteWhere;
147import org.apache.jena.sparql.modify.request.UpdateModify;
148import org.apache.jena.update.UpdateRequest;
149import org.modeshape.jcr.api.JcrTools;
150import org.slf4j.Logger;
151
152import com.google.common.base.Converter;
153import com.google.common.collect.ImmutableList;
154import com.google.common.collect.ImmutableMap;
155
156/**
157 * Common behaviors across {@link org.fcrepo.kernel.api.models.Container} and
158 * {@link org.fcrepo.kernel.api.models.NonRdfSourceDescription} types; also used
159 * when the exact type of an object is irrelevant
160 *
161 * @author ajs6f
162 */
163public class FedoraResourceImpl extends JcrTools implements FedoraTypes, FedoraResource {
164
165    private static final Logger LOGGER = getLogger(FedoraResourceImpl.class);
166
167    private static final long NO_TIME = 0L;
168    private static final String JCR_CHILD_VERSION_HISTORY = "jcr:childVersionHistory";
169    private static final String JCR_VERSIONABLE_UUID = "jcr:versionableUuid";
170    private static final String JCR_FROZEN_UUID = "jcr:frozenUuid";
171    private static final String JCR_VERSION_STORAGE = "jcr:versionStorage";
172
173    private static final PropertyConverter propertyConverter = new PropertyConverter();
174
175    // A curried type accepting resource, translator, and "minimality", returning triples.
176    private static interface RdfGenerator extends Function<FedoraResource,
177    Function<IdentifierConverter<Resource, FedoraResource>, Function<Boolean, Stream<Triple>>>> {}
178
179    @SuppressWarnings("resource")
180    private static RdfGenerator getDefaultTriples = resource -> translator -> uncheck(minimal -> {
181        final Stream<Stream<Triple>> min = of(
182            new TypeRdfContext(resource, translator),
183            new PropertiesRdfContext(resource, translator));
184        if (!minimal) {
185            final Stream<Stream<Triple>> extra = of(
186                new HashRdfContext(resource, translator),
187                new SkolemNodeRdfContext(resource, translator));
188            return concat(min, extra).reduce(empty(), Stream::concat);
189        }
190        return min.reduce(empty(), Stream::concat);
191    });
192
193    private static RdfGenerator getEmbeddedResourceTriples = resource -> translator -> uncheck(minimal ->
194            resource.getChildren().flatMap(child -> child.getTriples(translator, PROPERTIES)));
195
196    private static RdfGenerator getInboundTriples = resource -> translator -> uncheck(_minimal -> {
197        return new ReferencesRdfContext(resource, translator);
198    });
199
200    private static RdfGenerator getLdpContainsTriples = resource -> translator -> uncheck(_minimal -> {
201        return new ChildrenRdfContext(resource, translator);
202    });
203
204    private static RdfGenerator getVersioningTriples = resource -> translator -> uncheck(_minimal -> {
205        return new VersionsRdfContext(resource, translator);
206    });
207
208    @SuppressWarnings("resource")
209    private static RdfGenerator getServerManagedTriples = resource -> translator -> uncheck(minimal -> {
210        if (minimal) {
211            return new LdpRdfContext(resource, translator);
212        }
213        final Stream<Stream<Triple>> streams = of(
214            new LdpRdfContext(resource, translator),
215            new AclRdfContext(resource, translator),
216            new RootRdfContext(resource, translator),
217            new ContentRdfContext(resource, translator),
218            new ParentRdfContext(resource, translator));
219        return streams.reduce(empty(), Stream::concat);
220    });
221
222    @SuppressWarnings("resource")
223    private static 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    private static final Map<TripleCategory, RdfGenerator> contextMap =
231            ImmutableMap.<TripleCategory, RdfGenerator>builder()
232                    .put(PROPERTIES, getDefaultTriples)
233                    .put(VERSIONS, getVersioningTriples)
234                    .put(EMBED_RESOURCES, getEmbeddedResourceTriples)
235                    .put(INBOUND_REFERENCES, getInboundTriples)
236                    .put(SERVER_MANAGED, getServerManagedTriples)
237                    .put(LDP_MEMBERSHIP, getLdpMembershipTriples)
238                    .put(LDP_CONTAINMENT, getLdpContainsTriples)
239                    .build();
240
241    protected Node node;
242
243    /*
244     * A terminating slash means ModeShape has trouble extracting the localName, e.g., for http://myurl.org/.
245     * 
246     * @see <a href="https://jira.duraspace.org/browse/FCREPO-1409"> FCREPO-1409 </a> for details.
247     */
248    private static final Function<Quad, IllegalArgumentException> validatePredicateEndsWithSlash = uncheck(x -> {
249        if (x.getPredicate().isURI() && x.getPredicate().getURI().endsWith("/")) {
250            return new IllegalArgumentException("Invalid predicate ends with '/': " + x.getPredicate().getURI());
251        }
252        return null;
253    });
254
255    /*
256     * Ensures the object URI is valid
257     */
258    private static final Function<Quad, IllegalArgumentException> validateObjectUrl = uncheck(x -> {
259        if (x.getObject().isURI()) {
260            final String uri = x.getObject().toString();
261            try {
262                new URI(uri);
263            } catch (Exception ex) {
264                return new IllegalArgumentException("Invalid object URI (" + uri + " ) : " + ex.getMessage());
265            }
266        }
267        return null;
268    });
269
270    private static final List<Function<Quad, IllegalArgumentException>> quadValidators =
271            ImmutableList.<Function<Quad, IllegalArgumentException>>builder()
272                    .add(validatePredicateEndsWithSlash)
273                    .add(validateObjectUrl).build();
274
275    /**
276     * Construct a {@link org.fcrepo.kernel.api.models.FedoraResource} from an existing JCR Node
277     * @param node an existing JCR node to treat as an fcrepo object
278     */
279    public FedoraResourceImpl(final Node node) {
280        this.node = node;
281    }
282
283    /**
284     * Return the underlying JCR Node for this resource
285     *
286     * @return the JCR Node
287     */
288    public Node getNode() {
289        return node;
290    }
291
292    /* (non-Javadoc)
293     * @see org.fcrepo.kernel.api.models.FedoraResource#getPath()
294     */
295    @Override
296    public String getPath() {
297        try {
298            final String path = node.getPath();
299            return path.endsWith("/" + JCR_CONTENT) ? path.substring(0, path.length() - JCR_CONTENT.length() - 1)
300                : path;
301        } catch (final RepositoryException e) {
302            throw new RepositoryRuntimeException(e);
303        }
304    }
305
306    /* (non-Javadoc)
307     * @see org.fcrepo.kernel.api.models.FedoraResource#getChildren(Boolean recursive)
308     */
309    @Override
310    public Stream<FedoraResource> getChildren(final Boolean recursive) {
311        try {
312            if (recursive) {
313                return nodeToGoodChildren(node).flatMap(FedoraResourceImpl::getAllChildren);
314            }
315            return nodeToGoodChildren(node);
316        } catch (final RepositoryException e) {
317            throw new RepositoryRuntimeException(e);
318        }
319    }
320
321    /* (non-Javadoc)
322     * @see org.fcrepo.kernel.api.models.FedoraResource#getDescription()
323     */
324    @Override
325    public FedoraResource getDescription() {
326        return this;
327    }
328
329    /* (non-Javadoc)
330     * @see org.fcrepo.kernel.api.models.FedoraResource#getDescribedResource()
331     */
332    @Override
333    public FedoraResource getDescribedResource() {
334        return this;
335    }
336
337    /**
338     * Get the "good" children for a node by skipping all pairtree nodes in the way.
339     * @param input
340     * @return
341     * @throws RepositoryException
342     */
343    @SuppressWarnings("unchecked")
344    private Stream<FedoraResource> nodeToGoodChildren(final Node input) throws RepositoryException {
345        return iteratorToStream(input.getNodes()).filter(nastyChildren.negate())
346            .flatMap(uncheck((final Node child) -> child.isNodeType(FEDORA_PAIRTREE) ? nodeToGoodChildren(child) :
347                        of(nodeToObjectBinaryConverter.convert(child))));
348    }
349
350    /**
351     * Get all children recursively, and flatten into a single Stream.
352     */
353    private static Stream<FedoraResource> getAllChildren(final FedoraResource resource) {
354        return concat(of(resource), resource.getChildren().flatMap(FedoraResourceImpl::getAllChildren));
355    }
356
357    /**
358     * Children for whom we will not generate triples.
359     */
360    private static Predicate<Node> nastyChildren = isInternalNode
361                    .or(TombstoneImpl::hasMixin)
362                    .or(UncheckedPredicate.uncheck(p -> p.getName().equals(JCR_CONTENT)))
363                    .or(UncheckedPredicate.uncheck(p -> p.getName().equals("#")));
364
365    private static final Converter<FedoraResource, FedoraResource> datastreamToBinary
366            = new Converter<FedoraResource, FedoraResource>() {
367
368        @Override
369        protected FedoraResource doForward(final FedoraResource fedoraResource) {
370            return fedoraResource.getDescribedResource();
371        }
372
373        @Override
374        protected FedoraResource doBackward(final FedoraResource fedoraResource) {
375            return fedoraResource.getDescription();
376        }
377    };
378
379    private static final Converter<Node, FedoraResource> nodeToObjectBinaryConverter
380            = nodeConverter.andThen(datastreamToBinary);
381
382    @Override
383    public FedoraResource getContainer() {
384        return getContainingNode(getNode()).map(nodeConverter::convert).orElse(null);
385    }
386
387    @Override
388    public FedoraResource getChild(final String relPath) {
389        try {
390            return nodeConverter.convert(getNode().getNode(relPath));
391        } catch (final RepositoryException e) {
392            throw new RepositoryRuntimeException(e);
393        }
394    }
395
396    @Override
397    public boolean hasProperty(final String relPath) {
398        try {
399            return getNode().hasProperty(relPath);
400        } catch (final RepositoryException e) {
401            throw new RepositoryRuntimeException(e);
402        }
403    }
404
405    @Override
406    public void delete() {
407        try {
408            // Remove inbound references to this resource and, recursively, any of its children
409            removeReferences(node);
410
411            final Node parent = getNode().getDepth() > 0 ? getNode().getParent() : null;
412
413            final String name = getNode().getName();
414
415            // This is resolved immediately b/c we delete the node before updating an indirect container's target
416            final boolean shouldUpdateIndirectResource = ldpInsertedContentProperty(node)
417                .flatMap(resourceToProperty(getSession())).filter(this::hasProperty).isPresent();
418
419            final Optional<Node> containingNode = getContainingNode(getNode());
420
421            node.remove();
422
423            if (parent != null) {
424                createTombstone(parent, name);
425
426                // also update membershipResources for Direct/Indirect Containers
427                containingNode.filter(UncheckedPredicate.uncheck((final Node ancestor) ->
428                            ancestor.hasProperty(LDP_MEMBER_RESOURCE) && (ancestor.isNodeType(LDP_DIRECT_CONTAINER) ||
429                            shouldUpdateIndirectResource)))
430                    .ifPresent(ancestor -> {
431                        try {
432                            FedoraTypesUtils.touch(ancestor.getProperty(LDP_MEMBER_RESOURCE).getNode());
433                        } catch (final RepositoryException ex) {
434                            throw new RepositoryRuntimeException(ex);
435                        }
436                    });
437
438                // update the lastModified date on the parent node
439                containingNode.ifPresent(ancestor -> {
440                    FedoraTypesUtils.touch(ancestor);
441                });
442            }
443        } catch (final javax.jcr.AccessDeniedException e) {
444            throw new AccessDeniedException(e);
445        } catch (final RepositoryException e) {
446            throw new RepositoryRuntimeException(e);
447        }
448    }
449
450    private void removeReferences(final Node n) {
451        try {
452            // Remove references to this resource
453            doRemoveReferences(n);
454
455            // Recurse over children of this resource
456            if (n.hasNodes()) {
457                @SuppressWarnings("unchecked")
458                final Iterator<Node> nodes = n.getNodes();
459                nodes.forEachRemaining(this::removeReferences);
460            }
461        } catch (RepositoryException e) {
462            throw new RepositoryRuntimeException(e);
463        }
464    }
465
466    private void doRemoveReferences(final Node n) throws RepositoryException {
467        @SuppressWarnings("unchecked")
468        final Iterator<Property> references = n.getReferences();
469        @SuppressWarnings("unchecked")
470        final Iterator<Property> weakReferences = n.getWeakReferences();
471        concat(iteratorToStream(references), iteratorToStream(weakReferences)).forEach(prop -> {
472            try {
473                final List<Value> newVals = property2values.apply(prop).filter(
474                        UncheckedPredicate.uncheck(value ->
475                                !n.equals(getSession().getNodeByIdentifier(value.getString()))))
476                        .collect(toList());
477
478                if (newVals.size() == 0) {
479                    prop.remove();
480                } else {
481                    prop.setValue(newVals.toArray(new Value[newVals.size()]));
482                }
483            } catch (final RepositoryException ex) {
484                // Ignore error from trying to update properties on versioned resources
485                if (ex instanceof javax.jcr.nodetype.ConstraintViolationException &&
486                        ex.getMessage().contains(JCR_VERSION_STORAGE)) {
487                    LOGGER.debug("Ignoring exception trying to remove property from versioned resource: {}",
488                            ex.getMessage());
489                } else {
490                    throw new RepositoryRuntimeException(ex);
491                }
492            }
493        });
494    }
495
496    private void createTombstone(final Node parent, final String path) throws RepositoryException {
497        findOrCreateChild(parent, path, FEDORA_TOMBSTONE);
498    }
499
500    /* (non-Javadoc)
501     * @see org.fcrepo.kernel.api.models.FedoraResource#getCreatedDate()
502     */
503    @Override
504    public Instant getCreatedDate() {
505        try {
506            if (hasProperty(FEDORA_CREATED)) {
507                return ofEpochMilli(getTimestamp(FEDORA_CREATED, NO_TIME));
508            }
509            if (hasProperty(JCR_CREATED)) {
510                return ofEpochMilli(getTimestamp(JCR_CREATED, NO_TIME));
511            }
512        } catch (final PathNotFoundException e) {
513            throw new PathNotFoundRuntimeException(e);
514        } catch (final RepositoryException e) {
515            throw new RepositoryRuntimeException(e);
516        }
517        LOGGER.debug("Node {} does not have a createdDate", node);
518        return null;
519    }
520
521    /* (non-Javadoc)
522     * @see org.fcrepo.kernel.api.models.FedoraResource#getLastModifiedDate()
523     */
524
525    /**
526     * This method gets the last modified date for this FedoraResource.  Because
527     * the last modified date is managed by fcrepo (not ModeShape) while the created
528     * date *is* sometimes managed by ModeShape in the current implementation it's
529     * possible that the last modified date will be before the created date.  Instead
530     * of making a second update to correct the modified date, in cases where the modified
531     * date is ealier than the created date, this class presents the created date instead.
532     *
533     * Any method that exposes the last modified date must maintain this illusion so
534     * that that external callers are presented with a sensible and consistent
535     * representation of this resource.
536     * @return the last modified Instant (or the created Instant if it was after the last
537     *         modified date)
538     */
539    @Override
540    public Instant getLastModifiedDate() {
541
542        final Instant createdDate = getCreatedDate();
543        try {
544            final long created = createdDate == null ? NO_TIME : createdDate.toEpochMilli();
545            if (hasProperty(FEDORA_LASTMODIFIED)) {
546                return ofEpochMilli(getTimestamp(FEDORA_LASTMODIFIED, created));
547            } else if (hasProperty(JCR_LASTMODIFIED)) {
548                return ofEpochMilli(getTimestamp(JCR_LASTMODIFIED, created));
549            }
550        } catch (final PathNotFoundException e) {
551            throw new PathNotFoundRuntimeException(e);
552        } catch (final RepositoryException e) {
553            throw new RepositoryRuntimeException(e);
554        }
555        LOGGER.debug("Could not get last modified date property for node {}", node);
556
557        if (createdDate != null) {
558            LOGGER.trace("Using created date for last modified date for node {}", node);
559            return createdDate;
560        }
561
562        return null;
563    }
564
565    private long getTimestamp(final String property, final long created) throws RepositoryException {
566        LOGGER.trace("Using {} date", property);
567        final long timestamp = getProperty(property).getDate().getTimeInMillis();
568        if (timestamp < created && created > NO_TIME) {
569            LOGGER.trace("Returning the later created date ({} > {}) for {}", created, timestamp, property);
570            return created;
571        }
572        return timestamp;
573    }
574
575    @Override
576    public boolean hasType(final String type) {
577        try {
578            if (type.equals(FEDORA_REPOSITORY_ROOT)) {
579                return node.isNodeType(ROOT);
580            } else if (isFrozen.test(node) && hasProperty(FROZEN_MIXIN_TYPES)) {
581                return property2values.apply(getProperty(FROZEN_MIXIN_TYPES)).map(uncheck(Value::getString))
582                    .anyMatch(type::equals);
583            }
584            return node.isNodeType(type);
585        } catch (final PathNotFoundException e) {
586            throw new PathNotFoundRuntimeException(e);
587        } catch (final RepositoryException e) {
588            throw new RepositoryRuntimeException(e);
589        }
590    }
591
592    @Override
593    public List<URI> getTypes() {
594        try {
595            final List<NodeType> nodeTypes = new ArrayList<>();
596            final NodeType primaryNodeType = node.getPrimaryNodeType();
597            nodeTypes.add(primaryNodeType);
598            nodeTypes.addAll(asList(primaryNodeType.getSupertypes()));
599            final List<NodeType> mixinTypes = asList(node.getMixinNodeTypes());
600
601            nodeTypes.addAll(mixinTypes);
602            mixinTypes.stream()
603                .map(NodeType::getSupertypes)
604                .flatMap(Arrays::stream)
605                .forEach(nodeTypes::add);
606
607            final List<URI> types = nodeTypes.stream()
608                .map(uncheck(NodeType::getName))
609                .filter(hasInternalNamespace.negate())
610                .distinct()
611                .map(nodeTypeNameToURI)
612                .peek(x -> LOGGER.debug("node has rdf:type {}", x))
613                .collect(Collectors.toList());
614
615            if (isFrozenResource()) {
616                types.add(URI.create(REPOSITORY_NAMESPACE + "Version"));
617            }
618
619            return types;
620
621        } catch (final PathNotFoundException e) {
622            throw new PathNotFoundRuntimeException(e);
623        } catch (final RepositoryException e) {
624            throw new RepositoryRuntimeException(e);
625        }
626    }
627
628    private final Function<String, URI> nodeTypeNameToURI = uncheck(name -> {
629        final String prefix = name.split(":")[0];
630        final String typeName = name.split(":")[1];
631        final String namespace = getSession().getWorkspace().getNamespaceRegistry().getURI(prefix);
632        return URI.create(getRDFNamespaceForJcrNamespace(namespace) + typeName);
633    });
634
635    /* (non-Javadoc)
636     * @see org.fcrepo.kernel.api.models.FedoraResource#updateProperties
637     *     (org.fcrepo.kernel.api.identifiers.IdentifierConverter, java.lang.String, RdfStream)
638     */
639    @Override
640    public void updateProperties(final IdentifierConverter<Resource, FedoraResource> idTranslator,
641                                 final String sparqlUpdateStatement, final RdfStream originalTriples)
642            throws MalformedRdfException, AccessDeniedException {
643
644        final Model model = originalTriples.collect(toModel());
645
646        final UpdateRequest request = create(sparqlUpdateStatement,
647                idTranslator.reverse().convert(this).toString());
648
649        final Collection<IllegalArgumentException> errors = validateUpdateRequest(request);
650
651        final NamespaceRegistry namespaceRegistry = getNamespaceRegistry(getSession());
652
653        request.getPrefixMapping().getNsPrefixMap().forEach(
654            (k,v) -> {
655                try {
656                    LOGGER.debug("Prefix mapping is key:{} -> value:{}", k, v);
657                    if (Arrays.asList(namespaceRegistry.getPrefixes()).contains(k)
658                        &&  !v.equals(namespaceRegistry.getURI(k))) {
659
660                        final String namespaceURI = namespaceRegistry.getURI(k);
661                        LOGGER.debug("Prefix has already been defined: {}:{}", k, namespaceURI);
662                        throw new InvalidPrefixException("Prefix already exists as: " + k + " -> " + namespaceURI);
663                   }
664
665                } catch (final RepositoryException e) {
666                    throw new RepositoryRuntimeException(e);
667                }
668           });
669
670        if (!errors.isEmpty()) {
671            throw new IllegalArgumentException(errors.stream().map(Exception::getMessage).collect(joining(",\n")));
672        }
673
674        final FilteringJcrPropertyStatementListener listener = new FilteringJcrPropertyStatementListener(
675                idTranslator, getSession(), idTranslator.reverse().convert(this).asNode());
676
677        model.register(listener);
678
679        // If this resource's structural parent is an IndirectContainer, check whether the
680        // ldp:insertedContentRelation property is present in the stream of changed triples.
681        // If so, set the propertyChanged value to true.
682        final AtomicBoolean propertyChanged = new AtomicBoolean();
683        ldpInsertedContentProperty(getNode()).ifPresent(resource -> {
684            model.register(new PropertyChangedListener(resource, propertyChanged));
685        });
686
687        model.setNsPrefixes(request.getPrefixMapping());
688        execute(request, model);
689
690        removeEmptyFragments();
691
692        listener.assertNoExceptions();
693
694        try {
695            touch(propertyChanged.get(), listener.getAddedCreatedDate(), listener.getAddedCreatedBy(),
696                    listener.getAddedModifiedDate(), listener.getAddedModifiedBy());
697        } catch (RepositoryException e) {
698            throw new RuntimeException(e);
699        }
700    }
701
702    @Override
703    public RdfStream getTriples(final IdentifierConverter<Resource, FedoraResource> idTranslator,
704                                final TripleCategory context) {
705        return getTriples(idTranslator, singleton(context));
706    }
707
708    @Override
709    public RdfStream getTriples(final IdentifierConverter<Resource, FedoraResource> idTranslator,
710                                final Set<? extends TripleCategory> contexts) {
711
712        return new DefaultRdfStream(idTranslator.reverse().convert(this).asNode(), contexts.stream()
713                .filter(contextMap::containsKey)
714                .map(x -> contextMap.get(x).apply(this).apply(idTranslator).apply(contexts.contains(MINIMAL)))
715                .reduce(empty(), Stream::concat));
716    }
717
718    /*
719     * (non-Javadoc)
720     * @see org.fcrepo.kernel.api.models.FedoraResource#getBaseVersion()
721     */
722    @Override
723    public FedoraResource getBaseVersion() {
724        try {
725            return new FedoraResourceImpl(getVersionManager().getBaseVersion(getPath()).getFrozenNode());
726        } catch (final RepositoryException e) {
727            throw new RepositoryRuntimeException(e);
728        }
729    }
730
731    /* (non-Javadoc)
732     * @see org.fcrepo.kernel.api.models.FedoraResource#isNew()
733     */
734    @Override
735    public Boolean isNew() {
736        return node.isNew();
737    }
738
739    /* (non-Javadoc)
740     * @see org.fcrepo.kernel.api.models.FedoraResource#replaceProperties
741     *     (org.fcrepo.kernel.api.identifiers.IdentifierConverter, org.apache.jena.rdf.model.Model,
742     *     org.fcrepo.kernel.api.RdfStream)
743     */
744    @Override
745    public void replaceProperties(final IdentifierConverter<Resource, FedoraResource> idTranslator,
746        final Model inputModel, final RdfStream originalTriples) throws MalformedRdfException {
747
748        // remove any statements that update "relaxed" server-managed triples so they can be updated separately
749        final List<Statement> filteredStatements = new ArrayList<>();
750        final StmtIterator it = inputModel.listStatements();
751        while (it.hasNext()) {
752            final Statement next = it.next();
753            if (RdfLexicon.isRelaxed.test(next.getPredicate())) {
754                filteredStatements.add(next);
755                it.remove();
756            }
757        }
758        // remove any "relaxed" server-managed triples from the existing triples
759        final RdfStream filteredTriples = new DefaultRdfStream(originalTriples.topic(),
760                originalTriples.filter(triple -> !isRelaxed.test(createProperty(triple.getPredicate().getURI()))));
761
762
763        try (final RdfStream replacementStream =
764                new DefaultRdfStream(idTranslator.reverse().convert(this).asNode())) {
765
766            final GraphDifferencer differencer =
767                new GraphDifferencer(inputModel, filteredTriples);
768
769            final StringBuilder exceptions = new StringBuilder();
770            try (final DefaultRdfStream diffStream =
771                    new DefaultRdfStream(replacementStream.topic(), differencer.difference())) {
772                new RdfRemover(idTranslator, getSession(), diffStream).consume();
773            } catch (final ConstraintViolationException e) {
774                throw e;
775            } catch (final MalformedRdfException e) {
776                exceptions.append(e.getMessage());
777                exceptions.append("\n");
778            }
779
780            try (final DefaultRdfStream notCommonStream =
781                    new DefaultRdfStream(replacementStream.topic(), differencer.notCommon())) {
782                new RdfAdder(idTranslator, getSession(), notCommonStream, inputModel.getNsPrefixMap()).consume();
783            } catch (final ConstraintViolationException e) {
784                throw e;
785            } catch (final MalformedRdfException e) {
786                exceptions.append(e.getMessage());
787            }
788
789            // If this resource's structural parent is an IndirectContainer, check whether the
790            // ldp:insertedContentRelation property is present in the stream of changed triples.
791            // If so, set the propertyChanged value to true.
792            final AtomicBoolean propertyChanged = new AtomicBoolean();
793            ldpInsertedContentProperty(getNode()).ifPresent(resource -> {
794                propertyChanged.set(differencer.notCommon().map(Triple::getPredicate).anyMatch(resource::equals));
795            });
796
797            removeEmptyFragments();
798
799            if (exceptions.length() > 0) {
800                throw new MalformedRdfException(exceptions.toString());
801            }
802
803            try {
804                touch(propertyChanged.get(), RelaxedPropertiesHelper.getCreatedDate(filteredStatements),
805                        RelaxedPropertiesHelper.getCreatedBy(filteredStatements),
806                        RelaxedPropertiesHelper.getModifiedDate(filteredStatements),
807                        RelaxedPropertiesHelper.getModifiedBy(filteredStatements));
808            } catch (RepositoryException e) {
809                throw new RuntimeException(e);
810            }
811        }
812    }
813
814    /**
815     * Touches a resource to ensure that the implicitly updated properties are updated if
816     * not explicitly set.
817     * @param includeMembershipResource true if this touch should propagate through to
818     *                                  ldp membership resources
819     * @param createdDate the date to which the created date should be set or null to leave it unchanged
820     * @param createdUser the user to which the created by should be set or null to leave it unchanged
821     * @param modifiedDate the date to which the modified date should be set or null to use now
822     * @param modifyingUser the user making the modification or null to use the current user
823     * @throws RepositoryException an error occurs while updating the repository
824     */
825    @VisibleForTesting
826    public void touch(final boolean includeMembershipResource, final Calendar createdDate, final String createdUser,
827                      final Calendar modifiedDate, final String modifyingUser) throws RepositoryException {
828        FedoraTypesUtils.touch(getNode(), createdDate, createdUser, modifiedDate, modifyingUser);
829
830        // If the ldp:insertedContentRelation property was changed, update the
831        // ldp:membershipResource resource.
832        if (includeMembershipResource) {
833            touchLdpMembershipResource(getNode(), modifiedDate, modifyingUser);
834        }
835    }
836
837    private void removeEmptyFragments() {
838        try {
839            if (node.hasNode("#")) {
840                @SuppressWarnings("unchecked")
841                final Iterator<Node> nodes = node.getNode("#").getNodes();
842                nodes.forEachRemaining(n -> {
843                    try {
844                        @SuppressWarnings("unchecked")
845                        final Iterator<Property> properties = n.getProperties();
846                        final boolean hasUserProps = iteratorToStream(properties).map(propertyConverter::convert)
847                            .filter(p -> !jcrProperties.contains(p))
848                            .anyMatch(isManagedPredicate.negate());
849
850                        final boolean hasUserTypes = Arrays.stream(n.getMixinNodeTypes())
851                            .map(uncheck(NodeType::getName)).filter(hasInternalNamespace.negate())
852                            .map(uncheck(type ->
853                                getSession().getWorkspace().getNamespaceRegistry().getURI(type.split(":")[0])))
854                            .anyMatch(isManagedNamespace.negate());
855
856                        if (!hasUserProps && !hasUserTypes && !n.getWeakReferences().hasNext() &&
857                                !n.getReferences().hasNext()) {
858                            LOGGER.debug("Removing empty hash URI node: {}", n.getName());
859                            n.remove();
860                        }
861                    } catch (final RepositoryException ex) {
862                        throw new RepositoryRuntimeException("Error removing empty fragments", ex);
863                    }
864                });
865            }
866        } catch (final RepositoryException ex) {
867            throw new RepositoryRuntimeException("Error removing empty fragments", ex);
868        }
869    }
870
871    /* (non-Javadoc)
872     * @see org.fcrepo.kernel.api.models.FedoraResource#getEtagValue()
873     */
874    @Override
875    public String getEtagValue() {
876        final Instant lastModifiedDate = getLastModifiedDate();
877
878        if (lastModifiedDate != null) {
879            return sha1Hex(getPath() + lastModifiedDate.toEpochMilli());
880        }
881        return "";
882    }
883
884    @Override
885    public void enableVersioning() {
886        try {
887            node.addMixin("mix:versionable");
888        } catch (final RepositoryException e) {
889            throw new RepositoryRuntimeException(e);
890        }
891    }
892
893    @Override
894    public void disableVersioning() {
895        try {
896            node.removeMixin("mix:versionable");
897        } catch (final RepositoryException e) {
898            throw new RepositoryRuntimeException(e);
899        }
900
901    }
902
903    @Override
904    public boolean isVersioned() {
905        try {
906            return node.isNodeType("mix:versionable");
907        } catch (final RepositoryException e) {
908            throw new RepositoryRuntimeException(e);
909        }
910    }
911
912    @Override
913    public boolean isFrozenResource() {
914        return isFrozenNode.test(this);
915    }
916
917    @Override
918    public FedoraResource getVersionedAncestor() {
919
920        try {
921            if (!isFrozenResource()) {
922                return null;
923            }
924
925            Node versionableFrozenNode = getNode();
926            FedoraResource unfrozenResource = getUnfrozenResource();
927
928            // traverse the frozen tree looking for a node whose unfrozen equivalent is versioned
929            while (!unfrozenResource.isVersioned()) {
930
931                if (versionableFrozenNode.getDepth() == 0) {
932                    return null;
933                }
934
935                // node in the frozen tree
936                versionableFrozenNode = versionableFrozenNode.getParent();
937
938                // unfrozen equivalent
939                unfrozenResource = new FedoraResourceImpl(versionableFrozenNode).getUnfrozenResource();
940            }
941
942            return new FedoraResourceImpl(versionableFrozenNode);
943        } catch (final RepositoryException e) {
944            throw new RepositoryRuntimeException(e);
945        }
946
947    }
948
949    @Override
950    public FedoraResource getUnfrozenResource() {
951        if (!isFrozenResource()) {
952            return this;
953        }
954
955        try {
956            // Either this resource is frozen
957            if (hasProperty(JCR_FROZEN_UUID)) {
958                try {
959                    return new FedoraResourceImpl(getNodeByProperty(getProperty(JCR_FROZEN_UUID)));
960                } catch (final ItemNotFoundException e) {
961                    // The unfrozen resource has been deleted, return the tombstone.
962                    return new TombstoneImpl(getNode());
963                }
964
965                // ..Or it is a child-version-history on a frozen path
966            } else if (hasProperty(JCR_CHILD_VERSION_HISTORY)) {
967                final Node childVersionHistory = getNodeByProperty(getProperty(JCR_CHILD_VERSION_HISTORY));
968                try {
969                    final Node childNode = getNodeByProperty(childVersionHistory.getProperty(JCR_VERSIONABLE_UUID));
970                    return new FedoraResourceImpl(childNode);
971                } catch (final ItemNotFoundException e) {
972                    // The unfrozen resource has been deleted, return the tombstone.
973                    return new TombstoneImpl(childVersionHistory);
974                }
975
976            } else {
977                throw new RepositoryRuntimeException("Resource must be frozen or a child-history!");
978            }
979        } catch (final RepositoryException e) {
980            throw new RepositoryRuntimeException(e);
981        }
982    }
983
984    @Override
985    public FedoraResource getVersion(final String label) {
986        try {
987            final Node n = getFrozenNode(label);
988
989            if (n != null) {
990                return new FedoraResourceImpl(n);
991            }
992
993            if (isVersioned()) {
994                final VersionHistory hist = getVersionManager().getVersionHistory(getPath());
995
996                if (hist.hasVersionLabel(label)) {
997                    LOGGER.debug("Found version for {} by label {}.", this, label);
998                    return new FedoraResourceImpl(hist.getVersionByLabel(label).getFrozenNode());
999                }
1000            }
1001
1002            LOGGER.warn("Unknown version {} with label {}!", getPath(), label);
1003            return null;
1004        } catch (final RepositoryException e) {
1005            throw new RepositoryRuntimeException(e);
1006        }
1007
1008    }
1009
1010    @Override
1011    public Stream<FedoraVersion> getVersions() {
1012        try {
1013            final VersionHistory history = getVersionManager().getVersionHistory(getPath());
1014            @SuppressWarnings("unchecked")
1015            final Iterator<Version> versions = history.getAllVersions();
1016            return iteratorToStream(versions)
1017                /* discard jcr:rootVersion */
1018                .filter(UncheckedPredicate.uncheck(version ->
1019                            !version.getName().equals(history.getRootVersion().getName())))
1020                /* omit unlabelled versions */
1021                .filter(UncheckedPredicate.uncheck(version -> {
1022                    final String[] labels = history.getVersionLabels(version);
1023                    if (labels.length == 0) {
1024                        LOGGER.warn("An unlabelled version for {} was found! Omitting from version listing!",
1025                                getPath());
1026                    } else if (labels.length > 1) {
1027                        LOGGER.warn("Multiple version labels found for {}! Using first label, \"{}\".", getPath(),
1028                                labels[0]);
1029                    }
1030                    return labels.length > 0;
1031                }))
1032                .map(uncheck(version ->
1033                        new FedoraVersionImpl(history.getVersionLabels(version)[0], version.getCreated().toInstant())));
1034        } catch (final RepositoryException ex) {
1035            throw new RepositoryRuntimeException(ex);
1036        }
1037    }
1038
1039    @Override
1040    public String getVersionLabelOfFrozenResource() {
1041        if (!isFrozenResource()) {
1042            return null;
1043        }
1044
1045        // Frozen node is required to find associated version label
1046        final Node frozenResource;
1047        try {
1048            // Version History associated with this resource
1049            final VersionHistory history = getVersionManager().getVersionHistory(getUnfrozenResource().getPath());
1050
1051            // Possibly the frozen node is nested inside of current child-version-history
1052            if (getNode().hasProperty(JCR_CHILD_VERSION_HISTORY)) {
1053                final Node childVersionHistory = getNodeByProperty(getProperty(JCR_CHILD_VERSION_HISTORY));
1054                final Node childNode = getNodeByProperty(childVersionHistory.getProperty(JCR_VERSIONABLE_UUID));
1055                final Version childVersion = getVersionManager().getBaseVersion(childNode.getPath());
1056                frozenResource = childVersion.getFrozenNode();
1057
1058            } else {
1059                frozenResource = getNode();
1060            }
1061
1062            // Loop versions
1063            @SuppressWarnings("unchecked")
1064            final Stream<Version> versions = iteratorToStream(history.getAllVersions());
1065            return versions
1066                .filter(UncheckedPredicate.uncheck(version -> version.getFrozenNode().equals(frozenResource)))
1067                .map(uncheck(history::getVersionLabels))
1068                .flatMap(Arrays::stream)
1069                .findFirst().orElse(null);
1070        } catch (final RepositoryException e) {
1071            throw new RepositoryRuntimeException(e);
1072        }
1073    }
1074
1075    private Node getNodeByProperty(final Property property) throws RepositoryException {
1076        return getSession().getNodeByIdentifier(property.getString());
1077    }
1078
1079    protected VersionManager getVersionManager() {
1080        try {
1081            return getSession().getWorkspace().getVersionManager();
1082        } catch (final RepositoryException e) {
1083            throw new RepositoryRuntimeException(e);
1084        }
1085    }
1086
1087    private static Collection<IllegalArgumentException> validateUpdateRequest(final UpdateRequest request) {
1088        return request.getOperations().stream()
1089                .flatMap(x -> {
1090                    if (x instanceof UpdateModify) {
1091                        final UpdateModify y = (UpdateModify) x;
1092                        return concat(y.getInsertQuads().stream(), y.getDeleteQuads().stream());
1093                    } else if (x instanceof UpdateData) {
1094                        return ((UpdateData) x).getQuads().stream();
1095                    } else if (x instanceof UpdateDeleteWhere) {
1096                        return ((UpdateDeleteWhere) x).getQuads().stream();
1097                    } else {
1098                        return empty();
1099                    }
1100                })
1101                .flatMap(FedoraResourceImpl::validateQuad)
1102                .filter(x -> x != null)
1103                .collect(Collectors.toList());
1104    }
1105
1106    private static Stream<IllegalArgumentException> validateQuad(final Quad quad) {
1107        return quadValidators.stream().map(x -> x.apply(quad));
1108    }
1109
1110    private Node getFrozenNode(final String label) throws RepositoryException {
1111        try {
1112            final Session session = getSession();
1113
1114            final Node frozenNode = session.getNodeByIdentifier(label);
1115
1116            final String baseUUID = getNode().getIdentifier();
1117
1118            /*
1119             * We found a node whose identifier is the "label" for the version.  Now
1120             * we must do due dilligence to make sure it's a frozen node representing
1121             * a version of the subject node.
1122             */
1123            final Property p = frozenNode.getProperty(JCR_FROZEN_UUID);
1124            if (p != null) {
1125                if (p.getString().equals(baseUUID)) {
1126                    return frozenNode;
1127                }
1128            }
1129            /*
1130             * Though a node with an id of the label was found, it wasn't the
1131             * node we were looking for, so fall through and look for a labeled
1132             * node.
1133             */
1134        } catch (final ItemNotFoundException ex) {
1135            /*
1136             * the label wasn't a uuid of a frozen node but
1137             * instead possibly a version label.
1138             */
1139        }
1140        return null;
1141    }
1142
1143    @Override
1144    public boolean equals(final Object object) {
1145        if (object instanceof FedoraResourceImpl) {
1146            return ((FedoraResourceImpl) object).getNode().equals(this.getNode());
1147        }
1148        return false;
1149    }
1150
1151    @Override
1152    public int hashCode() {
1153        return getNode().hashCode();
1154    }
1155
1156    protected Session getSession() {
1157        try {
1158            return getNode().getSession();
1159        } catch (final RepositoryException e) {
1160            throw new RepositoryRuntimeException(e);
1161        }
1162    }
1163
1164    @Override
1165    public String toString() {
1166        return getNode().toString();
1167    }
1168
1169    protected Property getProperty(final String relPath) {
1170        try {
1171            return getNode().getProperty(relPath);
1172        } catch (final RepositoryException e) {
1173            throw new RepositoryRuntimeException(e);
1174        }
1175    }
1176
1177    /**
1178     * A method that takes a Triple and returns a Triple that is the correct representation of
1179     * that triple for the given resource.  The current implementation of this method is used by
1180     * {@link PropertiesRdfContext} to replace the reported {@link org.fcrepo.kernel.api.RdfLexicon#LAST_MODIFIED_DATE}
1181     * with the one produced by {@link #getLastModifiedDate}.
1182     * @param r the Fedora resource
1183     * @param translator a converter to get the external identifier from a jcr node
1184     * @return a function to convert triples
1185     */
1186    public static Function<Triple, Triple> fixDatesIfNecessary(final FedoraResource r,
1187                                                      final Converter<Node, Resource> translator) {
1188        return t -> {
1189            if (t.getPredicate().toString().equals(LAST_MODIFIED_DATE.toString())
1190                    && t.getSubject().equals(translator.convert(getJcrNode(r)).asNode())) {
1191                final Calendar c = new Calendar.Builder().setInstant(r.getLastModifiedDate().toEpochMilli()).build();
1192                return new Triple(t.getSubject(), t.getPredicate(), createTypedLiteral(c).asNode());
1193            }
1194            return t;
1195        };
1196    }
1197
1198}