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