diff --git a/rad.sh b/rad.sh
index 8c1e390..aa08375 100644
--- a/rad.sh
+++ b/rad.sh
@@ -1,7 +1,7 @@
-#!/bin/sh
+#!/bin/bash
discoverArtifactId() {
- ARTIFACT_ID=`mvn -q -Dexpression=project.artifactId -DforceStdout help:evaluate`
+ ARTIFACT_ID=`mvn -q -Dexpression=project.artifactId -DforceStdout help:evaluate | sed 's/\x1B\[[0-9;]\{1,\}[A-Za-z]//g'`
}
rebuild() {
diff --git a/src/main/java/com/inteligr8/alfresco/annotations/Asynchronous.java b/src/main/java/com/inteligr8/alfresco/annotations/Asynchronous.java
index a8b5531..cd7b3c2 100644
--- a/src/main/java/com/inteligr8/alfresco/annotations/Asynchronous.java
+++ b/src/main/java/com/inteligr8/alfresco/annotations/Asynchronous.java
@@ -8,5 +8,7 @@ import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Asynchronous {
+
+ boolean durable() default true;
}
diff --git a/src/main/java/com/inteligr8/alfresco/annotations/ClusterSynchronized.java b/src/main/java/com/inteligr8/alfresco/annotations/ClusterSynchronized.java
new file mode 100644
index 0000000..775f864
--- /dev/null
+++ b/src/main/java/com/inteligr8/alfresco/annotations/ClusterSynchronized.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 ClusterSynchronized {
+
+ String value() default "";
+
+ long acquireWaitBetweenRetriesInMillis() default 100L;
+
+ int acquireMaxRetries() default 300;
+
+ long lockTimeoutInMillis() default 5000L;
+
+}
diff --git a/src/main/java/com/inteligr8/alfresco/annotations/IfNotNull.java b/src/main/java/com/inteligr8/alfresco/annotations/IfNotNull.java
new file mode 100644
index 0000000..e03e5ca
--- /dev/null
+++ b/src/main/java/com/inteligr8/alfresco/annotations/IfNotNull.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 IfNotNull {
+
+}
diff --git a/src/main/java/com/inteligr8/alfresco/annotations/Threadable.java b/src/main/java/com/inteligr8/alfresco/annotations/Threadable.java
new file mode 100644
index 0000000..8d4e26a
--- /dev/null
+++ b/src/main/java/com/inteligr8/alfresco/annotations/Threadable.java
@@ -0,0 +1,17 @@
+package com.inteligr8.alfresco.annotations;
+
+public interface Threadable {
+
+ default Integer getThreads() {
+ return null;
+ }
+
+ default Integer getConcurrency() {
+ return null;
+ }
+
+ default Integer getThreadPriority() {
+ return null;
+ }
+
+}
diff --git a/src/main/java/com/inteligr8/alfresco/annotations/Threaded.java b/src/main/java/com/inteligr8/alfresco/annotations/Threaded.java
new file mode 100644
index 0000000..438ab56
--- /dev/null
+++ b/src/main/java/com/inteligr8/alfresco/annotations/Threaded.java
@@ -0,0 +1,24 @@
+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 Threaded {
+
+ String name() default "";
+
+ int threads() default 1;
+
+ int concurrency() default 0;
+
+ int priority() default Thread.NORM_PRIORITY;
+
+ boolean join() default false;
+
+ long joinWaitMillis() default 0L;
+
+}
diff --git a/src/main/java/com/inteligr8/alfresco/annotations/aspect/AbstractMethodAspect.java b/src/main/java/com/inteligr8/alfresco/annotations/aspect/AbstractMethodAspect.java
new file mode 100644
index 0000000..60fcfbb
--- /dev/null
+++ b/src/main/java/com/inteligr8/alfresco/annotations/aspect/AbstractMethodAspect.java
@@ -0,0 +1,53 @@
+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;
+
+public class AbstractMethodAspect extends AbstractWarnOnceService {
+
+ protected A getAnnotation(ProceedingJoinPoint joinPoint, Class annotationClass, boolean warnReturn, boolean warnThrows) {
+ Method method = this.getMethod(joinPoint, annotationClass, warnReturn, warnThrows);
+ return method.getAnnotation(annotationClass);
+ }
+
+ protected Method getMethod(ProceedingJoinPoint joinPoint, Object messagePrefix, boolean warnReturn, boolean warnThrows) {
+ if (!(joinPoint.getSignature() instanceof MethodSignature))
+ throw new IllegalStateException(this.createMessagePrefix(messagePrefix) + " must be on methods");
+
+ MethodSignature methodSig = (MethodSignature) joinPoint.getSignature();
+ if (warnReturn && methodSig.getReturnType() != null && !this.isReturnTypeVoid(methodSig)) {
+ this.warn(joinPoint.toLongString(),
+ "{} has a return value or throws clause; 'null' is always returned and subthread exceptions don't propagate: {}: {}",
+ this.createMessagePrefix(messagePrefix), joinPoint, methodSig.getReturnType());
+ }
+
+ if (warnThrows && methodSig.getExceptionTypes() != null && methodSig.getExceptionTypes().length > 0) {
+ this.warn(joinPoint.toLongString(),
+ "{} has a return value or throws clause; 'null' is always returned and subthread exceptions don't propagate: {}: {}",
+ this.createMessagePrefix(messagePrefix), joinPoint, methodSig.getExceptionTypes());
+ }
+
+ return methodSig.getMethod();
+ }
+
+ protected boolean isReturnTypeVoid(MethodSignature methodSig) {
+ return Void.class.equals(methodSig.getReturnType()) || void.class.equals(methodSig.getReturnType());
+ }
+
+ private String createMessagePrefix(Object obj) {
+ if (obj instanceof Class>) {
+ Class> clazz = (Class>) obj;
+ if (clazz.isAnnotation()) {
+ return "The @" + ((Class>) obj).getSimpleName() + " annotated method";
+ } else {
+ return "The " + ((Class>) obj).getSimpleName() + " class method";
+ }
+ } else {
+ return obj.toString();
+ }
+ }
+
+}
diff --git a/src/main/java/com/inteligr8/alfresco/annotations/aspect/MethodOrParameterAspect.java b/src/main/java/com/inteligr8/alfresco/annotations/aspect/AbstractMethodOrParameterAspect.java
similarity index 94%
rename from src/main/java/com/inteligr8/alfresco/annotations/aspect/MethodOrParameterAspect.java
rename to src/main/java/com/inteligr8/alfresco/annotations/aspect/AbstractMethodOrParameterAspect.java
index 2124abe..47f18d2 100644
--- a/src/main/java/com/inteligr8/alfresco/annotations/aspect/MethodOrParameterAspect.java
+++ b/src/main/java/com/inteligr8/alfresco/annotations/aspect/AbstractMethodOrParameterAspect.java
@@ -8,7 +8,7 @@ import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-public abstract class MethodOrParameterAspect {
+public abstract class AbstractMethodOrParameterAspect {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
diff --git a/src/main/java/com/inteligr8/alfresco/annotations/aspect/AbstractWarnOnceService.java b/src/main/java/com/inteligr8/alfresco/annotations/aspect/AbstractWarnOnceService.java
new file mode 100644
index 0000000..d14e6f0
--- /dev/null
+++ b/src/main/java/com/inteligr8/alfresco/annotations/aspect/AbstractWarnOnceService.java
@@ -0,0 +1,20 @@
+package com.inteligr8.alfresco.annotations.aspect;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class AbstractWarnOnceService {
+
+ private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+ private Set warned = new HashSet<>();
+
+ protected void warn(String key, String message, Object... arguments) {
+ if (this.warned.add(key))
+ this.logger.warn(message, arguments);
+ }
+
+}
diff --git a/src/main/java/com/inteligr8/alfresco/annotations/aspect/AsyncAspect.java b/src/main/java/com/inteligr8/alfresco/annotations/aspect/AsyncAspect.java
index 221f887..cd653c3 100644
--- a/src/main/java/com/inteligr8/alfresco/annotations/aspect/AsyncAspect.java
+++ b/src/main/java/com/inteligr8/alfresco/annotations/aspect/AsyncAspect.java
@@ -3,21 +3,32 @@ 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.DeclarePrecedence;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import com.inteligr8.alfresco.annotations.Asynchronous;
import com.inteligr8.alfresco.annotations.service.AsyncService;
@Aspect
-public class AsyncAspect {
+@DeclarePrecedence("com.inteligr8.alfresco.annotations.aspect.AsyncAspect, *")
+//@Order(Ordered.HIGHEST_PRECEDENCE + Byte.MAX_VALUE)
+//@Component
+public class AsyncAspect extends AbstractMethodAspect {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
- private AsyncService asyncService;
+ @Qualifier("async.mq")
+ private AsyncService durableAsyncService;
+
+ @Autowired
+ @Qualifier("async.thread")
+ private AsyncService volatileAsyncService;
@Pointcut("@annotation(com.inteligr8.alfresco.annotations.Asynchronous) && execution(* *(..))")
public void isAsyncAnnotated() {
@@ -25,16 +36,28 @@ public class AsyncAspect {
@Around("isAsyncAnnotated()")
public Object async(ProceedingJoinPoint joinPoint) throws Throwable {
- if (this.asyncService.isCurrentThreadAsynchronous()) {
- this.logger.trace("Intercepted an @Async method call while already asynchronous; executing synchronously");
+ this.logger.trace("async({})", joinPoint);
+
+ Asynchronous async = this.getAnnotation(joinPoint, Asynchronous.class, true, true);
+
+ AsyncService asyncService = async.durable() ? this.durableAsyncService : this.volatileAsyncService;
+ if (!asyncService.isEnabled()) {
+ this.warn(joinPoint.toLongString(),
+ "Intercepted an @Asynchronous method call while the appropriate asynchronous service is not enabled; continuing synchronously");
+ return joinPoint.proceed();
+ } else if (asyncService.isCurrentThreadAsynchronous()) {
+ this.logger.debug("Intercepted an @Asynchronous method call while already asynchronous; continuing synchronously");
return joinPoint.proceed();
} else {
- this.logger.trace("Intercepted an @Async method call; redirecting to Async service");
- this.asyncService.push(joinPoint);
+ this.logger.trace("Intercepted an @Asynchronous method call; redirecting to the appropriate asynchronous service");
+ asyncService.push(joinPoint);
MethodSignature methodSig = (MethodSignature) joinPoint.getSignature();
- if (!Void.class.equals(methodSig.getReturnType()))
- this.logger.warn("An @Asynchronous method returns a value, which is not expected/allowed: {}", methodSig.getMethod());
+ if (!this.isReturnTypeVoid(methodSig)) {
+ this.warn(joinPoint.toLongString(),
+ "An @Asynchronous method returns a value, which is not expected/allowed: {}",
+ methodSig.getMethod());
+ }
return null;
}
}
diff --git a/src/main/java/com/inteligr8/alfresco/annotations/aspect/AuthorizedAspect.java b/src/main/java/com/inteligr8/alfresco/annotations/aspect/AuthorizedAspect.java
index 7e0f4ce..4d2ba02 100644
--- a/src/main/java/com/inteligr8/alfresco/annotations/aspect/AuthorizedAspect.java
+++ b/src/main/java/com/inteligr8/alfresco/annotations/aspect/AuthorizedAspect.java
@@ -8,19 +8,20 @@ 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.DeclarePrecedence;
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 com.inteligr8.alfresco.annotations.Authorizable;
import com.inteligr8.alfresco.annotations.Authorized;
import com.inteligr8.alfresco.annotations.AuthorizedAsSystem;
@Aspect
-@Order(Ordered.HIGHEST_PRECEDENCE + Short.MAX_VALUE)
+@DeclarePrecedence("com.inteligr8.alfresco.annotations.aspect.AuthorizedAspect, com.inteligr8.alfresco.annotations.aspect.RetryingTransactionAspect")
+//@Order(Ordered.HIGHEST_PRECEDENCE + Short.MAX_VALUE)
+//@Component
public class AuthorizedAspect {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
diff --git a/src/main/java/com/inteligr8/alfresco/annotations/aspect/ChildIsPrimaryAspect.java b/src/main/java/com/inteligr8/alfresco/annotations/aspect/ChildIsPrimaryAspect.java
index c46fea0..3a54f32 100644
--- a/src/main/java/com/inteligr8/alfresco/annotations/aspect/ChildIsPrimaryAspect.java
+++ b/src/main/java/com/inteligr8/alfresco/annotations/aspect/ChildIsPrimaryAspect.java
@@ -9,14 +9,12 @@ 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 com.inteligr8.alfresco.annotations.IfChildAssociationIsPrimary;
@Aspect
-@Order(Ordered.HIGHEST_PRECEDENCE + Byte.MAX_VALUE) // ordering before transaction/authorized
-public class ChildIsPrimaryAspect extends MethodOrParameterAspect {
+//@Component
+public class ChildIsPrimaryAspect extends AbstractMethodOrParameterAspect {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
diff --git a/src/main/java/com/inteligr8/alfresco/annotations/aspect/ClusterSynchronizedAspect.java b/src/main/java/com/inteligr8/alfresco/annotations/aspect/ClusterSynchronizedAspect.java
new file mode 100644
index 0000000..2d48225
--- /dev/null
+++ b/src/main/java/com/inteligr8/alfresco/annotations/aspect/ClusterSynchronizedAspect.java
@@ -0,0 +1,67 @@
+package com.inteligr8.alfresco.annotations.aspect;
+
+import java.lang.reflect.Method;
+
+import org.alfresco.repo.lock.JobLockService;
+import org.alfresco.service.namespace.QName;
+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.DeclarePrecedence;
+import org.aspectj.lang.annotation.Pointcut;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import com.inteligr8.alfresco.annotations.ClusterSynchronized;
+
+@Aspect
+@DeclarePrecedence("com.inteligr8.alfresco.annotations.aspect.RetryingTransactionAspect, com.inteligr8.alfresco.annotations.aspect.ClusterSynchronizedAspect")
+//@Component
+//@Order(Ordered.LOWEST_PRECEDENCE - Byte.MAX_VALUE)
+public class ClusterSynchronizedAspect extends AbstractMethodAspect {
+
+ private static final String NS = "http://inteligr8.com/alfresco/model";
+ private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+ @Autowired
+ private JobLockService jobLockService;
+
+ @Pointcut("@annotation(com.inteligr8.alfresco.annotations.ClusterSynchronized) && execution(* *(..))")
+ public void isClusterSyncAnnotated() {
+ }
+
+ @Around("isClusterSyncAnnotated()")
+ public Object clusterSync(ProceedingJoinPoint joinPoint) throws Throwable {
+ this.logger.trace("clusterSync({})", joinPoint);
+
+ Method method = this.getMethod(joinPoint, ClusterSynchronized.class, false, false);
+ ClusterSynchronized clusterSync = method.getAnnotation(ClusterSynchronized.class);
+
+ QName lockQName = this.getLockQName(clusterSync, method);
+
+ this.logger.debug("Acquiring cluster lock: {}", lockQName);
+ String lockToken = this.jobLockService.getLock(lockQName, clusterSync.lockTimeoutInMillis(),
+ clusterSync.acquireWaitBetweenRetriesInMillis(), clusterSync.acquireMaxRetries());
+ try {
+ this.logger.trace("Acquired cluster lock: {}", lockQName);
+
+ return joinPoint.proceed();
+ } finally {
+ this.logger.debug("Releasing cluster lock: {}", lockQName);
+ this.jobLockService.releaseLock(lockToken, lockQName);
+ }
+ }
+
+ private QName getLockQName(ClusterSynchronized clusterSync, Method method) {
+ String lockName = StringUtils.trimToNull(clusterSync.value());
+ if (lockName != null) {
+ return QName.createQNameWithValidLocalName(NS, lockName);
+ } else {
+ String methodId = method.getDeclaringClass().getSimpleName() + "_" + method.getName();
+ return QName.createQNameWithValidLocalName(NS, methodId);
+ }
+ }
+
+}
diff --git a/src/main/java/com/inteligr8/alfresco/annotations/aspect/NodeAspectAspect.java b/src/main/java/com/inteligr8/alfresco/annotations/aspect/NodeAspectAspect.java
index ea16e82..95edcb6 100644
--- a/src/main/java/com/inteligr8/alfresco/annotations/aspect/NodeAspectAspect.java
+++ b/src/main/java/com/inteligr8/alfresco/annotations/aspect/NodeAspectAspect.java
@@ -18,6 +18,7 @@ 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.DeclarePrecedence;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -28,6 +29,8 @@ import com.inteligr8.alfresco.annotations.NodeAspectConstrainable;
import com.inteligr8.alfresco.annotations.IfNodeHasAspect;
@Aspect
+@DeclarePrecedence("com.inteligr8.alfresco.annotations.aspect.RetryingTransactionAspect, com.inteligr8.alfresco.annotations.aspect.NodeAspectAspect")
+//@Component
public class NodeAspectAspect extends QNameBasedAspect {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
diff --git a/src/main/java/com/inteligr8/alfresco/annotations/aspect/NodeTypeAspect.java b/src/main/java/com/inteligr8/alfresco/annotations/aspect/NodeTypeAspect.java
index 7bd4f3b..a89eb04 100644
--- a/src/main/java/com/inteligr8/alfresco/annotations/aspect/NodeTypeAspect.java
+++ b/src/main/java/com/inteligr8/alfresco/annotations/aspect/NodeTypeAspect.java
@@ -18,6 +18,7 @@ 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.DeclarePrecedence;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -28,6 +29,8 @@ import com.inteligr8.alfresco.annotations.IfNodeOfType;
import com.inteligr8.alfresco.annotations.NodeTypeConstrainable;
@Aspect
+@DeclarePrecedence("com.inteligr8.alfresco.annotations.aspect.RetryingTransactionAspect, com.inteligr8.alfresco.annotations.aspect.NodeTypeAspect")
+//@Component
public class NodeTypeAspect extends QNameBasedAspect {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
diff --git a/src/main/java/com/inteligr8/alfresco/annotations/aspect/NotNullAspect.java b/src/main/java/com/inteligr8/alfresco/annotations/aspect/NotNullAspect.java
index f7aa81b..3a13747 100644
--- a/src/main/java/com/inteligr8/alfresco/annotations/aspect/NotNullAspect.java
+++ b/src/main/java/com/inteligr8/alfresco/annotations/aspect/NotNullAspect.java
@@ -2,8 +2,6 @@ package com.inteligr8.alfresco.annotations.aspect;
import java.lang.reflect.Method;
-import javax.annotation.Nonnull;
-
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
@@ -11,27 +9,28 @@ 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 com.inteligr8.alfresco.annotations.IfNotNull;
@Aspect
-@Order(Ordered.HIGHEST_PRECEDENCE + 64)
+//@Order(Ordered.HIGHEST_PRECEDENCE + 64)
+//@Component
public class NotNullAspect {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
- @Pointcut("execution(* *(@javax.annotation.Nonnull (*), ..))")
- public void isNonnullAnnotated() {
+ @Pointcut("execution(* *(@com.inteligr8.alfresco.annotations.IfNotNull (*), ..))")
+ public void isNotNullAnnotated() {
}
- @Around("isNonnullAnnotated()")
+ @Around("isNotNullAnnotated()")
public Object isNotNull(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature methodSig = (MethodSignature) joinPoint.getSignature();
Method method = methodSig.getMethod();
for (int p = 0; p < method.getParameterCount(); p++) {
- if (joinPoint.getArgs()[p] == null && method.getParameters()[p].isAnnotationPresent(Nonnull.class)) {
- this.logger.debug("A @Nonnull parameter is `null`; skipping method: {}", method);
+ if (joinPoint.getArgs()[p] == null && method.getParameters()[p].isAnnotationPresent(IfNotNull.class)) {
+ this.logger.debug("A @IfNotNull parameter is `null`; skipping method: {}", method);
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
index ad60d2e..c477621 100644
--- a/src/main/java/com/inteligr8/alfresco/annotations/aspect/OperableNodeAspect.java
+++ b/src/main/java/com/inteligr8/alfresco/annotations/aspect/OperableNodeAspect.java
@@ -14,6 +14,7 @@ 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.DeclarePrecedence;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -22,7 +23,9 @@ import org.springframework.beans.factory.annotation.Autowired;
import com.inteligr8.alfresco.annotations.IfNodeExists;
@Aspect
-public class OperableNodeAspect extends MethodOrParameterAspect {
+@DeclarePrecedence("com.inteligr8.alfresco.annotations.aspect.RetryingTransactionAspect, com.inteligr8.alfresco.annotations.aspect.OperableNodeAspect")
+//@Component
+public class OperableNodeAspect extends AbstractMethodOrParameterAspect {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
diff --git a/src/main/java/com/inteligr8/alfresco/annotations/aspect/QNameBasedAspect.java b/src/main/java/com/inteligr8/alfresco/annotations/aspect/QNameBasedAspect.java
index ef437d7..2a944e7 100644
--- a/src/main/java/com/inteligr8/alfresco/annotations/aspect/QNameBasedAspect.java
+++ b/src/main/java/com/inteligr8/alfresco/annotations/aspect/QNameBasedAspect.java
@@ -17,7 +17,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
-public abstract class QNameBasedAspect extends MethodOrParameterAspect {
+public abstract class QNameBasedAspect extends AbstractMethodOrParameterAspect {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
diff --git a/src/main/java/com/inteligr8/alfresco/annotations/aspect/RetryingTransactionAspect.java b/src/main/java/com/inteligr8/alfresco/annotations/aspect/RetryingTransactionAspect.java
index b8bc15b..d5a6233 100644
--- a/src/main/java/com/inteligr8/alfresco/annotations/aspect/RetryingTransactionAspect.java
+++ b/src/main/java/com/inteligr8/alfresco/annotations/aspect/RetryingTransactionAspect.java
@@ -1,38 +1,34 @@
package com.inteligr8.alfresco.annotations.aspect;
import java.lang.reflect.Method;
-import java.util.HashSet;
-import java.util.Set;
import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
+import org.alfresco.repo.transaction.AlfrescoTransactionSupport.TxnReadState;
import org.alfresco.repo.transaction.RetryingTransactionHelper;
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
import org.alfresco.service.transaction.TransactionService;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
-import org.aspectj.lang.annotation.Before;
+import org.aspectj.lang.annotation.DeclarePrecedence;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.core.Ordered;
-import org.springframework.core.annotation.Order;
import org.springframework.transaction.IllegalTransactionStateException;
import org.springframework.transaction.annotation.Transactional;
import com.inteligr8.alfresco.annotations.TransactionalRetryable;
@Aspect
-@Order(Ordered.LOWEST_PRECEDENCE - Short.MAX_VALUE)
+@DeclarePrecedence("com.inteligr8.alfresco.annotations.aspect.AuthorizedAspect, com.inteligr8.alfresco.annotations.aspect.RetryingTransactionAspect")
//@Component
+//@Order(Ordered.LOWEST_PRECEDENCE - Short.MAX_VALUE)
public class RetryingTransactionAspect {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
- private final Set warned = new HashSet<>();
-
@Autowired
private TransactionService txService;
@@ -40,33 +36,29 @@ public class RetryingTransactionAspect {
public void isTransactionalAnnotated() {
}
- @Pointcut("@annotation(org.springframework.transaction.annotation.TransactionalRetryable) && execution(* *(..))")
+ @Pointcut("@annotation(com.inteligr8.alfresco.annotations.TransactionalRetryable) && execution(* *(..))")
public void isTransactionalRetryableAnnotated() {
}
- @Around("isTransactionalAnnotated()")
+ @Around("isTransactionalAnnotated() || isTransactionalRetryableAnnotated()")
public Object retryingTransactional(ProceedingJoinPoint joinPoint) throws Throwable {
this.logger.trace("retryingTransactional({})", joinPoint);
Method method = this.getMethod(joinPoint);
Transactional txl = method.getAnnotation(Transactional.class);
+ TransactionalRetryable txtry = method.getAnnotation(TransactionalRetryable.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());
return this.execute(joinPoint, txl, txtry);
+ } else if (this.doCreateNewTxRetryContext(txtry)) {
+ this.logger.debug("Changing TX context: retries: {}", txtry.maxRetries());
+ return this.execute(joinPoint, null, txtry);
} else {
return joinPoint.proceed();
}
}
- @Before("isTransactionalRetryableAnnotated() && !isTransactionalAnnotated()")
- public void warn(ProceedingJoinPoint joinPoint) throws Throwable {
- if (this.warned.add(joinPoint.toLongString()))
- this.logger.warn("A @TransactionalRetryable annotation was found without a @Transactional annotation; it will be ignored: {}", joinPoint);
- }
-
private Method getMethod(ProceedingJoinPoint joinPoint) {
if (!(joinPoint.getSignature() instanceof MethodSignature))
throw new IllegalStateException("The @Transactional or @TransactionalRetryable annotations must be on methods");
@@ -76,6 +68,9 @@ public class RetryingTransactionAspect {
}
private boolean isReadStateChange(Transactional txl) {
+ if (txl == null)
+ return false;
+
switch (txl.propagation()) {
case NEVER:
case NOT_SUPPORTED:
@@ -97,8 +92,14 @@ public class RetryingTransactionAspect {
}
}
+ private boolean doCreateNewTxRetryContext(TransactionalRetryable txtry) {
+ return txtry != null;
+ }
+
private boolean doCreateNewTxContext(Transactional txl) {
- switch (txl.propagation()) {
+ if (txl == null) {
+ return false;
+ } else switch (txl.propagation()) {
case NEVER:
switch (AlfrescoTransactionSupport.getTransactionReadState()) {
case TXN_NONE:
@@ -164,12 +165,13 @@ public class RetryingTransactionAspect {
if (txtry.incRetryWaitInMillis() > 0)
rthelper.setRetryWaitIncrementMs(txtry.incRetryWaitInMillis());
}
- if (txl.timeout() > 0)
+ if (txl != null && txl.timeout() > 0)
rthelper.setMaxExecutionMs(txl.timeout() * 1000L);
try {
this.logger.trace("source tx: {}", AlfrescoTransactionSupport.getTransactionId());
- return rthelper.doInTransaction(rtcallback, txl.readOnly(), true);
+ boolean readonly = txl != null && txl.readOnly() || txl == null && AlfrescoTransactionSupport.getTransactionReadState() == TxnReadState.TXN_READ_ONLY;
+ return rthelper.doInTransaction(rtcallback, readonly, txl != null);
} catch (RuntimeException re) {
// attempt to unwrap the exception
if (re.getMessage() == null) {
diff --git a/src/main/java/com/inteligr8/alfresco/annotations/aspect/ThreadedAspect.java b/src/main/java/com/inteligr8/alfresco/annotations/aspect/ThreadedAspect.java
new file mode 100644
index 0000000..2521f02
--- /dev/null
+++ b/src/main/java/com/inteligr8/alfresco/annotations/aspect/ThreadedAspect.java
@@ -0,0 +1,178 @@
+package com.inteligr8.alfresco.annotations.aspect;
+
+import java.lang.Thread.UncaughtExceptionHandler;
+import java.lang.reflect.Method;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Supplier;
+
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.DeclarePrecedence;
+import org.aspectj.lang.annotation.Pointcut;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import com.inteligr8.alfresco.annotations.Threadable;
+import com.inteligr8.alfresco.annotations.Threaded;
+
+@Aspect
+@DeclarePrecedence("com.inteligr8.alfresco.annotations.aspect.AsyncAspect, com.inteligr8.alfresco.annotations.aspect.ThreadedAspect, *")
+//@Order(Ordered.HIGHEST_PRECEDENCE + Byte.MAX_VALUE)
+//@Component
+public class ThreadedAspect extends AbstractMethodAspect {
+
+ private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+ private ThreadLocal> nested = ThreadLocal.withInitial(new Supplier>() {
+ public Set get() {
+ return new HashSet<>();
+ }
+ });
+
+ @Pointcut("@annotation(com.inteligr8.alfresco.annotations.Threaded) && execution(* *(..))")
+ public void isThreadedAnnotated() {
+ }
+
+ @Around("isThreadedAnnotated()")
+ public Object threaded(ProceedingJoinPoint joinPoint) throws Throwable {
+ // AspectJ does not recursively match annotations if the same thread is calling joinPoint.proceed()
+ // but when a different thread calls it, it doesn't know it is already processing the annotation
+ // so we are going to use a ThreadLocal to prevent recursion across threads
+ if (this.nested.get().contains(joinPoint.getSignature().toLongString()))
+ return joinPoint.proceed();
+
+ this.logger.trace("threaded({})", joinPoint);
+
+ Threaded threaded = this.getAnnotation(joinPoint, Threaded.class, true, true);
+ MergedThreadConfiguration threadConfig = new MergedThreadConfiguration(joinPoint, threaded);
+
+ ThreadFactoryBuilder tfbuilder = new ThreadFactoryBuilder()
+ .setPriority(threadConfig.getThreadPriority())
+ .setUncaughtExceptionHandler(new UncaughtExceptionHandler() {
+ @Override
+ public void uncaughtException(Thread t, Throwable e) {
+ logger.error("The thread '" + t.getName() + "' had an exception", e);
+ }
+ });
+ if (threaded.name().length() > 0)
+ tfbuilder.setNameFormat(threaded.name() + "-%d");
+
+ Integer concurrency = threadConfig.getConcurrency();
+ if (concurrency == null)
+ concurrency = threadConfig.getThreads();
+ BlockingQueue threadQueue = new ArrayBlockingQueue<>(threadConfig.getThreads().intValue());
+
+ ThreadPoolExecutor threadExecutor = new ThreadPoolExecutor(concurrency.intValue(), concurrency.intValue(),
+ 1L, TimeUnit.SECONDS,
+ threadQueue,
+ tfbuilder.build());
+
+ Callable