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