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