diff --git a/config/alfresco/authentication-services-context.xml b/config/alfresco/authentication-services-context.xml index 56c557e81d..ffb1782e5e 100644 --- a/config/alfresco/authentication-services-context.xml +++ b/config/alfresco/authentication-services-context.xml @@ -281,6 +281,9 @@ + + + diff --git a/config/alfresco/bootstrap-context.xml b/config/alfresco/bootstrap-context.xml index e416e5f22d..7e6715a295 100644 --- a/config/alfresco/bootstrap-context.xml +++ b/config/alfresco/bootstrap-context.xml @@ -77,6 +77,7 @@ classpath:alfresco/dbscripts/create/2.2/${db.script.dialect}/post-create-indexes-04.sql classpath:alfresco/dbscripts/create/3.0/${db.script.dialect}/create-activities-extras.sql classpath:alfresco/dbscripts/create/3.2/${db.script.dialect}/AlfrescoPostCreate-3.2-LockTables.sql + classpath:alfresco/dbscripts/create/3.2/${db.script.dialect}/AlfrescoPostCreate-3.2-ContentTables.sql @@ -95,6 +96,7 @@ + diff --git a/config/alfresco/cache-context.xml b/config/alfresco/cache-context.xml index a5f06cf3f7..2fc1726379 100644 --- a/config/alfresco/cache-context.xml +++ b/config/alfresco/cache-context.xml @@ -135,6 +135,78 @@ + + + + + + + + + + + + + + org.alfresco.cache.mimetypeEntityCache + + + + + + + + + + + + + + + + org.alfresco.cache.mimetypeEntityTransactionalCache + + + 100 + + + + + + + + + + + + + + + + + org.alfresco.cache.encodingEntityCache + + + + + + + + + + + + + + + + org.alfresco.cache.encodingEntityTransactionalCache + + + 100 + + + diff --git a/config/alfresco/content-services-context.xml b/config/alfresco/content-services-context.xml index 82670e0520..95718ae244 100644 --- a/config/alfresco/content-services-context.xml +++ b/config/alfresco/content-services-context.xml @@ -34,12 +34,18 @@ + + + + + + + + + - - - @@ -49,15 +55,9 @@ - - - - - ${system.content.eagerOrphanCleanup} - 14 @@ -74,6 +74,20 @@ + + + ${system.content.eagerOrphanCleanup} + + + + + + + + + + + diff --git a/config/alfresco/dao/dao-context.xml b/config/alfresco/dao/dao-context.xml index 9b6462d8ad..f6eb59da1f 100644 --- a/config/alfresco/dao/dao-context.xml +++ b/config/alfresco/dao/dao-context.xml @@ -20,4 +20,27 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/alfresco/dbscripts/create/3.2/org.hibernate.dialect.DerbyDialect/AlfrescoPostCreate-3.2-ContentTables.sql b/config/alfresco/dbscripts/create/3.2/org.hibernate.dialect.DerbyDialect/AlfrescoPostCreate-3.2-ContentTables.sql new file mode 100644 index 0000000000..0af906a017 --- /dev/null +++ b/config/alfresco/dbscripts/create/3.2/org.hibernate.dialect.DerbyDialect/AlfrescoPostCreate-3.2-ContentTables.sql @@ -0,0 +1,74 @@ +-- +-- Title: Create Content tables +-- Database: Derby +-- Since: V3.2 Schema 2012 +-- Author: Derek Hulley +-- +-- Please contact support@alfresco.com if you need assistance with the upgrade. +-- + +CREATE TABLE alf_mimetype +( + id BIGINT NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1), + version BIGINT NOT NULL, + mimetype_str VARCHAR(100) NOT NULL, + PRIMARY KEY (id), + UNIQUE (mimetype_str) +); + +CREATE TABLE alf_encoding +( + id BIGINT NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1), + version BIGINT NOT NULL, + encoding_str VARCHAR(100) NOT NULL, + PRIMARY KEY (id), + UNIQUE (encoding_str) +); + +-- This table may exist during upgrades, but must be removed. +-- The drop statement is therefore optional. +DROP TABLE alf_content_url; --(optional) +CREATE TABLE alf_content_url +( + id BIGINT NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1), + version BIGINT NOT NULL, + content_url VARCHAR(255) NOT NULL, + content_url_short VARCHAR(12) NOT NULL, + content_url_crc BIGINT NOT NULL, + content_size BIGINT NOT NULL, + CONSTRAINT idx_alf_cont_url_crc UNIQUE (content_url_short, content_url_crc), + PRIMARY KEY (id) +); + +CREATE TABLE alf_content_data +( + id BIGINT NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1), + version BIGINT NOT NULL, + content_url_id BIGINT, + content_mimetype_id BIGINT, + content_encoding_id BIGINT, + content_locale_id BIGINT, + CONSTRAINT fk_alf_cont_url FOREIGN KEY (content_url_id) REFERENCES alf_content_url (id), + CONSTRAINT fk_alf_cont_mim FOREIGN KEY (content_mimetype_id) REFERENCES alf_mimetype (id), + CONSTRAINT fk_alf_cont_enc FOREIGN KEY (content_encoding_id) REFERENCES alf_encoding (id), + CONSTRAINT fk_alf_cont_loc FOREIGN KEY (content_locale_id) REFERENCES alf_locale (id), + PRIMARY KEY (id) +); + +CREATE TABLE alf_content_clean +( + content_url VARCHAR(255) NOT NULL +); +CREATE INDEX idx_alf_contentclean_url ON alf_content_clean(content_url); + +-- +-- Record script finish +-- +DELETE FROM alf_applied_patch WHERE id = 'patch.db-V3.2-ContentTables'; +INSERT INTO alf_applied_patch + (id, description, fixes_from_schema, fixes_to_schema, applied_to_schema, target_schema, applied_on_date, applied_to_server, was_executed, succeeded, report) + VALUES + ( + 'patch.db-V3.2-ContentTables', 'Manually executed script upgrade V3.2: Content Tables', + 0, 2011, -1, 2012, null, 'UNKOWN', 1, 1, 'Script completed' + ); \ No newline at end of file diff --git a/config/alfresco/dbscripts/create/3.2/org.hibernate.dialect.MySQLInnoDBDialect/AlfrescoPostCreate-3.2-ContentTables.sql b/config/alfresco/dbscripts/create/3.2/org.hibernate.dialect.MySQLInnoDBDialect/AlfrescoPostCreate-3.2-ContentTables.sql new file mode 100644 index 0000000000..8d5fe7b6b1 --- /dev/null +++ b/config/alfresco/dbscripts/create/3.2/org.hibernate.dialect.MySQLInnoDBDialect/AlfrescoPostCreate-3.2-ContentTables.sql @@ -0,0 +1,74 @@ +-- +-- Title: Create Content tables +-- Database: MySQL InnoDB +-- Since: V3.2 Schema 2012 +-- Author: Derek Hulley +-- +-- Please contact support@alfresco.com if you need assistance with the upgrade. +-- + +CREATE TABLE alf_mimetype +( + id BIGINT NOT NULL AUTO_INCREMENT, + version BIGINT NOT NULL, + mimetype_str VARCHAR(100) NOT NULL, + PRIMARY KEY (id), + UNIQUE (mimetype_str) +) ENGINE=InnoDB; + +CREATE TABLE alf_encoding +( + id BIGINT NOT NULL AUTO_INCREMENT, + version BIGINT NOT NULL, + encoding_str VARCHAR(100) NOT NULL, + PRIMARY KEY (id), + UNIQUE (encoding_str) +) ENGINE=InnoDB; + +-- This table may exist during upgrades, but must be removed. +-- The drop statement is therefore optional. +DROP TABLE alf_content_url; --(optional) +CREATE TABLE alf_content_url +( + id BIGINT NOT NULL AUTO_INCREMENT, + version BIGINT NOT NULL, + content_url VARCHAR(255) NOT NULL, + content_url_short VARCHAR(12) NOT NULL, + content_url_crc BIGINT NOT NULL, + content_size BIGINT NOT NULL, + UNIQUE INDEX idx_alf_cont_url_crc (content_url_short, content_url_crc), + PRIMARY KEY (id) +) ENGINE=InnoDB; + +CREATE TABLE alf_content_data +( + id BIGINT NOT NULL AUTO_INCREMENT, + version BIGINT NOT NULL, + content_url_id BIGINT NULL, + content_mimetype_id BIGINT NULL, + content_encoding_id BIGINT NULL, + content_locale_id BIGINT NULL, + CONSTRAINT fk_alf_cont_url FOREIGN KEY (content_url_id) REFERENCES alf_content_url (id), + CONSTRAINT fk_alf_cont_mim FOREIGN KEY (content_mimetype_id) REFERENCES alf_mimetype (id), + CONSTRAINT fk_alf_cont_enc FOREIGN KEY (content_encoding_id) REFERENCES alf_encoding (id), + CONSTRAINT fk_alf_cont_loc FOREIGN KEY (content_locale_id) REFERENCES alf_locale (id), + PRIMARY KEY (id) +) ENGINE=InnoDB; + +CREATE TABLE alf_content_clean +( + content_url VARCHAR(255) NOT NULL, + INDEX idx_alf_contentclean_url (content_url) +) ENGINE=InnoDB; + +-- +-- Record script finish +-- +DELETE FROM alf_applied_patch WHERE id = 'patch.db-V3.2-ContentTables'; +INSERT INTO alf_applied_patch + (id, description, fixes_from_schema, fixes_to_schema, applied_to_schema, target_schema, applied_on_date, applied_to_server, was_executed, succeeded, report) + VALUES + ( + 'patch.db-V3.2-ContentTables', 'Manually executed script upgrade V3.2: Content Tables', + 0, 2011, -1, 2012, null, 'UNKOWN', 1, 1, 'Script completed' + ); \ No newline at end of file diff --git a/config/alfresco/ehcache-default.xml b/config/alfresco/ehcache-default.xml index 021dc71817..d312e986b8 100644 --- a/config/alfresco/ehcache-default.xml +++ b/config/alfresco/ehcache-default.xml @@ -290,6 +290,12 @@ eternal="true" overflowToDisk="false" /> + + + + + + + + + + + org/alfresco/repo/audit/hibernate/Audit.hbm.xml - - org/alfresco/repo/domain/hibernate/ContentUrl.hbm.xml - @@ -209,8 +206,6 @@ ${cache.strategy} ${cache.strategy} ${cache.strategy} - - ${cache.strategy} @@ -375,6 +370,9 @@ + + + @@ -453,27 +451,6 @@ - - - org.alfresco.repo.domain.ContentUrlDAO - - - - - - - sessionSizeResourceInterceptor - - - - - - - - - - - diff --git a/config/alfresco/ibatis/content-SqlMapConfig.xml b/config/alfresco/ibatis/content-SqlMapConfig.xml new file mode 100644 index 0000000000..83aafb92fe --- /dev/null +++ b/config/alfresco/ibatis/content-SqlMapConfig.xml @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/config/alfresco/ibatis/ibatis-context.xml b/config/alfresco/ibatis/ibatis-context.xml index 668a3217c2..e9d9bc3df3 100644 --- a/config/alfresco/ibatis/ibatis-context.xml +++ b/config/alfresco/ibatis/ibatis-context.xml @@ -62,4 +62,16 @@ + + + + + + classpath:alfresco/ibatis/content-SqlMapConfig.xml + + + + + + diff --git a/config/alfresco/ibatis/org.hibernate.dialect.DerbyDialect/content-insert-SqlMap.xml b/config/alfresco/ibatis/org.hibernate.dialect.DerbyDialect/content-insert-SqlMap.xml new file mode 100644 index 0000000000..3827729e62 --- /dev/null +++ b/config/alfresco/ibatis/org.hibernate.dialect.DerbyDialect/content-insert-SqlMap.xml @@ -0,0 +1,37 @@ + + + + + + + + + + values IDENTITY_VAL_LOCAL() + + + + + + + values IDENTITY_VAL_LOCAL() + + + + + + + values IDENTITY_VAL_LOCAL() + + + + + + + values IDENTITY_VAL_LOCAL() + + + + \ No newline at end of file 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 new file mode 100644 index 0000000000..ea97d634fa --- /dev/null +++ b/config/alfresco/ibatis/org.hibernate.dialect.Dialect/content-common-SqlMap.xml @@ -0,0 +1,253 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + insert into alf_mimetype (version, mimetype_str) + values (#version#, lower(#mimetype#)) + + + + insert into alf_encoding (version, encoding_str) + values (#version#, lower(#encoding#)) + + + + insert into alf_content_url (version, content_url, content_url_short, content_url_crc, content_size) + values (#version#, #contentUrl#, #contentUrlShort#, #contentUrlCrc#, #size#) + + + + insert into alf_content_data (version, content_url_id, content_mimetype_id, content_encoding_id, content_locale_id) + values (#version#, #contentUrlId#, #mimetypeId#, #encodingId#, #localeId#) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + delete + from + alf_content_data + where + id = ? + + + + + delete + from + alf_content_url + where + id = ? + + + + + + + + insert into alf_content_clean (content_url) values (lower(#contentUrl#)) + + + + + + + + delete + from + alf_content_clean + where + content_url = lower(#contentUrl#) + + + + + delete + from + alf_content_clean + + + \ No newline at end of file diff --git a/config/alfresco/ibatis/org.hibernate.dialect.MySQLInnoDBDialect/content-insert-SqlMap.xml b/config/alfresco/ibatis/org.hibernate.dialect.MySQLInnoDBDialect/content-insert-SqlMap.xml new file mode 100644 index 0000000000..809476a283 --- /dev/null +++ b/config/alfresco/ibatis/org.hibernate.dialect.MySQLInnoDBDialect/content-insert-SqlMap.xml @@ -0,0 +1,37 @@ + + + + + + + + + + KEY_COLUMN:GENERATED_KEY + + + + + + + KEY_COLUMN:GENERATED_KEY + + + + + + + KEY_COLUMN:GENERATED_KEY + + + + + + + KEY_COLUMN:GENERATED_KEY + + + + \ No newline at end of file diff --git a/config/alfresco/patch/patch-services-context.xml b/config/alfresco/patch/patch-services-context.xml index ca13772862..57390e412e 100644 --- a/config/alfresco/patch/patch-services-context.xml +++ b/config/alfresco/patch/patch-services-context.xml @@ -1910,4 +1910,16 @@ + + patch.db-V3.2-ContentTables + patch.schemaUpgradeScript.description + 0 + 2015 + 2016 + + + classpath:alfresco/dbscripts/create/3.2/${db.script.dialect}/AlfrescoPostCreate-3.2-ContentTables.sql + + + diff --git a/config/alfresco/public-services-context.xml b/config/alfresco/public-services-context.xml index 10e655d4cd..9be28843b5 100644 --- a/config/alfresco/public-services-context.xml +++ b/config/alfresco/public-services-context.xml @@ -267,6 +267,10 @@ + + + + diff --git a/config/alfresco/scheduled-jobs-context.xml b/config/alfresco/scheduled-jobs-context.xml index 24acf2a340..382c4054c3 100644 --- a/config/alfresco/scheduled-jobs-context.xml +++ b/config/alfresco/scheduled-jobs-context.xml @@ -129,6 +129,7 @@ + 0 0 4 * * ? +--> diff --git a/config/alfresco/version.properties b/config/alfresco/version.properties index 841c0fa1ed..f26cf5556e 100644 --- a/config/alfresco/version.properties +++ b/config/alfresco/version.properties @@ -19,4 +19,4 @@ version.build=@build-number@ # Schema number -version.schema=2015 +version.schema=2016 diff --git a/source/java/org/alfresco/repo/content/cleanup/ContentStoreCleaner.java b/source/java/org/alfresco/repo/content/cleanup/ContentStoreCleaner.java index a5ac6a7c79..e96dbbe1c8 100644 --- a/source/java/org/alfresco/repo/content/cleanup/ContentStoreCleaner.java +++ b/source/java/org/alfresco/repo/content/cleanup/ContentStoreCleaner.java @@ -28,39 +28,28 @@ import java.io.Serializable; import java.util.ArrayList; import java.util.Date; import java.util.List; -import java.util.Map; -import java.util.Set; import org.alfresco.error.AlfrescoRuntimeException; -import org.alfresco.model.ContentModel; import org.alfresco.repo.avm.AVMNodeDAO; import org.alfresco.repo.avm.AVMNodeDAO.ContentUrlHandler; -import org.alfresco.repo.content.ContentServicePolicies; import org.alfresco.repo.content.ContentStore; -import org.alfresco.repo.copy.CopyServicePolicies; -import org.alfresco.repo.domain.ContentUrlDAO; -import org.alfresco.repo.node.NodeServicePolicies; +import org.alfresco.repo.domain.contentclean.ContentCleanDAO; +import org.alfresco.repo.domain.contentclean.ContentCleanDAO.ContentUrlBatchProcessor; +import org.alfresco.repo.domain.contentdata.ContentDataDAO; +import org.alfresco.repo.lock.JobLockService; import org.alfresco.repo.node.db.NodeDaoService; import org.alfresco.repo.node.db.NodeDaoService.NodePropertyHandler; -import org.alfresco.repo.policy.JavaBehaviour; -import org.alfresco.repo.policy.PolicyComponent; -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.RetryingTransactionHelper.RetryingTransactionCallback; import org.alfresco.service.cmr.dictionary.DataTypeDefinition; import org.alfresco.service.cmr.dictionary.DictionaryService; import org.alfresco.service.cmr.repository.ContentData; -import org.alfresco.service.cmr.repository.ContentReader; import org.alfresco.service.cmr.repository.ContentService; -import org.alfresco.service.cmr.repository.ContentWriter; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter; import org.alfresco.service.namespace.NamespaceService; import org.alfresco.service.namespace.QName; import org.alfresco.service.transaction.TransactionService; -import org.alfresco.util.Pair; import org.alfresco.util.PropertyCheck; import org.alfresco.util.VmShutdownListener; import org.apache.commons.logging.Log; @@ -99,39 +88,21 @@ import org.apache.commons.logging.LogFactory; * @author Derek Hulley */ public class ContentStoreCleaner - extends TransactionListenerAdapter - implements CopyServicePolicies.OnCopyCompletePolicy, - NodeServicePolicies.BeforeDeleteNodePolicy, - ContentServicePolicies.OnContentPropertyUpdatePolicy - { - /** - * Content URLs to delete once the transaction commits. - * @see #onContentPropertyUpdate(NodeRef, QName, ContentData, ContentData) - * @see #afterCommit() - */ - private static final String KEY_POST_COMMIT_DELETION_URLS = "ContentStoreCleaner.PostCommitDeletionUrls"; - /** - * Content URLs to delete if the transaction rolls b ack. - * @see #onContentPropertyUpdate(NodeRef, QName, ContentData, ContentData) - * @see #afterRollback() - */ - private static final String KEY_POST_ROLLBACK_DELETION_URLS = "ContentStoreCleaner.PostRollbackDeletionUrls"; - private static Log logger = LogFactory.getLog(ContentStoreCleaner.class); /** kept to notify the thread that it should quit */ private static VmShutdownListener vmShutdownListener = new VmShutdownListener("ContentStoreCleaner"); + private JobLockService jobLockService; + private ContentCleanDAO contentCleanDAO; + private ContentDataDAO contentDataDAO; private DictionaryService dictionaryService; - private PolicyComponent policyComponent; private ContentService contentService; private NodeDaoService nodeDaoService; private AVMNodeDAO avmNodeDAO; - private ContentUrlDAO contentUrlDAO; private TransactionService transactionService; private List stores; - private boolean eagerOrphanCleanup; private List listeners; private int protectDays; @@ -142,6 +113,30 @@ public class ContentStoreCleaner this.protectDays = 7; } + /** + * @param jobLockService service used to ensure that cleanup runs are not duplicated + */ + public void setJobLockService(JobLockService jobLockService) + { + this.jobLockService = jobLockService; + } + + /** + * @param contentCleanDAO DAO used for manipulating content URLs + */ + public void setContentCleanDAO(ContentCleanDAO contentCleanDAO) + { + this.contentCleanDAO = contentCleanDAO; + } + + /** + * @param contentDataDAO DAO used for enumerating DM content URLs + */ + public void setContentDataDAO(ContentDataDAO contentDataDAO) + { + this.contentDataDAO = contentDataDAO; + } + /** * @param dictionaryService used to determine which properties are content properties */ @@ -150,14 +145,6 @@ public class ContentStoreCleaner this.dictionaryService = dictionaryService; } - /** - * @param policyComponent used to register to listen for content updates - */ - public void setPolicyComponent(PolicyComponent policyComponent) - { - this.policyComponent = policyComponent; - } - /** * @param contentService service to copy content binaries */ @@ -182,14 +169,6 @@ public class ContentStoreCleaner this.avmNodeDAO = avmNodeDAO; } - /** - * @param contentUrlDAO DAO for recording valid Content URLs - */ - public void setContentUrlDAO(ContentUrlDAO contentUrlDAO) - { - this.contentUrlDAO = contentUrlDAO; - } - /** * @param transactionService the component to ensure proper transactional wrapping */ @@ -206,15 +185,6 @@ public class ContentStoreCleaner this.stores = stores; } - /** - * - * @param eagerOrphanCleanup true to clean up content - */ - public void setEagerOrphanCleanup(boolean eagerOrphanCleanup) - { - this.eagerOrphanCleanup = eagerOrphanCleanup; - } - /** * @param listeners the listeners that can react to deletions */ @@ -240,26 +210,6 @@ public class ContentStoreCleaner public void init() { checkProperties(); - if (!eagerOrphanCleanup) - { - // Don't register for anything because eager cleanup is disabled - return; - } - // Register for the updates - this.policyComponent.bindClassBehaviour( - QName.createQName(NamespaceService.ALFRESCO_URI, "onContentPropertyUpdate"), - this, - new JavaBehaviour(this, "onContentPropertyUpdate")); - // TODO: This is a RM-specific hack. Once content properties are separated out, the - // following should be accomplished with a trigger to clean up orphaned content. - this.policyComponent.bindClassBehaviour( - QName.createQName(NamespaceService.ALFRESCO_URI, "beforeDeleteNode"), - ContentModel.TYPE_CONTENT, - new JavaBehaviour(this, "beforeDeleteNode")); - this.policyComponent.bindClassBehaviour( - QName.createQName(NamespaceService.ALFRESCO_URI, "onCopyComplete"), - ContentModel.TYPE_CONTENT, - new JavaBehaviour(this, "onCopyComplete")); } /** @@ -267,12 +217,13 @@ public class ContentStoreCleaner */ private void checkProperties() { + PropertyCheck.mandatory(this, "jobLockService", jobLockService); + PropertyCheck.mandatory(this, "contentCleanerDAO", contentCleanDAO); + PropertyCheck.mandatory(this, "contentDataDAO", contentDataDAO); PropertyCheck.mandatory(this, "dictionaryService", dictionaryService); - PropertyCheck.mandatory(this, "policyComponent", policyComponent); PropertyCheck.mandatory(this, "contentService", contentService); PropertyCheck.mandatory(this, "nodeDaoService", nodeDaoService); PropertyCheck.mandatory(this, "avmNodeDAO", avmNodeDAO); - PropertyCheck.mandatory(this, "contentUrlDAO", contentUrlDAO); PropertyCheck.mandatory(this, "transactionService", transactionService); PropertyCheck.mandatory(this, "listeners", listeners); @@ -288,193 +239,35 @@ public class ContentStoreCleaner "It is possible that in-transaction content will be deleted."); } } - - /** - * Makes sure that copied files get a new, unshared binary. - */ - public void onCopyComplete( - QName classRef, - NodeRef sourceNodeRef, - NodeRef targetNodeRef, - boolean copyToNewNode, - Map copyMap) - { - // Get the cm:content of the source - ContentReader sourceReader = contentService.getReader(sourceNodeRef, ContentModel.PROP_CONTENT); - if (sourceReader == null || !sourceReader.exists()) - { - // No point attempting to duplicate missing content. We're only interested in cleanin up. - return; - } - // Get the cm:content of the target - ContentReader targetReader = contentService.getReader(targetNodeRef, ContentModel.PROP_CONTENT); - if (targetReader == null || !targetReader.exists()) - { - // The target's content is not present, so we don't copy anything over - return; - } - else if (!targetReader.getContentUrl().equals(sourceReader.getContentUrl())) - { - // The target already has a different binary - return; - } - // Create new content for the target node. This will behave just like an update to the node - // but occurs in the same txn as the copy process. Clearly this is a hack and is only - // able to work when properties are copied with all the proper copy-related policies being - // triggered. - ContentWriter targetWriter = contentService.getWriter(targetNodeRef, ContentModel.PROP_CONTENT, true); - targetWriter.putContent(sourceReader); - // This will have triggered deletion of the source node's content because the target node - // is being updated. Force the source node's content to be protected. - Set urlsToDelete = TransactionalResourceHelper.getSet(KEY_POST_COMMIT_DELETION_URLS); - urlsToDelete.remove(sourceReader.getContentUrl()); - } - - /** - * Records the content URLs of cm:content for post-commit cleanup. - */ - public void beforeDeleteNode(NodeRef nodeRef) - { - // Get the cm:content property - Pair nodePair = nodeDaoService.getNodePair(nodeRef); - if (nodePair == null) - { - return; - } - ContentData contentData = (ContentData) nodeDaoService.getNodeProperty( - nodePair.getFirst(), ContentModel.PROP_CONTENT); - if (contentData == null || !ContentData.hasContent(contentData)) - { - return; - } - String contentUrl = contentData.getContentUrl(); - // Bind it to the list - Set urlsToDelete = TransactionalResourceHelper.getSet(KEY_POST_COMMIT_DELETION_URLS); - urlsToDelete.add(contentUrl); - AlfrescoTransactionSupport.bindListener(this); - } - - /** - * Checks for {@link #setEagerOrphanCleanup(boolean) eager cleanup} and pushes the old content URL into - * a list for post-transaction deletion. - */ - public void onContentPropertyUpdate( - NodeRef nodeRef, - QName propertyQName, - ContentData beforeValue, - ContentData afterValue) - { - boolean registerListener = false; - // Bind in URLs to delete when txn commits - if (beforeValue != null && ContentData.hasContent(beforeValue)) - { - // Register the URL to delete - String contentUrl = beforeValue.getContentUrl(); - Set urlsToDelete = TransactionalResourceHelper.getSet(KEY_POST_COMMIT_DELETION_URLS); - urlsToDelete.add(contentUrl); - // Register to listen for transaction commit - registerListener = true; - } - // Bind in URLs to delete when txn rolls back - if (afterValue != null && ContentData.hasContent(afterValue)) - { - // Register the URL to delete - String contentUrl = afterValue.getContentUrl(); - Set urlsToDelete = TransactionalResourceHelper.getSet(KEY_POST_ROLLBACK_DELETION_URLS); - urlsToDelete.add(contentUrl); - // Register to listen for transaction rollback - registerListener = true; - } - // Register listener - if (registerListener) - { - AlfrescoTransactionSupport.bindListener(this); - } - } - - /** - * Cleans up all newly-orphaned content - */ - @Override - public void afterCommit() - { - Set urlsToDelete = TransactionalResourceHelper.getSet(KEY_POST_COMMIT_DELETION_URLS); - // Debug - if (logger.isDebugEnabled()) - { - logger.debug("Post-commit deletion of old content URLs: "); - int count = 0; - for (String contentUrl : urlsToDelete) - { - if (count == 10) - { - logger.debug(" " + (urlsToDelete.size() - 10) + " more ..."); - } - else - { - logger.debug(" Deleting content URL: " + contentUrl); - } - count++; - } - } - // Delete - for (String contentUrl : urlsToDelete) - { - for (ContentStore store : stores) - { - for (ContentStoreCleanerListener listener : listeners) - { - listener.beforeDelete(store, contentUrl); - } - store.delete(contentUrl); - } - } - } - - @Override - public void afterRollback() - { - Set urlsToDelete = TransactionalResourceHelper.getSet(KEY_POST_ROLLBACK_DELETION_URLS); - // Debug - if (logger.isDebugEnabled()) - { - logger.debug("Post-rollback deletion of new content URLs: "); - int count = 0; - for (String contentUrl : urlsToDelete) - { - if (count == 10) - { - logger.debug(" " + (urlsToDelete.size() - 10) + " more ..."); - } - else - { - logger.debug(" Deleting content URL: " + contentUrl); - } - count++; - } - } - // Delete - for (String contentUrl : urlsToDelete) - { - for (ContentStore store : stores) - { - for (ContentStoreCleanerListener listener : listeners) - { - listener.beforeDelete(store, contentUrl); - } - store.delete(contentUrl); - } - } - } - - private void removeContentUrlsPresentInMetadata() + + private void removeContentUrlsPresentInMetadata(final ContentUrlBatchProcessor urlRemover) { RetryingTransactionHelper txnHelper = transactionService.getRetryingTransactionHelper(); // Remove all the Content URLs for the ADM repository - // Handler that records the URL + // Handlers that record the URLs + final ContentDataDAO.ContentUrlHandler contentUrlHandler = new ContentDataDAO.ContentUrlHandler() + { + long lastLock = 0L; + public void handle(String contentUrl) + { + if (vmShutdownListener.isVmShuttingDown()) + { + throw new VmShutdownException(); + } + urlRemover.processContentUrl(contentUrl); + // Check lock + long now = System.currentTimeMillis(); + if (now - lastLock > (long)(LOCK_TTL/2L)) + { + jobLockService.getTransactionalLock(LOCK_QNAME, LOCK_TTL); + lastLock = now; + } + } + }; final NodePropertyHandler nodePropertyHandler = new NodePropertyHandler() { + long lastLock = 0L; public void handle(NodeRef nodeRef, QName nodeTypeQName, QName propertyQName, Serializable value) { if (vmShutdownListener.isVmShuttingDown()) @@ -486,16 +279,24 @@ public class ContentStoreCleaner String contentUrl = contentData.getContentUrl(); if (contentUrl != null) { - contentUrlDAO.deleteContentUrl(contentUrl); + urlRemover.processContentUrl(contentUrl); + } + // Check lock + long now = System.currentTimeMillis(); + if (now - lastLock > (long)(LOCK_TTL/2L)) + { + jobLockService.getTransactionalLock(LOCK_QNAME, LOCK_TTL); + lastLock = now; } } }; final DataTypeDefinition contentDataType = dictionaryService.getDataType(DataTypeDefinition.CONTENT); // execute in READ-WRITE txn - RetryingTransactionCallback getUrlsCallback = new RetryingTransactionCallback() + RetryingTransactionCallback getUrlsCallback = new RetryingTransactionCallback() { - public Object execute() throws Exception + public Void execute() throws Exception { + contentDataDAO.getAllContentUrls(contentUrlHandler); nodeDaoService.getPropertyValuesByActualType(contentDataType, nodePropertyHandler); return null; }; @@ -503,21 +304,29 @@ public class ContentStoreCleaner txnHelper.doInTransaction(getUrlsCallback); // Do the same for the AVM repository. - final ContentUrlHandler handler = new ContentUrlHandler() + final AVMNodeDAO.ContentUrlHandler handler = new AVMNodeDAO.ContentUrlHandler() { + long lastLock = 0L; public void handle(String contentUrl) { if (vmShutdownListener.isVmShuttingDown()) { throw new VmShutdownException(); } - contentUrlDAO.deleteContentUrl(contentUrl); + urlRemover.processContentUrl(contentUrl); + // Check lock + long now = System.currentTimeMillis(); + if (now - lastLock > (long)(LOCK_TTL/2L)) + { + jobLockService.getTransactionalLock(LOCK_QNAME, LOCK_TTL); + lastLock = now; + } } }; // execute in READ-WRITE txn - RetryingTransactionCallback getAVMUrlsCallback = new RetryingTransactionCallback() + RetryingTransactionCallback getAVMUrlsCallback = new RetryingTransactionCallback() { - public Object execute() throws Exception + public Void execute() throws Exception { avmNodeDAO.getContentUrls(handler); return null; @@ -526,17 +335,25 @@ public class ContentStoreCleaner txnHelper.doInTransaction(getAVMUrlsCallback); } - private void addContentUrlsPresentInStores() + private void addContentUrlsPresentInStores(final ContentUrlBatchProcessor urlInserter) { org.alfresco.repo.content.ContentStore.ContentUrlHandler handler = new org.alfresco.repo.content.ContentStore.ContentUrlHandler() { + long lastLock = 0L; public void handle(String contentUrl) { if (vmShutdownListener.isVmShuttingDown()) { throw new VmShutdownException(); } - contentUrlDAO.createContentUrl(contentUrl); + urlInserter.processContentUrl(contentUrl); + // Check lock + long now = System.currentTimeMillis(); + if (now - lastLock > (long)(LOCK_TTL/2L)) + { + jobLockService.getTransactionalLock(LOCK_QNAME, LOCK_TTL); + lastLock = now; + } } }; Date checkAllBeforeDate = new Date(System.currentTimeMillis() - (long) protectDays * 3600L * 1000L * 24L); @@ -546,25 +363,54 @@ public class ContentStoreCleaner } } + private static final QName LOCK_QNAME = QName.createQName(NamespaceService.SYSTEM_MODEL_1_0_URI, "ContentStoreCleaner"); + private static final long LOCK_TTL = 30000L; public void execute() { checkProperties(); - - if (logger.isDebugEnabled()) + + RetryingTransactionCallback executeCallback = new RetryingTransactionCallback() { - logger.debug("Starting content store cleanup."); + public Void execute() throws Exception + { + logger.debug("Content store cleanup started."); + // Get the lock without any waiting + // The lock will be refreshed, but the first lock starts the process + jobLockService.getTransactionalLock(LOCK_QNAME, LOCK_TTL); + executeInternal(); + return null; + } + }; + try + { + RetryingTransactionHelper txnHelper = transactionService.getRetryingTransactionHelper(); + txnHelper.setMaxRetries(0); + txnHelper.doInTransaction(executeCallback); + // Done + if (logger.isDebugEnabled()) + { + logger.debug(" Content store cleanup completed."); + } + } + catch (VmShutdownException e) + { + // Aborted + if (logger.isDebugEnabled()) + { + logger.debug(" Content store cleanup aborted."); + } } - // Repeat attempts six times waiting 10 minutes between - executeInternal(0, 6, 600000); } - public void executeInternal(int currentAttempt, int maxAttempts, long waitTime) + public void executeInternal() { - currentAttempt++; - // This handler removes the URLs from all the stores - final org.alfresco.repo.domain.ContentUrlDAO.ContentUrlHandler handler = new org.alfresco.repo.domain.ContentUrlDAO.ContentUrlHandler() + final ContentUrlBatchProcessor storeUrlDeleteHandler = new ContentUrlBatchProcessor() { - public void handle(String contentUrl) + long lastLock = 0L; + public void start() + { + } + public void processContentUrl(String contentUrl) { for (ContentStore store : stores) { @@ -585,24 +431,52 @@ public class ContentStoreCleaner } // Delete store.delete(contentUrl); + // Check lock + long now = System.currentTimeMillis(); + if (now - lastLock > (long)(LOCK_TTL/2L)) + { + jobLockService.getTransactionalLock(LOCK_QNAME, LOCK_TTL); + lastLock = now; + } } } + public void end() + { + } }; // execute in READ-WRITE txn - RetryingTransactionCallback executeCallback = new RetryingTransactionCallback() + RetryingTransactionCallback executeCallback = new RetryingTransactionCallback() { - public Object execute() throws Exception + public Void execute() throws Exception { - // Delete all the URLs - contentUrlDAO.deleteAllContentUrls(); - // Populate the URLs from the content stores - addContentUrlsPresentInStores(); - // Remove URLs present in the metadata - removeContentUrlsPresentInMetadata(); + // Clean up + contentCleanDAO.cleanUp(); + // Push all store URLs in + ContentUrlBatchProcessor urlInserter = contentCleanDAO.getUrlInserter(); + try + { + urlInserter.start(); + addContentUrlsPresentInStores(urlInserter); + } + finally + { + urlInserter.end(); + } + // Delete all content URLs + ContentUrlBatchProcessor urlRemover = contentCleanDAO.getUrlRemover(); + try + { + urlRemover.start(); + removeContentUrlsPresentInMetadata(urlRemover); + } + finally + { + urlRemover.end(); + } // Any remaining URLs are URls present in the stores but not in the metadata - contentUrlDAO.getAllContentUrls(handler); - // Delete all the URLs - contentUrlDAO.deleteAllContentUrls(); + contentCleanDAO.listAllUrls(storeUrlDeleteHandler); + // Clean up + contentCleanDAO.cleanUp(); return null; }; }; @@ -623,22 +497,6 @@ public class ContentStoreCleaner logger.debug(" Content store cleanup aborted."); } } - catch (Throwable e) - { - if (currentAttempt >= maxAttempts) - { - throw new AlfrescoRuntimeException("Failed to initiate content store clean", e); - } - if (RetryingTransactionHelper.extractRetryCause(e) != null) - { - // There are grounds for waiting and retrying - synchronized(this) - { - try { this.wait(waitTime); } catch (InterruptedException ee) {} - } - executeInternal(currentAttempt, maxAttempts, waitTime); - } - } } /** diff --git a/source/java/org/alfresco/repo/content/cleanup/ContentStoreCleanerTest.java b/source/java/org/alfresco/repo/content/cleanup/ContentStoreCleanerTest.java index 27bc61f1e5..f838614109 100644 --- a/source/java/org/alfresco/repo/content/cleanup/ContentStoreCleanerTest.java +++ b/source/java/org/alfresco/repo/content/cleanup/ContentStoreCleanerTest.java @@ -29,7 +29,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.concurrent.CountDownLatch; import junit.framework.TestCase; @@ -38,9 +37,10 @@ import org.alfresco.repo.avm.AVMNodeDAO; import org.alfresco.repo.content.ContentStore; import org.alfresco.repo.content.MimetypeMap; import org.alfresco.repo.content.filestore.FileContentStore; -import org.alfresco.repo.domain.ContentUrlDAO; +import org.alfresco.repo.domain.contentclean.ContentCleanDAO; +import org.alfresco.repo.domain.contentdata.ContentDataDAO; +import org.alfresco.repo.lock.JobLockService; import org.alfresco.repo.node.db.NodeDaoService; -import org.alfresco.repo.policy.PolicyComponent; import org.alfresco.repo.security.authentication.AuthenticationUtil; import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; import org.alfresco.service.ServiceRegistry; @@ -49,16 +49,13 @@ import org.alfresco.service.cmr.repository.ContentIOException; import org.alfresco.service.cmr.repository.ContentReader; import org.alfresco.service.cmr.repository.ContentService; import org.alfresco.service.cmr.repository.ContentWriter; -import org.alfresco.service.cmr.repository.CopyService; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.NodeService; import org.alfresco.service.cmr.repository.StoreRef; import org.alfresco.service.namespace.QName; import org.alfresco.service.transaction.TransactionService; import org.alfresco.util.ApplicationContextHelper; -import org.alfresco.util.EqualsHelper; import org.alfresco.util.GUID; -import org.alfresco.util.Pair; import org.alfresco.util.TempFileProvider; import org.springframework.context.ConfigurableApplicationContext; @@ -73,9 +70,10 @@ public class ContentStoreCleanerTest extends TestCase private ContentService contentService; private NodeService nodeService; - private CopyService copyService; private TransactionService transactionService; + private JobLockService jobLockService; private ContentStoreCleaner cleaner; + private EagerContentStoreCleaner eagerCleaner; private ContentStore store; private ContentStoreCleanerListener listener; private List deletedUrls; @@ -88,14 +86,16 @@ public class ContentStoreCleanerTest extends TestCase ServiceRegistry serviceRegistry = (ServiceRegistry) ctx.getBean("ServiceRegistry"); contentService = serviceRegistry.getContentService(); nodeService = serviceRegistry.getNodeService(); - copyService = serviceRegistry.getCopyService(); transactionService = serviceRegistry.getTransactionService(); + jobLockService = serviceRegistry.getJobLockService(); TransactionService transactionService = serviceRegistry.getTransactionService(); DictionaryService dictionaryService = serviceRegistry.getDictionaryService(); - PolicyComponent policyComponent = (PolicyComponent) ctx.getBean("policyComponent"); NodeDaoService nodeDaoService = (NodeDaoService) ctx.getBean("nodeDaoService"); AVMNodeDAO avmNodeDAO = (AVMNodeDAO) ctx.getBean("avmNodeDAO"); - ContentUrlDAO contentUrlDAO = (ContentUrlDAO) ctx.getBean("contentUrlDAO"); + ContentCleanDAO contentCleanDAO = (ContentCleanDAO) ctx.getBean("contentCleanDAO"); + ContentDataDAO contentDataDAO = (ContentDataDAO) ctx.getBean("contentDataDAO"); + + eagerCleaner = (EagerContentStoreCleaner) ctx.getBean("eagerContentStoreCleaner"); // we need a store store = new FileContentStore(ctx, TempFileProvider.getTempDir().getAbsolutePath()); @@ -106,16 +106,16 @@ public class ContentStoreCleanerTest extends TestCase // construct the test cleaner cleaner = new ContentStoreCleaner(); + cleaner.setJobLockService(jobLockService); + cleaner.setContentCleanDAO(contentCleanDAO); + cleaner.setContentDataDAO(contentDataDAO); cleaner.setTransactionService(transactionService); cleaner.setDictionaryService(dictionaryService); - cleaner.setPolicyComponent(policyComponent); cleaner.setContentService(contentService); cleaner.setNodeDaoService(nodeDaoService); cleaner.setAvmNodeDAO(avmNodeDAO); - cleaner.setContentUrlDAO(contentUrlDAO); cleaner.setStores(Collections.singletonList(store)); cleaner.setListeners(Collections.singletonList(listener)); - cleaner.setEagerOrphanCleanup(false); } public void tearDown() throws Exception @@ -125,11 +125,7 @@ public class ContentStoreCleanerTest extends TestCase public void testEagerCleanupOnCommit() throws Exception { - // Get the context-defined cleaner - ContentStoreCleaner cleaner = (ContentStoreCleaner) ctx.getBean("contentStoreCleaner"); - // Force eager cleanup - cleaner.setEagerOrphanCleanup(true); - cleaner.init(); + eagerCleaner.setEagerOrphanCleanup(true); // Create a new file RetryingTransactionCallback makeContentCallback = new RetryingTransactionCallback() { @@ -178,7 +174,15 @@ public class ContentStoreCleanerTest extends TestCase } catch (RuntimeException e) { - // Expected + if (e.getMessage().equals("FAIL")) + { + // Expected + } + else + { + // Ooops + throw e; + } } // Make sure that the new content is not there // The original content must still be there @@ -221,61 +225,61 @@ public class ContentStoreCleanerTest extends TestCase assertFalse("Newly created content should be removed.", contentReaderNew.exists()); } - /** - * TODO: This test must be replaced with one that checks that the raw content binary lives - * as long as there is a reference to it. Once the RM-hacks are removed, content - * will once again be shared and must therefore be cleaned up based on unlinking of - * references. - */ - public void testEagerCleanupAfterCopy() throws Exception - { - // Get the context-defined cleaner - ContentStoreCleaner cleaner = (ContentStoreCleaner) ctx.getBean("contentStoreCleaner"); - // Force eager cleanup - cleaner.setEagerOrphanCleanup(true); - cleaner.init(); - // Create a new file, copy it - RetryingTransactionCallback> copyFileCallback = new RetryingTransactionCallback>() - { - public Pair execute() throws Throwable - { - // Create some content - StoreRef storeRef = nodeService.createStore("test", "testEagerCleanupAfterCopy-" + GUID.generate()); - NodeRef rootNodeRef = nodeService.getRootNode(storeRef); - Map properties = Collections.singletonMap(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"); - // Now copy it - NodeRef copiedNodeRef = copyService.copy( - contentNodeRef, - rootNodeRef, - ContentModel.ASSOC_CHILDREN, - ContentModel.ASSOC_CHILDREN); - // Done - return new Pair(contentNodeRef, copiedNodeRef); - } - }; - Pair nodeRefPair = transactionService.getRetryingTransactionHelper().doInTransaction(copyFileCallback); - // Check that the readers of the content have different URLs - ContentReader contentReaderSource = contentService.getReader(nodeRefPair.getFirst(), ContentModel.PROP_CONTENT); - assertNotNull("Expected reader for source cm:content", contentReaderSource); - assertTrue("Expected content for source cm:content", contentReaderSource.exists()); - ContentReader contentReaderCopy = contentService.getReader(nodeRefPair.getSecond(), ContentModel.PROP_CONTENT); - assertNotNull("Expected reader for copy cm:content", contentReaderCopy); - assertTrue("Expected content for copy cm:content", contentReaderCopy.exists()); - String contentUrlSource = contentReaderSource.getContentUrl(); - String contentUrlCopy = contentReaderCopy.getContentUrl(); - assertFalse("Source and copy must have different URLs", - EqualsHelper.nullSafeEquals(contentUrlSource, contentUrlCopy)); - } - +// /** +// * TODO: This test must be replaced with one that checks that the raw content binary lives +// * as long as there is a reference to it. Once the RM-hacks are removed, content +// * will once again be shared and must therefore be cleaned up based on unlinking of +// * references. +// */ +// public void testEagerCleanupAfterCopy() throws Exception +// { +// // Get the context-defined cleaner +// ContentStoreCleaner cleaner = (ContentStoreCleaner) ctx.getBean("contentStoreCleaner"); +// // Force eager cleanup +// cleaner.setEagerOrphanCleanup(true); +// cleaner.init(); +// // Create a new file, copy it +// RetryingTransactionCallback> copyFileCallback = new RetryingTransactionCallback>() +// { +// public Pair execute() throws Throwable +// { +// // Create some content +// StoreRef storeRef = nodeService.createStore("test", "testEagerCleanupAfterCopy-" + GUID.generate()); +// NodeRef rootNodeRef = nodeService.getRootNode(storeRef); +// Map properties = Collections.singletonMap(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"); +// // Now copy it +// NodeRef copiedNodeRef = copyService.copy( +// contentNodeRef, +// rootNodeRef, +// ContentModel.ASSOC_CHILDREN, +// ContentModel.ASSOC_CHILDREN); +// // Done +// return new Pair(contentNodeRef, copiedNodeRef); +// } +// }; +// Pair nodeRefPair = transactionService.getRetryingTransactionHelper().doInTransaction(copyFileCallback); +// // Check that the readers of the content have different URLs +// ContentReader contentReaderSource = contentService.getReader(nodeRefPair.getFirst(), ContentModel.PROP_CONTENT); +// assertNotNull("Expected reader for source cm:content", contentReaderSource); +// assertTrue("Expected content for source cm:content", contentReaderSource.exists()); +// ContentReader contentReaderCopy = contentService.getReader(nodeRefPair.getSecond(), ContentModel.PROP_CONTENT); +// assertNotNull("Expected reader for copy cm:content", contentReaderCopy); +// assertTrue("Expected content for copy cm:content", contentReaderCopy.exists()); +// String contentUrlSource = contentReaderSource.getContentUrl(); +// String contentUrlCopy = contentReaderCopy.getContentUrl(); +// assertFalse("Source and copy must have different URLs", +// EqualsHelper.nullSafeEquals(contentUrlSource, contentUrlCopy)); +// } +// public void testImmediateRemoval() throws Exception { cleaner.setProtectDays(0); @@ -307,30 +311,6 @@ public class ContentStoreCleanerTest extends TestCase assertTrue("Protected content was deleted", store.exists(contentUrl)); assertFalse("Content listener was called with deletion of protected URL", deletedUrls.contains(contentUrl)); } - -// public void testConcurrentRemoval() throws Exception -// { -// int threadCount = 2; -// final CountDownLatch endLatch = new CountDownLatch(threadCount); -// // Kick off the threads -// for (int i = 0; i < threadCount; i++) -// { -// Thread thread = new Thread() -// { -// @Override -// public void run() -// { -// cleaner.execute(); -// // Notify of completion -// endLatch.countDown(); -// } -// }; -// thread.start(); -// } -// // Wait for them all to be done -// endLatch.await(); -// } -// private class DummyCleanerListener implements ContentStoreCleanerListener { public void beforeDelete(ContentStore store, String contentUrl) throws ContentIOException diff --git a/source/java/org/alfresco/repo/content/cleanup/EagerContentStoreCleaner.java b/source/java/org/alfresco/repo/content/cleanup/EagerContentStoreCleaner.java new file mode 100644 index 0000000000..978c5269ab --- /dev/null +++ b/source/java/org/alfresco/repo/content/cleanup/EagerContentStoreCleaner.java @@ -0,0 +1,241 @@ +/* + * Copyright (C) 2005-2007 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.content.cleanup; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import org.alfresco.repo.content.ContentStore; +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.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.PropertyCheck; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * This component is responsible cleaning up orphaned content. + *

+ * Clean-up happens at two levels.

+ * Eager cleanup: (since 3.2)

+ * If {@link #setEagerOrphanCleanup(boolean) eager cleanup} is activated, then this + * component listens to all content property change events and recorded for post-transaction + * processing. All orphaned content is deleted from the registered store(s). Note that + * any {@link #setListeners(List) listeners} are called as normal; backup or scrubbing + * procedures should be plugged in as listeners if this is required. + *

+ * Lazy cleanup:

+ * This is triggered by means of a {@link ContentStoreCleanupJob Quartz job}. This is + * a heavy-weight process that effectively compares the database metadata with the + * content URLs controlled by the various stores. Once again, the listeners are called + * appropriately. + *

+ * How backup policies are affected:

+ * When restoring the system from a backup, the type of restore required is dictated by + * the cleanup policy being enforced. If eager cleanup is active, the system must
+ * (a) have a listeners configured to backup the deleted content + * e.g. {@link DeletedContentBackupCleanerListener}, or
+ * (b) ensure consistent backups across the database and content stores: backup + * when the system is not running; use a DB-based content store. This is the + * recommended route when running with eager cleanup. + *

+ * Lazy cleanup protects the content for a given period (e.g. 7 days) giving plenty of + * time for a backup to be taken; this allows hot backup without needing metadata-content + * consistency to be enforced. + * + * @author Derek Hulley + */ +public class EagerContentStoreCleaner extends TransactionListenerAdapter +{ + /** + * Content URLs to delete once the transaction commits. + * @see #onContentPropertyUpdate(NodeRef, QName, ContentData, ContentData) + * @see #afterCommit() + */ + private static final String KEY_POST_COMMIT_DELETION_URLS = "ContentStoreCleaner.PostCommitDeletionUrls"; + /** + * Content URLs to delete if the transaction rolls b ack. + * @see #onContentPropertyUpdate(NodeRef, QName, ContentData, ContentData) + * @see #afterRollback() + */ + private static final String KEY_POST_ROLLBACK_DELETION_URLS = "ContentStoreCleaner.PostRollbackDeletionUrls"; + + private static Log logger = LogFactory.getLog(EagerContentStoreCleaner.class); + + private boolean eagerOrphanCleanup; + private List stores; + private List listeners; + + public EagerContentStoreCleaner() + { + this.stores = new ArrayList(0); + this.listeners = new ArrayList(0); + } + + /** + * @param eagerOrphanCleanup true to enable this component, otherwise false + */ + public void setEagerOrphanCleanup(boolean eagerOrphanCleanup) + { + this.eagerOrphanCleanup = eagerOrphanCleanup; + } + + /** + * @param stores the content stores to clean + */ + public void setStores(List stores) + { + this.stores = stores; + } + + /** + * @param listeners the listeners that can react to deletions + */ + public void setListeners(List listeners) + { + this.listeners = listeners; + } + + /** + * Initializes the cleaner based on the {@link #setEagerOrphanCleanup(boolean) eagerCleanup} flag. + */ + public void init() + { + checkProperties(); + } + + /** + * Perform basic checks to ensure that the necessary dependencies were injected. + */ + private void checkProperties() + { + PropertyCheck.mandatory(this, "listeners", listeners); + } + + /** + * Queues orphaned content for post-transaction removal + */ + public void registerNewContentUrl(String contentUrl) + { + if (!eagerOrphanCleanup) + { + return; + } + Set urlsToDelete = TransactionalResourceHelper.getSet(KEY_POST_ROLLBACK_DELETION_URLS); + urlsToDelete.add(contentUrl); + // Register to listen for transaction rollback + AlfrescoTransactionSupport.bindListener(this); + } + + /** + * Queues orphaned content for post-transaction removal + */ + public void registerOrphanedContentUrl(String contentUrl) + { + if (!eagerOrphanCleanup) + { + return; + } + Set urlsToDelete = TransactionalResourceHelper.getSet(KEY_POST_COMMIT_DELETION_URLS); + urlsToDelete.add(contentUrl); + // Register to listen for transaction commit + AlfrescoTransactionSupport.bindListener(this); + } + + /** + * Cleans up all newly-orphaned content + */ + @Override + public void afterCommit() + { + Set urlsToDelete = TransactionalResourceHelper.getSet(KEY_POST_COMMIT_DELETION_URLS); + // Debug + if (logger.isDebugEnabled()) + { + logger.debug("Post-commit deletion of old content URLs: "); + int count = 0; + for (String contentUrl : urlsToDelete) + { + if (count == 10) + { + logger.debug(" " + (urlsToDelete.size() - 10) + " more ..."); + } + else + { + logger.debug(" Deleting content URL: " + contentUrl); + } + count++; + } + } + // Delete + for (String contentUrl : urlsToDelete) + { + for (ContentStore store : stores) + { + for (ContentStoreCleanerListener listener : listeners) + { + listener.beforeDelete(store, contentUrl); + } + store.delete(contentUrl); + } + } + } + + @Override + public void afterRollback() + { + Set urlsToDelete = TransactionalResourceHelper.getSet(KEY_POST_ROLLBACK_DELETION_URLS); + // Debug + if (logger.isDebugEnabled()) + { + logger.debug("Post-rollback deletion of new content URLs: "); + int count = 0; + for (String contentUrl : urlsToDelete) + { + if (count == 10) + { + logger.debug(" " + (urlsToDelete.size() - 10) + " more ..."); + } + else + { + logger.debug(" Deleting content URL: " + contentUrl); + } + count++; + } + } + // Delete, but don't call the listeners - these URLs never did get metadata + for (String contentUrl : urlsToDelete) + { + for (ContentStore store : stores) + { + store.delete(contentUrl); + } + } + } +} diff --git a/source/java/org/alfresco/repo/deploy/DeploymentServiceImpl.java b/source/java/org/alfresco/repo/deploy/DeploymentServiceImpl.java index 1763132906..0d5c1f74ff 100644 --- a/source/java/org/alfresco/repo/deploy/DeploymentServiceImpl.java +++ b/source/java/org/alfresco/repo/deploy/DeploymentServiceImpl.java @@ -1644,9 +1644,12 @@ public class DeploymentServiceImpl implements DeploymentService */ public void makeLock() { - jobLockService.getTransacionalLock(lockQName, getTargetLockTimeToLive(), getTargetLockRetryWait(), getTargetLockRetryCount()); + jobLockService.getTransactionalLock(lockQName, getTargetLockTimeToLive(), getTargetLockRetryWait(), getTargetLockRetryCount()); lockTime = new Date().getTime(); - fgLogger.debug("lock taken" + lockQName); + if (fgLogger.isDebugEnabled()) + { + fgLogger.debug("lock taken" + lockQName); + } } public void refreshLock() @@ -1657,8 +1660,11 @@ public class DeploymentServiceImpl implements DeploymentService Date now = new Date(); if(now.getTime() - lockTime > (targetLockTimeToLive / 2)) { - fgLogger.debug("lock refreshed" + lockQName); - jobLockService.getTransacionalLock(lockQName, getTargetLockTimeToLive(), getTargetLockRetryWait(), getTargetLockRetryCount()); + if (fgLogger.isDebugEnabled()) + { + fgLogger.debug("lock refreshed" + lockQName); + } + jobLockService.getTransactionalLock(lockQName, getTargetLockTimeToLive(), getTargetLockRetryWait(), getTargetLockRetryCount()); lockTime = new Date().getTime(); } } diff --git a/source/java/org/alfresco/repo/domain/ContentUrlDAO.java b/source/java/org/alfresco/repo/domain/ContentUrlDAO.java deleted file mode 100644 index b881238d3a..0000000000 --- a/source/java/org/alfresco/repo/domain/ContentUrlDAO.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright (C) 2005-2007 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; - -import java.util.Set; - -/** - * Abstraction for manipulating Content URL entities. - * - * @author Derek Hulley - * @since 2.0 - */ -public interface ContentUrlDAO -{ - /** - * Create a new Content URL or get an existing instance. - */ - ContentUrl createContentUrl(String contentUrl); - - /** - * Enumerate all the available Content URLs, calling back to the given handler. - * - * @param handler the component that will be called with each URL - */ - void getAllContentUrls(ContentUrlHandler handler); - - /** - * Delete the Content URL. - */ - void deleteContentUrl(String contentUrl); - - /** - * Delete a set of Content URL. - */ - void deleteContentUrls(Set contentUrls); - - /** - * Delete all Content URL entities. - */ - void deleteAllContentUrls(); - - /** - * A callback interface to handle Content URLS produced by iteration. - * - * @author Derek Hulley - * @since 2.0 - */ - public interface ContentUrlHandler - { - void handle(String contentUrl); - }; -} diff --git a/source/java/org/alfresco/repo/domain/ContentUrlDAOTest.java b/source/java/org/alfresco/repo/domain/ContentUrlDAOTest.java deleted file mode 100644 index 41b259aaa1..0000000000 --- a/source/java/org/alfresco/repo/domain/ContentUrlDAOTest.java +++ /dev/null @@ -1,240 +0,0 @@ -package org.alfresco.repo.domain; - -import java.util.HashSet; -import java.util.Set; - -import javax.transaction.UserTransaction; - -import junit.framework.TestCase; - -import org.alfresco.repo.content.filestore.FileContentStore; -import org.alfresco.repo.security.authentication.AuthenticationUtil; -import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; -import org.alfresco.service.ServiceRegistry; -import org.alfresco.service.cmr.repository.ContentService; -import org.alfresco.service.transaction.TransactionService; -import org.alfresco.util.ApplicationContextHelper; -import org.springframework.context.ApplicationContext; - -/** - * @see org.alfresco.repo.domain.ContentUrlDAO - * - * @author Derek Hulley - * @since 2.1 - */ -public class ContentUrlDAOTest extends TestCase -{ - private static ApplicationContext ctx = ApplicationContextHelper.getApplicationContext(); - - private ContentUrlDAO dao; - private TransactionService transactionService; - private ContentService contentService; - - @Override - protected void setUp() throws Exception - { - ServiceRegistry serviceRegistry = (ServiceRegistry) ctx.getBean(ServiceRegistry.SERVICE_REGISTRY); - dao = (ContentUrlDAO) ctx.getBean("contentUrlDAO"); - contentService = serviceRegistry.getContentService(); - transactionService = serviceRegistry.getTransactionService(); - } - - @Override - protected void tearDown() throws Exception - { - } - - public void testCreateContentUrl() throws Throwable - { - UserTransaction txn = transactionService.getUserTransaction(); - try - { - txn.begin(); - - RunAsWork getTempWriterWork = new RunAsWork() - { - public String doWork() throws Exception - { - return contentService.getTempWriter().getContentUrl(); - } - }; - String contentUrl = AuthenticationUtil.runAs(getTempWriterWork, AuthenticationUtil.SYSTEM_USER_NAME); - // Make sure that it can be written in duplicate - ContentUrl entity1 = dao.createContentUrl(contentUrl); - ContentUrl entity2 = dao.createContentUrl(contentUrl); - assertNotSame("Assigned IDs must be new", entity1.getId(), entity2.getId()); - - txn.commit(); - } - catch (Throwable e) - { - try { txn.rollback(); } catch (Throwable ee) {} - throw e; - } - } - - private Set makeUrls(int count) throws Throwable - { - final Set urls = new HashSet(count); - for (int i = 0; i < count; i++) - { - String contentUrl = String.format("%s%s/%04d", FileContentStore.STORE_PROTOCOL, getName(), i); - dao.createContentUrl(contentUrl); - urls.add(contentUrl); - } - return urls; - } - - public void testGetAllContentUrls() throws Throwable - { - UserTransaction txn = transactionService.getUserTransaction(); - try - { - txn.begin(); - - final Set urls = makeUrls(1000); - - // Now iterate over them in the same transaction - ContentUrlDAO.ContentUrlHandler handler = new ContentUrlDAO.ContentUrlHandler() - { - public void handle(String contentUrl) - { - urls.remove(contentUrl); - } - }; - dao.getAllContentUrls(handler); - assertEquals("Not all content URLs were enumerated", 0, urls.size()); - - txn.commit(); - } - catch (Throwable e) - { - try { txn.rollback(); } catch (Throwable ee) {} - throw e; - } - } - - public void testDeleteContentUrl() throws Throwable - { - UserTransaction txn = transactionService.getUserTransaction(); - try - { - txn.begin(); - - final Set urls = makeUrls(1000); - // Delete them - for (String url : urls) - { - dao.deleteContentUrl(url); - } - // Now iterate over them in the same transaction - ContentUrlDAO.ContentUrlHandler handler = new ContentUrlDAO.ContentUrlHandler() - { - public void handle(String contentUrl) - { - urls.remove(contentUrl); - } - }; - dao.getAllContentUrls(handler); - // All the URLs previously deleted will not have been removed from the Set - assertEquals("Specific content URLs were not deleted", 1000, urls.size()); - - txn.commit(); - } - catch (Throwable e) - { - try { txn.rollback(); } catch (Throwable ee) {} - throw e; - } - } - - public void testDeleteContentUrlSpeed() throws Throwable - { - UserTransaction txn = transactionService.getUserTransaction(); - try - { - txn.begin(); - - final Set urls = makeUrls(1000); - // Delete them - long startTimeNs = System.nanoTime(); - for (String url : urls) - { - dao.deleteContentUrl(url); - } - long endTimeNs = System.nanoTime(); - double aveTimeMs = (double) (endTimeNs - startTimeNs) / 1000000D / 1000D; - - System.out.println("Average delete is " + aveTimeMs + "ms per content URL"); - - txn.commit(); - } - catch (Throwable e) - { - try { txn.rollback(); } catch (Throwable ee) {} - throw e; - } - } - - public void testDeleteContentUrls() throws Throwable - { - UserTransaction txn = transactionService.getUserTransaction(); - try - { - txn.begin(); - - final Set urls = makeUrls(1000); - // Delete them - dao.deleteContentUrls(urls); - // Now iterate over them in the same transaction - ContentUrlDAO.ContentUrlHandler handler = new ContentUrlDAO.ContentUrlHandler() - { - public void handle(String contentUrl) - { - urls.remove(contentUrl); - } - }; - dao.getAllContentUrls(handler); - // All the URLs previously deleted will not have been removed from the Set - assertEquals("Specific content URLs were not deleted", 1000, urls.size()); - - txn.commit(); - } - catch (Throwable e) - { - try { txn.rollback(); } catch (Throwable ee) {} - throw e; - } - } - - public void testDeleteAllContentUrls() throws Throwable - { - UserTransaction txn = transactionService.getUserTransaction(); - try - { - txn.begin(); - - makeUrls(1000); - // Delete them - dao.deleteAllContentUrls(); - // Check that there are none left - - // Now iterate over them in the same transaction - ContentUrlDAO.ContentUrlHandler handler = new ContentUrlDAO.ContentUrlHandler() - { - public void handle(String contentUrl) - { - fail("There should not be any URLs remaining."); - } - }; - dao.getAllContentUrls(handler); - - txn.commit(); - } - catch (Throwable e) - { - try { txn.rollback(); } catch (Throwable ee) {} - throw e; - } - } -} diff --git a/source/java/org/alfresco/repo/domain/NodePropertyValue.java b/source/java/org/alfresco/repo/domain/NodePropertyValue.java index f1860d51d3..b69ffbf8f0 100644 --- a/source/java/org/alfresco/repo/domain/NodePropertyValue.java +++ b/source/java/org/alfresco/repo/domain/NodePropertyValue.java @@ -266,13 +266,27 @@ public class NodePropertyValue implements Cloneable, Serializable @Override protected ValueType getPersistedType(Serializable value) { - return ValueType.STRING; + if (value instanceof Long) + { + return ValueType.LONG; + } + else + { + return ValueType.STRING; + } } @Override Serializable convert(Serializable value) { - return DefaultTypeConverter.INSTANCE.convert(ContentData.class, value); + if (value instanceof Long) + { + return value; + } + else + { + return DefaultTypeConverter.INSTANCE.convert(ContentData.class, value); + } } }, NODEREF diff --git a/source/java/org/alfresco/repo/domain/contentclean/ContentCleanDAO.java b/source/java/org/alfresco/repo/domain/contentclean/ContentCleanDAO.java new file mode 100644 index 0000000000..a92ace1e36 --- /dev/null +++ b/source/java/org/alfresco/repo/domain/contentclean/ContentCleanDAO.java @@ -0,0 +1,59 @@ +/* + * 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/hibernate/ContentUrlImpl.java b/source/java/org/alfresco/repo/domain/contentclean/ContentCleanEntity.java similarity index 52% rename from source/java/org/alfresco/repo/domain/hibernate/ContentUrlImpl.java rename to source/java/org/alfresco/repo/domain/contentclean/ContentCleanEntity.java index 5335b3cc8e..2d42359376 100644 --- a/source/java/org/alfresco/repo/domain/hibernate/ContentUrlImpl.java +++ b/source/java/org/alfresco/repo/domain/contentclean/ContentCleanEntity.java @@ -1,86 +1,89 @@ -/* - * Copyright (C) 2005-2007 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.hibernate; - -import java.io.Serializable; - -import org.alfresco.repo.domain.ContentUrl; - -/** - * Bean containing all the persistence data representing a Content Url. - *

- * This implementation of the {@link org.alfresco.repo.domain.Node Node} interface is - * Hibernate specific. - * - * @author Derek Hulley - * @since 2.0 - */ -public class ContentUrlImpl extends LifecycleAdapter implements ContentUrl, Serializable -{ - private static final long serialVersionUID = -7368859912728834288L; - - private Long id; - private String contentUrl; -// private boolean isOrphaned; - - public ContentUrlImpl() - { -// isOrphaned = false; - } - - public Long getId() - { - return id; - } - - /** - * For Hibernate Use - */ - @SuppressWarnings("unused") - private void setId(Long id) - { - this.id = id; - } - - public String getContentUrl() - { - return contentUrl; - } - - public void setContentUrl(String contentUrl) - { - this.contentUrl = contentUrl; - } -// -// public boolean isOrphaned() -// { -// return isOrphaned; -// } -// -// public void setOrphaned(boolean isOrphaned) -// { -// this.isOrphaned = isOrphaned; -// } -} +/* + * 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 new file mode 100644 index 0000000000..94e617fa8b --- /dev/null +++ b/source/java/org/alfresco/repo/domain/contentclean/ibatis/ContentCleanDAOImpl.java @@ -0,0 +1,254 @@ +/* + * 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 = "insert.ContentCleanUrl"; + private static final String SELECT_CONTENT_CLEAN_URLS = "select.ContentCleanUrls"; + private static final String DELETE_CONTENT_CLEAN_BY_URL = "delete.ContentCleanUrl"; + private static final String DELETE_CONTENT_CLEAN = "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); + 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 new file mode 100644 index 0000000000..5f05f0772f --- /dev/null +++ b/source/java/org/alfresco/repo/domain/contentdata/AbstractContentDataDAOImpl.java @@ -0,0 +1,318 @@ +/* + * 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.contentdata; + +import java.io.Serializable; +import java.util.Locale; + +import org.alfresco.repo.cache.SimpleCache; +import org.alfresco.repo.content.cleanup.EagerContentStoreCleaner; +import org.alfresco.repo.domain.LocaleDAO; +import org.alfresco.repo.domain.encoding.EncodingDAO; +import org.alfresco.repo.domain.mimetype.MimetypeDAO; +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.util.Pair; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.dao.ConcurrencyFailureException; + +/** + * Abstract implementation for ContentData DAO. + *

+ * This provides basic services such as caching, but defers to the underlying implementation + * for CRUD operations. + *

+ * The DAO deals in {@link ContentData} instances. The cache is primarily present to decode + * IDs into ContentData instances. + * + * @author Derek Hulley + * @since 3.2 + */ +public abstract class AbstractContentDataDAOImpl implements ContentDataDAO +{ + private static Log logger = LogFactory.getLog(AbstractContentDataDAOImpl.class); + + private static final Long CACHE_NULL_LONG = Long.MIN_VALUE; + + private MimetypeDAO mimetypeDAO; + private EncodingDAO encodingDAO; + private LocaleDAO localeDAO; + private EagerContentStoreCleaner contentStoreCleaner; + private SimpleCache contentDataCache; + + public void setMimetypeDAO(MimetypeDAO mimetypeDAO) + { + this.mimetypeDAO = mimetypeDAO; + } + + public void setEncodingDAO(EncodingDAO encodingDAO) + { + this.encodingDAO = encodingDAO; + } + + public void setLocaleDAO(LocaleDAO localeDAO) + { + this.localeDAO = localeDAO; + } + + /** + * Set this property to enable eager cleanup of orphaned content. + * + * @param contentStoreCleaner an eager cleaner (may be null) + */ + public void setContentStoreCleaner(EagerContentStoreCleaner contentStoreCleaner) + { + this.contentStoreCleaner = contentStoreCleaner; + } + + /** + * @param contentDataCache the cache of IDs to ContentData and vice versa + */ + public void setContentDataCache(SimpleCache contentDataCache) + { + this.contentDataCache = contentDataCache; + } + + /** + * Register new content for post-rollback handling + */ + protected void registerNewContentUrl(String contentUrl) + { + if (contentStoreCleaner != null) + { + contentStoreCleaner.registerNewContentUrl(contentUrl); + } + } + + /** + * Register orphaned content for post-commit handling + */ + protected void registerOrphanedContentUrl(String contentUrl) + { + if (contentStoreCleaner != null) + { + contentStoreCleaner.registerOrphanedContentUrl(contentUrl); + } + } + + /** + * {@inheritDoc} + */ + public Pair createContentData(ContentData contentData) + { + /* + * TODO: Cache + */ + ContentDataEntity contentDataEntity = createContentDataEntity(contentData); + // Done + return new Pair(contentDataEntity.getId(), contentData); + } + + /** + * {@inheritDoc} + */ + public Pair getContentData(Long id) + { + /* + * TODO: Cache + */ + ContentDataEntity contentDataEntity = getContentDataEntity(id); + if (contentDataEntity == null) + { + return null; + } + // Convert back to ContentData + ContentData contentData = makeContentData(contentDataEntity); + // Done + return new Pair(id, contentData); + } + + /** + * {@inheritDoc} + */ + public void deleteContentData(Long id) + { + int deleted = deleteContentDataEntity(id); + if (deleted < 1) + { + throw new ConcurrencyFailureException("ContetntData with ID " + id + " no longer exists"); + } + return; + } + + /** + * Translates this instance into an externally-usable ContentData instance. + */ + private ContentData makeContentData(ContentDataEntity contentDataEntity) + { + // Decode content URL + String contentUrl = contentDataEntity.getContentUrl(); + long size = contentDataEntity.getSize() == null ? 0L : contentDataEntity.getSize().longValue(); + // Decode mimetype + Long mimetypeId = contentDataEntity.getMimetypeId(); + String mimetype = null; + if (mimetypeId != null) + { + mimetype = mimetypeDAO.getMimetype(mimetypeId).getSecond(); + } + // Decode encoding + Long encodingId = contentDataEntity.getEncodingId(); + String encoding = null; + if (encodingId != null) + { + encoding = encodingDAO.getEncoding(encodingId).getSecond(); + } + // Decode locale + Long localeId = contentDataEntity.getLocaleId(); + Locale locale = null; + if (localeId != null) + { + locale = localeDAO.getLocalePair(localeId).getSecond(); + } + // Build the ContentData + ContentData contentData = new ContentData(contentUrl, mimetype, size, encoding, locale); + // Done + return contentData; + } + + /** + * Translates the {@link ContentData} into persistable values using the helper DAOs + */ + private ContentDataEntity createContentDataEntity(ContentData contentData) + { + // Resolve the content URL + Long contentUrlId = null; + String contentUrl = contentData.getContentUrl(); + long size = contentData.getSize(); + if (contentUrl != null) + { + // We must find or create the ContentUrlEntity + contentUrlId = getOrCreateContentUrlEntity(contentUrl, size).getId(); + } + // 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(); + } + + // Create ContentDataEntity + ContentDataEntity contentDataEntity = createContentDataEntity(contentUrlId, mimetypeId, encodingId, localeId); + // Done + return 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 + if (contentUrlEntity != null) + { + // Reuse it + long existingSize = contentUrlEntity.getSize(); + if (size != existingSize) + { + logger.warn( + "Re-using Content URL, but size is mismatched: \n" + + " Inbound: " + contentUrl + "\n" + + " Existing: " + contentUrlEntity); + } + } + else + { + // Create it + contentUrlEntity = createContentUrlEntity(contentUrl, size); + } + // Done + return contentUrlEntity; + } + + /** + * @param contentUrl the content URL to create or search for + */ + protected abstract ContentUrlEntity createContentUrlEntity(String contentUrl, long size); + + /** + * @param id the ID of the content url entity + * @return Return the entity or null if it doesn't exist + */ + protected abstract ContentUrlEntity getContentUrlEntity(Long id); + + /** + * @param contentUrl the URL of the content url entity + * @return Return the entity or null if it doesn't exist + */ + protected abstract ContentUrlEntity getContentUrlEntity(String contentUrl); + + /** + * Delete the entity with the given ID + * @return Returns the number of rows deleted + */ + protected abstract int deleteContentUrlEntity(Long id); + + /** + * Create the row for the alf_content_data + */ + protected abstract ContentDataEntity createContentDataEntity( + Long contentUrlId, + Long mimetypeId, + Long encodingId, + Long localeId); + + /** + * @param id the entity ID + * @return Returns the entity or null if it doesn't exist + */ + protected abstract ContentDataEntity getContentDataEntity(Long id); + + /** + * Delete the entity with the given ID + * + * @return Returns the number of rows deleted + */ + protected abstract int deleteContentDataEntity(Long id); +} diff --git a/source/java/org/alfresco/repo/domain/contentdata/ContentDataDAO.java b/source/java/org/alfresco/repo/domain/contentdata/ContentDataDAO.java new file mode 100644 index 0000000000..f9ee71b818 --- /dev/null +++ b/source/java/org/alfresco/repo/domain/contentdata/ContentDataDAO.java @@ -0,0 +1,89 @@ +/* + * 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.contentdata; + +import java.util.Set; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.util.Pair; +import org.springframework.dao.ConcurrencyFailureException; + +/** + * DAO services for alf_content_data table + * + * @author Derek Hulley + * @since 3.2 + */ +public interface ContentDataDAO +{ + /** + * Create a new ContentData instance. + * + * @param contentData the ContentData details + * @return the ContentData pair (id, ContentData) (never null) + */ + Pair createContentData(ContentData contentData); + + /** + * @param id the unique ID of the entity + * @return the ContentData pair (id, ContentData) or null if it doesn't exist + * @throws AlfrescoRuntimeException if the ID provided is invalid + */ + Pair getContentData(Long id); + + /** + * Delete an instance of content. + * @param id the unique ID of the entity + * @throws ConcurrencyFailureException if the ID does not exist + */ + void deleteContentData(Long id); + + /** + * Deletes all alf_content_data rows that are referenced by the given node + * + * @param nodeId the node ID + * @param qnameIds the content properties to target + */ + void deleteContentDataForNode(Long nodeId, Set qnameIds); + + /** + * Interface for callbacks during content URL enumeration + * + * @author Derek Hulley + * @since 3.2 + */ + public static interface ContentUrlHandler + { + void handle(String contentUrl); + } + + /** + * Enumerate all available content URLs + * + * @param contentUrlHandler + */ + void getAllContentUrls(ContentUrlHandler contentUrlHandler); +} diff --git a/source/java/org/alfresco/repo/domain/contentdata/ContentDataDAOTest.java b/source/java/org/alfresco/repo/domain/contentdata/ContentDataDAOTest.java new file mode 100644 index 0000000000..6fcf399a9b --- /dev/null +++ b/source/java/org/alfresco/repo/domain/contentdata/ContentDataDAOTest.java @@ -0,0 +1,303 @@ +/* + * 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.contentdata; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import junit.framework.TestCase; + +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.transaction.RetryingTransactionHelper; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.ApplicationContextHelper; +import org.alfresco.util.Pair; +import org.alfresco.util.TempFileProvider; +import org.springframework.context.ConfigurableApplicationContext; + +/** + * @see ContentDataDAO + * + * @author Derek Hulley + * @since 3.2 + */ +public class ContentDataDAOTest extends TestCase +{ + private ConfigurableApplicationContext ctx = (ConfigurableApplicationContext) ApplicationContextHelper.getApplicationContext(); + + private TransactionService transactionService; + private RetryingTransactionHelper txnHelper; + private ContentDataDAO contentDataDAO; + private ContentStore contentStore; + + @Override + public void setUp() throws Exception + { + ServiceRegistry serviceRegistry = (ServiceRegistry) ctx.getBean(ServiceRegistry.SERVICE_REGISTRY); + transactionService = serviceRegistry.getTransactionService(); + txnHelper = transactionService.getRetryingTransactionHelper(); + + contentDataDAO = (ContentDataDAO) ctx.getBean("contentDataDAO"); + contentStore = new FileContentStore(ctx, TempFileProvider.getTempDir()); + } + + private Pair create(final ContentData contentData) + { + RetryingTransactionCallback> callback = new RetryingTransactionCallback>() + { + public Pair execute() throws Throwable + { + Pair contentDataPair = contentDataDAO.createContentData(contentData); + return contentDataPair; + } + }; + return txnHelper.doInTransaction(callback, false, false); + } + + /** + * Retrieves and checks the ContentData for equality + */ + private void getAndCheck(final Long contentDataId, ContentData checkContentData) + { + RetryingTransactionCallback> callback = new RetryingTransactionCallback>() + { + public Pair execute() throws Throwable + { + Pair contentDataPair = contentDataDAO.getContentData(contentDataId); + return contentDataPair; + } + }; + Pair resultPair = txnHelper.doInTransaction(callback, true, false); + assertNotNull("Failed to find result for ID " + contentDataId, resultPair); + assertEquals("ContentData retrieved not the same as persisted: ", checkContentData, resultPair.getSecond()); + } + + private ContentData getContentData() + { + ContentContext contentCtx = new ContentContext(null, null); + String contentUrl = contentStore.getWriter(contentCtx).getContentUrl(); + ContentData contentData = new ContentData( + contentUrl, + MimetypeMap.MIMETYPE_TEXT_PLAIN, + 12335L, + "UTF-8", + Locale.FRENCH); + return contentData; + } + + public void testGetWithInvalidId() + { + assertNull("Expected null for invalid ID", contentDataDAO.getContentData(-1L)); + } + + /** + * Check that the ContentData is decoded and persisted correctly. + */ + public void testCreateContentDataSimple() throws Exception + { + ContentData contentData = getContentData(); + + Pair resultPair = create(contentData); + getAndCheck(resultPair.getFirst(), contentData); + } + + /** + * Check that the ContentData is decoded and persisted correctly. + */ + public void testCreateContentDataNulls() throws Exception + { + ContentData contentData = new ContentData(null, null, 0L, null, null); + + Pair resultPair = create(contentData); + getAndCheck(resultPair.getFirst(), contentData); + } + + /** + * Ensure that upper and lowercase URLs don't clash + * @throws Exception + */ + public void testEnsureCaseSensitiveStorage() throws Exception + { + ContentData contentData = getContentData(); + String contentUrlUpper = contentData.getContentUrl().toUpperCase(); + ContentData contentDataUpper = new ContentData( + contentUrlUpper, MimetypeMap.MIMETYPE_TEXT_PLAIN, 0L, "UTF-8", new Locale("FR")); + String contentUrlLower = contentData.getContentUrl().toLowerCase(); + ContentData contentDataLower = new ContentData( + contentUrlLower, MimetypeMap.MIMETYPE_TEXT_PLAIN, 0L, "utf-8", new Locale("fr")); + + Pair resultPairUpper = create(contentDataUpper); + getAndCheck(resultPairUpper.getFirst(), contentDataUpper); + + Pair resultPairLower = create(contentDataLower); + getAndCheck(resultPairLower.getFirst(), contentDataLower); + } + + public void testDelete() throws Exception + { + ContentData contentData = getContentData(); + + Pair resultPair = create(contentData); + getAndCheck(resultPair.getFirst(), contentData); + contentDataDAO.deleteContentData(resultPair.getFirst()); + try + { + getAndCheck(resultPair.getFirst(), contentData); + fail("Entity still exists"); + } + catch (Throwable e) + { + // Expected + } + } + + private static final String[] MIMETYPES = new String[] + { + MimetypeMap.MIMETYPE_ACP, + MimetypeMap.MIMETYPE_EXCEL, + MimetypeMap.MIMETYPE_IMAGE_JPEG, + MimetypeMap.MIMETYPE_JAVASCRIPT, + MimetypeMap.MIMETYPE_RSS + }; + private static final String[] ENCODINGS = new String[] + { + "utf-8", + "ascii", + "latin1", + "wibbles", + "iso-whatever" + }; + private static final Locale[] LOCALES = new Locale[] + { + Locale.FRENCH, + Locale.CHINESE, + Locale.ITALIAN, + Locale.JAPANESE, + Locale.ENGLISH + }; + + private List> speedTestWrite(String name, int total) + { + System.out.println("Starting write speed test: " + name); + long start = System.nanoTime(); + List> pairs = new ArrayList>(100000); + // Loop and check for performance degradation + for (int i = 0; i < (total / 200 / 5); i++) + { + for (int j = 0; j < 200; j++) + { + for (int k = 0; k < 5; k++) + { + ContentData contentData = getContentData(); + String contentUrl = contentData.getContentUrl(); + contentData = new ContentData( + contentUrl, + MIMETYPES[k], + (long) j*k, + ENCODINGS[k], + LOCALES[k]); + Pair pair = create(contentData); + pairs.add(pair); + } + } + // That's 1000 + long now = System.nanoTime(); + double diffMs = (double) (now - start) / 1E6; + double aveMs = diffMs / (double) pairs.size(); + String msg = String.format( + " Wrote %7d rows; average is %5.2f ms per row or %5.2f rows per second", + pairs.size(), + aveMs, + 1000.0 / aveMs); + System.out.println(msg); + } + // Done + return pairs; + } + + private void speedTestRead(String name, List> pairs) + { + System.out.println("Starting read speed test: " + name); + long start = System.nanoTime(); + // Loop and check for performance degradation + int num = 1; + for (Pair pair : pairs) + { + Long id = pair.getFirst(); + ContentData contentData = pair.getSecond(); + // Retrieve it + getAndCheck(id, contentData); + // Report + if (num % 1000 == 0) + { + long now = System.nanoTime(); + double diffMs = (double) (now - start) / 1E6; + double aveMs = diffMs / (double) num; + String msg = String.format( + " Read %7d rows; average is %5.2f ms per row or %5.2f rows per second", + num, + aveMs, + 1000.0 / aveMs); + System.out.println(msg); + } + num++; + } + // Done + } + + public void testCreateSpeedIndividualTxns() + { + List> pairs = speedTestWrite(getName(), 2000); + speedTestRead(getName(), pairs); + } + + public void testCreateSpeedSingleTxn() + { + RetryingTransactionCallback>> writeCallback = new RetryingTransactionCallback>>() + { + public List> execute() throws Throwable + { + return speedTestWrite(getName(), 10000); + } + }; + final List> pairs = txnHelper.doInTransaction(writeCallback, false, false); + RetryingTransactionCallback readCallback = new RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + speedTestRead(getName(), pairs); + return null; + } + }; + txnHelper.doInTransaction(readCallback, false, false); + } +} diff --git a/source/java/org/alfresco/repo/domain/contentdata/ContentDataEntity.java b/source/java/org/alfresco/repo/domain/contentdata/ContentDataEntity.java new file mode 100644 index 0000000000..61b2c7c637 --- /dev/null +++ b/source/java/org/alfresco/repo/domain/contentdata/ContentDataEntity.java @@ -0,0 +1,174 @@ +/* + * 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.contentdata; + +import org.alfresco.util.EqualsHelper; + +/** + * Entity bean for alf_content_data 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 ContentDataEntity +{ + public static final Long CONST_LONG_ZERO = new Long(0L); + + private Long id; + private Long version; + private Long contentUrlId; + private String contentUrl; + private Long size; + private Long mimetypeId; + private Long encodingId; + private Long localeId; + + public ContentDataEntity() + { + } + + @Override + public int hashCode() + { + return (id == null ? 0 : id.hashCode()); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + { + return true; + } + else if (obj instanceof ContentDataEntity) + { + ContentDataEntity that = (ContentDataEntity) obj; + return EqualsHelper.nullSafeEquals(this.id, that.id); + } + else + { + return false; + } + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(512); + sb.append("ContentDataEntity") + .append("[ ID=").append(id) + .append(", contentUrlId=").append(contentUrlId) + .append(", contentUrl=").append(contentUrl) + .append(", size=").append(size) + .append(", mimetype=").append(mimetypeId) + .append(", encoding=").append(encodingId) + .append(", locale=").append(localeId) + .append("]"); + return sb.toString(); + } + + public Long getId() + { + return id; + } + + public void setId(Long id) + { + this.id = id; + } + + public Long getVersion() + { + return version; + } + + public void setVersion(Long version) + { + this.version = version; + } + + public Long getContentUrlId() + { + return contentUrlId; + } + + public void setContentUrlId(Long contentUrlId) + { + this.contentUrlId = contentUrlId; + } + + public String getContentUrl() + { + return contentUrl; + } + + public void setContentUrl(String contentUrl) + { + this.contentUrl = contentUrl; + } + + public Long getSize() + { + return size; + } + + public void setSize(Long size) + { + this.size = size; + } + + public Long getMimetypeId() + { + return mimetypeId; + } + + public void setMimetypeId(Long mimetypeId) + { + this.mimetypeId = mimetypeId; + } + + public Long getEncodingId() + { + return encodingId; + } + + public void setEncodingId(Long encodingId) + { + this.encodingId = encodingId; + } + + public Long getLocaleId() + { + return localeId; + } + + public void setLocaleId(Long localeId) + { + this.localeId = localeId; + } +} diff --git a/source/java/org/alfresco/repo/domain/contentdata/ContentUrlEntity.java b/source/java/org/alfresco/repo/domain/contentdata/ContentUrlEntity.java new file mode 100644 index 0000000000..3dbcd77429 --- /dev/null +++ b/source/java/org/alfresco/repo/domain/contentdata/ContentUrlEntity.java @@ -0,0 +1,245 @@ +/* + * 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.contentdata; + +import java.io.UnsupportedEncodingException; +import java.util.zip.CRC32; + +import org.alfresco.util.EqualsHelper; +import org.alfresco.util.Pair; + +/** + * 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 ContentUrlEntity +{ + public static final Long CONST_LONG_ZERO = new Long(0L); + 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; + + public ContentUrlEntity() + { + this.size = 0L; + } + + @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 ContentUrlEntity) + { + ContentUrlEntity that = (ContentUrlEntity) obj; + return EqualsHelper.nullSafeEquals(this.contentUrl, that.contentUrl); + } + else + { + return false; + } + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(512); + sb.append("ContentUrlEntity") + .append("[ ID=").append(id) + .append(", contentUrl=").append(contentUrl) + .append(", size=").append(size) + .append("]"); + return sb.toString(); + } + + /** + * @param + * @return Returns a pair of the short (12 chars lowercase) URL and the CRC value + */ + private static Pair getContentUrlCrcPair(String internalContentUrl) + { + if (internalContentUrl == null) + { + return new Pair(null, null); + } + + // Calculate the CRC value + CRC32 crc = new CRC32(); + try + { + crc.update(internalContentUrl.getBytes("UTF-8")); + } + catch (UnsupportedEncodingException e) + { + throw new RuntimeException("UTF-8 encoding is not supported"); + } + // Get the short name (case-insensitive) + String contentUrlShort = null; + int contentUrlLen = internalContentUrl.length(); + if (contentUrlLen < 12) + { + contentUrlShort = internalContentUrl.toLowerCase(); + } + else + { + contentUrlShort = internalContentUrl.toLowerCase().substring(contentUrlLen - 12); + } + // Done + return new Pair(contentUrlShort, crc.getValue()); + } + + private static String getInternalUrl(String contentUrl) + { + if (contentUrl == null) + { + return null; + } + // Deal with Oracle's NULL-EMPTY confusion + if (contentUrl.length() == 0) + { + return EMPTY_URL; + } + else + { + return contentUrl; + } + } + + /** + * @return Returns the originally-set content URL + */ + private static String getExternalUrl(String contentUrl) + { + if (contentUrl == null) + { + return null; + } + // Decode Oracle's NULL-EMPTY confusion + if (contentUrl.equals(EMPTY_URL)) + { + return ""; + } + else + { + return contentUrl; + } + } + + public Long getId() + { + return id; + } + + public void setId(Long id) + { + 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 + return ContentUrlEntity.getExternalUrl(contentUrl); + } + + public void setContentUrl(String contentUrl) + { + this.contentUrl = contentUrl; + // Convert the URL to a persistable value + String internalContentUrl = ContentUrlEntity.getInternalUrl(contentUrl); + Pair contentUrlPair = ContentUrlEntity.getContentUrlCrcPair(internalContentUrl); + this.contentUrlShort = contentUrlPair.getFirst(); + this.contentUrlCrc = contentUrlPair.getSecond(); + } + + /** + * For persistence use + */ + public String getContentUrlShort() + { + return contentUrlShort; + } + + /** + * For persistence use + */ + public void setContentUrlShort(String contentUrlShort) + { + this.contentUrlShort = contentUrlShort; + } + + /** + * For persistence use + */ + public long getContentUrlCrc() + { + return contentUrlCrc; + } + + /** + * For persistence use + */ + public void setContentUrlCrc(long contentUrlCrc) + { + this.contentUrlCrc = contentUrlCrc; + } + + public long getSize() + { + return size; + } + + public void setSize(long size) + { + this.size = size; + } +} diff --git a/source/java/org/alfresco/repo/domain/contentdata/ibatis/ContentDataDAOImpl.java b/source/java/org/alfresco/repo/domain/contentdata/ibatis/ContentDataDAOImpl.java new file mode 100644 index 0000000000..5076fd857e --- /dev/null +++ b/source/java/org/alfresco/repo/domain/contentdata/ibatis/ContentDataDAOImpl.java @@ -0,0 +1,196 @@ +/* + * 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.contentdata.ibatis; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.repo.domain.contentdata.AbstractContentDataDAOImpl; +import org.alfresco.repo.domain.contentdata.ContentDataEntity; +import org.alfresco.repo.domain.contentdata.ContentUrlEntity; +import org.springframework.orm.ibatis.SqlMapClientTemplate; + +import com.ibatis.sqlmap.client.event.RowHandler; + +/** + * iBatis-specific implementation of the ContentData DAO. + * + * @author Derek Hulley + * @since 3.2 + */ +public class ContentDataDAOImpl extends AbstractContentDataDAOImpl +{ + private static final String SELECT_CONTENT_URL_BY_ID = "select.ContentUrlById"; + private static final String SELECT_CONTENT_URL_BY_KEY = "select.ContentUrlByKey"; + private static final String SELECT_CONTENT_URLS = "select.ContentUrls"; + private static final String SELECT_CONTENT_DATA_BY_ID = "select.ContentDataById"; + private static final String SELECT_CONTENT_DATA_BY_NODE_AND_QNAME = "select.ContentDataByNodeAndQName"; + private static final String SELECT_CONTENT_DATA_BY_URL_ID = "select.ContentDataByContentUrlId"; + private static final String INSERT_CONTENT_URL = "insert.ContentUrl"; + private static final String INSERT_CONTENT_DATA = "insert.ContentData"; + private static final String DELETE_CONTENT_DATA = "delete.ContentData"; + private static final String DELETE_CONTENT_URL = "delete.ContentUrl"; + + private SqlMapClientTemplate template; + + public void setSqlMapClientTemplate(SqlMapClientTemplate sqlMapClientTemplate) + { + this.template = sqlMapClientTemplate; + } + + @Override + protected ContentUrlEntity createContentUrlEntity(String contentUrl, long size) + { + ContentUrlEntity contentUrlEntity = new ContentUrlEntity(); + contentUrlEntity.setVersion(ContentUrlEntity.CONST_LONG_ZERO); + contentUrlEntity.setContentUrl(contentUrl); + contentUrlEntity.setSize(size); + /* Long id = (Long) */ template.insert(INSERT_CONTENT_URL, contentUrlEntity); + /*contentUrlEntity.setId(id);*/ + // Register the url as new + registerNewContentUrl(contentUrl); + // Done + return contentUrlEntity; + } + + @Override + protected ContentUrlEntity getContentUrlEntity(Long id) + { + ContentUrlEntity contentUrlEntity = new ContentUrlEntity(); + contentUrlEntity.setId(id); + contentUrlEntity = (ContentUrlEntity) template.queryForObject(SELECT_CONTENT_URL_BY_ID, contentUrlEntity); + // Done + return contentUrlEntity; + } + + @Override + protected ContentUrlEntity getContentUrlEntity(String contentUrl) + { + ContentUrlEntity contentUrlEntity = new ContentUrlEntity(); + contentUrlEntity.setContentUrl(contentUrl); + contentUrlEntity = (ContentUrlEntity) template.queryForObject(SELECT_CONTENT_URL_BY_KEY, contentUrlEntity); + // Done + return contentUrlEntity; + } + + @Override + protected int deleteContentUrlEntity(Long id) + { + Map params = new HashMap(11); + params.put("id", id); + return template.delete(DELETE_CONTENT_URL, params); + } + + @Override + protected ContentDataEntity createContentDataEntity( + Long contentUrlId, + Long mimetypeId, + Long encodingId, + Long localeId) + { + ContentDataEntity contentDataEntity = new ContentDataEntity(); + contentDataEntity.setVersion(ContentDataEntity.CONST_LONG_ZERO); + contentDataEntity.setContentUrlId(contentUrlId); + contentDataEntity.setMimetypeId(mimetypeId); + contentDataEntity.setEncodingId(encodingId); + contentDataEntity.setLocaleId(localeId); + template.insert(INSERT_CONTENT_DATA, contentDataEntity); + // Done + return contentDataEntity; + } + + @Override + protected ContentDataEntity getContentDataEntity(Long id) + { + Map params = new HashMap(11); + params.put("id", id); + ContentDataEntity contentDataEntity = (ContentDataEntity) template.queryForObject(SELECT_CONTENT_DATA_BY_ID, params); + // Done + return contentDataEntity; + } + + @Override + protected int deleteContentDataEntity(Long id) + { + Map params = new HashMap(11); + params.put("id", id); + return template.delete(DELETE_CONTENT_DATA, params); + } + + public void deleteContentDataForNode(Long nodeId, Set qnameIds) + { + /* + * TODO: use IN clause in parameters + */ + for (Long qnameId : qnameIds) + { + // Get the ContentData that matches (may be multiple due to collection properties) + Map params = new HashMap(11); + params.put("nodeId", nodeId); + params.put("qnameId", qnameId); + @SuppressWarnings("unchecked") + List ids = (List) template.queryForList(SELECT_CONTENT_DATA_BY_NODE_AND_QNAME, params); + // Delete each one + for (Long id : ids) + { + // Get the content urls + ContentDataEntity contentDataEntity = getContentDataEntity(id); + // Only check the content URLs if one is present + String contentUrl = contentDataEntity.getContentUrl(); + Long contentUrlId = contentDataEntity.getContentUrlId(); + // Delete the ContentData entity + deleteContentData(id); + // Check if the content URL was orphaned + if (contentUrlId != null) + { + params.clear(); + params.put("id", contentUrlId); + @SuppressWarnings("unchecked") + List contentDataEntities = (List) template.queryForList(SELECT_CONTENT_DATA_BY_URL_ID, params); + // If there is still ContentData associated with the content URL, then leave it + if (contentDataEntities.size() == 0) + { + // Orphaned + registerOrphanedContentUrl(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/domain/encoding/AbstractEncodingDAOImpl.java b/source/java/org/alfresco/repo/domain/encoding/AbstractEncodingDAOImpl.java new file mode 100644 index 0000000000..025e89507b --- /dev/null +++ b/source/java/org/alfresco/repo/domain/encoding/AbstractEncodingDAOImpl.java @@ -0,0 +1,141 @@ +/* + * 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.encoding; + +import java.io.Serializable; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.repo.cache.SimpleCache; +import org.alfresco.util.Pair; +import org.alfresco.util.ParameterCheck; + +/** + * Abstract implementation for Encoding DAO. + *

+ * This provides basic services such as caching, but defers to the underlying implementation + * for CRUD operations. + * + * @author Derek Hulley + * @since 3.2 + */ +public abstract class AbstractEncodingDAOImpl implements EncodingDAO +{ + private static final Long CACHE_NULL_LONG = Long.MIN_VALUE; + private static final String NULL_SAFE_STRING = ".null"; + private SimpleCache encodingEntityCache; + + /** + * + * @param encodingEntityCache the cache of IDs to mimetypes + */ + public void setEncodingEntityCache(SimpleCache encodingEntityCache) + { + this.encodingEntityCache = encodingEntityCache; + } + + public Pair getEncoding(Long id) + { + // Check the cache + String encoding = (String) encodingEntityCache.get(id); + if (encoding != null) + { + return new Pair(id, encoding); + } + // Get it from the DB + EncodingEntity mimetypeEntity = getEncodingEntity(id); + if (mimetypeEntity == null) + { + throw new AlfrescoRuntimeException("The MimetypeEntity ID " + id + " doesn't exist."); + } + encoding = mimetypeEntity.getEncoding(); + // Cache it + encodingEntityCache.put(encoding, id); + encodingEntityCache.put(id, encoding); + // Done + return new Pair(id, encoding); + } + + public Pair getEncoding(String encoding) + { + ParameterCheck.mandatory("encoding", encoding); + + // Check the cache + Long id = (Long) encodingEntityCache.get(encoding); + if (id != null) + { + if (id.equals(CACHE_NULL_LONG)) + { + return null; + } + else + { + return new Pair(id, encoding); + } + } + // It's not in the cache, so query + EncodingEntity result = getEncodingEntity(encoding); + if (result == null) + { + // Cache it + encodingEntityCache.put(encoding, CACHE_NULL_LONG); + // Done + return null; + } + else + { + id = result.getId(); + // Cache it + encodingEntityCache.put(id, encoding); + encodingEntityCache.put(encoding, id); + // Done + return new Pair(id, encoding); + } + } + + public Pair getOrCreateEncoding(String encoding) + { + ParameterCheck.mandatory("encoding", encoding); + + Pair result = getEncoding(encoding); + if (result == null) + { + EncodingEntity encodingEntity = createEncodingEntity(encoding); + Long id = encodingEntity.getId(); + result = new Pair(id, encoding); + // Cache it + encodingEntityCache.put(id, encoding); + encodingEntityCache.put(encoding, id); + } + return result; + } + + /** + * @param id the ID of the encoding entity + * @return Return the entity or null if it doesn't exist + */ + protected abstract EncodingEntity getEncodingEntity(Long id); + protected abstract EncodingEntity getEncodingEntity(String encoding); + protected abstract EncodingEntity createEncodingEntity(String encoding); +} diff --git a/source/java/org/alfresco/repo/domain/ContentUrl.java b/source/java/org/alfresco/repo/domain/encoding/EncodingDAO.java similarity index 64% rename from source/java/org/alfresco/repo/domain/ContentUrl.java rename to source/java/org/alfresco/repo/domain/encoding/EncodingDAO.java index 949e3a4b0a..86453ed563 100644 --- a/source/java/org/alfresco/repo/domain/ContentUrl.java +++ b/source/java/org/alfresco/repo/domain/encoding/EncodingDAO.java @@ -1,49 +1,48 @@ -/* - * Copyright (C) 2005-2007 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; - -/** - * Interface for persistent Content URL objects. - *

- * Instances represent physically stored content. - * - * @author Derek Hulley - * @since 2.0 - */ -public interface ContentUrl -{ - /** - * @return Returns the auto-generated ID - */ - Long getId(); - - String getContentUrl(); - - void setContentUrl(String contentUrl); -// -// boolean isOrphaned(); -// -// void setOrphaned(boolean orphaned); -} +/* + * 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.encoding; + +import org.alfresco.util.Pair; + +/** + * DAO services for alf_encoding and related tables + * + * @author Derek Hulley + * @since 3.2 + */ +public interface EncodingDAO +{ + /** + * Get the encoding pair. + * + * @param encoding the encoding string + * @return the ID-encoding pair or null if it doesn't exsit + */ + Pair getEncoding(String encoding); + + Pair getEncoding(Long id); + + Pair getOrCreateEncoding(String encoding); +} diff --git a/source/java/org/alfresco/repo/domain/encoding/EncodingDAOTest.java b/source/java/org/alfresco/repo/domain/encoding/EncodingDAOTest.java new file mode 100644 index 0000000000..04ec30cd36 --- /dev/null +++ b/source/java/org/alfresco/repo/domain/encoding/EncodingDAOTest.java @@ -0,0 +1,145 @@ +/* + * 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.encoding; + +import junit.framework.TestCase; + +import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.ApplicationContextHelper; +import org.alfresco.util.GUID; +import org.alfresco.util.Pair; +import org.springframework.context.ApplicationContext; + +/** + * @see EncodingDAO + * + * @author Derek Hulley + * @since 3.2 + */ +public class EncodingDAOTest extends TestCase +{ + private ApplicationContext ctx = ApplicationContextHelper.getApplicationContext(); + + private TransactionService transactionService; + private RetryingTransactionHelper txnHelper; + private EncodingDAO encodingDAO; + + @Override + public void setUp() throws Exception + { + ServiceRegistry serviceRegistry = (ServiceRegistry) ctx.getBean(ServiceRegistry.SERVICE_REGISTRY); + transactionService = serviceRegistry.getTransactionService(); + txnHelper = transactionService.getRetryingTransactionHelper(); + + encodingDAO = (EncodingDAO) ctx.getBean("encodingDAO"); + } + + private Pair get(final String encoding, final boolean autoCreate, boolean expectSuccess) + { + RetryingTransactionCallback> callback = new RetryingTransactionCallback>() + { + public Pair execute() throws Throwable + { + Pair mimetypePair = null; + if (autoCreate) + { + mimetypePair = encodingDAO.getOrCreateEncoding(encoding); + } + else + { + mimetypePair = encodingDAO.getEncoding(encoding); + } + return mimetypePair; + } + }; + try + { + return txnHelper.doInTransaction(callback, !autoCreate, false); + } + catch (Throwable e) + { + if (expectSuccess) + { + // oops + throw new RuntimeException("Expected to get encoding '" + encoding + "'.", e); + } + else + { + return null; + } + } + } + + public void testCreateWithCommit() throws Exception + { + // Create an encoding + String encoding = GUID.generate(); + Pair encodingPair = get(encoding, true, true); + // Check that it can be retrieved + Pair encodingPairCheck = get(encodingPair.getSecond(), false, true); + assertEquals("Encoding ID changed", encodingPair.getFirst(), encodingPairCheck.getFirst()); + } + + public void testCreateWithRollback() throws Exception + { + final String encoding = GUID.generate(); + // Create an encoding + RetryingTransactionCallback> callback = new RetryingTransactionCallback>() + { + public Pair execute() throws Throwable + { + get(encoding, true, true); + // Now force a rollback + throw new RuntimeException("Forced"); + } + }; + try + { + txnHelper.doInTransaction(callback); + fail("Transaction didn't roll back"); + } + catch (RuntimeException e) + { + // Expected + } + // Check that it doesn't exist + get(encoding, false, false); + } + + public void testCaseInsensitivity() throws Exception + { + String encoding = "AAA-" + GUID.generate(); + Pair lowercasePair = get(encoding.toLowerCase(), true, true); + // Check that the same pair is retrievable using uppercase + Pair uppercasePair = get(encoding.toUpperCase(), true, true); + assertNotNull(uppercasePair); + assertEquals( + "Upper and lowercase encoding instance IDs were not the same", + lowercasePair.getFirst(), uppercasePair.getFirst()); + } +} diff --git a/source/java/org/alfresco/repo/domain/encoding/EncodingEntity.java b/source/java/org/alfresco/repo/domain/encoding/EncodingEntity.java new file mode 100644 index 0000000000..5d499b9cbf --- /dev/null +++ b/source/java/org/alfresco/repo/domain/encoding/EncodingEntity.java @@ -0,0 +1,110 @@ +/* + * 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.encoding; + +import org.alfresco.util.EqualsHelper; + +/** + * Entity bean for alf_encoding table. + *

+ * These are unique (see {@link #equals(Object) equals} and {@link #hashCode() hashCode}) based + * on the {@link #getEncoding() encoding} value. + * + * @author Derek Hulley + * @since 3.2 + */ +public class EncodingEntity +{ + public static final Long CONST_LONG_ZERO = new Long(0L); + + private Long id; + private Long version; + private String encoding; + + @Override + public int hashCode() + { + return (encoding == null ? 0 : encoding.hashCode()); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + { + return true; + } + else if (obj instanceof EncodingEntity) + { + EncodingEntity that = (EncodingEntity) obj; + return EqualsHelper.nullSafeEquals(this.encoding, that.encoding); + } + else + { + return false; + } + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(512); + sb.append("EncodingEntity") + .append("[ ID=").append(id) + .append(", encoding=").append(encoding) + .append("]"); + return sb.toString(); + } + + public Long getId() + { + return id; + } + + public void setId(Long id) + { + this.id = id; + } + + public Long getVersion() + { + return version; + } + + public void setVersion(Long version) + { + this.version = version; + } + + public String getEncoding() + { + return encoding; + } + + public void setEncoding(String encoding) + { + this.encoding = encoding; + } +} diff --git a/source/java/org/alfresco/repo/domain/encoding/ibatis/EncodingDAOImpl.java b/source/java/org/alfresco/repo/domain/encoding/ibatis/EncodingDAOImpl.java new file mode 100644 index 0000000000..60492bff8f --- /dev/null +++ b/source/java/org/alfresco/repo/domain/encoding/ibatis/EncodingDAOImpl.java @@ -0,0 +1,82 @@ +/* + * 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.encoding.ibatis; + +import org.alfresco.repo.domain.encoding.AbstractEncodingDAOImpl; +import org.alfresco.repo.domain.encoding.EncodingEntity; +import org.alfresco.repo.domain.mimetype.MimetypeEntity; +import org.springframework.orm.ibatis.SqlMapClientTemplate; + +/** + * iBatis-specific implementation of the Mimetype DAO. + * + * @author Derek Hulley + * @since 3.2 + */ +public class EncodingDAOImpl extends AbstractEncodingDAOImpl +{ + private static final String SELECT_ENCODING_BY_ID = "select.EncodingById"; + private static final String SELECT_ENCODING_BY_KEY = "select.EncodingByKey"; + private static final String INSERT_ENCODING = "insert.Encoding"; + + private SqlMapClientTemplate template; + + public void setSqlMapClientTemplate(SqlMapClientTemplate sqlMapClientTemplate) + { + this.template = sqlMapClientTemplate; + } + + @Override + protected EncodingEntity getEncodingEntity(Long id) + { + EncodingEntity encodingEntity = new EncodingEntity(); + encodingEntity.setId(id); + encodingEntity = (EncodingEntity) template.queryForObject(SELECT_ENCODING_BY_ID, encodingEntity); + // Done + return encodingEntity; + } + + @Override + protected EncodingEntity getEncodingEntity(String encoding) + { + EncodingEntity encodingEntity = new EncodingEntity(); + encodingEntity.setEncoding(encoding); + encodingEntity = (EncodingEntity) template.queryForObject(SELECT_ENCODING_BY_KEY, encodingEntity); + // Could be null + return encodingEntity; + } + + @Override + protected EncodingEntity createEncodingEntity(String encoding) + { + EncodingEntity encodingEntity = new EncodingEntity(); + encodingEntity.setVersion(MimetypeEntity.CONST_LONG_ZERO); + encodingEntity.setEncoding(encoding); + Long id = (Long) template.insert(INSERT_ENCODING, encodingEntity); + encodingEntity.setId(id); + // Done + return encodingEntity; + } +} diff --git a/source/java/org/alfresco/repo/domain/hibernate/ContentUrl.hbm.xml b/source/java/org/alfresco/repo/domain/hibernate/ContentUrl.hbm.xml deleted file mode 100644 index 5325aaaaa0..0000000000 --- a/source/java/org/alfresco/repo/domain/hibernate/ContentUrl.hbm.xml +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - - - - - - - - - - - - select - entity.contentUrl - from - org.alfresco.repo.domain.hibernate.ContentUrlImpl entity - - - - delete - from - org.alfresco.repo.domain.hibernate.ContentUrlImpl entity - where - entity.contentUrl in (:contentUrls) - - - - delete - from - org.alfresco.repo.domain.hibernate.ContentUrlImpl entity - where - entity.contentUrl = :contentUrl - - - - delete - from - org.alfresco.repo.domain.hibernate.ContentUrlImpl entity - - - diff --git a/source/java/org/alfresco/repo/domain/hibernate/HibernateContentUrlDAOImpl.java b/source/java/org/alfresco/repo/domain/hibernate/HibernateContentUrlDAOImpl.java deleted file mode 100644 index 9036602157..0000000000 --- a/source/java/org/alfresco/repo/domain/hibernate/HibernateContentUrlDAOImpl.java +++ /dev/null @@ -1,164 +0,0 @@ -package org.alfresco.repo.domain.hibernate; - -import java.util.Set; - -import org.alfresco.repo.domain.ContentUrl; -import org.alfresco.repo.domain.ContentUrlDAO; -import org.alfresco.repo.transaction.AlfrescoTransactionSupport; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.hibernate.FlushMode; -import org.hibernate.Query; -import org.hibernate.ScrollMode; -import org.hibernate.ScrollableResults; -import org.hibernate.Session; -import org.hibernate.type.TypeFactory; -import org.springframework.orm.hibernate3.HibernateCallback; -import org.springframework.orm.hibernate3.support.HibernateDaoSupport; - -/** - * Hibernate-specific implementation of the DAO layer for Content URLs. - * - * @author Derek Hulley - * @since 2.0 - */ -public class HibernateContentUrlDAOImpl extends HibernateDaoSupport implements ContentUrlDAO -{ - private static final String QUERY_GET_ALL = "contentUrl.GetAll"; - private static final String UPDATE_DELETE_BY_URL = "contentUrl.DeleteByUrl"; - private static final String UPDATE_DELETE_IN_LIST = "contentUrl.DeleteInList"; - private static final String UPDATE_DELETE_ALL = "contentUrl.DeleteAll"; - - /** Txn resource key to check for required flushes */ - private static final String KEY_REQUIRES_FLUSH = "HibernateContentUrlDAOImpl.requiresFlush"; - - private static Log logger = LogFactory.getLog(HibernateContentUrlDAOImpl.class); - - private void flushIfRequired() - { - Boolean requiresFlush = (Boolean) AlfrescoTransactionSupport.getResource(KEY_REQUIRES_FLUSH); - if (requiresFlush == null) - { - requiresFlush = Boolean.FALSE; - AlfrescoTransactionSupport.bindResource(KEY_REQUIRES_FLUSH, Boolean.FALSE); - } - else if (requiresFlush.booleanValue() == true) - { - getSession().flush(); - AlfrescoTransactionSupport.bindResource(KEY_REQUIRES_FLUSH, Boolean.FALSE); - } - } - - public ContentUrl createContentUrl(String contentUrl) - { - AlfrescoTransactionSupport.bindResource(KEY_REQUIRES_FLUSH, Boolean.TRUE); - ContentUrl entity = new ContentUrlImpl(); - entity.setContentUrl(contentUrl); - getSession().save(entity); - return entity; - } - - public void getAllContentUrls(final ContentUrlHandler handler) - { - // Force a flush if there are pending changes - flushIfRequired(); - - HibernateCallback callback = new HibernateCallback() - { - public Object doInHibernate(Session session) - { - Query query = session - .getNamedQuery(HibernateContentUrlDAOImpl.QUERY_GET_ALL) - ; - return query.scroll(ScrollMode.FORWARD_ONLY); - } - }; - ScrollableResults results = null; - try - { - results = (ScrollableResults) getHibernateTemplate().execute(callback); - while (results.next()) - { - String contentUrl = results.getText(0); - handler.handle(contentUrl); - } - } - finally - { - if(results != null) - { - results.close(); - } - } - } - - public void deleteContentUrl(final String contentUrl) - { - // Force a flush if there are pending changes - flushIfRequired(); - - HibernateCallback callback = new HibernateCallback() - { - public Object doInHibernate(Session session) - { - Query query = session - .getNamedQuery(HibernateContentUrlDAOImpl.UPDATE_DELETE_BY_URL) - .setFlushMode(FlushMode.MANUAL) - .setString("contentUrl", contentUrl); - return (Integer) query.executeUpdate(); - } - }; - Integer deletedCount = (Integer) getHibernateTemplate().execute(callback); - if (logger.isDebugEnabled()) - { - logger.debug("Deleted " + deletedCount + " ContentUrl entities."); - } - } - - public void deleteContentUrls(final Set contentUrls) - { - // Force a flush if there are pending changes - flushIfRequired(); - - HibernateCallback callback = new HibernateCallback() - { - public Object doInHibernate(Session session) - { - Query query = session - .getNamedQuery(HibernateContentUrlDAOImpl.UPDATE_DELETE_IN_LIST) - .setFlushMode(FlushMode.MANUAL) - .setParameterList("contentUrls", contentUrls, TypeFactory.basic("string")); - return (Integer) query.executeUpdate(); - } - }; - Integer deletedCount = (Integer) getHibernateTemplate().execute(callback); - if (logger.isDebugEnabled()) - { - logger.debug("Deleted " + deletedCount + " ContentUrl entities."); - } - } - - public void deleteAllContentUrls() - { - // Force a flush if there are pending changes - flushIfRequired(); - - HibernateCallback callback = new HibernateCallback() - { - public Object doInHibernate(Session session) - { - session.flush(); - Query query = session - .getNamedQuery(HibernateContentUrlDAOImpl.UPDATE_DELETE_ALL) - .setFlushMode(FlushMode.MANUAL) - ; - return (Integer) query.executeUpdate(); - } - }; - Integer deletedCount = (Integer) getHibernateTemplate().execute(callback); - if (logger.isDebugEnabled()) - { - logger.debug("Deleted " + deletedCount + " ContentUrl entities."); - } - } -} diff --git a/source/java/org/alfresco/repo/domain/hibernate/HibernateLocaleDAOImpl.java b/source/java/org/alfresco/repo/domain/hibernate/HibernateLocaleDAOImpl.java index d37f9985cc..6c8e51e07b 100644 --- a/source/java/org/alfresco/repo/domain/hibernate/HibernateLocaleDAOImpl.java +++ b/source/java/org/alfresco/repo/domain/hibernate/HibernateLocaleDAOImpl.java @@ -120,6 +120,10 @@ public class HibernateLocaleDAOImpl extends HibernateDaoSupport implements Local // Add the cache entry localeIdCache.put(id, localeStr); localeIdCache.put(localeStr, id); + + // Force a flush + DirtySessionMethodInterceptor.flushSession(getSession(), true); + // Done if (logger.isDebugEnabled()) { diff --git a/source/java/org/alfresco/repo/domain/mimetype/AbstractMimetypeDAOImpl.java b/source/java/org/alfresco/repo/domain/mimetype/AbstractMimetypeDAOImpl.java new file mode 100644 index 0000000000..327a239e53 --- /dev/null +++ b/source/java/org/alfresco/repo/domain/mimetype/AbstractMimetypeDAOImpl.java @@ -0,0 +1,141 @@ +/* + * 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.mimetype; + +import java.io.Serializable; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.repo.cache.SimpleCache; +import org.alfresco.util.Pair; +import org.alfresco.util.ParameterCheck; + +/** + * Abstract implementation for Mimetype DAO. + *

+ * This provides basic services such as caching, but defers to the underlying implementation + * for CRUD operations. + * + * @author Derek Hulley + * @since 3.2 + */ +public abstract class AbstractMimetypeDAOImpl implements MimetypeDAO +{ + private static final Long CACHE_NULL_LONG = Long.MIN_VALUE; + private static final String NULL_SAFE_STRING = ".null"; + private SimpleCache mimetypeEntityCache; + + /** + * + * @param mimetypeEntityCache the cache of IDs to mimetypes + */ + public void setMimetypeEntityCache(SimpleCache mimetypeEntityCache) + { + this.mimetypeEntityCache = mimetypeEntityCache; + } + + public Pair getMimetype(Long id) + { + // Check the cache + String mimetype = (String) mimetypeEntityCache.get(id); + if (mimetype != null) + { + return new Pair(id, mimetype); + } + // Get it from the DB + MimetypeEntity mimetypeEntity = getMimetypeEntity(id); + if (mimetypeEntity == null) + { + throw new AlfrescoRuntimeException("The MimetypeEntity ID " + id + " doesn't exist."); + } + mimetype = mimetypeEntity.getMimetype(); + // Cache it + mimetypeEntityCache.put(mimetype, id); + mimetypeEntityCache.put(id, mimetype); + // Done + return new Pair(id, mimetype); + } + + public Pair getMimetype(String mimetype) + { + ParameterCheck.mandatory("mimetype", mimetype); + + // Check the cache + Long id = (Long) mimetypeEntityCache.get(mimetype); + if (id != null) + { + if (id.equals(CACHE_NULL_LONG)) + { + return null; + } + else + { + return new Pair(id, mimetype); + } + } + // It's not in the cache, so query + MimetypeEntity result = getMimetypeEntity(mimetype); + if (result == null) + { + // Cache it + mimetypeEntityCache.put(mimetype, CACHE_NULL_LONG); + // Done + return null; + } + else + { + id = result.getId(); + // Cache it + mimetypeEntityCache.put(id, mimetype); + mimetypeEntityCache.put(mimetype, id); + // Done + return new Pair(id, mimetype); + } + } + + public Pair getOrCreateMimetype(String mimetype) + { + ParameterCheck.mandatory("mimetype", mimetype); + + Pair result = getMimetype(mimetype); + if (result == null) + { + MimetypeEntity mimetypeEntity = createMimetypeEntity(mimetype); + Long id = mimetypeEntity.getId(); + result = new Pair(id, mimetype); + // Cache it + mimetypeEntityCache.put(id, mimetype); + mimetypeEntityCache.put(mimetype, id); + } + return result; + } + + /** + * @param id the ID of the mimetype entity + * @return Return the entity or null if it doesn't exist + */ + protected abstract MimetypeEntity getMimetypeEntity(Long id); + protected abstract MimetypeEntity getMimetypeEntity(String mimetype); + protected abstract MimetypeEntity createMimetypeEntity(String mimetype); +} diff --git a/source/java/org/alfresco/repo/domain/mimetype/MimetypeDAO.java b/source/java/org/alfresco/repo/domain/mimetype/MimetypeDAO.java new file mode 100644 index 0000000000..ea01f663f3 --- /dev/null +++ b/source/java/org/alfresco/repo/domain/mimetype/MimetypeDAO.java @@ -0,0 +1,58 @@ +/* + * 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.mimetype; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.util.Pair; + +/** + * DAO services for alf_mimetype table + * + * @author Derek Hulley + * @since 3.2 + */ +public interface MimetypeDAO +{ + /** + * @param id the unique ID of the entity + * @return the Mimetype pair (id, mimetype) (never null) + * @throws AlfrescoRuntimeException if the ID provided is invalid + */ + Pair getMimetype(Long id); + + /** + * @param mimetype the Mimetype to query for + * @return the Mimetype pair (id, mimetype) or null if it doesn't exist + */ + Pair getMimetype(String mimetype); + + /** + * Retrieve an existing mimetype or create a new one if it doesn't exist. + * + * @param mimetype the Mimetype + * @return the Mimetype pair (id, mimetype) (never null) + */ + Pair getOrCreateMimetype(String mimetype); +} diff --git a/source/java/org/alfresco/repo/domain/mimetype/MimetypeDAOTest.java b/source/java/org/alfresco/repo/domain/mimetype/MimetypeDAOTest.java new file mode 100644 index 0000000000..08dc519852 --- /dev/null +++ b/source/java/org/alfresco/repo/domain/mimetype/MimetypeDAOTest.java @@ -0,0 +1,145 @@ +/* + * 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.mimetype; + +import junit.framework.TestCase; + +import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.ApplicationContextHelper; +import org.alfresco.util.GUID; +import org.alfresco.util.Pair; +import org.springframework.context.ApplicationContext; + +/** + * @see MimetypeDAO + * + * @author Derek Hulley + * @since 3.2 + */ +public class MimetypeDAOTest extends TestCase +{ + private ApplicationContext ctx = ApplicationContextHelper.getApplicationContext(); + + private TransactionService transactionService; + private RetryingTransactionHelper txnHelper; + private MimetypeDAO mimetypeDAO; + + @Override + public void setUp() throws Exception + { + ServiceRegistry serviceRegistry = (ServiceRegistry) ctx.getBean(ServiceRegistry.SERVICE_REGISTRY); + transactionService = serviceRegistry.getTransactionService(); + txnHelper = transactionService.getRetryingTransactionHelper(); + + mimetypeDAO = (MimetypeDAO) ctx.getBean("mimetypeDAO"); + } + + private Pair get(final String mimetype, final boolean autoCreate, boolean expectSuccess) + { + RetryingTransactionCallback> callback = new RetryingTransactionCallback>() + { + public Pair execute() throws Throwable + { + Pair mimetypePair = null; + if (autoCreate) + { + mimetypePair = mimetypeDAO.getOrCreateMimetype(mimetype); + } + else + { + mimetypePair = mimetypeDAO.getMimetype(mimetype); + } + return mimetypePair; + } + }; + try + { + return txnHelper.doInTransaction(callback, !autoCreate, false); + } + catch (Throwable e) + { + if (expectSuccess) + { + // oops + throw new RuntimeException("Expected to get mimetype '" + mimetype + "'.", e); + } + else + { + return null; + } + } + } + + public void testCreateWithCommit() throws Exception + { + // Create a mimetype + String mimetype = GUID.generate(); + Pair mimetypePair = get(mimetype, true, true); + // Check that it can be retrieved + Pair mimetypePairCheck = get(mimetypePair.getSecond(), false, true); + assertEquals("Mimetype ID changed", mimetypePair.getFirst(), mimetypePairCheck.getFirst()); + } + + public void testCreateWithRollback() throws Exception + { + final String mimetype = GUID.generate(); + // Create a mimetype + RetryingTransactionCallback> callback = new RetryingTransactionCallback>() + { + public Pair execute() throws Throwable + { + get(mimetype, true, true); + // Now force a rollback + throw new RuntimeException("Forced"); + } + }; + try + { + txnHelper.doInTransaction(callback); + fail("Transaction didn't roll back"); + } + catch (RuntimeException e) + { + // Expected + } + // Check that it doesn't exist + get(mimetype, false, false); + } + + public void testCaseInsensitivity() throws Exception + { + String mimetype = "AAA-" + GUID.generate(); + Pair lowercasePair = get(mimetype.toLowerCase(), true, true); + // Check that the same pair is retrievable using uppercase + Pair uppercasePair = get(mimetype.toUpperCase(), true, true); + assertNotNull(uppercasePair); + assertEquals( + "Upper and lowercase mimetype instance IDs were not the same", + lowercasePair.getFirst(), uppercasePair.getFirst()); + } +} diff --git a/source/java/org/alfresco/repo/domain/mimetype/MimetypeEntity.java b/source/java/org/alfresco/repo/domain/mimetype/MimetypeEntity.java new file mode 100644 index 0000000000..c2b9078dcb --- /dev/null +++ b/source/java/org/alfresco/repo/domain/mimetype/MimetypeEntity.java @@ -0,0 +1,110 @@ +/* + * 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.mimetype; + +import org.alfresco.util.EqualsHelper; + +/** + * Entity bean for alf_mimetype table. + *

+ * These are unique (see {@link #equals(Object) equals} and {@link #hashCode() hashCode}) based + * on the {@link #getMimetype() mimetype} value. + * + * @author Derek Hulley + * @since 3.2 + */ +public class MimetypeEntity +{ + public static final Long CONST_LONG_ZERO = new Long(0L); + + private Long id; + private Long version; + private String mimetype; + + @Override + public int hashCode() + { + return (mimetype == null ? 0 : mimetype.hashCode()); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + { + return true; + } + else if (obj instanceof MimetypeEntity) + { + MimetypeEntity that = (MimetypeEntity) obj; + return EqualsHelper.nullSafeEquals(this.mimetype, that.mimetype); + } + else + { + return false; + } + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(512); + sb.append("MimetypeEntity") + .append("[ ID=").append(id) + .append(", mimetype=").append(mimetype) + .append("]"); + return sb.toString(); + } + + public Long getId() + { + return id; + } + + public void setId(Long id) + { + this.id = id; + } + + public Long getVersion() + { + return version; + } + + public void setVersion(Long version) + { + this.version = version; + } + + public String getMimetype() + { + return mimetype; + } + + public void setMimetype(String mimetype) + { + this.mimetype = mimetype; + } +} diff --git a/source/java/org/alfresco/repo/domain/mimetype/ibatis/MimetypeDAOImpl.java b/source/java/org/alfresco/repo/domain/mimetype/ibatis/MimetypeDAOImpl.java new file mode 100644 index 0000000000..4cb78aa3aa --- /dev/null +++ b/source/java/org/alfresco/repo/domain/mimetype/ibatis/MimetypeDAOImpl.java @@ -0,0 +1,81 @@ +/* + * 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.mimetype.ibatis; + +import org.alfresco.repo.domain.mimetype.AbstractMimetypeDAOImpl; +import org.alfresco.repo.domain.mimetype.MimetypeEntity; +import org.springframework.orm.ibatis.SqlMapClientTemplate; + +/** + * iBatis-specific implementation of the Mimetype DAO. + * + * @author Derek Hulley + * @since 3.2 + */ +public class MimetypeDAOImpl extends AbstractMimetypeDAOImpl +{ + private static final String SELECT_MIMETYPE_BY_ID = "select.MimetypeById"; + private static final String SELECT_MIMETYPE_BY_KEY = "select.MimetypeByKey"; + private static final String INSERT_MIMETYPE = "insert.Mimetype"; + + private SqlMapClientTemplate template; + + public void setSqlMapClientTemplate(SqlMapClientTemplate sqlMapClientTemplate) + { + this.template = sqlMapClientTemplate; + } + + @Override + protected MimetypeEntity getMimetypeEntity(Long id) + { + MimetypeEntity mimetypeEntity = new MimetypeEntity(); + mimetypeEntity.setId(id); + mimetypeEntity = (MimetypeEntity) template.queryForObject(SELECT_MIMETYPE_BY_ID, mimetypeEntity); + // Done + return mimetypeEntity; + } + + @Override + protected MimetypeEntity getMimetypeEntity(String mimetype) + { + MimetypeEntity mimetypeEntity = new MimetypeEntity(); + mimetypeEntity.setMimetype(mimetype); + mimetypeEntity = (MimetypeEntity) template.queryForObject(SELECT_MIMETYPE_BY_KEY, mimetypeEntity); + // Could be null + return mimetypeEntity; + } + + @Override + protected MimetypeEntity createMimetypeEntity(String mimetype) + { + MimetypeEntity mimetypeEntity = new MimetypeEntity(); + mimetypeEntity.setVersion(MimetypeEntity.CONST_LONG_ZERO); + mimetypeEntity.setMimetype(mimetype); + Long id = (Long) template.insert(INSERT_MIMETYPE, mimetypeEntity); + mimetypeEntity.setId(id); + // Done + return mimetypeEntity; + } +} diff --git a/source/java/org/alfresco/repo/lock/JobLockService.java b/source/java/org/alfresco/repo/lock/JobLockService.java index 8edc2ecbea..6db55abc34 100644 --- a/source/java/org/alfresco/repo/lock/JobLockService.java +++ b/source/java/org/alfresco/repo/lock/JobLockService.java @@ -68,21 +68,63 @@ public interface JobLockService * @throws LockAcquisitionException if the lock could not be acquired * @throws IllegalStateException if a transaction is not active */ - void getTransacionalLock(QName lockQName, long timeToLive); + void getTransactionalLock(QName lockQName, long timeToLive); /** - * {@inheritDoc JobLockService#getTransacionalLock(QName, long)} + * {@inheritDoc JobLockService#getTransactionalLock(QName, long)} *

* If the lock cannot be immediately acquired, the process will wait and retry. Note * that second and subsequent attempts to get the lock during a transaction cannot * make use of retrying; the lock is actually being refreshed and will therefore never * become valid if it doesn't refresh directly. * - * @param timeToLive the time (in milliseconds) for the lock to remain valid * @param retryWait the time (in milliseconds) to wait before trying again * @param retryCount the maximum number of times to attempt the lock acquisition * @throws LockAcquisitionException if the lock could not be acquired * @throws IllegalStateException if a transaction is not active */ - void getTransacionalLock(QName lockQName, long timeToLive, long retryWait, int retryCount); + void getTransactionalLock(QName lockQName, long timeToLive, long retryWait, int retryCount); + + /** + * Take a manually-managed lock. The lock current thread or transaction will not be tagged - + * the returned lock token must be used for further management of the lock. + *

+ * No lock management is provided: the lock must be released manually or will only become + * available by expiry. No deadlock management is provided, either. + * + * @param lockQName the name of the lock to acquire + * @param timeToLive the time (in milliseconds) for the lock to remain valid + * @return Returns the newly-created lock token + * @throws LockAcquisitionException if the lock could not be acquired + */ + String getLock(QName lockQName, long timeToLive); + + /** + * {@inheritDoc JobLockService#getLock(QName, long)} + *

+ * If the lock cannot be immediately acquired, the process will wait and retry. + * + * @param retryWait the time (in milliseconds) to wait before trying again + * @param retryCount the maximum number of times to attempt the lock acquisition + * @throws LockAcquisitionException if the lock could not be acquired + */ + String getLock(QName lockQName, long timeToLive, long retryWait, int retryCount); + + /** + * Refresh the lock using a valid lock token. + * + * @param lockToken the lock token returned when the lock was acquired + * @param lockQName the name of the previously-acquired lock + * @param timeToLive the time (in milliseconds) for the lock to remain valid + * @throws LockAcquisitionException if the lock could not be refreshed or acquired + */ + void refreshLock(String lockToken, QName lockQName, long timeToLive); + + /** + * Release the lock using a valid lock token. + * + * @param lockToken the lock token returned when the lock was acquired + * @param lockQName the name of the previously-acquired lock + */ + void releaseLock(String lockToken, QName lockQName); } diff --git a/source/java/org/alfresco/repo/lock/JobLockServiceImpl.java b/source/java/org/alfresco/repo/lock/JobLockServiceImpl.java index 6555a37170..285d000653 100644 --- a/source/java/org/alfresco/repo/lock/JobLockServiceImpl.java +++ b/source/java/org/alfresco/repo/lock/JobLockServiceImpl.java @@ -33,6 +33,7 @@ import org.alfresco.repo.transaction.TransactionListenerAdapter; import org.alfresco.repo.transaction.TransactionalResourceHelper; import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; import org.alfresco.service.namespace.QName; +import org.alfresco.util.GUID; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -103,15 +104,15 @@ public class JobLockServiceImpl implements JobLockService /** * {@inheritDoc} */ - public void getTransacionalLock(QName lockQName, long timeToLive) + public void getTransactionalLock(QName lockQName, long timeToLive) { - getTransacionalLock(lockQName, timeToLive, defaultRetryWait, defaultRetryCount); + getTransactionalLock(lockQName, timeToLive, defaultRetryWait, defaultRetryCount); } /** * {@inheritDoc} */ - public void getTransacionalLock(QName lockQName, long timeToLive, long retryWait, int retryCount) + public void getTransactionalLock(QName lockQName, long timeToLive, long retryWait, int retryCount) { // Check that transaction is present final String txnId = AlfrescoTransactionSupport.getTransactionId(); @@ -127,7 +128,7 @@ public class JobLockServiceImpl implements JobLockService if (!added) { // It's a refresh. Ordering is not important here as we already hold the lock. - refreshLock(lockQName, timeToLive); + refreshLock(txnId, lockQName, timeToLive); } else { @@ -144,7 +145,7 @@ public class JobLockServiceImpl implements JobLockService } // If it was last in the set, then the order is correct and we use the // full retry behaviour. - getLock(lockQName, timeToLive, retryWait, retryCount); + getLockImpl(txnId, lockQName, timeToLive, retryWait, retryCount); } else { @@ -158,26 +159,47 @@ public class JobLockServiceImpl implements JobLockService } // The lock request is made out of natural order. // Unordered locks do not get any retry behaviour - getLock(lockQName, timeToLive, retryWait, 1); + getLockImpl(txnId, lockQName, timeToLive, retryWait, 1); } } // It went in, so add it to the transactionally-stored set heldLocks.add(lockQName); // Done } + + /** + * {@inheritDoc} + * + * @see #getLock(QName, long, long, int) + */ + public String getLock(QName lockQName, long timeToLive) + { + return getLock(lockQName, timeToLive, defaultRetryWait, defaultRetryCount); + } /** + * {@inheritDoc} + */ + public String getLock(QName lockQName, long timeToLive, long retryWait, int retryCount) + { + String lockToken = GUID.generate(); + getLockImpl(lockToken, lockQName, timeToLive, retryWait, retryCount); + // Done + return lockToken; + } + + /** + * {@inheritDoc} + * * @throws LockAcquisitionException on failure */ - private void refreshLock(final QName lockQName, final long timeToLive) + public void refreshLock(final String lockToken, final QName lockQName, final long timeToLive) { - // The lock token is the current transaction ID - final String txnId = AlfrescoTransactionSupport.getTransactionId(); RetryingTransactionCallback refreshLockCallback = new RetryingTransactionCallback() { public Object execute() throws Throwable { - lockDAO.releaseLock(lockQName, txnId); + lockDAO.refreshLock(lockQName, lockToken, timeToLive); return null; } }; @@ -192,7 +214,7 @@ public class JobLockServiceImpl implements JobLockService "Refreshed Lock: \n" + " Lock: " + lockQName + "\n" + " TTL: " + timeToLive + "\n" + - " Txn: " + txnId); + " Txn: " + lockToken); } } catch (LockAcquisitionException e) @@ -204,25 +226,39 @@ public class JobLockServiceImpl implements JobLockService "Lock refresh failed: \n" + " Lock: " + lockQName + "\n" + " TTL: " + timeToLive + "\n" + - " Txn: " + txnId + "\n" + + " Txn: " + lockToken + "\n" + " Error: " + e.getMessage()); } throw e; } } + /** + * {@inheritDoc} + */ + public void releaseLock(final String lockToken, final QName lockQName) + { + RetryingTransactionCallback releaseCallback = new RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + lockDAO.releaseLock(lockQName, lockToken); + return null; + } + }; + retryingTransactionHelper.doInTransaction(releaseCallback, false, true); + } + /** * @throws LockAcquisitionException on failure */ - private void getLock(final QName lockQName, final long timeToLive, long retryWait, int retryCount) + private void getLockImpl(final String lockToken, final QName lockQName, final long timeToLive, long retryWait, int retryCount) { - // The lock token is the current transaction ID - final String txnId = AlfrescoTransactionSupport.getTransactionId(); RetryingTransactionCallback getLockCallback = new RetryingTransactionCallback() { public Object execute() throws Throwable { - lockDAO.getLock(lockQName, txnId, timeToLive); + lockDAO.getLock(lockQName, lockToken, timeToLive); return null; } }; @@ -238,7 +274,7 @@ public class JobLockServiceImpl implements JobLockService "Acquired Lock: \n" + " Lock: " + lockQName + "\n" + " TTL: " + timeToLive + "\n" + - " Txn: " + txnId + "\n" + + " Txn: " + lockToken + "\n" + " Attempts: " + iterations); } } @@ -251,7 +287,7 @@ public class JobLockServiceImpl implements JobLockService "Lock acquisition failed: \n" + " Lock: " + lockQName + "\n" + " TTL: " + timeToLive + "\n" + - " Txn: " + txnId + "\n" + + " Txn: " + lockToken + "\n" + " Error: " + e.getMessage()); } throw e; diff --git a/source/java/org/alfresco/repo/lock/JobLockServiceTest.java b/source/java/org/alfresco/repo/lock/JobLockServiceTest.java index 855a04a630..7cb1d499ad 100644 --- a/source/java/org/alfresco/repo/lock/JobLockServiceTest.java +++ b/source/java/org/alfresco/repo/lock/JobLockServiceTest.java @@ -97,7 +97,7 @@ public class JobLockServiceTest extends TestCase { try { - jobLockService.getTransacionalLock(lockAAA, 50L); + jobLockService.getTransactionalLock(lockAAA, 50L); fail("Service did not enforce the presence of a transaction"); } catch (IllegalStateException e) @@ -116,7 +116,7 @@ public class JobLockServiceTest extends TestCase { public Object execute() throws Throwable { - jobLockService.getTransacionalLock(lockAAA, 500); + jobLockService.getTransactionalLock(lockAAA, 500); return null; } }; @@ -132,7 +132,7 @@ public class JobLockServiceTest extends TestCase { public Object execute() throws Throwable { - jobLockService.getTransacionalLock(lockAAA, 5000); + jobLockService.getTransactionalLock(lockAAA, 5000); return null; } }; @@ -142,7 +142,7 @@ public class JobLockServiceTest extends TestCase { public Object execute() throws Throwable { - jobLockService.getTransacionalLock(lockAAA, 50); + jobLockService.getTransactionalLock(lockAAA, 50); return null; } }; @@ -158,7 +158,7 @@ public class JobLockServiceTest extends TestCase { public Object execute() throws Throwable { - jobLockService.getTransacionalLock(lockAAA, 5000); + jobLockService.getTransactionalLock(lockAAA, 5000); throw new UnsupportedOperationException("ALERT!"); } }; @@ -176,7 +176,7 @@ public class JobLockServiceTest extends TestCase { public Object execute() throws Throwable { - jobLockService.getTransacionalLock(lockAAA, 50); + jobLockService.getTransactionalLock(lockAAA, 50); return null; } }; @@ -259,7 +259,7 @@ public class JobLockServiceTest extends TestCase { // Advance and grab the lock currentLock++; - jobLockService.getTransacionalLock(lockQNames[currentLock], 5000L); + jobLockService.getTransactionalLock(lockQNames[currentLock], 5000L); } else { diff --git a/source/java/org/alfresco/repo/node/db/hibernate/HibernateNodeDaoServiceImpl.java b/source/java/org/alfresco/repo/node/db/hibernate/HibernateNodeDaoServiceImpl.java index 00c6d680fb..a734e76350 100644 --- a/source/java/org/alfresco/repo/node/db/hibernate/HibernateNodeDaoServiceImpl.java +++ b/source/java/org/alfresco/repo/node/db/hibernate/HibernateNodeDaoServiceImpl.java @@ -61,6 +61,7 @@ import org.alfresco.repo.domain.Server; import org.alfresco.repo.domain.Store; import org.alfresco.repo.domain.Transaction; import org.alfresco.repo.domain.UsageDeltaDAO; +import org.alfresco.repo.domain.contentdata.ContentDataDAO; import org.alfresco.repo.domain.hibernate.ChildAssocImpl; import org.alfresco.repo.domain.hibernate.DMPermissionsDaoComponentImpl; import org.alfresco.repo.domain.hibernate.DbAccessControlListImpl; @@ -94,6 +95,7 @@ import org.alfresco.service.cmr.dictionary.PropertyDefinition; import org.alfresco.service.cmr.repository.AssociationExistsException; import org.alfresco.service.cmr.repository.AssociationRef; import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.ContentData; import org.alfresco.service.cmr.repository.DuplicateChildNodeNameException; import org.alfresco.service.cmr.repository.EntityRef; import org.alfresco.service.cmr.repository.InvalidNodeRefException; @@ -181,6 +183,7 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements } private QNameDAO qnameDAO; + private ContentDataDAO contentDataDAO; private UsageDeltaDAO usageDeltaDAO; private AclDaoComponent aclDaoComponent; private LocaleDAO localeDAO; @@ -255,6 +258,14 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements this.qnameDAO = qnameDAO; } + /** + * Set the component for storing and retrieving ContentData + */ + public void setContentDataDAO(ContentDataDAO contentDataDAO) + { + this.contentDataDAO = contentDataDAO; + } + public void setUsageDeltaDAO(UsageDeltaDAO usageDeltaDAO) { this.usageDeltaDAO = usageDeltaDAO; @@ -1284,7 +1295,7 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements Serializable propertyValue = HibernateNodeDaoServiceImpl.getPublicProperty( nodeProperties, propertyQName, - qnameDAO, localeDAO, dictionaryService); + qnameDAO, localeDAO, contentDataDAO, dictionaryService); return propertyValue; } @@ -1298,6 +1309,7 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements nodeProperties, qnameDAO, localeDAO, + contentDataDAO, dictionaryService); // Handle cm:auditable @@ -1327,6 +1339,18 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements PropertyDefinition propertyDef = dictionaryService.getProperty(qname); Long qnameId = qnameDAO.getOrCreateQName(qname).getFirst(); + /* + * TODO: Put into interceptor + * This method will replaces a content property. We therefore remove the existing content data. + * New ContentData entities will be created. Re-using content data will mean comparing old + * with new - it is faster to just create a new row. + */ + if (propertyDef != null && propertyDef.getDataType().getName().equals(DataTypeDefinition.CONTENT)) + { + Set contentQNamesToRemoveIds = Collections.singleton(qnameId); + contentDataDAO.deleteContentDataForNode(node.getId(), contentQNamesToRemoveIds); + } + Map persistableProperties = new HashMap(3); HibernateNodeDaoServiceImpl.addValueToPersistedProperties( @@ -1336,7 +1360,8 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements qnameId, localeId, value, - localeDAO); + localeDAO, + contentDataDAO); Map nodeProperties = node.getProperties(); @@ -1392,6 +1417,16 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements public void setNodeProperties(Long nodeId, Map propertiesIncl) { + /* + * TODO: Put into interceptor + * This method will replace all properties. We therefore remove all existing content data. + * New ContentData entities will be created. Re-using content data will mean comparing old + * with new - it is faster to just create a new row. + */ + Set contentQNames = new HashSet(dictionaryService.getAllProperties(DataTypeDefinition.CONTENT)); + Set contentQNameIds = qnameDAO.convertQNamesToIds(contentQNames, false); + contentDataDAO.deleteContentDataForNode(nodeId, contentQNameIds); + Node node = getNodeNotNull(nodeId); // Handle cm:auditable. These need to be removed from the properties. @@ -1413,6 +1448,7 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements properties, qnameDAO, localeDAO, + contentDataDAO, dictionaryService); // Get the persistent map attached to the node @@ -1443,6 +1479,24 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements public void removeNodeProperties(Long nodeId, Set propertyQNamesIncl) { + /* + * TODO: Put into interceptor + * This method will replace all properties. We therefore remove all existing content data. + * New ContentData entities will be created. Re-using content data will mean comparing old + * with new - it is faster to just create a new row. + */ + Set contentQNames = new HashSet(dictionaryService.getAllProperties(DataTypeDefinition.CONTENT)); + Set contentQNamesToRemove = new HashSet(3); + for (QName propertyQName : propertyQNamesIncl) + { + if (contentQNames.contains(propertyQName)) + { + contentQNamesToRemove.add(propertyQName); + } + } + Set contentQNamesToRemoveIds = qnameDAO.convertQNamesToIds(contentQNamesToRemove, false); + contentDataDAO.deleteContentDataForNode(nodeId, contentQNamesToRemoveIds); + Node node = getNodeNotNull(nodeId); // Handle cm:auditable. These need to be removed from the list. @@ -1559,6 +1613,14 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements */ public void deleteNode(Long nodeId) { + /* + * TODO: Put into interceptor + * This method will remove all properties. We therefore remove all existing content data. + */ + Set contentQNames = new HashSet(dictionaryService.getAllProperties(DataTypeDefinition.CONTENT)); + Set contentQNamesToRemoveIds = qnameDAO.convertQNamesToIds(contentQNames, false); + contentDataDAO.deleteContentDataForNode(nodeId, contentQNamesToRemoveIds); + Node node = getNodeNotNull(nodeId); // Propagate timestamps @@ -4032,6 +4094,7 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements Map in, QNameDAO qnameDAO, LocaleDAO localeDAO, + ContentDataDAO contentDataDAO, DictionaryService dictionaryService) { Map propertyMap = new HashMap(in.size() + 5); @@ -4053,7 +4116,8 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements propertyQNameId, propertylocaleId, value, - localeDAO); + localeDAO, + contentDataDAO); } // Done return propertyMap; @@ -4079,7 +4143,8 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements Long propertyQNameId, Long propertyLocaleId, Serializable value, - LocaleDAO localeDAO) + LocaleDAO localeDAO, + ContentDataDAO contentDataDAO) { if (value == null) { @@ -4134,7 +4199,8 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements propertyQNameId, propertyLocaleId, value, - localeDAO); + localeDAO, + contentDataDAO); } else if (collectionIndex == IDX_NO_COLLECTION && value instanceof Collection) { @@ -4188,7 +4254,8 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements propertyQNameId, propertyLocaleId, collectionValue, - localeDAO); + localeDAO, + contentDataDAO); } catch (Throwable e) { @@ -4213,6 +4280,13 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements " Type: " + propertyTypeQName + "\n" + " Value: " + value); } + // Handle ContentData + if (value instanceof ContentData) + { + // Needs converting to an ID + ContentData contentData = (ContentData) value; + value = contentDataDAO.createContentData(contentData).getFirst(); + } // Handle MLText if (value instanceof MLText) { @@ -4295,6 +4369,7 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements QName propertyQName, QNameDAO qnameDAO, LocaleDAO localeDAO, + ContentDataDAO contentDataDAO, DictionaryService dictionaryService) { // Get the qname ID @@ -4322,7 +4397,8 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements Serializable collapsedValue = HibernateNodeDaoServiceImpl.collapsePropertiesWithSameQName( propertyDef, scratch, - localeDAO); + localeDAO, + contentDataDAO); return collapsedValue; } else @@ -4335,6 +4411,7 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements Map propertyValues, QNameDAO qnameDAO, LocaleDAO localeDAO, + ContentDataDAO contentDataDAO, DictionaryService dictionaryService) { Map propertyMap = new HashMap(propertyValues.size(), 1.0F); @@ -4377,7 +4454,8 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements collapsedValue = HibernateNodeDaoServiceImpl.collapsePropertiesWithSameQNameAndListIndex( currentPropertyDef, scratch, - localeDAO); + localeDAO, + contentDataDAO); } else { @@ -4385,7 +4463,8 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements collapsedValue = HibernateNodeDaoServiceImpl.collapsePropertiesWithSameQName( currentPropertyDef, scratch, - localeDAO); + localeDAO, + contentDataDAO); } // If the property is multi-valued then the output property must be a collection if (currentPropertyDef != null && currentPropertyDef.isMultiValued()) @@ -4422,7 +4501,8 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements private static Serializable collapsePropertiesWithSameQName( PropertyDefinition propertyDef, SortedMap sortedPropertyValues, - LocaleDAO localeDAO) + LocaleDAO localeDAO, + ContentDataDAO contentDataDAO) { Serializable result = null; Collection collectionResult = null; @@ -4451,7 +4531,8 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements Serializable collapsedValue = HibernateNodeDaoServiceImpl.collapsePropertiesWithSameQNameAndListIndex( propertyDef, scratch, - localeDAO); + localeDAO, + contentDataDAO); // Store. If there is a value already, then we must build a collection. if (result == null) { @@ -4510,7 +4591,8 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements private static Serializable collapsePropertiesWithSameQNameAndListIndex( PropertyDefinition propertyDef, Map propertyValues, - LocaleDAO localeDAO) + LocaleDAO localeDAO, + ContentDataDAO contentDataDAO) { int propertyValuesSize = propertyValues.size(); Serializable value = null; @@ -4527,7 +4609,7 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements (propertyDef == null || !propertyDef.getDataType().getName().equals(DataTypeDefinition.MLTEXT))) { // This is the only value and it is NOT to be converted to MLText - value = HibernateNodeDaoServiceImpl.makeSerializableValue(propertyDef, propertyValue); + value = HibernateNodeDaoServiceImpl.makeSerializableValue(propertyDef, propertyValue, contentDataDAO); } else { @@ -4559,10 +4641,14 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements * * @param propertyDef the model property definition - may be null * @param propertyValue the persisted property + * @param contentDataDAO component that handles ContentData persistence * @return Returns the value of the property in the format dictated by the property * definition, or null if the property value is null */ - private static Serializable makeSerializableValue(PropertyDefinition propertyDef, NodePropertyValue propertyValue) + private static Serializable makeSerializableValue( + PropertyDefinition propertyDef, + NodePropertyValue propertyValue, + ContentDataDAO contentDataDAO) { if (propertyValue == null) { @@ -4582,6 +4668,20 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements try { Serializable value = propertyValue.getValue(propertyTypeQName); + // Handle conversions to and from ContentData + if (propertyTypeQName.equals(DataTypeDefinition.CONTENT) && (value instanceof Long)) + { + Pair contentDataPair = contentDataDAO.getContentData((Long)value); + if (contentDataPair == null) + { + // It is invalid + value = null; + } + else + { + value = contentDataPair.getSecond(); + } + } // done return value; } diff --git a/source/java/org/alfresco/repo/security/person/PersonDaoImpl.java b/source/java/org/alfresco/repo/security/person/PersonDaoImpl.java index 68718c7710..91a29da9ed 100644 --- a/source/java/org/alfresco/repo/security/person/PersonDaoImpl.java +++ b/source/java/org/alfresco/repo/security/person/PersonDaoImpl.java @@ -37,6 +37,7 @@ import org.alfresco.repo.domain.Node; import org.alfresco.repo.domain.NodePropertyValue; import org.alfresco.repo.domain.PropertyMapKey; import org.alfresco.repo.domain.QNameDAO; +import org.alfresco.repo.domain.contentdata.ContentDataDAO; import org.alfresco.repo.node.db.hibernate.HibernateNodeDaoServiceImpl; import org.alfresco.repo.tenant.TenantService; import org.alfresco.service.cmr.dictionary.DictionaryService; @@ -59,6 +60,7 @@ public class PersonDaoImpl extends HibernateDaoSupport implements PersonDao private Long qNamePropId; private Long qNameTypeId; private LocaleDAO localeDAO; + private ContentDataDAO contentDataDAO; private DictionaryService dictionaryService; private StoreRef storeRef; @@ -128,7 +130,8 @@ public class PersonDaoImpl extends HibernateDaoSupport implements PersonDao Map nodeProperties = node.getProperties(); // Convert the QName IDs - Map converted = HibernateNodeDaoServiceImpl.convertToPublicProperties(nodeProperties, qnameDAO, localeDAO, dictionaryService); + Map converted = HibernateNodeDaoServiceImpl.convertToPublicProperties( + nodeProperties, qnameDAO, localeDAO, contentDataDAO, dictionaryService); Serializable value = converted.get(ContentModel.PROP_USERNAME); String realUserName = DefaultTypeConverter.INSTANCE.convert(String.class, value); @@ -190,4 +193,9 @@ public class PersonDaoImpl extends HibernateDaoSupport implements PersonDao this.dictionaryService = dictionaryService; } + public void setContentDataDAO(ContentDataDAO contentDataDAO) + { + this.contentDataDAO = contentDataDAO; + } + } diff --git a/source/java/org/alfresco/repo/service/ServiceDescriptorRegistry.java b/source/java/org/alfresco/repo/service/ServiceDescriptorRegistry.java index b6d62dd6c9..27332a2404 100644 --- a/source/java/org/alfresco/repo/service/ServiceDescriptorRegistry.java +++ b/source/java/org/alfresco/repo/service/ServiceDescriptorRegistry.java @@ -31,6 +31,7 @@ import org.alfresco.cmis.CMISQueryService; import org.alfresco.cmis.CMISServices; import org.alfresco.mbeans.VirtServerRegistry; import org.alfresco.repo.forms.FormService; +import org.alfresco.repo.lock.JobLockService; import org.alfresco.repo.transaction.RetryingTransactionHelper; import org.alfresco.service.ServiceRegistry; import org.alfresco.service.cmr.action.ActionService; @@ -186,6 +187,11 @@ public class ServiceDescriptorRegistry return (LockService)getService(LOCK_SERVICE); } + public JobLockService getJobLockService() + { + return (JobLockService)getService(JOB_LOCK_SERVICE); + } + /* (non-Javadoc) * @see org.alfresco.repo.service.ServiceRegistry#getDictionaryService() */ diff --git a/source/java/org/alfresco/service/ServiceRegistry.java b/source/java/org/alfresco/service/ServiceRegistry.java index 73a87f4d25..d88e085fcb 100644 --- a/source/java/org/alfresco/service/ServiceRegistry.java +++ b/source/java/org/alfresco/service/ServiceRegistry.java @@ -31,6 +31,7 @@ import org.alfresco.cmis.CMISQueryService; import org.alfresco.cmis.CMISServices; import org.alfresco.mbeans.VirtServerRegistry; import org.alfresco.repo.forms.FormService; +import org.alfresco.repo.lock.JobLockService; import org.alfresco.repo.transaction.RetryingTransactionHelper; import org.alfresco.service.cmr.action.ActionService; import org.alfresco.service.cmr.attributes.AttributeService; @@ -110,6 +111,7 @@ public interface ServiceRegistry static final QName CATEGORY_SERVICE = QName.createQName(NamespaceService.ALFRESCO_URI, "CategoryService"); static final QName COPY_SERVICE = QName.createQName(NamespaceService.ALFRESCO_URI, "CopyService"); static final QName LOCK_SERVICE = QName.createQName(NamespaceService.ALFRESCO_URI, "LockService"); + static final QName JOB_LOCK_SERVICE = QName.createQName(NamespaceService.ALFRESCO_URI, "JobLockService"); static final QName VERSION_SERVICE = QName.createQName(NamespaceService.ALFRESCO_URI, "VersionService"); static final QName COCI_SERVICE = QName.createQName(NamespaceService.ALFRESCO_URI, "CheckoutCheckinService"); static final QName RULE_SERVICE = QName.createQName(NamespaceService.ALFRESCO_URI, "RuleService"); @@ -249,6 +251,12 @@ public interface ServiceRegistry @NotAuditable LockService getLockService(); + /** + * @return the job lock service (or null, if one is not provided) + */ + @NotAuditable + JobLockService getJobLockService(); + /** * @return the dictionary service (or null, if one is not provided) */ diff --git a/source/java/org/alfresco/service/cmr/repository/ContentData.java b/source/java/org/alfresco/service/cmr/repository/ContentData.java index 2c1947aa35..5fb08041c3 100644 --- a/source/java/org/alfresco/service/cmr/repository/ContentData.java +++ b/source/java/org/alfresco/service/cmr/repository/ContentData.java @@ -217,9 +217,9 @@ public class ContentData implements Serializable return false; ContentData that = (ContentData) obj; return (EqualsHelper.nullSafeEquals(this.contentUrl, that.contentUrl) && - EqualsHelper.nullSafeEquals(this.mimetype, that.mimetype) && + EqualsHelper.nullSafeEquals(this.mimetype, that.mimetype, true) && this.size == that.size && - EqualsHelper.nullSafeEquals(this.encoding, that.encoding) && + EqualsHelper.nullSafeEquals(this.encoding, that.encoding, true) && EqualsHelper.nullSafeEquals(this.locale, that.locale)); }