diff --git a/config/alfresco/ibatis/org.hibernate.dialect.Dialect/content-common-SqlMap.xml b/config/alfresco/ibatis/org.hibernate.dialect.Dialect/content-common-SqlMap.xml index 2c411afdbc..833788de48 100644 --- a/config/alfresco/ibatis/org.hibernate.dialect.Dialect/content-common-SqlMap.xml +++ b/config/alfresco/ibatis/org.hibernate.dialect.Dialect/content-common-SqlMap.xml @@ -271,6 +271,21 @@ cd.id is null + + + update diff --git a/source/java/org/alfresco/repo/content/cleanup/EagerContentStoreCleaner.java b/source/java/org/alfresco/repo/content/cleanup/EagerContentStoreCleaner.java index 98de6072ff..121c0e8ee0 100644 --- a/source/java/org/alfresco/repo/content/cleanup/EagerContentStoreCleaner.java +++ b/source/java/org/alfresco/repo/content/cleanup/EagerContentStoreCleaner.java @@ -266,11 +266,17 @@ public class EagerContentStoreCleaner extends TransactionListenerAdapter int deleted = 0; for (ContentStore store : stores) { - // Bypass if the store is read-only or doesn't support the URL - if (!store.isWriteSupported() || !store.isContentUrlSupported(contentUrl)) + // Bypass if the store is read-only + if (!store.isWriteSupported()) { continue; } + // MNT-12150 fix, bypass if the store doesn't support the URL but mark as deleted + if (!store.isContentUrlSupported(contentUrl)) + { + deleted++; + continue; + } if (callListeners) { // Call listeners diff --git a/source/java/org/alfresco/repo/domain/contentdata/ContentDataDAO.java b/source/java/org/alfresco/repo/domain/contentdata/ContentDataDAO.java index bed77cccf9..f73281526a 100644 --- a/source/java/org/alfresco/repo/domain/contentdata/ContentDataDAO.java +++ b/source/java/org/alfresco/repo/domain/contentdata/ContentDataDAO.java @@ -108,13 +108,22 @@ public interface ContentDataDAO * @param contentUrlHandler the callback object to process the rows * @param maxOrphanTimeExclusive the maximum orphan time (exclusive) * @param maxResults the maximum number of results (1 or greater) - * @return Returns a list of orphaned content URLs ordered by ID */ void getContentUrlsOrphaned( ContentUrlHandler contentUrlHandler, Long maxOrphanTimeExclusive, int maxResults); + /** + * Enumerate all available content URLs that were orphaned and cleanup for these urls failed + * + * @param contentUrlHandler the callback object to process the rows + * @param maxResults the maximum number of results (1 or greater) + */ + void getContentUrlsKeepOrphaned( + ContentUrlHandler contentUrlHandler, + int maxResults); + /** * Delete a batch of content URL entities. */ diff --git a/source/java/org/alfresco/repo/domain/contentdata/ibatis/ContentDataDAOImpl.java b/source/java/org/alfresco/repo/domain/contentdata/ibatis/ContentDataDAOImpl.java index 06695efea3..80426140be 100644 --- a/source/java/org/alfresco/repo/domain/contentdata/ibatis/ContentDataDAOImpl.java +++ b/source/java/org/alfresco/repo/domain/contentdata/ibatis/ContentDataDAOImpl.java @@ -57,6 +57,7 @@ public class ContentDataDAOImpl extends AbstractContentDataDAOImpl private static final String SELECT_CONTENT_URL_BY_KEY = "alfresco.content.select_ContentUrlByKey"; private static final String SELECT_CONTENT_URL_BY_KEY_UNREFERENCED = "alfresco.content.select_ContentUrlByKeyUnreferenced"; private static final String SELECT_CONTENT_URLS_ORPHANED = "alfresco.content.select.select_ContentUrlsOrphaned"; + private static final String SELECT_CONTENT_URLS_KEEP_ORPHANED = "alfresco.content.select_ContentUrlsKeepOrphaned"; private static final String SELECT_CONTENT_DATA_BY_ID = "alfresco.content.select_ContentDataById"; private static final String SELECT_CONTENT_DATA_BY_NODE_AND_QNAME = "alfresco.content.select_ContentDataByNodeAndQName"; private static final String SELECT_CONTENT_DATA_BY_NODE_IDS = "alfresco.content.select_ContentDataByNodeIds"; @@ -160,6 +161,23 @@ public class ContentDataDAOImpl extends AbstractContentDataDAOImpl } } + @SuppressWarnings("unchecked") + public void getContentUrlsKeepOrphaned( + final ContentUrlHandler contentUrlHandler, + final int maxResults) + { + List results = (List) template.selectList(SELECT_CONTENT_URLS_KEEP_ORPHANED, + new RowBounds(0, maxResults)); + // Pass the result to the callback + for (ContentUrlEntity result : results) + { + contentUrlHandler.handle( + result.getId(), + result.getContentUrl(), + result.getOrphanTime()); + } + } + public int updateContentUrlOrphanTime(Long id, Long orphanTime, Long oldOrphanTime) { ContentUrlUpdateEntity contentUrlUpdateEntity = new ContentUrlUpdateEntity(); diff --git a/source/test-java/org/alfresco/repo/content/cleanup/ContentStoreCleanerTest.java b/source/test-java/org/alfresco/repo/content/cleanup/ContentStoreCleanerTest.java index b2e5b6bcf7..77273eebf1 100644 --- a/source/test-java/org/alfresco/repo/content/cleanup/ContentStoreCleanerTest.java +++ b/source/test-java/org/alfresco/repo/content/cleanup/ContentStoreCleanerTest.java @@ -26,6 +26,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.TreeMap; import junit.framework.TestCase; @@ -33,7 +34,10 @@ import org.alfresco.model.ContentModel; import org.alfresco.repo.content.ContentStore; import org.alfresco.repo.content.MimetypeMap; import org.alfresco.repo.content.UnsupportedContentUrlException; +import org.alfresco.repo.content.cleanup.ContentStoreCleaner.DeleteFailureAction; +import org.alfresco.repo.content.filestore.FileContentStore; import org.alfresco.repo.domain.contentdata.ContentDataDAO; +import org.alfresco.repo.domain.contentdata.ContentDataDAO.ContentUrlHandler; import org.alfresco.repo.lock.JobLockService; import org.alfresco.repo.security.authentication.AuthenticationUtil; import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; @@ -74,6 +78,7 @@ public class ContentStoreCleanerTest extends TestCase private ContentStore store; private ContentStoreCleanerListener listener; private List deletedUrls; + private ContentDataDAO contentDataDAO; @Override public void setUp() throws Exception @@ -87,7 +92,7 @@ public class ContentStoreCleanerTest extends TestCase jobLockService = serviceRegistry.getJobLockService(); TransactionService transactionService = serviceRegistry.getTransactionService(); DictionaryService dictionaryService = serviceRegistry.getDictionaryService(); - ContentDataDAO contentDataDAO = (ContentDataDAO) ctx.getBean("contentDataDAO"); + contentDataDAO = (ContentDataDAO) ctx.getBean("contentDataDAO"); // we need a store store = (ContentStore) ctx.getBean("fileContentStore"); @@ -506,6 +511,96 @@ public class ContentStoreCleanerTest extends TestCase cleaner.execute(); } + public void testMNT_12150() + { + eagerCleaner.setEagerOrphanCleanup(false); + + // create content with binary data and delete it in the same transaction + final StoreRef storeRef = nodeService.createStore("test", getName() + "-" + GUID.generate()); + RetryingTransactionCallback prepareCallback = new RetryingTransactionCallback() + { + public ContentData execute() throws Throwable + { + // Create some content + NodeRef rootNodeRef = nodeService.getRootNode(storeRef); + Map properties = new HashMap(13); + properties.put(ContentModel.PROP_NAME, (Serializable)"test.txt"); + NodeRef contentNodeRef = nodeService.createNode( + rootNodeRef, + ContentModel.ASSOC_CHILDREN, + ContentModel.ASSOC_CHILDREN, + ContentModel.TYPE_CONTENT, + properties).getChildRef(); + ContentWriter writer = contentService.getWriter(contentNodeRef, ContentModel.PROP_CONTENT, true); + writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + writer.putContent("INITIAL CONTENT"); + ContentData contentData = writer.getContentData(); + + // Delete the first node, bypassing archive + nodeService.addAspect(contentNodeRef, ContentModel.ASPECT_TEMPORARY, null); + nodeService.deleteNode(contentNodeRef); + + // Done + return contentData; + } + }; + ContentData contentData = transactionService.getRetryingTransactionHelper().doInTransaction(prepareCallback); + + List stores = new ArrayList(2); + stores.add(store); + + // add another store which doesn't support any content url format + stores.add(new FileContentStore(ctx, store.getRootLocation()) + { + @Override + public boolean isContentUrlSupported(String contentUrl) + { + return false; + } + }); + + // configure cleaner to keep failed orphaned content urls + eagerCleaner.setStores(stores); + eagerCleaner.setListeners(Collections.emptyList()); + cleaner.setProtectDays(0); + cleaner.setDeletionFailureAction(DeleteFailureAction.KEEP_URL); + + // fire the cleaner + cleaner.execute(); + + // Go through the orphaned urls again + final TreeMap keptUrlsById = new TreeMap(); + final ContentUrlHandler contentUrlHandler = new ContentUrlHandler() + { + @Override + public void handle(Long id, String contentUrl, Long orphanTime) + { + keptUrlsById.put(id, contentUrl); + } + }; + + // look for any kept urls in database + RetryingTransactionCallback testCallback = new RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + contentDataDAO.getContentUrlsKeepOrphaned(contentUrlHandler, 1000); + return null; + } + }; + + transactionService.getRetryingTransactionHelper().doInTransaction(testCallback); + + // check that orphaned url was deleted + for (String url : keptUrlsById.values()) + { + if (url.equalsIgnoreCase(contentData.getContentUrl())) + { + fail("Failed to cleanup orphaned content: " + contentData.getContentUrl()); + } + } + } + private class DummyCleanerListener implements ContentStoreCleanerListener { public void beforeDelete(ContentStore store, String contentUrl) throws ContentIOException