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