16 Commits

13 changed files with 355 additions and 26 deletions

24
pom.xml
View File

@@ -5,7 +5,7 @@
<groupId>com.inteligr8.alfresco</groupId> <groupId>com.inteligr8.alfresco</groupId>
<artifactId>annotations-platform-module</artifactId> <artifactId>annotations-platform-module</artifactId>
<version>1.0.0</version> <version>1.0.4</version>
<packaging>jar</packaging> <packaging>jar</packaging>
<name>Annotations ACS Platform Module</name> <name>Annotations ACS Platform Module</name>
@@ -72,9 +72,29 @@
<dependency> <dependency>
<groupId>com.inteligr8.alfresco</groupId> <groupId>com.inteligr8.alfresco</groupId>
<artifactId>aspectj-platform-module</artifactId> <artifactId>aspectj-platform-module</artifactId>
<version>1.0.0</version> <version>1.0.1</version>
<type>amp</type>
</dependency>
<dependency>
<groupId>javax.transaction</groupId>
<artifactId>javax.transaction-api</artifactId>
<version>1.3</version>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency>
<groupId>jakarta.transaction</groupId>
<artifactId>jakarta.transaction-api</artifactId>
<version>2.0.1</version>
<scope>provided</scope>
</dependency>
<!-- AMP resources are included in the WAR, not the extension directory; this makes aspectjweaver available to javaagent -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>${aspectj.version}</version>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
<build> <build>

View File

@@ -1,5 +1,6 @@
package com.inteligr8.alfresco.annotations.aspect; package com.inteligr8.alfresco.annotations.aspect;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import org.alfresco.repo.transaction.AlfrescoTransactionSupport; import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
@@ -20,6 +21,10 @@ import org.springframework.transaction.IllegalTransactionStateException;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import com.inteligr8.alfresco.annotations.TransactionalRetryable; import com.inteligr8.alfresco.annotations.TransactionalRetryable;
import com.inteligr8.alfresco.annotations.util.JakartaTransactionalAnnotationAdapter;
import com.inteligr8.alfresco.annotations.util.JtaTransactionalAnnotationAdapter;
import com.inteligr8.alfresco.annotations.util.SpringTransactionalAnnotationAdapter;
import com.inteligr8.alfresco.annotations.util.TransactionalAnnotationAdapter;
/** /**
* This aspect implements the @Transactional and @TransactionalRetryable * This aspect implements the @Transactional and @TransactionalRetryable
@@ -50,20 +55,28 @@ public class RetryingTransactionAspect {
public void isTransactionalAnnotated() { public void isTransactionalAnnotated() {
} }
@Pointcut("@annotation(javax.transaction.Transactional) && execution(* *(..))")
public void isJtaTransactionalAnnotated() {
}
@Pointcut("@annotation(jakarta.transaction.Transactional) && execution(* *(..))")
public void isJakartaTransactionalAnnotated() {
}
@Pointcut("@annotation(com.inteligr8.alfresco.annotations.TransactionalRetryable) && execution(* *(..))") @Pointcut("@annotation(com.inteligr8.alfresco.annotations.TransactionalRetryable) && execution(* *(..))")
public void isTransactionalRetryableAnnotated() { public void isTransactionalRetryableAnnotated() {
} }
@Around("isTransactionalAnnotated() || isTransactionalRetryableAnnotated()") @Around("isTransactionalAnnotated() || isJtaTransactionalAnnotated() || isJakartaTransactionalAnnotated() || isTransactionalRetryableAnnotated()")
public Object retryingTransactional(ProceedingJoinPoint joinPoint) throws Throwable { public Object retryingTransactional(ProceedingJoinPoint joinPoint) throws Throwable {
this.logger.trace("retryingTransactional({})", joinPoint); this.logger.trace("retryingTransactional({})", joinPoint);
Method method = this.getMethod(joinPoint); Method method = this.getMethod(joinPoint);
Transactional txl = method.getAnnotation(Transactional.class); TransactionalAnnotationAdapter txl = this.wrapTransactionalAnnotation(method);
TransactionalRetryable txtry = method.getAnnotation(TransactionalRetryable.class); TransactionalRetryable txtry = method.getAnnotation(TransactionalRetryable.class);
if (this.doCreateNewTxContext(txl) || this.isReadStateChange(txl)) { if (this.doCreateNewTxContext(txl) || this.isReadStateChange(txl)) {
this.logger.debug("Changing TX context: {} => [ro: {}, new: {}]", AlfrescoTransactionSupport.getTransactionReadState(), txl.readOnly(), txl.propagation()); this.logger.debug("Changing TX context: {} => [ro: {}, new: {}]", AlfrescoTransactionSupport.getTransactionReadState(), txl.isReadOnly(), txl.getPropagation());
return this.execute(joinPoint, txl, txtry); return this.execute(joinPoint, txl, txtry);
} else if (this.doCreateNewTxRetryContext(txtry)) { } else if (this.doCreateNewTxRetryContext(txtry)) {
this.logger.debug("Changing TX context: retries: {}", txtry.maxRetries()); this.logger.debug("Changing TX context: retries: {}", txtry.maxRetries());
@@ -73,6 +86,33 @@ public class RetryingTransactionAspect {
} }
} }
private TransactionalAnnotationAdapter wrapTransactionalAnnotation(Method method) {
Annotation txl = method.getAnnotation(Transactional.class);
if (txl != null)
return new SpringTransactionalAnnotationAdapter((Transactional) txl);
txl = this.getOptionalAnnotation(method, "javax.transaction.Transactional");
if (txl != null)
return new JtaTransactionalAnnotationAdapter((javax.transaction.Transactional) txl);
txl = this.getOptionalAnnotation(method, "jakarta.transaction.Transactional");
if (txl != null)
return new JakartaTransactionalAnnotationAdapter((jakarta.transaction.Transactional) txl);
return null;
}
private <A extends Annotation> A getOptionalAnnotation(Method method, String fullyQualifiedAnnotationName) {
try {
@SuppressWarnings("unchecked")
Class<A> annotationClass = (Class<A>) Class.forName(fullyQualifiedAnnotationName);
return method.getAnnotation(annotationClass);
} catch (ClassNotFoundException cnfe) {
this.logger.trace("The {} annotation is not available in the classpath; assuming not set", fullyQualifiedAnnotationName);
return null;
}
}
private Method getMethod(ProceedingJoinPoint joinPoint) { private Method getMethod(ProceedingJoinPoint joinPoint) {
if (!(joinPoint.getSignature() instanceof MethodSignature)) if (!(joinPoint.getSignature() instanceof MethodSignature))
throw new IllegalStateException("The @Transactional or @TransactionalRetryable annotations must be on methods"); throw new IllegalStateException("The @Transactional or @TransactionalRetryable annotations must be on methods");
@@ -81,11 +121,11 @@ public class RetryingTransactionAspect {
return methodSig.getMethod(); return methodSig.getMethod();
} }
private boolean isReadStateChange(Transactional txl) { private boolean isReadStateChange(TransactionalAnnotationAdapter txl) {
if (txl == null) if (txl == null)
return false; return false;
switch (txl.propagation()) { switch (txl.getPropagation()) {
case NEVER: case NEVER:
case NOT_SUPPORTED: case NOT_SUPPORTED:
case SUPPORTS: case SUPPORTS:
@@ -98,9 +138,9 @@ public class RetryingTransactionAspect {
case TXN_NONE: case TXN_NONE:
return true; return true;
case TXN_READ_ONLY: case TXN_READ_ONLY:
return !txl.readOnly(); return !txl.isReadOnly();
case TXN_READ_WRITE: case TXN_READ_WRITE:
return txl.readOnly(); return txl.isReadOnly();
default: default:
throw new IllegalStateException(); throw new IllegalStateException();
} }
@@ -110,10 +150,10 @@ public class RetryingTransactionAspect {
return txtry != null; return txtry != null;
} }
private boolean doCreateNewTxContext(Transactional txl) { private boolean doCreateNewTxContext(TransactionalAnnotationAdapter txl) {
if (txl == null) { if (txl == null) {
return false; return false;
} else switch (txl.propagation()) { } else switch (txl.getPropagation()) {
case NEVER: case NEVER:
switch (AlfrescoTransactionSupport.getTransactionReadState()) { switch (AlfrescoTransactionSupport.getTransactionReadState()) {
case TXN_NONE: case TXN_NONE:
@@ -126,14 +166,20 @@ public class RetryingTransactionAspect {
case TXN_NONE: case TXN_NONE:
throw new IllegalTransactionStateException("A transaction does not exist where one is mandatory"); throw new IllegalTransactionStateException("A transaction does not exist where one is mandatory");
case TXN_READ_ONLY: case TXN_READ_ONLY:
if (!txl.readOnly()) if (!txl.isReadOnly())
throw new IllegalTransactionStateException("A read-only transaction exists where a read/write one is mandatory"); throw new IllegalTransactionStateException("A read-only transaction exists where a read/write one is mandatory");
case TXN_READ_WRITE: case TXN_READ_WRITE:
if (txl.readOnly()) if (txl.isReadOnly())
throw new IllegalTransactionStateException("A read/write transaction exists where a read-only one is mandatory"); throw new IllegalTransactionStateException("A read/write transaction exists where a read-only one is mandatory");
} }
case NOT_SUPPORTED:
switch (AlfrescoTransactionSupport.getTransactionReadState()) {
case TXN_NONE:
return false;
default:
throw new IllegalTransactionStateException("A transaction exists and pausing it is not supported");
}
case SUPPORTS: case SUPPORTS:
//case NOT_SUPPORTED: not supported; we would have to create another thread to simulate
return false; return false;
case REQUIRED: case REQUIRED:
switch (AlfrescoTransactionSupport.getTransactionReadState()) { switch (AlfrescoTransactionSupport.getTransactionReadState()) {
@@ -145,11 +191,11 @@ public class RetryingTransactionAspect {
case REQUIRES_NEW: case REQUIRES_NEW:
return true; return true;
default: default:
throw new IllegalTransactionStateException("The transactional propagation is not supported: " + txl.propagation()); throw new IllegalTransactionStateException("The transactional propagation is not supported: " + txl.getPropagation());
} }
} }
private Object execute(final ProceedingJoinPoint joinPoint, Transactional txl, TransactionalRetryable txtry) throws Throwable { private Object execute(final ProceedingJoinPoint joinPoint, TransactionalAnnotationAdapter txl, TransactionalRetryable txtry) throws Throwable {
RetryingTransactionCallback<Object> rtcallback = new RetryingTransactionCallback<Object>() { RetryingTransactionCallback<Object> rtcallback = new RetryingTransactionCallback<Object>() {
@Override @Override
public Object execute() throws Throwable { public Object execute() throws Throwable {
@@ -179,12 +225,12 @@ public class RetryingTransactionAspect {
if (txtry.incRetryWaitInMillis() > 0) if (txtry.incRetryWaitInMillis() > 0)
rthelper.setRetryWaitIncrementMs(txtry.incRetryWaitInMillis()); rthelper.setRetryWaitIncrementMs(txtry.incRetryWaitInMillis());
} }
if (txl != null && txl.timeout() > 0) if (txl != null && txl.getTimeoutInSeconds() > 0)
rthelper.setMaxExecutionMs(txl.timeout() * 1000L); rthelper.setMaxExecutionMs(txl.getTimeoutInSeconds() * 1000L);
try { try {
this.logger.trace("source tx: {}", AlfrescoTransactionSupport.getTransactionId()); this.logger.trace("source tx: {}", AlfrescoTransactionSupport.getTransactionId());
boolean readonly = txl != null && txl.readOnly() || txl == null && AlfrescoTransactionSupport.getTransactionReadState() == TxnReadState.TXN_READ_ONLY; boolean readonly = txl != null && txl.isReadOnly() || txl == null && AlfrescoTransactionSupport.getTransactionReadState() == TxnReadState.TXN_READ_ONLY;
return rthelper.doInTransaction(rtcallback, readonly, txl != null); return rthelper.doInTransaction(rtcallback, readonly, txl != null);
} catch (RuntimeException re) { } catch (RuntimeException re) {
// attempt to unwrap the exception // attempt to unwrap the exception

View File

@@ -9,6 +9,15 @@ import java.lang.reflect.Method;
import java.lang.reflect.Parameter; import java.lang.reflect.Parameter;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.Temporal;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
@@ -479,7 +488,28 @@ public class MqAsyncService extends AbstractLifecycleBean implements AsyncServic
Class<?> paramType = param.getType(); Class<?> paramType = param.getType();
this.logger.trace("Unmarshaling parameter of type: {}", paramType); this.logger.trace("Unmarshaling parameter of type: {}", paramType);
if (Version.class.isAssignableFrom(paramType)) { if (arg instanceof String || arg instanceof Number || arg instanceof Boolean) {
this.logger.trace("Unmarshaling primitive: {}", arg);
return arg;
} else if (Temporal.class.isAssignableFrom(paramType)) {
if (OffsetDateTime.class.isAssignableFrom(paramType)) {
return OffsetDateTime.from(DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(arg.toString()));
} else if (ZonedDateTime.class.isAssignableFrom(paramType)) {
return ZonedDateTime.from(DateTimeFormatter.ISO_ZONED_DATE_TIME.parse(arg.toString()));
} else if (LocalDate.class.isAssignableFrom(paramType)) {
return LocalDate.from(DateTimeFormatter.ISO_LOCAL_DATE.parse(arg.toString()));
} else if (LocalDateTime.class.isAssignableFrom(paramType)) {
return LocalDateTime.from(DateTimeFormatter.ISO_LOCAL_DATE_TIME.parse(arg.toString()));
} else if (Instant.class.isAssignableFrom(paramType)) {
return Instant.from(DateTimeFormatter.ISO_INSTANT.parse(arg.toString()));
} else if (LocalTime.class.isAssignableFrom(paramType)) {
return LocalTime.from(DateTimeFormatter.ISO_LOCAL_TIME.parse(arg.toString()));
} else if (OffsetTime.class.isAssignableFrom(paramType)) {
return OffsetTime.from(DateTimeFormatter.ISO_OFFSET_TIME.parse(arg.toString()));
} else {
throw new UnsupportedOperationException();
}
} else if (Version.class.isAssignableFrom(paramType)) {
this.logger.trace("Unmarshaling as JSON object: {}", arg); this.logger.trace("Unmarshaling as JSON object: {}", arg);
Map<String, Object> argMap = (Map<String, Object>) this.om.convertValue(arg, Map.class); Map<String, Object> argMap = (Map<String, Object>) this.om.convertValue(arg, Map.class);
@@ -515,13 +545,22 @@ public class MqAsyncService extends AbstractLifecycleBean implements AsyncServic
return cons.invoke(null, arg.toString()); return cons.invoke(null, arg.toString());
} else { } else {
this.logger.trace("Unmarshaling as POJO: {}", arg); this.logger.trace("Unmarshaling as POJO: {}", arg);
try {
Constructor<?> cons = paramType.getConstructor(String.class); Constructor<?> cons = paramType.getConstructor(String.class);
return cons.newInstance(arg.toString()); return cons.newInstance(arg.toString());
} catch (NoSuchMethodException nsme) {
Method method = paramType.getDeclaredMethod("valueOf", String.class);
return method.invoke(null, arg.toString());
}
} }
} }
private Object marshal(Object arg) { private Object marshal(Object arg) {
if (arg instanceof Version) { if (arg instanceof String || arg instanceof Number || arg instanceof Boolean) {
return arg;
} else if (arg instanceof Temporal) {
return arg.toString();
} else if (arg instanceof Version) {
Version version = (Version) arg; Version version = (Version) arg;
Map<String, Object> map = new HashMap<>(); Map<String, Object> map = new HashMap<>();
map.put("nodeRef", version.getFrozenStateNodeRef()); map.put("nodeRef", version.getFrozenStateNodeRef());
@@ -555,7 +594,8 @@ public class MqAsyncService extends AbstractLifecycleBean implements AsyncServic
this.logger.trace("Marshaling Java Map as JSON object: {}", map); this.logger.trace("Marshaling Java Map as JSON object: {}", map);
return this.om.convertValue(map, String.class); return this.om.convertValue(map, String.class);
} else { } else {
return arg; this.logger.trace("Marshaling Java object as JSON object: {}", arg);
return this.om.convertValue(arg, String.class);
} }
} }

View File

@@ -0,0 +1,63 @@
package com.inteligr8.alfresco.annotations.util;
import jakarta.transaction.Transactional;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
public class JakartaTransactionalAnnotationAdapter implements TransactionalAnnotationAdapter {
private final Transactional txl;
public JakartaTransactionalAnnotationAdapter(Transactional txl) {
this.txl = txl;
}
@Override
public boolean isReadOnly() {
return false;
}
@Override
public Propagation getPropagation() {
switch (this.txl.value()) {
case MANDATORY:
return Propagation.MANDATORY;
case REQUIRED:
return Propagation.REQUIRED;
case REQUIRES_NEW:
return Propagation.REQUIRES_NEW;
case SUPPORTS:
return Propagation.SUPPORTS;
case NOT_SUPPORTED:
return Propagation.NOT_SUPPORTED;
case NEVER:
return Propagation.NEVER;
default:
throw new IllegalStateException("This should never happen");
}
}
@Override
public Isolation getIsolation() {
return Isolation.DEFAULT;
}
@Override
public int getTimeoutInSeconds() {
return 0;
}
@SuppressWarnings("unchecked")
@Override
public Class<? extends Throwable>[] getRollbackFor() {
return this.txl.rollbackOn();
}
@SuppressWarnings("unchecked")
@Override
public Class<? extends Throwable>[] getNoRollbackFor() {
return this.txl.dontRollbackOn();
}
}

View File

@@ -0,0 +1,63 @@
package com.inteligr8.alfresco.annotations.util;
import javax.transaction.Transactional;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
public class JtaTransactionalAnnotationAdapter implements TransactionalAnnotationAdapter {
private final Transactional txl;
public JtaTransactionalAnnotationAdapter(Transactional txl) {
this.txl = txl;
}
@Override
public boolean isReadOnly() {
return false;
}
@Override
public Propagation getPropagation() {
switch (this.txl.value()) {
case MANDATORY:
return Propagation.MANDATORY;
case REQUIRED:
return Propagation.REQUIRED;
case REQUIRES_NEW:
return Propagation.REQUIRES_NEW;
case SUPPORTS:
return Propagation.SUPPORTS;
case NOT_SUPPORTED:
return Propagation.NOT_SUPPORTED;
case NEVER:
return Propagation.NEVER;
default:
throw new IllegalStateException("This should never happen");
}
}
@Override
public Isolation getIsolation() {
return Isolation.DEFAULT;
}
@Override
public int getTimeoutInSeconds() {
return 0;
}
@SuppressWarnings("unchecked")
@Override
public Class<? extends Throwable>[] getRollbackFor() {
return this.txl.rollbackOn();
}
@SuppressWarnings("unchecked")
@Override
public Class<? extends Throwable>[] getNoRollbackFor() {
return this.txl.dontRollbackOn();
}
}

View File

@@ -0,0 +1,32 @@
package com.inteligr8.alfresco.annotations.util;
import java.util.HashMap;
import java.util.Map;
import org.alfresco.util.Pair;
public class MapUtils {
public static <K, V> Map<K, V> build(Pair<K, V>... pairs) {
Map<K, V> map = new HashMap<>();
for (Pair<K, V> pair : pairs) {
map.put(pair.getFirst(), pair.getSecond());
}
return map;
}
public static Map<String, String> build(String... keyValuePairs) {
if (keyValuePairs.length % 2 == 1)
throw new IllegalArgumentException();
Map<String, String> map = new HashMap<>();
for (int pair = 0; pair < keyValuePairs.length / 2; pair++) {
int base = pair * 2;
map.put(keyValuePairs[base], keyValuePairs[base + 1]);
}
return map;
}
}

View File

@@ -0,0 +1,45 @@
package com.inteligr8.alfresco.annotations.util;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
public class SpringTransactionalAnnotationAdapter implements TransactionalAnnotationAdapter {
private final Transactional txl;
public SpringTransactionalAnnotationAdapter(Transactional txl) {
this.txl = txl;
}
@Override
public boolean isReadOnly() {
return this.txl.readOnly();
}
@Override
public Propagation getPropagation() {
return this.txl.propagation();
}
@Override
public Isolation getIsolation() {
return this.txl.isolation();
}
@Override
public int getTimeoutInSeconds() {
return this.txl.timeout();
}
@Override
public Class<? extends Throwable>[] getRollbackFor() {
return this.txl.rollbackFor();
}
@Override
public Class<? extends Throwable>[] getNoRollbackFor() {
return this.txl.noRollbackFor();
}
}

View File

@@ -0,0 +1,20 @@
package com.inteligr8.alfresco.annotations.util;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
public interface TransactionalAnnotationAdapter {
boolean isReadOnly();
Propagation getPropagation();
Isolation getIsolation();
int getTimeoutInSeconds();
Class<? extends Throwable>[] getRollbackFor();
Class<? extends Throwable>[] getNoRollbackFor();
}

View File

@@ -6,4 +6,4 @@ module.version=${project.version}
module.repo.version.min=6.0 module.repo.version.min=6.0
#module.repo.version.max= #module.repo.version.max=
module.depends.aspectj-platform-module=1.0-* module.depends.com.inteligr8.alfresco.aspectj-platform-module=1.0-*