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.impl; 017 018import static com.google.common.base.Predicates.not; 019import static com.google.common.base.Throwables.propagate; 020import static com.google.common.collect.Iterators.concat; 021import static com.google.common.collect.Iterators.filter; 022import static com.google.common.collect.Iterators.singletonIterator; 023import static com.google.common.collect.Iterators.transform; 024import static com.google.common.collect.Lists.newArrayList; 025import static com.hp.hpl.jena.update.UpdateAction.execute; 026import static com.hp.hpl.jena.update.UpdateFactory.create; 027import static org.apache.commons.codec.digest.DigestUtils.shaHex; 028import static org.fcrepo.kernel.impl.identifiers.NodeResourceConverter.nodeConverter; 029import static org.fcrepo.kernel.impl.utils.FedoraTypesUtils.isFrozenNode; 030import static org.fcrepo.kernel.impl.utils.FedoraTypesUtils.isInternalNode; 031import static org.fcrepo.kernel.services.functions.JcrPropertyFunctions.isFrozen; 032import static org.fcrepo.kernel.services.functions.JcrPropertyFunctions.property2values; 033import static org.fcrepo.kernel.services.functions.JcrPropertyFunctions.value2string; 034import static org.modeshape.jcr.api.JcrConstants.JCR_CONTENT; 035import static org.slf4j.LoggerFactory.getLogger; 036 037import java.lang.reflect.Constructor; 038import java.lang.reflect.InvocationTargetException; 039import java.util.ArrayList; 040import java.util.Collections; 041import java.util.Date; 042import java.util.Iterator; 043import java.util.List; 044 045import javax.jcr.AccessDeniedException; 046import javax.jcr.ItemNotFoundException; 047import javax.jcr.Node; 048import javax.jcr.PathNotFoundException; 049import javax.jcr.Property; 050import javax.jcr.RepositoryException; 051import javax.jcr.Session; 052import javax.jcr.Value; 053import javax.jcr.version.Version; 054import javax.jcr.version.VersionHistory; 055 056import com.google.common.base.Converter; 057import com.google.common.base.Function; 058import com.google.common.base.Predicate; 059import com.google.common.collect.Iterators; 060import com.hp.hpl.jena.rdf.model.Resource; 061 062import org.fcrepo.kernel.FedoraJcrTypes; 063import org.fcrepo.kernel.models.NonRdfSourceDescription; 064import org.fcrepo.kernel.models.FedoraBinary; 065import org.fcrepo.kernel.models.FedoraResource; 066import org.fcrepo.kernel.exception.MalformedRdfException; 067import org.fcrepo.kernel.exception.PathNotFoundRuntimeException; 068import org.fcrepo.kernel.exception.RepositoryRuntimeException; 069import org.fcrepo.kernel.identifiers.IdentifierConverter; 070import org.fcrepo.kernel.impl.utils.JcrPropertyStatementListener; 071import org.fcrepo.kernel.utils.iterators.GraphDifferencingIterator; 072import org.fcrepo.kernel.impl.utils.iterators.RdfAdder; 073import org.fcrepo.kernel.impl.utils.iterators.RdfRemover; 074import org.fcrepo.kernel.utils.iterators.RdfStream; 075 076import org.modeshape.jcr.api.JcrTools; 077import org.slf4j.Logger; 078 079import com.hp.hpl.jena.rdf.model.Model; 080import com.hp.hpl.jena.update.UpdateRequest; 081 082/** 083 * Common behaviors across {@link org.fcrepo.kernel.models.Container} and 084 * {@link org.fcrepo.kernel.models.NonRdfSourceDescription} types; also used 085 * when the exact type of an object is irrelevant 086 * 087 * @author ajs6f 088 */ 089public class FedoraResourceImpl extends JcrTools implements FedoraJcrTypes, FedoraResource { 090 091 private static final Logger LOGGER = getLogger(FedoraResourceImpl.class); 092 093 protected Node node; 094 095 /** 096 * Construct a {@link org.fcrepo.kernel.models.FedoraResource} from an existing JCR Node 097 * @param node an existing JCR node to treat as an fcrepo object 098 */ 099 public FedoraResourceImpl(final Node node) { 100 this.node = node; 101 } 102 103 /* (non-Javadoc) 104 * @see org.fcrepo.kernel.models.FedoraResource#getNode() 105 */ 106 @Override 107 public Node getNode() { 108 return node; 109 } 110 111 /* (non-Javadoc) 112 * @see org.fcrepo.kernel.models.FedoraResource#getPath() 113 */ 114 @Override 115 public String getPath() { 116 try { 117 return node.getPath(); 118 } catch (final RepositoryException e) { 119 throw new RepositoryRuntimeException(e); 120 } 121 } 122 123 /* (non-Javadoc) 124 * @see org.fcrepo.kernel.models.FedoraResource#getChildren() 125 */ 126 @Override 127 public Iterator<FedoraResource> getChildren() { 128 try { 129 return concat(nodeToGoodChildren(node)); 130 } catch (final RepositoryException e) { 131 throw new RepositoryRuntimeException(e); 132 } 133 } 134 135 /** 136 * Get the "good" children for a node by skipping all pairtree nodes in the way. 137 * @param input 138 * @return 139 * @throws RepositoryException 140 */ 141 private Iterator<Iterator<FedoraResource>> nodeToGoodChildren(final Node input) throws RepositoryException { 142 final Iterator<Node> allChildren = input.getNodes(); 143 final Iterator<Node> children = filter(allChildren, not(nastyChildren)); 144 return transform(children, new Function<Node, Iterator<FedoraResource>>() { 145 146 @Override 147 public Iterator<FedoraResource> apply(final Node input) { 148 try { 149 if (input.isNodeType(FEDORA_PAIRTREE)) { 150 return concat(nodeToGoodChildren(input)); 151 } 152 return singletonIterator(nodeToObjectBinaryConverter.convert(input)); 153 } catch (final RepositoryException e) { 154 throw new RepositoryRuntimeException(e); 155 } 156 } 157 }); 158 } 159 /** 160 * Children for whom we will not generate triples. 161 */ 162 private static Predicate<Node> nastyChildren = 163 new Predicate<Node>() { 164 165 @Override 166 public boolean apply(final Node n) { 167 LOGGER.trace("Testing child node {}", n); 168 try { 169 return isInternalNode.apply(n) 170 || n.getName().equals(JCR_CONTENT) 171 || TombstoneImpl.hasMixin(n) 172 || n.getName().equals("#"); 173 } catch (final RepositoryException e) { 174 throw new RepositoryRuntimeException(e); 175 } 176 } 177 }; 178 179 180 private static final Converter<FedoraResource, FedoraResource> datastreamToBinary 181 = new Converter<FedoraResource, FedoraResource>() { 182 183 @Override 184 protected FedoraResource doForward(final FedoraResource fedoraResource) { 185 if (fedoraResource instanceof NonRdfSourceDescription) { 186 return ((NonRdfSourceDescription) fedoraResource).getDescribedResource(); 187 } 188 return fedoraResource; 189 } 190 191 @Override 192 protected FedoraResource doBackward(final FedoraResource fedoraResource) { 193 if (fedoraResource instanceof FedoraBinary) { 194 return ((FedoraBinary) fedoraResource).getDescription(); 195 } 196 return fedoraResource; 197 } 198 }; 199 200 private static final Converter<Node, FedoraResource> nodeToObjectBinaryConverter 201 = nodeConverter.andThen(datastreamToBinary); 202 203 @Override 204 public FedoraResource getContainer() { 205 try { 206 207 if (getNode().getDepth() == 0) { 208 return null; 209 } 210 211 Node container = getNode().getParent(); 212 while (container.getDepth() > 0) { 213 if (container.isNodeType(FEDORA_PAIRTREE) 214 || container.isNodeType(FEDORA_NON_RDF_SOURCE_DESCRIPTION)) { 215 container = container.getParent(); 216 } else { 217 return nodeConverter.convert(container); 218 } 219 } 220 221 return nodeConverter.convert(container); 222 } catch (final RepositoryException e) { 223 throw new RepositoryRuntimeException(e); 224 } 225 } 226 227 @Override 228 public FedoraResource getChild(final String relPath) { 229 try { 230 return nodeConverter.convert(getNode().getNode(relPath)); 231 } catch (final RepositoryException e) { 232 throw new RepositoryRuntimeException(e); 233 } 234 } 235 236 @Override 237 public boolean hasProperty(final String relPath) { 238 try { 239 return getNode().hasProperty(relPath); 240 } catch (final RepositoryException e) { 241 throw new RepositoryRuntimeException(e); 242 } 243 } 244 245 @Override 246 public Property getProperty(final String relPath) { 247 try { 248 return getNode().getProperty(relPath); 249 } catch (final RepositoryException e) { 250 throw new RepositoryRuntimeException(e); 251 } 252 } 253 254 @Override 255 public void delete() { 256 try { 257 final Iterator<Property> references = node.getReferences(); 258 final Iterator<Property> weakReferences = node.getWeakReferences(); 259 final Iterator<Property> inboundProperties = Iterators.concat(references, weakReferences); 260 261 while (inboundProperties.hasNext()) { 262 final Property prop = inboundProperties.next(); 263 final List<Value> newVals = new ArrayList<>(); 264 final Iterator<Value> propIt = property2values.apply(prop); 265 while (propIt.hasNext()) { 266 final Value v = propIt.next(); 267 if (!node.equals(getSession().getNodeByIdentifier(v.getString()))) { 268 newVals.add(v); 269 LOGGER.trace("Keeping multivalue reference property when deleting node"); 270 } 271 } 272 if (newVals.size() == 0) { 273 prop.remove(); 274 } else { 275 prop.setValue(newVals.toArray(new Value[newVals.size()])); 276 } 277 } 278 279 final Node parent; 280 281 if (getNode().getDepth() > 0) { 282 parent = getNode().getParent(); 283 } else { 284 parent = null; 285 } 286 final String name = getNode().getName(); 287 288 node.remove(); 289 290 if (parent != null) { 291 createTombstone(parent, name); 292 } 293 294 } catch (final RepositoryException e) { 295 throw new RepositoryRuntimeException(e); 296 } 297 } 298 299 private void createTombstone(final Node parent, final String path) throws RepositoryException { 300 findOrCreateChild(parent, path, FEDORA_TOMBSTONE); 301 } 302 303 /* (non-Javadoc) 304 * @see org.fcrepo.kernel.models.FedoraResource#getCreatedDate() 305 */ 306 @Override 307 public Date getCreatedDate() { 308 try { 309 if (hasProperty(JCR_CREATED)) { 310 return new Date(getProperty(JCR_CREATED).getDate().getTimeInMillis()); 311 } 312 } catch (final PathNotFoundException e) { 313 throw new PathNotFoundRuntimeException(e); 314 } catch (final RepositoryException e) { 315 throw new RepositoryRuntimeException(e); 316 } 317 LOGGER.debug("Node {} does not have a createdDate", node); 318 return null; 319 } 320 321 /* (non-Javadoc) 322 * @see org.fcrepo.kernel.models.FedoraResource#getLastModifiedDate() 323 */ 324 @Override 325 public Date getLastModifiedDate() { 326 327 try { 328 if (hasProperty(JCR_LASTMODIFIED)) { 329 return new Date(getProperty(JCR_LASTMODIFIED).getDate().getTimeInMillis()); 330 } 331 } catch (final PathNotFoundException e) { 332 throw new PathNotFoundRuntimeException(e); 333 } catch (final RepositoryException e) { 334 throw new RepositoryRuntimeException(e); 335 } 336 LOGGER.debug("Could not get last modified date property for node {}", node); 337 338 final Date createdDate = getCreatedDate(); 339 if (createdDate != null) { 340 LOGGER.trace("Using created date for last modified date for node {}", node); 341 return createdDate; 342 } 343 344 return null; 345 } 346 347 348 @Override 349 public boolean hasType(final String type) { 350 try { 351 if (isFrozen.apply(node) && hasProperty(FROZEN_MIXIN_TYPES)) { 352 final List<String> types = newArrayList( 353 transform(property2values.apply(getProperty(FROZEN_MIXIN_TYPES)), value2string) 354 ); 355 return types.contains(type); 356 } 357 return node.isNodeType(type); 358 } catch (final PathNotFoundException e) { 359 throw new PathNotFoundRuntimeException(e); 360 } catch (final RepositoryException e) { 361 throw new RepositoryRuntimeException(e); 362 } 363 } 364 365 /* (non-Javadoc) 366 * @see org.fcrepo.kernel.models.FedoraResource#updateProperties 367 * (org.fcrepo.kernel.identifiers.IdentifierConverter, java.lang.String, RdfStream) 368 */ 369 @Override 370 public void updateProperties(final IdentifierConverter<Resource, FedoraResource> idTranslator, 371 final String sparqlUpdateStatement, final RdfStream originalTriples) 372 throws MalformedRdfException, AccessDeniedException { 373 374 final Model model = originalTriples.asModel(); 375 376 final JcrPropertyStatementListener listener = 377 new JcrPropertyStatementListener(idTranslator, getSession()); 378 379 model.register(listener); 380 381 final UpdateRequest request = create(sparqlUpdateStatement, 382 idTranslator.reverse().convert(this).toString()); 383 model.setNsPrefixes(request.getPrefixMapping()); 384 execute(request, model); 385 386 listener.assertNoExceptions(); 387 } 388 389 @Override 390 public RdfStream getTriples(final IdentifierConverter<Resource, FedoraResource> idTranslator, 391 final Class<? extends RdfStream> context) { 392 return getTriples(idTranslator, Collections.singleton(context)); 393 } 394 395 @Override 396 public RdfStream getTriples(final IdentifierConverter<Resource, FedoraResource> idTranslator, 397 final Iterable<? extends Class<? extends RdfStream>> contexts) { 398 final RdfStream stream = new RdfStream(); 399 400 for (final Class<? extends RdfStream> context : contexts) { 401 try { 402 final Constructor<? extends RdfStream> declaredConstructor 403 = context.getDeclaredConstructor(FedoraResource.class, IdentifierConverter.class); 404 405 final RdfStream rdfStream = declaredConstructor.newInstance(this, idTranslator); 406 rdfStream.session(getSession()); 407 408 stream.concat(rdfStream); 409 } catch (final NoSuchMethodException | 410 InstantiationException | 411 IllegalAccessException e) { 412 // Shouldn't happen. 413 throw propagate(e); 414 } catch (final InvocationTargetException e) { 415 final Throwable cause = e.getCause(); 416 if (cause instanceof RepositoryException) { 417 throw new RepositoryRuntimeException(cause); 418 } 419 throw propagate(cause); 420 } 421 } 422 423 return stream; 424 } 425 426 /* 427 * (non-Javadoc) 428 * @see org.fcrepo.kernel.models.FedoraResource#getBaseVersion() 429 */ 430 @Override 431 public Version getBaseVersion() { 432 try { 433 return getSession().getWorkspace().getVersionManager().getBaseVersion(getPath()); 434 } catch (final RepositoryException e) { 435 throw new RepositoryRuntimeException(e); 436 } 437 } 438 439 /* 440 * (non-Javadoc) 441 * @see org.fcrepo.kernel.models.FedoraResource#getVersionHistory() 442 */ 443 @Override 444 public VersionHistory getVersionHistory() { 445 try { 446 return getSession().getWorkspace().getVersionManager().getVersionHistory(getPath()); 447 } catch (final RepositoryException e) { 448 throw new RepositoryRuntimeException(e); 449 } 450 } 451 452 /* (non-Javadoc) 453 * @see org.fcrepo.kernel.models.FedoraResource#isNew() 454 */ 455 @Override 456 public Boolean isNew() { 457 return node.isNew(); 458 } 459 460 /* (non-Javadoc) 461 * @see org.fcrepo.kernel.models.FedoraResource#replaceProperties 462 * (org.fcrepo.kernel.identifiers.IdentifierConverter, com.hp.hpl.jena.rdf.model.Model) 463 */ 464 @Override 465 public void replaceProperties(final IdentifierConverter<Resource, FedoraResource> idTranslator, 466 final Model inputModel, final RdfStream originalTriples) throws MalformedRdfException { 467 468 final RdfStream replacementStream = new RdfStream().namespaces(inputModel.getNsPrefixMap()); 469 470 final GraphDifferencingIterator differencer = 471 new GraphDifferencingIterator(inputModel, originalTriples); 472 473 final StringBuilder exceptions = new StringBuilder(); 474 try { 475 new RdfRemover(idTranslator, getSession(), replacementStream 476 .withThisContext(differencer)).consume(); 477 } catch (final MalformedRdfException e) { 478 exceptions.append(e.getMessage()); 479 exceptions.append("\n"); 480 } 481 482 try { 483 new RdfAdder(idTranslator, getSession(), replacementStream 484 .withThisContext(differencer.notCommon())).consume(); 485 } catch (final MalformedRdfException e) { 486 exceptions.append(e.getMessage()); 487 } 488 489 if (exceptions.length() > 0) { 490 throw new MalformedRdfException(exceptions.toString()); 491 } 492 } 493 494 /* (non-Javadoc) 495 * @see org.fcrepo.kernel.models.FedoraResource#getEtagValue() 496 */ 497 @Override 498 public String getEtagValue() { 499 final Date lastModifiedDate = getLastModifiedDate(); 500 501 if (lastModifiedDate != null) { 502 return shaHex(getPath() + lastModifiedDate.getTime()); 503 } 504 return ""; 505 } 506 507 @Override 508 public void enableVersioning() { 509 try { 510 node.addMixin("mix:versionable"); 511 } catch (final RepositoryException e) { 512 throw new RepositoryRuntimeException(e); 513 } 514 } 515 516 @Override 517 public void disableVersioning() { 518 try { 519 node.removeMixin("mix:versionable"); 520 } catch (final RepositoryException e) { 521 throw new RepositoryRuntimeException(e); 522 } 523 524 } 525 526 @Override 527 public boolean isVersioned() { 528 try { 529 return node.isNodeType("mix:versionable"); 530 } catch (final RepositoryException e) { 531 throw new RepositoryRuntimeException(e); 532 } 533 } 534 535 @Override 536 public boolean isFrozenResource() { 537 return isFrozenNode.apply(this); 538 } 539 540 @Override 541 public FedoraResource getVersionedAncestor() { 542 543 try { 544 if (!isFrozenResource()) { 545 return null; 546 } 547 548 Node versionableFrozenNode = getNode(); 549 FedoraResource unfrozenResource = getUnfrozenResource(); 550 551 // traverse the frozen tree looking for a node whose unfrozen equivalent is versioned 552 while (!unfrozenResource.isVersioned()) { 553 554 if (versionableFrozenNode.getDepth() == 0) { 555 return null; 556 } 557 558 // node in the frozen tree 559 versionableFrozenNode = versionableFrozenNode.getParent(); 560 561 // unfrozen equivalent 562 unfrozenResource = new FedoraResourceImpl(versionableFrozenNode).getUnfrozenResource(); 563 } 564 565 return new FedoraResourceImpl(versionableFrozenNode); 566 } catch (final RepositoryException e) { 567 throw new RepositoryRuntimeException(e); 568 } 569 570 } 571 572 @Override 573 public FedoraResource getUnfrozenResource() { 574 if (!isFrozenResource()) { 575 return this; 576 } 577 578 try { 579 return new FedoraResourceImpl(getSession().getNodeByIdentifier(getProperty("jcr:frozenUuid").getString())); 580 } catch (final RepositoryException e) { 581 throw new RepositoryRuntimeException(e); 582 } 583 } 584 585 @Override 586 public Node getNodeVersion(final String label) { 587 try { 588 final Session session = getSession(); 589 590 final Node n = getFrozenNode(label); 591 592 if (n != null) { 593 return n; 594 } 595 596 if (isVersioned()) { 597 final VersionHistory hist = 598 session.getWorkspace().getVersionManager().getVersionHistory(getPath()); 599 600 if (hist.hasVersionLabel(label)) { 601 LOGGER.debug("Found version for {} by label {}.", this, label); 602 return hist.getVersionByLabel(label).getFrozenNode(); 603 } 604 } 605 606 LOGGER.warn("Unknown version {} with label or uuid {}!", this, label); 607 return null; 608 } catch (final RepositoryException e) { 609 throw new RepositoryRuntimeException(e); 610 } 611 612 } 613 614 private Node getFrozenNode(final String label) throws RepositoryException { 615 try { 616 final Session session = getSession(); 617 618 final Node frozenNode = session.getNodeByIdentifier(label); 619 620 final String baseUUID = getNode().getIdentifier(); 621 622 /* 623 * We found a node whose identifier is the "label" for the version. Now 624 * we must do due dilligence to make sure it's a frozen node representing 625 * a version of the subject node. 626 */ 627 final Property p = frozenNode.getProperty("jcr:frozenUuid"); 628 if (p != null) { 629 if (p.getString().equals(baseUUID)) { 630 return frozenNode; 631 } 632 } 633 /* 634 * Though a node with an id of the label was found, it wasn't the 635 * node we were looking for, so fall through and look for a labeled 636 * node. 637 */ 638 } catch (final ItemNotFoundException ex) { 639 /* 640 * the label wasn't a uuid of a frozen node but 641 * instead possibly a version label. 642 */ 643 } 644 return null; 645 } 646 647 @Override 648 public boolean equals(final Object object) { 649 if (object instanceof FedoraResourceImpl) { 650 return ((FedoraResourceImpl) object).getNode().equals(this.getNode()); 651 } 652 return false; 653 } 654 655 @Override 656 public int hashCode() { 657 return getNode().hashCode(); 658 } 659 660 protected Session getSession() { 661 try { 662 return getNode().getSession(); 663 } catch (final RepositoryException e) { 664 throw new RepositoryRuntimeException(e); 665 } 666 } 667 668 @Override 669 public String toString() { 670 return getNode().toString(); 671 } 672}