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