commit 85ff36503183403738df2011a7e5dc303c1d56f4 Author: Brian M. Long Date: Sun Apr 2 15:38:28 2023 -0400 initial checkin diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..620a11b --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +# Maven +target +pom.xml.versionsBackup + +# Eclipse +.settings +.project +.classpath + diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..2b70806 --- /dev/null +++ b/pom.xml @@ -0,0 +1,90 @@ + + 4.0.0 + + com.inteligr8.alfresco + annotations-platform-module + 1.0-SNAPSHOT + jar + + Annotations ACS Platform Module + A module to support annotation-based development for Alfresco Content Services modules. + + + scm:git:https://bitbucket.org/inteligr8/annotations-platform-module.git + scm:git:git@bitbucket.org:inteligr8/annotations-platform-module.git + https://bitbucket.org/inteligr8/annotations-platform-module + + + Inteligr8 + https://www.inteligr8.com + + + + brian.long + Brian Long + brian@inteligr8.com + https://twitter.com/brianmlong + + + + + UTF-8 + 8 + 8 + + 4.2.0 + 6.2.0-ga + + + + + + org.alfresco + acs-community-packaging + ${alfresco.platform.version} + pom + import + + + + + + + + + org.alfresco + alfresco-repository + provided + + + + + + + io.repaint.maven + tiles-maven-plugin + 2.26 + true + + + + com.inteligr8.ootbee:beedk-acs-platform-self-rad-tile:[1.0.0,2.0.0) + + com.inteligr8.ootbee:beedk-acs-platform-module-tile:[1.0.0,2.0.0) + + com.inteligr8.ootbee:beedk-acs-platform-self-it-tile:[1.0.0,2.0.0) + + + + + + + + + alfresco-public + https://artifacts.alfresco.com/nexus/content/groups/public + + + \ No newline at end of file diff --git a/rad.ps1 b/rad.ps1 new file mode 100644 index 0000000..61bcb2f --- /dev/null +++ b/rad.ps1 @@ -0,0 +1,74 @@ + +function discoverArtifactId { + $script:ARTIFACT_ID=(mvn -q -Dexpression=project"."artifactId -DforceStdout help:evaluate) +} + +function rebuild { + echo "Rebuilding project ..." + mvn process-classes +} + +function start_ { + echo "Rebuilding project and starting Docker containers to support rapid application development ..." + mvn -Drad process-classes +} + +function start_log { + echo "Rebuilding project and starting Docker containers to support rapid application development ..." + mvn -Drad "-Ddocker.showLogs" process-classes +} + +function stop_ { + discoverArtifactId + echo "Stopping Docker containers that supported rapid application development ..." + docker container ls --filter name=${ARTIFACT_ID}-* + echo "Stopping containers ..." + docker container stop (docker container ls -q --filter name=${ARTIFACT_ID}-*) + echo "Removing containers ..." + docker container rm (docker container ls -aq --filter name=${ARTIFACT_ID}-*) +} + +function tail_logs { + param ( + $container + ) + + discoverArtifactId + docker container logs -f (docker container ls -q --filter name=${ARTIFACT_ID}-${container}) +} + +function list { + discoverArtifactId + docker container ls --filter name=${ARTIFACT_ID}-* +} + +switch ($args[0]) { + "start" { + start_ + } + "start_log" { + start_log + } + "stop" { + stop_ + } + "restart" { + stop_ + start_ + } + "rebuild" { + rebuild + } + "tail" { + tail_logs $args[1] + } + "containers" { + list + } + default { + echo "Usage: .\rad.ps1 [ start | start_log | stop | restart | rebuild | tail {container} | containers ]" + } +} + +echo "Completed!" + diff --git a/rad.sh b/rad.sh new file mode 100644 index 0000000..7cb0a80 --- /dev/null +++ b/rad.sh @@ -0,0 +1,71 @@ +#!/bin/sh + +discoverArtifactId() { + ARTIFACT_ID=`mvn -q -Dexpression=project.artifactId -DforceStdout help:evaluate` +} + +rebuild() { + echo "Rebuilding project ..." + mvn process-classes +} + +start() { + echo "Rebuilding project and starting Docker containers to support rapid application development ..." + mvn -Drad process-classes +} + +start_log() { + echo "Rebuilding project and starting Docker containers to support rapid application development ..." + mvn -Drad -Ddocker.showLogs process-classes +} + +stop() { + discoverArtifactId + echo "Stopping Docker containers that supported rapid application development ..." + docker container ls --filter name=${ARTIFACT_ID}-* + echo "Stopping containers ..." + docker container stop `docker container ls -q --filter name=${ARTIFACT_ID}-*` + echo "Removing containers ..." + docker container rm `docker container ls -aq --filter name=${ARTIFACT_ID}-*` +} + +tail_logs() { + discoverArtifactId + docker container logs -f `docker container ls -q --filter name=${ARTIFACT_ID}-$1` +} + +list() { + discoverArtifactId + docker container ls --filter name=${ARTIFACT_ID}-* +} + +case "$1" in + start) + start + ;; + start_log) + start_log + ;; + stop) + stop + ;; + restart) + stop + start + ;; + rebuild) + rebuild + ;; + tail) + tail_logs $2 + ;; + containers) + list + ;; + *) + echo "Usage: ./rad.sh [ start | start_log | stop | restart | rebuild | tail {container} | containers ]" + exit 1 +esac + +echo "Completed!" + diff --git a/src/main/java/com/inteligr8/alfresco/annotations/AspectConstrainable.java b/src/main/java/com/inteligr8/alfresco/annotations/AspectConstrainable.java new file mode 100644 index 0000000..872bb63 --- /dev/null +++ b/src/main/java/com/inteligr8/alfresco/annotations/AspectConstrainable.java @@ -0,0 +1,42 @@ +package com.inteligr8.alfresco.annotations; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.namespace.QNamePattern; +import org.alfresco.service.namespace.RegexQNamePattern; +import org.springframework.beans.factory.BeanNameAware; + +public interface AspectConstrainable extends BeanNameAware { + + String getBeanName(); + + NamespaceService getNamespaceService(); + + default Collection constrainedPrefixedAspects() { + return Collections.emptySet(); + } + + default Collection constrainedRegexedAspects() { + return Collections.emptySet(); + } + + default Collection constrainedAspects() { + Set aspects = new HashSet<>(); + + Collection prefixedAspects = this.constrainedPrefixedAspects(); + for (String prefixedAspect : prefixedAspects) + aspects.add(QName.createQName(prefixedAspect, this.getNamespaceService())); + + Collection regexedAspects = this.constrainedRegexedAspects(); + for (String regexedAspect : regexedAspects) + aspects.add(new RegexQNamePattern(regexedAspect)); + + return aspects; + } + +} diff --git a/src/main/java/com/inteligr8/alfresco/annotations/AssociationTypeConstrainable.java b/src/main/java/com/inteligr8/alfresco/annotations/AssociationTypeConstrainable.java new file mode 100644 index 0000000..9dfecc8 --- /dev/null +++ b/src/main/java/com/inteligr8/alfresco/annotations/AssociationTypeConstrainable.java @@ -0,0 +1,11 @@ +package com.inteligr8.alfresco.annotations; + +import java.util.Collection; + +import org.alfresco.service.namespace.QNamePattern; + +public interface AssociationTypeConstrainable { + + Collection constrainedNodeTypes(); + +} diff --git a/src/main/java/com/inteligr8/alfresco/annotations/Asynchronous.java b/src/main/java/com/inteligr8/alfresco/annotations/Asynchronous.java new file mode 100644 index 0000000..a8b5531 --- /dev/null +++ b/src/main/java/com/inteligr8/alfresco/annotations/Asynchronous.java @@ -0,0 +1,12 @@ +package com.inteligr8.alfresco.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Asynchronous { + +} diff --git a/src/main/java/com/inteligr8/alfresco/annotations/Authorizable.java b/src/main/java/com/inteligr8/alfresco/annotations/Authorizable.java new file mode 100644 index 0000000..13c2d82 --- /dev/null +++ b/src/main/java/com/inteligr8/alfresco/annotations/Authorizable.java @@ -0,0 +1,7 @@ +package com.inteligr8.alfresco.annotations; + +public interface Authorizable { + + String authorizeAsUser(); + +} diff --git a/src/main/java/com/inteligr8/alfresco/annotations/Authorized.java b/src/main/java/com/inteligr8/alfresco/annotations/Authorized.java new file mode 100644 index 0000000..8af3698 --- /dev/null +++ b/src/main/java/com/inteligr8/alfresco/annotations/Authorized.java @@ -0,0 +1,14 @@ +package com.inteligr8.alfresco.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Authorized { + + String value() default ""; + +} diff --git a/src/main/java/com/inteligr8/alfresco/annotations/AuthorizedAsSystem.java b/src/main/java/com/inteligr8/alfresco/annotations/AuthorizedAsSystem.java new file mode 100644 index 0000000..7ddecac --- /dev/null +++ b/src/main/java/com/inteligr8/alfresco/annotations/AuthorizedAsSystem.java @@ -0,0 +1,12 @@ +package com.inteligr8.alfresco.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface AuthorizedAsSystem { + +} diff --git a/src/main/java/com/inteligr8/alfresco/annotations/BehaviorBean.java b/src/main/java/com/inteligr8/alfresco/annotations/BehaviorBean.java new file mode 100644 index 0000000..1169a9b --- /dev/null +++ b/src/main/java/com/inteligr8/alfresco/annotations/BehaviorBean.java @@ -0,0 +1,16 @@ +package com.inteligr8.alfresco.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface BehaviorBean { + + String defaultType() default "sys:base"; + + String defaultAssocType() default ""; + +} diff --git a/src/main/java/com/inteligr8/alfresco/annotations/IfAspect.java b/src/main/java/com/inteligr8/alfresco/annotations/IfAspect.java new file mode 100644 index 0000000..40c48a3 --- /dev/null +++ b/src/main/java/com/inteligr8/alfresco/annotations/IfAspect.java @@ -0,0 +1,17 @@ +package com.inteligr8.alfresco.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ + ElementType.METHOD, + ElementType.PARAMETER +}) +public @interface IfAspect { + + String aspect() default ""; + +} diff --git a/src/main/java/com/inteligr8/alfresco/annotations/IfChildAssociationIsPrimary.java b/src/main/java/com/inteligr8/alfresco/annotations/IfChildAssociationIsPrimary.java new file mode 100644 index 0000000..97a8db1 --- /dev/null +++ b/src/main/java/com/inteligr8/alfresco/annotations/IfChildAssociationIsPrimary.java @@ -0,0 +1,15 @@ +package com.inteligr8.alfresco.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ + ElementType.METHOD, + ElementType.PARAMETER +}) +public @interface IfChildAssociationIsPrimary { + +} diff --git a/src/main/java/com/inteligr8/alfresco/annotations/IfNodeExists.java b/src/main/java/com/inteligr8/alfresco/annotations/IfNodeExists.java new file mode 100644 index 0000000..cdf27d9 --- /dev/null +++ b/src/main/java/com/inteligr8/alfresco/annotations/IfNodeExists.java @@ -0,0 +1,15 @@ +package com.inteligr8.alfresco.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ + ElementType.METHOD, + ElementType.PARAMETER +}) +public @interface IfNodeExists { + +} diff --git a/src/main/java/com/inteligr8/alfresco/annotations/IfNodeType.java b/src/main/java/com/inteligr8/alfresco/annotations/IfNodeType.java new file mode 100644 index 0000000..670c590 --- /dev/null +++ b/src/main/java/com/inteligr8/alfresco/annotations/IfNodeType.java @@ -0,0 +1,17 @@ +package com.inteligr8.alfresco.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ + ElementType.METHOD, + ElementType.PARAMETER +}) +public @interface IfNodeType { + + String type() default ""; + +} diff --git a/src/main/java/com/inteligr8/alfresco/annotations/NodeTypeConstrainable.java b/src/main/java/com/inteligr8/alfresco/annotations/NodeTypeConstrainable.java new file mode 100644 index 0000000..4752564 --- /dev/null +++ b/src/main/java/com/inteligr8/alfresco/annotations/NodeTypeConstrainable.java @@ -0,0 +1,42 @@ +package com.inteligr8.alfresco.annotations; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.namespace.QNamePattern; +import org.alfresco.service.namespace.RegexQNamePattern; +import org.springframework.beans.factory.BeanNameAware; + +public interface NodeTypeConstrainable extends BeanNameAware { + + String getBeanName(); + + NamespaceService getNamespaceService(); + + default Collection constrainedPrefixedNodeTypes() { + return Collections.emptySet(); + } + + default Collection constrainedRegexedNodeTypes() { + return Collections.emptySet(); + } + + default Collection constrainedNodeTypes() { + Set nodeTypes = new HashSet<>(); + + Collection prefixedNodeTypes = this.constrainedPrefixedNodeTypes(); + for (String prefixedNodeType : prefixedNodeTypes) + nodeTypes.add(QName.createQName(prefixedNodeType, this.getNamespaceService())); + + Collection regexedNodeTypes = this.constrainedRegexedNodeTypes(); + for (String regexedNodeType : regexedNodeTypes) + nodeTypes.add(new RegexQNamePattern(regexedNodeType)); + + return nodeTypes; + } + +} diff --git a/src/main/java/com/inteligr8/alfresco/annotations/TransactionalRetryable.java b/src/main/java/com/inteligr8/alfresco/annotations/TransactionalRetryable.java new file mode 100644 index 0000000..1d2aa79 --- /dev/null +++ b/src/main/java/com/inteligr8/alfresco/annotations/TransactionalRetryable.java @@ -0,0 +1,20 @@ +package com.inteligr8.alfresco.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface TransactionalRetryable { + + int maxRetries() default -1; + + int minRetryWaitInMillis() default -1; + + int maxRetryWaitInMillis() default -1; + + int incRetryWaitInMillis() default -1; + +} diff --git a/src/main/java/com/inteligr8/alfresco/annotations/aspect/ApplicableParameterCallback.java b/src/main/java/com/inteligr8/alfresco/annotations/aspect/ApplicableParameterCallback.java new file mode 100644 index 0000000..6ee5f54 --- /dev/null +++ b/src/main/java/com/inteligr8/alfresco/annotations/aspect/ApplicableParameterCallback.java @@ -0,0 +1,12 @@ +package com.inteligr8.alfresco.annotations.aspect; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; + +import org.aspectj.lang.ProceedingJoinPoint; + +public interface ApplicableParameterCallback { + + boolean checkParameter(ProceedingJoinPoint joinPoint, Method method, T annotation, int parameterIndex); + +} diff --git a/src/main/java/com/inteligr8/alfresco/annotations/aspect/AspectAspect.java b/src/main/java/com/inteligr8/alfresco/annotations/aspect/AspectAspect.java new file mode 100644 index 0000000..e817780 --- /dev/null +++ b/src/main/java/com/inteligr8/alfresco/annotations/aspect/AspectAspect.java @@ -0,0 +1,145 @@ +package com.inteligr8.alfresco.annotations.aspect; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.alfresco.service.cmr.dictionary.ClassDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.AssociationRef; +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.namespace.QName; +import org.alfresco.service.namespace.QNamePattern; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import com.inteligr8.alfresco.annotations.AspectConstrainable; +import com.inteligr8.alfresco.annotations.IfAspect; + +@Aspect +@Component +public class AspectAspect extends QNameBasedAspect { + + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + + @Autowired + private DictionaryService dictionaryService; + + @Autowired + private NodeService nodeService; + + @Value("${inteligr8.cache.aspectConstrainable.maxBeans}") + private int maxBeans; + + @Override + public int getMaxBeansCache() { + return this.maxBeans; + } + + @Override + public Class getAnnotationClass() { + return IfAspect.class; + } + + @Pointcut("@annotation(com.inteligr8.alfresco.annotations.IfAspect)") + public void isAspectMethod() { + } + + @Around("isAspectMethod() or execution(* *(@com.inteligr8.alfresco.annotations.IfAspect (*), ..))") + public void isAspect(ProceedingJoinPoint joinPoint) throws Throwable { + this.checkParameters(joinPoint, new ApplicableParameterCallback() { + @Override + public boolean checkParameter(ProceedingJoinPoint joinPoint, Method method, IfAspect annotation, int parameterIndex) { + Object arg = joinPoint.getArgs()[parameterIndex]; + Collection nodeRefs = extractNodeRefs(arg); + if (nodeRefs == null) + return true; + + QNameBasedCallback callback = new QNameBasedCallback() { + @Override + public boolean isConstrained() { + return joinPoint.getThis() instanceof AspectConstrainable; + } + + @Override + public String getConstrainableClassSimpleName() { + return AspectConstrainable.class.getSimpleName(); + } + + @Override + public String getBeanName() { + return ((AspectConstrainable) joinPoint.getThis()).getBeanName(); + } + + @Override + public Collection constrainedQNames() { + return ((AspectConstrainable) joinPoint.getThis()).constrainedAspects(); + } + + @Override + public Collection allPossibleQNames() { + return dictionaryService.getAllAspects(); + } + + @Override + public void addAllAncestors(Set qnames, QName qname) { + ClassDefinition aspectDef = dictionaryService.getAspect(qname); + while (aspectDef != null) { + qnames.add(aspectDef.getName()); + aspectDef = aspectDef.getParentClassDefinition(); + } + } + + @Override + public String getAnnotationValue(IfAspect annotation) { + return annotation.aspect(); + } + }; + + Set aspects = getQNameCache(joinPoint, annotation, callback); + for (NodeRef nodeRef : nodeRefs) { + Set nodeAspects = nodeService.getAspects(nodeRef); + if (Collections.disjoint(aspects, nodeAspects)) { + logger.debug("The node '{}' aspects {} are not applicable; skipping method: {}", nodeRef, aspects, method); + return false; + } + } + + return true; + } + }); + } + + private Collection extractNodeRefs(Object obj) { + if (obj instanceof NodeRef) { + NodeRef nodeRef = (NodeRef) obj; + return Collections.singleton(nodeRef); + } else if (obj instanceof ChildAssociationRef) { + ChildAssociationRef childAssocRef = (ChildAssociationRef) obj; + return Collections.singleton(childAssocRef.getChildRef()); + } else if (obj instanceof AssociationRef) { + AssociationRef assocRef = (AssociationRef) obj; + return Arrays.asList(assocRef.getSourceRef(), assocRef.getTargetRef()); + } else if (obj instanceof Collection) { + Set nodeRefs = new LinkedHashSet<>(); + for (Object o : ((Collection) obj)) + nodeRefs.addAll(this.extractNodeRefs(o)); + return nodeRefs; + } else { + return null; + } + } + +} diff --git a/src/main/java/com/inteligr8/alfresco/annotations/aspect/AsyncAspect.java b/src/main/java/com/inteligr8/alfresco/annotations/aspect/AsyncAspect.java new file mode 100644 index 0000000..dfa26f3 --- /dev/null +++ b/src/main/java/com/inteligr8/alfresco/annotations/aspect/AsyncAspect.java @@ -0,0 +1,38 @@ +package com.inteligr8.alfresco.annotations.aspect; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import com.inteligr8.alfresco.annotations.service.AsyncService; + +@Aspect +@Component +public class AsyncAspect { + + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + + @Autowired + private AsyncService asyncService; + + @Pointcut("@annotation(com.inteligr8.alfresco.annotations.Asynchronous)") + public void asyncMethod() { + } + + @Around("asyncMethod()") + public void async(ProceedingJoinPoint joinPoint) throws Throwable { + if (this.asyncService.isCurrentThreadAsynchronous()) { + this.logger.trace("Intercepted an @Async method call while already asynchronous; executing synchronously"); + joinPoint.proceed(); + } else { + this.logger.trace("Intercepted an @Async method call; redirecting to Async service"); + this.asyncService.push(joinPoint); + } + } + +} diff --git a/src/main/java/com/inteligr8/alfresco/annotations/aspect/AuthorizedAspect.java b/src/main/java/com/inteligr8/alfresco/annotations/aspect/AuthorizedAspect.java new file mode 100644 index 0000000..0cda38a --- /dev/null +++ b/src/main/java/com/inteligr8/alfresco/annotations/aspect/AuthorizedAspect.java @@ -0,0 +1,103 @@ +package com.inteligr8.alfresco.annotations.aspect; + +import java.lang.reflect.Method; + +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; +import org.apache.commons.lang3.StringUtils; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.MethodSignature; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import com.inteligr8.alfresco.annotations.Authorized; +import com.inteligr8.alfresco.annotations.AuthorizedAsSystem; +import com.inteligr8.alfresco.annotations.Authorizable; + +@Aspect +@Order(Ordered.HIGHEST_PRECEDENCE + Short.MAX_VALUE) +@Component +public class AuthorizedAspect { + + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + + @Pointcut("@annotation(com.inteligr8.alfresco.annotations.Authorized || com.inteligr8.alfresco.annotations.AuthorizedAsSystem)") + public void runAsableMethod() { + } + + @Around("runAsableMethod()") + public void runAs(ProceedingJoinPoint joinPoint) throws Throwable { + String runAsUser = this.getRunAsUser(joinPoint); + + String currentRunAsUser = AuthenticationUtil.getRunAsUser(); + if (currentRunAsUser != null && currentRunAsUser.equals(runAsUser)) { + this.logger.trace("The current context is already running as the specified user: {}", currentRunAsUser); + joinPoint.proceed(); + } else { + this.logger.debug("Changing runAs context: {} => {}", currentRunAsUser, runAsUser); + + this.runAs(joinPoint, runAsUser); + } + } + + private String getRunAsUser(ProceedingJoinPoint joinPoint) { + if (!(joinPoint.getSignature() instanceof MethodSignature)) + throw new IllegalStateException("The @Authorized annotation must be on methods and methods have signatures"); + + MethodSignature methodSig = (MethodSignature) joinPoint.getSignature(); + Method method = methodSig.getMethod(); + if (method.getAnnotation(AuthorizedAsSystem.class) != null) { + String runAs = AuthenticationUtil.getSystemUserName(); + this.logger.trace("The @AuthorizedAsSystem method {}#{} will run as: {}", joinPoint.getThis().getClass(), method, runAs); + return runAs; + } + + if (joinPoint.getThis() instanceof Authorizable) { + String runAs = StringUtils.trimToNull(((Authorizable) joinPoint.getThis()).authorizeAsUser()); + this.logger.trace("The @Authorized method {}#{} is Authorizable: {}", joinPoint.getThis().getClass(), method, runAs); + return runAs; + } + + Authorized runAsAnnotation = method.getAnnotation(Authorized.class); + String runAs = StringUtils.trimToNull(runAsAnnotation.value()); + if (runAs != null) { + this.logger.trace("The @Authorized method {}#{} must run as: {}", joinPoint.getThis().getClass(), method, runAs); + return runAs; + } + + this.logger.trace("The @Authorized method {}#{} must run as system", joinPoint.getThis().getClass(), method); + return AuthenticationUtil.getSystemUserName(); + } + + private void runAs(final ProceedingJoinPoint joinPoint, String runAsUser) throws Throwable { + RunAsWork work = new RunAsWork() { + public Object doWork() throws Exception { + try { + return joinPoint.proceed(); + } catch (Exception | Error e) { + throw e; + } catch (Throwable t) { + throw new RuntimeException("This should never happen", t); + } + } + }; + + try { + AuthenticationUtil.runAs(work, runAsUser); + } catch (RuntimeException re) { + // attempt to unwrap the exception + if (re.getMessage() != null && re.getMessage().equals("Error during run as.")) { + throw re.getCause(); + } else { + throw re; + } + } + } + +} diff --git a/src/main/java/com/inteligr8/alfresco/annotations/aspect/ChildIsPrimaryAspect.java b/src/main/java/com/inteligr8/alfresco/annotations/aspect/ChildIsPrimaryAspect.java new file mode 100644 index 0000000..9cdc392 --- /dev/null +++ b/src/main/java/com/inteligr8/alfresco/annotations/aspect/ChildIsPrimaryAspect.java @@ -0,0 +1,52 @@ +package com.inteligr8.alfresco.annotations.aspect; + +import java.lang.reflect.Method; + +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import com.inteligr8.alfresco.annotations.IfChildAssociationIsPrimary; + +@Aspect +@Order(Ordered.HIGHEST_PRECEDENCE + 100) // ordering before transaction/authorized +@Component +public class ChildIsPrimaryAspect extends MethodOrParameterAspect { + + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + + @Override + public Class getAnnotationClass() { + return IfChildAssociationIsPrimary.class; + } + + @Pointcut("@annotation(com.inteligr8.alfresco.annotations.IfChildAssociationIsPrimary)") + public void isChildAssocPrimaryMethod() { + } + + @Around("isChildAssocPrimaryMethod() or execution(* *(@com.inteligr8.alfresco.annotations.IfChildAssociationIsPrimary (*), ..))") + public void isChildAssocPrimary(ProceedingJoinPoint joinPoint) throws Throwable { + this.checkParameters(joinPoint, new ApplicableParameterCallback() { + @Override + public boolean checkParameter(ProceedingJoinPoint joinPoint, Method method, IfChildAssociationIsPrimary annotation, int parameterIndex) { + Object arg = joinPoint.getArgs()[parameterIndex]; + if (arg instanceof ChildAssociationRef) { + if (!((ChildAssociationRef)arg).isPrimary()) { + logger.debug("The child association '{}' is not primary; skipping method: {}", arg, method); + return false; + } + } + + return true; + } + }); + } + +} diff --git a/src/main/java/com/inteligr8/alfresco/annotations/aspect/MethodOrParameterAspect.java b/src/main/java/com/inteligr8/alfresco/annotations/aspect/MethodOrParameterAspect.java new file mode 100644 index 0000000..e447102 --- /dev/null +++ b/src/main/java/com/inteligr8/alfresco/annotations/aspect/MethodOrParameterAspect.java @@ -0,0 +1,37 @@ +package com.inteligr8.alfresco.annotations.aspect; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.reflect.MethodSignature; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public abstract class MethodOrParameterAspect { + + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + + public abstract Class getAnnotationClass(); + + public void checkParameters(ProceedingJoinPoint joinPoint, ApplicableParameterCallback callback) throws Throwable { + MethodSignature methodSig = (MethodSignature) joinPoint.getSignature(); + Method method = methodSig.getMethod(); + T methodAnnotation = method.getAnnotation(this.getAnnotationClass()); + + for (int p = 0; p < method.getParameterCount(); p++) { + T annotation = method.getParameters()[p].getAnnotation(this.getAnnotationClass()); + if (annotation == null) + annotation = methodAnnotation; + if (annotation != null) { + if (!callback.checkParameter(joinPoint, method, annotation, p)) { + this.logger.debug("The parameter '{}' condition is false; skipping method: {}", method.getParameters()[p].getName(), method); + return; + } + } + } + + joinPoint.proceed(); + } + +} diff --git a/src/main/java/com/inteligr8/alfresco/annotations/aspect/NodeTypeAspect.java b/src/main/java/com/inteligr8/alfresco/annotations/aspect/NodeTypeAspect.java new file mode 100644 index 0000000..9393613 --- /dev/null +++ b/src/main/java/com/inteligr8/alfresco/annotations/aspect/NodeTypeAspect.java @@ -0,0 +1,145 @@ +package com.inteligr8.alfresco.annotations.aspect; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.alfresco.service.cmr.dictionary.ClassDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.AssociationRef; +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.namespace.QName; +import org.alfresco.service.namespace.QNamePattern; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import com.inteligr8.alfresco.annotations.IfNodeType; +import com.inteligr8.alfresco.annotations.NodeTypeConstrainable; + +@Aspect +@Component +public class NodeTypeAspect extends QNameBasedAspect { + + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + + @Autowired + private DictionaryService dictionaryService; + + @Autowired + private NodeService nodeService; + + @Value("${inteligr8.cache.nodeTypeConstrainable.maxBeans}") + private int maxBeans; + + @Override + public int getMaxBeansCache() { + return this.maxBeans; + } + + @Override + public Class getAnnotationClass() { + return IfNodeType.class; + } + + @Pointcut("@annotation(com.inteligr8.alfresco.annotations.IfNodeType)") + public void isNodeTypeMethod() { + } + + @Around("isNodeTypeMethod() or execution(* *(@com.inteligr8.alfresco.annotations.IfNodeType (*), ..))") + public void isNodeType(ProceedingJoinPoint joinPoint) throws Throwable { + this.checkParameters(joinPoint, new ApplicableParameterCallback() { + @Override + public boolean checkParameter(ProceedingJoinPoint joinPoint, Method method, IfNodeType annotation, int parameterIndex) { + Object arg = joinPoint.getArgs()[parameterIndex]; + Collection nodeRefs = extractNodeRefs(arg); + if (nodeRefs == null) + return true; + + QNameBasedCallback callback = new QNameBasedCallback() { + @Override + public boolean isConstrained() { + return joinPoint.getThis() instanceof NodeTypeConstrainable; + } + + @Override + public String getConstrainableClassSimpleName() { + return NodeTypeConstrainable.class.getSimpleName(); + } + + @Override + public String getBeanName() { + return ((NodeTypeConstrainable) joinPoint.getThis()).getBeanName(); + } + + @Override + public Collection constrainedQNames() { + return ((NodeTypeConstrainable) joinPoint.getThis()).constrainedNodeTypes(); + } + + @Override + public Collection allPossibleQNames() { + return dictionaryService.getAllTypes(); + } + + @Override + public void addAllAncestors(Set qnames, QName qname) { + ClassDefinition typeDef = dictionaryService.getType(qname); + while (typeDef != null) { + qnames.add(typeDef.getName()); + typeDef = typeDef.getParentClassDefinition(); + } + } + + @Override + public String getAnnotationValue(IfNodeType annotation) { + return annotation.type(); + } + }; + + Set nodeTypes = getQNameCache(joinPoint, annotation, callback); + for (NodeRef nodeRef : nodeRefs) { + QName nodeType = nodeService.getType(nodeRef); + if (!nodeTypes.contains(nodeType)) { + logger.debug("The node '{}' type '{}' is not applicable; skipping method: {}", nodeRef, nodeType, method); + return false; + } + } + + return true; + } + }); + } + + private Collection extractNodeRefs(Object obj) { + if (obj instanceof NodeRef) { + NodeRef nodeRef = (NodeRef) obj; + return Collections.singleton(nodeRef); + } else if (obj instanceof ChildAssociationRef) { + ChildAssociationRef childAssocRef = (ChildAssociationRef) obj; + return Collections.singleton(childAssocRef.getChildRef()); + } else if (obj instanceof AssociationRef) { + AssociationRef assocRef = (AssociationRef) obj; + return Arrays.asList(assocRef.getSourceRef(), assocRef.getTargetRef()); + } else if (obj instanceof Collection) { + Set nodeRefs = new LinkedHashSet<>(); + for (Object o : ((Collection) obj)) + nodeRefs.addAll(this.extractNodeRefs(o)); + return nodeRefs; + } else { + return null; + } + } + +} diff --git a/src/main/java/com/inteligr8/alfresco/annotations/aspect/OperableNodeAspect.java b/src/main/java/com/inteligr8/alfresco/annotations/aspect/OperableNodeAspect.java new file mode 100644 index 0000000..6409462 --- /dev/null +++ b/src/main/java/com/inteligr8/alfresco/annotations/aspect/OperableNodeAspect.java @@ -0,0 +1,97 @@ +package com.inteligr8.alfresco.annotations.aspect; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.alfresco.service.cmr.repository.AssociationRef; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import com.inteligr8.alfresco.annotations.IfNodeExists; + +@Aspect +@Component +public class OperableNodeAspect extends MethodOrParameterAspect { + + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + + @Autowired + private NodeService nodeService; + + @Override + public Class getAnnotationClass() { + return IfNodeExists.class; + } + + @Pointcut("@annotation(com.inteligr8.alfresco.annotations.IfNodeExists)") + public void isNodeOperableMethod() { + } + + @Around("isNodeOperableMethod() or execution(* *(@com.inteligr8.alfresco.annotations.IfNodeExists (*), ..))") + public void isNodeOperable(ProceedingJoinPoint joinPoint) throws Throwable { + this.checkParameters(joinPoint, new ApplicableParameterCallback() { + @Override + public boolean checkParameter(ProceedingJoinPoint joinPoint, Method method, IfNodeExists annotation, int parameterIndex) { + Object arg = joinPoint.getArgs()[parameterIndex]; + Collection nodeRefs = extractNodeRefs(arg); + if (nodeRefs == null) + return true; + + for (NodeRef nodeRef : nodeRefs) { + if (!isOperableNode(nodeRef)) { + logger.debug("The node '{}' does not exist; skipping method: {}", nodeRef, method); + return false; + } + } + + return true; + } + }); + } + + private Collection extractNodeRefs(Object obj) { + if (obj instanceof NodeRef) { + NodeRef nodeRef = (NodeRef) obj; + return Collections.singleton(nodeRef); + } else if (obj instanceof ChildAssociationRef) { + ChildAssociationRef childAssocRef = (ChildAssociationRef) obj; + return Collections.singleton(childAssocRef.getChildRef()); + } else if (obj instanceof AssociationRef) { + AssociationRef assocRef = (AssociationRef) obj; + return Arrays.asList(assocRef.getSourceRef(), assocRef.getTargetRef()); + } else if (obj instanceof Collection) { + Set nodeRefs = new LinkedHashSet<>(); + for (Object o : ((Collection) obj)) + nodeRefs.addAll(this.extractNodeRefs(o)); + return nodeRefs; + } else { + return null; + } + } + + private boolean isOperableNode(NodeRef nodeRef) { + if (!this.nodeService.exists(nodeRef)) { + this.logger.debug("The node '{}' does not exist", nodeRef); + return false; + } else if (this.nodeService.getNodeStatus(nodeRef).isDeleted()) { + this.logger.debug("The node '{}' was already deleted", nodeRef); + return false; + } + + return true; + } + +} diff --git a/src/main/java/com/inteligr8/alfresco/annotations/aspect/QNameBasedAspect.java b/src/main/java/com/inteligr8/alfresco/annotations/aspect/QNameBasedAspect.java new file mode 100644 index 0000000..949bc91 --- /dev/null +++ b/src/main/java/com/inteligr8/alfresco/annotations/aspect/QNameBasedAspect.java @@ -0,0 +1,99 @@ +package com.inteligr8.alfresco.annotations.aspect; + +import java.lang.annotation.Annotation; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +import javax.annotation.PostConstruct; + +import org.alfresco.repo.cache.DefaultSimpleCache; +import org.alfresco.repo.cache.SimpleCache; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.namespace.QNamePattern; +import org.aspectj.lang.ProceedingJoinPoint; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; + +public abstract class QNameBasedAspect extends MethodOrParameterAspect { + + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + + @Autowired + private NamespaceService namespaceService; + + private SimpleCache> qnameCache; + + public abstract int getMaxBeansCache(); + + @PostConstruct + public void init() { + this.qnameCache = new DefaultSimpleCache<>(this.getMaxBeansCache(), this.getClass().getName()); + } + + public Set getQNameCache(ProceedingJoinPoint joinPoint, T annotation, QNameBasedCallback callback) { + if (callback.isConstrained()) { + Set qnames = this.qnameCache.get(callback.getBeanName()); + if (qnames != null) { + this.logger.trace("Using cache of qnames for bean: {}", callback.getBeanName()); + return qnames; + } + + // caching all types; expensive now; fast at runtime + qnames = new HashSet<>(); + for (QNamePattern qnamePattern : callback.constrainedQNames()) { + if (qnamePattern instanceof QName) { + callback.addAllAncestors(qnames, (QName) qnamePattern); + } else { + for (QName qname : callback.allPossibleQNames()) { + if (qnamePattern.isMatch(qname)) { + callback.addAllAncestors(qnames, qname); + } + } + } + } + + this.logger.debug("Caching @{} qnames for bean: {}: {}", annotation.getClass().getSimpleName(), callback.getBeanName(), qnames); + this.qnameCache.put(callback.getBeanName(), qnames); + return qnames; + } else if (callback.getAnnotationValue(annotation).length() > 0) { + Set qnames = this.qnameCache.get(joinPoint.getThis().getClass().getName()); + if (qnames != null) { + this.logger.trace("Using cache of qnames for bean: {}", callback.getBeanName()); + return qnames; + } + + qnames = new HashSet<>(); + callback.addAllAncestors(qnames, QName.createQName(callback.getAnnotationValue(annotation), this.namespaceService)); + + this.logger.debug("Caching @{} node types for singleton: {}: {}", annotation.getClass().getSimpleName(), joinPoint.getThis().getClass(), qnames); + this.qnameCache.put(joinPoint.getThis().getClass().getName(), qnames); + return qnames; + } else { + throw new IllegalStateException("An @" + annotation.getClass().getSimpleName() + " must have a value or the class must implement " + callback.getConstrainableClassSimpleName()); + } + } + + + + public interface QNameBasedCallback { + + boolean isConstrained(); + + String getBeanName(); + + String getConstrainableClassSimpleName(); + + Collection constrainedQNames(); + + void addAllAncestors(Set qnames, QName qname); + + Collection allPossibleQNames(); + + String getAnnotationValue(T annotation); + + } + +} diff --git a/src/main/java/com/inteligr8/alfresco/annotations/aspect/RetryingTransactionAspect.java b/src/main/java/com/inteligr8/alfresco/annotations/aspect/RetryingTransactionAspect.java new file mode 100644 index 0000000..7e72569 --- /dev/null +++ b/src/main/java/com/inteligr8/alfresco/annotations/aspect/RetryingTransactionAspect.java @@ -0,0 +1,154 @@ +package com.inteligr8.alfresco.annotations.aspect; + +import java.lang.reflect.Method; + +import org.alfresco.repo.transaction.AlfrescoTransactionSupport; +import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.MethodSignature; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import org.springframework.transaction.IllegalTransactionStateException; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import com.inteligr8.alfresco.annotations.TransactionalRetryable; + +@Aspect +@Order(Ordered.LOWEST_PRECEDENCE - Short.MAX_VALUE) +@Component +public class RetryingTransactionAspect { + + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + + @Pointcut("@annotation(org.springframework.transaction.annotation.Transactional)") + public void transactionalMethod() { + } + + @Around("transactionalMethod()") + public void retryingTransactional(ProceedingJoinPoint joinPoint) throws Throwable { + this.logger.trace("tx: {}", AlfrescoTransactionSupport.getTransactionId()); + + Method method = this.getMethod(joinPoint); + Transactional txl = method.getAnnotation(Transactional.class); + + if (this.doCreateNewTxContext(txl) || this.isReadStateChange(txl)) { + TransactionalRetryable txtry = method.getAnnotation(TransactionalRetryable.class); + + this.logger.debug("Changing TX context: {} => [ro: {}, new: {}]", AlfrescoTransactionSupport.getTransactionReadState(), txl.readOnly(), txl.propagation()); + this.execute(joinPoint, txl, txtry); + } + } + + private Method getMethod(ProceedingJoinPoint joinPoint) { + if (!(joinPoint.getSignature() instanceof MethodSignature)) + throw new IllegalStateException("The @Transactional or @TransactionalRetryable annotations must be on methods"); + + MethodSignature methodSig = (MethodSignature) joinPoint.getSignature(); + return methodSig.getMethod(); + } + + private boolean isReadStateChange(Transactional txl) { + switch (AlfrescoTransactionSupport.getTransactionReadState()) { + case TXN_NONE: + switch (txl.propagation()) { + case NEVER: + case NOT_SUPPORTED: + case SUPPORTS: + return false; + default: + return true; + } + case TXN_READ_ONLY: + return !txl.readOnly(); + case TXN_READ_WRITE: + return txl.readOnly(); + default: + throw new IllegalStateException(); + } + } + + private boolean doCreateNewTxContext(Transactional txl) { + switch (txl.propagation()) { + case NEVER: + switch (AlfrescoTransactionSupport.getTransactionReadState()) { + case TXN_NONE: + return false; + default: + throw new IllegalTransactionStateException("A transaction exists where one is not allowed"); + } + case MANDATORY: + switch (AlfrescoTransactionSupport.getTransactionReadState()) { + case TXN_NONE: + throw new IllegalTransactionStateException("A transaction does not exist where one is mandatory"); + default: + return false; + } + case SUPPORTS: + return false; + case REQUIRED: + switch (AlfrescoTransactionSupport.getTransactionReadState()) { + case TXN_NONE: + return true; + default: + return false; + } + case REQUIRES_NEW: + return true; + default: + throw new UnsupportedOperationException(); + } + } + + private void execute(final ProceedingJoinPoint joinPoint, Transactional txl, TransactionalRetryable txtry) throws Throwable { + RetryingTransactionCallback rtcallback = new RetryingTransactionCallback() { + @Override + public Object execute() throws Throwable { + logger.trace("tx: {}", AlfrescoTransactionSupport.getTransactionId()); + + try { + return joinPoint.proceed(); + } catch (Exception | Error e) { + throw e; + } catch (Throwable t) { + throw new RuntimeException("This should never happen", t); + } + } + }; + + RetryingTransactionHelper rthelper = new RetryingTransactionHelper(); + if (txtry.maxRetries() > 0) + rthelper.setMaxRetries(txtry.maxRetries()); + if (txtry.minRetryWaitInMillis() > 0) + rthelper.setMinRetryWaitMs(txtry.minRetryWaitInMillis()); + if (txtry.maxRetryWaitInMillis() > 0) + rthelper.setMaxRetryWaitMs(txtry.maxRetryWaitInMillis()); + if (txtry.incRetryWaitInMillis() > 0) + rthelper.setRetryWaitIncrementMs(txtry.incRetryWaitInMillis()); + if (txl.timeout() > 0) + rthelper.setMaxExecutionMs(txl.timeout() * 1000L); + + try { + rthelper.doInTransaction(rtcallback, txl.readOnly(), txl.propagation().equals(Propagation.REQUIRES_NEW)); + } catch (RuntimeException re) { + // attempt to unwrap the exception + if (re.getMessage() == null) { + throw re; + } else if (re.getMessage().startsWith("Exception from transactional callback")) { + throw re.getCause(); + } else if (re.getMessage().startsWith("Exception in Transaction")) { + throw re.getCause(); + } else { + throw re; + } + } + } + +} diff --git a/src/main/java/com/inteligr8/alfresco/annotations/job/AsyncJob.java b/src/main/java/com/inteligr8/alfresco/annotations/job/AsyncJob.java new file mode 100644 index 0000000..671aba4 --- /dev/null +++ b/src/main/java/com/inteligr8/alfresco/annotations/job/AsyncJob.java @@ -0,0 +1,25 @@ +package com.inteligr8.alfresco.annotations.job; + +import org.quartz.Job; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; + +import com.inteligr8.alfresco.annotations.service.AsyncProcessException; +import com.inteligr8.alfresco.annotations.service.AsyncService; + +public class AsyncJob implements Job { + + @Override + public void execute(JobExecutionContext context) throws JobExecutionException { + AsyncService asyncService = (AsyncService) context.getMergedJobDataMap().get("asyncService"); + if (asyncService == null) + throw new JobExecutionException("An 'asyncService' object is required in the job map"); + + try { + asyncService.poll(); + } catch (AsyncProcessException ape) { + throw new JobExecutionException(ape, true); + } + } + +} diff --git a/src/main/java/com/inteligr8/alfresco/annotations/service/AsyncProcessException.java b/src/main/java/com/inteligr8/alfresco/annotations/service/AsyncProcessException.java new file mode 100644 index 0000000..91118d9 --- /dev/null +++ b/src/main/java/com/inteligr8/alfresco/annotations/service/AsyncProcessException.java @@ -0,0 +1,11 @@ +package com.inteligr8.alfresco.annotations.service; + +public class AsyncProcessException extends Exception { + + private static final long serialVersionUID = 8254359296736253436L; + + public AsyncProcessException(String message, Throwable t) { + super(message, t); + } + +} diff --git a/src/main/java/com/inteligr8/alfresco/annotations/service/AsyncService.java b/src/main/java/com/inteligr8/alfresco/annotations/service/AsyncService.java new file mode 100644 index 0000000..cd99963 --- /dev/null +++ b/src/main/java/com/inteligr8/alfresco/annotations/service/AsyncService.java @@ -0,0 +1,18 @@ +package com.inteligr8.alfresco.annotations.service; + +import org.aspectj.lang.ProceedingJoinPoint; + +/** + * @author brian@inteligr8.com + */ +public interface AsyncService { + + boolean isEnabled(); + + boolean isCurrentThreadAsynchronous(); + + void poll() throws AsyncProcessException; + + void push(ProceedingJoinPoint joinPoint) throws AsyncProcessException; + +} diff --git a/src/main/java/com/inteligr8/alfresco/annotations/service/impl/MqAsyncService.java b/src/main/java/com/inteligr8/alfresco/annotations/service/impl/MqAsyncService.java new file mode 100644 index 0000000..fd09d91 --- /dev/null +++ b/src/main/java/com/inteligr8/alfresco/annotations/service/impl/MqAsyncService.java @@ -0,0 +1,538 @@ +package com.inteligr8.alfresco.annotations.service.impl; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Serializable; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.UUID; +import java.util.function.Supplier; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.jms.Connection; +import javax.jms.JMSException; +import javax.jms.Message; +import javax.jms.MessageConsumer; +import javax.jms.MessageProducer; +import javax.jms.Queue; +import javax.jms.Session; +import javax.transaction.TransactionManager; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.cache.SimpleCache; +import org.alfresco.repo.dictionary.M2Model; +import org.alfresco.repo.version.common.VersionImpl; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.ActionService; +import org.alfresco.service.cmr.dictionary.CustomModelService; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.version.Version; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.Pair; +import org.apache.activemq.ActiveMQXAConnectionFactory; +import org.apache.activemq.jms.pool.XaPooledConnectionFactory; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.reflect.MethodSignature; +import org.quartz.JobKey; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; +import org.quartz.impl.JobDetailImpl; +import org.quartz.impl.StdSchedulerFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationEvent; +import org.springframework.extensions.surf.util.AbstractLifecycleBean; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.inteligr8.alfresco.annotations.AuthorizedAsSystem; +import com.inteligr8.alfresco.annotations.TransactionalRetryable; +import com.inteligr8.alfresco.annotations.job.AsyncJob; +import com.inteligr8.alfresco.annotations.service.AsyncProcessException; +import com.inteligr8.alfresco.annotations.service.AsyncService; + +/** + * This class provides integration with MQ for the asynchronous filing of nodes. + * + * @author brian@inteligr8.com + */ +@Component +public class MqAsyncService extends AbstractLifecycleBean implements AsyncService { + + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + private final Pattern typePattern = Pattern.compile("v([0-9]+):([^:#]+)#(.+)"); + private final ObjectMapper om = new ObjectMapper(); + + @Value("${inteligr8.async.mq.enabled}") + protected boolean enabled; + + @Value("${inteligr8.async.mq.url:#{null}}") + protected String url; + + @Value("${inteligr8.async.mq.username:#{null}}") + protected String username; + + @Value("${inteligr8.async.mq.password:#{null}}") + protected String password; + + @Value("${inteligr8.async.mq.queue}") + protected String queueName; + + @Value("${inteligr8.async.mq.errorQueue}") + protected String errorQueueName; + + @Value("${inteligr8.async.mq.clientId}") + protected String clientId; + + @Value("${inteligr8.async.mq.pool.max}") + protected short maxConnections; + + @Autowired + protected ActionService actionService; + + @Autowired + protected ContentService contentService; + + @Autowired + protected CustomModelService modelService; + + @Autowired + protected DictionaryService dictionaryService; + + @Autowired + protected NamespaceService namespaceService; + + @Autowired + protected TransactionService txService; + + @Autowired + protected TransactionManager txManager; + + private XaPooledConnectionFactory factory; + + private SimpleCache, String>, Method> methodCache; + + private ThreadLocal isAsync = ThreadLocal.withInitial(new Supplier() { + @Override + public Boolean get() { + return false; + } + }); + + @Override + protected void onBootstrap(ApplicationEvent event) { + if (!this.enabled) + return; + + ActiveMQXAConnectionFactory factory = new ActiveMQXAConnectionFactory(this.url); + + XaPooledConnectionFactory pool = new XaPooledConnectionFactory(); + pool.setConnectionFactory(factory); + pool.setMaxConnections(this.maxConnections); + pool.setTransactionManager(this.txManager); + pool.start(); + + this.factory = pool; + + JobKey jobKey = new JobKey("behaviour-async", "inteligr8-annotations"); + + JobDetailImpl jobDetail = new JobDetailImpl(); + jobDetail.setKey(jobKey); + jobDetail.setRequestsRecovery(true); + jobDetail.setJobClass(AsyncJob.class); + jobDetail.getJobDataMap().put("asyncService", this); + + try { + Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler(); + scheduler.addJob(jobDetail, false); + scheduler.triggerJob(jobKey); + } catch (SchedulerException se) { + this.logger.error("The behavior policy async service failed to start; no asynchronous policies will be processed!", se); + } + } + + @Override + protected void onShutdown(ApplicationEvent event) { + this.factory.stop(); + } + + @Override + public boolean isEnabled() { + return enabled; + } + + @Override + public boolean isCurrentThreadAsynchronous() { + return this.isAsync.get(); + } + + @Override + public void poll() throws AsyncProcessException { + this.logger.trace("poll()"); + this.isAsync.set(true); + + try { + Connection mqcon = this.factory.createConnection(this.username, this.password); + try { + mqcon.setClientID(this.clientId); + + Session mqsession = mqcon.createSession(true, Session.CLIENT_ACKNOWLEDGE); + try { + this.logger.debug("Polling messages for asynchronous policy execution"); + this.pollErrors(mqsession); + this.pollMain(mqsession); + } finally { + mqsession.close(); + } + } finally { + mqcon.close(); + } + } catch (JMSException je) { + throw new AsyncProcessException("A JMS messaging issue occurred", je); + } + } + + private void pollErrors(Session mqsession) throws JMSException { + this.logger.debug("Polling previously errored messages"); + + Queue mqqueue = mqsession.createQueue(this.errorQueueName); + Set msgIds = new HashSet<>(); + int ackd = 0; + + MessageConsumer consumer = mqsession.createConsumer(mqqueue); + try { + while (!Thread.currentThread().isInterrupted()) { + Boolean processed = this.pollTx(mqsession, consumer, msgIds); + if (processed == null) { + break; + } else if (processed.booleanValue()) { + ackd++; + } + } + } finally { + consumer.close(); + } + + this.logger.info("Successfully processed {} of {} previously errored messages", ackd, msgIds.size()); + } + + private void pollMain(Session mqsession) throws JMSException { + this.logger.debug("Polling ongoing messages ..."); + + Queue mqqueue = mqsession.createQueue(this.queueName); + + MessageConsumer consumer = mqsession.createConsumer(mqqueue); + try { + while (!Thread.currentThread().isInterrupted()) { + this.pollTx(mqsession, consumer, null); + } + } finally { + consumer.close(); + } + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + @TransactionalRetryable(maxRetries = 3) + @AuthorizedAsSystem + private Boolean pollTx(Session mqsession, MessageConsumer consumer, Set msgIds) throws JMSException { + Message mqmsg = consumer.receive(); + + if (msgIds != null && !msgIds.add(mqmsg.getJMSMessageID())) { + this.logger.debug("Received a message again; assuming we have (re)tried all errored messages: {}", mqmsg.getJMSMessageID()); + return null; + } + + try { + if (this.processIncomingMessage(mqsession, mqmsg, msgIds != null)) { + mqmsg.acknowledge(); + return true; + } + } catch (RuntimeException | Error e) { + this.logger.error("An unexpected issue occurred", e); + } + + return false; + } + + private boolean processIncomingMessage(Session mqsession, Message mqmsg, boolean isErrorQueue) throws JMSException { + String msgId = mqmsg.getJMSMessageID(); + this.logger.debug("Received message: {}", msgId); + + String type = mqmsg.getJMSType(); + Matcher matcher = this.typePattern.matcher(type); + if (!matcher.find()) { + this.logger.warn("The queue has a message ('{}') with an unsupported JMS type: {}", msgId, type); + return false; + } + + try { + Class beanClass = Class.forName(matcher.group(2)); + this.logger.trace("Preparing to execute using bean type: {}", beanClass); + Object bean = this.getApplicationContext().getBean(beanClass); + this.logger.trace("Found qualifying bean: {}", bean); + + String methodName = matcher.group(3); + Method method = this.findMethod(beanClass, methodName); + this.logger.trace("Found qualifying method: {}", method); + Parameter[] params = method.getParameters(); + + Object[] args = new Object[params.length]; + + for (int a = 0; a < args.length; a++) { + Object arg = mqmsg.getObjectProperty("arg" + a); + if (arg == null) + continue; + + args[a] = this.unmarshal(params[a], arg); + } + + switch (method.getName()) { + case "onLoadDynamicModel": + args[1] = args[0]; + args[0] = this.loadModel((NodeRef) args[1]); + } + + method.invoke(bean, args); + } catch (ClassNotFoundException cnfe) { + this.logger.error("A policy bean could not be found; will try on next restart"); + this.logger.error("The bean '{}' could not be found: {}", matcher.group(2), cnfe.getMessage()); + if (isErrorQueue) + return false; + this.moveToErrorQueue(mqsession, mqmsg); + } catch (IOException ie) { + this.logger.warn("This should never happen: " + ie.getMessage()); + // return to queue and retry indefinitely + return false; + } catch (NoSuchMethodException nsme) { + this.logger.error("A policy enumeration argument could not be constructed; will try on next restart"); + this.logger.error("An argument could not be The bean '{}' could not be found: {}", matcher.group(2), nsme.getMessage()); + if (isErrorQueue) + return false; + this.moveToErrorQueue(mqsession, mqmsg); + } catch (IllegalAccessException iae) { + this.logger.error("A policy method was not accessible (public); will try on next restart"); + this.logger.warn("The bean '{}' method '{}' is not accessible: {}", matcher.group(2), matcher.group(3), iae.getMessage()); + if (isErrorQueue) + return false; + this.moveToErrorQueue(mqsession, mqmsg); + } catch (InstantiationException | InvocationTargetException ie) { + this.logger.error("A policy method execution failed; will try on next restart"); + this.logger.warn("The bean '{}' method '{}' execution failed: {}", matcher.group(2), matcher.group(3), ie.getMessage()); + if (isErrorQueue) + return false; + this.moveToErrorQueue(mqsession, mqmsg); + } + + return true; + } + + private void moveToErrorQueue(Session mqsession, Message mqmsg) throws JMSException { + Queue mqqueue = mqsession.createQueue(this.errorQueueName); + + MessageProducer producer = mqsession.createProducer(mqqueue); + try { + producer.send(mqmsg); + } finally { + producer.close(); + } + } + + private Method findMethod(Class clazz, String methodName) { + Pair, String> key = new Pair<>(clazz, methodName); + Method method = this.methodCache.get(key); + if (method != null) { + this.logger.trace("Found method in cache: {}", method); + return method; + } + + this.logger.trace("Looping through bean type methods to find: {}", methodName); + + for (Method amethod : clazz.getDeclaredMethods()) { + if (amethod.getName().equals(methodName)) { + this.logger.debug("Found and caching method: {} => {}", key, amethod); + this.methodCache.put(key, amethod); + return amethod; + } + } + + throw new IllegalStateException("The bean (" + clazz + ") does not implement the method: " + methodName); + } + + public void push(ProceedingJoinPoint joinPoint) throws AsyncProcessException { + this.logger.trace("push({})", joinPoint); + + if (!(joinPoint.getSignature() instanceof MethodSignature)) + throw new IllegalStateException("The join point must be on methods and methods have signatures"); + + Class beanType = joinPoint.getThis().getClass(); + this.logger.debug("Queuing for bean: {}", beanType); + + MethodSignature methodSig = (MethodSignature) joinPoint.getSignature(); + Method method = methodSig.getMethod(); + this.logger.debug("Queuing for method: {}", method); + + this.push(beanType, method.getName(), Arrays.asList(joinPoint.getArgs())); + } + + @Transactional + public void push(Object callbackBean, String callbackMethod, List args) throws AsyncProcessException { + this.logger.trace("push({}, {}, {})", callbackBean, callbackMethod, args); + + UUID msgId = UUID.randomUUID(); + + try { + Connection mqcon = this.factory.createConnection(this.username, this.password); + try { + mqcon.setClientID(this.clientId); + + Session mqsession = mqcon.createSession(true, Session.AUTO_ACKNOWLEDGE); + try { + this.logger.trace("Sending policy as message: {} => {}", callbackMethod, msgId); + + Queue mqqueue = mqsession.createQueue(this.queueName); + + Message mqmsg = mqsession.createMessage(); + mqmsg.setJMSMessageID(msgId.toString()); + mqmsg.setJMSType("v1:" + callbackBean.getClass() + "#" + callbackMethod); + + int i = 0; + for (Object arg : args) + mqmsg.setObjectProperty("arg" + (i++), this.marshal(arg)); + + MessageProducer producer = mqsession.createProducer(mqqueue); + try { + producer.send(mqmsg); + } finally { + producer.close(); + } + + this.logger.debug("Sent node as message: {} => {}", callbackMethod, msgId); + mqsession.commit(); + } finally { + mqsession.close(); + } + } finally { + mqcon.close(); + } + } catch (JMSException je) { + throw new AsyncProcessException("A JMS messaging issue occurred", je); + } + } + + @SuppressWarnings({ "unchecked" }) + private Object unmarshal(Parameter param, Object arg) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { + Class paramType = param.getType(); + this.logger.trace("Unmarshaling parameter of type: {}", paramType); + + if (Version.class.isAssignableFrom(paramType)) { + this.logger.trace("Unmarshaling as JSON object: {}", arg); + Map argMap = (Map) this.om.convertValue(arg, Map.class); + + Map versionPropertiesMap = (Map) argMap.get("properties"); + NodeRef nodeRef = new NodeRef((String) argMap.get("nodeRef")); + + Version version = new VersionImpl(versionPropertiesMap, nodeRef); + this.logger.trace("Unmarshaled version: {} = {}", param.getName(), version); + return version; + } else if (Action.class.isAssignableFrom(paramType)) { + this.logger.trace("Unmarshaling as JSON object: {}", arg); + Map argMap = (Map) this.om.convertValue(arg, Map.class); + + String actionId = (String) argMap.get("actionId"); + NodeRef nodeRef = new NodeRef((String) argMap.get("nodeRef")); + this.logger.trace("Unmarshaling action: {}, {}", actionId, nodeRef); + + Action action = this.actionService.getAction(nodeRef, actionId); + this.logger.trace("Unmarshaled action: {} = {}", param.getName(), action); + return action; + } else if (Collection.class.isAssignableFrom(paramType)) { + this.logger.trace("Unmarshaling as JSON array: {}", arg); + return this.om.convertValue(arg, Collection.class); + } else if (Map.class.isAssignableFrom(paramType)) { + this.logger.trace("Unmarshaling as JSON object: {}", arg); + return this.om.convertValue(arg, Map.class); + } else if (QName.class.isAssignableFrom(paramType)) { + this.logger.trace("Unmarshaling as QName: {}", arg); + return QName.createQName((String) arg); + } else if (Enum.class.isAssignableFrom(paramType)) { + this.logger.trace("Unmarshaling as Enum: {}", arg); + Method cons = paramType.getDeclaredMethod("valueOf", String.class); + return cons.invoke(null, arg.toString()); + } else { + this.logger.trace("Unmarshaling as POJO: {}", arg); + Constructor cons = paramType.getConstructor(String.class); + return cons.newInstance(arg.toString()); + } + } + + private Object marshal(Object arg) { + if (arg instanceof Version) { + Version version = (Version) arg; + Map map = new HashMap<>(); + map.put("nodeRef", version.getFrozenStateNodeRef()); + map.put("properties", version.getVersionProperties()); + + this.logger.trace("Marshaling Version as JSON object: {}", map); + return this.om.convertValue(map, String.class); + } else if (arg instanceof Action) { + Action action = (Action) arg; + Map map = new HashMap<>(); + map.put("nodeRef", action.getNodeRef()); + map.put("actionId", action.getId()); + + this.logger.trace("Marshaling Action as JSON object: {}", map); + return this.om.convertValue(map, String.class); + } else if (arg instanceof Collection) { + List list = new ArrayList<>(((Collection)arg).size()); + for (Object obj : (Collection) arg) + list.add(this.marshal(obj)); + + this.logger.trace("Marshaling Java Collection as JSON array: {}", list); + return this.om.convertValue(list, String.class); + } else if (arg instanceof Map) { + Map map = new HashMap<>(); + for (Entry entry : ((Map) arg).entrySet()) { + Object key = this.marshal(entry.getKey()); + Object value = this.marshal(entry.getValue()); + map.put(key, value); + } + + this.logger.trace("Marshaling Java Map as JSON object: {}", map); + return this.om.convertValue(map, String.class); + } else { + return arg; + } + } + + private M2Model loadModel(NodeRef nodeRef) throws IOException { + ContentReader creader = this.contentService.getReader(nodeRef, ContentModel.PROP_CONTENT); + InputStream istream = creader.getContentInputStream(); + try { + return M2Model.createModel(istream); + } finally { + istream.close(); + } + } + +} diff --git a/src/main/resources/alfresco/module/com.inteligr8.alfresco.annotations-platform-module/alfresco-global.properties b/src/main/resources/alfresco/module/com.inteligr8.alfresco.annotations-platform-module/alfresco-global.properties new file mode 100644 index 0000000..a58d149 --- /dev/null +++ b/src/main/resources/alfresco/module/com.inteligr8.alfresco.annotations-platform-module/alfresco-global.properties @@ -0,0 +1,10 @@ + +inteligr8.async.mq.enabled=false +inteligr8.async.mq.url=${messaging.broker.url} +inteligr8.async.mq.username=${messaging.broker.username} +inteligr8.async.mq.password=${messaging.broker.password} +inteligr8.async.mq.queuePrefix=inteligr8.acs. +inteligr8.async.mq.clientId=acs +inteligr8.async.mq.pool.max=5 + +inteligr8.cache.nodeTypeConstrainable.maxBeans=32 diff --git a/src/main/resources/alfresco/module/com.inteligr8.alfresco.annotations-platform-module/log4j.properties b/src/main/resources/alfresco/module/com.inteligr8.alfresco.annotations-platform-module/log4j.properties new file mode 100644 index 0000000..e88e0d6 --- /dev/null +++ b/src/main/resources/alfresco/module/com.inteligr8.alfresco.annotations-platform-module/log4j.properties @@ -0,0 +1 @@ +log4j.logger.com.inteligr8.alfresco.annotations=info diff --git a/src/main/resources/alfresco/module/com.inteligr8.alfresco.annotations-platform-module/module-context.xml b/src/main/resources/alfresco/module/com.inteligr8.alfresco.annotations-platform-module/module-context.xml new file mode 100644 index 0000000..9edf260 --- /dev/null +++ b/src/main/resources/alfresco/module/com.inteligr8.alfresco.annotations-platform-module/module-context.xml @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/src/main/resources/alfresco/module/com.inteligr8.alfresco.annotations-platform-module/module.properties b/src/main/resources/alfresco/module/com.inteligr8.alfresco.annotations-platform-module/module.properties new file mode 100644 index 0000000..3f654aa --- /dev/null +++ b/src/main/resources/alfresco/module/com.inteligr8.alfresco.annotations-platform-module/module.properties @@ -0,0 +1,4 @@ +module.id=${project.artifactId} +module.title=${project.name} +module.description=${project.description} +module.version=${project.version} diff --git a/src/test/resources/alfresco/extension/debug-log4j.properties b/src/test/resources/alfresco/extension/debug-log4j.properties new file mode 100644 index 0000000..f01fa9a --- /dev/null +++ b/src/test/resources/alfresco/extension/debug-log4j.properties @@ -0,0 +1,8 @@ +# Module debugging +log4j.logger.com.inteligr8.alfresco.foldering=trace + +# WebScript debugging +log4j.logger.org.springframework.extensions.webscripts.ScriptLogger=debug + +# non-WebScript JavaScript execution debugging +log4j.logger.org.alfresco.repo.jscript.ScriptLogger=debug diff --git a/src/test/resources/alfresco/extension/disable-webscript-caching-context.xml b/src/test/resources/alfresco/extension/disable-webscript-caching-context.xml new file mode 100644 index 0000000..07829ea --- /dev/null +++ b/src/test/resources/alfresco/extension/disable-webscript-caching-context.xml @@ -0,0 +1,63 @@ + + + + + + + + javascript + + + js + + + + false + + + + + true + + + + + + + + + + ${spaces.store} + + + ${spaces.company_home.childname} + + + + +