001/* 002 * The contents of this file are subject to the license and copyright 003 * detailed in the LICENSE and NOTICE files at the root of the source 004 * tree. 005 */ 006package org.fcrepo.kernel.impl.services; 007 008import org.apache.jena.graph.Node; 009import org.apache.jena.graph.NodeFactory; 010import org.apache.jena.graph.Triple; 011import org.apache.jena.rdf.model.Property; 012import org.apache.jena.rdf.model.Resource; 013import org.apache.jena.rdf.model.Statement; 014 015import org.fcrepo.config.OcflPropsConfig; 016import org.fcrepo.kernel.api.RdfLexicon; 017import org.fcrepo.kernel.api.RdfStream; 018import org.fcrepo.kernel.api.Transaction; 019import org.fcrepo.kernel.api.exception.PathNotFoundException; 020import org.fcrepo.kernel.api.exception.PathNotFoundRuntimeException; 021import org.fcrepo.kernel.api.identifiers.FedoraId; 022import org.fcrepo.kernel.api.models.Binary; 023import org.fcrepo.kernel.api.models.Container; 024import org.fcrepo.kernel.api.models.FedoraResource; 025import org.fcrepo.kernel.api.models.ResourceFactory; 026import org.fcrepo.kernel.api.models.Tombstone; 027import org.fcrepo.kernel.api.rdf.DefaultRdfStream; 028import org.fcrepo.kernel.api.services.MembershipService; 029import org.slf4j.Logger; 030import org.springframework.stereotype.Component; 031 032import javax.annotation.Nonnull; 033import javax.inject.Inject; 034import java.time.Instant; 035import java.util.ArrayList; 036import java.util.List; 037import java.util.Objects; 038import java.util.stream.Collectors; 039 040import static org.fcrepo.kernel.api.RdfCollectors.toModel; 041import static org.slf4j.LoggerFactory.getLogger; 042import static org.apache.jena.rdf.model.ResourceFactory.createProperty; 043 044/** 045 * Implementation of a service which updates and persists membership properties for resources 046 * 047 * @author bbpennel 048 * @since 6.0.0 049 */ 050@Component 051public class MembershipServiceImpl implements MembershipService { 052 private static final Logger log = getLogger(MembershipServiceImpl.class); 053 054 public static final Instant NO_END_INSTANT = Instant.parse("9999-12-31T00:00:00.000Z"); 055 056 @Inject 057 private MembershipIndexManager indexManager; 058 059 @Inject 060 private ResourceFactory resourceFactory; 061 062 @Inject 063 private OcflPropsConfig propsConfig; 064 065 private enum ContainerType { 066 Direct, Indirect; 067 } 068 069 @Override 070 public void resourceCreated(final Transaction tx, final FedoraId fedoraId) { 071 final var fedoraResc = getFedoraResource(tx, fedoraId); 072 073 // Only need to compute membership for created containers and binaries 074 if (!(fedoraResc instanceof Container || fedoraResc instanceof Binary)) { 075 return; 076 } 077 078 if (isChildOfRoot(fedoraResc)) { 079 return; 080 } 081 082 final var parentResc = getParentResource(fedoraResc); 083 final var containerProperties = new DirectContainerProperties(parentResc); 084 085 if (containerProperties.containerType != null) { 086 final var newMembership = generateMembership(containerProperties, fedoraResc); 087 indexManager.addMembership(tx, parentResc.getFedoraId(), fedoraResc.getFedoraId(), 088 newMembership, fedoraResc.getCreatedDate()); 089 } 090 } 091 092 @Override 093 public void resourceModified(final Transaction tx, final FedoraId fedoraId) { 094 final var fedoraResc = getFedoraResource(tx, fedoraId); 095 final var containerProperties = new DirectContainerProperties(fedoraResc); 096 097 if (containerProperties.containerType != null) { 098 log.debug("Modified DirectContainer {}, recomputing generated membership relations", fedoraId); 099 100 if (propsConfig.isAutoVersioningEnabled()) { 101 modifyDCAutoversioned(tx, fedoraResc, containerProperties); 102 } else { 103 modifyDCOnDemandVersioning(tx, fedoraResc); 104 } 105 } 106 107 if (isChildOfRoot(fedoraResc)) { 108 return; 109 } 110 111 final var parentResc = getParentResource(fedoraResc); 112 final var parentProperties = new DirectContainerProperties(parentResc); 113 114 // Handle updates of proxies in IndirectContainer 115 if (ContainerType.Indirect.equals(parentProperties.containerType)) { 116 modifyProxy(tx, fedoraResc, parentProperties); 117 } 118 } 119 120 private void modifyProxy(final Transaction tx, final FedoraResource proxyResc, 121 final DirectContainerProperties containerProperties) { 122 final var lastModified = proxyResc.getLastModifiedDate(); 123 124 if (propsConfig.isAutoVersioningEnabled()) { 125 // end existing stuff 126 indexManager.endMembershipFromChild(tx, containerProperties.id, proxyResc.getFedoraId(), lastModified); 127 // add new membership 128 } else { 129 final var mementoDatetimes = proxyResc.getTimeMap().listMementoDatetimes(); 130 final Instant lastVersionDatetime; 131 if (mementoDatetimes.size() == 0) { 132 // If no previous versions of proxy, then cleanup and repopulate everything 133 lastVersionDatetime = null; 134 } else { 135 // If at least one past version, then reindex membership involving the last version and after 136 lastVersionDatetime = mementoDatetimes.get(mementoDatetimes.size() - 1); 137 } 138 indexManager.deleteMembershipForProxyAfter(tx, containerProperties.id, 139 proxyResc.getFedoraId(), lastVersionDatetime); 140 } 141 142 indexManager.addMembership(tx, containerProperties.id, proxyResc.getFedoraId(), 143 generateMembership(containerProperties, proxyResc), lastModified); 144 } 145 146 private void modifyDCAutoversioned(final Transaction tx, final FedoraResource dcResc, 147 final DirectContainerProperties containerProperties) { 148 final var dcId = dcResc.getFedoraId(); 149 final var dcLastModified = dcResc.getLastModifiedDate(); 150 // Delete/end existing membership from this container 151 indexManager.endMembershipForSource(tx, dcResc.getFedoraId(), dcLastModified); 152 153 // Add updated membership properties for all non-tombstone children 154 dcResc.getChildren() 155 .filter(child -> !(child instanceof Tombstone)) 156 .forEach(child -> { 157 final var newMembership = generateMembership(containerProperties, child); 158 indexManager.addMembership(tx, dcId, child.getFedoraId(), 159 newMembership, dcLastModified); 160 }); 161 } 162 163 private void modifyDCOnDemandVersioning(final Transaction tx, final FedoraResource dcResc) { 164 final var dcId = dcResc.getFedoraId(); 165 final var mementoDatetimes = dcResc.getTimeMap().listMementoDatetimes(); 166 final Instant lastVersionDatetime; 167 if (mementoDatetimes.size() == 0) { 168 // If no previous versions of DC, then cleanup and repopulate everything 169 lastVersionDatetime = null; 170 } else { 171 // If at least one past version, then reindex membership involving the last version and after 172 lastVersionDatetime = mementoDatetimes.get(mementoDatetimes.size() - 1); 173 } 174 indexManager.deleteMembershipForSourceAfter(tx, dcId, lastVersionDatetime); 175 populateMembershipHistory(tx, dcResc, lastVersionDatetime); 176 } 177 178 private Triple generateMembership(final DirectContainerProperties properties, final FedoraResource childResc) { 179 final var childRdfResc = getRdfResource(childResc.getFedoraId()); 180 181 final Node memberNode; 182 if (ContainerType.Indirect.equals(properties.containerType)) { 183 // Special case to use child as the member subject 184 if (RdfLexicon.MEMBER_SUBJECT.equals(properties.insertedContentRelation)) { 185 memberNode = childRdfResc.asNode(); 186 } else { 187 // get the member node from the child resource's insertedContentRelation property 188 final var childModelResc = getRdfResource(childResc); 189 final Statement stmt = childModelResc.getProperty(properties.insertedContentRelation); 190 // Ignore the child if it is missing the insertedContentRelation or its object is not a resource 191 if (stmt == null || !(stmt.getObject() instanceof Resource)) { 192 return null; 193 } 194 memberNode = stmt.getResource().asNode(); 195 } 196 } else { 197 memberNode = childRdfResc.asNode(); 198 } 199 200 return generateMembershipTriple(properties.membershipResource, memberNode, 201 properties.hasMemberRelation, properties.isMemberOfRelation); 202 } 203 204 private Triple generateMembershipTriple(final Node membership, final Node member, 205 final Node hasMemberRel, final Node memberOfRel) { 206 if (memberOfRel != null) { 207 return new Triple(member, memberOfRel, membership); 208 } else { 209 return new Triple(membership, hasMemberRel, member); 210 } 211 } 212 213 private Resource getRdfResource(final FedoraResource fedoraResc) { 214 final var model = fedoraResc.getTriples().collect(toModel()); 215 return model.getResource(fedoraResc.getFedoraId().getFullId()); 216 } 217 218 private Resource getRdfResource(final FedoraId fedoraId) { 219 return org.apache.jena.rdf.model.ResourceFactory.createResource(fedoraId.getFullId()); 220 } 221 222 private FedoraResource getFedoraResource(final Transaction transaction, final FedoraId fedoraId) { 223 try { 224 return resourceFactory.getResource(transaction, fedoraId); 225 } catch (final PathNotFoundException e) { 226 throw new PathNotFoundRuntimeException(e.getMessage(), e); 227 } 228 } 229 230 private FedoraResource getParentResource(final FedoraResource resc) { 231 try { 232 return resc.getParent(); 233 } catch (final PathNotFoundException e) { 234 throw new PathNotFoundRuntimeException(e.getMessage(), e); 235 } 236 } 237 238 @Override 239 public void resourceDeleted(@Nonnull final Transaction transaction, final FedoraId fedoraId) { 240 // delete DirectContainer, end all membership for that source 241 FedoraResource fedoraResc; 242 try { 243 fedoraResc = getFedoraResource(transaction, fedoraId); 244 } catch (final PathNotFoundRuntimeException e) { 245 log.debug("Deleted resource {} does not have a tombstone, cleanup any references", fedoraId); 246 indexManager.deleteMembershipReferences(transaction.getId(), fedoraId); 247 return; 248 } 249 if (fedoraResc instanceof Tombstone) { 250 fedoraResc = ((Tombstone) fedoraResc).getDeletedObject(); 251 } 252 253 final var resourceContainerType = getContainerType(fedoraResc); 254 if (resourceContainerType != null) { 255 log.debug("Ending membership for deleted Direct/IndirectContainer {} in {}", fedoraId, transaction); 256 indexManager.endMembershipForSource(transaction, fedoraId, fedoraResc.getLastModifiedDate()); 257 } 258 259 if (isChildOfRoot(fedoraResc)) { 260 return; 261 } 262 263 // delete child of DirectContainer, clear from tx and end existing 264 final var parentResc = getParentResource(fedoraResc); 265 final var parentContainerType = getContainerType(parentResc); 266 267 if (parentContainerType != null) { 268 log.debug("Ending membership for deleted proxy or member in tx {} for {} at {}", 269 transaction, parentResc.getFedoraId(), fedoraResc.getLastModifiedDate()); 270 indexManager.endMembershipFromChild(transaction, parentResc.getFedoraId(), fedoraResc.getFedoraId(), 271 fedoraResc.getLastModifiedDate()); 272 } 273 } 274 275 @Override 276 public RdfStream getMembership(final Transaction tx, final FedoraId fedoraId) { 277 final FedoraId subjectId; 278 if (fedoraId.isDescription()) { 279 subjectId = fedoraId.asBaseId(); 280 } else { 281 subjectId = fedoraId; 282 } 283 final var subject = NodeFactory.createURI(subjectId.getBaseId()); 284 final var membershipStream = indexManager.getMembership(tx, subjectId); 285 return new DefaultRdfStream(subject, membershipStream); 286 } 287 288 @Override 289 public void commitTransaction(final Transaction tx) { 290 indexManager.commitTransaction(tx); 291 } 292 293 @Override 294 public void rollbackTransaction(final Transaction tx) { 295 indexManager.deleteTransaction(tx); 296 } 297 298 @Override 299 public void reset() { 300 indexManager.clearIndex(); 301 } 302 303 @Override 304 public void populateMembershipHistory(@Nonnull final Transaction transaction, final FedoraId containerId) { 305 final FedoraResource fedoraResc = getFedoraResource(transaction, containerId); 306 final var containerType = getContainerType(fedoraResc); 307 308 if (containerType != null) { 309 populateMembershipHistory(transaction, fedoraResc, null); 310 } 311 } 312 313 private void populateMembershipHistory(final Transaction tx, final FedoraResource fedoraResc, 314 final Instant afterTime) { 315 final var containerId = fedoraResc.getFedoraId(); 316 final var propertyTimeline = makePropertyTimeline(fedoraResc); 317 final List<DirectContainerProperties> timeline; 318 // If provided, filter the timeline to just entries active on or after the specified time 319 if (afterTime != null) { 320 timeline = propertyTimeline.stream().filter(e -> e.startDatetime.compareTo(afterTime) >= 0 321 || e.endDatetime.compareTo(afterTime) >= 0) 322 .collect(Collectors.toList()); 323 } else { 324 timeline = propertyTimeline; 325 } 326 327 // get all the members of the DC and index the history for each, accounting for changes to the DC 328 fedoraResc.getChildren().forEach(member -> { 329 // must ensure the tx does not close before indexing is complete 330 tx.refresh(); 331 332 final var memberDeleted = member instanceof Tombstone; 333 log.debug("Populating membership history for DirectContainer {}member {}", 334 memberDeleted ? "deleted " : "", member.getFedoraId()); 335 final Instant memberCreated; 336 // Get the creation time from the deleted object if the member is a tombstone 337 if (memberDeleted) { 338 memberCreated = ((Tombstone) member).getDeletedObject().getCreatedDate(); 339 } else { 340 memberCreated = member.getCreatedDate(); 341 } 342 final var memberModified = member.getLastModifiedDate(); 343 final var memberEnd = memberDeleted ? memberModified : NO_END_INSTANT; 344 345 // Reduce timeline to just states in effect after the member was created 346 var timelineStream = timeline.stream() 347 .filter(e -> e.endDatetime.compareTo(memberCreated) > 0); 348 // If the member was deleted, then reduce timeline to states before the deletion 349 if (memberDeleted) { 350 timelineStream = timelineStream.filter(e -> e.startDatetime.compareTo(memberModified) < 0); 351 } 352 // Index each addition or change to the membership generated by this member 353 timelineStream.forEach(e -> { 354 // Start time of the membership is the later of member creation or membership resc memento time 355 indexManager.addMembership(tx, containerId, member.getFedoraId(), 356 generateMembership(e, member), 357 instantMax(memberCreated, e.startDatetime), 358 instantMin(memberEnd, e.endDatetime)); 359 }); 360 }); 361 } 362 363 private Instant instantMax(final Instant first, final Instant second) { 364 if (first.isAfter(second)) { 365 return first; 366 } else { 367 return second; 368 } 369 } 370 371 private Instant instantMin(final Instant first, final Instant second) { 372 if (first.isBefore(second)) { 373 return first; 374 } else { 375 return second; 376 } 377 } 378 379 /** 380 * Creates a timeline of states for a DirectContainer, tracking changes to its 381 * properties that impact membership. 382 * @param fedoraResc resource subject of the timeline 383 * @return timeline 384 */ 385 private List<DirectContainerProperties> makePropertyTimeline(final FedoraResource fedoraResc) { 386 final var entryList = fedoraResc.getTimeMap().getChildren() 387 .map(memento -> new DirectContainerProperties(memento)) 388 .collect(Collectors.toCollection(ArrayList::new)); 389 // For versioning on demand, add the head version to the timeline 390 if (!propsConfig.isAutoVersioningEnabled()) { 391 entryList.add(new DirectContainerProperties(fedoraResc)); 392 } 393 // First entry starts at creation time of the resource 394 entryList.get(0).startDatetime = fedoraResc.getCreatedDate(); 395 396 // Reduce timeline to entries where significant properties change 397 final var changeEntries = new ArrayList<DirectContainerProperties>(); 398 var curr = entryList.get(0); 399 changeEntries.add(curr); 400 for (int i = 1; i < entryList.size(); i++) { 401 final var next = entryList.get(i); 402 if (!Objects.equals(next.membershipResource, curr.membershipResource) 403 || !Objects.equals(next.hasMemberRelation, curr.hasMemberRelation) 404 || !Objects.equals(next.isMemberOfRelation, curr.isMemberOfRelation)) { 405 // Adjust the end the previous entry before the next state begins 406 curr.endDatetime = next.startDatetime; 407 changeEntries.add(next); 408 curr = next; 409 } 410 } 411 return changeEntries; 412 } 413 414 @Override 415 public void clearAllTransactions() { 416 indexManager.clearAllTransactions(); 417 } 418 419 /** 420 * The properties of a Direct or Indirect Container at a point in time. 421 * @author bbpennel 422 */ 423 private static class DirectContainerProperties { 424 public Node membershipResource; 425 public Node hasMemberRelation; 426 public Node isMemberOfRelation; 427 public Property insertedContentRelation; 428 public FedoraId id; 429 public ContainerType containerType; 430 public Instant startDatetime; 431 public Instant endDatetime = NO_END_INSTANT; 432 433 /** 434 * @param fedoraResc resource/memento from which the properties will be extracted 435 */ 436 public DirectContainerProperties(final FedoraResource fedoraResc) { 437 this.containerType = getContainerType(fedoraResc); 438 if (containerType == null) { 439 return; 440 } 441 id = fedoraResc.getFedoraId(); 442 startDatetime = fedoraResc.isMemento() ? 443 fedoraResc.getMementoDatetime() : fedoraResc.getLastModifiedDate(); 444 fedoraResc.getTriples().forEach(triple -> { 445 if (RdfLexicon.MEMBERSHIP_RESOURCE.asNode().equals(triple.getPredicate())) { 446 membershipResource = triple.getObject(); 447 } else if (RdfLexicon.HAS_MEMBER_RELATION.asNode().equals(triple.getPredicate())) { 448 hasMemberRelation = triple.getObject(); 449 } else if (RdfLexicon.IS_MEMBER_OF_RELATION.asNode().equals(triple.getPredicate())) { 450 isMemberOfRelation = triple.getObject(); 451 } else if (RdfLexicon.INSERTED_CONTENT_RELATION.asNode().equals(triple.getPredicate())) { 452 insertedContentRelation = createProperty(triple.getObject().getURI()); 453 } 454 }); 455 if (hasMemberRelation == null && isMemberOfRelation == null) { 456 hasMemberRelation = RdfLexicon.LDP_MEMBER.asNode(); 457 } 458 } 459 } 460 461 private static ContainerType getContainerType(final FedoraResource fedoraResc) { 462 if (!(fedoraResc instanceof Container)) { 463 return null; 464 } 465 466 if (RdfLexicon.INDIRECT_CONTAINER.getURI().equals(fedoraResc.getInteractionModel())) { 467 return ContainerType.Indirect; 468 } 469 470 if (RdfLexicon.DIRECT_CONTAINER.getURI().equals(fedoraResc.getInteractionModel())) { 471 return ContainerType.Direct; 472 } 473 474 return null; 475 } 476 477 @Override 478 public Instant getLastUpdatedTimestamp(final Transaction transaction, final FedoraId fedoraId) { 479 return indexManager.getLastUpdated(transaction, fedoraId); 480 } 481 482 private boolean isChildOfRoot(final FedoraResource resource) { 483 return resource.getParentId().isRepositoryRoot(); 484 } 485 486 /** 487 * @param indexManager the indexManager to set 488 */ 489 public void setMembershipIndexManager(final MembershipIndexManager indexManager) { 490 this.indexManager = indexManager; 491 } 492 493 /** 494 * @param resourceFactory the resourceFactory to set 495 */ 496 public void setResourceFactory(final ResourceFactory resourceFactory) { 497 this.resourceFactory = resourceFactory; 498 } 499}