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