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