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