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.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 org.apache.jena.rdf.model.Resource; 099import org.apache.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 org.apache.jena.rdf.model.Model; 140import org.apache.jena.sparql.modify.request.UpdateData; 141import org.apache.jena.sparql.modify.request.UpdateDeleteWhere; 142import org.apache.jena.sparql.modify.request.UpdateModify; 143import org.apache.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 @SuppressWarnings("unchecked") 366 final Iterator<Property> references = node.getReferences(); 367 @SuppressWarnings("unchecked") 368 final Iterator<Property> weakReferences = node.getWeakReferences(); 369 concat(iteratorToStream(references), iteratorToStream(weakReferences)).forEach(prop -> { 370 try { 371 final List<Value> newVals = property2values.apply(prop).filter( 372 UncheckedPredicate.uncheck(value -> 373 !node.equals(getSession().getNodeByIdentifier(value.getString())))) 374 .collect(toList()); 375 376 if (newVals.size() == 0) { 377 prop.remove(); 378 } else { 379 prop.setValue(newVals.toArray(new Value[newVals.size()])); 380 } 381 } catch (final RepositoryException ex) { 382 // Ignore error from trying to update properties on versioned resources 383 if (ex instanceof javax.jcr.nodetype.ConstraintViolationException && 384 ex.getMessage().contains(JCR_VERSION_STORAGE)) { 385 LOGGER.debug("Ignoring exception trying to remove property from versioned resource: {}", 386 ex.getMessage()); 387 } else { 388 throw new RepositoryRuntimeException(ex); 389 } 390 } 391 }); 392 393 final Node parent = getNode().getDepth() > 0 ? getNode().getParent() : null; 394 395 final String name = getNode().getName(); 396 397 // This is resolved immediately b/c we delete the node before updating an indirect container's target 398 final boolean shouldUpdateIndirectResource = ldpInsertedContentProperty(node) 399 .flatMap(resourceToProperty(getSession())).filter(this::hasProperty).isPresent(); 400 401 final Optional<Node> containingNode = getContainingNode(getNode()); 402 403 node.remove(); 404 405 if (parent != null) { 406 createTombstone(parent, name); 407 408 // also update membershipResources for Direct/Indirect Containers 409 containingNode.filter(UncheckedPredicate.uncheck((final Node ancestor) -> 410 ancestor.hasProperty(LDP_MEMBER_RESOURCE) && (ancestor.isNodeType(LDP_DIRECT_CONTAINER) || 411 shouldUpdateIndirectResource))) 412 .ifPresent(ancestor -> { 413 try { 414 FedoraTypesUtils.touch(ancestor.getProperty(LDP_MEMBER_RESOURCE).getNode()); 415 } catch (final RepositoryException ex) { 416 throw new RepositoryRuntimeException(ex); 417 } 418 }); 419 420 // update the lastModified date on the parent node 421 containingNode.ifPresent(ancestor -> { 422 FedoraTypesUtils.touch(ancestor); 423 }); 424 } 425 } catch (final javax.jcr.AccessDeniedException e) { 426 throw new AccessDeniedException(e); 427 } catch (final RepositoryException e) { 428 throw new RepositoryRuntimeException(e); 429 } 430 } 431 432 private void createTombstone(final Node parent, final String path) throws RepositoryException { 433 findOrCreateChild(parent, path, FEDORA_TOMBSTONE); 434 } 435 436 /* (non-Javadoc) 437 * @see org.fcrepo.kernel.api.models.FedoraResource#getCreatedDate() 438 */ 439 @Override 440 public Date getCreatedDate() { 441 try { 442 if (hasProperty(JCR_CREATED)) { 443 return new Date(getTimestamp(JCR_CREATED, NO_TIME)); 444 } 445 } catch (final PathNotFoundException e) { 446 throw new PathNotFoundRuntimeException(e); 447 } catch (final RepositoryException e) { 448 throw new RepositoryRuntimeException(e); 449 } 450 LOGGER.debug("Node {} does not have a createdDate", node); 451 return null; 452 } 453 454 /* (non-Javadoc) 455 * @see org.fcrepo.kernel.api.models.FedoraResource#getLastModifiedDate() 456 */ 457 458 /** 459 * This method gets the last modified date for this FedoraResource. Because 460 * the last modified date is managed by fcrepo (not ModeShape) while the created 461 * date *is* managed by ModeShape in the current implementation it's possible that 462 * the last modified date will be before the created date. Instead of making 463 * a second update to correct the modified date, in cases where the modified 464 * date is ealier than the created date, this class presents the created date instead. 465 * 466 * Any method that exposes the last modified date must maintain this illusion so 467 * that that external callers are presented with a sensible and consistent 468 * representation of this resource. 469 * @return the last modified Date (or the created date if it was after the last 470 * modified date) 471 */ 472 @Override 473 public Date getLastModifiedDate() { 474 475 final Date createdDate = getCreatedDate(); 476 try { 477 final long created = createdDate == null ? NO_TIME : createdDate.getTime(); 478 if (hasProperty(FEDORA_LASTMODIFIED)) { 479 return new Date(getTimestamp(FEDORA_LASTMODIFIED, created)); 480 } else if (hasProperty(JCR_LASTMODIFIED)) { 481 return new Date(getTimestamp(JCR_LASTMODIFIED, created)); 482 } 483 } catch (final PathNotFoundException e) { 484 throw new PathNotFoundRuntimeException(e); 485 } catch (final RepositoryException e) { 486 throw new RepositoryRuntimeException(e); 487 } 488 LOGGER.debug("Could not get last modified date property for node {}", node); 489 490 if (createdDate != null) { 491 LOGGER.trace("Using created date for last modified date for node {}", node); 492 return createdDate; 493 } 494 495 return null; 496 } 497 498 private long getTimestamp(final String property, final long created) throws RepositoryException { 499 LOGGER.trace("Using {} date", property); 500 final long timestamp = getProperty(property).getDate().getTimeInMillis(); 501 if (timestamp < created && created > NO_TIME) { 502 LOGGER.trace("Returning the later created date ({} > {}) for {}", created, timestamp, property); 503 return created; 504 } 505 return timestamp; 506 } 507 508 /** 509 * Set the last-modified date to the current date. 510 */ 511 public void touch() { 512 FedoraTypesUtils.touch(getNode()); 513 } 514 515 @Override 516 public boolean hasType(final String type) { 517 try { 518 if (type.equals(FEDORA_REPOSITORY_ROOT)) { 519 return node.isNodeType(ROOT); 520 } else if (isFrozen.test(node) && hasProperty(FROZEN_MIXIN_TYPES)) { 521 return property2values.apply(getProperty(FROZEN_MIXIN_TYPES)).map(uncheck(Value::getString)) 522 .anyMatch(type::equals); 523 } 524 return node.isNodeType(type); 525 } catch (final PathNotFoundException e) { 526 throw new PathNotFoundRuntimeException(e); 527 } catch (final RepositoryException e) { 528 throw new RepositoryRuntimeException(e); 529 } 530 } 531 532 @Override 533 public List<URI> getTypes() { 534 try { 535 final List<NodeType> nodeTypes = new ArrayList<>(); 536 final NodeType primaryNodeType = node.getPrimaryNodeType(); 537 nodeTypes.add(primaryNodeType); 538 nodeTypes.addAll(asList(primaryNodeType.getSupertypes())); 539 final List<NodeType> mixinTypes = asList(node.getMixinNodeTypes()); 540 541 nodeTypes.addAll(mixinTypes); 542 mixinTypes.stream() 543 .map(NodeType::getSupertypes) 544 .flatMap(Arrays::stream) 545 .forEach(nodeTypes::add); 546 547 final List<URI> types = nodeTypes.stream() 548 .map(uncheck(NodeType::getName)) 549 .filter(hasInternalNamespace.negate()) 550 .distinct() 551 .map(nodeTypeNameToURI) 552 .peek(x -> LOGGER.debug("node has rdf:type {}", x)) 553 .collect(Collectors.toList()); 554 555 if (isFrozenResource()) { 556 types.add(URI.create(REPOSITORY_NAMESPACE + "Version")); 557 } 558 559 return types; 560 561 } catch (final PathNotFoundException e) { 562 throw new PathNotFoundRuntimeException(e); 563 } catch (final RepositoryException e) { 564 throw new RepositoryRuntimeException(e); 565 } 566 } 567 568 private final Function<String, URI> nodeTypeNameToURI = uncheck(name -> { 569 final String prefix = name.split(":")[0]; 570 final String typeName = name.split(":")[1]; 571 final String namespace = getSession().getWorkspace().getNamespaceRegistry().getURI(prefix); 572 return URI.create(getRDFNamespaceForJcrNamespace(namespace) + typeName); 573 }); 574 575 /* (non-Javadoc) 576 * @see org.fcrepo.kernel.api.models.FedoraResource#updateProperties 577 * (org.fcrepo.kernel.api.identifiers.IdentifierConverter, java.lang.String, RdfStream) 578 */ 579 @Override 580 public void updateProperties(final IdentifierConverter<Resource, FedoraResource> idTranslator, 581 final String sparqlUpdateStatement, final RdfStream originalTriples) 582 throws MalformedRdfException, AccessDeniedException { 583 584 final Model model = originalTriples.collect(toModel()); 585 586 final UpdateRequest request = create(sparqlUpdateStatement, 587 idTranslator.reverse().convert(this).toString()); 588 589 final Collection<IllegalArgumentException> errors = checkInvalidPredicates(request); 590 591 final NamespaceRegistry namespaceRegistry = getNamespaceRegistry(getSession()); 592 593 request.getPrefixMapping().getNsPrefixMap().forEach( 594 (k,v) -> { 595 try { 596 LOGGER.debug("Prefix mapping is key:{} -> value:{}", k, v); 597 if (Arrays.asList(namespaceRegistry.getPrefixes()).contains(k) 598 && !v.equals(namespaceRegistry.getURI(k))) { 599 600 final String namespaceURI = namespaceRegistry.getURI(k); 601 LOGGER.debug("Prefix has already been defined: {}:{}", k, namespaceURI); 602 throw new InvalidPrefixException("Prefix already exists as: " + k + " -> " + namespaceURI); 603 } 604 605 } catch (final RepositoryException e) { 606 throw new RepositoryRuntimeException(e); 607 } 608 }); 609 610 if (!errors.isEmpty()) { 611 throw new IllegalArgumentException(errors.stream().map(Exception::getMessage).collect(joining(",\n"))); 612 } 613 614 final JcrPropertyStatementListener listener = new JcrPropertyStatementListener( 615 idTranslator, getSession(), idTranslator.reverse().convert(this).asNode()); 616 617 model.register(listener); 618 619 // If this resource's structural parent is an IndirectContainer, check whether the 620 // ldp:insertedContentRelation property is present in the stream of changed triples. 621 // If so, set the propertyChanged value to true. 622 final AtomicBoolean propertyChanged = new AtomicBoolean(); 623 ldpInsertedContentProperty(getNode()).ifPresent(resource -> { 624 model.register(new PropertyChangedListener(resource, propertyChanged)); 625 }); 626 627 model.setNsPrefixes(request.getPrefixMapping()); 628 execute(request, model); 629 630 removeEmptyFragments(); 631 632 listener.assertNoExceptions(); 633 634 // Update the fedora:lastModified property 635 touch(); 636 637 // Update the fedora:lastModified property of the ldp:memberResource 638 // resource, if necessary. 639 if (propertyChanged.get()) { 640 touchLdpMembershipResource(getNode()); 641 } 642 } 643 644 @Override 645 public RdfStream getTriples(final IdentifierConverter<Resource, FedoraResource> idTranslator, 646 final TripleCategory context) { 647 return getTriples(idTranslator, singleton(context)); 648 } 649 650 @Override 651 public RdfStream getTriples(final IdentifierConverter<Resource, FedoraResource> idTranslator, 652 final Set<? extends TripleCategory> contexts) { 653 654 return new DefaultRdfStream(idTranslator.reverse().convert(this).asNode(), contexts.stream() 655 .filter(contextMap::containsKey) 656 .map(x -> contextMap.get(x).apply(this).apply(idTranslator).apply(contexts.contains(MINIMAL))) 657 .reduce(empty(), Stream::concat)); 658 } 659 660 /* 661 * (non-Javadoc) 662 * @see org.fcrepo.kernel.api.models.FedoraResource#getBaseVersion() 663 */ 664 @Override 665 public Version getBaseVersion() { 666 try { 667 return getVersionManager().getBaseVersion(getPath()); 668 } catch (final RepositoryException e) { 669 throw new RepositoryRuntimeException(e); 670 } 671 } 672 673 /* 674 * (non-Javadoc) 675 * @see org.fcrepo.kernel.api.models.FedoraResource#getVersionHistory() 676 */ 677 @Override 678 public VersionHistory getVersionHistory() { 679 try { 680 return getVersionManager().getVersionHistory(getPath()); 681 } catch (final RepositoryException e) { 682 throw new RepositoryRuntimeException(e); 683 } 684 } 685 686 /* (non-Javadoc) 687 * @see org.fcrepo.kernel.api.models.FedoraResource#isNew() 688 */ 689 @Override 690 public Boolean isNew() { 691 return node.isNew(); 692 } 693 694 /* (non-Javadoc) 695 * @see org.fcrepo.kernel.api.models.FedoraResource#replaceProperties 696 * (org.fcrepo.kernel.api.identifiers.IdentifierConverter, org.apache.jena.rdf.model.Model) 697 */ 698 @Override 699 public void replaceProperties(final IdentifierConverter<Resource, FedoraResource> idTranslator, 700 final Model inputModel, final RdfStream originalTriples) throws MalformedRdfException { 701 702 try (final RdfStream replacementStream = 703 new DefaultRdfStream(idTranslator.reverse().convert(this).asNode())) { 704 705 final GraphDifferencer differencer = 706 new GraphDifferencer(inputModel, originalTriples); 707 708 final StringBuilder exceptions = new StringBuilder(); 709 try (final DefaultRdfStream diffStream = 710 new DefaultRdfStream(replacementStream.topic(), differencer.difference())) { 711 new RdfRemover(idTranslator, getSession(), diffStream).consume(); 712 } catch (final ConstraintViolationException e) { 713 throw e; 714 } catch (final MalformedRdfException e) { 715 exceptions.append(e.getMessage()); 716 exceptions.append("\n"); 717 } 718 719 try (final DefaultRdfStream notCommonStream = 720 new DefaultRdfStream(replacementStream.topic(), differencer.notCommon())) { 721 new RdfAdder(idTranslator, getSession(), notCommonStream).consume(); 722 } catch (final ConstraintViolationException e) { 723 throw e; 724 } catch (final MalformedRdfException e) { 725 exceptions.append(e.getMessage()); 726 } 727 728 // If this resource's structural parent is an IndirectContainer, check whether the 729 // ldp:insertedContentRelation property is present in the stream of changed triples. 730 // If so, set the propertyChanged value to true. 731 final AtomicBoolean propertyChanged = new AtomicBoolean(); 732 ldpInsertedContentProperty(getNode()).ifPresent(resource -> { 733 propertyChanged.set(differencer.notCommon().map(Triple::getPredicate).anyMatch(resource::equals)); 734 }); 735 736 removeEmptyFragments(); 737 738 if (exceptions.length() > 0) { 739 throw new MalformedRdfException(exceptions.toString()); 740 } 741 742 // Update the fedora:lastModified property 743 touch(); 744 745 // If the ldp:insertedContentRelation property was changed, update the 746 // ldp:membershipResource resource. 747 if (propertyChanged.get()) { 748 touchLdpMembershipResource(getNode()); 749 } 750 } 751 } 752 753 private void removeEmptyFragments() { 754 try { 755 if (node.hasNode("#")) { 756 @SuppressWarnings("unchecked") 757 final Iterator<Node> nodes = node.getNode("#").getNodes(); 758 nodes.forEachRemaining(n -> { 759 try { 760 @SuppressWarnings("unchecked") 761 final Iterator<Property> properties = n.getProperties(); 762 final boolean hasUserProps = iteratorToStream(properties).map(propertyConverter::convert) 763 .anyMatch(isManagedPredicate.negate()); 764 765 final boolean hasUserTypes = Arrays.stream(n.getMixinNodeTypes()) 766 .map(uncheck(NodeType::getName)).filter(hasInternalNamespace.negate()) 767 .map(uncheck(type -> 768 getSession().getWorkspace().getNamespaceRegistry().getURI(type.split(":")[0]))) 769 .anyMatch(isManagedNamespace.negate()); 770 771 if (!hasUserProps && !hasUserTypes && !n.getWeakReferences().hasNext() && 772 !n.getReferences().hasNext()) { 773 LOGGER.debug("Removing empty hash URI node: {}", n.getName()); 774 n.remove(); 775 } 776 } catch (final RepositoryException ex) { 777 throw new RepositoryRuntimeException("Error removing empty fragments", ex); 778 } 779 }); 780 } 781 } catch (final RepositoryException ex) { 782 throw new RepositoryRuntimeException("Error removing empty fragments", ex); 783 } 784 } 785 786 /* (non-Javadoc) 787 * @see org.fcrepo.kernel.api.models.FedoraResource#getEtagValue() 788 */ 789 @Override 790 public String getEtagValue() { 791 final Date lastModifiedDate = getLastModifiedDate(); 792 793 if (lastModifiedDate != null) { 794 return shaHex(getPath() + lastModifiedDate.getTime()); 795 } 796 return ""; 797 } 798 799 @Override 800 public void enableVersioning() { 801 try { 802 node.addMixin("mix:versionable"); 803 } catch (final RepositoryException e) { 804 throw new RepositoryRuntimeException(e); 805 } 806 } 807 808 @Override 809 public void disableVersioning() { 810 try { 811 node.removeMixin("mix:versionable"); 812 } catch (final RepositoryException e) { 813 throw new RepositoryRuntimeException(e); 814 } 815 816 } 817 818 @Override 819 public boolean isVersioned() { 820 try { 821 return node.isNodeType("mix:versionable"); 822 } catch (final RepositoryException e) { 823 throw new RepositoryRuntimeException(e); 824 } 825 } 826 827 @Override 828 public boolean isFrozenResource() { 829 return isFrozenNode.test(this); 830 } 831 832 @Override 833 public FedoraResource getVersionedAncestor() { 834 835 try { 836 if (!isFrozenResource()) { 837 return null; 838 } 839 840 Node versionableFrozenNode = getNode(); 841 FedoraResource unfrozenResource = getUnfrozenResource(); 842 843 // traverse the frozen tree looking for a node whose unfrozen equivalent is versioned 844 while (!unfrozenResource.isVersioned()) { 845 846 if (versionableFrozenNode.getDepth() == 0) { 847 return null; 848 } 849 850 // node in the frozen tree 851 versionableFrozenNode = versionableFrozenNode.getParent(); 852 853 // unfrozen equivalent 854 unfrozenResource = new FedoraResourceImpl(versionableFrozenNode).getUnfrozenResource(); 855 } 856 857 return new FedoraResourceImpl(versionableFrozenNode); 858 } catch (final RepositoryException e) { 859 throw new RepositoryRuntimeException(e); 860 } 861 862 } 863 864 @Override 865 public FedoraResource getUnfrozenResource() { 866 if (!isFrozenResource()) { 867 return this; 868 } 869 870 try { 871 // Either this resource is frozen 872 if (hasProperty(JCR_FROZEN_UUID)) { 873 try { 874 return new FedoraResourceImpl(getNodeByProperty(getProperty(JCR_FROZEN_UUID))); 875 } catch (final ItemNotFoundException e) { 876 // The unfrozen resource has been deleted, return the tombstone. 877 return new TombstoneImpl(getNode()); 878 } 879 880 // ..Or it is a child-version-history on a frozen path 881 } else if (hasProperty(JCR_CHILD_VERSION_HISTORY)) { 882 final Node childVersionHistory = getNodeByProperty(getProperty(JCR_CHILD_VERSION_HISTORY)); 883 try { 884 final Node childNode = getNodeByProperty(childVersionHistory.getProperty(JCR_VERSIONABLE_UUID)); 885 return new FedoraResourceImpl(childNode); 886 } catch (final ItemNotFoundException e) { 887 // The unfrozen resource has been deleted, return the tombstone. 888 return new TombstoneImpl(childVersionHistory); 889 } 890 891 } else { 892 throw new RepositoryRuntimeException("Resource must be frozen or a child-history!"); 893 } 894 } catch (final RepositoryException e) { 895 throw new RepositoryRuntimeException(e); 896 } 897 } 898 899 @Override 900 public FedoraResource getVersion(final String label) { 901 try { 902 final Node n = getFrozenNode(label); 903 904 if (n != null) { 905 return new FedoraResourceImpl(n); 906 } 907 908 if (isVersioned()) { 909 final VersionHistory hist = getVersionManager().getVersionHistory(getPath()); 910 911 if (hist.hasVersionLabel(label)) { 912 LOGGER.debug("Found version for {} by label {}.", this, label); 913 return new FedoraResourceImpl(hist.getVersionByLabel(label).getFrozenNode()); 914 } 915 } 916 917 LOGGER.warn("Unknown version {} with label {}!", getPath(), label); 918 return null; 919 } catch (final RepositoryException e) { 920 throw new RepositoryRuntimeException(e); 921 } 922 923 } 924 925 @Override 926 public String getVersionLabelOfFrozenResource() { 927 if (!isFrozenResource()) { 928 return null; 929 } 930 931 // Version History associated with this resource 932 final VersionHistory versionHistory = getUnfrozenResource().getVersionHistory(); 933 934 // Frozen node is required to find associated version label 935 final Node frozenResource; 936 try { 937 // Possibly the frozen node is nested inside of current child-version-history 938 if (getNode().hasProperty(JCR_CHILD_VERSION_HISTORY)) { 939 final Node childVersionHistory = getNodeByProperty(getProperty(JCR_CHILD_VERSION_HISTORY)); 940 final Node childNode = getNodeByProperty(childVersionHistory.getProperty(JCR_VERSIONABLE_UUID)); 941 final Version childVersion = getVersionManager().getBaseVersion(childNode.getPath()); 942 frozenResource = childVersion.getFrozenNode(); 943 944 } else { 945 frozenResource = getNode(); 946 } 947 948 // Loop versions 949 @SuppressWarnings("unchecked") 950 final Stream<Version> versions = iteratorToStream(versionHistory.getAllVersions()); 951 return versions 952 .filter(UncheckedPredicate.uncheck(version -> version.getFrozenNode().equals(frozenResource))) 953 .map(uncheck(versionHistory::getVersionLabels)) 954 .flatMap(Arrays::stream) 955 .findFirst().orElse(null); 956 } catch (final RepositoryException e) { 957 throw new RepositoryRuntimeException(e); 958 } 959 } 960 961 private Node getNodeByProperty(final Property property) throws RepositoryException { 962 return getSession().getNodeByIdentifier(property.getString()); 963 } 964 965 protected VersionManager getVersionManager() { 966 try { 967 return getSession().getWorkspace().getVersionManager(); 968 } catch (final RepositoryException e) { 969 throw new RepositoryRuntimeException(e); 970 } 971 } 972 973 /** 974 * Helps ensure that there are no terminating slashes in the predicate. 975 * A terminating slash means ModeShape has trouble extracting the localName, e.g., for 976 * http://myurl.org/. 977 * 978 * @see <a href="https://jira.duraspace.org/browse/FCREPO-1409"> FCREPO-1409 </a> for details. 979 */ 980 private static Collection<IllegalArgumentException> checkInvalidPredicates(final UpdateRequest request) { 981 return request.getOperations().stream() 982 .flatMap(x -> { 983 if (x instanceof UpdateModify) { 984 final UpdateModify y = (UpdateModify)x; 985 return concat(y.getInsertQuads().stream(), y.getDeleteQuads().stream()); 986 } else if (x instanceof UpdateData) { 987 return ((UpdateData)x).getQuads().stream(); 988 } else if (x instanceof UpdateDeleteWhere) { 989 return ((UpdateDeleteWhere)x).getQuads().stream(); 990 } else { 991 return empty(); 992 } 993 }) 994 .filter(x -> x.getPredicate().isURI() && x.getPredicate().getURI().endsWith("/")) 995 .map(x -> new IllegalArgumentException("Invalid predicate ends with '/': " + x.getPredicate().getURI())) 996 .collect(Collectors.toList()); 997 } 998 999 private Node getFrozenNode(final String label) throws RepositoryException { 1000 try { 1001 final Session session = getSession(); 1002 1003 final Node frozenNode = session.getNodeByIdentifier(label); 1004 1005 final String baseUUID = getNode().getIdentifier(); 1006 1007 /* 1008 * We found a node whose identifier is the "label" for the version. Now 1009 * we must do due dilligence to make sure it's a frozen node representing 1010 * a version of the subject node. 1011 */ 1012 final Property p = frozenNode.getProperty(JCR_FROZEN_UUID); 1013 if (p != null) { 1014 if (p.getString().equals(baseUUID)) { 1015 return frozenNode; 1016 } 1017 } 1018 /* 1019 * Though a node with an id of the label was found, it wasn't the 1020 * node we were looking for, so fall through and look for a labeled 1021 * node. 1022 */ 1023 } catch (final ItemNotFoundException ex) { 1024 /* 1025 * the label wasn't a uuid of a frozen node but 1026 * instead possibly a version label. 1027 */ 1028 } 1029 return null; 1030 } 1031 1032 @Override 1033 public boolean equals(final Object object) { 1034 if (object instanceof FedoraResourceImpl) { 1035 return ((FedoraResourceImpl) object).getNode().equals(this.getNode()); 1036 } 1037 return false; 1038 } 1039 1040 @Override 1041 public int hashCode() { 1042 return getNode().hashCode(); 1043 } 1044 1045 protected Session getSession() { 1046 try { 1047 return getNode().getSession(); 1048 } catch (final RepositoryException e) { 1049 throw new RepositoryRuntimeException(e); 1050 } 1051 } 1052 1053 @Override 1054 public String toString() { 1055 return getNode().toString(); 1056 } 1057 1058 protected Property getProperty(final String relPath) { 1059 try { 1060 return getNode().getProperty(relPath); 1061 } catch (final RepositoryException e) { 1062 throw new RepositoryRuntimeException(e); 1063 } 1064 } 1065 1066 /** 1067 * A method that takes a Triple and returns a Triple that is the correct representation of 1068 * that triple for the given resource. The current implementation of this method is used by 1069 * {@link PropertiesRdfContext} to replace the reported {@link org.fcrepo.kernel.api.RdfLexicon#LAST_MODIFIED_DATE} 1070 * with the one produced by {@link #getLastModifiedDate}. 1071 * @param r the Fedora resource 1072 * @param translator a converter to get the external identifier from a jcr node 1073 * @return a function to convert triples 1074 */ 1075 public static Function<Triple, Triple> fixDatesIfNecessary(final FedoraResource r, 1076 final Converter<Node, Resource> translator) { 1077 return t -> { 1078 if (t.getPredicate().toString().equals(LAST_MODIFIED_DATE.toString()) 1079 && t.getSubject().equals(translator.convert(getJcrNode(r)).asNode())) { 1080 final Calendar c = Calendar.getInstance(); 1081 c.setTime(r.getLastModifiedDate()); 1082 return new Triple(t.getSubject(), t.getPredicate(), createTypedLiteral(c).asNode()); 1083 } 1084 return t; 1085 }; 1086 } 1087 1088}