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