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).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}