diff --git a/config/alfresco/dao/dao-context.xml b/config/alfresco/dao/dao-context.xml index aa4cbbb7a5..4805156436 100644 --- a/config/alfresco/dao/dao-context.xml +++ b/config/alfresco/dao/dao-context.xml @@ -48,12 +48,15 @@ + + diff --git a/config/alfresco/dbscripts/create/3.2/org.hibernate.dialect.MySQLInnoDBDialect/AlfrescoPostCreate-3.2-AuditTables.sql b/config/alfresco/dbscripts/create/3.2/org.hibernate.dialect.MySQLInnoDBDialect/AlfrescoPostCreate-3.2-AuditTables.sql index e1b7cbf6fd..1b45a83754 100644 --- a/config/alfresco/dbscripts/create/3.2/org.hibernate.dialect.MySQLInnoDBDialect/AlfrescoPostCreate-3.2-AuditTables.sql +++ b/config/alfresco/dbscripts/create/3.2/org.hibernate.dialect.MySQLInnoDBDialect/AlfrescoPostCreate-3.2-AuditTables.sql @@ -20,10 +20,14 @@ CREATE TABLE alf_audit_model CREATE TABLE alf_audit_app ( id BIGINT NOT NULL AUTO_INCREMENT, - audit_model_id BIGINT NOT NULL, + version TINYINT NOT NULL, app_name_id BIGINT NOT NULL, - CONSTRAINT fk_alf_audit_app_model FOREIGN KEY (audit_model_id) REFERENCES alf_audit_model (id) ON DELETE CASCADE, + audit_model_id BIGINT NOT NULL, + disabled_paths_id BIGINT NOT NULL, CONSTRAINT fk_alf_audit_app_app FOREIGN KEY (app_name_id) REFERENCES alf_prop_value (id), + CONSTRAINT UNIQUE idx_alf_audit_app_app (app_name_id), + CONSTRAINT fk_alf_audit_app_model FOREIGN KEY (audit_model_id) REFERENCES alf_audit_model (id) ON DELETE CASCADE, + CONSTRAINT fk_alf_audit_app_dis FOREIGN KEY (disabled_paths_id) REFERENCES alf_prop_root (id), PRIMARY KEY (id) ) ENGINE=InnoDB; @@ -37,7 +41,7 @@ CREATE TABLE alf_audit_entry CONSTRAINT fk_alf_audit_ent_app FOREIGN KEY (audit_app_id) REFERENCES alf_audit_app (id) ON DELETE CASCADE, INDEX idx_alf_audit_ent_time (audit_time), CONSTRAINT fk_alf_audit_ent_user FOREIGN KEY (audit_user_id) REFERENCES alf_prop_value (id), - CONSTRAINT fk_alf_audit_ent_prop FOREIGN KEY (audit_values_id) REFERENCES alf_prop_value (id), + CONSTRAINT fk_alf_audit_ent_prop FOREIGN KEY (audit_values_id) REFERENCES alf_prop_root (id), PRIMARY KEY (id) ) ENGINE=InnoDB; diff --git a/config/alfresco/dbscripts/create/3.2/org.hibernate.dialect.MySQLInnoDBDialect/AlfrescoPostCreate-3.2-PropertyValueTables.sql b/config/alfresco/dbscripts/create/3.2/org.hibernate.dialect.MySQLInnoDBDialect/AlfrescoPostCreate-3.2-PropertyValueTables.sql index 2148da28d2..be0810c338 100644 --- a/config/alfresco/dbscripts/create/3.2/org.hibernate.dialect.MySQLInnoDBDialect/AlfrescoPostCreate-3.2-PropertyValueTables.sql +++ b/config/alfresco/dbscripts/create/3.2/org.hibernate.dialect.MySQLInnoDBDialect/AlfrescoPostCreate-3.2-PropertyValueTables.sql @@ -38,10 +38,11 @@ CREATE TABLE alf_prop_double_value ( id BIGINT NOT NULL AUTO_INCREMENT, double_value DOUBLE NOT NULL, - INDEX idx_alf_prop_dbl_val (double_value), + UNIQUE INDEX idx_alf_prop_dbl_val (double_value), PRIMARY KEY (id) ) ENGINE=InnoDB; +-- Stores unique, case-sensitive string values -- CREATE TABLE alf_prop_string_value ( id BIGINT NOT NULL AUTO_INCREMENT, @@ -49,7 +50,7 @@ CREATE TABLE alf_prop_string_value string_end_lower VARCHAR(16) NOT NULL, string_crc BIGINT NOT NULL, INDEX idx_alf_prop_str (string_value(32)), - INDEX idx_alf_prop_crc (string_end_lower, string_crc), + UNIQUE INDEX idx_alf_prop_crc (string_end_lower, string_crc), PRIMARY KEY (id) ) ENGINE=InnoDB; @@ -67,19 +68,29 @@ CREATE TABLE alf_prop_value persisted_type TINYINT NOT NULL, long_value BIGINT NOT NULL, INDEX idx_alf_prop_per (persisted_type, long_value), - INDEX idx_alf_prop_act (actual_type_id, long_value), + UNIQUE INDEX idx_alf_prop_act (actual_type_id, long_value), + PRIMARY KEY (id) +) ENGINE=InnoDB; + +CREATE TABLE alf_prop_root +( + id BIGINT NOT NULL AUTO_INCREMENT, + version TINYINT NOT NULL, PRIMARY KEY (id) ) ENGINE=InnoDB; CREATE TABLE alf_prop_link ( root_prop_id BIGINT NOT NULL, - current_prop_id BIGINT NOT NULL, - value_prop_id BIGINT NOT NULL, + prop_index BIGINT NOT NULL, + contained_in BIGINT NOT NULL, key_prop_id BIGINT NOT NULL, - INDEX idx_alf_prop_coll_rev (value_prop_id, root_prop_id), - CONSTRAINT fk_alf_prop_link_root FOREIGN KEY (root_prop_id) REFERENCES alf_prop_value (id) ON DELETE CASCADE, - PRIMARY KEY (root_prop_id, current_prop_id, value_prop_id, key_prop_id) + value_prop_id BIGINT NOT NULL, + CONSTRAINT fk_alf_prop_link_root FOREIGN KEY (root_prop_id) REFERENCES alf_prop_root (id) ON DELETE CASCADE, + CONSTRAINT fk_alf_prop_link_key FOREIGN KEY (key_prop_id) REFERENCES alf_prop_value (id) ON DELETE CASCADE, + CONSTRAINT fk_alf_prop_link_val FOREIGN KEY (value_prop_id) REFERENCES alf_prop_value (id) ON DELETE CASCADE, + INDEX idx_alf_prop_link_for (root_prop_id, key_prop_id, value_prop_id), + PRIMARY KEY (root_prop_id, contained_in, prop_index) ) ENGINE=InnoDB; -- diff --git a/config/alfresco/ibatis/org.hibernate.dialect.Dialect/audit-common-SqlMap.xml b/config/alfresco/ibatis/org.hibernate.dialect.Dialect/audit-common-SqlMap.xml index ec72904b1b..ac12abd764 100644 --- a/config/alfresco/ibatis/org.hibernate.dialect.Dialect/audit-common-SqlMap.xml +++ b/config/alfresco/ibatis/org.hibernate.dialect.Dialect/audit-common-SqlMap.xml @@ -27,8 +27,10 @@ - + + + @@ -76,8 +78,8 @@ - insert into alf_audit_app (audit_model_id, app_name_id) - values (#auditModelId#, #applicationNameId#) + insert into alf_audit_app (version, app_name_id, audit_model_id, disabled_paths_id) + values (#version#, #applicationNameId#, #auditModelId#, #disabledPathsId#) @@ -99,65 +101,60 @@ content_crc = #contentCrc# - - select * from alf_audit_app where - audit_model_id = ? + id = ? - - - select - entry.id as audit_entry_id, - user_sv.string_value as audit_user, - app_sv.string_value as audit_app_name, - entry.audit_time as audit_time, - entry.audit_values_id as audit_values_id + * from - alf_audit_app app - join alf_prop_value app_pv on (app_pv.id = app.app_name_id) - join alf_prop_string_value app_sv on (app_sv.id = app_pv.long_value and app_pv.persisted_type = 3) - join alf_audit_entry entry on (entry.audit_app_id = app.id) - join alf_prop_value user_pv on (user_pv.id = entry.audit_user_id) - join alf_prop_string_value user_sv on (user_sv.id = user_pv.long_value and user_pv.persisted_type = 3) - - - app_sv.string_end_lower = #auditAppNameShort# and - app_sv.string_crc = #auditAppNameCrc# - - - user_sv.string_end_lower = #auditUserShort# and - user_sv.string_crc = #auditUserCrc# - - - = #auditFromTime#]]> - - - - - - order by - entry.id + alf_audit_app + where + app_name_id = ? + + + update + alf_audit_app + set + version = #version#, + app_name_id = #applicationNameId# + audit_model_id = #auditModelId# + disabled_paths_id = #disabledPathsId# + where + id = #id# and + version = (#version# -1) + + - \ No newline at end of file diff --git a/config/alfresco/ibatis/org.hibernate.dialect.Dialect/propval-common-SqlMap.xml b/config/alfresco/ibatis/org.hibernate.dialect.Dialect/propval-common-SqlMap.xml index cfae055ede..9aea4a5bcf 100644 --- a/config/alfresco/ibatis/org.hibernate.dialect.Dialect/propval-common-SqlMap.xml +++ b/config/alfresco/ibatis/org.hibernate.dialect.Dialect/propval-common-SqlMap.xml @@ -17,6 +17,7 @@ + @@ -58,31 +59,34 @@ - - - - - - - + + + + + + + - - - - + + + + + - - - - - - - - - - + + + + + + + + + + + + @@ -92,6 +96,9 @@ + + + @@ -122,6 +129,11 @@ values (#actualTypeId#, #persistedType#, #longValue#) + + insert into alf_prop_root (version) + values (?) + + @@ -241,13 +253,13 @@ select - pv.id as id, - pv.actual_type_id as actual_type_id, - pv.persisted_type as persisted_type, - pv.long_value as long_value, - dv.double_value as double_value, - null as string_value, - null as serializable_value + pv.id as prop_id, + pv.actual_type_id as prop_actual_type_id, + pv.persisted_type as prop_persisted_type, + pv.long_value as prop_long_value, + dv.double_value as prop_double_value, + null as prop_string_value, + null as prop_serializable_value from alf_prop_value pv join alf_prop_double_value dv on (dv.id = pv.long_value and pv.persisted_type = #persistedType#) @@ -276,13 +288,13 @@ - + select + pv.id as prop_id, + pv.actual_type_id as prop_actual_type_id, + pv.persisted_type as prop_persisted_type, + pv.long_value as prop_long_value, + dv.double_value as prop_double_value, + sv.string_value as prop_string_value, + serv.serializable_value as prop_serializable_value + from + alf_prop_value pv + left join alf_prop_double_value dv on (dv.id = pv.long_value and pv.persisted_type = 2) + left join alf_prop_string_value sv on (sv.id = pv.long_value and (pv.persisted_type = 3 || pv.persisted_type = 5)) + left join alf_prop_serializable_value serv on (serv.id = pv.long_value and pv.persisted_type = 4) + where + pv.id = #id# + + + + + + + update + alf_prop_root + set + version = #version# + where + id = #id# and + version = (#version# -1) + + + + delete from + alf_prop_root + where + id = #id# + + insert into alf_prop_link ( - root_prop_id, current_prop_id, value_prop_id, key_prop_id + root_prop_id, prop_index, contained_in, key_prop_id, value_prop_id ) values ( - #rootPropId#, #currentPropId#, #valuePropId#, #keyPropId# + #rootPropId#, #propIndex#, #containedIn#, #keyPropId#, #valuePropId# ) + + + delete from + alf_prop_link + where + root_prop_id = #id# + \ No newline at end of file diff --git a/config/alfresco/ibatis/org.hibernate.dialect.MySQLInnoDBDialect/propval-insert-SqlMap.xml b/config/alfresco/ibatis/org.hibernate.dialect.MySQLInnoDBDialect/propval-insert-SqlMap.xml index 0e8f779bc8..240ee92030 100644 --- a/config/alfresco/ibatis/org.hibernate.dialect.MySQLInnoDBDialect/propval-insert-SqlMap.xml +++ b/config/alfresco/ibatis/org.hibernate.dialect.MySQLInnoDBDialect/propval-insert-SqlMap.xml @@ -41,4 +41,11 @@ + + + + KEY_COLUMN:GENERATED_KEY + + + \ No newline at end of file diff --git a/source/java/org/alfresco/repo/audit/hibernate/HibernateAuditDAO.java b/source/java/org/alfresco/repo/audit/hibernate/HibernateAuditDAO.java index 544bea42dc..7e0737c016 100644 --- a/source/java/org/alfresco/repo/audit/hibernate/HibernateAuditDAO.java +++ b/source/java/org/alfresco/repo/audit/hibernate/HibernateAuditDAO.java @@ -39,6 +39,7 @@ import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Set; import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.repo.audit.AuditComponentImpl; @@ -48,6 +49,7 @@ import org.alfresco.repo.content.ContentContext; import org.alfresco.repo.content.ContentStore; import org.alfresco.repo.content.MimetypeMap; import org.alfresco.repo.domain.audit.AuditDAO; +import org.alfresco.repo.domain.audit.AuditDAO.AuditApplicationInfo; import org.alfresco.repo.security.authentication.AuthenticationUtil; import org.alfresco.repo.transaction.TransactionalDao; import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; @@ -670,7 +672,40 @@ public class HibernateAuditDAO extends HibernateDaoSupport implements AuditDAO, * @throws UnsupportedOperationException always * @since 3.2 */ - public Long getOrCreateAuditApplication(Long modelId, String applicationName) + public AuditApplicationInfo createAuditApplication(String application, Long modelId) + { + throw new UnsupportedOperationException(); + } + + /** + * Fallout implementation from new audit DAO + * + * @throws UnsupportedOperationException always + * @since 3.2 + */ + public AuditApplicationInfo getAuditApplication(String applicationName) + { + throw new UnsupportedOperationException(); + } + + /** + * Fallout implementation from new audit DAO + * + * @throws UnsupportedOperationException always + * @since 3.2 + */ + public void updateAuditApplicationModel(Long id, Long modelId) + { + throw new UnsupportedOperationException(); + } + + /** + * Fallout implementation from new audit DAO + * + * @throws UnsupportedOperationException always + * @since 3.2 + */ + public void updateAuditApplicationDisabledPaths(Long id, Set disabledPaths) { throw new UnsupportedOperationException(); } diff --git a/source/java/org/alfresco/repo/audit/model/AuditModelRegistry.java b/source/java/org/alfresco/repo/audit/model/AuditModelRegistry.java index 338c303747..ad31db8a9d 100644 --- a/source/java/org/alfresco/repo/audit/model/AuditModelRegistry.java +++ b/source/java/org/alfresco/repo/audit/model/AuditModelRegistry.java @@ -55,6 +55,7 @@ import org.alfresco.repo.audit.model._3.DataExtractors; import org.alfresco.repo.audit.model._3.DataGenerators; import org.alfresco.repo.audit.model._3.ObjectFactory; import org.alfresco.repo.domain.audit.AuditDAO; +import org.alfresco.repo.domain.audit.AuditDAO.AuditApplicationInfo; import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.transaction.TransactionService; @@ -99,6 +100,10 @@ public class AuditModelRegistry * Used to lookup a reference to the application */ private final Map auditApplicationIdsByApplicationsName; + /** + * Used to lookup application disabled paths + */ + private final Map> auditDisabledPathsByApplicationsName; /** * Default constructor @@ -115,6 +120,7 @@ public class AuditModelRegistry auditModels = new ArrayList(7); auditApplicationsByName = new HashMap(7); auditApplicationIdsByApplicationsName = new HashMap(7); + auditDisabledPathsByApplicationsName = new HashMap>(7); } /** @@ -246,8 +252,8 @@ public class AuditModelRegistry clearCaches(); throw new AuditModelException( - "Failed to load audit model: " + auditModelUrl + "\n" + - e.getMessage()); + "Failed to load audit model: " + auditModelUrl, + e); } } // NOTE: If we support other types of loading, then that will have to go here, too @@ -308,6 +314,25 @@ public class AuditModelRegistry } } + /** + * Get all disabled paths for the given application name + * + * @param applicationName the name of the audited application + * @return a set of paths for which logging is disabled + */ + public Set getAuditDisabledPaths(String applicationName) + { + readLock.lock(); + try + { + return auditDisabledPathsByApplicationsName.get(applicationName); + } + finally + { + readLock.unlock(); + } + } + /** * Unmarshalls the Audit model from the URL. * @@ -516,11 +541,21 @@ public class AuditModelRegistry } // Get the ID of the application - Long appId = auditDAO.getOrCreateAuditApplication(auditModelId, name); + AuditApplicationInfo appInfo = auditDAO.getAuditApplication(name); + if (appInfo == null) + { + appInfo = auditDAO.createAuditApplication(name, auditModelId); + } + else + { + // Update it with the new model ID + auditDAO.updateAuditApplicationModel(appInfo.getId(), auditModelId); + } AuditApplication wrapperApp = new AuditApplication(dataExtractorsByName, dataGeneratorsByName, application); auditApplicationsByName.put(name, wrapperApp); - auditApplicationIdsByApplicationsName.put(name, appId); + auditApplicationIdsByApplicationsName.put(name, appInfo.getId()); + auditDisabledPathsByApplicationsName.put(name, appInfo.getDisabledPaths()); } // Store the model itself auditModels.add(audit); diff --git a/source/java/org/alfresco/repo/domain/audit/AbstractAuditDAOImpl.java b/source/java/org/alfresco/repo/domain/audit/AbstractAuditDAOImpl.java index f99509d0d6..03b1074da0 100644 --- a/source/java/org/alfresco/repo/domain/audit/AbstractAuditDAOImpl.java +++ b/source/java/org/alfresco/repo/domain/audit/AbstractAuditDAOImpl.java @@ -29,8 +29,10 @@ import java.io.InputStream; import java.io.Serializable; import java.net.URL; import java.util.Date; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.zip.CRC32; import org.alfresco.error.AlfrescoRuntimeException; @@ -49,6 +51,7 @@ import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.util.Pair; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.dao.DataIntegrityViolationException; /** * Abstract helper DAO for alf_audit_XXX tables. @@ -206,33 +209,105 @@ public abstract class AbstractAuditDAOImpl implements AuditDAO * alf_audit_application */ - public Long getOrCreateAuditApplication(Long modelId, String application) + @SuppressWarnings("unchecked") + public AuditApplicationInfo getAuditApplication(String application) { - // Search for it - AuditApplicationEntity entity = getAuditApplicationByModelIdAndName(modelId, application); + AuditApplicationEntity entity = getAuditApplicationByName(application); if (entity == null) { - // Create it - // Persist the string - Long appNameId = propertyValueDAO.getOrCreatePropertyValue(application).getFirst(); - // Create the audit session - entity = createAuditApplication(modelId, appNameId); + return null; + } + else + { + AuditApplicationInfo appInfo = new AuditApplicationInfo(); + appInfo.setId(entity.getId()); + appInfo.setname(application); + appInfo.setModelId(entity.getAuditModelId()); + // Resolve the disabled paths + Set disabledPaths = (Set) propertyValueDAO.getPropertyById(entity.getDisabledPathsId()); + appInfo.setDisabledPaths(disabledPaths); // Done if (logger.isDebugEnabled()) { logger.debug( - "Created new audit application: \n" + - " Model: " + modelId + "\n" + - " App: " + application + "\n" + - " Result: " + entity); + "Found existing audit application: \n" + + " " + appInfo); } + return appInfo; } + } + + public AuditApplicationInfo createAuditApplication(String application, Long modelId) + { + // Persist the string + Long appNameId = propertyValueDAO.getOrCreatePropertyValue(application).getFirst(); + // We need a property to hold any disabled paths + Set disabledPaths = new HashSet(); + Long disabledPathsId = propertyValueDAO.createProperty((Serializable)disabledPaths); + // Create the audit app + AuditApplicationEntity entity = createAuditApplication(appNameId, modelId, disabledPathsId); + + // Create return value + AuditApplicationInfo appInfo = new AuditApplicationInfo(); + appInfo.setId(entity.getId()); + appInfo.setname(application); + appInfo.setModelId(modelId); + appInfo.setDisabledPaths(disabledPaths); // Done - return entity.getId(); + if (logger.isDebugEnabled()) + { + logger.debug( + "Created new audit application: \n" + + " Model: " + modelId + "\n" + + " App: " + application + "\n" + + " Result: " + entity); + } + return appInfo; } - protected abstract AuditApplicationEntity getAuditApplicationByModelIdAndName(Long modelId, String appName); - protected abstract AuditApplicationEntity createAuditApplication(Long modelId, Long appNameId); + public void updateAuditApplicationModel(Long id, Long modelId) + { + AuditApplicationEntity entity = getAuditApplicationById(id); + if (entity == null) + { + throw new DataIntegrityViolationException("No audit application exists for ID " + id); + } + if (entity.getAuditModelId().equals(modelId)) + { + // There is nothing to update + return; + } + // Update + entity.setAuditModelId(modelId); + updateAuditApplication(entity); + } + + @SuppressWarnings("unchecked") + public void updateAuditApplicationDisabledPaths(Long id, Set disabledPaths) + { + AuditApplicationEntity entity = getAuditApplicationById(id); + if (entity == null) + { + throw new DataIntegrityViolationException("No audit application exists for ID " + id); + } + // Resolve the current set + Long disabledPathsId = entity.getDisabledPathsId(); + Set oldDisabledPaths = (Set) propertyValueDAO.getPropertyById(disabledPathsId); + if (oldDisabledPaths.equals(disabledPaths)) + { + // Nothing changed + return; + } + // Update the property + propertyValueDAO.updateProperty(disabledPathsId, (Serializable) disabledPaths); + // Do a precautionary update to ensure that the application row is locked appropriately + updateAuditApplication(entity); + } + + protected abstract AuditApplicationEntity getAuditApplicationById(Long id); + protected abstract AuditApplicationEntity getAuditApplicationByName(String appName); + protected abstract AuditApplicationEntity createAuditApplication(Long appNameId, Long modelId, Long disabledPathsId); + protected abstract AuditApplicationEntity updateAuditApplication(AuditApplicationEntity entity); /* * alf_audit_entry @@ -253,7 +328,7 @@ public abstract class AbstractAuditDAOImpl implements AuditDAO Long valuesId = null; if (values != null && values.size() > 0) { - valuesId = propertyValueDAO.getOrCreatePropertyValue((Serializable)values).getFirst(); + valuesId = propertyValueDAO.createProperty((Serializable)values); } // Create the audit entry diff --git a/source/java/org/alfresco/repo/domain/audit/AuditApplicationEntity.java b/source/java/org/alfresco/repo/domain/audit/AuditApplicationEntity.java index 4205fb8f62..ed93721935 100644 --- a/source/java/org/alfresco/repo/domain/audit/AuditApplicationEntity.java +++ b/source/java/org/alfresco/repo/domain/audit/AuditApplicationEntity.java @@ -33,8 +33,10 @@ package org.alfresco.repo.domain.audit; public class AuditApplicationEntity { private Long id; - private Long auditModelId; + private short version; private Long applicationNameId; + private Long auditModelId; + private Long disabledPathsId; public AuditApplicationEntity() { @@ -46,12 +48,26 @@ public class AuditApplicationEntity StringBuilder sb = new StringBuilder(512); sb.append("AuditApplicationEntity") .append("[ ID=").append(id) - .append(", auditModelId=").append(auditModelId) + .append(", version=").append(version) .append(", applicationNameId=").append(applicationNameId) + .append(", auditModelId=").append(auditModelId) + .append(", disabledPathsId=").append(disabledPathsId) .append("]"); return sb.toString(); } + public void incrementVersion() + { + if (version >= Short.MAX_VALUE) + { + this.version = 0; + } + else + { + this.version++; + } + } + public Long getId() { return id; @@ -62,14 +78,14 @@ public class AuditApplicationEntity this.id = id; } - public Long getAuditModelId() + public short getVersion() { - return auditModelId; + return version; } - public void setAuditModelId(Long auditModelId) + public void setVersion(short version) { - this.auditModelId = auditModelId; + this.version = version; } public Long getApplicationNameId() @@ -81,4 +97,24 @@ public class AuditApplicationEntity { this.applicationNameId = applicationNameId; } + + public Long getAuditModelId() + { + return auditModelId; + } + + public void setAuditModelId(Long auditModelId) + { + this.auditModelId = auditModelId; + } + + public Long getDisabledPathsId() + { + return disabledPathsId; + } + + public void setDisabledPathsId(Long disabledPathsId) + { + this.disabledPathsId = disabledPathsId; + } } diff --git a/source/java/org/alfresco/repo/domain/audit/AuditDAO.java b/source/java/org/alfresco/repo/domain/audit/AuditDAO.java index bee6aa5033..bfb910ed26 100644 --- a/source/java/org/alfresco/repo/domain/audit/AuditDAO.java +++ b/source/java/org/alfresco/repo/domain/audit/AuditDAO.java @@ -28,6 +28,7 @@ import java.io.Serializable; import java.net.URL; import java.util.List; import java.util.Map; +import java.util.Set; import org.alfresco.repo.audit.AuditState; import org.alfresco.service.cmr.audit.AuditInfo; @@ -66,6 +67,66 @@ public interface AuditDAO * V3.2 methods after here only, please */ + /** + * Information about the audit application to be passed in an out of the interface. + * + * @author Derek Hulley + * @since 3.2 + */ + public static class AuditApplicationInfo + { + private Long id; + private String name; + private Long modelId; + private Set disabledPaths; + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append("AuditApplicationInfo ") + .append("[ id=").append(id) + .append(", name=").append(name) + .append(", modelId=").append(modelId) + .append(", disabledPaths=").append(disabledPaths) + .append("]"); + return sb.toString(); + } + + public Long getId() + { + return id; + } + public void setId(Long id) + { + this.id = id; + } + public String getName() + { + return name; + } + public void setname(String name) + { + this.name = name; + } + public Long getModelId() + { + return modelId; + } + public void setModelId(Long modelId) + { + this.modelId = modelId; + } + public Set getDisabledPaths() + { + return disabledPaths; + } + public void setDisabledPaths(Set disabledPaths) + { + this.disabledPaths = disabledPaths; + } + } + /** * Creates a new audit model entry or finds an existing one * @@ -76,13 +137,38 @@ public interface AuditDAO Pair getOrCreateAuditModel(URL url); /** - * Creates a new audit application or finds an existing one + * Get the audit application details. * - * @param modelId the ID of the model configuration * @param applicationName the name of the application - * @return Returns the ID of the application entry + * @return Returns details of an existing application or null if it doesn't exist */ - Long getOrCreateAuditApplication(Long modelId, String applicationName); + AuditApplicationInfo getAuditApplication(String applicationName); + + /** + * Creates a new audit application. The application name must be unique. + * + * @param application the name of the application + * @param modelId the ID of the model configuration + */ + AuditApplicationInfo createAuditApplication(String application, Long modelId); + + /** + * Update the audit application to refer to a new model. + * If the model did not change, then nothing will be done. + * + * @param id the ID of the audit application + * @param modelId the ID of the new model + */ + void updateAuditApplicationModel(Long id, Long modelId); + + /** + * Update the audit application to hold a new set of disabled paths. + * If the value did not change, then nothing will be done. + * + * @param id the ID of the audit application + * @param disabledPaths the new disabled paths + */ + void updateAuditApplicationDisabledPaths(Long id, Set disabledPaths); /** * Create a new audit entry with the given map of values. diff --git a/source/java/org/alfresco/repo/domain/audit/AuditDAOTest.java b/source/java/org/alfresco/repo/domain/audit/AuditDAOTest.java index 13c5400032..f239ffa5e6 100644 --- a/source/java/org/alfresco/repo/domain/audit/AuditDAOTest.java +++ b/source/java/org/alfresco/repo/domain/audit/AuditDAOTest.java @@ -33,6 +33,7 @@ import java.util.Map; import junit.framework.TestCase; import org.alfresco.repo.content.transform.AbstractContentTransformerTest; +import org.alfresco.repo.domain.audit.AuditDAO.AuditApplicationInfo; import org.alfresco.repo.domain.contentdata.ContentDataDAO; import org.alfresco.repo.transaction.RetryingTransactionHelper; import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; @@ -111,7 +112,11 @@ public class AuditDAOTest extends TestCase { for (int i = 0; i < count; i++) { - auditDAO.getOrCreateAuditApplication(modelId, appName); + AuditApplicationInfo appInfo = auditDAO.getAuditApplication(appName); + if (appInfo == null) + { + appInfo = auditDAO.createAuditApplication(appName, modelId); + } } return null; } @@ -142,8 +147,13 @@ public class AuditDAOTest extends TestCase { public Long execute() throws Throwable { - Long modelId = auditDAO.getOrCreateAuditModel(url).getFirst(); - return auditDAO.getOrCreateAuditApplication(modelId, appName); + AuditApplicationInfo appInfo = auditDAO.getAuditApplication(appName); + if (appInfo == null) + { + Long modelId = auditDAO.getOrCreateAuditModel(url).getFirst(); + appInfo = auditDAO.createAuditApplication(appName, modelId); + } + return appInfo.getId(); } }; final Long sessionId = txnHelper.doInTransaction(createAppCallback); diff --git a/source/java/org/alfresco/repo/domain/audit/ibatis/AuditDAOImpl.java b/source/java/org/alfresco/repo/domain/audit/ibatis/AuditDAOImpl.java index 0775b15767..559341a8c6 100644 --- a/source/java/org/alfresco/repo/domain/audit/ibatis/AuditDAOImpl.java +++ b/source/java/org/alfresco/repo/domain/audit/ibatis/AuditDAOImpl.java @@ -52,11 +52,14 @@ public class AuditDAOImpl extends AbstractAuditDAOImpl private static final String SELECT_MODEL_BY_CRC = "alfresco.audit.select_AuditModelByCrc"; private static final String INSERT_MODEL = "alfresco.audit.insert_AuditModel"; - private static final String SELECT_APPLICATION_BY_MODEL_ID = "alfresco.audit.select_AuditApplicationByModelId"; + private static final String SELECT_APPLICATION_BY_ID = "alfresco.audit.select_AuditApplicationById"; + private static final String SELECT_APPLICATION_BY_NAME_ID = "alfresco.audit.select_AuditApplicationByNameId"; private static final String INSERT_APPLICATION = "alfresco.audit.insert_AuditApplication"; + private static final String UPDATE_APPLICATION = "alfresco.audit.update_AuditApplication"; private static final String INSERT_ENTRY = "alfresco.audit.insert_AuditEntry"; + @SuppressWarnings("unused") private static final String SELECT_ENTRIES_SIMPLE = "alfresco.audit.select_AuditEntriesSimple"; private static final String SELECT_ENTRIES_WITH_VALUES = "alfresco.audit.select_AuditEntriesWithValues"; @@ -90,54 +93,75 @@ public class AuditDAOImpl extends AbstractAuditDAOImpl return entity; } - @SuppressWarnings("unchecked") @Override - protected AuditApplicationEntity getAuditApplicationByModelIdAndName(Long modelId, String appName) + protected AuditApplicationEntity getAuditApplicationById(Long id) { Map params = new HashMap(11); - params.put("id", modelId); - List results = (List) template.queryForList( - SELECT_APPLICATION_BY_MODEL_ID, + params.put("id", id); + AuditApplicationEntity entity = (AuditApplicationEntity) template.queryForObject( + SELECT_APPLICATION_BY_ID, params); - // There could be multiple hits for the model ID. Go through them and find the correct app name. - AuditApplicationEntity result = null; - for (AuditApplicationEntity row : results) - { - Long appNameId = row.getApplicationNameId(); - Pair propPair = getPropertyValueDAO().getPropertyValueById(appNameId); - if (propPair == null) - { - // There is a FK to protect against this, but we'll just log it - logger.warn("An audit application references a non-existent app_name_id: " + appNameId); - } - // Check for exact match - Serializable propValue = propPair.getSecond(); - if (propValue instanceof String && propValue.equals(appName)) - { - // Got it - result = row; - break; - } - } // Done if (logger.isDebugEnabled()) { - logger.debug("Searched for audit application with model id " + modelId + " and found: " + result); + logger.debug("Searched for audit application ID " + id + " and found: " + entity); } - return result; + return entity; } @Override - protected AuditApplicationEntity createAuditApplication(Long modelId, Long appNameId) + protected AuditApplicationEntity getAuditApplicationByName(String appName) + { + // Resolve the name as a property ID + Pair appNamePair = propertyValueDAO.getPropertyValue(appName); + if (appNamePair == null) + { + // There will be no results + return null; + } + + Map params = new HashMap(11); + params.put("id", appNamePair.getFirst()); + AuditApplicationEntity entity = (AuditApplicationEntity) template.queryForObject( + SELECT_APPLICATION_BY_NAME_ID, + params); + // Done + if (logger.isDebugEnabled()) + { + logger.debug("Searched for audit application '" + appName + "' and found: " + entity); + } + return entity; + } + + @Override + protected AuditApplicationEntity createAuditApplication(Long appNameId, Long modelId, Long disabledPathsId) { AuditApplicationEntity entity = new AuditApplicationEntity(); - entity.setAuditModelId(modelId); + entity.setVersion((short)0); entity.setApplicationNameId(appNameId); + entity.setAuditModelId(modelId); + entity.setDisabledPathsId(disabledPathsId); Long id = (Long) template.insert(INSERT_APPLICATION, entity); entity.setId(id); return entity; } + @Override + protected AuditApplicationEntity updateAuditApplication(AuditApplicationEntity entity) + { + AuditApplicationEntity updateEntity = new AuditApplicationEntity(); + updateEntity.setId(entity.getId()); + updateEntity.setVersion(entity.getVersion()); + updateEntity.incrementVersion(); + updateEntity.setApplicationNameId(entity.getApplicationNameId()); + updateEntity.setAuditModelId(entity.getAuditModelId()); + updateEntity.setDisabledPathsId(entity.getDisabledPathsId()); + + template.update(UPDATE_APPLICATION, updateEntity, 1); + // Done + return updateEntity; + } + @Override protected AuditEntryEntity createAuditEntry(Long applicationId, long time, Long usernameId, Long valuesId) { diff --git a/source/java/org/alfresco/repo/domain/propval/AbstractPropertyValueDAOImpl.java b/source/java/org/alfresco/repo/domain/propval/AbstractPropertyValueDAOImpl.java index 4f9b612a08..3640831f6b 100644 --- a/source/java/org/alfresco/repo/domain/propval/AbstractPropertyValueDAOImpl.java +++ b/source/java/org/alfresco/repo/domain/propval/AbstractPropertyValueDAOImpl.java @@ -33,17 +33,17 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.TreeMap; -import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.repo.cache.SimpleCache; import org.alfresco.repo.cache.lookup.EntityLookupCache; import org.alfresco.repo.cache.lookup.EntityLookupCache.EntityLookupCallbackDAOAdaptor; import org.alfresco.repo.domain.CrcHelper; import org.alfresco.repo.domain.propval.PropertyValueEntity.PersistedType; -import org.alfresco.service.cmr.repository.MLText; import org.alfresco.util.Pair; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.dao.DataIntegrityViolationException; /** * Abstract implementation for Property Value DAO. @@ -62,8 +62,9 @@ public abstract class AbstractPropertyValueDAOImpl implements PropertyValueDAO private static final String CACHE_REGION_PROPERTY_DOUBLE_VALUE = "PropertyDoubleValue"; private static final String CACHE_REGION_PROPERTY_SERIALIZABLE_VALUE = "PropertySerializableValue"; private static final String CACHE_REGION_PROPERTY_VALUE = "PropertyValue"; + private static final String CACHE_REGION_PROPERTY = "Property"; - private static final Log logger = LogFactory.getLog(AbstractPropertyValueDAOImpl.class); + protected final Log logger = LogFactory.getLog(getClass()); protected PropertyTypeConverter converter; @@ -73,6 +74,7 @@ public abstract class AbstractPropertyValueDAOImpl implements PropertyValueDAO private final PropertyDoubleValueCallbackDAO propertyDoubleValueCallback; private final PropertySerializableValueCallbackDAO propertySerializableValueCallback; private final PropertyValueCallbackDAO propertyValueCallback; + private final PropertyCallbackDAO propertyCallback; /** * Cache for the property class:
* KEY: ID
@@ -115,6 +117,13 @@ public abstract class AbstractPropertyValueDAOImpl implements PropertyValueDAO * VALUE KEY: A value key based on the persisted type
*/ private EntityLookupCache propertyValueCache; + /** + * Cache for the property:
+ * KEY: ID
+ * VALUE: The Serializable instance
+ * VALUE KEY: A value key based on the persisted type
+ */ + private EntityLookupCache propertyCache; /** * Default constructor. @@ -130,6 +139,7 @@ public abstract class AbstractPropertyValueDAOImpl implements PropertyValueDAO this.propertyDoubleValueCallback = new PropertyDoubleValueCallbackDAO(); this.propertySerializableValueCallback = new PropertySerializableValueCallbackDAO(); this.propertyValueCallback = new PropertyValueCallbackDAO(); + this.propertyCallback = new PropertyCallbackDAO(); this.propertyClassCache = new EntityLookupCache, String>(propertyClassDaoCallback); this.propertyDateValueCache = new EntityLookupCache(propertyDateValueCallback); @@ -137,6 +147,7 @@ public abstract class AbstractPropertyValueDAOImpl implements PropertyValueDAO this.propertyDoubleValueCache = new EntityLookupCache(propertyDoubleValueCallback); this.propertySerializableValueCache = new EntityLookupCache(propertySerializableValueCallback); this.propertyValueCache = new EntityLookupCache(propertyValueCallback); + this.propertyCache = new EntityLookupCache(propertyCallback); } /** @@ -225,6 +236,19 @@ public abstract class AbstractPropertyValueDAOImpl implements PropertyValueDAO propertyValueCallback); } + /** + * Set the cache to use for alf_prop_root lookups (optional). + * + * @param propertyValueCache the cache of IDs to property values + */ + public void setPropertyCache(SimpleCache propertyCache) + { + this.propertyCache = new EntityLookupCache( + propertyCache, + CACHE_REGION_PROPERTY, + propertyCallback); + } + //================================ // 'alf_prop_class' accessors //================================ @@ -238,7 +262,7 @@ public abstract class AbstractPropertyValueDAOImpl implements PropertyValueDAO Pair> entityPair = propertyClassCache.getByKey(id); if (entityPair == null) { - throw new AlfrescoRuntimeException("No property class exists for ID " + id); + throw new DataIntegrityViolationException("No property class exists for ID " + id); } return entityPair; } @@ -321,7 +345,7 @@ public abstract class AbstractPropertyValueDAOImpl implements PropertyValueDAO Pair entityPair = propertyDateValueCache.getByKey(id); if (entityPair == null) { - throw new AlfrescoRuntimeException("No property date value exists for ID " + id); + throw new DataIntegrityViolationException("No property date value exists for ID " + id); } return entityPair; } @@ -422,7 +446,7 @@ public abstract class AbstractPropertyValueDAOImpl implements PropertyValueDAO Pair entityPair = propertyStringValueCache.getByKey(id); if (entityPair == null) { - throw new AlfrescoRuntimeException("No property string value exists for ID " + id); + throw new DataIntegrityViolationException("No property string value exists for ID " + id); } return entityPair; } @@ -507,7 +531,7 @@ public abstract class AbstractPropertyValueDAOImpl implements PropertyValueDAO Pair entityPair = propertyDoubleValueCache.getByKey(id); if (entityPair == null) { - throw new AlfrescoRuntimeException("No property double value exists for ID " + id); + throw new DataIntegrityViolationException("No property double value exists for ID " + id); } return entityPair; } @@ -590,7 +614,7 @@ public abstract class AbstractPropertyValueDAOImpl implements PropertyValueDAO Pair entityPair = propertySerializableValueCache.getByKey(id); if (entityPair == null) { - throw new AlfrescoRuntimeException("No property serializable value exists for ID " + id); + throw new DataIntegrityViolationException("No property serializable value exists for ID " + id); } return entityPair; } @@ -651,7 +675,7 @@ public abstract class AbstractPropertyValueDAOImpl implements PropertyValueDAO Pair entityPair = propertyValueCache.getByKey(id); if (entityPair == null) { - throw new AlfrescoRuntimeException("No property value exists for ID " + id); + throw new DataIntegrityViolationException("No property value exists for ID " + id); } return entityPair; } @@ -662,64 +686,10 @@ public abstract class AbstractPropertyValueDAOImpl implements PropertyValueDAO return entityPair; } - /** - * {@inheritDoc} - * @see #getOrCreatePropertyValueImpl(Serializable, int, int) - */ public Pair getOrCreatePropertyValue(Serializable value) { - return getOrCreatePropertyValueImpl(value, null, Integer.MAX_VALUE, 0); - } - - /** - * {@inheritDoc} - * @see #getOrCreatePropertyValueImpl(Serializable, Long, int, int) - */ - public Pair getOrCreatePropertyValue(Serializable value, int maxDepth) - { - return getOrCreatePropertyValueImpl(value, null, maxDepth, 0); - } - - @SuppressWarnings("unchecked") - private Pair getOrCreatePropertyValueImpl( - Serializable value, - Long rootId, - int maxDepth, - int currentDepth) - { - if (value != null && maxDepth > currentDepth && value instanceof Map) - { - // TODO: Go through cache? - // The default is to do a deep expansion - Long mapId = createPropertyMapImpl( - (Map)value, - rootId, - maxDepth, - currentDepth); - Pair entityPair = new Pair(mapId, value); - // Cache instance by ID only - propertyValueCache.updateValue(mapId, value); - return entityPair; - } - else if (value != null && maxDepth > currentDepth && value instanceof Collection) - { - // TODO: Go through cache? - // The default is to do a deep expansion - Long collectionId = createPropertyCollectionImpl( - (Collection)value, - rootId, - maxDepth, - currentDepth); - Pair entityPair = new Pair(collectionId, value); - // Cache instance by ID only - propertyValueCache.updateValue(collectionId, value); - return entityPair; - } - else - { - Pair entityPair = propertyValueCache.getOrCreateByValue(value); - return (Pair) entityPair; - } + Pair entityPair = propertyValueCache.getOrCreateByValue(value); + return (Pair) entityPair; } /** @@ -727,25 +697,16 @@ public abstract class AbstractPropertyValueDAOImpl implements PropertyValueDAO */ private class PropertyValueCallbackDAO extends EntityLookupCallbackDAOAdaptor { + @SuppressWarnings("unchecked") private final Serializable convertToValue(PropertyValueEntity entity) { if (entity == null) { return null; } - final Serializable actualValue; - if (entity.getPersistedTypeEnum() == PersistedType.CONSTRUCTABLE) - { - actualValue = entity.getPersistedValue(); - } - else - { - Long actualTypeId = entity.getActualTypeId(); - Class actualType = getPropertyClassById(actualTypeId).getSecond(); - - Serializable entityValue = entity.getPersistedValue(); - actualValue = (Serializable) converter.convert(actualType, entityValue); - } + Long actualTypeId = entity.getActualTypeId(); + final Class actualType = (Class) getPropertyClassById(actualTypeId).getSecond(); + final Serializable actualValue = entity.getValue(actualType, converter); // Done return actualValue; } @@ -764,11 +725,11 @@ public abstract class AbstractPropertyValueDAOImpl implements PropertyValueDAO public Serializable getValueKey(Serializable value) { - PersistedType persistedType = PropertyValueEntity.getPersistedTypeEnum(value); + PersistedType persistedType = PropertyValueEntity.getPersistedTypeEnum(value, converter); // We don't return keys for pure Serializable instances if (persistedType == PersistedType.SERIALIZABLE) { - // It will be Serialized, so no key + // It will be Serialized, so no search key return null; } else if (value instanceof String) @@ -785,34 +746,18 @@ public abstract class AbstractPropertyValueDAOImpl implements PropertyValueDAO public Pair createValue(Serializable value) { PropertyValueEntity entity = createPropertyValue(value); - Long entityId = entity.getId(); - // Create the link entry for the property - createPropertyLink(entityId, entityId, entityId, 0L); // Done return new Pair(entity.getId(), value); } public Pair findByKey(Long key) { - List rows = findPropertyValueById(key); - if (rows.size() == 0) - { - // No results - return null; - } - Serializable value = convertPropertyIdSearchRows(rows); - return new Pair(key, value); + PropertyValueEntity entity = findPropertyValueById(key); + return convertEntityToPair(entity); } public Pair findByValue(Serializable value) { - if (value != null) - { - if (value instanceof Map || value instanceof Collection) - { - throw new IllegalArgumentException("Should not be searching for Maps or Collections"); - } - } PropertyValueEntity entity = findPropertyValueByValue(value); return convertEntityToPair(entity); } @@ -828,22 +773,304 @@ public abstract class AbstractPropertyValueDAOImpl implements PropertyValueDAO } } - protected abstract List findPropertyValueById(Long id); + protected abstract PropertyValueEntity findPropertyValueById(Long id); protected abstract PropertyValueEntity findPropertyValueByValue(Serializable value); protected abstract PropertyValueEntity createPropertyValue(Serializable value); + //================================ + // 'alf_prop_root' accessors + //================================ + + public Serializable getPropertyById(Long id) + { + if (id == null) + { + throw new IllegalArgumentException("Cannot look up entity by null ID."); + } + Pair entityPair = propertyCache.getByKey(id); + if (entityPair == null) + { + throw new DataIntegrityViolationException("No property value exists for ID " + id); + } + return entityPair.getSecond(); + } + + /** + * {@inheritDoc} + * @see #createPropertyImpl(Serializable, int, int) + */ + public Long createProperty(Serializable value) + { + // We will need a new root + Long rootPropId = createPropertyRoot(); + createPropertyImpl(rootPropId, 0L, 0L, null, value); + // Push this value into the cache + propertyCache.updateValue(rootPropId, value); + // Done + if (logger.isDebugEnabled()) + { + logger.debug( + "Created property: \n" + + " ID: " + rootPropId + "\n" + + " Value: " + value); + } + return rootPropId; + } + + public void updateProperty(Long rootPropId, Serializable value) + { + // Remove all entries for the root + PropertyRootEntity entity = getPropertyRoot(rootPropId); + if (entity == null) + { + throw new DataIntegrityViolationException("No property root exists for ID " + rootPropId); + } + // Remove all links using the root + deletePropertyLinks(rootPropId); + // Create the new properties and update the cache + createPropertyImpl(rootPropId, 0L, 0L, null, value); + propertyCache.updateValue(rootPropId, value); + // Update the property root to detect concurrent modification + updatePropertyRoot(entity); + // Done + if (logger.isDebugEnabled()) + { + logger.debug( + "Updated property: \n" + + " ID: " + rootPropId + "\n" + + " Value: " + value); + } + } + + public void deleteProperty(Long id) + { + deletePropertyRoot(id); + // Done + if (logger.isDebugEnabled()) + { + logger.debug( + "Deleted property: \n" + + " ID: " + id); + } + } + + /** + * @param propIndex a unique index within the context of the current property root + */ + @SuppressWarnings("unchecked") + private long createPropertyImpl( + Long rootPropId, + long propIndex, + long containedIn, + Long keyPropId, + Serializable value) + { + // Keep track of the index for this property. It gets used later when making the link entry. + long thisPropIndex = propIndex; + + Long valuePropId = null; + if (value == null) + { + // The key and the value are the same + valuePropId = getOrCreatePropertyValue(value).getFirst(); + } + else if (value instanceof Map) + { + Map map = (Map) value; + // Check if the it has a default constructor + Serializable emptyInstance = constructEmptyContainer(value.getClass()); + if (emptyInstance == null) + { + // No default constructor, so we just throw the whole thing in as a single property + valuePropId = getOrCreatePropertyValue(value).getFirst(); + } + else + { + // Persist the empty map + valuePropId = getOrCreatePropertyValue(emptyInstance).getFirst(); + // Persist the individual entries + for (Map.Entry entry : map.entrySet()) + { + // Recurse for each value + Serializable mapKey = entry.getKey(); + Serializable mapValue = entry.getValue(); + // Get the IDs for these + Long mapKeyId = getOrCreatePropertyValue(mapKey).getFirst(); + propIndex = createPropertyImpl( + rootPropId, + propIndex + 1L, + thisPropIndex, + mapKeyId, + mapValue); + } + } + } + else if (value instanceof Collection) + { + Collection collection = (Collection) value; + // Check if the it has a default constructor + Serializable emptyInstance = constructEmptyContainer(value.getClass()); + if (emptyInstance == null) + { + // No default constructor, so we just throw the whole thing in as a single property + valuePropId = getOrCreatePropertyValue(value).getFirst(); + } + else + { + // Persist the empty collection + valuePropId = getOrCreatePropertyValue(emptyInstance).getFirst(); + // Persist the individual entries + for (Serializable collectionValue : collection) + { + // Recurse for each value + propIndex = createPropertyImpl( + rootPropId, + propIndex + 1L, + thisPropIndex, + null, + collectionValue); + } + } + } + else + { + // The key and the value are the same + valuePropId = getOrCreatePropertyValue(value).getFirst(); + } + + // Create a link entry + if (keyPropId == null) + { + // If the key matches the value then it is the root + keyPropId = valuePropId; + } + createPropertyLink(rootPropId, thisPropIndex, containedIn, keyPropId, valuePropId); + + // Done + return propIndex; + } + + private static final Serializable EMPTY_HASHMAP = new HashMap(); + private static final Serializable EMPTY_LIST = new ArrayList(); + private static final Serializable EMPTY_SET = new HashSet(); + + /** + * Returns a reconstructable instance + * + * @return Returns an empty instance of the given container (map or collection), or + * null if it is not possible to do + */ + protected Serializable constructEmptyContainer(Class clazz) + { + try + { + return (Serializable) clazz.getConstructor().newInstance(); + } + catch (Throwable e) + { + // Can't be constructed, so we just choose a well-known implementation. + // There are so many variations on maps and collections (Unmodifiable, Immutable, etc) + // that to not choose an alternative would leave the database full of BLOBs + } + if (Map.class.isAssignableFrom(clazz)) + { + return EMPTY_HASHMAP; + } + else if (List.class.isAssignableFrom(clazz)) + { + return EMPTY_LIST; + } + else if (Set.class.isAssignableFrom(clazz)) + { + return EMPTY_SET; + } + else + { + logger.warn("Unable to find suitable container type with default constructor: " + clazz); + return null; + } + } + + /** + * Callback for alf_prop_root DAO. + */ + private class PropertyCallbackDAO extends EntityLookupCallbackDAOAdaptor + { + public Pair createValue(Serializable value) + { + PropertyValueEntity entity = createPropertyValue(value); + // Done + return new Pair(entity.getId(), value); + } + + public Pair findByKey(Long key) + { + List rows = findPropertyById(key); + if (rows.size() == 0) + { + // No results + return null; + } + Serializable value = convertPropertyIdSearchRows(rows); + return new Pair(key, value); + } + + /** + * No-op. This is implemented as we just want to update the cache. + * @return Returns 0 always + */ + @Override + public int updateValue(Long key, Serializable value) + { + return 0; + } + } + + + protected abstract List findPropertyById(Long id); + protected abstract Long createPropertyRoot(); + protected abstract PropertyRootEntity getPropertyRoot(Long id); + protected abstract PropertyRootEntity updatePropertyRoot(PropertyRootEntity entity); + protected abstract void deletePropertyRoot(Long id); + + /** + * Create an entry for the map or collection link. + * + * @param rootPropId the root (entry-point) property ID + * @param propIndex the property number within the root property + * @param containedIn the property that contains the current value + * @param keyPropId the map key entity ID or collection position count + * @param valuePropId the ID of the entity storing the value (may be another map or collection) + */ + protected abstract void createPropertyLink( + Long rootPropId, + Long propIndex, + Long containedIn, + Long keyPropId, + Long valuePropId); + + /** + * Remove all property links for a given property root. + * + * @param rootPropId the root (entry-point) property ID + */ + protected abstract int deletePropertyLinks(Long rootPropId); + @SuppressWarnings("unchecked") public Serializable convertPropertyIdSearchRows(List rows) { + // Shortcut if there are no results + if (rows.size() == 0) + { + return null; + } /* - * The results are ordered by the root_prop_id, current_prop_id and value_prop_id. - * However, for safety (data patching, etc) we take a first pass to create the - * basic properties before hooking them all together in a second pass. + * The results all share the same root property. Pass through the results and construct all + * instances, storing them ordered by prop_index. */ - final Map values = new HashMap(rows.size()); - List keyRows = new ArrayList(5); - Serializable result = null; - Long rootPropId = null; + Map valuesByPropIndex = new HashMap(7); + TreeMap linkEntitiesByPropIndex = new TreeMap(); + Long rootPropId = null; // Keep this to ensure the root_prop_id is common for (PropertyIdSearchRow row : rows) { // Check that we are handling a single root property @@ -859,216 +1086,74 @@ public abstract class AbstractPropertyValueDAOImpl implements PropertyValueDAO } PropertyLinkEntity linkEntity = row.getLinkEntity(); + Long propIndex = linkEntity.getPropIndex(); + Long valuePropId = linkEntity.getValuePropId(); PropertyValueEntity valueEntity = row.getValueEntity(); - // Construct the value (Maps and Collections should be CONSTRUCTABLE) - Serializable value = propertyValueCallback.convertToValue(valueEntity); - // Keep it - values.put(new Long(linkEntity.getValuePropId()), value); - - // If this row is a mapping row (the property value ID does not match the current property ID) - // then store it for quicker use later - if (linkEntity.getCurrentPropId() != linkEntity.getValuePropId()) + // Get the value + Serializable value; + if (valueEntity != null) { - keyRows.add(linkEntity); + value = propertyValueCallback.convertToValue(valueEntity); } - if (linkEntity.getRootPropId() == linkEntity.getValuePropId()) + else { - // We found the root - result = value; + // Go N+1 if the value entity was not retrieved + value = getPropertyValueById(valuePropId); } + // Keep it for later + valuesByPropIndex.put(propIndex, value); + linkEntitiesByPropIndex.put(propIndex, linkEntity); } - // We expect a value to be found unless the results are empty - if (result == null) + Serializable result = null; + // Iterate again, adding values to the collections and looking for the root property + for (Map.Entry entry : linkEntitiesByPropIndex.entrySet()) { - return null; - } - - // Now we have all our constructed values and the mapping rows: build the collections - for (PropertyLinkEntity propertyLinkEntity : keyRows) - { - Serializable value = values.get(propertyLinkEntity.getValuePropId()); - Serializable currentProp = values.get(propertyLinkEntity.getCurrentPropId()); - if (value == null) + PropertyLinkEntity linkEntity = entry.getValue(); + Long propIndex = linkEntity.getPropIndex(); + Long containedIn = linkEntity.getContainedIn(); + Long keyPropId = linkEntity.getKeyPropId(); + Serializable value = valuesByPropIndex.get(propIndex); + // Check if this is the root property + if (propIndex.equals(containedIn)) { - logger.error("No value found for link property: " + propertyLinkEntity); - continue; - } - if (currentProp == null) - { - logger.error("No current property found for link property: " + propertyLinkEntity); - continue; - } - Long keyId = propertyLinkEntity.getKeyPropId(); - // put the value into the container - if (currentProp instanceof Map) - { - Pair keyPair = getPropertyValueById(keyId); - if (keyPair == null) + if (result != null) { - logger.error("Current property (map) has key without a value: " + propertyLinkEntity); + logger.error("Found inconsistent property root data: " + linkEntity); continue; } - Serializable key = keyPair.getSecond(); - Map map = (Map) currentProp; - map.put(key, value); + // This property is contained in itself i.e. it's the root + result = value; } - else if (currentProp instanceof Set) + else { - // We can ignore the key - it won't make a difference - Set set = (Set) currentProp; - set.add(value); - } -// Results 'should' be ordered by key -// else if (currentProp instanceof List) -// { -// // The order is important -// List collection = (List) currentProp; -// collection.add(keyId.intValue(), value); -// } - else if (currentProp instanceof Collection) - { - // The order is important - Collection collection = (Collection) currentProp; - collection.add(value); + // Add the value to the container to which it belongs. + // The ordering is irrelevant for some containers; but where it is important, + // ordering given by the prop_index will ensure that values are added back + // in the order in which the container originally iterated over them + Serializable container = valuesByPropIndex.get(containedIn); + if (container == null) + { + logger.error("Found container ID that doesn't have a value: " + linkEntity); + } + else if (container instanceof Map) + { + Map map = (Map) container; + Serializable mapKey = getPropertyValueById(keyPropId).getSecond(); + map.put(mapKey, value); + } + else if (container instanceof Collection) + { + Collection collection = (Collection) container; + collection.add(value); + } + else + { + logger.error("Found container ID that is not a map or collection: " + linkEntity); + } } } // This will have put the values into the correct containers return result; } - - //================================ - // Special handling of maps and collections - //================================ - - /** - * Recursive method to write a map out. If the root entity ID is null then the current - * map ID is used as the root. - * - * @param value the map to write - * @param rootId the root entity ID, which may be a map or a collection - * @return Returns the ID of the newly-written map - */ - private Long createPropertyMapImpl( - Map map, - Long rootId, - int maxDepth, - int currentDepth) - { - // Create the root of the map - Class clazz = null; - if (map instanceof MLText) - { - clazz = MLText.class; - } - else - { - clazz = HashMap.class; - } - - // Can't use a cached instance as each map is unique. Go direct to entity creation. - Long entityId = createPropertyValue(clazz).getId(); - - // Use this as the root if this is the first entry into this method - if (rootId == null) - { - rootId = entityId; - } - - // Create the link entry for the root - createPropertyLink(rootId, entityId, entityId, 0L); - - // Now iterate over the entries and create properties for the keys and values - for (Map.Entry entry : map.entrySet()) - { - K key = entry.getKey(); - Long keyId = getOrCreatePropertyValue(key).getFirst(); - - V value = entry.getValue(); - // Callback with this level, incrementing the current depth - Pair valuePair = getOrCreatePropertyValueImpl( - (Serializable) value, - rootId, - maxDepth, - currentDepth + 1); - Long valueId = valuePair.getFirst(); - - // Now write the mapping entry - createPropertyLink(rootId, entityId, valueId, keyId); - } - - // Done - return entityId; - } - - /** - * Recursive method to write a collection out. If the root entity ID is null then the current - * collection ID is used as the root. - * - * @param value the collection to write - * @param rootId the root property ID - * @return Returns the ID of the newly-written collection - */ - private Long createPropertyCollectionImpl( - Collection collection, - Long rootId, - int maxDepth, - int currentDepth) - { - // Create the root of the collection - Class clazz = null; - if (collection instanceof Set) - { - clazz = HashSet.class; - } - else - { - clazz = ArrayList.class; - } - - // Can't use a cached instance as each collection is unique. Go direct to entity creation. - Long entityId = createPropertyValue(clazz).getId(); - - // Use this as the root if this is the first entry into this method - if (rootId == null) - { - rootId = entityId; - } - - // Create the link entry for the root - createPropertyLink(rootId, entityId, entityId, 0L); - - // Now iterate over the entries and create properties for the keys and values - long index = 0L; - for (V value : collection) - { - // Callback with this level, incrementing the current depth - Pair valuePair = getOrCreatePropertyValueImpl( - (Serializable) value, - rootId, - maxDepth, - currentDepth + 1); - Long valueId = valuePair.getFirst(); - // Now write the mapping entry - Long keyId = new Long(index); - createPropertyLink(rootId, entityId, valueId, keyId); - // Keep iterating - index++; - } - return entityId; - } - - /** - * Create an entry for the map or collection link - * - * @param rootPropId the root (entry-point) map or collection ID - * @param currentPropId the current map or collection ID - * @param valueId the ID of the entity storing the value (may be another map or collection) - * @param keyId the map key entity ID or collection position count - */ - protected abstract void createPropertyLink( - Long rootPropId, - Long currentPropId, - Long valueId, - Long keyId); } diff --git a/source/java/org/alfresco/repo/domain/propval/DefaultPropertyTypeConverter.java b/source/java/org/alfresco/repo/domain/propval/DefaultPropertyTypeConverter.java index 3519ae79a2..167abf9992 100644 --- a/source/java/org/alfresco/repo/domain/propval/DefaultPropertyTypeConverter.java +++ b/source/java/org/alfresco/repo/domain/propval/DefaultPropertyTypeConverter.java @@ -25,11 +25,15 @@ package org.alfresco.repo.domain.propval; import java.io.Serializable; +import java.lang.reflect.Constructor; +import java.util.Collection; import java.util.Collections; +import java.util.Date; import java.util.HashMap; import java.util.Locale; import java.util.Map; +import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.repo.domain.propval.PropertyValueEntity.PersistedType; import org.alfresco.service.cmr.repository.AssociationRef; import org.alfresco.service.cmr.repository.ChildAssociationRef; @@ -59,12 +63,23 @@ public class DefaultPropertyTypeConverter implements PropertyTypeConverter { // Create the map of class-type Map, PersistedType> mapClass = new HashMap, PersistedType>(29); + mapClass.put(Boolean.class, PersistedType.LONG); + mapClass.put(Short.class, PersistedType.LONG); + mapClass.put(Integer.class, PersistedType.LONG); + mapClass.put(Long.class, PersistedType.LONG); + mapClass.put(Date.class, PersistedType.LONG); + mapClass.put(Float.class, PersistedType.DOUBLE); + mapClass.put(Double.class, PersistedType.DOUBLE); + mapClass.put(String.class, PersistedType.STRING); + mapClass.put(Class.class, PersistedType.STRING); mapClass.put(NodeRef.class, PersistedType.STRING); mapClass.put(Period.class, PersistedType.STRING); mapClass.put(Locale.class, PersistedType.STRING); mapClass.put(AssociationRef.class, PersistedType.STRING); mapClass.put(ChildAssociationRef.class, PersistedType.STRING); + // Everything else is just Serializable defaultPersistedTypesByClass = Collections.unmodifiableMap(mapClass); + } private Map, PersistedType> persistenceMapping; @@ -89,6 +104,72 @@ public class DefaultPropertyTypeConverter implements PropertyTypeConverter this.persistenceMapping.put(clazz, targetType); } + /** + * Determines if the value can be adequately recreated (to equality) by creating + * a new instance. For example, a java.util.HashMap is constructable provided + * that the map is empty. + *

+ * Subclasses can override this to handle any well-known types, and in conjunction with + * {@link #constructInstance(String)}, even choose to return true if it needs a + * non-default constructor. + * + * @param value the value to check + * @return Returns true if the value can be reconstructed by + * instantiation using a default constructor + */ + protected boolean isConstructable(Serializable value) + { + // Is it in the set directly + Class valueClazz = value.getClass(); + // Check for default constructor + try + { + valueClazz.getConstructor(); + } + catch (NoSuchMethodException e) + { + // Can't reconstruct using just a type name + return false; + } + // Maps and Collections + if (value instanceof Map) + { + Map mapValue = (Map) value; + return mapValue.isEmpty(); + } + else if (value instanceof Collection) + { + Collection collectionValue = (Collection) value; + return collectionValue.isEmpty(); + } + else + { + // We don't recognise it + return false; + } + } + + /** + * {@inheritDoc} + */ + public Serializable constructInstance(String clazzName) + { + try + { + Class clazz = Class.forName(clazzName); + Constructor constructor = clazz.getConstructor(); + return (Serializable) constructor.newInstance(); + } + catch (ClassCastException e) + { + throw new AlfrescoRuntimeException("The constructed property is not serializable: " + clazzName); + } + catch (Throwable e) + { + throw new AlfrescoRuntimeException("Unable to construct property for class: " + clazzName); + } + } + /** * {@inheritDoc} */ @@ -98,20 +179,38 @@ public class DefaultPropertyTypeConverter implements PropertyTypeConverter Class clazz = value.getClass(); PersistedType type = persistenceMapping.get(clazz); - if (type == null) + if (type != null) { - return PersistedType.SERIALIZABLE; + return type; + } + // Before we give up, check if it is constructable + if (isConstructable(value)) + { + // It'll just be given back as a class name i.e. a CONSTRUCTABLE + return PersistedType.CONSTRUCTABLE; } else { - return type; + // Check if there are converters to and from well-known types, just in case + if (DefaultTypeConverter.INSTANCE.getConverter(clazz, Long.class) != null && + DefaultTypeConverter.INSTANCE.getConverter(Long.class, clazz) != null) + { + return PersistedType.LONG; + } + else if (DefaultTypeConverter.INSTANCE.getConverter(clazz, String.class) != null && + DefaultTypeConverter.INSTANCE.getConverter(String.class, clazz) != null) + { + return PersistedType.STRING; + } + // No hope of doing anything useful other than storing it + return PersistedType.SERIALIZABLE; } } /** * Performs the conversion */ - public T convert(Class targetClass, Object value) + public T convert(Class targetClass, Serializable value) { return DefaultTypeConverter.INSTANCE.convert(targetClass, value); } diff --git a/source/java/org/alfresco/repo/domain/propval/PropertyIdSearchRow.java b/source/java/org/alfresco/repo/domain/propval/PropertyIdSearchRow.java index cb7b76ed8a..d8c2dc517c 100644 --- a/source/java/org/alfresco/repo/domain/propval/PropertyIdSearchRow.java +++ b/source/java/org/alfresco/repo/domain/propval/PropertyIdSearchRow.java @@ -64,25 +64,30 @@ public class PropertyIdSearchRow return valueEntity; } - public void setRootPropId(long rootPropId) + public void setRootPropId(Long rootPropId) { linkEntity.setRootPropId(rootPropId); } - public void setCurrentPropId(long currentPropId) + public void setPropIndex(Long propIndex) { - linkEntity.setCurrentPropId(currentPropId); + linkEntity.setPropIndex(propIndex); } - public void setValuePropId(long valuePropId) + public void setContainedIn(Long containedIn) { - linkEntity.setValuePropId(valuePropId); + linkEntity.setContainedIn(containedIn); } - public void setKeyPropId(long keyPropId) + public void setKeyPropId(Long keyPropId) { linkEntity.setKeyPropId(keyPropId); } + + public void setValuePropId(Long valuePropId) + { + linkEntity.setValuePropId(valuePropId); + } public void setActualTypeId(Long actualTypeId) { diff --git a/source/java/org/alfresco/repo/domain/propval/PropertyLinkEntity.java b/source/java/org/alfresco/repo/domain/propval/PropertyLinkEntity.java index 542655f009..e2b9020315 100644 --- a/source/java/org/alfresco/repo/domain/propval/PropertyLinkEntity.java +++ b/source/java/org/alfresco/repo/domain/propval/PropertyLinkEntity.java @@ -32,93 +32,77 @@ package org.alfresco.repo.domain.propval; */ public class PropertyLinkEntity { - private long rootPropId; - private long currentPropId; - private long valuePropId; - private long keyPropId; + private Long rootPropId; + private Long propIndex; + private Long containedIn; + private Long keyPropId; + private Long valuePropId; public PropertyLinkEntity() { } - @Override - public int hashCode() - { - return (int) rootPropId + (int) valuePropId; - } - - @Override - public boolean equals(Object obj) - { - if (this == obj) - { - return true; - } - else if (obj instanceof PropertyLinkEntity) - { - PropertyLinkEntity that = (PropertyLinkEntity) obj; - return - this.rootPropId == that.rootPropId && - this.currentPropId == that.currentPropId && - this.valuePropId == that.valuePropId && - this.keyPropId == that.keyPropId; - } - else - { - return false; - } - } - @Override public String toString() { StringBuilder sb = new StringBuilder(512); sb.append("PropertyLinkEntity") .append("[ rootPropId=").append(rootPropId) - .append(", currentPropId=").append(currentPropId) - .append(", valuePropId=").append(valuePropId) + .append(", propIndex=").append(propIndex) + .append(", containedIn=").append(containedIn) .append(", keyPropId=").append(keyPropId) + .append(", valuePropId=").append(valuePropId) .append("]"); return sb.toString(); } - public long getRootPropId() + public Long getRootPropId() { return rootPropId; } - public void setRootPropId(long rootPropId) + public void setRootPropId(Long rootPropId) { this.rootPropId = rootPropId; } - public long getCurrentPropId() + public Long getPropIndex() { - return currentPropId; + return propIndex; } - public void setCurrentPropId(long currentPropId) + public void setPropIndex(Long propIndex) { - this.currentPropId = currentPropId; + this.propIndex = propIndex; } - public long getValuePropId() + public Long getContainedIn() { - return valuePropId; + return containedIn; } - public void setValuePropId(long valuePropId) + public void setContainedIn(Long containedIn) { - this.valuePropId = valuePropId; + this.containedIn = containedIn; } - public long getKeyPropId() + public Long getKeyPropId() { return keyPropId; } - public void setKeyPropId(long keyPropId) + public void setKeyPropId(Long keyPropId) { this.keyPropId = keyPropId; } + + public Long getValuePropId() + { + return valuePropId; + } + + public void setValuePropId(Long valuePropId) + { + this.valuePropId = valuePropId; + } } diff --git a/source/java/org/alfresco/repo/domain/propval/PropertyRootEntity.java b/source/java/org/alfresco/repo/domain/propval/PropertyRootEntity.java new file mode 100644 index 0000000000..489cec80d5 --- /dev/null +++ b/source/java/org/alfresco/repo/domain/propval/PropertyRootEntity.java @@ -0,0 +1,84 @@ +/* + * 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.propval; + +/** + * Entity bean for alf_prop_root table. + * + * @author Derek Hulley + * @since 3.2 + */ +public class PropertyRootEntity +{ + private Long id; + private short version; + + public PropertyRootEntity() + { + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(512); + sb.append("PropertyRootEntity") + .append("[ ID=").append(id) + .append(", version=").append(version) + .append("]"); + return sb.toString(); + } + + public void incrementVersion() + { + if (version >= Short.MAX_VALUE) + { + this.version = 0; + } + else + { + this.version++; + } + } + + public Long getId() + { + return id; + } + + public void setId(Long id) + { + this.id = id; + } + + public short getVersion() + { + return version; + } + + public void setVersion(short version) + { + this.version = version; + } +} diff --git a/source/java/org/alfresco/repo/domain/propval/PropertyTypeConverter.java b/source/java/org/alfresco/repo/domain/propval/PropertyTypeConverter.java index f6a151cc8a..0a5f436b1b 100644 --- a/source/java/org/alfresco/repo/domain/propval/PropertyTypeConverter.java +++ b/source/java/org/alfresco/repo/domain/propval/PropertyTypeConverter.java @@ -48,10 +48,14 @@ public interface PropertyTypeConverter *

  • {@link PersistedType#DOUBLE}
  • *
  • {@link PersistedType#STRING}
  • *
  • {@link PersistedType#SERIALIZABLE}
  • + *
  • {@link PersistedType#CONSTRUCTABLE}
  • * * The converter should return {@link PersistedType#SERIALIZABLE} if no further conversions * are possible. Implicit in the return value is the converter's ability to do the * conversion when required. + *

    + * If the converter can fully reconstruct an equal instance using just the name of the value's + * class, then {@link PersistedType#CONSTRUCTABLE} can be used. * * @param value the value that does not have an obvious persistence slot * @return Returns the type of persistence to use @@ -64,5 +68,13 @@ public interface PropertyTypeConverter * @param value the value to convert * @return Returns the persisted type and value to persist */ - T convert(Class targetClass, Object value); + T convert(Class targetClass, Serializable value); + + /** + * Construct an instance of an object that was deemed to be {@link PersistedType#CONSTRUCTABLE}. + * + * @param clazzName the name of the class + * @return Returns the new instance + */ + Serializable constructInstance(String clazzName); } diff --git a/source/java/org/alfresco/repo/domain/propval/PropertyValueDAO.java b/source/java/org/alfresco/repo/domain/propval/PropertyValueDAO.java index 831b8f325e..80d06dff76 100644 --- a/source/java/org/alfresco/repo/domain/propval/PropertyValueDAO.java +++ b/source/java/org/alfresco/repo/domain/propval/PropertyValueDAO.java @@ -175,27 +175,41 @@ public interface PropertyValueDAO * @param value the value to find the ID for (may be null) */ Pair getOrCreatePropertyValue(Serializable value); + + //================================ + // 'alf_prop_root' accessors + //================================ /** - * alf_prop_value accessor: find or create a property based on the value. - * Note: This method will not recurse into maps or collections. Use the - * dedicated methods if you want recursion; otherwise maps and collections will - * be serialized and probably stored as BLOB values. - *

    - * Max depth examples (there is no upper limit): - *

      - *
    • 0: don't expand the value if it's a map or collection
    • - *
    • 1: open the value up if it is a map or collection but don't do any more
    • - *
    • ...
    • - *
    • 10: open up 10 levels of maps or collections
    • - *
    - * All collections that are not opened up will be serialized unless there is a - * custom {@link PropertyTypeConverter converter} which can serialize it in an - * alternative format. + * alf_prop_root accessor: get a property based on the database ID * - * @param value the value to find the ID for (may be null) - * @param maxDepth the maximum depth of collections and maps to iterate into + * @param id the ID (may not be null) + * @return Returns the value of the property (never null) */ - Pair getOrCreatePropertyValue(Serializable value, int maxDepth); + Serializable getPropertyById(Long id); + /** + * alf_prop_root accessor: find or create a property based on the value. + *

    + * All collections and maps will be opened up to any depth. + * + * @param value the value to create (may be null) + * @return Returns the new property's ID + */ + Long createProperty(Serializable value); + + /** + * alf_prop_root accessor: update the property root to contain a new value. + * + * @param id the ID of the root property to change + * @param value the new property value + */ + void updateProperty(Long id, Serializable value); + + /** + * alf_prop_root accessor: delete a property root completely + * + * @param id the ID of the root property to delete + */ + void deleteProperty(Long id); /** * Utility method to convert property query results into the original value. Note diff --git a/source/java/org/alfresco/repo/domain/propval/PropertyValueDAOTest.java b/source/java/org/alfresco/repo/domain/propval/PropertyValueDAOTest.java index 2901b28deb..e122088c43 100644 --- a/source/java/org/alfresco/repo/domain/propval/PropertyValueDAOTest.java +++ b/source/java/org/alfresco/repo/domain/propval/PropertyValueDAOTest.java @@ -28,6 +28,7 @@ import java.io.Serializable; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; import java.util.Random; @@ -65,6 +66,7 @@ public class PropertyValueDAOTest extends TestCase ServiceRegistry serviceRegistry = (ServiceRegistry) ctx.getBean(ServiceRegistry.SERVICE_REGISTRY); transactionService = serviceRegistry.getTransactionService(); txnHelper = transactionService.getRetryingTransactionHelper(); + txnHelper.setMaxRetries(0); propertyValueDAO = (PropertyValueDAO) ctx.getBean("propertyValueDAO"); @@ -434,6 +436,24 @@ public class PropertyValueDAOTest extends TestCase } } + public void testPropertyValue_EmptyHashMap() throws Exception + { + final HashMap map = new HashMap(15); + runPropertyValueTest(map, true); + } + + public void testPropertyValue_EmptyArrayList() throws Exception + { + final ArrayList list = new ArrayList(20); + runPropertyValueTest(list, true); + } + + public void testPropertyValue_EmptyHashSet() throws Exception + { + final HashSet set = new HashSet(20); + runPropertyValueTest(set, true); + } + public void testPropertyValue_MapOfStrings() throws Exception { final HashMap map = new HashMap(15); @@ -446,7 +466,53 @@ public class PropertyValueDAOTest extends TestCase runPropertyValueTest(map, false); } - public void testPropertyValue_MapOfMapOfSerializables() throws Exception + /** + * Tests that the given value can be persisted and retrieved with the same resulting ID + */ + private Long runPropertyTest(final Serializable value) throws Exception + { + // Create it + RetryingTransactionCallback createValueCallback = new RetryingTransactionCallback() + { + public Long execute() throws Throwable + { + // Get the classes + return propertyValueDAO.createProperty(value); + } + }; + final Long entityId = txnHelper.doInTransaction(createValueCallback, false); + assertNotNull(entityId); + + // Retrieve it by ID + RetryingTransactionCallback getByIdCallback = new RetryingTransactionCallback() + { + public Serializable execute() throws Throwable + { + // Get the classes + return propertyValueDAO.getPropertyById(entityId); + } + }; + final Serializable entityValueCheck = txnHelper.doInTransaction(getByIdCallback, false); + assertNotNull(entityValueCheck); + assertEquals(value, entityValueCheck); + + // Done + return entityId; + } + + public void testProperty_MapOfStrings() throws Exception + { + final HashMap map = new HashMap(15); + for (int i = 0; i < 20; i++) + { + String key = "MAP-KEY-" + i; + String value = "MAP-VALUE-" + i; + map.put(key, value); + } + runPropertyTest(map); + } + + public void testProperty_MapOfMapOfSerializables() throws Exception { final HashMap mapInner = new HashMap(15); for (int i = 0; i < 20; i++) @@ -461,10 +527,10 @@ public class PropertyValueDAOTest extends TestCase String key = "OUTERMAP-KEY-" + i; mapOuter.put(key, mapInner); } - runPropertyValueTest(mapOuter, false); + runPropertyTest(mapOuter); } - public void testPropertyValue_MapOfMapOfStrings() throws Exception + public void testProperty_MapOfMapOfStrings() throws Exception { final HashMap mapInner = new HashMap(15); for (int i = 0; i < 20; i++) @@ -479,10 +545,10 @@ public class PropertyValueDAOTest extends TestCase String key = "OUTERMAP-KEY-" + i; mapOuter.put(key, mapInner); } - runPropertyValueTest(mapOuter, false); + runPropertyTest(mapOuter); } - public void testPropertyValue_CollectionOfStrings() throws Exception + public void testProperty_CollectionOfStrings() throws Exception { final ArrayList list = new ArrayList(20); for (int i = 0; i < 20; i++) @@ -490,9 +556,77 @@ public class PropertyValueDAOTest extends TestCase String value = "COLL-VALUE-" + i; list.add(value); } - runPropertyValueTest(list, false); + runPropertyTest(list); } + public void testProperty_UpdateCollection() throws Exception + { + final ArrayList list = new ArrayList(20); + for (int i = 0; i < 20; i++) + { + String value = "COLL-VALUE-" + i; + list.add(value); + } + final Long propId = runPropertyTest(list); + + // Now update it + list.add("Additional value"); + + RetryingTransactionCallback updateAndGetCallback = new RetryingTransactionCallback() + { + public Serializable execute() throws Throwable + { + // Get the classes + propertyValueDAO.updateProperty(propId, list); + // Get it by the ID again + return propertyValueDAO.getPropertyById(propId); + } + }; + final Serializable entityValueCheck = txnHelper.doInTransaction(updateAndGetCallback, false); + assertNotNull(entityValueCheck); + assertEquals(list, entityValueCheck); + } + + public void testProperty_Delete() throws Exception + { + final ArrayList list = new ArrayList(20); + final Long propId = runPropertyTest(list); + + // Now delete it + RetryingTransactionCallback deleteCallback = new RetryingTransactionCallback() + { + public Serializable execute() throws Throwable + { + // Get the classes + propertyValueDAO.deleteProperty(propId); + return null; + } + }; + txnHelper.doInTransaction(deleteCallback, false); + + RetryingTransactionCallback failedGetCallback = new RetryingTransactionCallback() + { + public Serializable execute() throws Throwable + { + // Get it by the ID again + return propertyValueDAO.getPropertyById(propId); + } + }; + try + { + final Serializable entityValueCheck = txnHelper.doInTransaction(failedGetCallback, false); + fail("Deleted property should not be gettable. Got: " + entityValueCheck); + } + catch(Throwable e) + { + // Expected + } + } + + /* + * Switch off caches and rerun some of the tests + */ + public void testPropertyClass_NoCache() throws Exception { removeCaches(); diff --git a/source/java/org/alfresco/repo/domain/propval/PropertyValueEntity.java b/source/java/org/alfresco/repo/domain/propval/PropertyValueEntity.java index bf269a3d59..f5d5bbb0cc 100644 --- a/source/java/org/alfresco/repo/domain/propval/PropertyValueEntity.java +++ b/source/java/org/alfresco/repo/domain/propval/PropertyValueEntity.java @@ -25,9 +25,7 @@ package org.alfresco.repo.domain.propval; import java.io.Serializable; -import java.util.Collection; import java.util.Collections; -import java.util.Date; import java.util.HashMap; import java.util.Map; @@ -171,11 +169,6 @@ public class PropertyValueEntity */ public static final Map persistedTypesByOrdinal; - /** - * An unmodifiable map of persisted type enums keyed by the classes they store - */ - public static final Map, PersistedType> persistedTypesByClass; - static { // Create a pair for null values @@ -187,20 +180,6 @@ public class PropertyValueEntity mapOrdinal.put(persistedType.getOrdinalNumber(), persistedType); } persistedTypesByOrdinal = Collections.unmodifiableMap(mapOrdinal); - // Create the map of class-type - Map, PersistedType> mapClass = new HashMap, PersistedType>(29); - mapClass.put(Boolean.class, PersistedType.LONG); - mapClass.put(Short.class, PersistedType.LONG); - mapClass.put(Integer.class, PersistedType.LONG); - mapClass.put(Long.class, PersistedType.LONG); - mapClass.put(Float.class, PersistedType.DOUBLE); - mapClass.put(Double.class, PersistedType.DOUBLE); - mapClass.put(String.class, PersistedType.STRING); - mapClass.put(Date.class, PersistedType.LONG); - mapClass.put(Map.class, PersistedType.SERIALIZABLE); // Will be serialized if encountered - mapClass.put(Collection.class, PersistedType.SERIALIZABLE); // Will be serialized if encountered - mapClass.put(Class.class, PersistedType.CONSTRUCTABLE); // Will construct a new instance - persistedTypesByClass = Collections.unmodifiableMap(mapClass); } private static final Log logger = LogFactory.getLog(PropertyValueEntity.class); @@ -259,40 +238,29 @@ public class PropertyValueEntity } /** - * Gets the value based on the persisted type. - * Note that this is the value as persisted and not the original, client-required - * value. - * @return Returns the persisted value + * Helper method to get the value based on the persisted type. + * + * @param actualType the type to convert to + * @param converter the data converter to use + * @return Returns the converted value */ - public Serializable getPersistedValue() + public Serializable getValue(Class actualType, PropertyTypeConverter converter) { switch (persistedTypeEnum) { case NULL: return null; case LONG: - return longValue; + return converter.convert(actualType, Long.valueOf(longValue)); case DOUBLE: - return doubleValue; + return converter.convert(actualType, Double.valueOf(doubleValue)); case STRING: - return stringValue; + return converter.convert(actualType, stringValue); case SERIALIZABLE: - return serializableValue; + return converter.convert(actualType, serializableValue); case CONSTRUCTABLE: - // Construct an instance - try - { - Class clazz = Class.forName(stringValue); - return (Serializable) clazz.newInstance(); - } - catch (ClassNotFoundException e) - { - throw new RuntimeException("Unable to construct instance of class " + stringValue, e); - } - catch (Throwable e) - { - throw new RuntimeException("Unable to create new instance of " + stringValue, e); - } + // Construct an instance using the converter (it knows best!) + return converter.constructInstance(stringValue); default: throw new IllegalStateException("Should not be able to get through switch"); } @@ -313,22 +281,11 @@ public class PropertyValueEntity this.persistedTypeEnum = PersistedType.NULL; this.longValue = LONG_ZERO; } - else if (value instanceof Class) - { - Class clazz = (Class) value; - stringValue = clazz.getName(); - persistedTypeEnum = PersistedType.CONSTRUCTABLE; - persistedType = persistedTypeEnum.getOrdinalNumber(); - } else { - Class valueClazz = value.getClass(); - persistedTypeEnum = persistedTypesByClass.get(valueClazz); - if (persistedTypeEnum == null) - { - // Give the converter a chance to change the type it must be persisted as - persistedTypeEnum = converter.getPersistentType(value); - } + // The converter will be responsible for deserializing, so let it choose + // how the data is to be stored. + persistedTypeEnum = converter.getPersistentType(value); persistedType = persistedTypeEnum.getOrdinalNumber(); // Get the class to persist as switch (persistedTypeEnum) @@ -342,6 +299,10 @@ public class PropertyValueEntity case STRING: stringValue = converter.convert(String.class, value); break; + case CONSTRUCTABLE: + // A special case. There is no conversion, so just Store the name of the class. + stringValue = value.getClass().getName(); + break; case SERIALIZABLE: serializableValue = value; break; @@ -359,9 +320,12 @@ public class PropertyValueEntity * Helper method to determine how the given value will be stored. * * @param value the value to check + * @param converter the type converter * @return Returns the persisted type + * + * @see PropertyTypeConverter#getPersistentType(Serializable) */ - public static PersistedType getPersistedTypeEnum(Serializable value) + public static PersistedType getPersistedTypeEnum(Serializable value, PropertyTypeConverter converter) { PersistedType persistedTypeEnum; if (value == null) @@ -370,12 +334,7 @@ public class PropertyValueEntity } else { - Class valueClazz = value.getClass(); - persistedTypeEnum = persistedTypesByClass.get(valueClazz); - if (persistedTypeEnum == null) - { - persistedTypeEnum = PersistedType.SERIALIZABLE; - } + persistedTypeEnum = converter.getPersistentType(value); } return persistedTypeEnum; } diff --git a/source/java/org/alfresco/repo/domain/propval/ibatis/PropertyValueDAOImpl.java b/source/java/org/alfresco/repo/domain/propval/ibatis/PropertyValueDAOImpl.java index 5bfe50f796..30d2c06a46 100644 --- a/source/java/org/alfresco/repo/domain/propval/ibatis/PropertyValueDAOImpl.java +++ b/source/java/org/alfresco/repo/domain/propval/ibatis/PropertyValueDAOImpl.java @@ -34,6 +34,7 @@ import org.alfresco.repo.domain.propval.PropertyDateValueEntity; import org.alfresco.repo.domain.propval.PropertyDoubleValueEntity; import org.alfresco.repo.domain.propval.PropertyIdSearchRow; import org.alfresco.repo.domain.propval.PropertyLinkEntity; +import org.alfresco.repo.domain.propval.PropertyRootEntity; import org.alfresco.repo.domain.propval.PropertySerializableValueEntity; import org.alfresco.repo.domain.propval.PropertyStringQueryEntity; import org.alfresco.repo.domain.propval.PropertyStringValueEntity; @@ -75,7 +76,13 @@ public class PropertyValueDAOImpl extends AbstractPropertyValueDAOImpl private static final String SELECT_PROPERTY_VALUE_BY_STRING_VALUE = "alfresco.propval.select_PropertyValueByStringValue"; private static final String INSERT_PROPERTY_VALUE = "alfresco.propval.insert_PropertyValue"; + private static final String SELECT_PROPERTY_BY_ID = "alfresco.propval.select_PropertyById"; + private static final String SELECT_PROPERTY_ROOT_BY_ID = "alfresco.propval.select_PropertyRootById"; + private static final String INSERT_PROPERTY_ROOT = "alfresco.propval.insert_PropertyRoot"; + private static final String UPDATE_PROPERTY_ROOT = "alfresco.propval.update_PropertyRoot"; + private static final String DELETE_PROPERTY_ROOT_BY_ID = "alfresco.propval.delete_PropertyRootById"; private static final String INSERT_PROPERTY_LINK = "alfresco.propval.insert_PropertyLink"; + private static final String DELETE_PROPERTY_LINKS_BY_ROOT_ID = "alfresco.propval.delete_PropertyLinksByRootId"; private SqlMapClientTemplate template; @@ -287,18 +294,30 @@ public class PropertyValueDAOImpl extends AbstractPropertyValueDAOImpl @SuppressWarnings("unchecked") @Override - protected List findPropertyValueById(Long id) + protected PropertyValueEntity findPropertyValueById(Long id) { PropertyValueEntity entity = new PropertyValueEntity(); entity.setId(id); - List results = (List) template.queryForList( + List results = (List) template.queryForList( SELECT_PROPERTY_VALUE_BY_ID, entity); - // Done - return results; + // At most one of the results represents a real value + int size = results.size(); + if (size == 0) + { + return null; + } + else if (size == 1) + { + return results.get(0); + } + else + { + logger.error("Found property value linked to multiple raw types: " + results); + return results.get(0); + } } - @SuppressWarnings("unchecked") @Override protected PropertyValueEntity findPropertyValueByValue(Serializable value) { @@ -336,6 +355,8 @@ public class PropertyValueDAOImpl extends AbstractPropertyValueDAOImpl case DOUBLE: query = SELECT_PROPERTY_VALUE_BY_DOUBLE_VALUE; break; + case CONSTRUCTABLE: + // The string value is the name of the class (e.g. 'java.util.HashMap') case STRING: // It's best to query using the CRC and short end-value query = SELECT_PROPERTY_VALUE_BY_STRING_VALUE; @@ -355,15 +376,8 @@ public class PropertyValueDAOImpl extends AbstractPropertyValueDAOImpl PropertyValueEntity result = null; if (query != null) { - List results = (List) template.queryForList( - query, - queryObject, - 0, 1); // Only want one result - for (PropertyValueEntity row : results) - { - result = row; - break; - } + // Uniqueness is guaranteed by the tables, so we get one value only + result = (PropertyValueEntity) template.queryForObject(query, queryObject); } // Done @@ -416,19 +430,77 @@ public class PropertyValueDAOImpl extends AbstractPropertyValueDAOImpl return insertEntity; } + //================================ + // 'alf_prop_root' accessors + //================================ + + @SuppressWarnings("unchecked") + @Override + protected List findPropertyById(Long id) + { + PropertyValueEntity entity = new PropertyValueEntity(); + entity.setId(id); + List results = (List) template.queryForList( + SELECT_PROPERTY_BY_ID, + entity); + return results; + } + + @Override + protected Long createPropertyRoot() + { + PropertyRootEntity rootEntity = new PropertyRootEntity(); + rootEntity.setVersion((short)0); + return (Long) template.insert(INSERT_PROPERTY_ROOT, rootEntity); + } + + @Override + protected PropertyRootEntity getPropertyRoot(Long id) + { + PropertyRootEntity entity = new PropertyRootEntity(); + entity.setId(id); + return (PropertyRootEntity) template.queryForObject(SELECT_PROPERTY_ROOT_BY_ID, entity); + } + + @Override + protected PropertyRootEntity updatePropertyRoot(PropertyRootEntity entity) + { + entity.incrementVersion(); + template.update(UPDATE_PROPERTY_ROOT, entity, 1); + return entity; + } + + @Override + protected void deletePropertyRoot(Long id) + { + PropertyRootEntity entity = new PropertyRootEntity(); + entity.setId(id); + template.delete(DELETE_PROPERTY_ROOT_BY_ID, entity); + } + @Override protected void createPropertyLink( Long rootPropId, - Long currentPropId, - Long valueId, - Long keyId) + Long propIndex, + Long containedIn, + Long keyPropId, + Long valuePropId) { PropertyLinkEntity insertEntity = new PropertyLinkEntity(); insertEntity.setRootPropId(rootPropId); - insertEntity.setCurrentPropId(currentPropId); - insertEntity.setValuePropId(valueId); - insertEntity.setKeyPropId(keyId); + insertEntity.setPropIndex(propIndex); + insertEntity.setContainedIn(containedIn); + insertEntity.setKeyPropId(keyPropId); + insertEntity.setValuePropId(valuePropId); template.insert(INSERT_PROPERTY_LINK, insertEntity); // Done } + + @Override + protected int deletePropertyLinks(Long rootPropId) + { + PropertyRootEntity entity = new PropertyRootEntity(); + entity.setId(rootPropId); + return template.delete(DELETE_PROPERTY_LINKS_BY_ROOT_ID, entity); + } }