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