From ef67ac777a88b6dbc56b64d8d851351053da0864 Mon Sep 17 00:00:00 2001 From: Derek Hulley Date: Wed, 26 Aug 2009 06:01:52 +0000 Subject: [PATCH] AuditComponent implementation and fallout - alf_prop_string_value now includes a CRC column and handles Oracle empty string issues - All property values are/must now be Serializable for auditing - Pushing data into audit is working git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@15915 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261 --- .../audit/alfresco-audit-repository.xml | 6 +- .../AlfrescoPostCreate-3.2-AuditTables.sql | 14 ++ ...escoPostCreate-3.2-PropertyValueTables.sql | 7 +- .../audit-common-SqlMap.xml | 13 ++ .../propval-common-SqlMap.xml | 33 +-- .../audit-insert-SqlMap.xml | 7 + .../alfresco/repo/audit/AuditComponent.java | 26 ++- .../repo/audit/AuditComponentImpl.java | 213 ++++++++++++++++-- .../repo/audit/AuditComponentTest.java | 155 +++++++++++++ .../org/alfresco/repo/audit/AuditSession.java | 10 - .../repo/audit/extractor/DataExtractor.java | 6 +- .../extractor/SimpleValueDataExtractor.java | 6 +- .../AuthenticatedUserDataGenerator.java | 4 +- .../repo/audit/generator/DataGenerator.java | 4 +- .../generator/SystemTimeDataGenerator.java | 3 +- .../generator/TransactionIdDataGenerator.java | 4 +- .../audit/hibernate/HibernateAuditDAO.java | 12 + .../repo/audit/model/AuditApplication.java | 24 ++ .../repo/audit/model/AuditModelRegistry.java | 25 +- .../alfresco/repo/domain/DomainTestSuite.java | 2 + .../domain/audit/AbstractAuditDAOImpl.java | 49 +++- .../alfresco/repo/domain/audit/AuditDAO.java | 15 +- .../repo/domain/audit/AuditDAOTest.java | 43 ++++ .../repo/domain/audit/AuditEntryEntity.java | 109 +++++++++ .../domain/audit/ibatis/AuditDAOImpl.java | 16 ++ .../propval/PropertyStringValueEntity.java | 37 +-- .../domain/propval/PropertyValueDAOTest.java | 20 +- .../propval/ibatis/PropertyValueDAOImpl.java | 18 +- .../alfresco/audit/alfresco-audit-test.xml | 4 +- 29 files changed, 767 insertions(+), 118 deletions(-) create mode 100644 source/java/org/alfresco/repo/audit/AuditComponentTest.java create mode 100644 source/java/org/alfresco/repo/domain/audit/AuditEntryEntity.java diff --git a/config/alfresco/audit/alfresco-audit-repository.xml b/config/alfresco/audit/alfresco-audit-repository.xml index 40e7f3311c..955a7e7c3b 100644 --- a/config/alfresco/audit/alfresco-audit-repository.xml +++ b/config/alfresco/audit/alfresco-audit-repository.xml @@ -17,14 +17,10 @@ - - - - @@ -49,4 +45,4 @@ - \ No newline at end of file + 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 712696ccd5..4795de2da9 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 @@ -27,6 +27,20 @@ CREATE TABLE alf_audit_session PRIMARY KEY (id) ) ENGINE=InnoDB; +CREATE TABLE alf_audit_entry +( + id BIGINT NOT NULL AUTO_INCREMENT, + audit_session_id BIGINT NOT NULL, + audit_time BIGINT NOT NULL, + audit_user_id BIGINT NULL, + audit_values_id BIGINT NOT NULL, + CONSTRAINT fk_alf_audit_ent_sess FOREIGN KEY (audit_session_id) REFERENCES alf_audit_session (id), + 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), + PRIMARY KEY (id) +) ENGINE=InnoDB; + -- -- Record script finish -- 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 f09c67d2d7..f23442cd8e 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 @@ -46,9 +46,10 @@ CREATE TABLE alf_prop_string_value ( id BIGINT NOT NULL AUTO_INCREMENT, string_value TEXT NOT NULL, - string_end VARCHAR(16) NOT NULL, - INDEX idx_alf_prop_str_start (string_value(32)), - INDEX idx_alf_prop_str_end (string_end), + 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), PRIMARY KEY (id) ) 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 aa7ebcd557..ddfac2d6f4 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 @@ -12,6 +12,7 @@ + @@ -27,6 +28,13 @@ + + + + + + + @@ -50,6 +58,11 @@ values (#auditModelId#, #applicationNameId#) + + insert into alf_audit_entry (audit_session_id, audit_user_id, audit_time, audit_values_id) + values (#auditSessionId#, #auditUserId#, #auditTime#, #auditValuesId#) + + 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 4b40761679..234f3ad293 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 @@ -44,7 +44,8 @@ - + + @@ -92,8 +93,8 @@ - insert into alf_prop_string_value (string_value, string_end) - values (#stringValue#, #stringEnd#) + insert into alf_prop_string_value (string_value, string_end_lower, string_crc) + values (#stringValue#, #stringEndLower#, #stringCrc#) @@ -180,32 +181,16 @@ id = #id# - - select - id, - - - - string_end as string_value, - string_end - - - string_value, - string_end - - + id from alf_prop_string_value where - string_end = #stringEnd# - - - and string_value = #stringValue# - - + string_end_lower = #stringEndLower# and + string_crc = #stringCrc# diff --git a/config/alfresco/ibatis/org.hibernate.dialect.MySQLInnoDBDialect/audit-insert-SqlMap.xml b/config/alfresco/ibatis/org.hibernate.dialect.MySQLInnoDBDialect/audit-insert-SqlMap.xml index 9b6026cfef..3559481dd7 100644 --- a/config/alfresco/ibatis/org.hibernate.dialect.MySQLInnoDBDialect/audit-insert-SqlMap.xml +++ b/config/alfresco/ibatis/org.hibernate.dialect.MySQLInnoDBDialect/audit-insert-SqlMap.xml @@ -20,4 +20,11 @@ + + + + KEY_COLUMN:GENERATED_KEY + + + \ No newline at end of file diff --git a/source/java/org/alfresco/repo/audit/AuditComponent.java b/source/java/org/alfresco/repo/audit/AuditComponent.java index 908fef34fb..a016caf948 100644 --- a/source/java/org/alfresco/repo/audit/AuditComponent.java +++ b/source/java/org/alfresco/repo/audit/AuditComponent.java @@ -24,6 +24,7 @@ */ package org.alfresco.repo.audit; +import java.io.Serializable; import java.util.List; import java.util.Map; @@ -82,29 +83,44 @@ public interface AuditComponent */ /** - * Start an audit session for the given root path. All later audit operations on the resulting - * session will be relative to this root path. + * Start an audit session for the given root path. All later audit values must start with + * the same root path. *

* The name of the application controls part of the audit model will be used. The root path must * start with the matching key attribute that was declared for the matching * Application element in the audit configuration. + *

+ * This is a read-write method. Client code must wrap calls in the appropriate transactional wrappers. * - * @param application the name of the application to log against + * @param applicationName the name of the application to log against * @param rootPath a base path of {@link AuditPath} key entries concatenated with . (period) * @return Returns the unique session + * @throws IllegalStateException if there is not a writable transaction present */ - public AuditSession startAuditSession(String application, String rootPath); + AuditSession startAuditSession(String applicationName, String rootPath); + + /** + * {@inheritDoc #startAuditSession(String, String)} + + * @param values values to associate with the session. These values will override or + * complement generated session-specific values + * @throws IllegalStateException if there is not a writable transaction present + */ + AuditSession startAuditSession(String applicationName, String rootPath, Map values); /** * Record a set of values against the given session. + *

+ * This is a read-write method. Client code must wrap calls in the appropriate transactional wrappers. * * @param session a pre-existing audit session to continue with * @param values the values to audit mapped by {@link AuditPath} key relative to the session * root path + * @throws IllegalStateException if there is not a writable transaction present * * @see #startAuditSession() * * @since 3.2 */ - public void audit(AuditSession session, Map values); + void audit(AuditSession session, Map values); } diff --git a/source/java/org/alfresco/repo/audit/AuditComponentImpl.java b/source/java/org/alfresco/repo/audit/AuditComponentImpl.java index 5f859bf60d..d5c819c4e4 100644 --- a/source/java/org/alfresco/repo/audit/AuditComponentImpl.java +++ b/source/java/org/alfresco/repo/audit/AuditComponentImpl.java @@ -29,9 +29,13 @@ import java.lang.reflect.Method; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.Date; +import java.util.HashMap; import java.util.List; import java.util.Map; +import org.alfresco.repo.audit.extractor.DataExtractor; +import org.alfresco.repo.audit.generator.DataGenerator; +import org.alfresco.repo.audit.generator.DataGenerator.DataGeneratorScope; import org.alfresco.repo.audit.model.AuditApplication; import org.alfresco.repo.audit.model.AuditEntry; import org.alfresco.repo.audit.model.AuditModelRegistry; @@ -53,6 +57,7 @@ import org.alfresco.service.cmr.repository.StoreRef; import org.alfresco.service.cmr.search.SearchParameters; import org.alfresco.service.namespace.NamespacePrefixResolver; import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.ParameterCheck; import org.aopalliance.intercept.MethodInvocation; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -751,8 +756,6 @@ public class AuditComponentImpl implements AuditComponent * V3.2 from here on. Put all fixes to the older audit code before this point, please. */ - private static final AuditSession NO_AUDIT_SESSION = new AuditSession(); - private AuditModelRegistry auditModelRegistry; /** @@ -764,8 +767,27 @@ public class AuditComponentImpl implements AuditComponent this.auditModelRegistry = auditModelRegistry; } + /** + * @see #startAuditSession(String, String, Map) + * @since 3.2 + */ public AuditSession startAuditSession(String applicationName, String rootPath) { + return startAuditSession(applicationName, rootPath, new HashMap(11)); + } + /** + * {@inheritDoc} + * @since 3.2 + */ + public AuditSession startAuditSession(String applicationName, String rootPath, Map values) + { + ParameterCheck.mandatory("applicationName", applicationName); + ParameterCheck.mandatory("values", values); + + if (AlfrescoTransactionSupport.getTransactionReadState() != TxnReadState.TXN_READ_WRITE) + { + throw new IllegalStateException("Auditing requires a read-write transaction."); + } // Get the application AuditApplication application = auditModelRegistry.getAuditApplication(applicationName); if (application == null) @@ -774,8 +796,10 @@ public class AuditComponentImpl implements AuditComponent { logger.debug("No audit application named '" + applicationName + "' has been registered."); } - return NO_AUDIT_SESSION; + return null; } + // Check the path against the application + application.checkPath(rootPath); // Get the model ID for the application Long modelId = auditModelRegistry.getAuditModelId(applicationName); if (modelId == null) @@ -783,23 +807,24 @@ public class AuditComponentImpl implements AuditComponent throw new AuditException("No model exists for audit application: " + applicationName); } - // Validate root path against application - String appRootKey = application.getApplicationKey() + AuditApplication.AUDIT_PATH_SEPARATOR; - if (rootPath == null || !rootPath.startsWith(appRootKey)) - { - throw new AuditException( - "An audit session's root path must start with the application's root key.\n" + - " Application: " + application.getApplicationName() + "\n" + - " Root key: " + application.getApplicationKey() + "\n" + - " Given root: " + rootPath); - } - - // TODO: Pull out session properties and persist - // Now create the session Long sessionId = auditDAO.createAuditSession(modelId, applicationName); - // Create the session info and store it on the transaction AuditSession session = new AuditSession(application, rootPath, sessionId); + + // Generate session data + Map generators = application.getDataGenerators(rootPath, DataGeneratorScope.SESSION); + Map sessionData = generateData(generators); + + // Extract data from the values passed in + Map extractedData = extractData(application, values); + + // Combine the values + Map allData = new HashMap(sessionData); + allData.putAll(extractedData); + + // Audit it + audit(session, allData); + // Done if (logger.isDebugEnabled()) { @@ -808,13 +833,159 @@ public class AuditComponentImpl implements AuditComponent return session; } - public void audit(AuditSession session, Map values) + /** + * {@inheritDoc} + * @since 3.2 + */ + public void audit(AuditSession session, Map values) { - if (session == NO_AUDIT_SESSION) + ParameterCheck.mandatory("session", session); + ParameterCheck.mandatory("values", values); + + if (AlfrescoTransactionSupport.getTransactionReadState() != TxnReadState.TXN_READ_WRITE) { - // For some reason, the session was not to be used + throw new IllegalStateException("Auditing requires a read-write transaction."); + } + + // Audit nothing if there are no values (otherwise we're just creating maps for nothing) + if (values.size() == 0) + { + if (logger.isDebugEnabled()) + { + logger.debug("Nothing audited because there are no audit values."); + } return; } - throw new UnsupportedOperationException(); + + Long sessionId = session.getSessionId(); + long time = System.currentTimeMillis(); + String username = AuthenticationUtil.getFullyAuthenticatedUser(); + + Long entryId = auditDAO.createAuditEntry(sessionId, time, username, values); + + // Done + if (logger.isDebugEnabled()) + { + logger.debug("New audit entry: " + entryId); + } + } + + /** + * Extracts data from a given map using data extractors from the given application. + * + * @param application the application providing the data extractors + * @param values the data values from which to generate data + * @return Returns a map of derived data keyed by full path + * + * @since 3.2 + */ + private Map extractData( + AuditApplication application, + Map values) + { + Map newData = new HashMap(values.size() + 5); + for (Map.Entry entry : values.entrySet()) + { + String path = entry.getKey(); + Serializable value = entry.getValue(); + // Get the applicable extractor + Map extractors = application.getDataExtractors(path); + for (Map.Entry extractorElement : extractors.entrySet()) + { + String extractorPath = extractorElement.getKey(); + DataExtractor extractor = extractorElement.getValue(); + // Check if the extraction is supported + if (!extractor.isSupported(value)) + { + continue; + } + // Use the extractor to pull the value out + final Serializable data; + try + { + data = extractor.convert(value); + } + catch (Throwable e) + { + Log extractorLogger = LogFactory.getLog(extractor.getClass()); + if (extractorLogger.isDebugEnabled()) + { + extractorLogger.debug( + "Failed to extract audit data: \n" + + " Path: " + path + "\n" + + " Raw value: " + value + "\n" + + " Extractor: " + extractor, + e); + } + else + { + extractorLogger.warn( + "Failed to extract audit data (turn on DEBUG for full stack): \n" + + " Path: " + path + "\n" + + " Raw value: " + value + "\n" + + " Extractor: " + extractor + "\n" + + " Error: " + e.getMessage()); + } + continue; + } + // Add it to the map + newData.put(extractorPath, data); + } + } + // Done + if (logger.isDebugEnabled()) + { + logger.debug("Extracted audit data: \n" + + " Application: " + application + "\n" + + " Raw values: " + values + "\n" + + " Extracted: " + newData); + } + return newData; + } + + /** + * @param generators the data generators + * @return Returns a map of generated data keyed by full path + * + * @since 3.2 + */ + private Map generateData(Map generators) + { + Map newData = new HashMap(generators.size() + 5); + for (Map.Entry entry : generators.entrySet()) + { + String path = entry.getKey(); + DataGenerator generator = entry.getValue(); + final Serializable data; + try + { + data = generator.getData(); + } + catch (Throwable e) + { + Log generatorLogger = LogFactory.getLog(generator.getClass()); + if (generatorLogger.isDebugEnabled()) + { + generatorLogger.debug( + "Failed to generate audit data: \n" + + " Path: " + path + "\n" + + " Generator: " + generator, + e); + } + else + { + generatorLogger.warn( + "Failed to generate audit data (turn on DEBUG for full stack): \n" + + " Path: " + path + "\n" + + " Generator: " + generator + "\n" + + " Error: " + e.getMessage()); + } + continue; + } + // Add it to the map + newData.put(path, data); + } + // Done + return newData; } } diff --git a/source/java/org/alfresco/repo/audit/AuditComponentTest.java b/source/java/org/alfresco/repo/audit/AuditComponentTest.java new file mode 100644 index 0000000000..8b2f8a6443 --- /dev/null +++ b/source/java/org/alfresco/repo/audit/AuditComponentTest.java @@ -0,0 +1,155 @@ +/* + * 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.audit; + +import java.io.Serializable; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; + +import junit.framework.TestCase; + +import org.alfresco.repo.audit.model.AuditModelException; +import org.alfresco.repo.audit.model.AuditModelRegistry; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.ApplicationContextHelper; +import org.springframework.context.ApplicationContext; +import org.springframework.util.ResourceUtils; + +/** + * Tests component-level auditing i.e. audit sessions and audit logging. + * + * @see AuditComponent + * @see AuditComponentImpl + * + * @author Derek Hulley + * @since 3.2 + */ +public class AuditComponentTest extends TestCase +{ + private static final String APPLICATION_TEST = "Alfresco Test"; + + private static ApplicationContext ctx = ApplicationContextHelper.getApplicationContext(); + + private AuditModelRegistry auditModelRegistry; + private AuditComponent auditComponent; + private TransactionService transactionService; + + @Override + public void setUp() throws Exception + { + auditModelRegistry = (AuditModelRegistry) ctx.getBean("auditModel.registry"); + auditComponent = (AuditComponent) ctx.getBean("auditComponent"); + transactionService = (TransactionService) ctx.getBean("transactionService"); + + // Register the test model + URL testModelUrl = ResourceUtils.getURL("classpath:alfresco/audit/alfresco-audit-test.xml"); + auditModelRegistry.registerModel(testModelUrl); + auditModelRegistry.loadAuditModels(); + } + + public void testSetUp() + { + // Just here to fail if the basic startup fails + } + + public void testStartSessionWithBadPath() throws Exception + { + try + { + auditComponent.startAuditSession(APPLICATION_TEST, "test"); + fail("Should fail due to lack of a transaction."); + } + catch (IllegalStateException e) + { + // Expected + } + RetryingTransactionCallback testCallback = new RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + try + { + auditComponent.startAuditSession(APPLICATION_TEST, "test"); + fail("Failed to detect illegal path"); + } + catch (AuditModelException e) + { + // Expected + } + try + { + auditComponent.startAuditSession(APPLICATION_TEST, "/test/"); + fail("Failed to detect illegal path"); + } + catch (AuditModelException e) + { + // Expected + } + AuditSession session; + session = auditComponent.startAuditSession("Bogus App", "/test"); + assertNull("Invalid app should return null session.", session); + session = auditComponent.startAuditSession(APPLICATION_TEST, "/test"); + assertNotNull("Valid app and root path failed to create session.", session); + + return null; + } + }; + transactionService.getRetryingTransactionHelper().doInTransaction(testCallback); + } + + /** + * Start a session and use it within a single txn + */ + public void testBasicSession() throws Exception + { + final RetryingTransactionCallback testCallback = new RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + AuditSession session = auditComponent.startAuditSession(APPLICATION_TEST, "/test/1.1"); + + Map values = new HashMap(13); + values.put("/test/1.1/2.1/3.1/4.1", new Long(41)); + values.put("/test/1.1/2.1/3.1/4.2", "42"); + + auditComponent.audit(session, values); + + return null; + } + }; + RunAsWork testRunAs = new RunAsWork() + { + public Void doWork() throws Exception + { + return transactionService.getRetryingTransactionHelper().doInTransaction(testCallback); + } + }; + AuthenticationUtil.runAs(testRunAs, "Peanut"); + } +} diff --git a/source/java/org/alfresco/repo/audit/AuditSession.java b/source/java/org/alfresco/repo/audit/AuditSession.java index 4245c473d7..c733614740 100644 --- a/source/java/org/alfresco/repo/audit/AuditSession.java +++ b/source/java/org/alfresco/repo/audit/AuditSession.java @@ -39,16 +39,6 @@ public class AuditSession private final String rootPath; private final Long sessionId; - /** - * Constructor used to denote a dummy (no-audit) session - */ - /* package */ AuditSession() - { - application = null; - rootPath = null; - sessionId = null; - } - /** * @param application the audit application config being used * @param rootPath the root path being used for the session diff --git a/source/java/org/alfresco/repo/audit/extractor/DataExtractor.java b/source/java/org/alfresco/repo/audit/extractor/DataExtractor.java index 2d2d67ea9a..c7ae3a0a10 100644 --- a/source/java/org/alfresco/repo/audit/extractor/DataExtractor.java +++ b/source/java/org/alfresco/repo/audit/extractor/DataExtractor.java @@ -24,6 +24,8 @@ */ package org.alfresco.repo.audit.extractor; +import java.io.Serializable; + /** * Interface for Audit data value extractors. These are used to extract auditable values * from those arguments, return values, exceptions and any other value passed into the audit @@ -44,7 +46,7 @@ public interface DataExtractor * @param data the data that might be useful to this extractor (could be null) * @return Returns true if the data is meaningful to this extractor */ - public boolean isSupported(Object data); + public boolean isSupported(Serializable data); /** * Convert an value passed into the audit components into a value to be recorded. @@ -53,5 +55,5 @@ public interface DataExtractor * @return the (potentially) converted value * @throws Throwable All errors will be handled by the calling framework */ - public Object convert(Object value) throws Throwable; + public Serializable convert(Serializable value) throws Throwable; } diff --git a/source/java/org/alfresco/repo/audit/extractor/SimpleValueDataExtractor.java b/source/java/org/alfresco/repo/audit/extractor/SimpleValueDataExtractor.java index 2dad029e5f..820b1cad4f 100644 --- a/source/java/org/alfresco/repo/audit/extractor/SimpleValueDataExtractor.java +++ b/source/java/org/alfresco/repo/audit/extractor/SimpleValueDataExtractor.java @@ -24,6 +24,8 @@ */ package org.alfresco.repo.audit.extractor; +import java.io.Serializable; + /** * An extractor that supports all values and does not conversion. * This implementation can be used as a base class, although there is little @@ -37,7 +39,7 @@ public class SimpleValueDataExtractor extends AbstractDataExtractor /** * @return Returns true always */ - public boolean isSupported(Object data) + public boolean isSupported(Serializable data) { return true; } @@ -45,7 +47,7 @@ public class SimpleValueDataExtractor extends AbstractDataExtractor /** * Just returns the value unchanged */ - public Object convert(Object in) throws Throwable + public Serializable convert(Serializable in) throws Throwable { return in; } diff --git a/source/java/org/alfresco/repo/audit/generator/AuthenticatedUserDataGenerator.java b/source/java/org/alfresco/repo/audit/generator/AuthenticatedUserDataGenerator.java index 4e94ec1d67..6e337a1fb9 100644 --- a/source/java/org/alfresco/repo/audit/generator/AuthenticatedUserDataGenerator.java +++ b/source/java/org/alfresco/repo/audit/generator/AuthenticatedUserDataGenerator.java @@ -24,6 +24,8 @@ */ package org.alfresco.repo.audit.generator; +import java.io.Serializable; + import org.alfresco.repo.security.authentication.AuthenticationUtil; /** @@ -37,7 +39,7 @@ public class AuthenticatedUserDataGenerator extends AbstractDataGenerator /** * @return Returns the currently-authenticated user */ - public Object getData() throws Throwable + public Serializable getData() throws Throwable { return AuthenticationUtil.getFullyAuthenticatedUser(); } diff --git a/source/java/org/alfresco/repo/audit/generator/DataGenerator.java b/source/java/org/alfresco/repo/audit/generator/DataGenerator.java index 7a14bb0fa4..9a35bc6b29 100644 --- a/source/java/org/alfresco/repo/audit/generator/DataGenerator.java +++ b/source/java/org/alfresco/repo/audit/generator/DataGenerator.java @@ -24,6 +24,8 @@ */ package org.alfresco.repo.audit.generator; +import java.io.Serializable; + /** * Interface for Audit data value generators.These are used to produce auditable data values * extract auditable values from nothing; typically these values are derived from the system @@ -52,5 +54,5 @@ public interface DataGenerator * @return Returns the generated data * @throws Throwable All exceptions are handled by the framework */ - public Object getData() throws Throwable; + public Serializable getData() throws Throwable; } diff --git a/source/java/org/alfresco/repo/audit/generator/SystemTimeDataGenerator.java b/source/java/org/alfresco/repo/audit/generator/SystemTimeDataGenerator.java index 6815b2cfa0..8efc81f3ff 100644 --- a/source/java/org/alfresco/repo/audit/generator/SystemTimeDataGenerator.java +++ b/source/java/org/alfresco/repo/audit/generator/SystemTimeDataGenerator.java @@ -24,6 +24,7 @@ */ package org.alfresco.repo.audit.generator; +import java.io.Serializable; import java.util.Date; /** @@ -37,7 +38,7 @@ public class SystemTimeDataGenerator extends AbstractDataGenerator /** * @return Returns the current time */ - public Object getData() throws Throwable + public Serializable getData() throws Throwable { return new Date(); } diff --git a/source/java/org/alfresco/repo/audit/generator/TransactionIdDataGenerator.java b/source/java/org/alfresco/repo/audit/generator/TransactionIdDataGenerator.java index 6ce1eac89a..d3faba66c1 100644 --- a/source/java/org/alfresco/repo/audit/generator/TransactionIdDataGenerator.java +++ b/source/java/org/alfresco/repo/audit/generator/TransactionIdDataGenerator.java @@ -24,6 +24,8 @@ */ package org.alfresco.repo.audit.generator; +import java.io.Serializable; + import org.alfresco.repo.transaction.AlfrescoTransactionSupport; /** @@ -37,7 +39,7 @@ public class TransactionIdDataGenerator extends AbstractDataGenerator /** * @return Returns the current transaction ID (null if not in a transction) */ - public Object getData() throws Throwable + public Serializable getData() throws Throwable { return AlfrescoTransactionSupport.getTransactionId(); } diff --git a/source/java/org/alfresco/repo/audit/hibernate/HibernateAuditDAO.java b/source/java/org/alfresco/repo/audit/hibernate/HibernateAuditDAO.java index 07fdc56d75..f32f48dd7a 100644 --- a/source/java/org/alfresco/repo/audit/hibernate/HibernateAuditDAO.java +++ b/source/java/org/alfresco/repo/audit/hibernate/HibernateAuditDAO.java @@ -38,6 +38,7 @@ import java.util.GregorianCalendar; import java.util.HashMap; import java.util.Iterator; import java.util.List; +import java.util.Map; import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.repo.audit.AuditComponentImpl; @@ -161,6 +162,17 @@ public class HibernateAuditDAO extends HibernateDaoSupport implements AuditDAO, throw new UnsupportedOperationException(); } + /** + * Fallout implementation from new audit DAO + * + * @throws UnsupportedOperationException always + * @since 3.2 + */ + public Long createAuditEntry(Long sessionId, long time, String username, Map values) + { + throw new UnsupportedOperationException(); + } + public void audit(AuditState auditInfo) { if (auditInfo.getUserIdentifier() == null) diff --git a/source/java/org/alfresco/repo/audit/model/AuditApplication.java b/source/java/org/alfresco/repo/audit/model/AuditApplication.java index 5b7a5376c8..a2040215df 100644 --- a/source/java/org/alfresco/repo/audit/model/AuditApplication.java +++ b/source/java/org/alfresco/repo/audit/model/AuditApplication.java @@ -179,6 +179,25 @@ public class AuditApplication } } + /** + * Compile a path or part of a path into a single string which always starts with the + * {@link #AUDIT_PATH_SEPARATOR}. This can be a relative path so need not always start with + * the application root key. + * + * @param pathElements the elements of the path e.g. "a", "b", "c". + * @return Returns the compiled path e.g "/a/b/c". + */ + public String buildPath(String ... pathComponents) + { + StringBuilder sb = new StringBuilder(pathComponents.length * 10); + for (String pathComponent : pathComponents) + { + sb.append(AUDIT_PATH_SEPARATOR).append(pathComponent); + } + // Done + return sb.toString(); + } + /** * Get all data extractors applicable to a given path and scope. * @@ -325,6 +344,11 @@ public class AuditApplication } // All the extractors apply to the current path dataExtractors.put(currentPath, upperExtractorsByPath); +// // Data extractors only apply directly to data in which they appear. +// // TODO: Examine this assumption. If it is not true, i.e. data extractors apply to +// // data anywhere down the hierarchy, then the followin line of code should be +// // removed and the use-cases tested appropriately. +// upperExtractorsByPath.clear(); // Get the data generators declared for this key for (GenerateValue element : auditPath.getGenerateValue()) diff --git a/source/java/org/alfresco/repo/audit/model/AuditModelRegistry.java b/source/java/org/alfresco/repo/audit/model/AuditModelRegistry.java index d671a6a075..7a0e9763d7 100644 --- a/source/java/org/alfresco/repo/audit/model/AuditModelRegistry.java +++ b/source/java/org/alfresco/repo/audit/model/AuditModelRegistry.java @@ -73,14 +73,17 @@ import org.xml.sax.SAXParseException; public class AuditModelRegistry { public static final String AUDIT_SCHEMA_LOCATION = "classpath:alfresco/audit/alfresco-audit-3.2.xsd"; + public static final String AUDIT_RESERVED_KEY_USERNAME = "username"; + public static final String AUDIT_RESERVED_KEY_SYSTEMTIME = "systemTime"; + + private static final Log logger = LogFactory.getLog(AuditModelRegistry.class); private TransactionService transactionService; private AuditDAO auditDAO; - private static final Log logger = LogFactory.getLog(AuditModelRegistry.class); - private final ReentrantReadWriteLock.ReadLock readLock; private final ReentrantReadWriteLock.WriteLock writeLock; + private final ObjectFactory objectFactory; private final Set auditModelUrls; private final List auditModels; @@ -104,6 +107,8 @@ public class AuditModelRegistry readLock = lock.readLock(); writeLock = lock.writeLock(); + objectFactory = new ObjectFactory(); + auditModelUrls = new HashSet(7); auditModels = new ArrayList(7); dataExtractorsByName = new HashMap(13); @@ -239,15 +244,15 @@ public class AuditModelRegistry /** * Get the ID of the persisted audit model for the given application name * - * @param application the name of the audited application + * @param applicationName the name of the audited application * @return the unique ID of the persisted model (null if not found) */ - public Long getAuditModelId(String application) + public Long getAuditModelId(String applicationName) { readLock.lock(); try { - return auditModelIdsByApplicationsName.get(application); + return auditModelIdsByApplicationsName.get(applicationName); } finally { @@ -258,15 +263,15 @@ public class AuditModelRegistry /** * Get the application model for the given application name * - * @param application the name of the audited application + * @param applicationName the name of the audited application * @return the java model (null if not found) */ - public AuditApplication getAuditApplication(String application) + public AuditApplication getAuditApplication(String applicationName) { readLock.lock(); try { - return auditApplicationsByName.get(application); + return auditApplicationsByName.get(applicationName); } finally { @@ -364,7 +369,7 @@ public class AuditModelRegistry DataExtractors extractorsElement = audit.getDataExtractors(); if (extractorsElement == null) { - extractorsElement = new ObjectFactory().createDataExtractors(); + extractorsElement = objectFactory.createDataExtractors(); } List converterElements = extractorsElement.getDataExtractor(); for (org.alfresco.repo.audit.model._3.DataExtractor converterElement : converterElements) @@ -404,7 +409,7 @@ public class AuditModelRegistry DataGenerators generatorsElement = audit.getDataGenerators(); if (generatorsElement == null) { - generatorsElement = new ObjectFactory().createDataGenerators(); + generatorsElement = objectFactory.createDataGenerators(); } List generatorElements = generatorsElement.getDataGenerator(); for (org.alfresco.repo.audit.model._3.DataGenerator generatorElement : generatorElements) diff --git a/source/java/org/alfresco/repo/domain/DomainTestSuite.java b/source/java/org/alfresco/repo/domain/DomainTestSuite.java index 314110adce..d009a7f73c 100644 --- a/source/java/org/alfresco/repo/domain/DomainTestSuite.java +++ b/source/java/org/alfresco/repo/domain/DomainTestSuite.java @@ -27,6 +27,7 @@ package org.alfresco.repo.domain; import junit.framework.Test; import junit.framework.TestSuite; +import org.alfresco.repo.domain.audit.AuditDAOTest; import org.alfresco.repo.domain.contentdata.ContentDataDAOTest; import org.alfresco.repo.domain.encoding.EncodingDAOTest; import org.alfresco.repo.domain.hibernate.HibernateSessionHelperTest; @@ -53,6 +54,7 @@ public class DomainTestSuite extends TestSuite suite.addTestSuite(PropertyValueTest.class); suite.addTestSuite(QNameDAOTest.class); suite.addTestSuite(PropertyValueTest.class); + suite.addTestSuite(AuditDAOTest.class); return suite; } diff --git a/source/java/org/alfresco/repo/domain/audit/AbstractAuditDAOImpl.java b/source/java/org/alfresco/repo/domain/audit/AbstractAuditDAOImpl.java index 7a16d61d02..98ffaedbec 100644 --- a/source/java/org/alfresco/repo/domain/audit/AbstractAuditDAOImpl.java +++ b/source/java/org/alfresco/repo/domain/audit/AbstractAuditDAOImpl.java @@ -26,8 +26,11 @@ package org.alfresco.repo.domain.audit; import java.io.IOException; import java.io.InputStream; +import java.io.Serializable; import java.net.URL; +import java.util.Date; import java.util.List; +import java.util.Map; import java.util.zip.CRC32; import org.alfresco.error.AlfrescoRuntimeException; @@ -193,7 +196,7 @@ public abstract class AbstractAuditDAOImpl implements AuditDAO protected abstract AuditModelEntity createAuditModel(Long contentDataId, long crc); /* - * alf_audit_model + * alf_audit_session */ public Long createAuditSession(Long modelId, String application) @@ -203,8 +206,52 @@ public abstract class AbstractAuditDAOImpl implements AuditDAO // Create the audit session AuditSessionEntity entity = createAuditSession(appNameId, modelId); // Done + if (logger.isDebugEnabled()) + { + logger.debug( + "Created new audit session: \n" + + " Model: " + modelId + "\n" + + " App: " + application + "\n" + + " Result: " + entity); + } return entity.getId(); } protected abstract AuditSessionEntity createAuditSession(Long appNameId, Long modelId); + + /* + * alf_audit_entry + */ + + public Long createAuditEntry(Long sessionId, long time, String username, Map values) + { + final Long usernameId; + if (username != null) + { + usernameId = propertyValueDAO.getOrCreatePropertyValue(username).getFirst(); + } + else + { + usernameId = null; + } + // Now persist the data values + final Long valuesId = propertyValueDAO.getOrCreatePropertyValue((Serializable)values).getFirst(); + + // Create the audit entry + AuditEntryEntity entity = createAuditEntry(sessionId, time, usernameId, valuesId); + + // Done + if (logger.isDebugEnabled()) + { + logger.debug( + "Created new audit entry: \n" + + " Session: " + sessionId + "\n" + + " Time: " + (new Date(time)) + "\n" + + " User: " + username + "\n" + + " Result: " + entity); + } + return entity.getId(); + } + + protected abstract AuditEntryEntity createAuditEntry(Long sessionId, long time, Long usernameId, Long valuesId); } diff --git a/source/java/org/alfresco/repo/domain/audit/AuditDAO.java b/source/java/org/alfresco/repo/domain/audit/AuditDAO.java index f7634e9374..aa2d923095 100644 --- a/source/java/org/alfresco/repo/domain/audit/AuditDAO.java +++ b/source/java/org/alfresco/repo/domain/audit/AuditDAO.java @@ -24,8 +24,10 @@ */ package org.alfresco.repo.domain.audit; +import java.io.Serializable; import java.net.URL; import java.util.List; +import java.util.Map; import org.alfresco.repo.audit.AuditState; import org.alfresco.service.cmr.audit.AuditInfo; @@ -75,9 +77,20 @@ public interface AuditDAO /** * Creates a new audit session entry - there is no session re-use. * - * @param modelId a pre-existing model's ID + * @param modelId an existing audit model ID * @param application the name of the application * @return Returns the unique session ID */ Long createAuditSession(Long modelId, String application); + + /** + * Create a new audit entry with the given map of values. + * + * @param sessionId an existing audit session ID + * @param time the time (ms since epoch) to log the entry against + * @param username the authenticated user (null if not present) + * @param values the values to record + * @return Returns the unique entry ID + */ + Long createAuditEntry(Long sessionId, long time, String username, Map values); } diff --git a/source/java/org/alfresco/repo/domain/audit/AuditDAOTest.java b/source/java/org/alfresco/repo/domain/audit/AuditDAOTest.java index beb351aa7b..bd23769c3d 100644 --- a/source/java/org/alfresco/repo/domain/audit/AuditDAOTest.java +++ b/source/java/org/alfresco/repo/domain/audit/AuditDAOTest.java @@ -25,7 +25,10 @@ package org.alfresco.repo.domain.audit; import java.io.File; +import java.io.Serializable; import java.net.URL; +import java.util.Collections; +import java.util.Map; import junit.framework.TestCase; @@ -119,4 +122,44 @@ public class AuditDAOTest extends TestCase "Time for " + count + " session creations was " + ((double)(after - before)/(10E6)) + "ms"); } + + public void testAuditEntry() throws Exception + { + final File file = AbstractContentTransformerTest.loadQuickTestFile("pdf"); + assertNotNull(file); + final URL url = new URL("file:" + file.getAbsolutePath()); + final String appName = getName() + "." + System.currentTimeMillis(); + + RetryingTransactionCallback createSessionCallback = new RetryingTransactionCallback() + { + public Long execute() throws Throwable + { + Long modelId = auditDAO.getOrCreateAuditModel(url).getFirst(); + return auditDAO.createAuditSession(modelId, appName); + } + }; + final Long sessionId = txnHelper.doInTransaction(createSessionCallback); + + final int count = 1000; + final String username = "alexi"; + RetryingTransactionCallback createEntryCallback = new RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + for (int i = 0; i < count; i++) + { + Map values = Collections.singletonMap("/a/b/c", (Serializable) new Integer(i)); + long now = System.currentTimeMillis(); + auditDAO.createAuditEntry(sessionId, now, username, values); + } + return null; + } + }; + long before = System.nanoTime(); + txnHelper.doInTransaction(createEntryCallback); + long after = System.nanoTime(); + System.out.println( + "Time for " + count + " entry creations was " + + ((double)(after - before)/(10E6)) + "ms"); + } } diff --git a/source/java/org/alfresco/repo/domain/audit/AuditEntryEntity.java b/source/java/org/alfresco/repo/domain/audit/AuditEntryEntity.java new file mode 100644 index 0000000000..f289d1758a --- /dev/null +++ b/source/java/org/alfresco/repo/domain/audit/AuditEntryEntity.java @@ -0,0 +1,109 @@ +/* + * 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.audit; + +import java.util.Date; + +/** + * Entity bean for alf_audit_entry table. + * + * @author Derek Hulley + * @since 3.2 + */ +public class AuditEntryEntity +{ + private Long id; + private Long auditSessionId; + private Long auditUserId; + private long auditTime; + private Long auditValuesId; + + public AuditEntryEntity() + { + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(512); + sb.append("AuditEntryEntity") + .append("[ ID=").append(id) + .append(", auditSessionId=").append(auditSessionId) + .append(", auditTime").append(new Date(auditTime)) + .append(", auditValuesId=").append(auditValuesId) + .append("]"); + return sb.toString(); + } + + public Long getId() + { + return id; + } + + public void setId(Long id) + { + this.id = id; + } + + public Long getAuditSessionId() + { + return auditSessionId; + } + + public void setAuditSessionId(Long auditSessionId) + { + this.auditSessionId = auditSessionId; + } + + public Long getAuditUserId() + { + return auditUserId; + } + + public void setAuditUserId(Long auditUserId) + { + this.auditUserId = auditUserId; + } + + public long getAuditTime() + { + return auditTime; + } + + public void setAuditTime(long auditTime) + { + this.auditTime = auditTime; + } + + public Long getAuditValuesId() + { + return auditValuesId; + } + + public void setAuditValuesId(Long auditValuesId) + { + this.auditValuesId = auditValuesId; + } +} 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 a420bf9bb8..c293eedac7 100644 --- a/source/java/org/alfresco/repo/domain/audit/ibatis/AuditDAOImpl.java +++ b/source/java/org/alfresco/repo/domain/audit/ibatis/AuditDAOImpl.java @@ -25,6 +25,7 @@ package org.alfresco.repo.domain.audit.ibatis; import org.alfresco.repo.domain.audit.AbstractAuditDAOImpl; +import org.alfresco.repo.domain.audit.AuditEntryEntity; import org.alfresco.repo.domain.audit.AuditModelEntity; import org.alfresco.repo.domain.audit.AuditSessionEntity; import org.springframework.orm.ibatis.SqlMapClientTemplate; @@ -42,6 +43,8 @@ public class AuditDAOImpl extends AbstractAuditDAOImpl private static final String INSERT_SESSION = "insert.AuditSession"; + private static final String INSERT_ENTRY = "insert.AuditEntry"; + private SqlMapClientTemplate template; public void setSqlMapClientTemplate(SqlMapClientTemplate sqlMapClientTemplate) @@ -82,4 +85,17 @@ public class AuditDAOImpl extends AbstractAuditDAOImpl entity.setId(id); return entity; } + + @Override + protected AuditEntryEntity createAuditEntry(Long sessionId, long time, Long usernameId, Long valuesId) + { + AuditEntryEntity entity = new AuditEntryEntity(); + entity.setAuditSessionId(sessionId); + entity.setAuditTime(time); + entity.setAuditUserId(usernameId); + entity.setAuditValuesId(valuesId); + Long id = (Long) template.insert(INSERT_ENTRY, entity); + entity.setId(id); + return entity; + } } diff --git a/source/java/org/alfresco/repo/domain/propval/PropertyStringValueEntity.java b/source/java/org/alfresco/repo/domain/propval/PropertyStringValueEntity.java index a96b49d3ab..5403625866 100644 --- a/source/java/org/alfresco/repo/domain/propval/PropertyStringValueEntity.java +++ b/source/java/org/alfresco/repo/domain/propval/PropertyStringValueEntity.java @@ -24,6 +24,7 @@ */ package org.alfresco.repo.domain.propval; +import org.alfresco.repo.domain.CrcHelper; import org.alfresco.util.EqualsHelper; import org.alfresco.util.Pair; @@ -37,7 +38,8 @@ public class PropertyStringValueEntity { private Long id; private String stringValue; - private String stringEnd; + private String stringEndLower; + private Long stringCrc; public PropertyStringValueEntity() { @@ -88,16 +90,15 @@ public class PropertyStringValueEntity */ public void setValue(String value) { - this.stringValue = value; - int len = stringValue.length(); - if (len > 16) + if (value == null) { - stringEnd = stringValue.substring(len - 16); - } - else - { - stringEnd = stringValue; + throw new IllegalArgumentException("Null strings cannot be persisted"); } + stringValue = value; + // Calculate the crc value from the original value + Pair crcPair = CrcHelper.getStringCrcPair(value, 16, false, true); + stringEndLower = crcPair.getFirst(); + stringCrc = crcPair.getSecond(); } public Long getId() @@ -120,13 +121,23 @@ public class PropertyStringValueEntity this.stringValue = stringValue; } - public String getStringEnd() + public String getStringEndLower() { - return stringEnd; + return stringEndLower; } - public void setStringEnd(String stringEnd) + public void setStringEndLower(String stringEndLower) { - this.stringEnd = stringEnd; + this.stringEndLower = stringEndLower; + } + + public Long getStringCrc() + { + return stringCrc; + } + + public void setStringCrc(Long stringCrc) + { + this.stringCrc = stringCrc; } } diff --git a/source/java/org/alfresco/repo/domain/propval/PropertyValueDAOTest.java b/source/java/org/alfresco/repo/domain/propval/PropertyValueDAOTest.java index 52aa84beed..1c3cca1d72 100644 --- a/source/java/org/alfresco/repo/domain/propval/PropertyValueDAOTest.java +++ b/source/java/org/alfresco/repo/domain/propval/PropertyValueDAOTest.java @@ -187,7 +187,7 @@ public class PropertyValueDAOTest extends TestCase assertEquals(stringValue, stringEntityPair.getSecond()); // Check that the uppercase and lowercase strings don't have entries - RetryingTransactionCallback getClassCallback = new RetryingTransactionCallback() + RetryingTransactionCallback getStringCallback = new RetryingTransactionCallback() { public Void execute() throws Throwable { @@ -201,7 +201,7 @@ public class PropertyValueDAOTest extends TestCase return null; } }; - txnHelper.doInTransaction(getClassCallback, true); + txnHelper.doInTransaction(getStringCallback, true); RetryingTransactionCallback> createStringUpperCallback = new RetryingTransactionCallback>() { @@ -216,6 +216,22 @@ public class PropertyValueDAOTest extends TestCase assertNotNull(stringUpperEntityPair.getFirst()); assertEquals(stringValueUpper, stringUpperEntityPair.getSecond()); assertNotSame("String IDs were not different", stringEntityPair.getFirst(), stringUpperEntityPair.getFirst()); + + // Check empty string + RetryingTransactionCallback emptyStringCallback = new RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + Pair emptyStringPair1 = propertyValueDAO.getOrCreatePropertyStringValue(""); + assertNotNull(emptyStringPair1); + assertEquals("", emptyStringPair1.getSecond()); + Pair emptyStringPair2 = propertyValueDAO.getOrCreatePropertyStringValue(""); + assertNotNull(emptyStringPair2); + assertEquals(emptyStringPair1, emptyStringPair2); + return null; + } + }; + txnHelper.doInTransaction(emptyStringCallback, true); } public void testPropertyDoubleValue() throws Exception 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 1f287e8a1b..1233f2f833 100644 --- a/source/java/org/alfresco/repo/domain/propval/ibatis/PropertyValueDAOImpl.java +++ b/source/java/org/alfresco/repo/domain/propval/ibatis/PropertyValueDAOImpl.java @@ -168,27 +168,17 @@ public class PropertyValueDAOImpl extends AbstractPropertyValueDAOImpl return value; } - @SuppressWarnings("unchecked") @Override protected Long findStringValueByValue(String value) { PropertyStringValueEntity entity = new PropertyStringValueEntity(); entity.setValue(value); - List results = (List) template.queryForList( + Long id = (Long) template.queryForObject( SELECT_PROPERTY_STRING_VALUE_BY_VALUE, entity); - // There could be several matches (if the database is case-insensitive), so find the first - // value that matches exactly. - for (PropertyStringValueEntity resultEntity : results) - { - if (value.equals(resultEntity.getStringValue())) - { - // Found a match - return resultEntity.getId(); - } - } - // No real match - return null; + // The CRC match prevents incorrect results from coming back. Although there could be + // several matches, we are sure that the matches are case-sensitive. + return id; } @Override diff --git a/source/test-resources/alfresco/audit/alfresco-audit-test.xml b/source/test-resources/alfresco/audit/alfresco-audit-test.xml index b6f3364f7f..bb65363dec 100644 --- a/source/test-resources/alfresco/audit/alfresco-audit-test.xml +++ b/source/test-resources/alfresco/audit/alfresco-audit-test.xml @@ -28,7 +28,9 @@ + + @@ -47,7 +49,7 @@ - +