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}