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