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