From b5f470ee6f23aac797fa5732b1e80aecf5eb29af Mon Sep 17 00:00:00 2001 From: Ben Pennell Date: Thu, 9 Jun 2022 15:10:08 -0400 Subject: [PATCH 1/3] Return null as primary object if the primary object is a tombstone --- .../exceptions/TombstoneFoundException.java | 29 +++++ .../model/fcrepo/objects/WorkObjectImpl.java | 12 +- .../services/RepositoryObjectDriver.java | 6 + .../destroy/AbstractDestroyObjectsJob.java | 2 +- .../camel/solr/AbstractSolrProcessorIT.java | 13 +- .../camel/solr/SolrIngestProcessorIT.java | 118 +++++++++++++++++- 6 files changed, 175 insertions(+), 5 deletions(-) create mode 100644 model-api/src/main/java/edu/unc/lib/boxc/model/api/exceptions/TombstoneFoundException.java diff --git a/model-api/src/main/java/edu/unc/lib/boxc/model/api/exceptions/TombstoneFoundException.java b/model-api/src/main/java/edu/unc/lib/boxc/model/api/exceptions/TombstoneFoundException.java new file mode 100644 index 0000000000..53b10ed1fd --- /dev/null +++ b/model-api/src/main/java/edu/unc/lib/boxc/model/api/exceptions/TombstoneFoundException.java @@ -0,0 +1,29 @@ +/** + * Copyright 2008 The University of North Carolina at Chapel Hill + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package edu.unc.lib.boxc.model.api.exceptions; + +/** + * Exception indicating that the requested resource has been destroyed and replaced by a tombstone object + * + * @author bbpennel + */ +public class TombstoneFoundException extends RepositoryException { + private static final long serialVersionUID = 1L; + + public TombstoneFoundException(String message) { + super(message); + } +} diff --git a/model-fcrepo/src/main/java/edu/unc/lib/boxc/model/fcrepo/objects/WorkObjectImpl.java b/model-fcrepo/src/main/java/edu/unc/lib/boxc/model/fcrepo/objects/WorkObjectImpl.java index 43e1c491f3..a44b9e5dfc 100644 --- a/model-fcrepo/src/main/java/edu/unc/lib/boxc/model/fcrepo/objects/WorkObjectImpl.java +++ b/model-fcrepo/src/main/java/edu/unc/lib/boxc/model/fcrepo/objects/WorkObjectImpl.java @@ -17,6 +17,8 @@ import java.net.URI; +import edu.unc.lib.boxc.model.api.exceptions.NotFoundException; +import edu.unc.lib.boxc.model.api.exceptions.TombstoneFoundException; import org.apache.commons.lang3.StringUtils; import org.apache.jena.rdf.model.Model; import org.apache.jena.rdf.model.ModelFactory; @@ -39,6 +41,8 @@ import edu.unc.lib.boxc.model.api.services.RepositoryObjectFactory; import edu.unc.lib.boxc.model.fcrepo.ids.PIDs; import edu.unc.lib.boxc.model.fcrepo.services.RepositoryObjectDriver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * A repository object which represents a single work, and should contain one or @@ -51,6 +55,7 @@ * */ public class WorkObjectImpl extends AbstractContentContainerObject implements WorkObject { + private static final Logger log = LoggerFactory.getLogger(WorkObjectImpl.class); protected WorkObjectImpl(PID pid, RepositoryObjectDriver driver, RepositoryObjectFactory repoObjFactory) { super(pid, driver, repoObjFactory); @@ -116,7 +121,12 @@ public FileObject getPrimaryObject() { } PID primaryPid = PIDs.get(primaryStmt.getResource().getURI()); - return driver.getRepositoryObject(primaryPid, FileObject.class); + try { + return driver.getRepositoryObject(primaryPid, FileObject.class); + } catch (TombstoneFoundException e) { + log.debug("Cannot retrieve primary object for {}", getPid().getId(), e); + } + return null; } @Override diff --git a/model-fcrepo/src/main/java/edu/unc/lib/boxc/model/fcrepo/services/RepositoryObjectDriver.java b/model-fcrepo/src/main/java/edu/unc/lib/boxc/model/fcrepo/services/RepositoryObjectDriver.java index c5e8184579..dac36aba7e 100644 --- a/model-fcrepo/src/main/java/edu/unc/lib/boxc/model/fcrepo/services/RepositoryObjectDriver.java +++ b/model-fcrepo/src/main/java/edu/unc/lib/boxc/model/fcrepo/services/RepositoryObjectDriver.java @@ -23,6 +23,9 @@ import java.util.ArrayList; import java.util.List; +import edu.unc.lib.boxc.model.api.exceptions.NotFoundException; +import edu.unc.lib.boxc.model.api.exceptions.TombstoneFoundException; +import edu.unc.lib.boxc.model.api.objects.Tombstone; import org.apache.http.HttpStatus; import org.apache.jena.query.QueryExecution; import org.apache.jena.query.QuerySolution; @@ -162,6 +165,9 @@ public RepositoryObject getRepositoryObject(PID pid) { public T getRepositoryObject(PID pid, Class type) throws ObjectTypeMismatchException { RepositoryObject repoObj = repositoryObjectLoader.getRepositoryObject(pid); + if (repoObj instanceof Tombstone) { + throw new TombstoneFoundException("Tombstone found, requested object " + pid + " no longer exists"); + } if (!type.isInstance(repoObj)) { throw new ObjectTypeMismatchException("Requested object " + pid + " is not a " + type.getName()); } diff --git a/operations/src/main/java/edu/unc/lib/boxc/operations/impl/destroy/AbstractDestroyObjectsJob.java b/operations/src/main/java/edu/unc/lib/boxc/operations/impl/destroy/AbstractDestroyObjectsJob.java index 173b82019f..ecab56c3e4 100644 --- a/operations/src/main/java/edu/unc/lib/boxc/operations/impl/destroy/AbstractDestroyObjectsJob.java +++ b/operations/src/main/java/edu/unc/lib/boxc/operations/impl/destroy/AbstractDestroyObjectsJob.java @@ -127,7 +127,7 @@ protected void destroyBinaries() { } cleanupBinaryUris.forEach(contentUri -> { try { - log.debug("Deleting destroyed binary {}", contentUri); + log.error("Deleting destroyed binary {}", contentUri); StorageLocation storageLoc = locManager.getStorageLocationForUri(contentUri); transferSession.forDestination(storageLoc) .delete(contentUri); diff --git a/services-camel-app/src/test/java/edu/unc/lib/boxc/services/camel/solr/AbstractSolrProcessorIT.java b/services-camel-app/src/test/java/edu/unc/lib/boxc/services/camel/solr/AbstractSolrProcessorIT.java index 5fa96cf994..24170f0f60 100644 --- a/services-camel-app/src/test/java/edu/unc/lib/boxc/services/camel/solr/AbstractSolrProcessorIT.java +++ b/services-camel-app/src/test/java/edu/unc/lib/boxc/services/camel/solr/AbstractSolrProcessorIT.java @@ -25,8 +25,13 @@ import java.io.File; import java.io.InputStream; import java.net.URI; +import java.nio.file.Paths; +import java.util.UUID; import edu.unc.lib.boxc.indexing.solr.test.RepositoryObjectSolrIndexer; +import edu.unc.lib.boxc.model.fcrepo.ids.PIDs; +import edu.unc.lib.boxc.persist.api.storage.StorageLocationManager; +import edu.unc.lib.boxc.persist.impl.storage.StorageLocationTestHelper; import org.apache.camel.Exchange; import org.apache.camel.Message; import org.apache.camel.test.spring.CamelSpringRunner; @@ -104,6 +109,8 @@ public abstract class AbstractSolrProcessorIT { protected RepositoryObjectTreeIndexer treeIndexer; @Autowired protected RepositoryObjectSolrIndexer repositoryObjectSolrIndexer; + @Autowired + protected StorageLocationManager locManager; protected ContentRootObject rootObj; protected AdminUnit unitObj; @@ -143,10 +150,12 @@ protected void indexObjectsInTripleStore() throws Exception { } protected URI makeContentUri(String content) throws Exception { - File contentFile = File.createTempFile("test", ".txt"); + var loc1 = locManager.getStorageLocationById(StorageLocationTestHelper.LOC1_ID); + var storageUri = loc1.getNewStorageUri(PIDs.get(UUID.randomUUID().toString())); + var contentFile = new File(storageUri); contentFile.deleteOnExit(); FileUtils.write(contentFile, content, UTF_8); - return contentFile.toPath().toUri(); + return storageUri; } protected InputStream streamResource(String resourcePath) throws Exception { diff --git a/services-camel-app/src/test/java/edu/unc/lib/boxc/services/camel/solr/SolrIngestProcessorIT.java b/services-camel-app/src/test/java/edu/unc/lib/boxc/services/camel/solr/SolrIngestProcessorIT.java index 9cb7105e76..cb39f029f7 100644 --- a/services-camel-app/src/test/java/edu/unc/lib/boxc/services/camel/solr/SolrIngestProcessorIT.java +++ b/services-camel-app/src/test/java/edu/unc/lib/boxc/services/camel/solr/SolrIngestProcessorIT.java @@ -19,6 +19,7 @@ import static edu.unc.lib.boxc.fcrepo.FcrepoJmsConstants.RESOURCE_TYPE; import static edu.unc.lib.boxc.model.api.DatastreamType.MD_DESCRIPTIVE; import static edu.unc.lib.boxc.model.api.DatastreamType.ORIGINAL_FILE; +import static edu.unc.lib.boxc.model.api.DatastreamType.MD_EVENTS; import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -34,12 +35,29 @@ import java.util.List; import java.util.stream.Collectors; +import edu.unc.lib.boxc.auth.api.services.AccessControlService; +import edu.unc.lib.boxc.auth.fcrepo.models.AccessGroupSetImpl; +import edu.unc.lib.boxc.auth.fcrepo.models.AgentPrincipalsImpl; +import edu.unc.lib.boxc.auth.fcrepo.services.InheritedAclFactory; +import edu.unc.lib.boxc.fcrepo.utils.TransactionManager; +import edu.unc.lib.boxc.model.api.services.RepositoryObjectFactory; +import edu.unc.lib.boxc.model.api.sparql.SparqlUpdateService; +import edu.unc.lib.boxc.operations.api.events.PremisLoggerFactory; +import edu.unc.lib.boxc.operations.impl.delete.MarkForDeletionJob; +import edu.unc.lib.boxc.operations.impl.delete.MarkForDeletionService; +import edu.unc.lib.boxc.operations.impl.destroy.DestroyObjectsJob; import edu.unc.lib.boxc.operations.jms.MessageSender; +import edu.unc.lib.boxc.operations.jms.destroy.DestroyObjectsRequest; +import edu.unc.lib.boxc.operations.jms.indexing.IndexingMessageSender; +import edu.unc.lib.boxc.persist.api.storage.StorageLocationManager; +import edu.unc.lib.boxc.persist.api.transfer.BinaryTransferService; +import edu.unc.lib.boxc.search.solr.services.ObjectPathFactory; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.io.FileUtils; import org.apache.jena.rdf.model.Model; import org.apache.jena.rdf.model.ModelFactory; import org.apache.jena.rdf.model.Resource; +import org.fcrepo.client.FcrepoClient; import org.junit.Before; import org.junit.Test; import org.mockito.Mock; @@ -86,15 +104,35 @@ public class SolrIngestProcessorIT extends AbstractSolrProcessorIT { private RepositoryObjectLoader repositoryObjectLoader; @Autowired private DerivativeService derivativeService; - @Mock private AgentPrincipals agent; @Autowired private MessageSender updateWorkSender; + @Autowired + private AccessControlService aclService; + @Autowired + private TransactionManager txManager; + @Autowired + private ObjectPathFactory pathFactory; + @Autowired + private FcrepoClient fcrepoClient; + @Autowired + private InheritedAclFactory inheritedAclFactory; + @Autowired + private BinaryTransferService transferService; + @Mock + private IndexingMessageSender indexingMessageSender; + @Mock + private MessageSender binaryDestroyedMessageSender; + @Autowired + private PremisLoggerFactory premisLoggerFactory; + @Autowired + private SparqlUpdateService sparqlUpdateService; @Before public void setUp() throws Exception { initMocks(this); + agent = new AgentPrincipalsImpl("user", new AccessGroupSetImpl("group")); TestHelper.setContentBase(baseAddress); processor = new SolrIngestProcessor(dipFactory, solrFullUpdatePipeline, driver, repositoryObjectLoader); @@ -283,6 +321,84 @@ public void testIndexBinaryInWork() throws Exception { assertNotNull(fileMd.getDatastreamObject(ORIGINAL_FILE.getId())); } + // Relates to bug: BXC-3676 + @Test + public void testWorkWithTombstonePrimaryObject() throws Exception { + repositoryObjectSolrIndexer.index(unitObj.getPid(), collObj.getPid()); + + WorkObject workObj = repositoryObjectFactory.createWorkObject(null); + InputStream modsStream = streamResource("/datastreams/simpleMods.xml"); + updateDescriptionService.updateDescription(new UpdateDescriptionRequest(agent, workObj.getPid(), modsStream)); + collObj.addMember(workObj); + + FileObject fileObj = workObj.addDataFile(makeContentUri(CONTENT_TEXT), + "text.txt", "text/plain", null, null); + workObj.setPrimaryObject(fileObj.getPid()); + + indexObjectsInTripleStore(); + + setMessageTarget(fileObj); + when(message.getHeader(RESOURCE_TYPE)).thenReturn(Cdr.FileObject.getURI()); + processor.process(exchange); + server.commit(); + + setMessageTarget(workObj); + processor.process(exchange); + server.commit(); + + // Replace primary object with tombstone + deleteAndDestroyObject(fileObj); + + indexObjectsInTripleStore(); + + setMessageTarget(workObj); + processor.process(exchange); + server.commit(); + + SimpleIdRequest idRequest = new SimpleIdRequest(workObj.getPid(), accessGroups); + ContentObjectRecord workMd = solrSearchService.getObjectById(idRequest); + + assertEquals("Work", workMd.getResourceType()); + + assertNotNull("Date added must be set", workMd.getDateAdded()); + assertNotNull("Date updated must be set", workMd.getDateUpdated()); + + assertEquals("Object title", workMd.getTitle()); + assertEquals("Boxy", workMd.getCreator().get(0)); + + assertAncestorIds(workMd, rootObj, unitObj, collObj, workObj); + + // Should not have an original file, but other datastreams should still be present + assertEquals(2, workMd.getDatastream().size()); + assertNull(workMd.getDatastreamObject(ORIGINAL_FILE.getId())); + assertNotNull(workMd.getDatastreamObject(MD_DESCRIPTIVE.getId())); + assertNotNull(workMd.getDatastreamObject(MD_EVENTS.getId())); + } + + private void deleteAndDestroyObject(RepositoryObject repoObj) { + // Object must be marked for deletion before destroying it + var markForDeleteJob = new MarkForDeletionJob(repoObj.getPid(), "delete me", agent, repositoryObjectLoader, + sparqlUpdateService, aclService, premisLoggerFactory); + markForDeleteJob.run(); + + // Now destroy it + var destroyRequest = new DestroyObjectsRequest("job", agent, repoObj.getPid().getId()); + var destroyJob = new DestroyObjectsJob(destroyRequest); + destroyJob.setPathFactory(pathFactory); + destroyJob.setInheritedAclFactory(inheritedAclFactory); + destroyJob.setBinaryDestroyedMessageSender(binaryDestroyedMessageSender); + destroyJob.setAclService(aclService); + destroyJob.setFcrepoClient(fcrepoClient); + destroyJob.setPremisLoggerFactory(premisLoggerFactory); + destroyJob.setRepoObjFactory(repositoryObjectFactory); + destroyJob.setRepoObjLoader(repositoryObjectLoader); + destroyJob.setIndexingMessageSender(indexingMessageSender); + destroyJob.setStorageLocationManager(locManager); + destroyJob.setTransactionManager(txManager); + destroyJob.setBinaryTransferService(transferService); + destroyJob.run(); + } + private void assertAncestorIds(ContentObjectRecord md, RepositoryObject... ancestorObjs) { String joinedIds = "/" + Arrays.stream(ancestorObjs) .map(obj -> obj.getPid().getId()) From 19572dde4862bc55a54be27d1959e1b3d6dfda69 Mon Sep 17 00:00:00 2001 From: Ben Pennell Date: Thu, 9 Jun 2022 16:56:38 -0400 Subject: [PATCH 2/3] Clear primary object relation on work if the primary object gets destroyed --- .../boxc/model/api/objects/WorkObject.java | 7 +- .../destroy/AbstractDestroyObjectsJob.java | 2 +- .../impl/destroy/DestroyObjectsJob.java | 9 + .../impl/destroy/DestroyObjectsJobIT.java | 182 ++++++++++++------ 4 files changed, 135 insertions(+), 65 deletions(-) diff --git a/model-api/src/main/java/edu/unc/lib/boxc/model/api/objects/WorkObject.java b/model-api/src/main/java/edu/unc/lib/boxc/model/api/objects/WorkObject.java index 92fd74c381..0811cfe7d2 100644 --- a/model-api/src/main/java/edu/unc/lib/boxc/model/api/objects/WorkObject.java +++ b/model-api/src/main/java/edu/unc/lib/boxc/model/api/objects/WorkObject.java @@ -71,14 +71,13 @@ FileObject addDataFile(URI storageUri, String filename, String mimetype, String * original file, using the provided pid as the identifier for the new * FileObject. * - * - * @param contentStream - * Inputstream containing the binary content for the data file. Required. + * @param filePid + * @param storageUri * @param filename * @param mimetype * @param sha1Checksum + * @param md5Checksum * @param model - * model containing properties for the new fileObject * @return */ FileObject addDataFile(PID filePid, URI storageUri, String filename, String mimetype, String sha1Checksum, diff --git a/operations/src/main/java/edu/unc/lib/boxc/operations/impl/destroy/AbstractDestroyObjectsJob.java b/operations/src/main/java/edu/unc/lib/boxc/operations/impl/destroy/AbstractDestroyObjectsJob.java index ecab56c3e4..173b82019f 100644 --- a/operations/src/main/java/edu/unc/lib/boxc/operations/impl/destroy/AbstractDestroyObjectsJob.java +++ b/operations/src/main/java/edu/unc/lib/boxc/operations/impl/destroy/AbstractDestroyObjectsJob.java @@ -127,7 +127,7 @@ protected void destroyBinaries() { } cleanupBinaryUris.forEach(contentUri -> { try { - log.error("Deleting destroyed binary {}", contentUri); + log.debug("Deleting destroyed binary {}", contentUri); StorageLocation storageLoc = locManager.getStorageLocationForUri(contentUri); transferSession.forDestination(storageLoc) .delete(contentUri); diff --git a/operations/src/main/java/edu/unc/lib/boxc/operations/impl/destroy/DestroyObjectsJob.java b/operations/src/main/java/edu/unc/lib/boxc/operations/impl/destroy/DestroyObjectsJob.java index 7fd8c5ac9b..342ba6f4f0 100644 --- a/operations/src/main/java/edu/unc/lib/boxc/operations/impl/destroy/DestroyObjectsJob.java +++ b/operations/src/main/java/edu/unc/lib/boxc/operations/impl/destroy/DestroyObjectsJob.java @@ -27,6 +27,7 @@ import java.util.Collections; import java.util.List; +import edu.unc.lib.boxc.model.api.objects.WorkObject; import org.apache.jena.rdf.model.Model; import org.apache.jena.rdf.model.ModelFactory; import org.apache.jena.rdf.model.NodeIterator; @@ -95,6 +96,14 @@ public void run() { if (!repoObj.getResource(true).hasProperty(RDF.type, Cdr.Tombstone)) { RepositoryObject parentObj = repoObj.getParent(); + // If the object being deleted is the primary object of a work, then clear the relation + if (parentObj instanceof WorkObject && repoObj instanceof FileObject) { + var parentWork = (WorkObject) parentObj; + var primaryObj = parentWork.getPrimaryObject(); + if (primaryObj != null && primaryObj.getPid().equals(repoObj.getPid())) { + parentWork.clearPrimaryObject(); + } + } // purge tree with repoObj as root from repository // Add the root of the tree to delete diff --git a/operations/src/test/java/edu/unc/lib/boxc/operations/impl/destroy/DestroyObjectsJobIT.java b/operations/src/test/java/edu/unc/lib/boxc/operations/impl/destroy/DestroyObjectsJobIT.java index 3f0062a9fa..d7976f0cea 100644 --- a/operations/src/test/java/edu/unc/lib/boxc/operations/impl/destroy/DestroyObjectsJobIT.java +++ b/operations/src/test/java/edu/unc/lib/boxc/operations/impl/destroy/DestroyObjectsJobIT.java @@ -15,57 +15,6 @@ */ package edu.unc.lib.boxc.operations.impl.destroy; -import static edu.unc.lib.boxc.model.api.rdf.CdrAcl.markedForDeletion; -import static edu.unc.lib.boxc.model.api.sparql.SparqlUpdateHelper.createSparqlReplace; -import static edu.unc.lib.boxc.model.api.xml.JDOMNamespaceUtil.CDR_MESSAGE_NS; -import static edu.unc.lib.boxc.model.fcrepo.ids.RepositoryPaths.getContentRootPid; -import static edu.unc.lib.boxc.operations.jms.indexing.IndexingActionType.DELETE_SOLR_TREE; -import static java.util.Arrays.asList; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyString; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import static org.mockito.MockitoAnnotations.initMocks; - -import java.io.File; -import java.net.URI; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.apache.commons.io.FileUtils; -import org.apache.jena.rdf.model.Model; -import org.apache.jena.rdf.model.Resource; -import org.apache.jena.vocabulary.RDF; -import org.fcrepo.client.FcrepoClient; -import org.jdom2.Document; -import org.jdom2.Element; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; -import org.mockito.Mock; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.ContextHierarchy; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; - import edu.unc.lib.boxc.auth.api.models.AccessGroupSet; import edu.unc.lib.boxc.auth.api.models.AgentPrincipals; import edu.unc.lib.boxc.auth.api.services.AccessControlService; @@ -75,6 +24,7 @@ import edu.unc.lib.boxc.fcrepo.utils.TransactionManager; import edu.unc.lib.boxc.model.api.ResourceType; import edu.unc.lib.boxc.model.api.ids.PID; +import edu.unc.lib.boxc.model.api.ids.PIDMinter; import edu.unc.lib.boxc.model.api.objects.AdminUnit; import edu.unc.lib.boxc.model.api.objects.BinaryObject; import edu.unc.lib.boxc.model.api.objects.CollectionObject; @@ -104,6 +54,57 @@ import edu.unc.lib.boxc.persist.impl.storage.StorageLocationManagerImpl; import edu.unc.lib.boxc.search.api.models.ObjectPath; import edu.unc.lib.boxc.search.solr.services.ObjectPathFactory; +import org.apache.commons.io.FileUtils; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.RDFNode; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.vocabulary.RDF; +import org.fcrepo.client.FcrepoClient; +import org.jdom2.Document; +import org.jdom2.Element; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.io.File; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static edu.unc.lib.boxc.model.api.rdf.CdrAcl.markedForDeletion; +import static edu.unc.lib.boxc.model.api.sparql.SparqlUpdateHelper.createSparqlReplace; +import static edu.unc.lib.boxc.model.api.xml.JDOMNamespaceUtil.CDR_MESSAGE_NS; +import static edu.unc.lib.boxc.model.fcrepo.ids.RepositoryPaths.getContentRootPid; +import static edu.unc.lib.boxc.operations.jms.indexing.IndexingActionType.DELETE_SOLR_TREE; +import static java.util.Arrays.asList; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.initMocks; /** * @@ -161,6 +162,8 @@ public class DestroyObjectsJobIT { private StorageLocationManagerImpl locationManager; @Autowired private BinaryTransferService transferService; + @Autowired + private PIDMinter pidMinter; private AgentPrincipals agent; @@ -224,6 +227,59 @@ public void destroySingleFileObjectTest() throws Exception { assertMessagePresent(docCaptor.getAllValues(), filesToCleanup, null); } + @Test + public void destroyPrimaryObjectInMultiFileWorkTest() throws Exception { + PID fileObjPid = objsToDestroy.get(2); + var workObj = repoObjLoader.getWorkObject(objsToDestroy.get(1)); + workObj.setPrimaryObject(fileObjPid); + // Add a second file + var fileObj2 = addFileToWork(workObj); + treeIndexer.indexAll(baseAddress); + + initializeJob(asList(fileObjPid)); + + FileObject fileObj = repoObjLoader.getFileObject(fileObjPid); + Map> filesToCleanup = derivativesToCleanup(fileObj); + + URI contentUri = fileObj.getOriginalFile().getContentUri(); + assertTrue(Files.exists(Paths.get(contentUri))); + + job.run(); + + Model logParentModel = fileObj.getParent().getPremisLog().getEventsModel(); + assertTrue(logParentModel.contains(null, RDF.type, Premis.Deletion)); + assertTrue(logParentModel.contains(null, Premis.note, + "1 object(s) were destroyed")); + + Tombstone stoneFile = repoObjLoader.getTombstone(fileObjPid); + Resource stoneResc = stoneFile.getResource(); + assertTrue(stoneResc.hasProperty(RDF.type, Cdr.Tombstone)); + assertTrue(stoneResc.hasProperty(RDF.type, Cdr.FileObject)); + // check to make sure metadata from binary was retained by file obj's tombstone + assertTrue(stoneResc.hasProperty(Ebucore.filename)); + assertTrue(stoneResc.hasProperty(Cdr.hasMessageDigest)); + assertTrue(stoneResc.hasProperty(Ebucore.hasMimeType)); + assertTrue(stoneResc.hasProperty(Cdr.hasSize)); + + assertFalse("Original file must be deleted", Files.exists(Paths.get(contentUri))); + + verify(indexingMessageSender).sendIndexingOperation(anyString(), eq(fileObjPid), eq(DELETE_SOLR_TREE)); + + verify(binaryDestroyedMessageSender).sendMessage(docCaptor.capture()); + + assertMessagePresent(docCaptor.getAllValues(), filesToCleanup, null); + + workObj.shouldRefresh(); + + assertNull("Primary object of work must now be null", workObj.getPrimaryObject()); + var members = workObj.getMembers(); + assertTrue("Second file must still be present", members.stream() + .anyMatch(m -> m.getPid().equals(fileObj2.getPid()))); + assertEquals(2, members.size()); + assertFalse("Primary object must be unset", + workObj.getModel().contains(null, Cdr.primaryObject, (RDFNode) null)); + } + @Test public void destroyObjectsInSameTreeTest() throws Exception { //remove unrelated folder obj before running job @@ -383,15 +439,7 @@ private List createContentTree() throws Exception { collection.addMember(folder2); WorkObject work = repoObjFactory.createWorkObject(null); folder.addMember(work); - String bodyString = "Content"; - String mimetype = "text/plain"; - Path storagePath = Paths.get(locationManager.getStorageLocationById(LOC1_ID).getNewStorageUri(work.getPid())); - Files.createDirectories(storagePath); - File contentFile = Files.createTempFile(storagePath, "file", ".txt").toFile(); - String sha1 = "4f9be057f0ea5d2ba72fd2c810e8d7b9aa98b469"; - String filename = contentFile.getName(); - FileUtils.writeStringToFile(contentFile, bodyString, "UTF-8"); - FileObject file = work.addDataFile(contentFile.toPath().toUri(), filename, mimetype, sha1, null); + var file = addFileToWork(work); treeIndexer.indexAll(baseAddress); @@ -404,6 +452,20 @@ private List createContentTree() throws Exception { return objsToDestroy; } + private FileObject addFileToWork(WorkObject workObj) throws Exception { + String bodyString = "Content"; + String mimetype = "text/plain"; + PID filePid = pidMinter.mintContentPid(); + Path storagePath = Paths.get(locationManager.getStorageLocationById(LOC1_ID) + .getNewStorageUri(filePid)); + Files.createDirectories(storagePath); + File contentFile = Files.createTempFile(storagePath, "file", ".txt").toFile(); + String sha1 = "4f9be057f0ea5d2ba72fd2c810e8d7b9aa98b469"; + String filename = contentFile.getName(); + FileUtils.writeStringToFile(contentFile, bodyString, "UTF-8"); + return workObj.addDataFile(filePid, contentFile.toPath().toUri(), filename, mimetype, sha1, null, null); + } + private void initializeJob(List objsToDestroy) { DestroyObjectsRequest request = new DestroyObjectsRequest("jobid", agent, objsToDestroy.stream().map(PID::getId).toArray(String[]::new)); From 8159ac06b94ee47f2108a8214f384d993886e5ae Mon Sep 17 00:00:00 2001 From: Ben Pennell Date: Fri, 10 Jun 2022 17:26:27 -0400 Subject: [PATCH 3/3] Remove import --- .../lib/boxc/model/fcrepo/services/RepositoryObjectDriver.java | 1 - 1 file changed, 1 deletion(-) diff --git a/model-fcrepo/src/main/java/edu/unc/lib/boxc/model/fcrepo/services/RepositoryObjectDriver.java b/model-fcrepo/src/main/java/edu/unc/lib/boxc/model/fcrepo/services/RepositoryObjectDriver.java index dac36aba7e..e9faca8789 100644 --- a/model-fcrepo/src/main/java/edu/unc/lib/boxc/model/fcrepo/services/RepositoryObjectDriver.java +++ b/model-fcrepo/src/main/java/edu/unc/lib/boxc/model/fcrepo/services/RepositoryObjectDriver.java @@ -23,7 +23,6 @@ import java.util.ArrayList; import java.util.List; -import edu.unc.lib.boxc.model.api.exceptions.NotFoundException; import edu.unc.lib.boxc.model.api.exceptions.TombstoneFoundException; import edu.unc.lib.boxc.model.api.objects.Tombstone; import org.apache.http.HttpStatus;