urlsToDelete = TransactionalResourceHelper.getSet(KEY_POST_ROLLBACK_DELETION_URLS);
urlsToDelete.add(contentUrl);
// Register to listen for transaction rollback
@@ -158,7 +154,18 @@ public class EagerContentStoreCleaner extends TransactionListenerAdapter
*/
public void registerOrphanedContentUrl(String contentUrl)
{
- if (!eagerOrphanCleanup)
+ registerOrphanedContentUrl(contentUrl, false);
+ }
+
+ /**
+ * Queues orphaned content for post-transaction removal
+ *
+ * @param force true for force the post-commit URL deletion
+ * regardless of the setting {@link #setEagerOrphanCleanup(boolean)}.
+ */
+ public void registerOrphanedContentUrl(String contentUrl, boolean force)
+ {
+ if (!eagerOrphanCleanup && !force)
{
return;
}
diff --git a/source/java/org/alfresco/repo/domain/contentclean/ContentCleanDAO.java b/source/java/org/alfresco/repo/domain/contentclean/ContentCleanDAO.java
deleted file mode 100644
index a92ace1e36..0000000000
--- a/source/java/org/alfresco/repo/domain/contentclean/ContentCleanDAO.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright (C) 2005-2009 Alfresco Software Limited.
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU General Public License
- * as published by the Free Software Foundation; either version 2
- * of the License, or (at your option) any later version.
-
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
-
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
- * As a special exception to the terms and conditions of version 2.0 of
- * the GPL, you may redistribute this Program in connection with Free/Libre
- * and Open Source Software ("FLOSS") applications as described in Alfresco's
- * FLOSS exception. You should have recieved a copy of the text describing
- * the FLOSS exception, and it is also available here:
- * http://www.alfresco.com/legal/licensing"
- */
-package org.alfresco.repo.domain.contentclean;
-
-
-/**
- * DAO services for alf_contentclean table.
- * This DAO is geared towards bulk processing of content URLs.
- *
- * Content URLs are lowercased and CRC'ed
- *
- * @author Derek Hulley
- * @since 3.2
- */
-public interface ContentCleanDAO
-{
- /**
- * Interface callback for putting and getting content URL values
- *
- * @author Derek Hulley
- * @since 3.2
- */
- public interface ContentUrlBatchProcessor
- {
- void start();
- void processContentUrl(String contentUrl);
- void end();
- }
-
- void cleanUp();
-
- ContentUrlBatchProcessor getUrlInserter();
-
- ContentUrlBatchProcessor getUrlRemover();
-
- void listAllUrls(ContentUrlBatchProcessor batchProcessor);
-}
diff --git a/source/java/org/alfresco/repo/domain/contentclean/ContentCleanEntity.java b/source/java/org/alfresco/repo/domain/contentclean/ContentCleanEntity.java
deleted file mode 100644
index 2d42359376..0000000000
--- a/source/java/org/alfresco/repo/domain/contentclean/ContentCleanEntity.java
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * Copyright (C) 2005-2009 Alfresco Software Limited.
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU General Public License
- * as published by the Free Software Foundation; either version 2
- * of the License, or (at your option) any later version.
-
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
-
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
- * As a special exception to the terms and conditions of version 2.0 of
- * the GPL, you may redistribute this Program in connection with Free/Libre
- * and Open Source Software ("FLOSS") applications as described in Alfresco's
- * FLOSS exception. You should have recieved a copy of the text describing
- * the FLOSS exception, and it is also available here:
- * http://www.alfresco.com/legal/licensing"
- */
-package org.alfresco.repo.domain.contentclean;
-
-import org.alfresco.util.EqualsHelper;
-
-/**
- * Entity bean for alf_content_url table.
- *
- * These are unique (see {@link #equals(Object) equals} and {@link #hashCode() hashCode}) based
- * on the {@link #getContentUrl() content URL} value.
- *
- * @author Derek Hulley
- * @since 3.2
- */
-public class ContentCleanEntity
-{
- private String contentUrl;
-
- public ContentCleanEntity()
- {
- }
-
- @Override
- public int hashCode()
- {
- return (contentUrl == null ? 0 : contentUrl.hashCode());
- }
-
- @Override
- public boolean equals(Object obj)
- {
- if (this == obj)
- {
- return true;
- }
- else if (obj instanceof ContentCleanEntity)
- {
- ContentCleanEntity that = (ContentCleanEntity) obj;
- return EqualsHelper.nullSafeEquals(this.contentUrl, that.contentUrl);
- }
- else
- {
- return false;
- }
- }
-
- @Override
- public String toString()
- {
- StringBuilder sb = new StringBuilder(512);
- sb.append("ContentCleanEntity")
- .append("[ contentUrl=").append(contentUrl)
- .append("]");
- return sb.toString();
- }
-
- public String getContentUrl()
- {
- return contentUrl;
- }
-
- public void setContentUrl(String contentUrl)
- {
- this.contentUrl = contentUrl;
- }
-}
diff --git a/source/java/org/alfresco/repo/domain/contentclean/ibatis/ContentCleanDAOImpl.java b/source/java/org/alfresco/repo/domain/contentclean/ibatis/ContentCleanDAOImpl.java
deleted file mode 100644
index 44c9d1be62..0000000000
--- a/source/java/org/alfresco/repo/domain/contentclean/ibatis/ContentCleanDAOImpl.java
+++ /dev/null
@@ -1,254 +0,0 @@
-/*
- * Copyright (C) 2005-2009 Alfresco Software Limited.
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU General Public License
- * as published by the Free Software Foundation; either version 2
- * of the License, or (at your option) any later version.
-
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
-
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
- * As a special exception to the terms and conditions of version 2.0 of
- * the GPL, you may redistribute this Program in connection with Free/Libre
- * and Open Source Software ("FLOSS") applications as described in Alfresco's
- * FLOSS exception. You should have recieved a copy of the text describing
- * the FLOSS exception, and it is also available here:
- * http://www.alfresco.com/legal/licensing"
- */
-package org.alfresco.repo.domain.contentclean.ibatis;
-
-import java.sql.SQLException;
-
-import org.alfresco.error.AlfrescoRuntimeException;
-import org.alfresco.repo.domain.contentclean.ContentCleanDAO;
-import org.alfresco.repo.domain.contentclean.ContentCleanEntity;
-import org.apache.commons.logging.Log;
-import org.apache.commons.logging.LogFactory;
-import org.springframework.orm.ibatis.SqlMapClientTemplate;
-
-import com.ibatis.sqlmap.client.SqlMapClient;
-import com.ibatis.sqlmap.client.event.RowHandler;
-
-/**
- * iBatis-specific implementation of the Content Cleaner DAO.
- *
- * @author Derek Hulley
- * @since 3.2
- */
-public class ContentCleanDAOImpl implements ContentCleanDAO
-{
- private static Log logger = LogFactory.getLog(ContentCleanDAOImpl.class);
-
- private static final int DEFAULT_BATCH_SIZE = 50;
-
- private static final String INSERT_CONTENT_CLEAN = "alfresco.content.insert_ContentCleanUrl";
- private static final String SELECT_CONTENT_CLEAN_URLS = "alfresco.content.select_ContentCleanUrls";
- private static final String DELETE_CONTENT_CLEAN_BY_URL = "alfresco.content.delete_ContentCleanUrl";
- private static final String DELETE_CONTENT_CLEAN = "alfresco.content.delete_ContentCleanUrls";
-
- private SqlMapClientTemplate template;
-
- public void setSqlMapClientTemplate(SqlMapClientTemplate sqlMapClientTemplate)
- {
- this.template = sqlMapClientTemplate;
- }
-
- /**
- * {@inheritDoc}
- */
- public ContentUrlBatchProcessor getUrlInserter()
- {
- final SqlMapClient sqlMapClient = template.getSqlMapClient();
- ContentUrlBatchProcessor processor = new ContentUrlBatchProcessor()
- {
- private int count = 0;
- private int total = 0;
-
- public void start()
- {
- try
- {
- sqlMapClient.startBatch();
- count = 0;
- }
- catch (SQLException e)
- {
- // Batches not supported, so don't do batching
- count = -1;
- }
- }
- public void processContentUrl(String contentUrl)
- {
- ContentCleanEntity contentCleanEntity = new ContentCleanEntity();
- contentCleanEntity.setContentUrl(contentUrl == null ? null : contentUrl.toLowerCase());
- template.insert(INSERT_CONTENT_CLEAN, contentCleanEntity);
- // Write the batch
- executeBatch();
- total++;
- }
- public void end()
- {
- // Write the batch
- executeBatch();
- if (logger.isDebugEnabled())
- {
- logger.debug(" Inserted " + total + " content URLs (FINISHED)");
- }
- }
- private void executeBatch()
- {
- // Are we batching?
- if (count > -1)
- {
- // Write the batch, if required
- if (++count >= DEFAULT_BATCH_SIZE)
- {
- try
- {
- sqlMapClient.executeBatch();
- sqlMapClient.startBatch();
- }
- catch (SQLException e)
- {
- throw new AlfrescoRuntimeException("Failed to execute batch", e);
- }
- count = 0;
- }
- }
- if (logger.isDebugEnabled() && (total == 0 || (total % 1000 == 0) ))
- {
- logger.debug(" Inserted " + total + " content URLs");
- }
- }
- };
- // Done
- return processor;
- }
-
- /**
- * {@inheritDoc}
- */
- public ContentUrlBatchProcessor getUrlRemover()
- {
- final SqlMapClient sqlMapClient = template.getSqlMapClient();
- ContentUrlBatchProcessor processor = new ContentUrlBatchProcessor()
- {
- private int count = 0;
- private int total = 0;
-
- public void start()
- {
- try
- {
- sqlMapClient.startBatch();
- count = 0;
- }
- catch (SQLException e)
- {
- // Batches not supported, so don't do batching
- count = -1;
- }
- }
- public void processContentUrl(String contentUrl)
- {
- ContentCleanEntity contentCleanEntity = new ContentCleanEntity();
- contentCleanEntity.setContentUrl(contentUrl);
- template.delete(DELETE_CONTENT_CLEAN_BY_URL, contentCleanEntity);
- // Write the batch
- executeBatch();
- total++;
- }
- public void end()
- {
- // Write the batch
- executeBatch();
- if (logger.isDebugEnabled())
- {
- logger.debug(" Removed " + total + " content URLs (FINISHED)");
- }
- }
- private void executeBatch()
- {
- // Are we batching?
- if (count > -1)
- {
- // Write the batch, if required
- if (++count >= DEFAULT_BATCH_SIZE)
- {
- try
- {
- sqlMapClient.executeBatch();
- sqlMapClient.startBatch();
- }
- catch (SQLException e)
- {
- throw new AlfrescoRuntimeException("Failed to execute batch", e);
- }
- count = 0;
- }
- }
- if (logger.isDebugEnabled() && (total == 0 || (total % 1000 == 0) ))
- {
- logger.debug(" Removed " + total + " content URLs");
- }
- }
- };
- // Done
- return processor;
- }
-
- /**
- * {@inheritDoc}
- */
- public void listAllUrls(ContentUrlBatchProcessor batchProcessor)
- {
- ListAllRowHandler rowHandler = new ListAllRowHandler(batchProcessor);
-
- batchProcessor.start();
- template.queryWithRowHandler(SELECT_CONTENT_CLEAN_URLS, rowHandler);
- batchProcessor.end();
- if (logger.isDebugEnabled())
- {
- logger.debug(" Listed " + rowHandler.total + " content URLs");
- }
- }
-
- /**
- * Row handler for listing all content clean URLs
- * @author Derek Hulley
- * @since 3.2
- */
- private static class ListAllRowHandler implements RowHandler
- {
- private final ContentUrlBatchProcessor batchProcessor;
- private int total = 0;
- private ListAllRowHandler(ContentUrlBatchProcessor batchProcessor)
- {
- this.batchProcessor = batchProcessor;
- }
- public void handleRow(Object valueObject)
- {
- batchProcessor.processContentUrl((String)valueObject);
- total++;
- if (logger.isDebugEnabled() && (total == 0 || (total % 1000 == 0) ))
- {
- logger.debug(" Listed " + total + " content URLs");
- }
- }
- }
-
- /**
- * {@inheritDoc}
- */
- public void cleanUp()
- {
- template.delete(DELETE_CONTENT_CLEAN);
- }
-}
diff --git a/source/java/org/alfresco/repo/domain/contentdata/AbstractContentDataDAOImpl.java b/source/java/org/alfresco/repo/domain/contentdata/AbstractContentDataDAOImpl.java
index 91169acb4d..b98a7c819b 100644
--- a/source/java/org/alfresco/repo/domain/contentdata/AbstractContentDataDAOImpl.java
+++ b/source/java/org/alfresco/repo/domain/contentdata/AbstractContentDataDAOImpl.java
@@ -29,6 +29,8 @@ import java.util.Locale;
import java.util.Set;
import org.alfresco.repo.cache.SimpleCache;
+import org.alfresco.repo.cache.lookup.EntityLookupCache;
+import org.alfresco.repo.cache.lookup.EntityLookupCache.EntityLookupCallbackDAOAdaptor;
import org.alfresco.repo.content.cleanup.EagerContentStoreCleaner;
import org.alfresco.repo.domain.LocaleDAO;
import org.alfresco.repo.domain.encoding.EncodingDAO;
@@ -37,10 +39,12 @@ import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
import org.alfresco.repo.transaction.TransactionListenerAdapter;
import org.alfresco.repo.transaction.TransactionalResourceHelper;
import org.alfresco.service.cmr.repository.ContentData;
-import org.springframework.extensions.surf.util.Pair;
+import org.alfresco.util.EqualsHelper;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.dao.ConcurrencyFailureException;
+import org.springframework.dao.DataIntegrityViolationException;
+import org.springframework.extensions.surf.util.Pair;
/**
* Abstract implementation for ContentData DAO.
@@ -56,6 +60,7 @@ import org.springframework.dao.ConcurrencyFailureException;
*/
public abstract class AbstractContentDataDAOImpl implements ContentDataDAO
{
+ private static final String CACHE_REGION_CONTENT_DATA = "ContentData";
/**
* Content URL IDs to delete before final commit.
*/
@@ -63,12 +68,29 @@ public abstract class AbstractContentDataDAOImpl implements ContentDataDAO
private static Log logger = LogFactory.getLog(AbstractContentDataDAOImpl.class);
+ private final ContentDataCallbackDAO contentDataCallbackDAO;
private MimetypeDAO mimetypeDAO;
private EncodingDAO encodingDAO;
private LocaleDAO localeDAO;
private EagerContentStoreCleaner contentStoreCleaner;
- private SimpleCache contentDataCache;
+ /**
+ * Cache for the ContentData class:
+ * KEY: ID
+ * VALUE: ContentData object
+ * VALUE KEY: NONE
+ */
+ private EntityLookupCache contentDataCache;
+
+ /**
+ * Default constructor
+ */
+ public AbstractContentDataDAOImpl()
+ {
+ this.contentDataCallbackDAO = new ContentDataCallbackDAO();
+ this.contentDataCache = new EntityLookupCache(contentDataCallbackDAO);
+ }
+
public void setMimetypeDAO(MimetypeDAO mimetypeDAO)
{
this.mimetypeDAO = mimetypeDAO;
@@ -97,9 +119,12 @@ public abstract class AbstractContentDataDAOImpl implements ContentDataDAO
/**
* @param contentDataCache the cache of IDs to ContentData and vice versa
*/
- public void setContentDataCache(SimpleCache contentDataCache)
+ public void setContentDataCache(SimpleCache contentDataCache)
{
- this.contentDataCache = contentDataCache;
+ this.contentDataCache = new EntityLookupCache(
+ contentDataCache,
+ CACHE_REGION_CONTENT_DATA,
+ contentDataCallbackDAO);
}
/**
@@ -114,7 +139,7 @@ public abstract class AbstractContentDataDAOImpl implements ContentDataDAO
* A content_url entity was dereferenced. This makes no assumptions about the
* current references - dereference deletion is handled in the commit phase.
*/
- protected void registerDereferenceContentUrl(String contentUrl)
+ protected void registerDereferencedContentUrl(String contentUrl)
{
Set contentUrls = TransactionalResourceHelper.getSet(KEY_PRE_COMMIT_CONTENT_URL_DELETIONS);
if (contentUrls.size() == 0)
@@ -130,12 +155,12 @@ public abstract class AbstractContentDataDAOImpl implements ContentDataDAO
*/
public Pair createContentData(ContentData contentData)
{
- /*
- * TODO: Cache
- */
- ContentDataEntity contentDataEntity = createContentDataEntity(contentData);
- // Done
- return new Pair(contentDataEntity.getId(), contentData);
+ if (contentData == null)
+ {
+ throw new IllegalArgumentException("ContentData values cannot be null");
+ }
+ Pair entityPair = contentDataCache.getOrCreateByValue(contentData);
+ return entityPair;
}
/**
@@ -143,34 +168,98 @@ public abstract class AbstractContentDataDAOImpl implements ContentDataDAO
*/
public Pair getContentData(Long id)
{
- /*
- * TODO: Cache
- */
- ContentDataEntity contentDataEntity = getContentDataEntity(id);
- if (contentDataEntity == null)
+ if (id == null)
{
- return null;
+ throw new IllegalArgumentException("Cannot look up ContentData by null ID.");
}
- // Convert back to ContentData
- ContentData contentData = makeContentData(contentDataEntity);
- // Done
- return new Pair(id, contentData);
+ Pair entityPair = contentDataCache.getByKey(id);
+ if (entityPair == null)
+ {
+ throw new DataIntegrityViolationException("No ContentData value exists for ID " + id);
+ }
+ return entityPair;
}
/**
* {@inheritDoc}
*/
+ public void updateContentData(Long id, ContentData contentData)
+ {
+ if (id == null)
+ {
+ throw new IllegalArgumentException("Cannot look up ContentData by null ID.");
+ }
+ if (contentData == null)
+ {
+ throw new IllegalArgumentException("Cannot update ContentData with a null.");
+ }
+ int updated = contentDataCache.updateValue(id, contentData);
+ if (updated < 1)
+ {
+ throw new ConcurrencyFailureException("ContentData with ID " + id + " not updated");
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
public void deleteContentData(Long id)
{
- int deleted = deleteContentDataEntity(id);
+ if (id == null)
+ {
+ throw new IllegalArgumentException("Cannot delete ContentData by null ID.");
+ }
+ int deleted = contentDataCache.deleteByKey(id);
if (deleted < 1)
{
- throw new ConcurrencyFailureException("ContetntData with ID " + id + " no longer exists");
+ throw new ConcurrencyFailureException("ContentData with ID " + id + " no longer exists");
}
return;
}
/**
+ * Callback for alf_content_data DAO.
+ */
+ private class ContentDataCallbackDAO extends EntityLookupCallbackDAOAdaptor
+ {
+ public Pair createValue(ContentData value)
+ {
+ ContentDataEntity contentDataEntity = createContentDataEntity(value);
+ // Done
+ return new Pair(contentDataEntity.getId(), value);
+ }
+
+ public Pair findByKey(Long key)
+ {
+ ContentDataEntity contentDataEntity = getContentDataEntity(key);
+ if (contentDataEntity == null)
+ {
+ return null;
+ }
+ ContentData contentData = makeContentData(contentDataEntity);
+ // Done
+ return new Pair(key, contentData);
+ }
+
+ @Override
+ public int updateValue(Long key, ContentData value)
+ {
+ ContentDataEntity contentDataEntity = getContentDataEntity(key);
+ if (contentDataEntity == null)
+ {
+ return 0; // The client (outer-level code) will decide if this is an error
+ }
+ return updateContentDataEntity(contentDataEntity, value);
+ }
+
+ @Override
+ public int deleteByKey(Long key)
+ {
+ return deleteContentDataEntity(key);
+ }
+ }
+
+ /**
* Translates this instance into an externally-usable ContentData
instance.
*/
private ContentData makeContentData(ContentDataEntity contentDataEntity)
@@ -248,13 +337,66 @@ public abstract class AbstractContentDataDAOImpl implements ContentDataDAO
}
/**
+ * Translates the {@link ContentData} into persistable values using the helper DAOs
+ */
+ private int updateContentDataEntity(ContentDataEntity contentDataEntity, ContentData contentData)
+ {
+ // Resolve the content URL
+ String oldContentUrl = contentDataEntity.getContentUrl();
+ String newContentUrl = contentData.getContentUrl();
+ if (!EqualsHelper.nullSafeEquals(oldContentUrl, newContentUrl))
+ {
+ if (oldContentUrl != null)
+ {
+ // We have a changed value. The old content URL has been dereferenced.
+ registerDereferencedContentUrl(oldContentUrl);
+ }
+ if (newContentUrl != null)
+ {
+ Long contentUrlId = getOrCreateContentUrlEntity(newContentUrl, contentData.getSize()).getId();
+ contentDataEntity.setContentUrlId(contentUrlId);
+ contentDataEntity.setContentUrl(newContentUrl);
+ }
+ else
+ {
+ contentDataEntity.setContentUrlId(null);
+ contentDataEntity.setContentUrl(null);
+ }
+ }
+ // Resolve the mimetype
+ Long mimetypeId = null;
+ String mimetype = contentData.getMimetype();
+ if (mimetype != null)
+ {
+ mimetypeId = mimetypeDAO.getOrCreateMimetype(mimetype).getFirst();
+ }
+ // Resolve the encoding
+ Long encodingId = null;
+ String encoding = contentData.getEncoding();
+ if (encoding != null)
+ {
+ encodingId = encodingDAO.getOrCreateEncoding(encoding).getFirst();
+ }
+ // Resolve the locale
+ Long localeId = null;
+ Locale locale = contentData.getLocale();
+ if (locale != null)
+ {
+ localeId = localeDAO.getOrCreateLocalePair(locale).getFirst();
+ }
+
+ contentDataEntity.setMimetypeId(mimetypeId);
+ contentDataEntity.setEncodingId(encodingId);
+ contentDataEntity.setLocaleId(localeId);
+
+ return updateContentDataEntity(contentDataEntity);
+ }
+
+ /**
* Caching method that creates an entity for content_url_entity.
*/
private ContentUrlEntity getOrCreateContentUrlEntity(String contentUrl, long size)
{
- /*
- * TODO: Check for cache requirements
- */
// Create the content URL entity
ContentUrlEntity contentUrlEntity = getContentUrlEntity(contentUrl);
// If it exists, then we can just re-use it, but check that the size is consistent
@@ -304,10 +446,13 @@ public abstract class AbstractContentDataDAOImpl implements ContentDataDAO
protected abstract ContentUrlEntity getContentUrlEntityUnreferenced(String contentUrl);
/**
- * Delete the entity with the given ID
- * @return Returns the number of rows deleted
+ * Update a content URL with the given orphan time
+ *
+ * @param id the unique ID of the entity
+ * @param orphanTime the time (ms since epoch) that the entity was orphaned
+ * @return Returns the number of rows updated
*/
- protected abstract int deleteContentUrlEntity(Long id);
+ protected abstract int updateContentUrlOrphanTime(Long id, long orphanTime);
/**
* Create the row for the alf_content_data
@@ -325,6 +470,14 @@ public abstract class AbstractContentDataDAOImpl implements ContentDataDAO
protected abstract ContentDataEntity getContentDataEntity(Long id);
/**
+ * Update an existing alf_content_data entity
+ *
+ * @param entity the existing entity that will be updated
+ * @return Returns the number of rows updated (should be 1)
+ */
+ protected abstract int updateContentDataEntity(ContentDataEntity entity);
+
+ /**
* Delete the entity with the given ID
*
* @return Returns the number of rows deleted
@@ -347,6 +500,7 @@ public abstract class AbstractContentDataDAOImpl implements ContentDataDAO
return;
}
Set contentUrls = TransactionalResourceHelper.getSet(KEY_PRE_COMMIT_CONTENT_URL_DELETIONS);
+ long orphanTime = System.currentTimeMillis();
for (String contentUrl : contentUrls)
{
ContentUrlEntity contentUrlEntity = getContentUrlEntityUnreferenced(contentUrl);
@@ -355,9 +509,9 @@ public abstract class AbstractContentDataDAOImpl implements ContentDataDAO
// It is still referenced, so ignore it
continue;
}
- // It needs to be deleted
+ // We mark the URL as orphaned.
Long contentUrlId = contentUrlEntity.getId();
- deleteContentUrlEntity(contentUrlId);
+ updateContentUrlOrphanTime(contentUrlId, orphanTime);
// Pop this in the queue for deletion from the content store
contentStoreCleaner.registerOrphanedContentUrl(contentUrl);
}
diff --git a/source/java/org/alfresco/repo/domain/contentdata/ContentDataDAO.java b/source/java/org/alfresco/repo/domain/contentdata/ContentDataDAO.java
index 13fb34747d..0e55f6ca6d 100644
--- a/source/java/org/alfresco/repo/domain/contentdata/ContentDataDAO.java
+++ b/source/java/org/alfresco/repo/domain/contentdata/ContentDataDAO.java
@@ -24,6 +24,7 @@
*/
package org.alfresco.repo.domain.contentdata;
+import java.util.List;
import java.util.Set;
import org.alfresco.error.AlfrescoRuntimeException;
@@ -46,6 +47,14 @@ public interface ContentDataDAO
* @return the ContentData pair (id, ContentData) (never null)
*/
Pair createContentData(ContentData contentData);
+
+ /**
+ * Update a content data instance
+ *
+ * @param id the unique ID of the entity
+ * @param contentData the new data
+ */
+ void updateContentData(Long id, ContentData contentData);
/**
* @param id the unique ID of the entity
@@ -77,13 +86,28 @@ public interface ContentDataDAO
*/
public static interface ContentUrlHandler
{
- void handle(String contentUrl);
+ void handle(Long id, String contentUrl, Long orphanTime);
}
/**
- * Enumerate all available content URLs
+ * Enumerate all available content URLs that were orphaned on or before the given time
*
- * @param contentUrlHandler
+ * @param contentUrlHandler the callback object to process the rows
+ * @param maxOrphanTime the maximum orphan time
*/
- void getAllContentUrls(ContentUrlHandler contentUrlHandler);
+ void getContentUrlsOrphaned(ContentUrlHandler contentUrlHandler, long maxOrphanTime);
+
+ /**
+ * Enumerate all available content URLs that were orphaned on or before the given time
+ *
+ * @param contentUrlHandler the callback object to process the rows
+ * @param maxOrphanTime the maximum orphan time
+ * @param maxResults the maximum number of results (1 or greater)
+ */
+ void getContentUrlsOrphaned(ContentUrlHandler contentUrlHandler, long maxOrphanTime, int maxResults);
+
+ /**
+ * Delete a batch of content URL entities.
+ */
+ int deleteContentUrls(List ids);
}
diff --git a/source/java/org/alfresco/repo/domain/contentdata/ContentDataDAOTest.java b/source/java/org/alfresco/repo/domain/contentdata/ContentDataDAOTest.java
index 6ecd2058b6..82242c5cd7 100644
--- a/source/java/org/alfresco/repo/domain/contentdata/ContentDataDAOTest.java
+++ b/source/java/org/alfresco/repo/domain/contentdata/ContentDataDAOTest.java
@@ -34,6 +34,7 @@ import org.alfresco.repo.content.ContentContext;
import org.alfresco.repo.content.ContentStore;
import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.repo.content.filestore.FileContentStore;
+import org.alfresco.repo.domain.contentdata.ContentDataDAO.ContentUrlHandler;
import org.alfresco.repo.transaction.RetryingTransactionHelper;
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
import org.alfresco.service.ServiceRegistry;
@@ -44,6 +45,7 @@ import org.springframework.extensions.surf.util.Pair;
import org.alfresco.util.TempFileProvider;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.dao.DataIntegrityViolationException;
/**
* @see ContentDataDAO
@@ -85,6 +87,32 @@ public class ContentDataDAOTest extends TestCase
return txnHelper.doInTransaction(callback, false, false);
}
+ private Pair update(final Long id, final ContentData contentData)
+ {
+ RetryingTransactionCallback> callback = new RetryingTransactionCallback>()
+ {
+ public Pair execute() throws Throwable
+ {
+ contentDataDAO.updateContentData(id, contentData);
+ return new Pair(id, contentData);
+ }
+ };
+ return txnHelper.doInTransaction(callback, false, false);
+ }
+
+ private void delete(final Long id)
+ {
+ RetryingTransactionCallback callback = new RetryingTransactionCallback()
+ {
+ public Void execute() throws Throwable
+ {
+ contentDataDAO.deleteContentData(id);
+ return null;
+ }
+ };
+ txnHelper.doInTransaction(callback, false, false);
+ }
+
/**
* Retrieves and checks the ContentData for equality
*/
@@ -118,7 +146,15 @@ public class ContentDataDAOTest extends TestCase
public void testGetWithInvalidId()
{
- assertNull("Expected null for invalid ID", contentDataDAO.getContentData(-1L));
+ try
+ {
+ contentDataDAO.getContentData(-1L);
+ fail("Invalid ContentData IDs must generate DataIntegrityViolationException.");
+ }
+ catch (DataIntegrityViolationException e)
+ {
+ // Expected
+ }
}
/**
@@ -164,13 +200,27 @@ public class ContentDataDAOTest extends TestCase
getAndCheck(resultPairLower.getFirst(), contentDataLower);
}
+ public void testUpdate() throws Exception
+ {
+ ContentData contentData = getContentData();
+ Pair resultPair = create(contentData);
+ Long id = resultPair.getFirst();
+ // Update
+ contentData = ContentData.setMimetype(contentData, MimetypeMap.MIMETYPE_HTML);
+ contentData = ContentData.setEncoding(contentData, "UTF-16");
+ // Don't update the content itself
+ update(id, contentData);
+ // Check
+ getAndCheck(id, contentData);
+ }
+
public void testDelete() throws Exception
{
ContentData contentData = getContentData();
Pair resultPair = create(contentData);
getAndCheck(resultPair.getFirst(), contentData);
- contentDataDAO.deleteContentData(resultPair.getFirst());
+ delete(resultPair.getFirst());
try
{
getAndCheck(resultPair.getFirst(), contentData);
@@ -182,6 +232,66 @@ public class ContentDataDAOTest extends TestCase
}
}
+ public void testContentUrl_FetchingOrphansNoLimit() throws Exception
+ {
+ ContentData contentData = getContentData();
+ Pair resultPair = create(contentData);
+ getAndCheck(resultPair.getFirst(), contentData);
+ delete(resultPair.getFirst());
+ // The content URL is orphaned
+ final String contentUrlOrphaned = contentData.getContentUrl();
+ final boolean[] found = new boolean[] {false};
+
+ // Iterate over all orphaned content URLs and ensure that we hit the one we just orphaned
+ ContentUrlHandler handler = new ContentUrlHandler()
+ {
+ public void handle(Long id, String contentUrl, Long orphanTime)
+ {
+ // Check
+ if (id == null || contentUrl == null || orphanTime == null)
+ {
+ fail("Invalid orphan data returned to handler: " + id + "-" + contentUrl + "-" + orphanTime);
+ }
+ // Did we get the one we wanted?
+ if (contentUrl.equals(contentUrlOrphaned))
+ {
+ found[0] = true;
+ }
+ }
+ };
+ contentDataDAO.getContentUrlsOrphaned(handler, Long.MAX_VALUE);
+ assertTrue("Newly-orphaned content URL not found", found[0]);
+ }
+
+ public void testContentUrl_FetchingOrphansWithLimit() throws Exception
+ {
+ // Orphan some content
+ for (int i = 0; i < 5; i++)
+ {
+ ContentData contentData = getContentData();
+ Pair resultPair = create(contentData);
+ getAndCheck(resultPair.getFirst(), contentData);
+ delete(resultPair.getFirst());
+ }
+ final int[] count = new int[] {0};
+
+ // Iterate over all orphaned content URLs and ensure that we hit the one we just orphaned
+ ContentUrlHandler handler = new ContentUrlHandler()
+ {
+ public void handle(Long id, String contentUrl, Long orphanTime)
+ {
+ // Check
+ if (id == null || contentUrl == null || orphanTime == null)
+ {
+ fail("Invalid orphan data returned to handler: " + id + "-" + contentUrl + "-" + orphanTime);
+ }
+ count[0]++;
+ }
+ };
+ contentDataDAO.getContentUrlsOrphaned(handler, Long.MAX_VALUE, 5);
+ assertEquals("Expected exactly 5 results callbacks", 5, count[0]);
+ }
+
private static final String[] MIMETYPES = new String[]
{
MimetypeMap.MIMETYPE_ACP,
diff --git a/source/java/org/alfresco/repo/domain/contentdata/ContentDataEntity.java b/source/java/org/alfresco/repo/domain/contentdata/ContentDataEntity.java
index 61b2c7c637..1fd5fbf007 100644
--- a/source/java/org/alfresco/repo/domain/contentdata/ContentDataEntity.java
+++ b/source/java/org/alfresco/repo/domain/contentdata/ContentDataEntity.java
@@ -92,6 +92,18 @@ public class ContentDataEntity
return sb.toString();
}
+ public void incrementVersion()
+ {
+ if (version >= Short.MAX_VALUE)
+ {
+ this.version = 0L;
+ }
+ else
+ {
+ this.version++;
+ }
+ }
+
public Long getId()
{
return id;
diff --git a/source/java/org/alfresco/repo/domain/contentdata/ContentUrlEntity.java b/source/java/org/alfresco/repo/domain/contentdata/ContentUrlEntity.java
index 9d01029365..5286633208 100644
--- a/source/java/org/alfresco/repo/domain/contentdata/ContentUrlEntity.java
+++ b/source/java/org/alfresco/repo/domain/contentdata/ContentUrlEntity.java
@@ -43,11 +43,11 @@ public class ContentUrlEntity
public static final String EMPTY_URL = "empty";
private Long id;
- private Long version;
private String contentUrl;
private String contentUrlShort;
private long contentUrlCrc;
private long size;
+ private Long orphanTime;
public ContentUrlEntity()
{
@@ -86,6 +86,7 @@ public class ContentUrlEntity
.append("[ ID=").append(id)
.append(", contentUrl=").append(contentUrl)
.append(", size=").append(size)
+ .append(", orphanTime=").append(orphanTime)
.append("]");
return sb.toString();
}
@@ -129,16 +130,6 @@ public class ContentUrlEntity
this.id = id;
}
- public Long getVersion()
- {
- return version;
- }
-
- public void setVersion(Long version)
- {
- this.version = version;
- }
-
public String getContentUrl()
{
// Convert the persisted content URL to an external value
@@ -195,4 +186,14 @@ public class ContentUrlEntity
{
this.size = size;
}
+
+ public Long getOrphanTime()
+ {
+ return orphanTime;
+ }
+
+ public void setOrphanTime(Long orphanTime)
+ {
+ this.orphanTime = orphanTime;
+ }
}
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 b1c2928a96..71f2518fb2 100644
--- a/source/java/org/alfresco/repo/domain/contentdata/ibatis/ContentDataDAOImpl.java
+++ b/source/java/org/alfresco/repo/domain/contentdata/ibatis/ContentDataDAOImpl.java
@@ -47,13 +47,15 @@ public class ContentDataDAOImpl extends AbstractContentDataDAOImpl
private static final String SELECT_CONTENT_URL_BY_ID = "alfresco.content.select_ContentUrlById";
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 = "alfresco.content.select_ContentUrls";
+ private static final String SELECT_CONTENT_URLS_BY_ORPHAN_TIME = "alfresco.content.select_ContentUrlByOrphanTime";
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 INSERT_CONTENT_URL = "alfresco.content.insert_ContentUrl";
private static final String INSERT_CONTENT_DATA = "alfresco.content.insert_ContentData";
+ private static final String UPDATE_CONTENT_URL_ORPHAN_TIME = "alfresco.content.update_ContentUrlOrphanTime";
+ private static final String UPDATE_CONTENT_DATA = "alfresco.content.update_ContentData";
private static final String DELETE_CONTENT_DATA = "alfresco.content.delete_ContentData";
- private static final String DELETE_CONTENT_URL = "alfresco.content.delete_ContentUrl";
+ private static final String DELETE_CONTENT_URLS = "alfresco.content.delete_ContentUrls";
private SqlMapClientTemplate template;
@@ -66,9 +68,9 @@ public class ContentDataDAOImpl extends AbstractContentDataDAOImpl
protected ContentUrlEntity createContentUrlEntity(String contentUrl, long size)
{
ContentUrlEntity contentUrlEntity = new ContentUrlEntity();
- contentUrlEntity.setVersion(ContentUrlEntity.CONST_LONG_ZERO);
contentUrlEntity.setContentUrl(contentUrl);
contentUrlEntity.setSize(size);
+ contentUrlEntity.setOrphanTime(null);
/* Long id = (Long) */ template.insert(INSERT_CONTENT_URL, contentUrlEntity);
/*contentUrlEntity.setId(id);*/
// Register the url as new
@@ -101,12 +103,56 @@ public class ContentDataDAOImpl extends AbstractContentDataDAOImpl
return contentUrlEntity;
}
- @Override
- protected int deleteContentUrlEntity(Long id)
+ public void getContentUrlsOrphaned(final ContentUrlHandler contentUrlHandler, long maxOrphanTime)
{
- Map params = new HashMap(11);
- params.put("id", id);
- return template.delete(DELETE_CONTENT_URL, params);
+ RowHandler rowHandler = new RowHandler()
+ {
+ public void handleRow(Object valueObject)
+ {
+ ContentUrlEntity contentUrlEntity = (ContentUrlEntity) valueObject;
+ contentUrlHandler.handle(
+ contentUrlEntity.getId(),
+ contentUrlEntity.getContentUrl(),
+ contentUrlEntity.getOrphanTime());
+ }
+ };
+ ContentUrlEntity contentUrlEntity = new ContentUrlEntity();
+ contentUrlEntity.setOrphanTime(maxOrphanTime);
+ template.queryWithRowHandler(SELECT_CONTENT_URLS_BY_ORPHAN_TIME, contentUrlEntity, rowHandler);
+ }
+
+ @SuppressWarnings("unchecked")
+ public void getContentUrlsOrphaned(final ContentUrlHandler contentUrlHandler, long maxOrphanTime, int maxResults)
+ {
+ ContentUrlEntity contentUrlEntity = new ContentUrlEntity();
+ contentUrlEntity.setOrphanTime(maxOrphanTime);
+ List results = template.queryForList(
+ SELECT_CONTENT_URLS_BY_ORPHAN_TIME,
+ contentUrlEntity, 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)
+ {
+ ContentUrlEntity contentUrlEntity = new ContentUrlEntity();
+ contentUrlEntity.setId(id);
+ contentUrlEntity.setOrphanTime(orphanTime);
+ return template.update(UPDATE_CONTENT_URL_ORPHAN_TIME, contentUrlEntity);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public int deleteContentUrls(List ids)
+ {
+ return template.delete(DELETE_CONTENT_URLS, ids);
}
@Override
@@ -151,9 +197,30 @@ public class ContentDataDAOImpl extends AbstractContentDataDAOImpl
return contentDataEntity;
}
+ @Override
+ protected int updateContentDataEntity(ContentDataEntity entity)
+ {
+ entity.incrementVersion();
+ return template.update(UPDATE_CONTENT_DATA, entity);
+ }
+
@Override
protected int deleteContentDataEntity(Long id)
{
+ // Get the content urls
+ ContentDataEntity contentDataEntity = getContentDataEntity(id);
+ // This might be null as there is no constraint ensuring that the node points to a valid ContentData entity
+ if (contentDataEntity != null)
+ {
+ // Register the content URL for a later orphan-check
+ String contentUrl = contentDataEntity.getContentUrl();
+ if (contentUrl != null)
+ {
+ // It has been dereferenced and may be orphaned - we'll check later
+ registerDereferencedContentUrl(contentUrl);
+ }
+ }
+ // Issue the delete statement
Map params = new HashMap(11);
params.put("id", id);
return template.delete(DELETE_CONTENT_DATA, params);
@@ -175,36 +242,9 @@ public class ContentDataDAOImpl extends AbstractContentDataDAOImpl
// Delete each one
for (Long id : ids)
{
- // Get the content urls
- ContentDataEntity contentDataEntity = getContentDataEntity(id);
- // This might be null as there is no constraint ensuring that the node points to a valid ContentData entity
- if (contentDataEntity == null)
- {
- continue;
- }
- // Only check the content URLs if one is present
- String contentUrl = contentDataEntity.getContentUrl();
// Delete the ContentData entity
deleteContentData(id);
- // Check if the content URL was orphaned
- if (contentUrl != null)
- {
- // It has been dereferenced and may be orphaned - we'll check later
- registerDereferenceContentUrl(contentUrl);
- }
}
}
}
-
- public void getAllContentUrls(final ContentUrlHandler contentUrlHandler)
- {
- RowHandler rowHandler = new RowHandler()
- {
- public void handleRow(Object valueObject)
- {
- contentUrlHandler.handle((String)valueObject);
- }
- };
- template.queryWithRowHandler(SELECT_CONTENT_URLS, rowHandler);
- }
}
diff --git a/source/java/org/alfresco/repo/lock/JobLockServiceImpl.java b/source/java/org/alfresco/repo/lock/JobLockServiceImpl.java
index 285d000653..dc94ecac97 100644
--- a/source/java/org/alfresco/repo/lock/JobLockServiceImpl.java
+++ b/source/java/org/alfresco/repo/lock/JobLockServiceImpl.java
@@ -31,6 +31,7 @@ import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
import org.alfresco.repo.transaction.RetryingTransactionHelper;
import org.alfresco.repo.transaction.TransactionListenerAdapter;
import org.alfresco.repo.transaction.TransactionalResourceHelper;
+import org.alfresco.repo.transaction.AlfrescoTransactionSupport.TxnReadState;
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
import org.alfresco.service.namespace.QName;
import org.alfresco.util.GUID;
@@ -265,8 +266,11 @@ public class JobLockServiceImpl implements JobLockService
try
{
int iterations = doWithRetry(getLockCallback, retryWait, retryCount);
- // Bind in a listener
- AlfrescoTransactionSupport.bindListener(txnListener);
+ // Bind in a listener, if we are in a transaction
+ if (AlfrescoTransactionSupport.getTransactionReadState() != TxnReadState.TXN_NONE)
+ {
+ AlfrescoTransactionSupport.bindListener(txnListener);
+ }
// Success
if (logger.isDebugEnabled())
{
diff --git a/source/test-resources/tenant/mt-contentstore-context.xml b/source/test-resources/tenant/mt-contentstore-context.xml
index 1a9f934f0b..09b84df820 100644
--- a/source/test-resources/tenant/mt-contentstore-context.xml
+++ b/source/test-resources/tenant/mt-contentstore-context.xml
@@ -41,12 +41,18 @@
-
+
+
+ ${system.content.eagerOrphanCleanup}
+
+
+
+