Audit config, XSD and write-persistence tests

- Audit paths can now use mixed case (after alf_prop_string_value enhancements)
 - Pluggable data conversion when pushing values into persistence
 - Relaxed XSD to allow mixed-case key values
 - Regex checking of paths and names when building strings


git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@15976 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261
This commit is contained in:
Derek Hulley
2009-08-27 16:16:05 +00:00
parent 19d232f649
commit f0cd5ef0d8
19 changed files with 427 additions and 139 deletions

View File

@@ -104,7 +104,7 @@
<xs:restriction base="xs:string"> <xs:restriction base="xs:string">
<xs:minLength value="1"/> <xs:minLength value="1"/>
<xs:maxLength value="128"/> <xs:maxLength value="128"/>
<xs:pattern value="([a-z]|[0-9]|\-|\.)*"/> <xs:pattern value="([a-z]|[A-Z]|[0-9]|\-|\.)*"/>
</xs:restriction> </xs:restriction>
</xs:simpleType> </xs:simpleType>

View File

@@ -33,7 +33,7 @@ CREATE TABLE alf_audit_entry
audit_session_id BIGINT NOT NULL, audit_session_id BIGINT NOT NULL,
audit_time BIGINT NOT NULL, audit_time BIGINT NOT NULL,
audit_user_id BIGINT NULL, audit_user_id BIGINT NULL,
audit_values_id BIGINT NOT NULL, audit_values_id BIGINT NULL,
CONSTRAINT fk_alf_audit_ent_sess FOREIGN KEY (audit_session_id) REFERENCES alf_audit_session (id), CONSTRAINT fk_alf_audit_ent_sess FOREIGN KEY (audit_session_id) REFERENCES alf_audit_session (id),
INDEX idx_alf_audit_ent_time (audit_time), 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_user FOREIGN KEY (audit_user_id) REFERENCES alf_prop_value (id),

View File

@@ -36,6 +36,8 @@ import org.alfresco.repo.audit.model.AuditApplication;
import org.alfresco.repo.audit.model.AuditModelException; import org.alfresco.repo.audit.model.AuditModelException;
import org.alfresco.repo.audit.model.AuditModelRegistry; import org.alfresco.repo.audit.model.AuditModelRegistry;
import org.alfresco.util.ApplicationContextHelper; import org.alfresco.util.ApplicationContextHelper;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.util.ResourceUtils; import org.springframework.util.ResourceUtils;
@@ -52,6 +54,7 @@ public class AuditBootstrapTest extends TestCase
private static final String APPLICATION_TEST = "Alfresco Test"; private static final String APPLICATION_TEST = "Alfresco Test";
private static ApplicationContext ctx = ApplicationContextHelper.getApplicationContext(); private static ApplicationContext ctx = ApplicationContextHelper.getApplicationContext();
private static final Log logger = LogFactory.getLog(AuditBootstrapTest.class);
private AuditModelRegistry auditModelRegistry; private AuditModelRegistry auditModelRegistry;
@@ -83,6 +86,7 @@ public class AuditBootstrapTest extends TestCase
catch (AuditModelException e) catch (AuditModelException e)
{ {
// Expected // Expected
logger.error("Expected AuditModelException: " + e.getMessage());
} }
} }
@@ -111,6 +115,11 @@ public class AuditBootstrapTest extends TestCase
loadBadModel("classpath:alfresco/audit/alfresco-audit-test-bad-05.xml"); loadBadModel("classpath:alfresco/audit/alfresco-audit-test-bad-05.xml");
} }
public void testModelLoading_BadGeneratorRegisteredName() throws Exception
{
loadBadModel("classpath:alfresco/audit/alfresco-audit-test-bad-06.xml");
}
public void testGetModelId() public void testGetModelId()
{ {
Long repoId = auditModelRegistry.getAuditModelId(APPLICATION_TEST); Long repoId = auditModelRegistry.getAuditModelId(APPLICATION_TEST);

View File

@@ -109,18 +109,25 @@ public interface AuditComponent
AuditSession startAuditSession(String applicationName, String rootPath, Map<String, Serializable> values); AuditSession startAuditSession(String applicationName, String rootPath, Map<String, Serializable> values);
/** /**
* Record a set of values against the given session. * Record a set of values against the given session. The map is a path (starting with '/') relative
* to the root path given when {@link #startAuditSession(String, String) starting the session}. All
* resulting path values (session root path + map entry paths) must have data recorder entries and
* be enabled for data to be recorded.
* <p/>
* The return values reflect what was actually persisted and is controlled by the data extractors
* defined in the audit configuration.
* <p/> * <p/>
* This is a read-write method. Client code must wrap calls in the appropriate transactional wrappers. * 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 session a pre-existing audit session to continue with
* @param values the values to audit mapped by {@link AuditPath} key relative to the session * @param values the values to audit mapped by {@link AuditPath} key relative to the session
* root path * root path
* @return Returns the values that were actually persisted, keyed by their full path.
* @throws IllegalStateException if there is not a writable transaction present * @throws IllegalStateException if there is not a writable transaction present
* *
* @see #startAuditSession() * @see #startAuditSession()
* *
* @since 3.2 * @since 3.2
*/ */
void audit(AuditSession session, Map<String, Serializable> values); Map<String, Serializable> audit(AuditSession session, Map<String, Serializable> values);
} }

View File

@@ -837,7 +837,7 @@ public class AuditComponentImpl implements AuditComponent
* {@inheritDoc} * {@inheritDoc}
* @since 3.2 * @since 3.2
*/ */
public void audit(AuditSession session, Map<String, Serializable> values) public Map<String, Serializable> audit(AuditSession session, Map<String, Serializable> values)
{ {
ParameterCheck.mandatory("session", session); ParameterCheck.mandatory("session", session);
ParameterCheck.mandatory("values", values); ParameterCheck.mandatory("values", values);
@@ -847,27 +847,40 @@ public class AuditComponentImpl implements AuditComponent
throw new IllegalStateException("Auditing requires a read-write transaction."); throw new IllegalStateException("Auditing requires a read-write transaction.");
} }
// Audit nothing if there are no values (otherwise we're just creating maps for nothing) AuditApplication app = session.getApplication();
if (values.size() == 0) String rootPath = session.getRootPath();
Long sessionId = session.getSessionId();
// Build the key paths using the session root path
Map<String, Serializable> pathedValues = new HashMap<String, Serializable>(values.size() * 2);
for (Map.Entry<String, Serializable> entry : values.entrySet())
{ {
if (logger.isDebugEnabled()) String pathElement = entry.getKey();
{ String path = AuditApplication.buildPath(rootPath, pathElement);
logger.debug("Nothing audited because there are no audit values."); pathedValues.put(path, entry.getValue());
}
return;
} }
Long sessionId = session.getSessionId(); // Now extract values
Map<String, Serializable> extractedValues = extractData(app, pathedValues);
// Time and username are intrinsic
long time = System.currentTimeMillis(); long time = System.currentTimeMillis();
String username = AuthenticationUtil.getFullyAuthenticatedUser(); String username = AuthenticationUtil.getFullyAuthenticatedUser();
Long entryId = auditDAO.createAuditEntry(sessionId, time, username, values); // Persist the values
Long entryId = auditDAO.createAuditEntry(sessionId, time, username, pathedValues);
// Done // Done
if (logger.isDebugEnabled()) if (logger.isDebugEnabled())
{ {
logger.debug("New audit entry: " + entryId); logger.debug(
"New audit entry: \n" +
" Session ID: " + sessionId + "\n" +
" Entry ID: " + entryId + "\n" +
" Path Values: " + pathedValues + "\n" +
" Extracted Values: " + extractedValues);
} }
return extractedValues;
} }
/** /**

View File

@@ -32,13 +32,19 @@ import java.util.Map;
import junit.framework.TestCase; import junit.framework.TestCase;
import org.alfresco.repo.audit.model.AuditApplication;
import org.alfresco.repo.audit.model.AuditModelException; import org.alfresco.repo.audit.model.AuditModelException;
import org.alfresco.repo.audit.model.AuditModelRegistry; import org.alfresco.repo.audit.model.AuditModelRegistry;
import org.alfresco.repo.security.authentication.AuthenticationUtil; import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork;
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
import org.alfresco.service.ServiceRegistry;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.repository.StoreRef;
import org.alfresco.service.transaction.TransactionService; import org.alfresco.service.transaction.TransactionService;
import org.alfresco.util.ApplicationContextHelper; import org.alfresco.util.ApplicationContextHelper;
import org.alfresco.util.EqualsHelper;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.util.ResourceUtils; import org.springframework.util.ResourceUtils;
@@ -59,22 +65,37 @@ public class AuditComponentTest extends TestCase
private AuditModelRegistry auditModelRegistry; private AuditModelRegistry auditModelRegistry;
private AuditComponent auditComponent; private AuditComponent auditComponent;
private ServiceRegistry serviceRegistry;
private TransactionService transactionService; private TransactionService transactionService;
private NodeService nodeService;
private NodeRef nodeRef;
@Override @Override
public void setUp() throws Exception public void setUp() throws Exception
{ {
auditModelRegistry = (AuditModelRegistry) ctx.getBean("auditModel.modelRegistry"); auditModelRegistry = (AuditModelRegistry) ctx.getBean("auditModel.modelRegistry");
auditComponent = (AuditComponent) ctx.getBean("auditComponent"); auditComponent = (AuditComponent) ctx.getBean("auditComponent");
transactionService = (TransactionService) ctx.getBean("transactionService"); serviceRegistry = (ServiceRegistry) ctx.getBean(ServiceRegistry.SERVICE_REGISTRY);
transactionService = serviceRegistry.getTransactionService();
nodeService = serviceRegistry.getNodeService();
// Register the test model // Register the test model
URL testModelUrl = ResourceUtils.getURL("classpath:alfresco/audit/alfresco-audit-test.xml"); URL testModelUrl = ResourceUtils.getURL("classpath:alfresco/audit/alfresco-audit-test.xml");
auditModelRegistry.registerModel(testModelUrl); auditModelRegistry.registerModel(testModelUrl);
auditModelRegistry.loadAuditModels(); auditModelRegistry.loadAuditModels();
RunAsWork<NodeRef> testRunAs = new RunAsWork<NodeRef>()
{
public NodeRef doWork() throws Exception
{
return nodeService.getRootNode(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE);
}
};
nodeRef = AuthenticationUtil.runAs(testRunAs, AuthenticationUtil.getSystemUserName());
// Authenticate // Authenticate
AuthenticationUtil.setFullyAuthenticatedUser("User-" + getName() + System.currentTimeMillis()); AuthenticationUtil.setFullyAuthenticatedUser("User-" + getName());
} }
@Override @Override
@@ -164,34 +185,102 @@ public class AuditComponentTest extends TestCase
AuthenticationUtil.runAs(testRunAs, "SomeOtherUser"); AuthenticationUtil.runAs(testRunAs, "SomeOtherUser");
} }
private Map<String, Serializable> auditTestAction(
final String action,
NodeRef nodeRef,
Map<String, Serializable> parameters)
{
final Map<String, Serializable> adjustedValues = new HashMap<String, Serializable>(parameters.size() * 2);
// Add the noderef
adjustedValues.put(AuditApplication.buildPath("context-node"), nodeRef);
// Compile path-name snippets for the parameters
for (Map.Entry<String, Serializable> entry : parameters.entrySet())
{
String paramName = entry.getKey();
String path = AuditApplication.buildPath("params", paramName);
adjustedValues.put(path, entry.getValue());
}
RetryingTransactionCallback<Map<String, Serializable>> auditCallback =
new RetryingTransactionCallback<Map<String, Serializable>>()
{
public Map<String, Serializable> execute() throws Throwable
{
String actionPath = AuditApplication.buildPath("test/actions", action);
AuditSession session = auditComponent.startAuditSession(APPLICATION_TEST, actionPath);
return auditComponent.audit(session, adjustedValues);
}
};
return transactionService.getRetryingTransactionHelper().doInTransaction(auditCallback);
}
private void checkAuditMaps(Map<String, Serializable> result, Map<String, Serializable> expected)
{
Map<String, Serializable> copyResult = new HashMap<String, Serializable>(result);
boolean failure = false;
StringBuilder sb = new StringBuilder(1024);
sb.append("\nValues that don't match the expected values: ");
for (Map.Entry<String, Serializable> entry : expected.entrySet())
{
String key = entry.getKey();
Serializable expectedValue = entry.getValue();
Serializable resultValue = result.get(key);
if (!EqualsHelper.nullSafeEquals(resultValue, expectedValue))
{
sb.append("\n")
.append(" Key: ").append(key).append("\n")
.append(" Result: ").append(resultValue).append("\n")
.append(" Expected: ").append(expectedValue);
failure = true;
}
copyResult.remove(key);
}
sb.append("\nValues that are present but should not be: ");
for (Map.Entry<String, Serializable> entry : copyResult.entrySet())
{
String key = entry.getKey();
Serializable resultValue = entry.getValue();
sb.append("\n")
.append(" Key: ").append(key).append("\n")
.append(" Result: ").append(resultValue);
failure = true;
}
if (failure)
{
fail(sb.toString());
}
}
/** /**
* Start a session and use it within a single txn * Start a session and use it within a single txn
*/ */
public void testSession_Extended01() throws Exception public void testSession_Action01() throws Exception
{ {
final RetryingTransactionCallback<Void> testCallback = new RetryingTransactionCallback<Void>() Serializable valueA = new Date();
{ Serializable valueB = "BBB-value-here";
public Void execute() throws Throwable Serializable valueC = new Float(16.0F);
{ // Get a noderef
AuditSession session = auditComponent.startAuditSession(APPLICATION_TEST, "/test/1.1"); final Map<String, Serializable> parameters = new HashMap<String, Serializable>(13);
parameters.put("A", valueA);
Map<String, Serializable> values = new HashMap<String, Serializable>(13); parameters.put("B", valueB);
values.put("/test/1.1/2.1/3.1/4.1", new Long(41)); parameters.put("C", valueC);
values.put("/test/1.1/2.1/3.1/4.2", "42"); // lowercase versions are not in the config
values.put("/test/1.1/2.1/3.1/4.2", new Date()); parameters.put("a", valueA);
parameters.put("b", valueB);
auditComponent.audit(session, values); parameters.put("c", valueC);
return null; Map<String, Serializable> result = auditTestAction("action-01", nodeRef, parameters);
}
}; Map<String, Serializable> expected = new HashMap<String, Serializable>();
RunAsWork<Void> testRunAs = new RunAsWork<Void>() expected.put("/test/actions/action-01/context-node/noderef", nodeRef);
{ expected.put("/test/actions/action-01/params/A/value", valueA);
public Void doWork() throws Exception expected.put("/test/actions/action-01/params/B/value", valueB);
{ expected.put("/test/actions/action-01/params/C/value", valueC);
return transactionService.getRetryingTransactionHelper().doInTransaction(testCallback);
} // Check
}; checkAuditMaps(result, expected);
AuthenticationUtil.runAs(testRunAs, "SomeOtherUser");
} }
} }

View File

@@ -53,6 +53,8 @@ import org.apache.commons.logging.LogFactory;
public class AuditApplication public class AuditApplication
{ {
public static final String AUDIT_PATH_SEPARATOR = "/"; public static final String AUDIT_PATH_SEPARATOR = "/";
public static final String AUDIT_KEY_REGEX = "[a-zA-Z0-9\\-\\.]+";
public static final String AUDIT_PATH_REGEX = "(/[a-zA-Z0-9\\-\\.]+)+";
private static final Log logger = LogFactory.getLog(AuditApplication.class); private static final Log logger = LogFactory.getLog(AuditApplication.class);
@@ -145,57 +147,73 @@ public class AuditApplication
* Helper method to check that a path is correct for this application instance * Helper method to check that a path is correct for this application instance
* *
* @param path the path in format <b>/app-key/x/y/z</b> * @param path the path in format <b>/app-key/x/y/z</b>
* @throws AuditModelException if the path is invalid * @throws AuditModelException if the path is invalid
*
* @see #AUDIT_PATH_REGEX
*/ */
public void checkPath(String path) public void checkPath(String path)
{ {
if (path == null || path.length() == 0) if (path == null || path.length() == 0)
{ {
generateException(path, "Invalid audit path."); generateException(path, "Empty or null audit path");
} }
if (!path.startsWith(AUDIT_PATH_SEPARATOR)) else if (!path.matches(AUDIT_PATH_REGEX))
{ {
generateException( generateException(
path, path,
"An audit path must always start with the separator '" + AUDIT_PATH_SEPARATOR + "'."); "An audit must match regular expression: " + AUDIT_PATH_REGEX);
} }
if (path.indexOf(applicationKey, 0) != 1) else if (path.indexOf(applicationKey, 0) != 1)
{ {
generateException( generateException(
path, path,
"An audit path's first element must be the application's key i.e. '" + applicationKey + "'."); "An audit path's first element must be the application's key i.e. '" + applicationKey + "'.");
} }
if (path.endsWith(AUDIT_PATH_SEPARATOR))
{
generateException(
path,
"An audit path may not end with the separator '" + AUDIT_PATH_SEPARATOR + "'.");
}
if (!path.toLowerCase().equals(path))
{
generateException(
path,
"An audit path may only contain lowercase letters, '-' and '.' i.e. regex ([a-z]|[0-9]|\\-|\\.)*");
}
} }
/** /**
* Compile a path or part of a path into a single string which always starts with the * 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 * {@link #AUDIT_PATH_SEPARATOR}. This can be a relative path so need not always start with
* the application root key. * the application root key.
* <p>
* If the path separator is present at the beginning of a path component, then it is not added,
* so <code>"/a", "b", "/c"</code> becomes <code>"/a/b/c"</code> allowing path to be appended
* to other paths.
* <p>
* The final result is checked against a {@link #AUDIT_PATH_REGEX regular expression} to ensure
* it is valid.
* *
* @param pathElements the elements of the path e.g. <code>"a", "b", "c"</code>. * @param pathElements the elements of the path e.g. <code>"a", "b", "c"</code>.
* @return Returns the compiled path e.g <code>"/a/b/c"</code>. * @return Returns the compiled path e.g <code>"/a/b/c"</code>.
*/ */
public String buildPath(String ... pathComponents) public static String buildPath(String ... pathComponents)
{ {
StringBuilder sb = new StringBuilder(pathComponents.length * 10); StringBuilder sb = new StringBuilder(pathComponents.length * 10);
for (String pathComponent : pathComponents) for (String pathComponent : pathComponents)
{ {
sb.append(AUDIT_PATH_SEPARATOR).append(pathComponent); if (!pathComponent.startsWith(AUDIT_PATH_SEPARATOR))
{
sb.append(AUDIT_PATH_SEPARATOR);
}
sb.append(pathComponent);
}
String path = sb.toString();
// Check the path format
if (!path.matches(AUDIT_PATH_REGEX))
{
StringBuffer msg = new StringBuffer();
msg.append("The audit path is invalid and must be matched by regular expression: ").append(AUDIT_PATH_REGEX).append("\n")
.append(" Path elements: ");
for (String pathComponent : pathComponents)
{
msg.append(pathComponent).append(", ");
}
msg.append("\n")
.append(" Result: ").append(path);
throw new AuditModelException(msg.toString());
} }
// Done // Done
return sb.toString(); return path;
} }
/** /**
@@ -292,7 +310,7 @@ public class AuditApplication
{ {
buildAuditPaths( buildAuditPaths(
auditPath, auditPath,
"", null,
new HashSet<String>(37), new HashSet<String>(37),
new HashMap<String, DataExtractor>(13), new HashMap<String, DataExtractor>(13),
new HashMap<String, DataGenerator>(13)); new HashMap<String, DataGenerator>(13));
@@ -312,26 +330,29 @@ public class AuditApplication
upperGeneratorsByPath = new HashMap<String, DataGenerator>(upperGeneratorsByPath); upperGeneratorsByPath = new HashMap<String, DataGenerator>(upperGeneratorsByPath);
// Append the current audit path to the current path // Append the current audit path to the current path
currentPath = (currentPath + AUDIT_PATH_SEPARATOR + auditPath.getKey()); if (currentPath == null)
{
currentPath = AuditApplication.buildPath(auditPath.getKey());
}
else
{
currentPath = AuditApplication.buildPath(currentPath, auditPath.getKey());
}
// Make sure we have not processed it before // Make sure we have not processed it before
if (!existingPaths.add(currentPath)) if (!existingPaths.add(currentPath))
{ {
generateException(currentPath, "The audit path already exists."); generateException(currentPath, "The audit path already exists.");
} }
// Make sure that the path is all lowercase
checkPathCase(currentPath);
// Get the data extractors declared for this key // Get the data extractors declared for this key
for (RecordValue element : auditPath.getRecordValue()) for (RecordValue element : auditPath.getRecordValue())
{ {
String key = element.getKey(); String key = element.getKey();
String extractorPath = (currentPath + AUDIT_PATH_SEPARATOR + key); String extractorPath = AuditApplication.buildPath(currentPath, key);
if (!existingPaths.add(extractorPath)) if (!existingPaths.add(extractorPath))
{ {
generateException(extractorPath, "The audit path already exists."); generateException(extractorPath, "The audit path already exists.");
} }
// Make sure that the path is all lowercase
checkPathCase(extractorPath);
String extractorName = element.getDataExtractor(); String extractorName = element.getDataExtractor();
DataExtractor extractor = dataExtractorsByName.get(extractorName); DataExtractor extractor = dataExtractorsByName.get(extractorName);
@@ -354,13 +375,11 @@ public class AuditApplication
for (GenerateValue element : auditPath.getGenerateValue()) for (GenerateValue element : auditPath.getGenerateValue())
{ {
String key = element.getKey(); String key = element.getKey();
String generatorPath = (currentPath + AUDIT_PATH_SEPARATOR + key); String generatorPath = AuditApplication.buildPath(currentPath, key);
if (!existingPaths.add(generatorPath)) if (!existingPaths.add(generatorPath))
{ {
generateException(generatorPath, "The audit path already exists."); generateException(generatorPath, "The audit path already exists.");
} }
// Make sure that the path is all lowercase
checkPathCase(generatorPath);
String generatorName = element.getDataGenerator(); String generatorName = element.getDataGenerator();
DataGenerator generator = dataGeneratorsByName.get(generatorName); DataGenerator generator = dataGeneratorsByName.get(generatorName);
@@ -401,14 +420,6 @@ public class AuditApplication
} }
} }
private void checkPathCase(String path)
{
if (!path.equals(path.toLowerCase()))
{
generateException(path, "Audit key entries may only be in lowercase to ensure case-insensitivity.");
}
}
private void generateException(String path, String msg) throws AuditModelException private void generateException(String path, String msg) throws AuditModelException
{ {
throw new AuditModelException("" + throw new AuditModelException("" +

View File

@@ -435,11 +435,12 @@ public class AuditModelRegistry
} }
else if (extractorElement.getRegisteredName() != null) else if (extractorElement.getRegisteredName() != null)
{ {
dataExtractor = dataExtractors.getNamedObject(extractorElement.getRegisteredName()); String registeredName = extractorElement.getRegisteredName();
dataExtractor = dataExtractors.getNamedObject(registeredName);
if (dataExtractor == null) if (dataExtractor == null)
{ {
throw new AuditModelException( throw new AuditModelException(
"No registered audit data extractor exists for '" + name + "'."); "No registered audit data extractor exists for '" + registeredName + "'.");
} }
} }
else else
@@ -488,11 +489,12 @@ public class AuditModelRegistry
} }
else if (generatorElement.getRegisteredName() != null) else if (generatorElement.getRegisteredName() != null)
{ {
dataGenerator = dataGenerators.getNamedObject(generatorElement.getRegisteredName()); String registeredName = generatorElement.getRegisteredName();
dataGenerator = dataGenerators.getNamedObject(registeredName);
if (dataGenerator == null) if (dataGenerator == null)
{ {
throw new AuditModelException( throw new AuditModelException(
"No registered audit data generator exists for '" + name + "'."); "No registered audit data generator exists for '" + registeredName + "'.");
} }
} }
else else

View File

@@ -34,19 +34,11 @@ public class ObjectFactory {
} }
/** /**
* Create an instance of {@link Application } * Create an instance of {@link KeyedAuditDefinition }
* *
*/ */
public Application createApplication() { public KeyedAuditDefinition createKeyedAuditDefinition() {
return new Application(); return new KeyedAuditDefinition();
}
/**
* Create an instance of {@link DataGenerators }
*
*/
public DataGenerators createDataGenerators() {
return new DataGenerators();
} }
/** /**
@@ -57,30 +49,6 @@ public class ObjectFactory {
return new DataExtractor(); return new DataExtractor();
} }
/**
* Create an instance of {@link AuditPath }
*
*/
public AuditPath createAuditPath() {
return new AuditPath();
}
/**
* Create an instance of {@link RecordValue }
*
*/
public RecordValue createRecordValue() {
return new RecordValue();
}
/**
* Create an instance of {@link GenerateValue }
*
*/
public GenerateValue createGenerateValue() {
return new GenerateValue();
}
/** /**
* Create an instance of {@link Audit } * Create an instance of {@link Audit }
* *
@@ -89,14 +57,6 @@ public class ObjectFactory {
return new Audit(); return new Audit();
} }
/**
* Create an instance of {@link KeyedAuditDefinition }
*
*/
public KeyedAuditDefinition createKeyedAuditDefinition() {
return new KeyedAuditDefinition();
}
/** /**
* Create an instance of {@link DataExtractors } * Create an instance of {@link DataExtractors }
* *
@@ -105,6 +65,14 @@ public class ObjectFactory {
return new DataExtractors(); return new DataExtractors();
} }
/**
* Create an instance of {@link AuditPath }
*
*/
public AuditPath createAuditPath() {
return new AuditPath();
}
/** /**
* Create an instance of {@link DataGenerator } * Create an instance of {@link DataGenerator }
* *
@@ -113,6 +81,38 @@ public class ObjectFactory {
return new DataGenerator(); return new DataGenerator();
} }
/**
* Create an instance of {@link DataGenerators }
*
*/
public DataGenerators createDataGenerators() {
return new DataGenerators();
}
/**
* Create an instance of {@link RecordValue }
*
*/
public RecordValue createRecordValue() {
return new RecordValue();
}
/**
* Create an instance of {@link Application }
*
*/
public Application createApplication() {
return new Application();
}
/**
* Create an instance of {@link GenerateValue }
*
*/
public GenerateValue createGenerateValue() {
return new GenerateValue();
}
/** /**
* Create an instance of {@link JAXBElement }{@code <}{@link Audit }{@code >}} * Create an instance of {@link JAXBElement }{@code <}{@link Audit }{@code >}}
* *

View File

@@ -235,7 +235,11 @@ public abstract class AbstractAuditDAOImpl implements AuditDAO
usernameId = null; usernameId = null;
} }
// Now persist the data values // Now persist the data values
final Long valuesId = propertyValueDAO.getOrCreatePropertyValue((Serializable)values).getFirst(); Long valuesId = null;
if (values != null && values.size() > 0)
{
valuesId = propertyValueDAO.getOrCreatePropertyValue((Serializable)values).getFirst();
}
// Create the audit entry // Create the audit entry
AuditEntryEntity entity = createAuditEntry(sessionId, time, usernameId, valuesId); AuditEntryEntity entity = createAuditEntry(sessionId, time, usernameId, valuesId);

View File

@@ -583,7 +583,7 @@ public abstract class AbstractPropertyValueDAOImpl implements PropertyValueDAO
/** /**
* {@inheritDoc} * {@inheritDoc}
* @see #getOrCreatePropertyValueImpl(Serializable, int, int) * @see #getOrCreatePropertyValueImpl(Serializable, Long, int, int)
*/ */
public Pair<Long, Serializable> getOrCreatePropertyValue(Serializable value, int maxDepth) public Pair<Long, Serializable> getOrCreatePropertyValue(Serializable value, int maxDepth)
{ {
@@ -599,6 +599,7 @@ public abstract class AbstractPropertyValueDAOImpl implements PropertyValueDAO
{ {
if (value != null && maxDepth > currentDepth && value instanceof Map<?, ?>) if (value != null && maxDepth > currentDepth && value instanceof Map<?, ?>)
{ {
// TODO: Go through cache?
// The default is to do a deep expansion // The default is to do a deep expansion
Long mapId = createPropertyMapImpl( Long mapId = createPropertyMapImpl(
(Map<? extends Serializable, ? extends Serializable>)value, (Map<? extends Serializable, ? extends Serializable>)value,
@@ -606,11 +607,13 @@ public abstract class AbstractPropertyValueDAOImpl implements PropertyValueDAO
maxDepth, maxDepth,
currentDepth); currentDepth);
Pair<Long, Serializable> entityPair = new Pair<Long, Serializable>(mapId, value); Pair<Long, Serializable> entityPair = new Pair<Long, Serializable>(mapId, value);
// TODO: Go through cache? // Cache instance by ID only
propertyValueCache.updateValue(mapId, value);
return entityPair; return entityPair;
} }
else if (value != null && maxDepth > currentDepth && value instanceof Collection<?>) else if (value != null && maxDepth > currentDepth && value instanceof Collection<?>)
{ {
// TODO: Go through cache?
// The default is to do a deep expansion // The default is to do a deep expansion
Long collectionId = createPropertyCollectionImpl( Long collectionId = createPropertyCollectionImpl(
(Collection<? extends Serializable>)value, (Collection<? extends Serializable>)value,
@@ -618,7 +621,8 @@ public abstract class AbstractPropertyValueDAOImpl implements PropertyValueDAO
maxDepth, maxDepth,
currentDepth); currentDepth);
Pair<Long, Serializable> entityPair = new Pair<Long, Serializable>(collectionId, value); Pair<Long, Serializable> entityPair = new Pair<Long, Serializable>(collectionId, value);
// TODO: Go through cache? // Cache instance by ID only
propertyValueCache.updateValue(collectionId, value);
return entityPair; return entityPair;
} }
else else
@@ -722,6 +726,16 @@ public abstract class AbstractPropertyValueDAOImpl implements PropertyValueDAO
PropertyValueEntity entity = findPropertyValueByValue(value); PropertyValueEntity entity = findPropertyValueByValue(value);
return convertEntityToPair(entity); return convertEntityToPair(entity);
} }
/**
* 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<PropertyIdSearchRow> findPropertyValueById(Long id); protected abstract List<PropertyIdSearchRow> findPropertyValueById(Long id);

View File

@@ -24,26 +24,89 @@
*/ */
package org.alfresco.repo.domain.propval; package org.alfresco.repo.domain.propval;
import java.io.Serializable;
import java.util.Collections;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import org.alfresco.repo.domain.propval.PropertyValueEntity.PersistedType;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.Period;
import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter; import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter;
import org.alfresco.util.ParameterCheck;
/** /**
* Default converter for handling data going to and from the persistence layer. * Default converter for handling data going to and from the persistence layer.
* <p/> * <p/>
* Apart from converting between <code>Boolean</code> and <code>Long</code> values, * Properties are stored as a set of well-defined types defined by the enumeration
* the {@link DefaultTypeConverter} is used. * {@link PersistedType}. Ultimately, data can be persisted as BLOB data, but must
* be the last resort.
* *
* @author Derek Hulley * @author Derek Hulley
* @since 3.2 * @since 3.2
*/ */
public class DefaultPropertyTypeConverter implements PropertyTypeConverter public class DefaultPropertyTypeConverter implements PropertyTypeConverter
{ {
/**
* An unmodifiable map of types and how they should be persisted
*/
protected static final Map<Class<?>, PersistedType> defaultPersistedTypesByClass;
static
{
// Create the map of class-type
Map<Class<?>, PersistedType> mapClass = new HashMap<Class<?>, PersistedType>(29);
mapClass.put(NodeRef.class, PersistedType.STRING);
mapClass.put(Period.class, PersistedType.STRING);
mapClass.put(Locale.class, PersistedType.STRING);
defaultPersistedTypesByClass = Collections.unmodifiableMap(mapClass);
}
private Map<Class<?>, PersistedType> persistenceMapping;
/** /**
* Default constructor * Default constructor
*/ */
public DefaultPropertyTypeConverter() public DefaultPropertyTypeConverter()
{ {
persistenceMapping = new HashMap<Class<?>, PersistedType>(
DefaultPropertyTypeConverter.defaultPersistedTypesByClass);
} }
/**
* Allow subclasses to add further type mappings specific to the implementation
*
* @param clazz the class to be converted
* @param targetType the target persisted type
*/
protected void addTypeMapping(Class<?> clazz, PersistedType targetType)
{
this.persistenceMapping.put(clazz, targetType);
}
/**
* {@inheritDoc}
*/
public PersistedType getPersistentType(Serializable value)
{
ParameterCheck.mandatory("value", value);
Class<?> clazz = value.getClass();
PersistedType type = persistenceMapping.get(clazz);
if (type == null)
{
return PersistedType.SERIALIZABLE;
}
else
{
return type;
}
}
/**
* Performs the conversion
*/
public <T> T convert(Class<T> targetClass, Object value) public <T> T convert(Class<T> targetClass, Object value)
{ {
return DefaultTypeConverter.INSTANCE.convert(targetClass, value); return DefaultTypeConverter.INSTANCE.convert(targetClass, value);

View File

@@ -24,6 +24,10 @@
*/ */
package org.alfresco.repo.domain.propval; package org.alfresco.repo.domain.propval;
import java.io.Serializable;
import org.alfresco.repo.domain.propval.PropertyValueEntity.PersistedType;
/** /**
* Interface for converters that to translate between persisted values and external values. * Interface for converters that to translate between persisted values and external values.
* <p/> * <p/>
@@ -35,6 +39,25 @@ package org.alfresco.repo.domain.propval;
*/ */
public interface PropertyTypeConverter public interface PropertyTypeConverter
{ {
/**
* When external to persisted type mappings are not obvious, the persistence framework,
* before persisting as {@link PersistedType#SERIALIZABLE}, will give the converter
* a chance to choose how the value must be persisted:
* <ul>
* <li>{@link PersistedType#LONG}</li>
* <li>{@link PersistedType#DOUBLE}</li>
* <li>{@link PersistedType#STRING}</li>
* <li>{@link PersistedType#SERIALIZABLE}</li>
* </ul>
* 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.
*
* @param value the value that does not have an obvious persistence slot
* @return Returns the type of persistence to use
*/
PersistedType getPersistentType(Serializable value);
/** /**
* Convert a value to a given type. * Convert a value to a given type.
* *

View File

@@ -326,7 +326,8 @@ public class PropertyValueEntity
persistedTypeEnum = persistedTypesByClass.get(valueClazz); persistedTypeEnum = persistedTypesByClass.get(valueClazz);
if (persistedTypeEnum == null) if (persistedTypeEnum == null)
{ {
persistedTypeEnum = PersistedType.SERIALIZABLE; // Give the converter a chance to change the type it must be persisted as
persistedTypeEnum = converter.getPersistentType(value);
} }
persistedType = persistedTypeEnum.getOrdinalNumber(); persistedType = persistedTypeEnum.getOrdinalNumber();
// Get the class to persist as // Get the class to persist as
@@ -345,7 +346,11 @@ public class PropertyValueEntity
serializableValue = value; serializableValue = value;
break; break;
default: default:
throw new IllegalStateException("Should not be able to get through switch"); throw new IllegalStateException(
"PropertyTypeConverter.convertToPersistentType returned illegal type: " +
" Converter: " + converter + "\n" +
" Type Returned: " + persistedTypeEnum + "\n" +
" From Value: " + value);
} }
} }
} }

View File

@@ -8,6 +8,10 @@
xsi:schemaLocation="http://www.alfresco.org/repo/audit/model/3.2 alfresco-audit-3.2.xsd" xsi:schemaLocation="http://www.alfresco.org/repo/audit/model/3.2 alfresco-audit-3.2.xsd"
> >
<DataExtractors>
<DataExtractor name="simpleValue" registeredName="auditModel.extractor.simpleValue"/>
</DataExtractors>
<Application name="Alfresco Test Bad 03" key="test-bad-03"> <Application name="Alfresco Test Bad 03" key="test-bad-03">
<AuditPath key="1.1"> <AuditPath key="1.1">
<AuditPath key="2.1"> <AuditPath key="2.1">

View File

@@ -8,10 +8,14 @@
xsi:schemaLocation="http://www.alfresco.org/repo/audit/model/3.2 alfresco-audit-3.2.xsd" xsi:schemaLocation="http://www.alfresco.org/repo/audit/model/3.2 alfresco-audit-3.2.xsd"
> >
<DataExtractors>
<DataExtractor name="simpleValue" registeredName="auditModel.extractor.simpleValue"/>
</DataExtractors>
<Application name="Alfresco Test Bad 04" key="test-bad-04"> <Application name="Alfresco Test Bad 04" key="test-bad-04">
<AuditPath key="1.1"> <AuditPath key="1.1">
<AuditPath key="2.1"> <AuditPath key="2.1">
<RecordValue key="value.UPPER" dataExtractor="simpleValue"/> <RecordValue key="value.£" dataExtractor="simpleValue"/>
</AuditPath> </AuditPath>
</AuditPath> </AuditPath>
</Application> </Application>

View File

@@ -8,6 +8,10 @@
xsi:schemaLocation="http://www.alfresco.org/repo/audit/model/3.2 alfresco-audit-3.2.xsd" xsi:schemaLocation="http://www.alfresco.org/repo/audit/model/3.2 alfresco-audit-3.2.xsd"
> >
<DataGenerators>
<DataGenerator name="systemTime" registeredName="auditModel.generator.time"/>
</DataGenerators>
<Application name="Alfresco Test Bad 05" key="test-bad-05"> <Application name="Alfresco Test Bad 05" key="test-bad-05">
<AuditPath key="1.1"> <AuditPath key="1.1">
<AuditPath key="2.1"> <AuditPath key="2.1">

View File

@@ -0,0 +1,23 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Default Audit Configuration -->
<Audit
xmlns="http://www.alfresco.org/repo/audit/model/3.2"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.alfresco.org/repo/audit/model/3.2 alfresco-audit-3.2.xsd"
>
<DataGenerators>
<DataGenerator name="systemTime" registeredName="blah"/>
</DataGenerators>
<Application name="Alfresco Test Bad 06" key="test-bad-06">
<AuditPath key="1.1">
<AuditPath key="2.1">
<GenerateValue key="time" dataGenerator="systemTime" scope="SESSION"/>
</AuditPath>
</AuditPath>
</Application>
</Audit>

View File

@@ -64,9 +64,22 @@
</AuditPath> </AuditPath>
</AuditPath> </AuditPath>
</AuditPath> </AuditPath>
<AuditPath key="node"> <AuditPath key="actions">
<AuditPath key="t1"> <AuditPath key="action-01">
<RecordValue key="noderef" dataExtractor="simpleValue"/> <AuditPath key="context-node">
<RecordValue key="noderef" dataExtractor="simpleValue"/>
</AuditPath>
<AuditPath key="params">
<AuditPath key="A">
<RecordValue key="value" dataExtractor="simpleValue"/>
</AuditPath>
<AuditPath key="B">
<RecordValue key="value" dataExtractor="simpleValue"/>
</AuditPath>
<AuditPath key="C">
<RecordValue key="value" dataExtractor="simpleValue"/>
</AuditPath>
</AuditPath>
</AuditPath> </AuditPath>
</AuditPath> </AuditPath>
</Application> </Application>