/* * Copyright (C) 2005-2014 Alfresco Software Limited. * * This file is part of Alfresco * * Alfresco is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Alfresco 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with Alfresco. If not, see . */ package org.alfresco.repo.domain.audit; import java.io.File; import java.io.IOException; import java.io.Serializable; import java.net.URL; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import javax.transaction.UserTransaction; import junit.framework.TestCase; import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.repo.content.transform.AbstractContentTransformerTest; import org.alfresco.repo.domain.audit.AuditDAO.AuditApplicationInfo; import org.alfresco.repo.domain.contentdata.ContentDataDAO; import org.alfresco.repo.domain.propval.PropValGenerator; import org.alfresco.repo.domain.propval.PropertyValueDAO; import org.alfresco.repo.transaction.RetryingTransactionHelper; import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; import org.alfresco.service.ServiceRegistry; import org.alfresco.service.cmr.audit.AuditQueryParameters; import org.alfresco.service.cmr.audit.AuditService.AuditQueryCallback; import org.alfresco.service.cmr.repository.ContentData; import org.alfresco.service.transaction.TransactionService; import org.alfresco.test_category.OwnJVMTestsCategory; import org.alfresco.util.ApplicationContextHelper; import org.alfresco.util.GUID; import org.alfresco.util.Pair; import org.apache.commons.lang.mutable.MutableInt; import org.junit.experimental.categories.Category; import org.springframework.context.ConfigurableApplicationContext; /** * @see ContentDataDAO * * @author Derek Hulley * @since 3.2 */ @Category(OwnJVMTestsCategory.class) public class AuditDAOTest extends TestCase { private ConfigurableApplicationContext ctx = (ConfigurableApplicationContext) ApplicationContextHelper.getApplicationContext(); private TransactionService transactionService; private RetryingTransactionHelper txnHelper; private AuditDAO auditDAO; private PropertyValueDAO propertyValueDAO; @Override public void setUp() throws Exception { ServiceRegistry serviceRegistry = (ServiceRegistry) ctx.getBean(ServiceRegistry.SERVICE_REGISTRY); transactionService = serviceRegistry.getTransactionService(); txnHelper = transactionService.getRetryingTransactionHelper(); auditDAO = (AuditDAO) ctx.getBean("auditDAO"); propertyValueDAO = ctx.getBean(PropertyValueDAO.class); } public void testAuditModel() throws Exception { final File file = AbstractContentTransformerTest.loadQuickTestFile("pdf"); assertNotNull(file); final URL url = new URL("file:" + file.getAbsolutePath()); RetryingTransactionCallback> callback = new RetryingTransactionCallback>() { public Pair execute() throws Throwable { Pair auditModelPair = auditDAO.getOrCreateAuditModel(url); return auditModelPair; } }; Pair configPair = txnHelper.doInTransaction(callback); assertNotNull(configPair); // Now repeat. The results should be exactly the same. Pair configPairCheck = txnHelper.doInTransaction(callback); assertNotNull(configPairCheck); assertEquals(configPair, configPairCheck); } public void testAuditApplication() throws Exception { final File file = AbstractContentTransformerTest.loadQuickTestFile("pdf"); assertNotNull(file); final URL url = new URL("file:" + file.getAbsolutePath()); RetryingTransactionCallback createModelCallback = new RetryingTransactionCallback() { public Long execute() throws Throwable { return auditDAO.getOrCreateAuditModel(url).getFirst(); } }; final Long modelId = txnHelper.doInTransaction(createModelCallback); final String appName = getName() + "." + System.currentTimeMillis(); final int count = 1000; RetryingTransactionCallback createAppCallback = new RetryingTransactionCallback() { public Void execute() throws Throwable { for (int i = 0; i < count; i++) { AuditApplicationInfo appInfo = auditDAO.getAuditApplication(appName); if (appInfo == null) { appInfo = auditDAO.createAuditApplication(appName, modelId); } } return null; } }; long before = System.nanoTime(); txnHelper.doInTransaction(createAppCallback); long after = System.nanoTime(); System.out.println( "Time for " + count + " application creations was " + ((double)(after - before)/(10E6)) + "ms"); } public void testAuditEntry() throws Exception { doAuditEntryImpl(1000); } /** * @return Returns the name of the application */ private String doAuditEntryImpl(final int count) 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 createAppCallback = new RetryingTransactionCallback() { public Long execute() throws Throwable { AuditApplicationInfo appInfo = auditDAO.getAuditApplication(appName); if (appInfo == null) { Long modelId = auditDAO.getOrCreateAuditModel(url).getFirst(); appInfo = auditDAO.createAuditApplication(appName, modelId); } return appInfo.getId(); } }; final Long sessionId = txnHelper.doInTransaction(createAppCallback); 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"); // Done return appName; } public synchronized void testAuditQuery() throws Exception { // Some entries doAuditEntryImpl(1); final MutableInt count = new MutableInt(0); final LinkedList timestamps = new LinkedList(); // Find everything, but look for a specific key final AuditQueryCallback callback = new AuditQueryCallback() { public boolean valuesRequired() { return false; } public boolean handleAuditEntry( Long entryId, String applicationName, String user, long time, Map values) { count.setValue(count.intValue() + 1); timestamps.add(time); return true; } public boolean handleAuditEntryError(Long entryId, String errorMsg, Throwable error) { throw new AlfrescoRuntimeException(errorMsg, error); } }; final AuditQueryParameters params = new AuditQueryParameters(); params.addSearchKey("/a/b/c", null); RetryingTransactionCallback findCallback = new RetryingTransactionCallback() { public Void execute() throws Throwable { auditDAO.findAuditEntries(callback, params, 2); return null; } }; count.setValue(0); timestamps.clear(); txnHelper.doInTransaction(findCallback); assertTrue("Expected at least one result", count.intValue() > 0); // // Make sure that the last two entries are in forward order (ascending time) // Long lastTimestamp = timestamps.removeLast(); // Long secondLastTimeStamp = timestamps.removeLast(); // assertTrue("The timestamps should be in ascending order", lastTimestamp.compareTo(secondLastTimeStamp) > 0); // // Make sure that the last two entries differ in time wait(1000L); // Search in reverse order doAuditEntryImpl(1); RetryingTransactionCallback findReverseCallback = new RetryingTransactionCallback() { public Void execute() throws Throwable { params.setForward(false); auditDAO.findAuditEntries(callback, params, 2); params.setForward(true); return null; } }; timestamps.clear(); txnHelper.doInTransaction(findReverseCallback); // // // Make sure that the last two entries are in reverse order (descending time) // lastTimestamp = timestamps.removeLast(); // secondLastTimeStamp = timestamps.removeLast(); // assertTrue("The timestamps should be in descending order", lastTimestamp.compareTo(secondLastTimeStamp) < 0); } public void testAuditDeleteEntries() throws Exception { final AuditQueryCallback noResultsCallback = new AuditQueryCallback() { public boolean valuesRequired() { return false; } public boolean handleAuditEntry( Long entryId, String applicationName, String user, long time, Map values) { fail("Expected no results. All entries should have been removed."); return false; } public boolean handleAuditEntryError(Long entryId, String errorMsg, Throwable error) { throw new AlfrescoRuntimeException(errorMsg, error); } }; // Some entries final String appName = doAuditEntryImpl(1); final AuditQueryParameters params = new AuditQueryParameters(); params.setApplicationName(appName); // Delete the entries RetryingTransactionCallback deletedCallback = new RetryingTransactionCallback() { public Void execute() throws Throwable { Long appId = auditDAO.getAuditApplication(appName).getId(); auditDAO.deleteAuditEntries(appId, null, null); // There should be no entries auditDAO.findAuditEntries(noResultsCallback, params, -1); return null; } }; txnHelper.doInTransaction(deletedCallback); } /** * Ensure that only the correct application's audit entries are deleted. * @throws Exception */ public void testAuditDeleteEntriesForApplication() throws Exception { final String app1 = doAuditEntryImpl(6); final String app2 = doAuditEntryImpl(18); final AuditQueryCallbackImpl resultsCallback = new AuditQueryCallbackImpl(); RetryingTransactionCallback deletedCallback = new RetryingTransactionCallback() { public Void execute() throws Throwable { Long app1Id = auditDAO.getAuditApplication(app1).getId(); auditDAO.deleteAuditEntries(app1Id, null, null); // There should be no entries for app1 // but still entries for app2 auditDAO.findAuditEntries(resultsCallback, new AuditQueryParameters(), -1); assertEquals("All entries should have been deleted from app1", 0, resultsCallback.numEntries(app1)); assertEquals("No entries should have been deleted from app2", 18, resultsCallback.numEntries(app2)); return null; } }; txnHelper.doInTransaction(deletedCallback); } /** * Ensure that an application's audit entries can be deleted between 2 times. * @throws Exception */ public void testAuditDeleteEntriesForApplicationBetweenTimes() throws Exception { RetryingTransactionCallback deletedCallback = new RetryingTransactionCallback() { AuditQueryCallbackImpl preDeleteCallback = new AuditQueryCallbackImpl(); AuditQueryCallbackImpl resultsCallback = new AuditQueryCallbackImpl(); public Void execute() throws Throwable { AuditApplicationInfo info1 = createAuditApp(); String app1 = info1.getName(); Long app1Id = info1.getId(); AuditApplicationInfo info2 = createAuditApp(); String app2 = info2.getName(); // Create items 10, 11, 12, 13, 14 for application 1 // Create items 21, 22 for application 2 createItem(info1, 10); createItem(info1, 11); Thread.sleep(10); // stop previous statements being executed during t1 Thread.sleep(10); final long t1 = System.currentTimeMillis(); Thread.sleep(10); Thread.sleep(10); createItem(info2, 21); createItem(info1, 12); createItem(info1, 13); Thread.sleep(10); Thread.sleep(10); final long t2 = System.currentTimeMillis(); Thread.sleep(10); // stop next statements being executed during t2 Thread.sleep(10); createItem(info2, 22); createItem(info1, 14); auditDAO.findAuditEntries(preDeleteCallback, new AuditQueryParameters(), -1); assertEquals(5, preDeleteCallback.numEntries(app1)); assertEquals(2, preDeleteCallback.numEntries(app2)); auditDAO.deleteAuditEntries(app1Id, t1, t2); auditDAO.findAuditEntries(resultsCallback, new AuditQueryParameters(), -1); assertEquals("Two entries should have been deleted from app1", 3, resultsCallback.numEntries(app1)); assertEquals("No entries should have been deleted from app2", 2, resultsCallback.numEntries(app2)); return null; } }; txnHelper.doInTransaction(deletedCallback); } /** * Ensure audit entries can be deleted between two times - for all applications. * @throws Exception */ public void testAuditDeleteEntriesBetweenTimes() throws Exception { RetryingTransactionCallback deletedCallback = new RetryingTransactionCallback() { AuditQueryCallbackImpl preDeleteCallback = new AuditQueryCallbackImpl(); AuditQueryCallbackImpl resultsCallback = new AuditQueryCallbackImpl(); public Void execute() throws Throwable { AuditApplicationInfo info1 = createAuditApp(); String app1 = info1.getName(); AuditApplicationInfo info2 = createAuditApp(); String app2 = info2.getName(); // Create items 10, 11, 12, 13, 14 for application 1 // Create items 21, 22 for application 2 createItem(info1, 10); createItem(info1, 11); Thread.sleep(10); Thread.sleep(10); // stop previous statements being executed during t1 final long t1 = System.currentTimeMillis(); Thread.sleep(10); Thread.sleep(10); createItem(info2, 21); createItem(info1, 12); createItem(info1, 13); Thread.sleep(10); Thread.sleep(10); final long t2 = System.currentTimeMillis(); Thread.sleep(10); // stop next statements being executed during t2 Thread.sleep(10); createItem(info2, 22); createItem(info1, 14); auditDAO.findAuditEntries(preDeleteCallback, new AuditQueryParameters(), -1); assertEquals(5, preDeleteCallback.numEntries(app1)); assertEquals(2, preDeleteCallback.numEntries(app2)); // Delete audit entries between times - for all applications. auditDAO.deleteAuditEntries(null, t1, t2); auditDAO.findAuditEntries(resultsCallback, new AuditQueryParameters(), -1); assertEquals("Two entries should have been deleted from app1", 3, resultsCallback.numEntries(app1)); assertEquals("One entry should have been deleted from app2", 1, resultsCallback.numEntries(app2)); return null; } }; txnHelper.doInTransaction(deletedCallback); } /** * Create an audit item * @param appInfo The audit application to create the item for. * @param value The value that will be stored against the path /a/b/c */ private void createItem(final AuditApplicationInfo appInfo, final int value) { String username = "alexi"; Map values = Collections.singletonMap("/a/b/c", (Serializable) value); long now = System.currentTimeMillis(); auditDAO.createAuditEntry(appInfo.getId(), now, username, values); } /** * Create an audit application. * @return AuditApplicationInfo for the new application. * @throws IOException */ private AuditApplicationInfo createAuditApp() throws IOException { String appName = getName() + "." + GUID.generate(); File file = AbstractContentTransformerTest.loadQuickTestFile("pdf"); assertNotNull(file); URL url = new URL("file:" + file.getAbsolutePath()); AuditApplicationInfo appInfo = auditDAO.getAuditApplication(appName); if (appInfo == null) { Long modelId = auditDAO.getOrCreateAuditModel(url).getFirst(); appInfo = auditDAO.createAuditApplication(appName, modelId); } return appInfo; } public class AuditQueryCallbackImpl implements AuditQueryCallback { private Map countsByApp = new HashMap(); public boolean valuesRequired() { return false; } public boolean handleAuditEntry( Long entryId, String applicationName, String user, long time, Map values) { Integer count = countsByApp.get(applicationName); if (count == null) countsByApp.put(applicationName, 1); else countsByApp.put(applicationName, ++count); return true; } public boolean handleAuditEntryError(Long entryId, String errorMsg, Throwable error) { throw new AlfrescoRuntimeException(errorMsg, error); } public int numEntries(String appName) { if (countsByApp.containsKey(appName)) return countsByApp.get(appName); else return 0; } } /** * MNT-10067: use a script to delete the orphaned audit data (property values). */ public void testScriptCanDeleteOrphanedProps() throws Exception { // single test scriptCanDeleteOrphanedPropsWork(false); } private void scriptCanDeleteOrphanedPropsWork(final boolean performance) throws Exception { final int iterationStep, maxIterations; if (performance) { iterationStep = 1000; maxIterations = 1000; } else { iterationStep = 1; maxIterations = 1; } UserTransaction txn; for (int i = iterationStep; i <= maxIterations*iterationStep; i+=iterationStep) { List stringValues = new LinkedList(); List doubleValues = new LinkedList(); List dateValues = new LinkedList(); txn = transactionService.getUserTransaction(); long startCreate = System.currentTimeMillis(); txn.begin(); for (int j = 0; j < i; j++) { PropValGenerator valueGen = new PropValGenerator(propertyValueDAO); String stringValue = valueGen.createUniqueString(); stringValues.add(stringValue); Double doubleValue = valueGen.createUniqueDouble(); doubleValues.add(doubleValue); Date dateValue = valueGen.createUniqueDate(); dateValues.add(dateValue); AuditQueryCallbackImpl preDeleteCallback = new AuditQueryCallbackImpl(); AuditQueryCallbackImpl resultsCallback = new AuditQueryCallbackImpl(); AuditApplicationInfo info1 = createAuditApp(); String app1 = info1.getName(); String username = "alexi"; Map values = new HashMap(); values.put("/a/b/string-" + j, stringValue); values.put("/a/b/double-" + j, doubleValue); values.put("/a/b/date-" + j, dateValue); // TODO: how to deal with Serializable values which cannot be retrieved later in test by value alone? long now = System.currentTimeMillis(); auditDAO.createAuditEntry(info1.getId(), now, username, values); auditDAO.findAuditEntries(preDeleteCallback, new AuditQueryParameters(), -1); assertEquals(1, preDeleteCallback.numEntries(app1)); // Delete audit entries between times - for all applications. auditDAO.deleteAuditEntries(info1.getId(), null, null); if (!performance) { auditDAO.findAuditEntries(resultsCallback, new AuditQueryParameters(), -1); assertEquals("All entries should have been deleted from app1", 0, resultsCallback.numEntries(app1)); } } txn.commit(); System.out.println("Created values for " + i + " entries in " + (System.currentTimeMillis() - startCreate) + " ms."); txn = transactionService.getUserTransaction(); txn.begin(); if (!performance) { // Check there are some persisted values to delete. // Unlike PropertyValueDAOTest we're using the getPropertyValue() method here, // instead of the datatype-specific methods (e.g. getPropertyStringValue()). // This is because AuditDAO persists an entire map of values resulting in different behaviour // (i.e. dates are persisted as Serializable) for (String stringValue : stringValues) { assertEquals(stringValue, propertyValueDAO.getPropertyValue(stringValue).getSecond()); } for (Double doubleValue : doubleValues) { assertEquals(doubleValue, propertyValueDAO.getPropertyValue(doubleValue).getSecond()); } for (Date dateValue : dateValues) { assertEquals(dateValue, propertyValueDAO.getPropertyValue(dateValue).getSecond()); } } long startDelete = System.currentTimeMillis(); propertyValueDAO.cleanupUnusedValues(); txn.commit(); System.out.println("Cleaned values for " + i + " entries in " + (System.currentTimeMillis() - startDelete) + " ms."); if (!performance) { // Check all the properties have been deleted. txn = transactionService.getUserTransaction(); txn.begin(); for (String stringValue : stringValues) { assertPropDeleted(propertyValueDAO.getPropertyValue(stringValue)); } for (Double doubleValue : doubleValues) { assertPropDeleted(propertyValueDAO.getPropertyValue(doubleValue)); } for (Date dateValue : dateValues) { assertPropDeleted(propertyValueDAO.getPropertyValue(dateValue)); } txn.commit(); } } } private void assertPropDeleted(Pair value) { if (value != null) { String msg = String.format("Property value [%s=%s] should have been deleted by cleanup script.", value.getSecond().getClass().getSimpleName(), value.getSecond()); fail(msg); } } public void scriptCanDeleteOrphanedPropsPerformance() throws Exception { scriptCanDeleteOrphanedPropsWork(true); } public static void main(String[] args) { try { AuditDAOTest test = new AuditDAOTest(); test.setUp(); System.out.println("Press any key to run performance test."); System.in.read(); test.scriptCanDeleteOrphanedPropsPerformance(); System.out.println("Press any key to shutdown."); System.in.read(); test.tearDown(); } catch (Throwable e) { e.printStackTrace(); } finally { ApplicationContextHelper.closeApplicationContext(); } } }