From 29f7f5d073cdf9e32ccd3dec226c866cf48f270e Mon Sep 17 00:00:00 2001 From: Alan Davis Date: Mon, 20 Jun 2011 12:42:10 +0000 Subject: [PATCH] Merged BRANCHES/DEV/V3.4-BUG-FIX to HEAD 28236: ALF-8810: Removed trailing space from discussion.discussion_for Italian translation 28241: Incremented version revision for 3.4.4 28284: ALF-835 - WCM/AVM: copy (empty) folder into itself 28285: ALF-6863: More than one cifs device breaks the web UI (explorer) 28290: ALF-8840: user-*.atomentry.ftl 28291: ALF-6863: Continuation of fix by Arseny 28336: ALF-8768: Fixed typo in comment on wcm-bootstrap-context.xml 28363: Merged DEV to V3.4-BUG-FIX 28262: ALF-8847: WCM: OrphanReaper contention throws error after 39 retries. Checkin Comment: Use JobLockService to make sure that only one OrphanReaper job is working. Generate list of nodes that must be processed in OrphanReaper.doBatch() transaction. 28386: ALF-9100: Merged PATCHES/V3.4.1 to V3.4-BUG-FIX 28249: ALF-8946: Avoid one full table scan per batch in full reindex - Now each batch scans a single time sample, dynamically adjusted based on the number of transactions in the previous sample, always aiming for 1000 transactions per sample. 28394: Fixed ALF-9090: NPE during inter-cluster subsystem messaging - Bean ID is a List and might not be recognized on receiving machine - Log warning when bean ID is not available (unsymmetrical configuration, perhaps?) 28396: Merged DEV to V3.4-BUG-FIX 28384: ALF-6150: Initial state lost when non-versionable document is saved for the first time Creation of new version of document before writing its content was added to - AbstractAlfrescoMethodHandler->putDocument (this method is used by Office 2003, 2007) - VtiIfHeaderAction->doPut (this method is used by Office 2007 and 2010 on Windows 7) Creation of new version was added twice to AbstractAlfrescoMethodHandler to avoid affecting initial version when transaction is committed. 28432: Merged DEV to V3.4-BUG-FIX 28431: ALF-8530: Pressing the info icon creates an unrecorded file in the ContentStore Use ContentService.getTempWriter() in BaseContentNode$TemplateContentData.getContentAsText() method. 28435: Merged DEV/TEMPORARY to V3.4-BUG-FIX 28428: ALF-9015: cm:modifier not updated when document is updated via CIFS In ContentDiskDriver.closeFile() added ContentModel.PROP_MODIFIER property update. 28436: ALF-8550: Number of http requests (currentThreadsBusy) increases when session times out during creation of webform - Corrected use of read and write locks 28465: Fix for ALF-8023 Share preview doesn't work if... fixed as outlined by Dmitry. 28478: Merged BRANCHES/DEV/ALAN/AUDIT to BRANCHES/DEV/V3.4-BUG-FIX: 28062-28477 (28062,28063,28080,28081,28302,28303,28334,28340,28464,28469,28477) ALF-8438 Need higher level audit of user actions git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@28481 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261 --- config/alfresco/audit-services-context.xml | 25 +- config/alfresco/audit/alfresco-audit-3.2.xsd | 4 +- .../alfresco/audit/alfresco-audit-access.xml | 92 +++ .../authentication-services-context.xml | 55 +- config/alfresco/avm-services-context.xml | 9 +- .../discussion-messages_it.properties | 2 +- config/alfresco/public-services-context.xml | 4 +- config/alfresco/repository.properties | 8 + .../filesys/repo/ContentDiskDriver.java | 1 + .../repo/audit/AnnotationTestInterface.java | 3 +- .../repo/audit/AuditComponentImpl.java | 13 +- .../org/alfresco/repo/audit/AuditFilter.java | 42 + .../repo/audit/AuditMethodInterceptor.java | 45 +- ...iceIdentifier.java => BeanIdentifier.java} | 15 +- .../repo/audit/BeanIdentifierImpl.java | 119 +++ .../repo/audit/PropertyAuditFilter.java | 370 +++++++++ .../repo/audit/PropertyAuditFilterTest.java | 226 ++++++ .../audit/PublicServiceIdentifierImpl.java | 163 ---- .../repo/audit/access/AccessAuditor.java | 561 +++++++++++++ .../repo/audit/access/AccessAuditorTest.java | 587 ++++++++++++++ .../repo/audit/access/NodeChange.java | 735 ++++++++++++++++++ .../repo/audit/access/NodeChangeTest.java | 491 ++++++++++++ .../alfresco/repo/audit/access/NodeInfo.java | 107 +++ .../repo/audit/access/NodeInfoFactory.java | 121 +++ .../repo/audit/model/AuditApplication.java | 3 +- .../org/alfresco/repo/avm/AVMServiceImpl.java | 7 +- .../org/alfresco/repo/avm/AVMServiceTest.java | 34 +- .../org/alfresco/repo/avm/OrphanReaper.java | 121 ++- .../org/alfresco/repo/avm/PurgeTestP.java | 16 +- .../index/FullIndexRecoveryComponent.java | 84 +- .../repo/rendition/RenditionNodeManager.java | 22 +- .../repo/template/BaseContentNode.java | 2 +- .../org/alfresco/service/ServiceRegistry.java | 3 +- .../service/cmr/action/ActionService.java | 3 +- .../cmr/action/ActionTrackingService.java | 3 +- .../cmr/activities/ActivityService.java | 3 +- .../service/cmr/admin/RepoAdminService.java | 3 +- .../service/cmr/audit/AuditService.java | 3 +- .../cmr/avm/deploy/DeploymentService.java | 3 +- .../cmr/coci/CheckOutCheckInService.java | 3 +- .../service/cmr/email/EmailService.java | 3 +- .../cmr/invitation/InvitationService.java | 3 +- .../service/cmr/lock/LockService.java | 3 +- .../cmr/ml/ContentFilterLanguagesService.java | 3 +- .../service/cmr/ml/EditionService.java | 3 +- .../cmr/ml/MultilingualContentService.java | 3 +- .../service/cmr/model/FileFolderService.java | 1 - .../service/cmr/rating/RatingService.java | 1 - .../cmr/rendition/RenditionService.java | 3 +- .../cmr/replication/ReplicationService.java | 3 +- .../cmr/repository/ContentService.java | 3 +- .../service/cmr/repository/CopyService.java | 3 +- .../service/cmr/repository/ScriptService.java | 3 +- .../cmr/repository/TemplateService.java | 3 +- .../service/cmr/rule/RuleService.java | 3 +- .../service/cmr/search/CategoryService.java | 3 +- .../cmr/security/AuthenticationService.java | 3 +- .../cmr/security/AuthorityService.java | 1 - .../service/cmr/security/OwnableService.java | 3 +- .../service/cmr/security/PersonService.java | 1 - .../security/PublicServiceAccessService.java | 3 +- .../service/cmr/site/SiteService.java | 1 - .../subscriptions/SubscriptionService.java | 2 - .../service/cmr/tagging/TaggingService.java | 3 +- .../service/cmr/transfer/TransferService.java | 3 +- .../cmr/transfer/TransferService2.java | 3 +- .../cmr/usage/ContentUsageService.java | 3 +- .../service/cmr/usage/UsageService.java | 3 +- .../service/cmr/version/VersionService.java | 3 +- .../service/cmr/view/ExporterService.java | 3 +- .../service/cmr/view/ImporterService.java | 3 +- .../cmr/view/RepositoryExporterService.java | 3 +- .../service/cmr/workflow/WorkflowService.java | 3 +- .../service/license/LicenseService.java | 3 +- .../transaction/TransactionService.java | 3 +- .../org/alfresco/wcm/asset/AssetService.java | 3 +- .../alfresco/wcm/sandbox/SandboxService.java | 3 +- .../wcm/webproject/WebProjectService.java | 3 +- .../alfresco/site/user-joined.atomentry.ftl | 4 +- .../org/alfresco/site/user-left.atomentry.ftl | 4 +- .../site/user-role-changed.atomentry.ftl | 4 +- 81 files changed, 3808 insertions(+), 418 deletions(-) create mode 100644 config/alfresco/audit/alfresco-audit-access.xml create mode 100644 source/java/org/alfresco/repo/audit/AuditFilter.java rename source/java/org/alfresco/repo/audit/{PublicServiceIdentifier.java => BeanIdentifier.java} (63%) create mode 100644 source/java/org/alfresco/repo/audit/BeanIdentifierImpl.java create mode 100644 source/java/org/alfresco/repo/audit/PropertyAuditFilter.java create mode 100644 source/java/org/alfresco/repo/audit/PropertyAuditFilterTest.java delete mode 100644 source/java/org/alfresco/repo/audit/PublicServiceIdentifierImpl.java create mode 100644 source/java/org/alfresco/repo/audit/access/AccessAuditor.java create mode 100644 source/java/org/alfresco/repo/audit/access/AccessAuditorTest.java create mode 100644 source/java/org/alfresco/repo/audit/access/NodeChange.java create mode 100644 source/java/org/alfresco/repo/audit/access/NodeChangeTest.java create mode 100644 source/java/org/alfresco/repo/audit/access/NodeInfo.java create mode 100644 source/java/org/alfresco/repo/audit/access/NodeInfoFactory.java diff --git a/config/alfresco/audit-services-context.xml b/config/alfresco/audit-services-context.xml index 53b3dd62fa..6e9618fdc3 100644 --- a/config/alfresco/audit-services-context.xml +++ b/config/alfresco/audit-services-context.xml @@ -30,11 +30,32 @@ + + + + + + + + + + + + + + + + + + + + + - + @@ -74,7 +95,7 @@ - + diff --git a/config/alfresco/audit/alfresco-audit-3.2.xsd b/config/alfresco/audit/alfresco-audit-3.2.xsd index 5b8d205366..62d21e69be 100644 --- a/config/alfresco/audit/alfresco-audit-3.2.xsd +++ b/config/alfresco/audit/alfresco-audit-3.2.xsd @@ -120,7 +120,7 @@ - + @@ -128,7 +128,7 @@ - + diff --git a/config/alfresco/audit/alfresco-audit-access.xml b/config/alfresco/audit/alfresco-audit-access.xml new file mode 100644 index 0000000000..4da6bf464c --- /dev/null +++ b/config/alfresco/audit/alfresco-audit-access.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/alfresco/authentication-services-context.xml b/config/alfresco/authentication-services-context.xml index 453ce4b47b..d14e6bb2ce 100644 --- a/config/alfresco/authentication-services-context.xml +++ b/config/alfresco/authentication-services-context.xml @@ -542,32 +542,45 @@ - + + - - - + + + org.alfresco.repo.security.authentication.TicketComponent - - - - ${authentication.ticket.validDuration} + + + + + + + + + ${authentication.ticket.validDuration} + + + + ${authentication.ticket.ticketsExpire} + + + + false + + + + + + ${authentication.ticket.expiryMode} + + - - - ${authentication.ticket.ticketsExpire} - - - - false - - - - - - ${authentication.ticket.expiryMode} + + + + diff --git a/config/alfresco/avm-services-context.xml b/config/alfresco/avm-services-context.xml index 058a3c9a38..8977a758df 100644 --- a/config/alfresco/avm-services-context.xml +++ b/config/alfresco/avm-services-context.xml @@ -67,15 +67,12 @@ 50 - - 1000 - - - - + + + diff --git a/config/alfresco/messages/discussion-messages_it.properties b/config/alfresco/messages/discussion-messages_it.properties index 33b71e431e..81b6d11da2 100755 --- a/config/alfresco/messages/discussion-messages_it.properties +++ b/config/alfresco/messages/discussion-messages_it.properties @@ -1,3 +1,3 @@ # Discussion-related messages -discussion.discussion_for={0}discussione +discussion.discussion_for={0}discussione diff --git a/config/alfresco/public-services-context.xml b/config/alfresco/public-services-context.xml index da1729d3b2..90e2a213eb 100644 --- a/config/alfresco/public-services-context.xml +++ b/config/alfresco/public-services-context.xml @@ -31,8 +31,8 @@ - - + + diff --git a/config/alfresco/repository.properties b/config/alfresco/repository.properties index fa3d6aa0ac..a8258b6afe 100644 --- a/config/alfresco/repository.properties +++ b/config/alfresco/repository.properties @@ -323,10 +323,18 @@ db.pool.abandoned.log=false # Audit configuration audit.enabled=true audit.tagging.enabled=true +audit.alfresco-access.enabled=false +audit.alfresco-access.sub-events.enabled=false audit.cmischangelog.enabled=false audit.dod5015.enabled=false # Setting this flag to true will force startup failure when invalid audit configurations are detected audit.config.strict=false +# Audit map filter for AccessAuditor - restricts recorded events to user driven events +audit.filter.alfresco-access.default.enabled=true +audit.filter.alfresco-access.transaction.user=~System;~null;.* +audit.filter.alfresco-access.transaction.type=cm:folder;cm:content +audit.filter.alfresco-access.transaction.path=~/sys:archivedItem;~/ver:;.* + # System Configuration system.store=system://system diff --git a/source/java/org/alfresco/filesys/repo/ContentDiskDriver.java b/source/java/org/alfresco/filesys/repo/ContentDiskDriver.java index 142ac89dbe..24a29a59ce 100644 --- a/source/java/org/alfresco/filesys/repo/ContentDiskDriver.java +++ b/source/java/org/alfresco/filesys/repo/ContentDiskDriver.java @@ -2566,6 +2566,7 @@ public class ContentDiskDriver extends AlfrescoDiskDriver implements DiskInterfa if (permissionService.hasPermission((NodeRef) finalFileState.getFilesystemObject(), PermissionService.WRITE_PROPERTIES) == AccessStatus.ALLOWED) { + nodeService.setProperty(nodeRef, ContentModel.PROP_MODIFIER, authService.getCurrentUserName()); Date modifyDate = new Date(finalFileState.getModifyDateTime()); nodeService.setProperty(nodeRef, ContentModel.PROP_MODIFIED, modifyDate); diff --git a/source/java/org/alfresco/repo/audit/AnnotationTestInterface.java b/source/java/org/alfresco/repo/audit/AnnotationTestInterface.java index fb4cbbbf94..906ffc2720 100644 --- a/source/java/org/alfresco/repo/audit/AnnotationTestInterface.java +++ b/source/java/org/alfresco/repo/audit/AnnotationTestInterface.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2011 Alfresco Software Limited. * * This file is part of Alfresco * @@ -26,7 +26,6 @@ import org.alfresco.service.PublicService; * * @author Andy Hind */ -@PublicService public interface AnnotationTestInterface { @Auditable() diff --git a/source/java/org/alfresco/repo/audit/AuditComponentImpl.java b/source/java/org/alfresco/repo/audit/AuditComponentImpl.java index 5a6b3829f9..9c6d8af741 100644 --- a/source/java/org/alfresco/repo/audit/AuditComponentImpl.java +++ b/source/java/org/alfresco/repo/audit/AuditComponentImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2011 Alfresco Software Limited. * * This file is part of Alfresco * @@ -71,6 +71,7 @@ public class AuditComponentImpl implements AuditComponent private PropertyValueDAO propertyValueDAO; private AuditDAO auditDAO; private TransactionService transactionService; + private AuditFilter auditFilter; /** * Default constructor @@ -113,6 +114,14 @@ public class AuditComponentImpl implements AuditComponent { this.transactionService = transactionService; } + + /** + * Set the component used to filter which audit events to record + */ + public void setAuditFilter(AuditFilter auditFilter) + { + this.auditFilter = auditFilter; + } /** * {@inheritDoc} @@ -487,7 +496,7 @@ public class AuditComponentImpl implements AuditComponent ParameterCheck.mandatory("rootPath", rootPath); AuditApplication.checkPathFormat(rootPath); - if (values == null || values.isEmpty() || !areAuditValuesRequired()) + if (values == null || values.isEmpty() || !areAuditValuesRequired() || !auditFilter.accept(rootPath, values)) { return Collections.emptyMap(); } diff --git a/source/java/org/alfresco/repo/audit/AuditFilter.java b/source/java/org/alfresco/repo/audit/AuditFilter.java new file mode 100644 index 0000000000..2e093021e7 --- /dev/null +++ b/source/java/org/alfresco/repo/audit/AuditFilter.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2005-2011 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.audit; + +import java.io.Serializable; +import java.util.Map; + +import org.alfresco.repo.audit.model.AuditApplication; +import org.alfresco.repo.audit.model._3.AuditPath; + +/** + * Filter of audit map values before an audit record is written. + * + * @author Alan Davis + */ +public interface AuditFilter +{ + /** + * Returns {@code true} if the audit map values have not been discarded by audit filters. + * @param rootPath String a base path of {@link AuditPath} key entries concatenated with the + * path separator '/' ({@link AuditApplication#AUDIT_PATH_SEPARATOR}) + * @param auditMap Map of values to audit, mapped by {@link AuditPath} key relative to root path. + * @return {@code true} if the audit map values should be recorded. + */ + boolean accept(String rootPath, Map auditMap); +} \ No newline at end of file diff --git a/source/java/org/alfresco/repo/audit/AuditMethodInterceptor.java b/source/java/org/alfresco/repo/audit/AuditMethodInterceptor.java index dfd6c1dc2f..c61923da3a 100644 --- a/source/java/org/alfresco/repo/audit/AuditMethodInterceptor.java +++ b/source/java/org/alfresco/repo/audit/AuditMethodInterceptor.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2011 Alfresco Software Limited. * * This file is part of Alfresco * @@ -89,7 +89,7 @@ public class AuditMethodInterceptor implements MethodInterceptor private static final Log logger = LogFactory.getLog(AuditMethodInterceptor.class); - private PublicServiceIdentifier publicServiceIdentifier; + private BeanIdentifier beanIdentifier; private AuditComponent auditComponent; private TransactionService transactionService; @@ -112,9 +112,9 @@ public class AuditMethodInterceptor implements MethodInterceptor logger.warn("Property 'useNewConfig' is no longer used."); } - public void setPublicServiceIdentifier(PublicServiceIdentifier serviceIdentifier) + public void setBeanIdentifier(BeanIdentifier beanIdentifier) { - this.publicServiceIdentifier = serviceIdentifier; + this.beanIdentifier = beanIdentifier; } public void setAuditComponent(AuditComponent auditComponent) @@ -185,7 +185,7 @@ public class AuditMethodInterceptor implements MethodInterceptor Object[] args = mi.getArguments(); Map namedArguments = getInvocationArguments(auditableDef, args); // Get the service name - String serviceName = publicServiceIdentifier.getPublicServiceName(mi); + String serviceName = beanIdentifier.getBeanName(mi); if (serviceName == null) { // Not a public service @@ -338,10 +338,22 @@ public class AuditMethodInterceptor implements MethodInterceptor final String methodName, final Map namedArguments) { - final String rootPath = AuditApplication.buildPath(AUDIT_PATH_API_PRE, serviceName, methodName, AUDIT_SNIPPET_ARGS); + String rootPath; + Map auditData; + if (namedArguments == null || namedArguments.isEmpty()) + { + rootPath = AuditApplication.buildPath(AUDIT_PATH_API_PRE, serviceName, methodName); + auditData = new HashMap(1); + auditData.put(AUDIT_SNIPPET_ARGS, null); + } + else + { + rootPath = AuditApplication.buildPath(AUDIT_PATH_API_PRE, serviceName, methodName, AUDIT_SNIPPET_ARGS); + auditData = namedArguments; + } // Audit in a read-write txn - Map auditedData = auditComponent.recordAuditValues(rootPath, namedArguments); + Map auditedData = auditComponent.recordAuditValues(rootPath, auditData); // Done if (logger.isDebugEnabled() && auditedData.size() > 0) { @@ -369,13 +381,20 @@ public class AuditMethodInterceptor implements MethodInterceptor final String rootPath = AuditApplication.buildPath(AUDIT_PATH_API_POST, serviceName, methodName); final Map auditData = new HashMap(23); - for (Map.Entry entry : namedArguments.entrySet()) + if (namedArguments.isEmpty()) { - String argName = entry.getKey(); - Serializable argValue = entry.getValue(); - auditData.put( - AuditApplication.buildPath(AUDIT_SNIPPET_ARGS, argName), - argValue); + auditData.put(AUDIT_SNIPPET_ARGS, null); + } + else + { + for (Map.Entry entry : namedArguments.entrySet()) + { + String argName = entry.getKey(); + Serializable argValue = entry.getValue(); + auditData.put( + AuditApplication.buildPath(AUDIT_SNIPPET_ARGS, argName), + argValue); + } } if (ret != null) { diff --git a/source/java/org/alfresco/repo/audit/PublicServiceIdentifier.java b/source/java/org/alfresco/repo/audit/BeanIdentifier.java similarity index 63% rename from source/java/org/alfresco/repo/audit/PublicServiceIdentifier.java rename to source/java/org/alfresco/repo/audit/BeanIdentifier.java index cb0d5592e1..5c6956c575 100644 --- a/source/java/org/alfresco/repo/audit/PublicServiceIdentifier.java +++ b/source/java/org/alfresco/repo/audit/BeanIdentifier.java @@ -21,18 +21,19 @@ package org.alfresco.repo.audit; import org.aopalliance.intercept.MethodInvocation; /** - * This defines the API to identify the public service upon which a method invocation has been made. + * Identify a bean upon which a method invocation has been made. Originally + * this was only public services but has been relaxed to be any bean. * - * @author Andy Hind + * @author Andy Hind, David Ward, Alan Davis */ -public interface PublicServiceIdentifier +public interface BeanIdentifier { /** - * Get the name of the public service for the method invocation. + * Get the name of the bean (normally a service) for the method invocation. * * @param mi the method invocation - * @return Returns the name of the public service or null if it is - * not recognized as a public service + * @return Returns the name of the bean or null if it is + * not recognized */ - public String getPublicServiceName(MethodInvocation mi); + public String getBeanName(MethodInvocation mi); } diff --git a/source/java/org/alfresco/repo/audit/BeanIdentifierImpl.java b/source/java/org/alfresco/repo/audit/BeanIdentifierImpl.java new file mode 100644 index 0000000000..8f8db97206 --- /dev/null +++ b/source/java/org/alfresco/repo/audit/BeanIdentifierImpl.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2005-2011 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.audit; + +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import org.alfresco.service.Auditable; +import org.aopalliance.intercept.MethodInvocation; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.aop.ProxyMethodInvocation; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.ListableBeanFactory; + +/** + * Lookup the name of a bean that is being audited by {@link AuditMethodInterceptor}. + *

+ * Originally used to look up public services annotated with {@code @PublicService}, + * but has now been relaxed to be any bean that uses a proxy. For the method to be + * audited it still needs to be annotated with {@code @Auditable}. + * + * @author Andy Hind, David Ward, Alan Davis + */ +public class BeanIdentifierImpl implements BeanIdentifier, BeanFactoryAware +{ + private static Log s_logger = LogFactory.getLog(BeanIdentifierImpl.class); + private static ThreadLocal> methodToBeanMap = + new ThreadLocal>(); + + private ListableBeanFactory beanFactory; + + public BeanIdentifierImpl() + { + super(); + } + + public void setBeanFactory(BeanFactory beanFactory) throws BeansException + { + this.beanFactory = (ListableBeanFactory)beanFactory; + } + + /** + * {@inheritDoc} + * Cache service name look up. + */ + public String getBeanName(MethodInvocation mi) + { + return getName(mi); + } + + private String getName(MethodInvocation mi) throws BeansException + { + if (methodToBeanMap.get() == null) + { + methodToBeanMap.set(new HashMap()); + } + Method method = mi.getMethod(); + String name = methodToBeanMap.get().get(method); + if (name == null) + { + name = getBeanNameImpl(mi); + methodToBeanMap.get().put(method, name); + } + else + { + if (s_logger.isDebugEnabled()) + { + s_logger.debug("Cached look up for " + name + "." + method.getName()); + } + } + return name; + } + + /** + * Do the look up by interface type. + * + * @return Returns the name of the service or null if not found + */ + private String getBeanNameImpl(MethodInvocation mi) throws BeansException + { + if (mi instanceof ProxyMethodInvocation) + { + Object proxy = ((ProxyMethodInvocation) mi).getProxy(); + Map beans = beanFactory.getBeansOfType(proxy.getClass()); + Iterator iter = beans.entrySet().iterator(); + while (iter.hasNext()) + { + Map.Entry entry = (Map.Entry) iter.next(); + String name = (String) entry.getKey(); + if (proxy == entry.getValue() && !name.equals("DescriptorService")) + { + return name; + } + } + } + return null; + } +} diff --git a/source/java/org/alfresco/repo/audit/PropertyAuditFilter.java b/source/java/org/alfresco/repo/audit/PropertyAuditFilter.java new file mode 100644 index 0000000000..b4a90f14f1 --- /dev/null +++ b/source/java/org/alfresco/repo/audit/PropertyAuditFilter.java @@ -0,0 +1,370 @@ +/* + * Copyright (C) 2005-2011 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.audit; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.WeakHashMap; +import java.util.regex.Pattern; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Filter using property file values to accept or reject audit map values.

+ * + * The last component in the {@code rootPath} is considered to be the event + * action. The keys in an audit map identify each audit value. Properties may be + * defined to accept or reject each value. If any value in an audit map is + * rejected, the whole map is rejected. So that one does not have to define + * too many properties, a 'default' event action property may be defined. This + * will be inherited by all actions unless a property is defined for a particular + * event action. For example: + *

+ *   audit.filter.alfresco-access.default.enabled=true
+ *   audit.filter.alfresco-access.default.user=~System;.*
+ *   audit.filter.alfresco-access.default.type=cm:folder;cm:content;st:site
+ *   audit.filter.alfresco-access.default.path=/app:company_home/.*
+ *   audit.filter.alfresco-access.transaction.user=
+ *   audit.filter.alfresco-access.login.user=jblogs
+ *   ...
+ * 
+ * + * Each property value defines a list of regular expressions that will be used + * to match the actual audit map values. In the above example, events created + * by any user except for the internal user 'System' will be recorded by default + * for all event actions. However the property for the 'transaction' event action + * overrides this to record even 'System' events.

+ * + * For any filters to be applied to an event action, that action's filters must be + * enabled with an 'enabled' property set to {@code "true"}. However this may + * also be done by using the 'default' event action, as shown above.

+ * + * Note: Property names have a {@code "audit.filter."} prefix and use {@code '.'} + * as a separator where as components of rootPath and keys in the audit map use + * {@code '/'}. The following is an example rootPath and audit map which could be + * used with the corresponding property names shown above: + * + *

+ *     rootPath                       auditMap
+ *     "/alfresco-access/transaction" "user" => "System"
+ *                                    "path" => "/app:company_home/st:sites/cm:mysite/cm:documentLibrary/cm:folder1"
+ *                                    "type" => "cm:folder"
+ *                                    "node" => ...
+ * 
+ * + * Lists are evaluated from left to right allowing one flexibility to accept or + * reject different combinations of values. If no match is made by the end of the + * list the value is rejected. If there is not a property for a given value or + * an empty list is defined (as above for the user value on a transaction action) + * any value is accepted.

+ * + * Each regular expression in the list is separated by a {@code ';'}. Expressions + * that include a {@code ';'} may be escaped using a {@code '\'}. An expression + * that starts with a {@code '~'} indicates that any matching value should be + * rejected. If the first character of an expression needs to be a {@code '~'} it + * too may be escaped with a {@code '\'}.

+ * + * A property value may be a reference to another property, which saves having + * multiple copies. This is indicated by a {@code '$' as the first character of the + * property value. If the first character of an expression needs to be a + * {@code '$'} it too may be escaped with a {@code '\'}. For example: + *

+ *   audit.filter.alfresco-access.default.type=cm:folder;cm:content
+ *   audit.filter.alfresco-access.moveNode.from.type=$audit.filter.alfresco-access.default.type
+ * 
+ * + * @author Alan Davis + */ +public class PropertyAuditFilter implements AuditFilter +{ + private static Log logger = LogFactory.getLog(PropertyAuditFilter.class); + + private static final char NOT = '~'; + private static final char REDIRECT = '$'; + private static final String REG_EXP_SEPARATOR = ";"; + private static final char PROPERTY_SEPARATOR = '.'; + private static final String PROPERY_NAME_PREFIX = "audit.filter"; + private static final char ESCAPE = '\\'; + + private static final String ESCAPED_REDIRECT = ""+ESCAPE+REDIRECT; + private static final String ESCAPED_REG_EXP_SEPARATOR = ""+ESCAPE+REG_EXP_SEPARATOR; + private static final String ESCAPED_NOT = ""+ESCAPE+NOT; + + private static final String ENABLED = "enabled"; + private static final String DEFAULT = "default"; + + /** + * Cache of {@code Patterns} for performance. + */ + static Map patternCache = + Collections.synchronizedMap(new WeakHashMap()); + + /** + * Properties to drive the filter. + */ + Properties properties; + + /** + * Set the properties object holding filter configuration + * @since 3.2 + */ + public void setProperties(Properties properties) + { + this.properties = properties; + } + + /** + * @inheritDoc + * @param @inheritDoc + * @param @inheritDoc + * @return @inheritDoc + */ + @Override + public boolean accept(String rootPath, Map auditMap) + { + String[] root = splitPath(rootPath); + String rootProperty = getPropertyName(PROPERY_NAME_PREFIX, getPropertyName(root)); + String defaultRootProperty = getDefaultRootProperty(root); + + if ("true".equalsIgnoreCase(getProperty(rootProperty, defaultRootProperty, ENABLED))) + { + for (Map.Entry entry : auditMap.entrySet()) + { + Serializable value = entry.getValue(); + if (value == null) + { + value = "null"; + } + String stringValue = (value instanceof String) ? (String)value : value.toString(); + String[] key = splitPath(entry.getKey()); + String propertyValue = getProperty(rootProperty, defaultRootProperty, key); + if (!acceptValue(stringValue, propertyValue, rootProperty, key)) + { + if (logger.isDebugEnabled()) + { + logger.debug("Rejected \n\t "+rootPath+'/'+entry.getKey()+"="+stringValue+ + "\n\t"+getPropertyName(rootProperty, getPropertyName(key))+"="+propertyValue); + } + return false; + } + } + } + + return true; + } + + /** + * Checks a single value against a list of regular expressions. + */ + private boolean acceptValue(String value, String regExpValue, String rootProperty, String... key) + { + // If no property or zero length it matches. + if (regExpValue == null || regExpValue.length() == 0) + { + return true; + } + + for (String regExp: getRegExpList(regExpValue, rootProperty, key)) + { + boolean includeExp = regExp.charAt(0) != NOT; + if (!includeExp || regExp.startsWith(ESCAPED_NOT)) + { + regExp = regExp.substring(1); + } + if (getPattern(regExp).matcher(value).matches()) + { + return includeExp; + } + } + + return false; + } + + private Pattern getPattern(String regExp) + { + Pattern pattern = patternCache.get(regExp); + if (pattern == null) + { + pattern = Pattern.compile(regExp); + patternCache.put(regExp, pattern); + } + return pattern; + } + + /** + * @return the root property name for the default event action. + */ + private String getDefaultRootProperty(String[] root) + { + String action = root[root.length-1]; + root[root.length-1] = DEFAULT; + String defaultRootProperty = getPropertyName(PROPERY_NAME_PREFIX, getPropertyName(root)); + root[root.length-1] = action; + return defaultRootProperty; + } + + /** + * @return the value of the property {@code rootProperty+'.'+getPropertyName(keyComponents)} + * defaulting to {@code defaultRootProperty+'.'+getPropertyName(keyComponents)}. + */ + private String getProperty(String rootProperty, String defaultRootProperty, String... keyComponents) + { + String keyName = getPropertyName(keyComponents); + String propertyName = getPropertyName(rootProperty, keyName); + String value = getProperty(null, propertyName); + if (value == null) + { + value = getProperty(null, getPropertyName(defaultRootProperty, keyName)); + } + return value; + } + + /** + * @return a property value, including redirected values (where the value + * of a property starts with a {@code '$'} indicating it is another property + * name). + * @throws IllegalArgumentException if redirecting properties reference themselves. + */ + private String getProperty(List loopCheck, String propertyName) + { + String value = properties.getProperty(propertyName); + + // Handle redirection of properties. + if (value != null && value.length() > 0 && value.charAt(0) == REDIRECT) + { + String newPropertyName = value.substring(1); + if (loopCheck == null) + { + loopCheck = new ArrayList(); + } + if (loopCheck.contains(newPropertyName)) + { + RuntimeException e = new IllegalArgumentException("Redirected property "+ + newPropertyName+" referes back to itself."); + logger.error("Error found in properties for audit filter.", e); + throw e; + } + loopCheck.add(propertyName); + value = getProperty(loopCheck, newPropertyName); + } + else if (value == null && loopCheck != null && !loopCheck.isEmpty()) + { + RuntimeException e = new IllegalArgumentException("Redirected property "+ + loopCheck.get(loopCheck.size()-1)+ + " points to "+propertyName+" but it does not exist."); + logger.error("Error found in properties for audit filter.", e); + throw e; + } + + return value; + } + + /** + * Returns a List of regular expressions from a property's String value. + * A leading {@code '~'} indicating the regular expression should be used + * to reject values. This may be escaped with a leading back slash + * ({@code "\\~"}) if the first character must be a semicolon. Other + * escape characters are removed. A check is made that no expression is + * zero length. + * @return a List of regular expressions. + * @throws IllegalArgumentException if there are any zero length expressions. + */ + private List getRegExpList(String value, String rootProperty, String... key) + { + // Split the value into substrings separated by ';'. This may be escaped using "\;". + List regExpList = new ArrayList(); + { + int j = 0; + int i = j - 1; + do + { + i = value.indexOf(';', i+1); + if (i != -1) + { + if (i == 0 || value.charAt(i-1) != '\\') + { + regExpList.add(value.substring(j, i)); + j = i + 1; + } + } + } + while (i != -1); + if (j < value.length()-1) + { + regExpList.add(value.substring(j)); + } + } + + // Remove escape characters other than the NOT (\~) + // \$ at the start becomes "$" + // \; anywhere becomes ";" + for (int i=regExpList.size()-1; i >= 0; i--) + { + String regExp = regExpList.get(i); + if (regExp.startsWith(ESCAPED_REDIRECT)) + { + regExp = regExp.substring(1); + } + regExp = regExp.replaceAll(ESCAPED_REG_EXP_SEPARATOR, REG_EXP_SEPARATOR); + + if (regExp.length() == 0 || (regExp.charAt(0) == NOT && regExp.length() == 1)) + { + throw new IllegalArgumentException(getPropertyName(rootProperty, getPropertyName(key))+"="+value+ + "includes an empty regular expression."); + } + regExpList.set(i, regExp); + } + return regExpList; + } + + /** + * @return a property name from the supplied components. Each component is + * separated by a {@code '.'}. + */ + private String getPropertyName(String... components) + { + StringBuilder sb = new StringBuilder(); + for (String component: components) + { + if (sb.length() > 0) + { + sb.append(PROPERTY_SEPARATOR); + } + sb.append(component); + } + return sb.toString(); + } + + /** + * @return a list of components separated by '/' characters. + */ + private String[] splitPath(String path) + { + if (path.length() > 0 && path.charAt(0) == '/') + { + path = path.substring(1); + } + return path.split("/"); + } +} diff --git a/source/java/org/alfresco/repo/audit/PropertyAuditFilterTest.java b/source/java/org/alfresco/repo/audit/PropertyAuditFilterTest.java new file mode 100644 index 0000000000..c310995a76 --- /dev/null +++ b/source/java/org/alfresco/repo/audit/PropertyAuditFilterTest.java @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2005-2011 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.audit; + +import static org.junit.Assert.*; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +/** + * A low level unit test of the filter on audit maps. + * + * @author Alan Davis + */ +public class PropertyAuditFilterTest +{ + private PropertyAuditFilter filter; + private Properties properties; + + private String rootPath; + private Map auditMap; + + @Before + public void setUp() throws Exception + { + filter = new PropertyAuditFilter(); + properties = new Properties(); + filter.setProperties(properties); + + rootPath = "root/action"; + auditMap = new HashMap(); + auditMap.put("name", "value"); + } + + @Test + public void testNoFilterIfNoProperties() + { + boolean actual = filter.accept(rootPath, auditMap); + assertTrue("Filter should only run if properties are set.", actual); + } + + @Test + public void testNoRegexOnValue() + { + properties.put("audit.filter.root.action.enabled", "true"); + + boolean actual = filter.accept(rootPath, auditMap); + assertTrue("Value should have been accepted.", actual); + } + + @Test + public void testRegexOnValue() + { + properties.put("audit.filter.root.action.enabled", "true"); + properties.put("audit.filter.root.action.name", "value"); + + boolean actual = filter.accept(rootPath, auditMap); + assertTrue("Value should have been accepted.", actual); + } + + @Test + public void testRegexOnBadValue() + { + properties.put("audit.filter.root.action.enabled", "true"); + properties.put("audit.filter.root.action.name", "~value"); + + boolean actual = filter.accept(rootPath, auditMap); + assertFalse("Value should have been rejected.", actual); + } + + @Test + public void testNullValue() + { + auditMap.put("name", null); + + properties.put("audit.filter.root.action.enabled", "true"); + properties.put("audit.filter.root.action.name", "null"); + + boolean actual = filter.accept(rootPath, auditMap); + assertTrue("A null value should match null", actual); + } + + @Test + public void testNullStringValue() + { + auditMap.put("name", "null"); + + properties.put("audit.filter.root.action.enabled", "true"); + properties.put("audit.filter.root.action.name", "null"); + + boolean actual = filter.accept(rootPath, auditMap); + assertTrue("A null value should match null", actual); + } + + @Test + public void testNonStringValue() + { + LinkedHashSet value = new LinkedHashSet(); + value.add(Integer.valueOf(1)); + value.add(Integer.valueOf(2)); + value.add(Integer.valueOf(3)); + auditMap.put("name", value); + + properties.put("audit.filter.root.action.enabled", "true"); + properties.put("audit.filter.root.action.name", "\\[1, 2, 3\\]"); + + boolean actual = filter.accept(rootPath, auditMap); + assertTrue("The check should have worked on the value.toString().", actual); + } + + @Test + public void testZeroLengthRegex() + { + properties.put("audit.filter.root.action.enabled", "true"); + properties.put("audit.filter.root.action.name", ""); + + boolean actual = filter.accept(rootPath, auditMap); + assertTrue("Should match any values just like having no property", actual); + } + + @Test + public void testDefaultActionUsedAsFallback() + { + properties.put("audit.filter.root.default.enabled", "true"); + properties.put("audit.filter.root.default.name", "~value"); + + boolean actual = filter.accept(rootPath, auditMap); + assertFalse("The 'default' fallback action should have been used to " + + "enable the filter and reject the value.", actual); + } + + @Test + public void testRedirect() + { + properties.put("audit.filter.root.action.enabled", "true"); + properties.put("audit.filter.root.action.name", "$anotherProperty"); + properties.put("anotherProperty", "$theFinalProperty"); + properties.put("theFinalProperty", "~value"); + + boolean actual = filter.accept(rootPath, auditMap); + assertFalse("Redirected properties should have rejected the value.", actual); + } + + @Test + public void testMultipleRegExp() + { + properties.put("audit.filter.root.action.enabled", "true"); + properties.put("audit.filter.root.action.name", "beGood;~b.*;.*"); + + auditMap.put("name", "beGood"); + assertTrue("Should match 1st regex", filter.accept(rootPath, auditMap)); + + auditMap.put("name", "bad"); + assertFalse("Should match 2nd regex", filter.accept(rootPath, auditMap)); + + auditMap.put("name", "value"); + assertTrue("Should match 3rd regex", filter.accept(rootPath, auditMap)); + } + + @Test + public void testMultipleRegExpWithNoCatchAll() + { + properties.put("audit.filter.root.action.enabled", "true"); + properties.put("audit.filter.root.action.name", "beGood;~b.*"); + + auditMap.put("name", "value"); + assertFalse("Should match nothing", filter.accept(rootPath, auditMap)); + } + + @Test + public void testEscapedSemicolon() + { + properties.put("audit.filter.root.action.enabled", "true"); + properties.put("audit.filter.root.action.name", "value\\\\;value"); + + auditMap.put("name", "value\\;value"); + assertTrue("Should match 1st regex", filter.accept(rootPath, auditMap)); + } + + @Test + public void testEscapedRedirect() + { + properties.put("audit.filter.root.action.enabled", "true"); + properties.put("audit.filter.root.action.name", "\\$"); + + auditMap.put("name", ""); + assertTrue("Should match only zero length values", filter.accept(rootPath, auditMap)); + } + + @Test + public void testEscapedNot() + { + properties.put("audit.filter.root.action.enabled", "true"); + properties.put("audit.filter.root.action.name", "\\~.*"); + + auditMap.put("name", "~good"); + assertTrue("Should match any value starting with '~'.", filter.accept(rootPath, auditMap)); + } +} diff --git a/source/java/org/alfresco/repo/audit/PublicServiceIdentifierImpl.java b/source/java/org/alfresco/repo/audit/PublicServiceIdentifierImpl.java deleted file mode 100644 index f26100c7fe..0000000000 --- a/source/java/org/alfresco/repo/audit/PublicServiceIdentifierImpl.java +++ /dev/null @@ -1,163 +0,0 @@ -/* - * Copyright (C) 2005-2010 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.audit; - -import java.lang.reflect.Method; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; - -import org.alfresco.service.PublicService; -import org.aopalliance.intercept.MethodInvocation; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.BeanFactory; -import org.springframework.beans.factory.BeanFactoryAware; -import org.springframework.beans.factory.ListableBeanFactory; - -/** - * Identify public services by method invocation. Look ups are cached on a thread local as they are quite expensive. - * All public service names end with "Service" and start with capital letter. - * This pattern is used to filter bean names. TODO: Look at pulling out all the mappings at start up. - * - * @author Andy Hind - */ -public class PublicServiceIdentifierImpl implements PublicServiceIdentifier, BeanFactoryAware -{ - private static Log s_logger = LogFactory.getLog(PublicServiceIdentifierImpl.class); - private static ThreadLocal> methodToServiceMap = new ThreadLocal>(); - - private ListableBeanFactory beanFactory; - - public PublicServiceIdentifierImpl() - { - super(); - } - - public void setBeanFactory(BeanFactory beanFactory) throws BeansException - { - this.beanFactory = (ListableBeanFactory)beanFactory; - } - - public String getPublicServiceName(MethodInvocation mi) - { - return getServiceName(mi); - } - - /** - * {@inheritDoc} - * Cache service name look up. - */ - private String getServiceName(MethodInvocation mi) throws BeansException - { - if (methodToServiceMap.get() == null) - { - methodToServiceMap.set(new HashMap()); - } - Method method = mi.getMethod(); - String serviceName = methodToServiceMap.get().get(method); - if (serviceName == null) - { - serviceName = getServiceNameImpl(mi); - methodToServiceMap.get().put(method, serviceName); - } - else - { - if (s_logger.isDebugEnabled()) - { - s_logger.debug("Cached look up for " + serviceName + "." + method.getName()); - } - } - return serviceName; - } - - /** - * Do the look up by interface type. - * - * @return Returns the name of the service or null if not found - */ - @SuppressWarnings("unchecked") - private String getServiceNameImpl(MethodInvocation mi) throws BeansException - { - Class clazz = mi.getThis().getClass(); - while (clazz != null) - { - Class[] interfaces = clazz.getInterfaces(); - for (Class iFace : interfaces) - { - Class publicServiceInterface = findPublicService(iFace); - if (publicServiceInterface != null) - { - Map beans = beanFactory.getBeansOfType(publicServiceInterface); - Iterator iter = beans.entrySet().iterator(); - while (iter.hasNext()) - { - Map.Entry entry = (Map.Entry) iter.next(); - String serviceName = (String) entry.getKey(); - if ((serviceName.endsWith("Service")) - && (Character.isUpperCase(serviceName.charAt(0))) - && !serviceName.equals("DescriptorService")) - { - return serviceName; - } - } - } - - } - clazz = clazz.getSuperclass(); - } - return null; - } - - /** - * We use a marker annotation to identify public interfaces. - * The interfaces have to be walked to determine if a public interface is implemented. - * - * Only one public service interface is expected. - * - * @param clazz - * @return - */ - @SuppressWarnings("unchecked") - private Class findPublicService(Class clazz) - { - if (!clazz.isInterface()) - { - return null; - } - - if (clazz.isAnnotationPresent(PublicService.class)) - { - return clazz; - } - - Class[] classes = clazz.getInterfaces(); - for(Class implemented: classes) - { - Class answer = findPublicService(implemented); - if(answer != null) - { - return answer; - } - } - return null; - - } -} diff --git a/source/java/org/alfresco/repo/audit/access/AccessAuditor.java b/source/java/org/alfresco/repo/audit/access/AccessAuditor.java new file mode 100644 index 0000000000..ff9b086b2b --- /dev/null +++ b/source/java/org/alfresco/repo/audit/access/AccessAuditor.java @@ -0,0 +1,561 @@ +/* + * Copyright (C) 2005-2011 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.audit.access; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.TreeSet; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.audit.AuditComponent; +import org.alfresco.repo.audit.model.AuditApplication; +import org.alfresco.repo.coci.CheckOutCheckInServicePolicies.OnCancelCheckOut; +import org.alfresco.repo.coci.CheckOutCheckInServicePolicies.OnCheckIn; +import org.alfresco.repo.coci.CheckOutCheckInServicePolicies.OnCheckOut; +import org.alfresco.repo.content.ContentServicePolicies.OnContentReadPolicy; +import org.alfresco.repo.content.ContentServicePolicies.OnContentUpdatePolicy; +import org.alfresco.repo.copy.CopyServicePolicies.OnCopyCompletePolicy; +import org.alfresco.repo.node.NodeServicePolicies.BeforeDeleteNodePolicy; +import org.alfresco.repo.node.NodeServicePolicies.OnAddAspectPolicy; +import org.alfresco.repo.node.NodeServicePolicies.OnCreateNodePolicy; +import org.alfresco.repo.node.NodeServicePolicies.OnMoveNodePolicy; +import org.alfresco.repo.node.NodeServicePolicies.OnRemoveAspectPolicy; +import org.alfresco.repo.node.NodeServicePolicies.OnUpdatePropertiesPolicy; +import org.alfresco.repo.policy.JavaBehaviour; +import org.alfresco.repo.policy.PolicyComponent; +import org.alfresco.repo.policy.PolicyScope; +import org.alfresco.repo.transaction.AlfrescoTransactionSupport; +import org.alfresco.repo.transaction.TransactionListener; +import org.alfresco.repo.transaction.TransactionListenerAdapter; +import org.alfresco.repo.transaction.TransactionalResourceHelper; +import org.alfresco.repo.version.VersionServicePolicies.OnCreateVersionPolicy; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.PropertyCheck; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.InitializingBean; + +/** + * Creates high level audit records on the creation, deletion, modification and access + * of content and folders. Lower level events are grouped together by transaction + * and node.

+ * + * To turn on auditing of these events and sub events add the following property to + * alfresco-global.properties: + *

+ *    # Enable audit in general
+ *    audit.enabled=true
+ *
+ *    # Enable the alfresco-access audit application
+ *    audit.alfresco-access.enabled=true
+ *
+ *    # Enable the auditing of sub-actions. Normally disabled as these values are
+ *    # not normally needed by audit configurations, but may be useful to developers
+ *    audit.alfresco-access.sub-actions.enabled=true
+ * 
+ * + * The following properties are set by default to discard events where the user is + * 'null' or 'System', the node path is '/sys:archivedItem' or under '/ver:' or + * the node type is not 'cm:folder' or 'cm:content'. These values result in events + * only being recorded if they are initiated by users of the system. These vales may + * be overridden if required. + *
+ *    audit.filter.alfresco-access.default.enabled=true
+ *    audit.filter.alfresco-access.transaction.user=~System;~null;.*
+ *    audit.filter.alfresco-access.transaction.type=cm:folder;cm:content
+ *    audit.filter.alfresco-access.transaction.path=~/sys:archivedItem;~/ver:;.*
+ * 
+ * + * Node and Content changes generate the following audit structure. Elements are omitted + * if not changed by the transaction. The {@code /sub-action/} structure holds + * cut down details of each sub-action, but are only included if the global property + * {@code audit.alfresco-access.sub-actions.enabled=true}. + *
+ *    /alfresco-access
+ *     /transaction
+ *       /action=<actionName>
+ *       /sub-actions=<sub action list>
+ *       /path=<prefixPath>
+ *       /type=<prefixType>
+ *       /node=<nodeRef>
+ *       /user=<user>
+ *       /copy
+ *         /from
+ *           /node=<nodeRef>
+ *           /path=<prefixPath>
+ *           /type=<prefixType>
+ *       /move
+ *         /from
+ *           /node=<nodeRef>
+ *           /path=<prefixPath>
+ *           /type=<prefixType>
+ *       /properties
+ *          /from=<mapOfValues>
+ *            /<propertyName>=<propertyValue>
+ *          /to=<mapOfValues>
+ *            /<propertyName>=<propertyValue>
+ *          /add=<mapOfValues>
+ *            /<propertyName>=<propertyValue>
+ *          /delete=<mapOfValues>
+ *            /<propertyName>=<propertyValue>
+ *        /aspects
+ *          /add=<mapOfNames>
+ *            /<aspectName>=null
+ *          /delete=<mapOfNames>
+ *            /<aspectName>=null
+ *        /version-properties=<mapOfValues>
+ *        /sub-action/<sequence>
+ *          /action=<actionName>
+ *          /move
+ *            ...
+ *          /properties
+ *            ...
+ *          /aspects
+ *            ...
+ *            
+ *  Example data:
+ *    /alfresco-access/transaction/action=MOVE
+ *    /alfresco-access/transaction/node=workspace://SpacesStore/74a5985a-45dd-4698-82db-8eaeff9df8d7
+ *    /alfresco-access/transaction/move/from/node=workspace://SpacesStore/d8a0dfd8-fe45-47da-acc2-fd8df9ea2b2e
+ *    /alfresco-access/transaction/move/from/path=/app:company_home/st:sites/cm:abc/cm:documentLibrary/cm:folder1/cm:Word 123.docx
+ *    /alfresco-access/transaction/move/from/type=cm:folder
+ *    /alfresco-access/transaction/path=/app:company_home/st:sites/cm:abc/cm:documentLibrary/cm:folder2/cm:Word 123.docx
+ *    /alfresco-access/transaction/sub-actions=moveNode readContent
+ *    /alfresco-access/transaction/type=cm:content
+ *    /alfresco-access/transaction/user=admin
+ *    /alfresco-access/transaction/sub-action/00/action=moveNode
+ *    /alfresco-access/transaction/sub-action/00/move/from/node=workspace://SpacesStore/d8a0dfd8-fe45-47da-acc2-fd8df9ea2b2e
+ *    /alfresco-access/transaction/sub-action/00/move/from/path=/app:company_home/st:sites/cm:abc/cm:documentLibrary/cm:folder1/cm:Word 123.docx
+ *    /alfresco-access/transaction/sub-action/00/move/from/type=cm:folder
+ *    /alfresco-access/transaction/sub-action/01/action=readContent
+ * 
+ * + * @author Alan Davis + */ +public class AccessAuditor implements InitializingBean, + + BeforeDeleteNodePolicy, OnAddAspectPolicy, OnCreateNodePolicy, OnMoveNodePolicy, + OnRemoveAspectPolicy, OnUpdatePropertiesPolicy, + + OnContentReadPolicy, OnContentUpdatePolicy, + + OnCreateVersionPolicy, + + OnCopyCompletePolicy, + + OnCheckOut, OnCheckIn, OnCancelCheckOut +{ + /** Logger */ + private static Log logger = LogFactory.getLog(AccessAuditor.class); + + private static final String ROOT_PATH = "/alfresco-access"; + private static final String TRANSACTION = "transaction"; + private static final String AUDIT_SUB_ACTIONS = "audit.alfresco-access.sub-actions.enabled"; + + private Properties properties; + private PolicyComponent policyComponent; + private AuditComponent auditComponent; + private TransactionService transactionService; + private NodeInfoFactory nodeInfoFactory; + private NamespaceService namespaceService; + + private TransactionListener transactionListener = new AccessTransactionListener(); + private boolean auditSubActions = false; + + /** + * Set the properties object holding filter configuration + * @since 3.2 + */ + public void setProperties(Properties properties) + { + this.properties = properties; + auditSubActions = properties.getProperty(AUDIT_SUB_ACTIONS, "false").equalsIgnoreCase("true"); + } + + /** + * Set the component used to bind to behaviour callbacks + */ + public void setPolicyComponent(PolicyComponent policyComponent) + { + this.policyComponent = policyComponent; + } + + /** + * The component to create audit events + */ + public void setAuditComponent(AuditComponent auditComponent) + { + this.auditComponent = auditComponent; + } + + /** + * Set the component used to start new transactions + */ + public void setTransactionService(TransactionService transactionService) + { + this.transactionService = transactionService; + } + + /** + * Set the component used to resolve namespaces. + */ + public void setNamespaceService(NamespaceService namespaceService) + { + this.namespaceService = namespaceService; + } + + /** + * Set the component used to create {@link NodeInfo} objects. + */ + public void setNodeInfoFactory(NodeInfoFactory nodeInfoFactory) + { + this.nodeInfoFactory = nodeInfoFactory; + } + + /** + * Checks that all necessary properties have been set and binds with the policy component. + */ + public void afterPropertiesSet() + { + PropertyCheck.mandatory(this, "properties", properties); + PropertyCheck.mandatory(this, "policyComponent", policyComponent); + PropertyCheck.mandatory(this, "auditComponent", auditComponent); + PropertyCheck.mandatory(this, "transactionService", transactionService); + PropertyCheck.mandatory(this, "namespaceService", namespaceService); + PropertyCheck.mandatory(this, "nodeInfoFactory", nodeInfoFactory); + + policyComponent.bindClassBehaviour(BeforeDeleteNodePolicy.QNAME, this, new JavaBehaviour(this, "beforeDeleteNode")); + policyComponent.bindClassBehaviour(OnCreateNodePolicy.QNAME, this, new JavaBehaviour(this, "onCreateNode")); + policyComponent.bindClassBehaviour(OnMoveNodePolicy.QNAME, this, new JavaBehaviour(this, "onMoveNode")); + policyComponent.bindClassBehaviour(OnUpdatePropertiesPolicy.QNAME, this, new JavaBehaviour(this, "onUpdateProperties")); + policyComponent.bindClassBehaviour(OnAddAspectPolicy.QNAME, this, new JavaBehaviour(this, "onAddAspect")); + policyComponent.bindClassBehaviour(OnRemoveAspectPolicy.QNAME, this, new JavaBehaviour(this, "onRemoveAspect")); + + policyComponent.bindClassBehaviour(OnContentUpdatePolicy.QNAME, this, new JavaBehaviour(this, "onContentUpdate")); + policyComponent.bindClassBehaviour(OnContentReadPolicy.QNAME, this, new JavaBehaviour(this, "onContentRead")); + + policyComponent.bindClassBehaviour(OnCreateVersionPolicy.QNAME, ContentModel.TYPE_CONTENT, new JavaBehaviour(this, "onCreateVersion")); + policyComponent.bindClassBehaviour(OnCreateVersionPolicy.QNAME, ContentModel.TYPE_FOLDER, new JavaBehaviour(this, "onCreateVersion")); + + policyComponent.bindClassBehaviour(OnCopyCompletePolicy.QNAME, ContentModel.TYPE_CONTENT, new JavaBehaviour(this, "onCopyComplete")); + policyComponent.bindClassBehaviour(OnCopyCompletePolicy.QNAME, ContentModel.TYPE_FOLDER, new JavaBehaviour(this, "onCopyComplete")); + + policyComponent.bindClassBehaviour(OnCheckOut.QNAME, ContentModel.TYPE_CONTENT, new JavaBehaviour(this, "onCheckOut")); + policyComponent.bindClassBehaviour(OnCheckIn.QNAME, ContentModel.TYPE_CONTENT, new JavaBehaviour(this, "onCheckIn")); + policyComponent.bindClassBehaviour(OnCancelCheckOut.QNAME, ContentModel.TYPE_CONTENT, new JavaBehaviour(this, "onCancelCheckOut")); + } + + private boolean auditEnabled() + { + return transactionService.getAllowWrite() && + auditComponent.areAuditValuesRequired(ROOT_PATH); + } + + @Override + public void beforeDeleteNode(NodeRef nodeRef) + { + if (auditEnabled()) + { + getNodeChange(nodeRef).beforeDeleteNode(nodeRef); + } + } + + @Override + public void onCreateNode(ChildAssociationRef childAssocRef) + { + if (auditEnabled()) + { + getNodeChange(childAssocRef.getChildRef()).onCreateNode(childAssocRef); + } + } + + @Override + public void onMoveNode(ChildAssociationRef fromChildAssocRef, + ChildAssociationRef toChildAssocRef) + { + if (auditEnabled()) + { + getNodeChange(toChildAssocRef.getChildRef()).onMoveNode(fromChildAssocRef, toChildAssocRef); + } + } + + @Override + public void onUpdateProperties(NodeRef nodeRef, + Map fromProperties, Map toProperties) + { + if (auditEnabled()) + { + getNodeChange(nodeRef).onUpdateProperties(nodeRef, fromProperties, toProperties); + } + } + + @Override + public void onRemoveAspect(NodeRef nodeRef, QName aspect) + { + if (auditEnabled()) + { + getNodeChange(nodeRef).onRemoveAspect(nodeRef, aspect); + } + } + + @Override + public void onAddAspect(NodeRef nodeRef, QName aspect) + { + if (auditEnabled()) + { + getNodeChange(nodeRef).onAddAspect(nodeRef, aspect); + } + + } + + @Override + public void onContentUpdate(NodeRef nodeRef, boolean newContent) + { + if (auditEnabled()) + { + getNodeChange(nodeRef).onContentUpdate(nodeRef, newContent); + } + } + + @Override + public void onContentRead(NodeRef nodeRef) + { + if (auditEnabled()) + { + getNodeChange(nodeRef).onContentRead(nodeRef); + } + } + + @Override + public void onCreateVersion(QName classRef, NodeRef nodeRef, + Map versionProperties, PolicyScope nodeDetails) + { + if (auditEnabled()) + { + getNodeChange(nodeRef).onCreateVersion(classRef, nodeRef, versionProperties, nodeDetails); + } + } + + public void onCopyComplete(QName classRef, NodeRef sourceNodeRef, NodeRef targetNodeRef, + boolean copyToNewNode, Map copyMap) + { + if (auditEnabled()) + { + getNodeChange(targetNodeRef).onCopyComplete(classRef, sourceNodeRef, targetNodeRef, + copyToNewNode, copyMap); + } + } + + public void onCheckOut(NodeRef workingCopy) + { + if (auditEnabled()) + { + getNodeChange(workingCopy).onCheckOut(workingCopy); + } + } + + public void onCheckIn(NodeRef nodeRef) + { + if (auditEnabled()) + { + getNodeChange(nodeRef).onCheckIn(nodeRef); + } + } + + public void onCancelCheckOut(NodeRef nodeRef) + { + if (auditEnabled()) + { + getNodeChange(nodeRef).onCancelCheckOut(nodeRef); + } + } + + /** + * @return the {@link NodeChange} for the supplied {@code nodeRef} from + * the current transaction context or create one if required. + */ + private NodeChange getNodeChange(NodeRef nodeRef) + { + Map accessAuditNodes = + TransactionalResourceHelper.getMap(transactionListener); + + if (accessAuditNodes.isEmpty()) + { + AlfrescoTransactionSupport.bindListener(transactionListener); + } + + NodeChange nodeChange = accessAuditNodes.get(nodeRef); + if (nodeChange == null) + { + nodeChange = new NodeChange(nodeInfoFactory, namespaceService, nodeRef); + nodeChange.setAuditSubActions(auditSubActions); + accessAuditNodes.put(nodeRef, nodeChange); + } + + return nodeChange; + } + + /** + * Record audit values and log trace and debug messages. + * @param action String giving the action performed. Becomes the second component + * of the audit path after the root path. + * @param auditMap Map of values to be audited. + * @return {@code true} if any values were audited. + */ + private boolean recordAuditValues(String action, Map auditMap) + { + String rootPath = AuditApplication.buildPath(ROOT_PATH, action); + Map recordedAuditMap = auditComponent.recordAuditValues(rootPath, auditMap); + + if (!recordedAuditMap.isEmpty()) + { + if (logger.isDebugEnabled()) + { + StringBuilder sb = new StringBuilder("\n\tAudit data:"); + for (String key : new TreeSet(recordedAuditMap.keySet())) + { + sb.append("\n\t\t").append(key).append('='); + appendAuditMapValue(sb, recordedAuditMap.get(key)); + } + + sb.append('\n'); + sb.append("\n\tInbound audit values: "); + for (String key : new TreeSet(auditMap.keySet())) + { + sb.append("\n\t\t").append(rootPath).append('/').append(key).append('='); + appendAuditMapValue(sb, auditMap.get(key)); + } + logger.debug(sb.toString()); + } + return true; + } + return false; + } + + /** + * Appends a more readable version of an audit map value. The prefix is used for + * {@link QName} values, including when used in Maps, Sets and Lists. + */ + private void appendAuditMapValue(StringBuilder sb, Serializable value) + { + if (value instanceof QName) + { + // Note there is no need to use the toPrefixString(namespace) method + // as all QNames will have a prefix by this stage. + sb.append(((QName)value).toPrefixString()); + } + else if (value instanceof Map) + { + sb.append('{'); + boolean first = true; + Map map = (Map)value; + for (Map.Entry entry: map.entrySet()) + { + if (!first) + { + sb.append(", "); + } + else + { + first = false; + } + Serializable key = (Serializable)entry.getKey(); + Serializable val = (Serializable)entry.getValue(); + appendAuditMapValue(sb, key); + sb.append('='); + appendAuditMapValue(sb, val); + } + sb.append('}'); + } + else if (value instanceof List) + { + sb.append('['); + boolean first = true; + List list = (List)value; + for (Object element: list) + { + if (!first) + { + sb.append(", "); + } + else + { + first = false; + } + appendAuditMapValue(sb, (Serializable)element); + } + sb.append(']'); + } + else if (value instanceof Set) + { + sb.append('['); + boolean first = true; + Set set = (Set)value; + for (Object element: set) + { + if (!first) + { + sb.append(", "); + } + else + { + first = false; + } + appendAuditMapValue(sb, (Serializable)element); + } + sb.append(']'); + } + else + { + sb.append(value); + } + } + + /** + * Listen for commit to audit gathered audit activity for the current user transaction. + */ + private class AccessTransactionListener extends TransactionListenerAdapter + { + @Override + public void afterCommit() + { + // Note: auditComponent.recordAuditValues(...) creates a transaction to record + // audit messages, so there is no need to create our own. dod5015 still + // does (not sure why). + + final Map changedNodes = TransactionalResourceHelper.getMap(this); + for (Map.Entry entry : changedNodes.entrySet()) + { + NodeChange nodeChange = entry.getValue(); + if (!nodeChange.isTemporaryNode()) + { + Map auditMap = nodeChange.getAuditData(false); + recordAuditValues(TRANSACTION, auditMap); + } + } + } + } +} diff --git a/source/java/org/alfresco/repo/audit/access/AccessAuditorTest.java b/source/java/org/alfresco/repo/audit/access/AccessAuditorTest.java new file mode 100644 index 0000000000..8d04906d26 --- /dev/null +++ b/source/java/org/alfresco/repo/audit/access/AccessAuditorTest.java @@ -0,0 +1,587 @@ +/* + * Copyright (C) 2005-2011 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.audit.access; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.anyMap; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import javax.transaction.UserTransaction; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.audit.AuditComponent; +import org.alfresco.repo.policy.PolicyComponent; +import org.alfresco.repo.security.authentication.AuthenticationComponent; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.version.VersionModel; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.repository.ContentWriter; +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.cmr.version.Version; +import org.alfresco.service.cmr.version.VersionType; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.ApplicationContextHelper; +import org.alfresco.util.PropertyMap; +import org.alfresco.util.debug.NodeStoreInspector; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.ComparisonFailure; +import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.springframework.context.ApplicationContext; + +/** + * Integration test for AccessAuditor. + * + * @author Alan Davis + */ +public class AccessAuditorTest +{ + // Integration test environment + private static ApplicationContext ctx = ApplicationContextHelper.getApplicationContext(); + private static ServiceRegistry serviceRegistry = (ServiceRegistry) ctx.getBean(ServiceRegistry.SERVICE_REGISTRY); + private static NodeService nodeService = serviceRegistry.getNodeService(); + private static TransactionService transactionService = serviceRegistry.getTransactionService(); + private static NamespaceService namespaceService = serviceRegistry.getNamespaceService(); + private static PolicyComponent policyComponent = (PolicyComponent) ctx.getBean("policyComponent"); + private static AuthenticationComponent authenticationComponent = (AuthenticationComponent) ctx.getBean("authenticationComponent"); + + // Integration test data store + private static StoreRef storeRef; + private static NodeRef homeFolder; + private static NodeRef folder0; + private static NodeRef folder1; + private static NodeRef folder2; + private static NodeRef folder3; + private static NodeRef content0; + private static NodeRef content1; + private static NodeRef content2; + private static NodeRef content3; + + // Test setup + private static AccessAuditor auditor; + private static Properties properties; + private static NodeRef workingCopyNodeRef; + private UserTransaction txn; + + // To check results + private static List> auditMapList = new ArrayList>(); + + @SuppressWarnings("unchecked") + @BeforeClass + public static void setUpBeforeClass() throws Exception + { + AuthenticationUtil.setRunAsUserSystem(); + + storeRef = nodeService.createStore(StoreRef.PROTOCOL_WORKSPACE, "Test_" + System.currentTimeMillis()); + NodeRef rootNodeRef = nodeService.getRootNode(storeRef); + + homeFolder = nodeService.createNode( + rootNodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "homeFolder"), + ContentModel.TYPE_FOLDER).getChildRef(); + + folder0 = newFolder(homeFolder, "folder0"); + folder1 = newFolder(homeFolder, "folder1"); + folder2 = newFolder(homeFolder, "folder2"); + folder3 = newFolder(homeFolder, "folder3"); + + content0 = newContent(folder0, "content0"); + content1 = newContent(folder1, "content1"); + content2 = newContent(folder2, "content2"); + content3 = newContent(folder3, "content3"); + + System.out.println(NodeStoreInspector.dumpNodeStore(nodeService, storeRef)); + + try + { + authenticationComponent.clearCurrentSecurityContext(); + } + catch (Throwable e) + { + // ignore + } + + // Mock up an auditComponent to see the results of our tests + AuditComponent auditComponent = mock(AuditComponent.class); + when(auditComponent.areAuditValuesRequired()).thenReturn(true); + when(auditComponent.recordAuditValues(anyString(), anyMap())).thenAnswer(new Answer>() + { + public Map answer(InvocationOnMock invocation) throws Throwable + { + Object[] args = invocation.getArguments(); + Map auditMap = (Map)args[1]; + if ("/alfresco-access/transaction".equals(args[0])) + { + auditMapList.add(auditMap); + } + return auditMap; + } + }); + + // Create our own properties object for use by the auditor + properties = new Properties(); + properties.put("audit.alfresco-access.sub-actions.enabled", "false"); + + // Set properties + auditor = new AccessAuditor(); + auditor.setTransactionService(transactionService); + auditor.setNamespaceService(namespaceService); + auditor.setNodeInfoFactory(new NodeInfoFactory(nodeService, namespaceService)); + auditor.setPolicyComponent(policyComponent); + auditor.setProperties(properties); + auditor.setAuditComponent(auditComponent); + + // Simulate spring call after properties set + auditor.afterPropertiesSet(); + } + + @AfterClass + public static void tearDownAfterClass() throws Exception + { + AuthenticationUtil.setRunAsUserSystem(); + + System.out.println(NodeStoreInspector.dumpNodeStore(nodeService, storeRef)); + + nodeService.deleteStore(storeRef); + + try + { + authenticationComponent.clearCurrentSecurityContext(); + } + catch (Throwable e) + { + // ignore + } + + properties = null; + auditor = null; + } + + @Before + public void setUp() throws Exception + { + // authenticate + authenticationComponent.setSystemUserAsCurrentUser(); + + // start the transaction + txn = transactionService.getUserTransaction(); + txn.begin(); + } + + @After + public void tearDown() throws Exception + { + try + { + authenticationComponent.clearCurrentSecurityContext(); + } + catch (Throwable e) + { + // ignore + } + + try + { + if (txn != null) + { + txn.rollback(); + } + } + catch (Throwable e) + { + // ignore + } + + auditMapList.clear(); + } + + private static NodeRef newFolder(NodeRef parent, String name) + { + return serviceRegistry.getFileFolderService().create( + parent, + name, + ContentModel.TYPE_FOLDER).getNodeRef(); + } + + private static NodeRef newContent(NodeRef parent, String name) + { + PropertyMap propertyMap0 = new PropertyMap(); + propertyMap0.put(ContentModel.PROP_CONTENT, new ContentData(null, "text/plain", 0L, "UTF-16", Locale.ENGLISH)); + NodeRef content = nodeService.createNode( + parent, + ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, name), + ContentModel.TYPE_CONTENT, + propertyMap0).getChildRef(); + ContentWriter writer = serviceRegistry.getContentService().getWriter(content, ContentModel.TYPE_CONTENT, true); + writer.putContent("The cat sat on the mat."); + + return content; + } + + private Map getVersionProperties() + { + Map versionProperties = new HashMap(); + versionProperties.put(Version.PROP_DESCRIPTION, "This is a test"); + return versionProperties; + } + + private void assertContains(String expected, Serializable actual) + { + String actualString = (String)actual; + if (actual == null || !actualString.contains(expected)) + { + throw new ComparisonFailure("Expected not contained in actual.", expected, actualString); + } + } + + @Test + public final void testOnCreateNodeAndOnUpdateProperties() throws Exception + { + newContent(homeFolder, "content4"); + + txn.commit(); + txn = null; + + assertEquals(1, auditMapList.size()); + Map auditMap = auditMapList.get(0); + assertEquals("CREATE", auditMap.get("action")); + assertContains("createNode", auditMap.get("sub-actions")); + assertContains("updateNodeProperties", auditMap.get("sub-actions")); + assertEquals("/cm:homeFolder/cm:content4", auditMap.get("path")); + assertEquals("cm:content", auditMap.get("type")); + } + + @Test + public final void testOnCopyComplete() throws Exception + { + serviceRegistry.getFileFolderService().copy(content2, folder1, null); // keep leaf name + + txn.commit(); + txn = null; + + // TODO do we record the parent or the full path? Do we need to? + + assertEquals(1, auditMapList.size()); + Map auditMap = auditMapList.get(0); + assertEquals("COPY", auditMap.get("action")); + assertContains("createNode", auditMap.get("sub-actions")); + assertContains("updateNodeProperties", auditMap.get("sub-actions")); + assertContains("addNodeAspect", auditMap.get("sub-actions")); + assertContains("copyNode", auditMap.get("sub-actions")); + assertEquals("/cm:homeFolder/cm:folder1/cm:content2", auditMap.get("path")); + assertEquals("/cm:homeFolder/cm:folder2/cm:content2", auditMap.get("copy/from/path")); + assertEquals("cm:content", auditMap.get("type")); + } + + @Test + public final void testOnCopyCompleteAndNewName() throws Exception + { + serviceRegistry.getFileFolderService().copy(content2, folder1, "newName1"); + + txn.commit(); + txn = null; + + // TODO do we record the parent or the full path? Do we need to? + + assertEquals(1, auditMapList.size()); + Map auditMap = auditMapList.get(0); + assertEquals("COPY", auditMap.get("action")); + assertContains("createNode", auditMap.get("sub-actions")); + assertContains("updateNodeProperties", auditMap.get("sub-actions")); + assertContains("addNodeAspect", auditMap.get("sub-actions")); + assertContains("copyNode", auditMap.get("sub-actions")); + assertEquals("/cm:homeFolder/cm:folder1/cm:newName1", auditMap.get("path")); + assertEquals("/cm:homeFolder/cm:folder2/cm:content2", auditMap.get("copy/from/path")); + assertEquals("cm:content", auditMap.get("type")); + } + + @Test + public final void testOnMoveNode() throws Exception + { + serviceRegistry.getNodeService().moveNode(content3, folder1, ContentModel.ASSOC_CONTAINS, null); // keep leaf name + + txn.commit(); + txn = null; + + // TODO do we record the parent or the full path? Do we need to? + + assertEquals(1, auditMapList.size()); + Map auditMap = auditMapList.get(0); + assertEquals("MOVE", auditMap.get("action")); + assertContains("moveNode", auditMap.get("sub-actions")); + assertEquals("/cm:homeFolder/cm:folder1/cm:content3", auditMap.get("path")); + assertEquals("/cm:homeFolder/cm:folder3/cm:content3", auditMap.get("move/from/path")); + assertEquals("cm:content", auditMap.get("type")); + } + + @Test + public final void testOnMoveNodeAndNewName() throws Exception + { + serviceRegistry.getNodeService().moveNode(content3, folder1, ContentModel.ASSOC_CONTAINS, QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "newName2")); + + txn.commit(); + txn = null; + + assertEquals(1, auditMapList.size()); + Map auditMap = auditMapList.get(0); + assertEquals("MOVE", auditMap.get("action")); + assertContains("moveNode", auditMap.get("sub-actions")); + assertEquals("/cm:homeFolder/cm:folder1/cm:newName2", auditMap.get("path")); + assertEquals("/cm:homeFolder/cm:folder1/cm:content3", auditMap.get("move/from/path")); + assertEquals("cm:content", auditMap.get("type")); + } + + @Test + public final void testBeforeDeleteNode() throws Exception + { + serviceRegistry.getNodeService().deleteNode(content0); + + txn.commit(); + txn = null; + + assertEquals(1, auditMapList.size()); + Map auditMap = auditMapList.get(0); + assertEquals("DELETE", auditMap.get("action")); + assertContains("deleteNode", auditMap.get("sub-actions")); + assertEquals("/cm:homeFolder/cm:folder0/cm:content0", auditMap.get("path")); + assertEquals("cm:content", auditMap.get("type")); + } + + @Test + public final void testOnAddAspect() throws Exception + { + serviceRegistry.getNodeService().addAspect(content1, ContentModel.ASPECT_AUTHOR, null); + serviceRegistry.getNodeService().addAspect(content1, ContentModel.ASPECT_OWNABLE, null); + + txn.commit(); + txn = null; + + assertEquals(1, auditMapList.size()); + Map auditMap = auditMapList.get(0); + assertEquals("addNodeAspect", auditMap.get("action")); + assertContains("addNodeAspect", auditMap.get("sub-actions")); + assertEquals("/cm:homeFolder/cm:folder1/cm:content1", auditMap.get("path")); + assertEquals(2, ((Set)auditMap.get("aspects/add")).size()); + assertTrue("Individual author aspect missing", auditMap.containsKey("aspects/add/cm:author")); + assertTrue("Individual ownable aspect missing", auditMap.containsKey("aspects/add/cm:ownable")); + assertEquals("cm:content", auditMap.get("type")); + } + + @Test + public final void testOnRemoveAspect() throws Exception + { + serviceRegistry.getNodeService().removeAspect(content1, ContentModel.ASPECT_AUTHOR); + + txn.commit(); + txn = null; + + assertEquals(1, auditMapList.size()); + Map auditMap = auditMapList.get(0); + assertEquals("deleteNodeAspect", auditMap.get("action")); + assertContains("deleteNodeAspect", auditMap.get("sub-actions")); + assertEquals("/cm:homeFolder/cm:folder1/cm:content1", auditMap.get("path")); + assertEquals(1, ((Set)auditMap.get("aspects/delete")).size()); + assertTrue("Individual author aspect missing", auditMap.containsKey("aspects/delete/cm:author")); + assertEquals("cm:content", auditMap.get("type")); + } + + @Test + public final void testOnContentUpdate() throws Exception + { + ContentWriter writer = serviceRegistry.getContentService().getWriter(content1, ContentModel.TYPE_CONTENT, true); + writer.putContent("The cow jumped over the moon."); + + txn.commit(); + txn = null; + + assertEquals(1, auditMapList.size()); + Map auditMap = auditMapList.get(0); + assertEquals("UPDATE CONTENT", auditMap.get("action")); // TODO Should be UPDATE CONTENT + assertContains("updateContent", auditMap.get("sub-actions")); + assertContains("updateNodeProperties", auditMap.get("sub-actions")); + assertEquals("/cm:homeFolder/cm:folder1/cm:content1", auditMap.get("path")); + assertEquals("cm:content", auditMap.get("type")); + } + + @Test + public final void testOnContentRead() throws Exception + { + serviceRegistry.getContentService().getReader(content1, ContentModel.TYPE_CONTENT); + + txn.commit(); + txn = null; + + assertEquals(1, auditMapList.size()); + Map auditMap = auditMapList.get(0); + assertEquals("READ", auditMap.get("action")); + assertContains("readContent", auditMap.get("sub-actions")); + assertEquals("/cm:homeFolder/cm:folder1/cm:content1", auditMap.get("path")); + assertEquals("cm:content", auditMap.get("type")); + } + + @Test + public final void testOnCreateVersion() throws Exception + { + Map versionProperties = getVersionProperties(); + serviceRegistry.getVersionService().createVersion(content1, versionProperties); + + txn.commit(); + txn = null; + + assertEquals(1, auditMapList.size()); + Map auditMap = auditMapList.get(0); + assertEquals("CREATE VERSION", auditMap.get("action")); + assertContains("updateNodeProperties", auditMap.get("sub-actions")); + assertContains("createVersion", auditMap.get("sub-actions")); + assertEquals("/cm:homeFolder/cm:folder1/cm:content1", auditMap.get("path")); + assertTrue("cm:versionable should be a value with in the set", ((Set)auditMap.get("aspects/add")).contains(ContentModel.ASPECT_VERSIONABLE)); + assertTrue("Individual versionable aspect should exist", auditMap.containsKey("aspects/add/cm:versionable")); + assertEquals("cm:content", auditMap.get("type")); + } + + @Test + public final void testOnCheckOut() throws Exception + { + workingCopyNodeRef = serviceRegistry.getCheckOutCheckInService().checkout(content1); + + txn.commit(); + txn = null; + + assertEquals(2, auditMapList.size()); + boolean origIn0 = ((String)auditMapList.get(0).get("path")).endsWith("cm:content1"); + Map origMap = auditMapList.get(origIn0 ? 0 : 1); + Map workMap = auditMapList.get(origIn0 ? 1 : 0); + + // original file + assertEquals("addNodeAspect", origMap.get("action")); + // createNode createContent readContent updateNodeProperties addNodeAspect copyNode checkOut createVersion + assertContains("updateNodeProperties", origMap.get("sub-actions")); + assertContains("readContent", origMap.get("sub-actions")); + assertEquals("cm:content", origMap.get("type")); + assertEquals("/cm:homeFolder/cm:folder1/cm:content1", origMap.get("path")); + + // working copy + assertEquals("CHECK OUT", workMap.get("action")); + assertContains("createNode", workMap.get("sub-actions")); + assertContains("createContent", workMap.get("sub-actions")); + assertContains("readContent", workMap.get("sub-actions")); + assertContains("updateNodeProperties", workMap.get("sub-actions")); + assertContains("addNodeAspect", workMap.get("sub-actions")); + assertContains("copyNode", workMap.get("sub-actions")); + assertContains("checkOut", workMap.get("sub-actions")); + assertContains("createVersion", workMap.get("sub-actions")); + assertTrue("Expected working copy", ((String)workMap.get("path")).endsWith("(Working Copy)") && + ((String)workMap.get("path")).startsWith("/cm:homeFolder/cm:folder1/")); + assertEquals("cm:content", workMap.get("type")); + } + + @Test + public final void testOnCheckIn() throws Exception + { + Map checkinProperties = new HashMap(); + checkinProperties.put(Version.PROP_DESCRIPTION, null); + checkinProperties.put(VersionModel.PROP_VERSION_TYPE, VersionType.MAJOR); + serviceRegistry.getCheckOutCheckInService().checkin(workingCopyNodeRef, checkinProperties); + + txn.commit(); + txn = null; + + assertEquals(2, auditMapList.size()); + boolean origIn0 = ((String)auditMapList.get(0).get("path")).endsWith("cm:content1"); + Map origMap = auditMapList.get(origIn0 ? 0 : 1); + Map workMap = auditMapList.get(origIn0 ? 1 : 0); + + // working copy + assertEquals("DELETE", workMap.get("action")); + assertContains("deleteNode", workMap.get("sub-actions")); + assertTrue("Expected working copy", ((String)workMap.get("path")).endsWith("(Working Copy)") && + ((String)workMap.get("path")).startsWith("/cm:homeFolder/cm:folder1/")); + assertEquals("cm:content", workMap.get("type")); + + // original file + assertEquals("CHECK IN", origMap.get("action")); + assertContains("deleteNodeAspect", origMap.get("sub-actions")); + assertContains("addNodeAspect", origMap.get("sub-actions")); + assertContains("copyNode", origMap.get("sub-actions")); + assertContains("createVersion", origMap.get("sub-actions")); + assertContains("updateNodeProperties", origMap.get("sub-actions")); + assertContains("checkIn", origMap.get("sub-actions")); + assertContains("readContent", origMap.get("sub-actions")); + assertEquals("/cm:homeFolder/cm:folder1/cm:content1", origMap.get("path")); + assertEquals("cm:content", origMap.get("type")); + } + + @Test + public final void testOnCancelCheckOut() throws Exception + { + workingCopyNodeRef = serviceRegistry.getCheckOutCheckInService().checkout(content1); + txn.commit(); + txn = null; + + tearDown(); + setUp(); + + serviceRegistry.getCheckOutCheckInService().cancelCheckout(workingCopyNodeRef); + + txn.commit(); + txn = null; + + assertEquals(2, auditMapList.size()); + boolean origIn0 = ((String)auditMapList.get(0).get("path")).endsWith("cm:content1"); + Map origMap = auditMapList.get(origIn0 ? 0 : 1); + Map workMap = auditMapList.get(origIn0 ? 1 : 0); + + // working copy + assertEquals("DELETE", workMap.get("action")); + assertContains("deleteNode", workMap.get("sub-actions")); + assertTrue("Expected working copy", ((String)workMap.get("path")).endsWith("(Working Copy)") && + ((String)workMap.get("path")).startsWith("/cm:homeFolder/cm:folder1/")); + assertEquals("cm:content", workMap.get("type")); + + // original file + assertEquals("CANCEL CHECK OUT", origMap.get("action")); + assertContains("deleteNodeAspect", origMap.get("sub-actions")); + assertContains("cancelCheckOut", origMap.get("sub-actions")); + assertEquals("/cm:homeFolder/cm:folder1/cm:content1", origMap.get("path")); + assertEquals("cm:content", origMap.get("type")); + } +} diff --git a/source/java/org/alfresco/repo/audit/access/NodeChange.java b/source/java/org/alfresco/repo/audit/access/NodeChange.java new file mode 100644 index 0000000000..dca68487a1 --- /dev/null +++ b/source/java/org/alfresco/repo/audit/access/NodeChange.java @@ -0,0 +1,735 @@ +/* + * Copyright (C) 2005-2011 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.audit.access; + +import static org.alfresco.repo.audit.model.AuditApplication.AUDIT_PATH_SEPARATOR; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; + +import org.alfresco.repo.audit.model.AuditApplication; +import org.alfresco.repo.coci.CheckOutCheckInServicePolicies.OnCancelCheckOut; +import org.alfresco.repo.coci.CheckOutCheckInServicePolicies.OnCheckIn; +import org.alfresco.repo.coci.CheckOutCheckInServicePolicies.OnCheckOut; +import org.alfresco.repo.content.ContentServicePolicies.OnContentReadPolicy; +import org.alfresco.repo.content.ContentServicePolicies.OnContentUpdatePolicy; +import org.alfresco.repo.copy.CopyServicePolicies.OnCopyCompletePolicy; +import org.alfresco.repo.node.NodeServicePolicies.BeforeDeleteNodePolicy; +import org.alfresco.repo.node.NodeServicePolicies.OnAddAspectPolicy; +import org.alfresco.repo.node.NodeServicePolicies.OnCreateNodePolicy; +import org.alfresco.repo.node.NodeServicePolicies.OnMoveNodePolicy; +import org.alfresco.repo.node.NodeServicePolicies.OnRemoveAspectPolicy; +import org.alfresco.repo.node.NodeServicePolicies.OnUpdatePropertiesPolicy; +import org.alfresco.repo.policy.PolicyScope; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.version.VersionServicePolicies.OnCreateVersionPolicy; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; + +/** + * Changes made to a {@code Node} in a single transaction. For example the creation of + * a Node also involves updating properties, but the main action remains create node. + * + * @author Alan Davis + */ +/*package*/ class NodeChange implements + + BeforeDeleteNodePolicy, OnAddAspectPolicy, OnCreateNodePolicy, OnMoveNodePolicy, + OnRemoveAspectPolicy, OnUpdatePropertiesPolicy, + + OnContentReadPolicy, OnContentUpdatePolicy, + + OnCreateVersionPolicy, + + OnCopyCompletePolicy, + + OnCheckOut, OnCheckIn, OnCancelCheckOut +{ + private static final String USER = "user"; + private static final String ACTION = "action"; + private static final String SUB_ACTIONS = "sub-actions"; + private static final String NODE = "node"; + private static final String PATH = "path"; + private static final String TYPE = "type"; + private static final String FROM = "from"; + private static final String TO = "to"; + private static final String ADD = "add"; + private static final String DELETE = "delete"; + + private static final String COPY = "copy"; + private static final String MOVE = "move"; + private static final String PROPERTIES = "properties"; + private static final String ASPECTS = "aspects"; + private static final String VERSION_PROPERTIES = "version-properties"; + private static final String SUB_ACTION = "sub-action"; + + private static final String DELETE_NODE = "deleteNode"; + private static final String CREATE_NODE = "createNode"; + private static final String MOVE_NODE = "moveNode"; + private static final String UPDATE_NODE_PROPERTIES = "updateNodeProperties"; + private static final String DELETE_NODE_ASPECT = "deleteNodeAspect"; + private static final String ADD_NODE_ASPECT = "addNodeAspect"; + private static final String CREATE_CONTENT = "createContent"; + private static final String UPDATE_CONTENT = "updateContent"; + private static final String READ_CONTENT = "readContent"; + private static final String CREATE_VERSION = "createVersion"; + private static final String COPY_NODE = "copyNode"; + private static final String CHECK_IN = "checkIn"; + private static final String CHECK_OUT = "checkOut"; + private static final String CANCEL_CHECK_OUT = "cancelCheckOut"; + + private static final String INVALID_PATH_CHAR_REPLACEMENT = "-"; + private static final Pattern INVALID_PATH_COMP_CHAR_PATTERN = + Pattern.compile(AuditApplication.AUDIT_INVALID_PATH_COMP_CHAR_REGEX); + + private final NodeInfoFactory nodeInfoFactory; + private final NamespaceService namespaceService; + private final NodeInfo nodeInfo; + + private String action; + + private boolean auditSubActions = false; + private Set subActions = new LinkedHashSet(); + private List> subActionAuditMaps; + + private NodeInfo copyFrom; + + private NodeInfo moveFrom; + private NodeInfo moveTo; + + private Map fromProperties; + private Map toProperties; + + private HashSet addedAspects; + private HashSet deletedAspects; + + private HashMap versionProperties; + + /*package*/ NodeChange(NodeInfoFactory nodeInfoFactory, NamespaceService namespaceService, NodeRef nodeRef) + { + this.nodeInfoFactory = nodeInfoFactory; + this.nodeInfo = nodeInfoFactory.newNodeInfo(nodeRef); + this.namespaceService = namespaceService; + } + + /** + * @return a derived action for a transaction based on the sub-actions that have taken place. + */ + public String getDerivedAction() + { + // Derive higher level action + String action; + if (subActions.contains(CHECK_OUT)) + { + action = "CHECK OUT"; + } + else if (subActions.contains(CHECK_IN)) + { + action = "CHECK IN"; + } + else if (subActions.contains(CANCEL_CHECK_OUT)) + { + action = "CANCEL CHECK OUT"; + } + else if (subActions.contains(COPY_NODE)) + { + action = "COPY"; + } + else if (subActions.contains(CREATE_NODE)) + { + action = "CREATE"; + } + else if (subActions.size() == 1 && subActions.contains(READ_CONTENT)) + { + // Reads in combinations with other actions tend to only facilitate the other action. + action = "READ"; + } + else if (subActions.contains(DELETE_NODE)) + { + action = "DELETE"; + } + else if (subActions.contains(CREATE_VERSION)) // && !subActions.contains(CREATE_NODE) + { + action = "CREATE VERSION"; + } + else if (subActions.contains(UPDATE_CONTENT)) // && !subActions.contains(CREATE_NODE) + { + action = "UPDATE CONTENT"; + } + else if (subActions.contains(MOVE_NODE)) + { + action = "MOVE"; + } + else + { + // Default to first sub-action + action = this.action; + } + + return action; + } + + /** + * @return {@code true} if the node has been created and then deleted. + */ + public boolean isTemporaryNode() + { + // No need to check the order as a new node would be given a ned node ref. + return subActions.contains(CREATE_NODE) && subActions.contains(DELETE_NODE); + } + + private NodeChange setAction(String action) + { + this.action = action; + return this; + } + + private void appendSubAction(NodeChange subNodeChange) + { + // Remember sub-actions so we can check them later to derive the high level action + subActions.add(subNodeChange.action); + + // Default the action to the first sub-action; + if (action == null) + { + action = subNodeChange.action; + } + + // Audit sub actions if required. + if (auditSubActions) + { + if (subActionAuditMaps == null) + { + subActionAuditMaps = new ArrayList>(); + } + subActionAuditMaps.add(subNodeChange.getAuditData(true)); + } + } + + public NodeChange setAuditSubActions(boolean auditSubActions) + { + this.auditSubActions = auditSubActions; + return this; + } + + private NodeChange setCopyFrom(NodeRef copyFrom) + { + this.copyFrom = nodeInfoFactory.newNodeInfo(copyFrom); + return this; + } + + private NodeChange setMoveFrom(ChildAssociationRef childAssocRef) + { + // Don't overwrite original value if multiple calls. + if (this.moveFrom == null) + { + this.moveFrom = nodeInfoFactory.newNodeInfo(childAssocRef); + } + return this; + } + + private NodeChange setMoveTo(ChildAssociationRef childAssocRef) + { + this.moveTo = nodeInfoFactory.newNodeInfo(childAssocRef); + + // Clear values if we are back to where we started. + if (this.moveTo.equals(moveFrom)) + { + this.moveTo = null; + moveFrom = null; + } + return this; + } + + private NodeChange setFromProperties(Map fromProperties) + { + // Don't overwrite original value if multiple calls. + if (this.fromProperties == null) + { + this.fromProperties = fromProperties; + } + return this; + } + + private NodeChange setToProperties(Map toProperties) + { + this.toProperties = toProperties; + return this; + } + + /** + * Add an aspect - if just deleted, remove the delete, otherwise record the add. + */ + private NodeChange addAspect(QName aspect) + { + if (addedAspects == null) + { + addedAspects = new HashSet(); + } + + // Consider sequences + // add = add + // del add = --- + // add del add = add + // add add = add + if (deletedAspects != null && deletedAspects.contains(aspect)) + { + deletedAspects.remove(aspect); + } + else + { + addedAspects.add(aspect); + } + + return this; + } + + /** + * Delete an aspect - if just added, remove the add, otherwise record the delete. + */ + private NodeChange deleteAspect(QName aspect) + { + if (deletedAspects == null) + { + deletedAspects = new HashSet(); + } + + // Consider sequences + // del = del + // add del = --- + // del add del = del + // del del = del + if (addedAspects != null && addedAspects.contains(aspect)) + { + addedAspects.remove(aspect); + } + else + { + deletedAspects.add(aspect); + } + + return this; + } + + private NodeChange setVersionProperties( + HashMap versionProperties) + { + if (this.versionProperties == null) + { + this.versionProperties = new HashMap(); + } + this.versionProperties.putAll(versionProperties); + return this; + } + + @Override + public void beforeDeleteNode(NodeRef nodeRef) + { + appendSubAction(new NodeChange(nodeInfoFactory, namespaceService, nodeRef). + setAction(DELETE_NODE)); + } + + @Override + public void onCreateNode(ChildAssociationRef childAssocRef) + { + appendSubAction(new NodeChange(nodeInfoFactory, namespaceService, childAssocRef.getChildRef()). + setAction(CREATE_NODE)); + } + + @Override + public void onMoveNode(ChildAssociationRef fromChildAssocRef, ChildAssociationRef toChildAssocRef) + { + setMoveFrom(fromChildAssocRef); + setMoveTo(toChildAssocRef); + + // Note: A change of the child node name will be picked up as a property name change. + + appendSubAction(new NodeChange(nodeInfoFactory, namespaceService, toChildAssocRef.getChildRef()). + setAction(MOVE_NODE). + setMoveFrom(fromChildAssocRef). + setMoveTo(toChildAssocRef)); + } + + @Override + public void onUpdateProperties(NodeRef nodeRef, + Map fromProperties, Map toProperties) + { + setFromProperties(fromProperties); + setToProperties(toProperties); + + appendSubAction(new NodeChange(nodeInfoFactory, namespaceService, nodeRef). + setAction(UPDATE_NODE_PROPERTIES). + setFromProperties(fromProperties). + setToProperties(toProperties)); + } + + @Override + public void onRemoveAspect(NodeRef nodeRef, QName aspect) + { + deleteAspect(aspect); + + appendSubAction(new NodeChange(nodeInfoFactory, namespaceService, nodeRef). + setAction(DELETE_NODE_ASPECT). + deleteAspect(aspect)); + } + + @Override + public void onAddAspect(NodeRef nodeRef, QName aspect) + { + addAspect(aspect); + + appendSubAction(new NodeChange(nodeInfoFactory, namespaceService, nodeRef). + setAction(ADD_NODE_ASPECT). + addAspect(aspect)); + } + + @Override + public void onContentUpdate(NodeRef nodeRef, boolean newContent) + { + if (newContent) + { + appendSubAction(new NodeChange(nodeInfoFactory, namespaceService, nodeRef). + setAction(CREATE_CONTENT)); + } + else + { + appendSubAction(new NodeChange(nodeInfoFactory, namespaceService, nodeRef). + setAction(UPDATE_CONTENT)); + } + } + + @Override + public void onContentRead(NodeRef nodeRef) + { + appendSubAction(new NodeChange(nodeInfoFactory, namespaceService, nodeRef). + setAction(READ_CONTENT)); + } + + @Override + public void onCreateVersion(QName classRef, NodeRef nodeRef, + Map versionProperties, PolicyScope nodeDetails) + { + setVersionProperties((HashMap)versionProperties); + + appendSubAction(new NodeChange(nodeInfoFactory, namespaceService, nodeRef). + setAction(CREATE_VERSION). + setVersionProperties((HashMap)versionProperties)); + // Note nodeDetails are not used + } + + public void onCopyComplete(QName classRef, NodeRef sourceNodeRef, NodeRef targetNodeRef, + boolean copyToNewNode, Map copyMap) + { + setCopyFrom(sourceNodeRef); + + appendSubAction(new NodeChange(nodeInfoFactory, namespaceService, targetNodeRef). + setAction(COPY_NODE). + setCopyFrom(sourceNodeRef)); + } + + public void onCheckOut(NodeRef workingCopy) + { + appendSubAction(new NodeChange(nodeInfoFactory, namespaceService, workingCopy). + setAction(CHECK_OUT)); + } + + public void onCheckIn(NodeRef nodeRef) + { + appendSubAction(new NodeChange(nodeInfoFactory, namespaceService, nodeRef). + setAction(CHECK_IN)); + } + + public void onCancelCheckOut(NodeRef nodeRef) + { + appendSubAction(new NodeChange(nodeInfoFactory, namespaceService, nodeRef). + setAction(CANCEL_CHECK_OUT)); + } + + public Map getAuditData(boolean subAction) + { + Map auditMap = new HashMap( + 2 * + (1 + // action + 1 + // user + 1 + // sub actions + 3 + // node, path, type + 3 + // copy source's node, path, type + 3 + // move source's node, path, type + (fromProperties != null ? fromProperties.size() + toProperties.size() + 4 : 0) + + // individual property changes + // grouped from, to, add and delete changes + (addedAspects != null ? addedAspects.size() : 0) + + (deletedAspects != null ? deletedAspects.size() : 0) + + (versionProperties != null ? versionProperties.size()+1 : 0) + + getSubAuditDataSize())); + + // For a transaction, set the action + if (!subAction) + { + setAction(getDerivedAction()); + } + auditMap.put(ACTION, action); + + if (!subAction) // no need to repeat for sub actions + { + auditMap.put(USER, AuthenticationUtil.getFullyAuthenticatedUser()); + addSubActionsToAuditMap(auditMap); + + auditMap.put(NODE, nodeInfo.getNodeRef()); + auditMap.put(PATH, nodeInfo.getPrefixPath()); + auditMap.put(TYPE, nodeInfo.getPrefixType()); + } + + if (copyFrom != null) + { + auditMap.put(buildPath(COPY, FROM, NODE), copyFrom.getNodeRef()); + auditMap.put(buildPath(COPY, FROM, PATH), copyFrom.getPrefixPath()); + auditMap.put(buildPath(COPY, FROM, TYPE), copyFrom.getPrefixType()); + } + + if (moveFrom != null) + { + auditMap.put(buildPath(MOVE, FROM, NODE), moveFrom.getNodeRef()); + auditMap.put(buildPath(MOVE, FROM, PATH), moveFrom.getPrefixPath()); + auditMap.put(buildPath(MOVE, FROM, TYPE), moveFrom.getPrefixType()); + } + + if (fromProperties != null) + { + addPropertyChangesToAuditMap(auditMap, subAction); + } + + if (addedAspects != null && !addedAspects.isEmpty()) + { + addAspectChangesToAuditMap(auditMap, ADD, addedAspects, subAction); + } + + if (deletedAspects != null && !deletedAspects.isEmpty()) + { + addAspectChangesToAuditMap(auditMap, DELETE, deletedAspects, subAction); + } + + if (versionProperties != null && !versionProperties.isEmpty()) + { + addVersionPropertiesToAuditMap(auditMap, versionProperties, subAction); + } + + addSubActionAuditMapsToAuditMap(auditMap); + + return auditMap; + } + + private void addSubActionsToAuditMap(Map auditMap) + { + StringBuilder sb = new StringBuilder(); + for (String subAction: subActions) + { + if (sb.length() > 0) + { + sb.append(' '); + } + sb.append(subAction); + } + + auditMap.put(SUB_ACTIONS, sb.toString()); + } + + private void addPropertyChangesToAuditMap(Map auditMap, boolean subAction) + { + HashMap from = new HashMap(fromProperties.size()); + HashMap to = new HashMap(toProperties.size()); + HashMap add = new HashMap(toProperties.size()); + HashMap delete = new HashMap(fromProperties.size()); + + // Initially check for changes to existing keys and values. + // Record individual value changes and group (from, to, delete) changes in their own maps. + for (Map.Entry entry : fromProperties.entrySet()) + { + // Audit QNames with the prefix set. The key can be used in original Set and + // Map operations as only the namesapace and local name are used in equals and + // hashcode methods. + QName key = entry.getKey().getPrefixedQName(namespaceService); + + String name = replaceInvalidPathChars(key.toPrefixString()); + Serializable beforeValue = entry.getValue(); + Serializable afterValue = null; + + boolean exists = toProperties.containsKey(key); + boolean same = false; + if (exists) + { + // Audit nothing if both values are null or equal. + afterValue = toProperties.get(key); + if ((beforeValue == afterValue) || + (beforeValue != null && beforeValue.equals(afterValue))) + same = true; + } + + if (!same) + { + if (exists) + { + auditMap.put(buildPath(PROPERTIES, FROM, name), beforeValue); + auditMap.put(buildPath(PROPERTIES, TO, name), afterValue); + from.put(key, beforeValue); + to.put(key, afterValue); + } + else + { + auditMap.put(buildPath(PROPERTIES, DELETE, name), beforeValue); + delete.put(key, beforeValue); + } + } + } + + // Check for new values. Record individual values and group as a single map. + Set newKeys = new HashSet(toProperties.keySet()); + newKeys.removeAll(fromProperties.keySet()); + for (QName key: newKeys) + { + key = key.getPrefixedQName(namespaceService); // Audit QNames with the prefix set. + Serializable afterValue = toProperties.get(key); + String name = replaceInvalidPathChars(key.toPrefixString()); + auditMap.put(buildPath(PROPERTIES, ADD, name), afterValue); + add.put(key, afterValue); + } + + // Record maps of additions, deletes and paired from and to values. + if (!subAction) + { + if (!add.isEmpty()) + { + auditMap.put(buildPath(PROPERTIES, ADD), add); + } + if (!delete.isEmpty()) + { + auditMap.put(buildPath(PROPERTIES, DELETE), delete); + } + if (!from.isEmpty()) + { + auditMap.put(buildPath(PROPERTIES, FROM), from); + } + if (!to.isEmpty()) + { + auditMap.put(buildPath(PROPERTIES, TO), to); + } + } + } + + private void addAspectChangesToAuditMap(Map auditMap, + String addOrDelete, HashSet aspects, boolean subAction) + { + // Audit Set where the QName has the prefix set. + HashSet prefixedAspects = new HashSet(aspects.size()); + for (QName aspect: aspects) + { + aspect = aspect.getPrefixedQName(namespaceService); // Audit QNames with the prefix set. + prefixedAspects.add(aspect); + String name = replaceInvalidPathChars(aspect.toPrefixString()); + auditMap.put(buildPath(ASPECTS, addOrDelete, name), null); + } + if (!subAction) + { + auditMap.put(buildPath(ASPECTS, addOrDelete), prefixedAspects); + } + } + + private void addVersionPropertiesToAuditMap(Map auditMap, + HashMap properties, boolean subAction) + { + for (Map.Entry entry: properties.entrySet()) + { + auditMap.put(buildPath(VERSION_PROPERTIES, entry.getKey()), entry.getValue()); + } + if (!subAction) + { + auditMap.put(VERSION_PROPERTIES, properties); + } + } + + private int getSubAuditDataSize() + { + int size = 0; + // No point doing sub actions if only one! + if (subActionAuditMaps != null && subActionAuditMaps.size() > 1) + { + for (Map subActionAuditMap: subActionAuditMaps) + { + size += subActionAuditMap.size(); + } + } + return size; + } + + private void addSubActionAuditMapsToAuditMap(Map auditMap) + { + // No point doing sub actions if only one! + if (subActionAuditMaps != null && subActionAuditMaps.size() > 1) + { + String format = "%0"+Integer.toString(auditMap.size()).length()+"d"; + int i = 0; + for (Map subActionAuditMap : subActionAuditMaps) + { + String seq = String.format(format, i); + for (Map.Entry entry : subActionAuditMap.entrySet()) + { + auditMap.put(buildPath(SUB_ACTION, seq, entry.getKey()), entry.getValue()); + } + i++; + } + } + } + + /** + * Returns a path to be used in an audit map. Unlike {@link AuditApplication#buildPath(String...)} + * the returned value is relative (no leading slash). + * @param components String.. components of the path. + * @return a component path of the supplied values. + */ + private String buildPath(String... components) + { + StringBuilder sb = new StringBuilder(); + for (String component: components) + { + if (sb.length() > 0) + { + sb.append(AUDIT_PATH_SEPARATOR); + } + sb.append(component); + } + return sb.toString(); + } + + /** + * @return a String where all invalid audit path characters are replaced by a '-'. + */ + private String replaceInvalidPathChars(String path) + { + return INVALID_PATH_COMP_CHAR_PATTERN.matcher(path).replaceAll(INVALID_PATH_CHAR_REPLACEMENT); + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/repo/audit/access/NodeChangeTest.java b/source/java/org/alfresco/repo/audit/access/NodeChangeTest.java new file mode 100644 index 0000000000..1e1cb701df --- /dev/null +++ b/source/java/org/alfresco/repo/audit/access/NodeChangeTest.java @@ -0,0 +1,491 @@ +/* + * Copyright (C) 2005-2011 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.audit.access; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.alfresco.model.ContentModel; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.Path; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.version.Version; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +/** + * Unit test for NodeChange which is the main class behind AccessAuditor. + * + * @author Alan Davis + */ +public class NodeChangeTest +{ + private static final StoreRef STORE = new StoreRef("protocol", "store"); + + private NodeChange nodeChange; + + private NodeInfoFactory nodeInfoFactory; + private NamespaceService namespaceService; + private NodeService nodeService; + + private NodeRef folder1; + private NodeRef folder2; + private NodeRef content1; + private Path folderPath1; + private Path folderPath2; + + @Before + public void setUp() throws Exception + { + namespaceService = mock(NamespaceService.class); + Collection cmAlways = new ArrayList(); + cmAlways.add("cm"); + when(namespaceService.getPrefixes(anyString())).thenReturn(cmAlways); + when(namespaceService.getNamespaceURI(anyString())).thenReturn("cm"); + + nodeService = mock(NodeService.class); + + Path rootPath = newPath(null, "/"); + Path homeFolderPath = newPath(rootPath, "cm:homeFolder"); + folderPath1 = newPath(homeFolderPath, "cm:folder1"); + folderPath2 = newPath(homeFolderPath, "cm:folder2"); + folder1 = newFolder(folderPath1); + folder2 = newFolder(folderPath2); + content1 = newContent(folderPath1, "cm:content1"); + + nodeInfoFactory = new NodeInfoFactory(nodeService, namespaceService); + nodeChange = new NodeChange(nodeInfoFactory, namespaceService, content1); + } + + private NodeRef newFolder(Path path) + { + String name = path.get(path.size()-1).getElementString(); + return newNodeRef(path, name, "folder"); + } + + private NodeRef newContent(Path parentPath, String name) + { + Path path = newPath(parentPath, name); + return newNodeRef(path, name, "content"); + } + + private NodeRef newNodeRef(Path path, String name, String type) + { + NodeRef nodeRef = new NodeRef(STORE, name); + QName qNameType = QName.createQName("URI", type); + when(nodeService.getType(nodeRef)).thenReturn(qNameType); + when(nodeService.getPath(nodeRef)).thenReturn(path); + return nodeRef; + } + + @SuppressWarnings("serial") + private Path newPath(Path parent, final String name) + { + Path path = new Path(); + if (parent != null) + { + for(Path.Element element: parent) + { + path.append(element); + } + } + path.append(new Path.Element() + { + @Override + public String getElementString() + { + return name; + } + }); + return path; + } + + private void assertStandardData(Map auditMap, + String expectedAction, String expectedSubActions) + { + String expectedPath = "/cm:homeFolder/cm:folder1/cm:content1"; + assertEquals(expectedAction, auditMap.get("action")); + assertEquals(expectedSubActions, auditMap.get("sub-actions")); + assertEquals(expectedPath, auditMap.get("path")); + assertEquals(content1, auditMap.get("node")); + assertEquals("cm:content", auditMap.get("type")); + } + + private void callCreateNode() + { + ChildAssociationRef childAssocRef = mock(ChildAssociationRef.class); + when(childAssocRef.getChildRef()).thenReturn(content1); + nodeChange.onCreateNode(childAssocRef); + } + + private void callDeleteNode() + { + nodeChange.beforeDeleteNode(content1); + } + + @After + public void tearDown() throws Exception + { + } + + @Test + public final void testGetAuditDataTrueSubAction() + { + callCreateNode(); + + Map auditMap = nodeChange.getAuditData(true); + + assertEquals("Should NOT be derived", "createNode", auditMap.get("action")); + assertFalse("'user' should not exist in a subAction", auditMap.keySet().contains("user")); + assertFalse("'sub-actions' should not exist in a subAction", auditMap.keySet().contains("sub-actions")); + assertFalse("'node' should not exist in a subAction", auditMap.keySet().contains("node")); + assertFalse("'path' should not exist in a subAction", auditMap.keySet().contains("path")); + assertFalse("'type' should not exist in a subAction", auditMap.keySet().contains("type")); + } + + @Test + public final void testGetAuditDataFalseTopLevelAction() + { + callCreateNode(); + + Map auditMap = nodeChange.getAuditData(false); + + assertEquals("Should be derived", "CREATE", auditMap.get("action")); + assertTrue("'user' should exist if not a subAction", auditMap.keySet().contains("user")); + assertTrue("'sub-actions' should exist if not a subAction", auditMap.keySet().contains("sub-actions")); + assertTrue("'node' should exist if not a subAction", auditMap.keySet().contains("node")); + assertTrue("'path' should exist if not a subAction", auditMap.keySet().contains("path")); + assertTrue("'type' should exist if not a subAction", auditMap.keySet().contains("type")); + } + + @Test + public final void testSetAuditSubActionsTrue() + { + nodeChange.setAuditSubActions(true); + callCreateNode(); + nodeChange.beforeDeleteNode(content1); + + Map auditMap = nodeChange.getAuditData(false); + + assertEquals("subAction audit should exist", "createNode", auditMap.get("sub-action/0/action")); + assertEquals("subAction audit should exist", "deleteNode", auditMap.get("sub-action/1/action")); + } + + @Test + public final void testSetAuditSubActionsFalse() + { + nodeChange.setAuditSubActions(false); + callCreateNode(); + nodeChange.beforeDeleteNode(content1); + + Map auditMap = nodeChange.getAuditData(false); + + assertFalse("subAction audit should NOT exist", auditMap.keySet().contains("sub-action/0/action")); + assertFalse("subAction audit should NOT exist", auditMap.keySet().contains("sub-action/1/action")); + } + + @Test + public final void testOnCreateNode() + { + callCreateNode(); + + Map auditMap = nodeChange.getAuditData(false); + + assertStandardData(auditMap, "CREATE", "createNode"); + } + + @Test + public final void testBeforeDeleteNode() + { + callDeleteNode(); + + Map auditMap = nodeChange.getAuditData(false); + + assertStandardData(auditMap, "DELETE", "deleteNode"); + } + + @Test + public final void testIsTemporaryNodeYes() + { + callCreateNode(); + nodeChange.beforeDeleteNode(content1); + + assertTrue("A node was created and deleted so should have been temporary.", + nodeChange.isTemporaryNode()); + } + + @Test + public final void testOnMoveNode() + { + ChildAssociationRef fromChildAssocRef = mock(ChildAssociationRef.class); + when(fromChildAssocRef.getChildRef()).thenReturn(content1); // correct as the move has taken place + when(fromChildAssocRef.getParentRef()).thenReturn(folder2); + when(fromChildAssocRef.getQName()).thenReturn(QName.createQName("URI", "content1")); + + ChildAssociationRef toChildAssocRef = mock(ChildAssociationRef.class); + when(toChildAssocRef.getChildRef()).thenReturn(content1); + when(toChildAssocRef.getParentRef()).thenReturn(folder1); + when(toChildAssocRef.getQName()).thenReturn(QName.createQName("URI", "content1")); + + nodeChange.onMoveNode(fromChildAssocRef, toChildAssocRef); + + Map auditMap = nodeChange.getAuditData(false); + + assertStandardData(auditMap, "MOVE", "moveNode"); + assertEquals("/cm:homeFolder/cm:folder2/cm:content1", auditMap.get("move/from/path")); + assertEquals(content1, auditMap.get("move/from/node")); + assertEquals("cm:content", auditMap.get("move/from/type")); + } + + @Test + public final void testOnCopyComplete() + { + NodeRef content2 = newContent(folderPath2, "cm:content2"); + nodeChange.onCopyComplete(null, content2, content1, true, null); + + Map auditMap = nodeChange.getAuditData(false); + + assertStandardData(auditMap, "COPY", "copyNode"); + assertEquals("/cm:homeFolder/cm:folder2/cm:content2", auditMap.get("copy/from/path")); + assertEquals(content2, auditMap.get("copy/from/node")); + assertEquals("cm:content", auditMap.get("copy/from/type")); + } + + @Test + public final void testOnUpdateProperties() + { + Map fromProperties = new HashMap(); + fromProperties.put(ContentModel.PROP_CREATED, "created"); + fromProperties.put(ContentModel.PROP_CREATOR, "creator"); + fromProperties.put(ContentModel.PROP_CONTENT, "content"); + fromProperties.put(ContentModel.PROP_LOCATION, "location"); + fromProperties.put(ContentModel.PROP_MOBILE, "mobile"); + fromProperties.put(ContentModel.PROP_HITS, "hits"); + fromProperties.put(ContentModel.PROP_TITLE, "title"); + + Map toProperties = new HashMap(fromProperties); + toProperties.put(ContentModel.PROP_AUTHOR, "AUTHOR"); + toProperties.put(ContentModel.PROP_ADDRESSEE, "ADDRESSEE"); + toProperties.remove(ContentModel.PROP_CREATED); + toProperties.remove(ContentModel.PROP_CREATOR); + toProperties.remove(ContentModel.PROP_CONTENT); + toProperties.put(ContentModel.PROP_LOCATION, "LOCATION"); + + nodeChange.onUpdateProperties(content1, fromProperties, toProperties); + + Map auditMap = nodeChange.getAuditData(false); + + assertStandardData(auditMap, "updateNodeProperties", "updateNodeProperties"); + + assertEquals(1, ((Map)auditMap.get("properties/from")).size()); + assertEquals("location", ((Map)auditMap.get("properties/from")).get(ContentModel.PROP_LOCATION)); + assertEquals("location", auditMap.get("properties/from/cm:location")); + + assertEquals(1, ((Map)auditMap.get("properties/to")).size()); + assertEquals("LOCATION", ((Map)auditMap.get("properties/to")).get(ContentModel.PROP_LOCATION)); + assertEquals("LOCATION", auditMap.get("properties/to/cm:location")); + + assertEquals(2, ((Map)auditMap.get("properties/add")).size()); + assertEquals("AUTHOR", ((Map)auditMap.get("properties/add")).get(ContentModel.PROP_AUTHOR)); + assertEquals("ADDRESSEE", ((Map)auditMap.get("properties/add")).get(ContentModel.PROP_ADDRESSEE)); + assertEquals("AUTHOR", auditMap.get("properties/add/cm:author")); + assertEquals("ADDRESSEE", auditMap.get("properties/add/cm:addressee")); + + assertEquals(3, ((Map)auditMap.get("properties/delete")).size()); + assertEquals("created", ((Map)auditMap.get("properties/delete")).get(ContentModel.PROP_CREATED)); + assertEquals("creator", ((Map)auditMap.get("properties/delete")).get(ContentModel.PROP_CREATOR)); + assertEquals("content", ((Map)auditMap.get("properties/delete")).get(ContentModel.PROP_CONTENT)); + assertEquals("created", auditMap.get("properties/delete/cm:created")); + assertEquals("creator", auditMap.get("properties/delete/cm:creator")); + assertEquals("content", auditMap.get("properties/delete/cm:content")); + } + @Test + public final void testReplaceInvalidPathChars() + { + Map fromProperties = new HashMap(); + Map toProperties = new HashMap(); + QName qName = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "valid/&\u3001"); + fromProperties.put(qName, "/&\u3001"); + + nodeChange.onUpdateProperties(content1, fromProperties, toProperties); + + Map auditMap = nodeChange.getAuditData(false); + + assertEquals("/&\u3001", ((Map)auditMap.get("properties/delete")).get(qName)); + assertEquals("/&\u3001", auditMap.get("properties/delete/cm:valid---")); + } + + @Test + public final void testOnAddAspect() + { + // add = add + // del add = --- + // add del add = add + // add add = add + + nodeChange.onAddAspect(content1, ContentModel.ASPECT_ARCHIVED); + + nodeChange.onRemoveAspect(content1, ContentModel.ASPECT_COPIEDFROM); + nodeChange.onAddAspect(content1, ContentModel.ASPECT_COPIEDFROM); + + nodeChange.onAddAspect(content1, ContentModel.ASPECT_EMAILED); + nodeChange.onRemoveAspect(content1, ContentModel.ASPECT_EMAILED); + nodeChange.onAddAspect(content1, ContentModel.ASPECT_EMAILED); + + nodeChange.onAddAspect(content1, ContentModel.ASPECT_GEOGRAPHIC); + nodeChange.onAddAspect(content1, ContentModel.ASPECT_GEOGRAPHIC); + + Map auditMap = nodeChange.getAuditData(false); + + assertStandardData(auditMap, "addNodeAspect", "addNodeAspect deleteNodeAspect"); + + assertEquals(3, ((Set)auditMap.get("aspects/add")).size()); + assertTrue("Grouped cm:archived aspect missing", ((Set)auditMap.get("aspects/add")).contains(ContentModel.ASPECT_ARCHIVED)); + assertTrue("Grouped cm:emailed aspect missing", ((Set)auditMap.get("aspects/add")).contains(ContentModel.ASPECT_EMAILED)); + assertTrue("Grouped cm:geographic aspect missing", ((Set)auditMap.get("aspects/add")).contains(ContentModel.ASPECT_GEOGRAPHIC)); + assertTrue("Individual cm:archived aspect missing", auditMap.containsKey("aspects/add/cm:archived")); + assertTrue("Individual cm:emailed aspect missing", auditMap.containsKey("aspects/add/cm:emailed")); + assertTrue("Individual cm:geographic aspect missing", auditMap.containsKey("aspects/add/cm:geographic")); + } + + @Test + public final void testOnRemoveAspect() + { + // del = del + // add del = --- + // del add del = del + // del del = del + + nodeChange.onRemoveAspect(content1, ContentModel.ASPECT_ARCHIVED); + + nodeChange.onAddAspect(content1, ContentModel.ASPECT_COPIEDFROM); + nodeChange.onRemoveAspect(content1, ContentModel.ASPECT_COPIEDFROM); + + nodeChange.onRemoveAspect(content1, ContentModel.ASPECT_EMAILED); + nodeChange.onAddAspect(content1, ContentModel.ASPECT_EMAILED); + nodeChange.onRemoveAspect(content1, ContentModel.ASPECT_EMAILED); + + nodeChange.onRemoveAspect(content1, ContentModel.ASPECT_GEOGRAPHIC); + nodeChange.onRemoveAspect(content1, ContentModel.ASPECT_GEOGRAPHIC); + + Map auditMap = nodeChange.getAuditData(false); + + assertStandardData(auditMap, "deleteNodeAspect", "deleteNodeAspect addNodeAspect"); + + assertEquals(3, ((Set)auditMap.get("aspects/delete")).size()); + assertTrue("Grouped cm:archived aspect missing", ((Set)auditMap.get("aspects/delete")).contains(ContentModel.ASPECT_ARCHIVED)); + assertTrue("Grouped cm:emailed aspect missing", ((Set)auditMap.get("aspects/delete")).contains(ContentModel.ASPECT_EMAILED)); + assertTrue("Grouped cm:geographic aspect missing", ((Set)auditMap.get("aspects/delete")).contains(ContentModel.ASPECT_GEOGRAPHIC)); + assertTrue("Individual cm:archived aspect missing", auditMap.containsKey("aspects/delete/cm:archived")); + assertTrue("Individual cm:emailed aspect missing", auditMap.containsKey("aspects/delete/cm:emailed")); + assertTrue("Individual cm:geographic aspect missing", auditMap.containsKey("aspects/delete/cm:geographic")); + } + + @Test + public final void testOnContentUpdateTrue() + { + nodeChange.onContentUpdate(content1, true); + + Map auditMap = nodeChange.getAuditData(false); + + assertStandardData(auditMap, "createContent", "createContent"); + } + + @Test + public final void testOnContentUpdateFalse() + { + nodeChange.onContentUpdate(content1, false); + + Map auditMap = nodeChange.getAuditData(false); + + assertStandardData(auditMap, "UPDATE CONTENT", "updateContent"); + } + + @Test + public final void testOnContentRead() + { + nodeChange.onContentRead(content1); + + Map auditMap = nodeChange.getAuditData(false); + + assertStandardData(auditMap, "READ", "readContent"); + } + + @Test + public final void testOnCreateVersion() + { + Map versionProperties = new HashMap(); + versionProperties.put(Version.PROP_DESCRIPTION, "This is a test"); + nodeChange.onCreateVersion(null, content1, versionProperties, null); + + Map auditMap = nodeChange.getAuditData(false); + + assertStandardData(auditMap, "CREATE VERSION", "createVersion"); + + assertEquals(1, ((Map)auditMap.get("version-properties")).size()); + assertEquals("Grouped description version-properties missing", "This is a test", ((Map)auditMap.get("version-properties")).get(Version.PROP_DESCRIPTION)); + assertEquals("Individual description version-properties missing", "This is a test", auditMap.get("version-properties/description")); + } + + @Test + public final void testOnCheckOut() + { + nodeChange.onCheckOut(content1); + + Map auditMap = nodeChange.getAuditData(false); + + assertStandardData(auditMap, "CHECK OUT", "checkOut"); + } + + @Test + public final void testOnCheckIn() + { + nodeChange.onCheckIn(content1); + + Map auditMap = nodeChange.getAuditData(false); + + assertStandardData(auditMap, "CHECK IN", "checkIn"); + } + + @Test + public final void testOnCancelCheckOut() + { + nodeChange.onCancelCheckOut(content1); + + Map auditMap = nodeChange.getAuditData(false); + + assertStandardData(auditMap, "CANCEL CHECK OUT", "cancelCheckOut"); + } +} diff --git a/source/java/org/alfresco/repo/audit/access/NodeInfo.java b/source/java/org/alfresco/repo/audit/access/NodeInfo.java new file mode 100644 index 0000000000..1f46fd1ef5 --- /dev/null +++ b/source/java/org/alfresco/repo/audit/access/NodeInfo.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2005-2011 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.audit.access; + +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * Wrapper for a {@link NodeRef} to provide path and type values using namespace prefixes. + * Use the {@link NodeInfoFactory#newNodeInfo()} to create new instances. + * + * @author Alan Davis + */ +/*package*/ class NodeInfo +{ + private final NodeRef nodeRef; + private final String path; + private final String type; + + /*package*/ NodeInfo(NodeRef nodeRef, String path, String type) + { + this.nodeRef = nodeRef; + this.path = path; + this.type = type; + } + + public NodeRef getNodeRef() + { + return nodeRef; + } + + public String getPrefixPath() + { + return path; + } + + public String getPrefixType() + { + return type; + } + + @Override + public int hashCode() + { + final int prime = 31; + int result = 1; + result = prime * result + ((path == null) ? 0 : path.hashCode()); + result = prime * result + ((type == null) ? 0 : type.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + { + return true; + } + if (obj == null) + { + return false; + } + if (getClass() != obj.getClass()) + { + return false; + } + NodeInfo other = (NodeInfo) obj; + if (path == null) + { + if (other.path != null) + { + return false; + } + } + else if (!path.equals(other.path)) + { + return false; + } + if (type == null) + { + if (other.type != null) + { + return false; + } + } + else if (!type.equals(other.type)) + { + return false; + } + return true; + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/repo/audit/access/NodeInfoFactory.java b/source/java/org/alfresco/repo/audit/access/NodeInfoFactory.java new file mode 100644 index 0000000000..348507e149 --- /dev/null +++ b/source/java/org/alfresco/repo/audit/access/NodeInfoFactory.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2005-2011 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.audit.access; + +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.InvalidNodeRefException; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.namespace.NamespaceException; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.ISO9075; + +/** + * Factory for {@link NodeInfo} objects. Avoids having to pass in {@link NodeService} + * and {@link NamespaceService} each time a NodeInfo is created.

+ * + * @author Alan Davis + */ +public class NodeInfoFactory +{ + private final NodeService nodeService; + private final NamespaceService namespaceService; + + public NodeInfoFactory(NodeService nodeService, NamespaceService namespaceService) + { + this.nodeService = nodeService; + this.namespaceService = namespaceService; + } + + public NodeInfo newNodeInfo(NodeRef nodeRef) + { + // The path and type values are calculated when a NodeInfo is created as + // it will not be possible to calculate later if the node is deleted. + String path = getPath(nodeRef); + String type = getType(nodeRef); + return new NodeInfo(nodeRef, path, type); + } + + public NodeInfo newNodeInfo(ChildAssociationRef childAssocRef) + { + // Build up the path from the parent and child name, as + // the child may no longer be under the parent. + String path = getPath(childAssocRef.getParentRef())+'/'+getName(childAssocRef.getQName()); + String type = getType(childAssocRef.getChildRef()); + return new NodeInfo(childAssocRef.getChildRef(), path, type); + } + + private String getPath(NodeRef nodeRef) + { + String path = null; + + try + { + path = nodeService.getPath(nodeRef).toPrefixString(namespaceService); + } catch (NamespaceException e) + { + path = nodeService.getPath(nodeRef).toString(); + } + catch (InvalidNodeRefException e) + { + // If the node has been removed, return null. + } + path = ISO9075.decode(path); + + return path; + } + + private String getName(QName qName) + { + String name = null; + + try + { + name = qName.toPrefixString(namespaceService); + } + catch (NamespaceException e) + { + name = qName.toPrefixString(); + } + name = ISO9075.decode(name); + + return name; + } + + private String getType(NodeRef nodeRef) + { + String type = null; + + try + { + type = nodeService.getType(nodeRef).toPrefixString(namespaceService); + } + catch (NamespaceException e) + { + type = nodeService.getType(nodeRef).toPrefixString(); + } + catch (InvalidNodeRefException e) + { + // If the node has been removed, return null. + } + + return type; + } +} diff --git a/source/java/org/alfresco/repo/audit/model/AuditApplication.java b/source/java/org/alfresco/repo/audit/model/AuditApplication.java index 9e42dc924a..8604dbe58e 100644 --- a/source/java/org/alfresco/repo/audit/model/AuditApplication.java +++ b/source/java/org/alfresco/repo/audit/model/AuditApplication.java @@ -47,7 +47,8 @@ public class AuditApplication { 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\\-\\_\\.]+)+"; + public static final String AUDIT_PATH_REGEX = "(/[a-zA-Z0-9:\\-\\_\\.]+)+"; + public static final String AUDIT_INVALID_PATH_COMP_CHAR_REGEX = "[^a-zA-Z0-9:\\-\\_\\.]"; private static final Log logger = LogFactory.getLog(AuditApplication.class); diff --git a/source/java/org/alfresco/repo/avm/AVMServiceImpl.java b/source/java/org/alfresco/repo/avm/AVMServiceImpl.java index 02a5694db7..b796fcece5 100644 --- a/source/java/org/alfresco/repo/avm/AVMServiceImpl.java +++ b/source/java/org/alfresco/repo/avm/AVMServiceImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2011 Alfresco Software Limited. * * This file is part of Alfresco * @@ -1568,6 +1568,9 @@ public class AVMServiceImpl implements AVMService removeNode(newPath); existing = null; } + + Map listing = getDirectoryListing(desc); + if (existing == null) { createDirectory(path, name); @@ -1577,7 +1580,7 @@ public class AVMServiceImpl implements AVMService setAclAsSystem(newPath, AVMDAOs.Instance().fAclDAO.getAclCopy(acl.getId(), parentAclId, ACLCopyMode.COPY)); } } - Map listing = getDirectoryListing(desc); + for (Map.Entry entry : listing.entrySet()) { recursiveCopy(version, entry.getValue(), newPath, entry.getKey()); diff --git a/source/java/org/alfresco/repo/avm/AVMServiceTest.java b/source/java/org/alfresco/repo/avm/AVMServiceTest.java index 83f0e9e4dc..48a2867511 100644 --- a/source/java/org/alfresco/repo/avm/AVMServiceTest.java +++ b/source/java/org/alfresco/repo/avm/AVMServiceTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2011 Alfresco Software Limited. * * This file is part of Alfresco * @@ -1563,6 +1563,38 @@ public class AVMServiceTest extends AVMServiceTestBase throw e; } } + + public void testCopy_ALF_835() throws Exception + { + try + { + fService.createStore("mainA"); + + fService.createDirectory("mainA:/", "a"); + fService.createDirectory("mainA:/a", "www"); + + AVMNodeDescriptor desc = fService.lookup(-1, "mainA:/a/www"); + assertTrue(desc.isDirectory()); + + desc = fService.lookup(-1, "mainA:/a/www/www"); + assertNull(desc); + + // copy empty directory into itself + fService.copy(-1, "mainA:/a/www", "mainA:/a/www", "www"); + + desc = fService.lookup(-1, "mainA:/a/www/www"); + assertTrue(desc.isDirectory()); + } + catch (Exception e) + { + e.printStackTrace(System.err); + throw e; + } + finally + { + fService.purgeStore("mainA"); + } + } protected void runQueriesAgainstBasicTreePlusFileCopy(String store) { diff --git a/source/java/org/alfresco/repo/avm/OrphanReaper.java b/source/java/org/alfresco/repo/avm/OrphanReaper.java index 8636140bc5..1b87f6ab46 100644 --- a/source/java/org/alfresco/repo/avm/OrphanReaper.java +++ b/source/java/org/alfresco/repo/avm/OrphanReaper.java @@ -18,20 +18,21 @@ package org.alfresco.repo.avm; -import java.sql.Savepoint; import java.util.LinkedList; import java.util.List; import org.alfresco.repo.domain.avm.AVMHistoryLinkEntity; import org.alfresco.repo.domain.avm.AVMMergeLinkEntity; -import org.alfresco.repo.domain.control.ControlDAO; import org.alfresco.repo.domain.permissions.Acl; +import org.alfresco.repo.lock.JobLockService; +import org.alfresco.repo.lock.LockAcquisitionException; import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; import org.alfresco.service.transaction.TransactionService; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.springframework.dao.ConcurrencyFailureException; /** * This is the background thread for reaping no longer referenced nodes in the AVM repository. These orphans arise from @@ -107,13 +108,14 @@ public class OrphanReaper private Log fgLogger = LogFactory.getLog(OrphanReaper.class); + private static final QName LOCK = QName.createQName(NamespaceService.SYSTEM_MODEL_1_0_URI, "OrphanReaper"); + private JobLockService jobLockService; + /** * The Transaction Service */ private TransactionService fTransactionService; - private ControlDAO controlDAO; - /** * Active base sleep interval. */ @@ -129,16 +131,6 @@ public class OrphanReaper */ private boolean fActive; - /** - * The maximum length of the queue. - */ - private int fQueueLength; - - /** - * The linked list containing ids of nodes that are purgable. - */ - private LinkedList fPurgeQueue; - private boolean fDone = false; private boolean fRunning = false; @@ -150,7 +142,6 @@ public class OrphanReaper { fActiveBaseSleep = 1000; fBatchSize = 50; - fQueueLength = 1000; fActive = false; } @@ -190,19 +181,11 @@ public class OrphanReaper } /** - * Set the maximum size of the queue of purgeable nodes. - * - * @param queueLength - * The max length. + * @param jobLockService service used to ensure that reaper runs are not duplicated */ - public void setMaxQueueLength(int queueLength) + public void setJobLockService(JobLockService jobLockService) { - fQueueLength = queueLength; - } - - public void setControlDAO(ControlDAO controlDAO) - { - this.controlDAO = controlDAO; + this.jobLockService = jobLockService; } /** @@ -221,6 +204,38 @@ public class OrphanReaper fDone = true; } + /** + * Attempts to get the lock. If the lock couldn't be taken, then null is returned. + * + * @return Returns the lock token or null + */ + private String getLock(long time) + { + try + { + return jobLockService.getLock(LOCK, time); + } + catch (LockAcquisitionException e) + { + return null; + } + } + + /** + * Attempts to get the lock. If it fails, the current transaction is marked for rollback. + * + * @return Returns the lock token + */ + private void refreshLock(String lockToken, long time) + { + if (lockToken == null) + { + throw new IllegalArgumentException("Must provide existing lockToken"); + } + jobLockService.refreshLock(lockToken, LOCK, time); + } + + /** * Sit in a loop, periodically querying for orphans. When orphans are found, unhook them in bite sized batches. */ @@ -270,14 +285,21 @@ public class OrphanReaper { public Object execute() throws Exception { + String lockToken = getLock(20000L); + if (lockToken == null) + { + fgLogger.warn("Can't get lock. Assume multiple reapers ..."); + fActive = false; + return null; + } + if (fgLogger.isTraceEnabled()) { - fgLogger.trace("Orphan reaper doBatch: batchSize="+fBatchSize+", maxQueueLength="+fQueueLength+", fActiveBaseSleep="+fActiveBaseSleep); + fgLogger.trace("Orphan reaper doBatch: batchSize="+fBatchSize+", fActiveBaseSleep="+fActiveBaseSleep); } - if (fPurgeQueue == null) - { - List nodes = AVMDAOs.Instance().fAVMNodeDAO.getOrphans(fQueueLength); + refreshLock(lockToken, fBatchSize * 100L); + List nodes = AVMDAOs.Instance().fAVMNodeDAO.getOrphans(fBatchSize); if (nodes.size() == 0) { if (fgLogger.isTraceEnabled()) @@ -289,7 +311,8 @@ public class OrphanReaper return null; } - fPurgeQueue = new LinkedList(); + refreshLock(lockToken, nodes.size() * 100L); + LinkedList fPurgeQueue = new LinkedList(); for (AVMNode node : nodes) { fPurgeQueue.add(node.getId()); @@ -299,14 +322,6 @@ public class OrphanReaper { fgLogger.debug("Queue was empty so got more orphans from DB. Orphan queue size = "+fPurgeQueue.size()); } - } - else - { - if (fgLogger.isDebugEnabled()) - { - fgLogger.debug("Queue was not empty. Orphan queue size = "+fPurgeQueue.size()); - } - } fActive = true; @@ -327,16 +342,9 @@ public class OrphanReaper break; } + refreshLock(lockToken, 10000L); Long nodeId = fPurgeQueue.removeFirst(); AVMNode node = AVMDAOs.Instance().fAVMNodeDAO.getByID(nodeId); - if (node == null) - { - // eg. cluster, multiple reapers - - fgLogger.warn("Node ["+nodeId+"] not found - assume multiple reapers ..."); - - continue; - } // Save away the ancestor and merged from fields from this node. @@ -418,30 +426,13 @@ public class OrphanReaper } } - Savepoint savepoint = controlDAO.createSavepoint("OrphanReaper"); - - try - { // Finally, delete it AVMDAOs.Instance().fAVMNodeDAO.delete(node); - controlDAO.releaseSavepoint(savepoint); if (fgLogger.isTraceEnabled()) { fgLogger.trace("Deleted Node ["+node.getId()+"]"); } - } - catch (ConcurrencyFailureException e) - { - // Since we are deleting the row, it doesn't matter - // if it is deleted here or not. - controlDAO.rollbackToSavepoint(savepoint); - - if (fgLogger.isTraceEnabled()) - { - fgLogger.trace("Node already deleted ["+node.getId()+"]"); - } - } reapCnt++; } diff --git a/source/java/org/alfresco/repo/avm/PurgeTestP.java b/source/java/org/alfresco/repo/avm/PurgeTestP.java index 719c4fa248..271473575b 100644 --- a/source/java/org/alfresco/repo/avm/PurgeTestP.java +++ b/source/java/org/alfresco/repo/avm/PurgeTestP.java @@ -36,7 +36,7 @@ public class PurgeTestP extends AVMServiceTestBase { super.setUp(); - runOrphanReaper(1000, 1000, 0); + runOrphanReaper(1000, 0); } public void testRemoveNodes() throws Throwable @@ -191,10 +191,10 @@ public class PurgeTestP extends AVMServiceTestBase private void runOrphanReaper() { // use configured defaults (eg. 50, 1000, 1000) - runOrphanReaper(-1, -1, -1); + runOrphanReaper(-1, -1); } - private void runOrphanReaper(int batchSize, int maxQueueLength, int activeBaseSleep) + private void runOrphanReaper(int batchSize, int activeBaseSleep) { logger.info("Reaper started"); @@ -203,11 +203,6 @@ public class PurgeTestP extends AVMServiceTestBase fReaper.setBatchSize(batchSize); } - if (maxQueueLength != -1) - { - fReaper.setMaxQueueLength(maxQueueLength); - } - if (activeBaseSleep != -1) { fReaper.setActiveBaseSleep(activeBaseSleep); @@ -245,11 +240,6 @@ public class PurgeTestP extends AVMServiceTestBase fReaper.setBatchSize(50); } - if (maxQueueLength != -1) - { - fReaper.setMaxQueueLength(1000); - } - if (activeBaseSleep != -1) { fReaper.setActiveBaseSleep(1000); diff --git a/source/java/org/alfresco/repo/node/index/FullIndexRecoveryComponent.java b/source/java/org/alfresco/repo/node/index/FullIndexRecoveryComponent.java index 3266284287..69e6688fc9 100644 --- a/source/java/org/alfresco/repo/node/index/FullIndexRecoveryComponent.java +++ b/source/java/org/alfresco/repo/node/index/FullIndexRecoveryComponent.java @@ -19,7 +19,6 @@ package org.alfresco.repo.node.index; import java.util.ArrayList; -import java.util.Collections; import java.util.Date; import java.util.Iterator; import java.util.List; @@ -319,6 +318,7 @@ public class FullIndexRecoveryComponent extends AbstractReindexComponent } private static final int MAX_TRANSACTIONS_PER_ITERATION = 1000; + private static final long MIN_SAMPLE_TIME = 10000L; private void performFullRecovery() { RetryingTransactionCallback deleteWork = new RetryingTransactionCallback() @@ -346,11 +346,19 @@ public class FullIndexRecoveryComponent extends AbstractReindexComponent // count the transactions int processedCount = 0; - long fromTimeInclusive = Long.MIN_VALUE; - long toTimeExclusive = Long.MAX_VALUE; - List lastTxnIds = Collections.emptyList(); + long fromTimeInclusive = nodeDAO.getMinTxnCommitTime(); + long maxToTimeExclusive = nodeDAO.getMaxTxnCommitTime() + 1; + // Our first sample will be 10 seconds long (as we often hit 'fake' transactions with time zero). We'll rebalance intervals from there... + long toTimeExclusive = fromTimeInclusive + MIN_SAMPLE_TIME; + long sampleStartTimeInclusive = fromTimeInclusive; + long sampleEndTimeExclusive = -1; + long txnsPerSample = 0; + List lastTxnIds = new ArrayList(MAX_TRANSACTIONS_PER_ITERATION); while(true) - { + { + + boolean startedSampleForQuery = false; + List nextTxns = nodeDAO.getTxnsByCommitTimeAscending( fromTimeInclusive, toTimeExclusive, @@ -358,7 +366,16 @@ public class FullIndexRecoveryComponent extends AbstractReindexComponent lastTxnIds, false); - lastTxnIds = new ArrayList(nextTxns.size()); + // have we finished? + if (nextTxns.size() == 0) + { + if (toTimeExclusive >= maxToTimeExclusive) + { + // there are no more + break; + } + } + // reindex each transaction List txnIdBuffer = new ArrayList(maxTransactionsPerLuceneCommit); Iterator txnIterator = nextTxns.iterator(); @@ -366,8 +383,27 @@ public class FullIndexRecoveryComponent extends AbstractReindexComponent { Transaction txn = txnIterator.next(); Long txnId = txn.getId(); - // Keep it to ensure we exclude it from the next iteration + // Remember the IDs of the last simultaneous transactions so they can be excluded from the next query + long txnCommitTime = txn.getCommitTimeMs(); + if (lastTxnIds.isEmpty() || txnCommitTime != fromTimeInclusive) + { + if (!startedSampleForQuery) + { + sampleStartTimeInclusive = txnCommitTime; + sampleEndTimeExclusive = -1; + txnsPerSample = 0; + startedSampleForQuery = true; + } + else + { + txnsPerSample += lastTxnIds.size(); + sampleEndTimeExclusive = txnCommitTime; + } + lastTxnIds.clear(); + fromTimeInclusive = txnCommitTime; + } lastTxnIds.add(txnId); + // check if we have to terminate if (isShuttingDown()) { @@ -399,11 +435,8 @@ public class FullIndexRecoveryComponent extends AbstractReindexComponent // Clear the buffer txnIdBuffer = new ArrayList(maxTransactionsPerLuceneCommit); } - } - // Although we use the same time as this transaction for the next iteration, we also - // make use of the exclusion list to ensure that it doesn't get pulled back again. - fromTimeInclusive = txn.getCommitTimeMs(); - + } + // dump a progress report every 10% of the way double before = (double) processedCount / (double) txnCount * 10.0; // 0 - 10 processedCount++; @@ -419,12 +452,31 @@ public class FullIndexRecoveryComponent extends AbstractReindexComponent // Wait for the asynchronous process to catch up waitForAsynchronousReindexing(); - // have we finished? - if (nextTxns.size() == 0) + // Move the start marker on and extend the sample time if we have completed results + if (nextTxns.size() < MAX_TRANSACTIONS_PER_ITERATION) { - // there are no more - break; + // Move past the query end + if (!lastTxnIds.isEmpty()) + { + txnsPerSample += lastTxnIds.size(); + lastTxnIds.clear(); + } + fromTimeInclusive = toTimeExclusive; + sampleEndTimeExclusive = toTimeExclusive; } + + // Move the end marker on based on the current transaction rate + long sampleTime; + if (txnsPerSample == 0) + { + sampleTime = MIN_SAMPLE_TIME; + } + else + { + sampleTime = Math.max(MIN_SAMPLE_TIME, MAX_TRANSACTIONS_PER_ITERATION + * (sampleEndTimeExclusive - sampleStartTimeInclusive) / txnsPerSample); + } + toTimeExclusive = fromTimeInclusive + sampleTime; } // done String msgDone = I18NUtil.getMessage(MSG_RECOVERY_COMPLETE); diff --git a/source/java/org/alfresco/repo/rendition/RenditionNodeManager.java b/source/java/org/alfresco/repo/rendition/RenditionNodeManager.java index a8717917ef..7d9417d24b 100644 --- a/source/java/org/alfresco/repo/rendition/RenditionNodeManager.java +++ b/source/java/org/alfresco/repo/rendition/RenditionNodeManager.java @@ -333,7 +333,12 @@ public class RenditionNodeManager } else { - result = createNewRendition(renditionName); + QName nodeType = null; + if (null != tempRenditionNode) + { + nodeType = nodeService.getType(tempRenditionNode); + } + result = createNewRendition(renditionName, nodeType); } if (logger.isDebugEnabled()) @@ -375,15 +380,19 @@ public class RenditionNodeManager * a child of the source node. * * @param renditionName + * @param nodeTypeQName * @return the primary parent association of the newly created rendition node. */ - private ChildAssociationRef createNewRendition(QName renditionName) + private ChildAssociationRef createNewRendition(QName renditionName, QName nodeTypeQName) { NodeRef parentRef = location.getParentRef(); boolean parentIsSource = parentRef.equals(sourceNode); QName renditionType = RenditionModel.ASSOC_RENDITION; QName assocTypeQName = parentIsSource ? renditionType : ContentModel.ASSOC_CONTAINS; - QName nodeTypeQName = ContentModel.TYPE_CONTENT; + if (null == nodeTypeQName) + { + nodeTypeQName = ContentModel.TYPE_CONTENT; + } QName assocName = getAssociationName(parentIsSource, renditionName); @@ -482,9 +491,12 @@ public class RenditionNodeManager logger.debug(msg.toString()); } - // Copy the type from the temporary rendition to the real rendition + // Copy the type from the temporary rendition to the real rendition, if required QName type = nodeService.getType(tempRenditionNode); - nodeService.setType(targetNode, type); + if ((null != type) && !type.equals(nodeService.getType(targetNode))) + { + nodeService.setType(targetNode, type); + } // Copy over all regular properties from the temporary rendition Map newProps = new HashMap(); diff --git a/source/java/org/alfresco/repo/template/BaseContentNode.java b/source/java/org/alfresco/repo/template/BaseContentNode.java index 7aac226e39..1d1fa30936 100644 --- a/source/java/org/alfresco/repo/template/BaseContentNode.java +++ b/source/java/org/alfresco/repo/template/BaseContentNode.java @@ -617,7 +617,7 @@ public abstract class BaseContentNode implements TemplateContent ContentReader reader = contentService.getReader(getNodeRef(), property); // get the writer and set it up for text convert - ContentWriter writer = contentService.getWriter(null, ContentModel.PROP_CONTENT, true); + ContentWriter writer = contentService.getTempWriter(); writer.setMimetype("text/plain"); writer.setEncoding(reader.getEncoding()); diff --git a/source/java/org/alfresco/service/ServiceRegistry.java b/source/java/org/alfresco/service/ServiceRegistry.java index 78f11c9566..7f5dc117f4 100644 --- a/source/java/org/alfresco/service/ServiceRegistry.java +++ b/source/java/org/alfresco/service/ServiceRegistry.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2011 Alfresco Software Limited. * * This file is part of Alfresco * @@ -87,7 +87,6 @@ import org.alfresco.wcm.webproject.WebProjectService; * * @author David Caruana */ -@PublicService public interface ServiceRegistry { // Service Bean Names diff --git a/source/java/org/alfresco/service/cmr/action/ActionService.java b/source/java/org/alfresco/service/cmr/action/ActionService.java index decfa368d3..dd68586664 100644 --- a/source/java/org/alfresco/service/cmr/action/ActionService.java +++ b/source/java/org/alfresco/service/cmr/action/ActionService.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2011 Alfresco Software Limited. * * This file is part of Alfresco * @@ -31,7 +31,6 @@ import org.alfresco.service.cmr.repository.NodeRef; * * @author Roy Wetherall */ -@PublicService public interface ActionService { /** diff --git a/source/java/org/alfresco/service/cmr/action/ActionTrackingService.java b/source/java/org/alfresco/service/cmr/action/ActionTrackingService.java index 9c88d76dee..f507047a65 100644 --- a/source/java/org/alfresco/service/cmr/action/ActionTrackingService.java +++ b/source/java/org/alfresco/service/cmr/action/ActionTrackingService.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2011 Alfresco Software Limited. * * This file is part of Alfresco * @@ -28,7 +28,6 @@ import org.alfresco.service.PublicService; * * @author Nick Burch */ -@PublicService public interface ActionTrackingService { /** diff --git a/source/java/org/alfresco/service/cmr/activities/ActivityService.java b/source/java/org/alfresco/service/cmr/activities/ActivityService.java index cbc580d844..d63ddfe8dd 100644 --- a/source/java/org/alfresco/service/cmr/activities/ActivityService.java +++ b/source/java/org/alfresco/service/cmr/activities/ActivityService.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2011 Alfresco Software Limited. * * This file is part of Alfresco * @@ -28,7 +28,6 @@ import org.alfresco.service.PublicService; /** * The activity service */ -@PublicService public interface ActivityService extends ActivityPostService { /* diff --git a/source/java/org/alfresco/service/cmr/admin/RepoAdminService.java b/source/java/org/alfresco/service/cmr/admin/RepoAdminService.java index 646989f7db..37e4e4af35 100644 --- a/source/java/org/alfresco/service/cmr/admin/RepoAdminService.java +++ b/source/java/org/alfresco/service/cmr/admin/RepoAdminService.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2011 Alfresco Software Limited. * * This file is part of Alfresco * @@ -34,7 +34,6 @@ import org.alfresco.service.namespace.QName; * Client facing API for interacting with Alfresco Repository Admin services. * */ -@PublicService public interface RepoAdminService { // diff --git a/source/java/org/alfresco/service/cmr/audit/AuditService.java b/source/java/org/alfresco/service/cmr/audit/AuditService.java index 0ea3857f8d..f9b346ff5b 100644 --- a/source/java/org/alfresco/service/cmr/audit/AuditService.java +++ b/source/java/org/alfresco/service/cmr/audit/AuditService.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2011 Alfresco Software Limited. * * This file is part of Alfresco * @@ -29,7 +29,6 @@ import org.alfresco.service.PublicService; * * @author Derek Hulley */ -@PublicService public interface AuditService { /** diff --git a/source/java/org/alfresco/service/cmr/avm/deploy/DeploymentService.java b/source/java/org/alfresco/service/cmr/avm/deploy/DeploymentService.java index 5063fc3405..3fa8258fe5 100644 --- a/source/java/org/alfresco/service/cmr/avm/deploy/DeploymentService.java +++ b/source/java/org/alfresco/service/cmr/avm/deploy/DeploymentService.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2011 Alfresco Software Limited. * * This file is part of Alfresco * @@ -31,7 +31,6 @@ import org.alfresco.service.NotAuditable; * A service to handle WCM AVM repository to remote AVM repository deployment. * @author britt */ -@PublicService public interface DeploymentService { /** diff --git a/source/java/org/alfresco/service/cmr/coci/CheckOutCheckInService.java b/source/java/org/alfresco/service/cmr/coci/CheckOutCheckInService.java index c4bb84de75..44b08fcc55 100644 --- a/source/java/org/alfresco/service/cmr/coci/CheckOutCheckInService.java +++ b/source/java/org/alfresco/service/cmr/coci/CheckOutCheckInService.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2011 Alfresco Software Limited. * * This file is part of Alfresco * @@ -32,7 +32,6 @@ import org.alfresco.service.namespace.QName; * * @author Roy Wetherall */ -@PublicService public interface CheckOutCheckInService { /** diff --git a/source/java/org/alfresco/service/cmr/email/EmailService.java b/source/java/org/alfresco/service/cmr/email/EmailService.java index 566eed5bb7..9408724730 100644 --- a/source/java/org/alfresco/service/cmr/email/EmailService.java +++ b/source/java/org/alfresco/service/cmr/email/EmailService.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2011 Alfresco Software Limited. * * This file is part of Alfresco * @@ -35,7 +35,6 @@ import org.alfresco.service.cmr.repository.NodeRef; * @since 2.2 * @author Derek Hulley */ -@PublicService public interface EmailService { /** diff --git a/source/java/org/alfresco/service/cmr/invitation/InvitationService.java b/source/java/org/alfresco/service/cmr/invitation/InvitationService.java index 20814515a4..2a24bc253e 100644 --- a/source/java/org/alfresco/service/cmr/invitation/InvitationService.java +++ b/source/java/org/alfresco/service/cmr/invitation/InvitationService.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2011 Alfresco Software Limited. * * This file is part of Alfresco * @@ -34,7 +34,6 @@ import org.alfresco.service.PublicService; * * @author mrogers */ -@PublicService public interface InvitationService { /** diff --git a/source/java/org/alfresco/service/cmr/lock/LockService.java b/source/java/org/alfresco/service/cmr/lock/LockService.java index 0162c92c8c..d200d06c5b 100644 --- a/source/java/org/alfresco/service/cmr/lock/LockService.java +++ b/source/java/org/alfresco/service/cmr/lock/LockService.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2011 Alfresco Software Limited. * * This file is part of Alfresco * @@ -32,7 +32,6 @@ import org.alfresco.service.cmr.repository.StoreRef; * * @author Roy Wetherall */ -@PublicService public interface LockService { /** diff --git a/source/java/org/alfresco/service/cmr/ml/ContentFilterLanguagesService.java b/source/java/org/alfresco/service/cmr/ml/ContentFilterLanguagesService.java index 5734ac7d86..c4420dc89d 100644 --- a/source/java/org/alfresco/service/cmr/ml/ContentFilterLanguagesService.java +++ b/source/java/org/alfresco/service/cmr/ml/ContentFilterLanguagesService.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2011 Alfresco Software Limited. * * This file is part of Alfresco * @@ -32,7 +32,6 @@ import org.alfresco.service.PublicService; * @author Yannick Pignot * */ -@PublicService public interface ContentFilterLanguagesService { diff --git a/source/java/org/alfresco/service/cmr/ml/EditionService.java b/source/java/org/alfresco/service/cmr/ml/EditionService.java index 7443f812d9..d563f0d0c5 100644 --- a/source/java/org/alfresco/service/cmr/ml/EditionService.java +++ b/source/java/org/alfresco/service/cmr/ml/EditionService.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2011 Alfresco Software Limited. * * This file is part of Alfresco * @@ -35,7 +35,6 @@ import org.alfresco.service.namespace.QName; * @since 2.1 * @author Yannick Pignot */ -@PublicService public interface EditionService { /** diff --git a/source/java/org/alfresco/service/cmr/ml/MultilingualContentService.java b/source/java/org/alfresco/service/cmr/ml/MultilingualContentService.java index a9f13f451a..12f684e2e2 100644 --- a/source/java/org/alfresco/service/cmr/ml/MultilingualContentService.java +++ b/source/java/org/alfresco/service/cmr/ml/MultilingualContentService.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2011 Alfresco Software Limited. * * This file is part of Alfresco * @@ -35,7 +35,6 @@ import org.alfresco.service.cmr.repository.NodeRef; * @author Derek Hulley * @author Philippe Dubois */ -@PublicService public interface MultilingualContentService { /** diff --git a/source/java/org/alfresco/service/cmr/model/FileFolderService.java b/source/java/org/alfresco/service/cmr/model/FileFolderService.java index c12d939223..eaa70cf126 100644 --- a/source/java/org/alfresco/service/cmr/model/FileFolderService.java +++ b/source/java/org/alfresco/service/cmr/model/FileFolderService.java @@ -42,7 +42,6 @@ import org.springframework.extensions.surf.util.I18NUtil; * * @author Derek Hulley */ -@PublicService public interface FileFolderService { /** diff --git a/source/java/org/alfresco/service/cmr/rating/RatingService.java b/source/java/org/alfresco/service/cmr/rating/RatingService.java index e4af169cd6..b7df6b4b49 100644 --- a/source/java/org/alfresco/service/cmr/rating/RatingService.java +++ b/source/java/org/alfresco/service/cmr/rating/RatingService.java @@ -43,7 +43,6 @@ import org.alfresco.service.cmr.repository.NodeRef; * @author Neil McErlean * @since 3.4 */ -@PublicService public interface RatingService { /** diff --git a/source/java/org/alfresco/service/cmr/rendition/RenditionService.java b/source/java/org/alfresco/service/cmr/rendition/RenditionService.java index 7754314f63..ad6b42538b 100644 --- a/source/java/org/alfresco/service/cmr/rendition/RenditionService.java +++ b/source/java/org/alfresco/service/cmr/rendition/RenditionService.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2011 Alfresco Software Limited. * * This file is part of Alfresco * @@ -34,7 +34,6 @@ import org.alfresco.service.NotAuditable; * @author Nick Smith * @author Neil McErlean */ -@PublicService public interface RenditionService extends RenditionDefinitionPersister { /** diff --git a/source/java/org/alfresco/service/cmr/replication/ReplicationService.java b/source/java/org/alfresco/service/cmr/replication/ReplicationService.java index b73200f1c8..676cc8df19 100644 --- a/source/java/org/alfresco/service/cmr/replication/ReplicationService.java +++ b/source/java/org/alfresco/service/cmr/replication/ReplicationService.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2011 Alfresco Software Limited. * * This file is part of Alfresco * @@ -26,7 +26,6 @@ import org.alfresco.service.PublicService; * The Replication service. * @author Nick Burch */ -@PublicService public interface ReplicationService extends ReplicationDefinitionPersister { /** * Creates a new {@link ReplicationDefinition} and sets the replication diff --git a/source/java/org/alfresco/service/cmr/repository/ContentService.java b/source/java/org/alfresco/service/cmr/repository/ContentService.java index 0509ce5654..03272e1e00 100644 --- a/source/java/org/alfresco/service/cmr/repository/ContentService.java +++ b/source/java/org/alfresco/service/cmr/repository/ContentService.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2011 Alfresco Software Limited. * * This file is part of Alfresco * @@ -48,7 +48,6 @@ import org.alfresco.service.namespace.QName; * * @author Derek Hulley */ -@PublicService public interface ContentService { /** diff --git a/source/java/org/alfresco/service/cmr/repository/CopyService.java b/source/java/org/alfresco/service/cmr/repository/CopyService.java index c5d710cf41..cbd7da67fd 100644 --- a/source/java/org/alfresco/service/cmr/repository/CopyService.java +++ b/source/java/org/alfresco/service/cmr/repository/CopyService.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2011 Alfresco Software Limited. * * This file is part of Alfresco * @@ -32,7 +32,6 @@ import org.alfresco.service.namespace.QName; * * @author Roy Wetherall */ -@PublicService public interface CopyService { /** diff --git a/source/java/org/alfresco/service/cmr/repository/ScriptService.java b/source/java/org/alfresco/service/cmr/repository/ScriptService.java index 1788e29699..21058196f9 100644 --- a/source/java/org/alfresco/service/cmr/repository/ScriptService.java +++ b/source/java/org/alfresco/service/cmr/repository/ScriptService.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2011 Alfresco Software Limited. * * This file is part of Alfresco * @@ -40,7 +40,6 @@ import org.alfresco.service.namespace.QName; * * @author Kevin Roast */ -@PublicService public interface ScriptService { /** diff --git a/source/java/org/alfresco/service/cmr/repository/TemplateService.java b/source/java/org/alfresco/service/cmr/repository/TemplateService.java index f4df7bd878..0119ec36f0 100644 --- a/source/java/org/alfresco/service/cmr/repository/TemplateService.java +++ b/source/java/org/alfresco/service/cmr/repository/TemplateService.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2011 Alfresco Software Limited. * * This file is part of Alfresco * @@ -38,7 +38,6 @@ import org.alfresco.service.PublicService; * * @author Kevin Roast */ -@PublicService public interface TemplateService { /** Keys for default model values */ diff --git a/source/java/org/alfresco/service/cmr/rule/RuleService.java b/source/java/org/alfresco/service/cmr/rule/RuleService.java index 1d977404ac..96624d0c5e 100644 --- a/source/java/org/alfresco/service/cmr/rule/RuleService.java +++ b/source/java/org/alfresco/service/cmr/rule/RuleService.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2011 Alfresco Software Limited. * * This file is part of Alfresco * @@ -30,7 +30,6 @@ import org.alfresco.service.cmr.repository.NodeRef; * * @author Roy Wetherall */ -@PublicService public interface RuleService { /** diff --git a/source/java/org/alfresco/service/cmr/search/CategoryService.java b/source/java/org/alfresco/service/cmr/search/CategoryService.java index 83e3187f0b..9814e4f355 100644 --- a/source/java/org/alfresco/service/cmr/search/CategoryService.java +++ b/source/java/org/alfresco/service/cmr/search/CategoryService.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2011 Alfresco Software Limited. * * This file is part of Alfresco * @@ -42,7 +42,6 @@ import org.alfresco.util.Pair; * @author Andy Hind * */ -@PublicService public interface CategoryService { /** diff --git a/source/java/org/alfresco/service/cmr/security/AuthenticationService.java b/source/java/org/alfresco/service/cmr/security/AuthenticationService.java index e7d17f8406..4a2f23e452 100644 --- a/source/java/org/alfresco/service/cmr/security/AuthenticationService.java +++ b/source/java/org/alfresco/service/cmr/security/AuthenticationService.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2011 Alfresco Software Limited. * * This file is part of Alfresco * @@ -30,7 +30,6 @@ import org.alfresco.service.PublicService; * * @author Andy Hind */ -@PublicService public interface AuthenticationService { /** diff --git a/source/java/org/alfresco/service/cmr/security/AuthorityService.java b/source/java/org/alfresco/service/cmr/security/AuthorityService.java index 2917ce74b8..d069b12364 100644 --- a/source/java/org/alfresco/service/cmr/security/AuthorityService.java +++ b/source/java/org/alfresco/service/cmr/security/AuthorityService.java @@ -40,7 +40,6 @@ import org.alfresco.service.cmr.repository.NodeRef; * * @author Andy Hind */ -@PublicService public interface AuthorityService { /** diff --git a/source/java/org/alfresco/service/cmr/security/OwnableService.java b/source/java/org/alfresco/service/cmr/security/OwnableService.java index 5c0c43445f..9894c191e2 100644 --- a/source/java/org/alfresco/service/cmr/security/OwnableService.java +++ b/source/java/org/alfresco/service/cmr/security/OwnableService.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2011 Alfresco Software Limited. * * This file is part of Alfresco * @@ -27,7 +27,6 @@ import org.alfresco.service.cmr.repository.NodeRef; * * @author Andy Hind */ -@PublicService public interface OwnableService { public static String NO_OWNER = ""; diff --git a/source/java/org/alfresco/service/cmr/security/PersonService.java b/source/java/org/alfresco/service/cmr/security/PersonService.java index b69cd4ac69..0266052c84 100644 --- a/source/java/org/alfresco/service/cmr/security/PersonService.java +++ b/source/java/org/alfresco/service/cmr/security/PersonService.java @@ -43,7 +43,6 @@ import org.alfresco.util.Pair; * * @author Andy Hind */ -@PublicService public interface PersonService { /** diff --git a/source/java/org/alfresco/service/cmr/security/PublicServiceAccessService.java b/source/java/org/alfresco/service/cmr/security/PublicServiceAccessService.java index 625fd3ab52..8723ed84ac 100644 --- a/source/java/org/alfresco/service/cmr/security/PublicServiceAccessService.java +++ b/source/java/org/alfresco/service/cmr/security/PublicServiceAccessService.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2011 Alfresco Software Limited. * * This file is part of Alfresco * @@ -27,7 +27,6 @@ import org.alfresco.service.PublicService; * @author andyh * */ -@PublicService public interface PublicServiceAccessService { /** diff --git a/source/java/org/alfresco/service/cmr/site/SiteService.java b/source/java/org/alfresco/service/cmr/site/SiteService.java index 0ef94ff9d1..3158a60bd7 100644 --- a/source/java/org/alfresco/service/cmr/site/SiteService.java +++ b/source/java/org/alfresco/service/cmr/site/SiteService.java @@ -36,7 +36,6 @@ import org.alfresco.service.namespace.QName; * * @author Roy Wetherall */ -@PublicService public interface SiteService { static String DOCUMENT_LIBRARY = "documentLibrary"; diff --git a/source/java/org/alfresco/service/cmr/subscriptions/SubscriptionService.java b/source/java/org/alfresco/service/cmr/subscriptions/SubscriptionService.java index 890cffb505..8677854f7e 100644 --- a/source/java/org/alfresco/service/cmr/subscriptions/SubscriptionService.java +++ b/source/java/org/alfresco/service/cmr/subscriptions/SubscriptionService.java @@ -21,7 +21,6 @@ package org.alfresco.service.cmr.subscriptions; import org.alfresco.query.PagingRequest; import org.alfresco.service.Auditable; import org.alfresco.service.NotAuditable; -import org.alfresco.service.PublicService; import org.alfresco.service.cmr.repository.NodeRef; /** @@ -30,7 +29,6 @@ import org.alfresco.service.cmr.repository.NodeRef; * @author Florian Mueller * @since 4.0 */ -@PublicService public interface SubscriptionService { // --- subscription --- diff --git a/source/java/org/alfresco/service/cmr/tagging/TaggingService.java b/source/java/org/alfresco/service/cmr/tagging/TaggingService.java index faff1f3216..0c4da6326f 100644 --- a/source/java/org/alfresco/service/cmr/tagging/TaggingService.java +++ b/source/java/org/alfresco/service/cmr/tagging/TaggingService.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2011 Alfresco Software Limited. * * This file is part of Alfresco * @@ -31,7 +31,6 @@ import org.alfresco.service.cmr.repository.StoreRef; * * @author Roy Wetherall */ -@PublicService public interface TaggingService { /** diff --git a/source/java/org/alfresco/service/cmr/transfer/TransferService.java b/source/java/org/alfresco/service/cmr/transfer/TransferService.java index 46e0e49fb4..60f9106735 100644 --- a/source/java/org/alfresco/service/cmr/transfer/TransferService.java +++ b/source/java/org/alfresco/service/cmr/transfer/TransferService.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2010 Alfresco Software Limited. + * Copyright (C) 2009-2011 Alfresco Software Limited. * * This file is part of Alfresco * @@ -35,7 +35,6 @@ import org.alfresco.service.NotAuditable; * * @author Mark Rogers */ -@PublicService @Deprecated public interface TransferService { diff --git a/source/java/org/alfresco/service/cmr/transfer/TransferService2.java b/source/java/org/alfresco/service/cmr/transfer/TransferService2.java index c1f5834aa7..5174a2231e 100644 --- a/source/java/org/alfresco/service/cmr/transfer/TransferService2.java +++ b/source/java/org/alfresco/service/cmr/transfer/TransferService2.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2010 Alfresco Software Limited. + * Copyright (C) 2009-2011 Alfresco Software Limited. * * This file is part of Alfresco * @@ -32,7 +32,6 @@ import org.alfresco.service.PublicService; * * @author davidc */ -@PublicService public interface TransferService2 { diff --git a/source/java/org/alfresco/service/cmr/usage/ContentUsageService.java b/source/java/org/alfresco/service/cmr/usage/ContentUsageService.java index 74db34276c..369a2a34a2 100644 --- a/source/java/org/alfresco/service/cmr/usage/ContentUsageService.java +++ b/source/java/org/alfresco/service/cmr/usage/ContentUsageService.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2011 Alfresco Software Limited. * * This file is part of Alfresco * @@ -21,7 +21,6 @@ package org.alfresco.service.cmr.usage; import org.alfresco.service.Auditable; import org.alfresco.service.PublicService; -@PublicService public interface ContentUsageService { /** diff --git a/source/java/org/alfresco/service/cmr/usage/UsageService.java b/source/java/org/alfresco/service/cmr/usage/UsageService.java index f1d8f6f6dd..277701a14e 100644 --- a/source/java/org/alfresco/service/cmr/usage/UsageService.java +++ b/source/java/org/alfresco/service/cmr/usage/UsageService.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2011 Alfresco Software Limited. * * This file is part of Alfresco * @@ -30,7 +30,6 @@ import org.alfresco.service.cmr.repository.NodeRef; * @author janv * @since 2.9, 3.0 */ -@PublicService public interface UsageService { /** diff --git a/source/java/org/alfresco/service/cmr/version/VersionService.java b/source/java/org/alfresco/service/cmr/version/VersionService.java index fa28165070..5a3320bc54 100644 --- a/source/java/org/alfresco/service/cmr/version/VersionService.java +++ b/source/java/org/alfresco/service/cmr/version/VersionService.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2011 Alfresco Software Limited. * * This file is part of Alfresco * @@ -35,7 +35,6 @@ import org.alfresco.service.namespace.QName; * * @author Roy Wetherall, janv */ -@PublicService public interface VersionService { /** diff --git a/source/java/org/alfresco/service/cmr/view/ExporterService.java b/source/java/org/alfresco/service/cmr/view/ExporterService.java index 9d97928109..c948be5aa3 100644 --- a/source/java/org/alfresco/service/cmr/view/ExporterService.java +++ b/source/java/org/alfresco/service/cmr/view/ExporterService.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2011 Alfresco Software Limited. * * This file is part of Alfresco * @@ -29,7 +29,6 @@ import org.alfresco.service.PublicService; * * @author David Caruana */ -@PublicService public interface ExporterService { /** diff --git a/source/java/org/alfresco/service/cmr/view/ImporterService.java b/source/java/org/alfresco/service/cmr/view/ImporterService.java index 573827bcb0..892d29cd60 100644 --- a/source/java/org/alfresco/service/cmr/view/ImporterService.java +++ b/source/java/org/alfresco/service/cmr/view/ImporterService.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2011 Alfresco Software Limited. * * This file is part of Alfresco * @@ -30,7 +30,6 @@ import org.alfresco.service.PublicService; * @author David Caruana * */ -@PublicService public interface ImporterService { diff --git a/source/java/org/alfresco/service/cmr/view/RepositoryExporterService.java b/source/java/org/alfresco/service/cmr/view/RepositoryExporterService.java index 1482b6c5ba..b006a6af30 100644 --- a/source/java/org/alfresco/service/cmr/view/RepositoryExporterService.java +++ b/source/java/org/alfresco/service/cmr/view/RepositoryExporterService.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2011 Alfresco Software Limited. * * This file is part of Alfresco * @@ -31,7 +31,6 @@ import org.alfresco.service.cmr.repository.StoreRef; * * @author davidc */ -@PublicService public interface RepositoryExporterService { diff --git a/source/java/org/alfresco/service/cmr/workflow/WorkflowService.java b/source/java/org/alfresco/service/cmr/workflow/WorkflowService.java index f5e072bd77..4aaa7da9a1 100644 --- a/source/java/org/alfresco/service/cmr/workflow/WorkflowService.java +++ b/source/java/org/alfresco/service/cmr/workflow/WorkflowService.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2011 Alfresco Software Limited. * * This file is part of Alfresco * @@ -36,7 +36,6 @@ import org.alfresco.service.namespace.QName; * * @author davidc */ -@PublicService public interface WorkflowService { // diff --git a/source/java/org/alfresco/service/license/LicenseService.java b/source/java/org/alfresco/service/license/LicenseService.java index 04e3917aaa..e1b82a1695 100644 --- a/source/java/org/alfresco/service/license/LicenseService.java +++ b/source/java/org/alfresco/service/license/LicenseService.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2011 Alfresco Software Limited. * * This file is part of Alfresco * @@ -26,7 +26,6 @@ import org.alfresco.service.PublicService; * * @author davidc */ -@PublicService public interface LicenseService { /** diff --git a/source/java/org/alfresco/service/transaction/TransactionService.java b/source/java/org/alfresco/service/transaction/TransactionService.java index b56b072c5f..c99a2a1730 100644 --- a/source/java/org/alfresco/service/transaction/TransactionService.java +++ b/source/java/org/alfresco/service/transaction/TransactionService.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2011 Alfresco Software Limited. * * This file is part of Alfresco * @@ -33,7 +33,6 @@ import org.alfresco.service.PublicService; * * @author David Caruana */ -@PublicService public interface TransactionService { /** diff --git a/source/java/org/alfresco/wcm/asset/AssetService.java b/source/java/org/alfresco/wcm/asset/AssetService.java index 7e060cd52d..a03866dfc6 100644 --- a/source/java/org/alfresco/wcm/asset/AssetService.java +++ b/source/java/org/alfresco/wcm/asset/AssetService.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2011 Alfresco Software Limited. * * This file is part of Alfresco * @@ -38,7 +38,6 @@ import org.alfresco.service.NotAuditable; * * @author janv */ -@PublicService public interface AssetService { /** diff --git a/source/java/org/alfresco/wcm/sandbox/SandboxService.java b/source/java/org/alfresco/wcm/sandbox/SandboxService.java index 6c1067d892..b5701e098d 100644 --- a/source/java/org/alfresco/wcm/sandbox/SandboxService.java +++ b/source/java/org/alfresco/wcm/sandbox/SandboxService.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2011 Alfresco Software Limited. * * This file is part of Alfresco * @@ -37,7 +37,6 @@ import org.alfresco.service.NotAuditable; * * @author janv */ -@PublicService public interface SandboxService { /** diff --git a/source/java/org/alfresco/wcm/webproject/WebProjectService.java b/source/java/org/alfresco/wcm/webproject/WebProjectService.java index 1142c275f3..03de704e16 100644 --- a/source/java/org/alfresco/wcm/webproject/WebProjectService.java +++ b/source/java/org/alfresco/wcm/webproject/WebProjectService.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2011 Alfresco Software Limited. * * This file is part of Alfresco * @@ -34,7 +34,6 @@ import org.alfresco.service.NotAuditable; * * @author janv */ -@PublicService public interface WebProjectService { // diff --git a/source/test-resources/activities/org/alfresco/site/user-joined.atomentry.ftl b/source/test-resources/activities/org/alfresco/site/user-joined.atomentry.ftl index bdcd1f57ed..b94cf73120 100644 --- a/source/test-resources/activities/org/alfresco/site/user-joined.atomentry.ftl +++ b/source/test-resources/activities/org/alfresco/site/user-joined.atomentry.ftl @@ -1,10 +1,10 @@ - ${memberFirstName?xml}<#if memberLastName??> ${memberLastName?xml}</#if> joined ${siteNetwork?xml} site + <#if memberFirstName??>${memberFirstName?xml}</#if><#if memberLastName??> ${memberLastName?xml}</#if> joined ${siteNetwork?xml} site ${id} ${xmldate(date)} -

${memberFirstName?xml}<#if memberLastName??> ${memberLastName?xml} joined ${siteNetwork?xml} site (with role ${role}) + <#if memberFirstName??>${memberFirstName?xml}<#if memberLastName??> ${memberLastName?xml} joined ${siteNetwork?xml} site (with role ${role}) ${userId} diff --git a/source/test-resources/activities/org/alfresco/site/user-left.atomentry.ftl b/source/test-resources/activities/org/alfresco/site/user-left.atomentry.ftl index a2fe4320c5..e86de34ec6 100644 --- a/source/test-resources/activities/org/alfresco/site/user-left.atomentry.ftl +++ b/source/test-resources/activities/org/alfresco/site/user-left.atomentry.ftl @@ -1,10 +1,10 @@ - ${memberFirstName?xml}<#if memberLastName??> ${memberLastName?xml}</#if> left ${siteNetwork?xml} site + <#if memberFirstName??>${memberFirstName?xml}</#if><#if memberLastName??> ${memberLastName?xml}</#if> left ${siteNetwork?xml} site ${id} ${xmldate(date)} - ${memberFirstName?xml}<#if memberLastName??> ${memberLastName?xml} left ${siteNetwork?xml} site + <#if memberFirstName??>${memberFirstName?xml}<#if memberLastName??> ${memberLastName?xml} left ${siteNetwork?xml} site ${userId} diff --git a/source/test-resources/activities/org/alfresco/site/user-role-changed.atomentry.ftl b/source/test-resources/activities/org/alfresco/site/user-role-changed.atomentry.ftl index 6f52b41ae3..4a7041c1eb 100644 --- a/source/test-resources/activities/org/alfresco/site/user-role-changed.atomentry.ftl +++ b/source/test-resources/activities/org/alfresco/site/user-role-changed.atomentry.ftl @@ -1,10 +1,10 @@ - ${memberFirstName?xml}<#if memberLastName??> ${memberLastName?xml}</#if> role changed for ${siteNetwork?xml} site + <#if memberFirstName??>${memberFirstName?xml}</#if><#if memberLastName??> ${memberLastName?xml}</#if> role changed for ${siteNetwork?xml} site ${id} ${xmldate(date)} - ${memberFirstName?xml}<#if memberLastName??> ${memberLastName?xml} role changed to ${role} for ${siteNetwork?xml} site + <#if memberFirstName??>${memberFirstName?xml}<#if memberLastName??> ${memberLastName?xml} role changed to ${role} for ${siteNetwork?xml} site ${userId}