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