11 Commits

Author SHA1 Message Date
2158c6d8c7 v1.0.5 pom 2024-09-20 14:18:43 -04:00
7bc7e0cc34 Merge branch 'develop' into stable 2024-09-20 14:18:28 -04:00
00036df1a9 v1.0.4 pom 2024-09-16 15:33:24 -04:00
cfcb7fd75a Merge branch 'develop' into stable 2024-09-16 15:28:44 -04:00
ba7609ec06 v1.0.3 pom 2024-09-12 10:21:28 -04:00
ed3e01e9a5 Merge branch 'develop' into stable 2024-09-12 10:21:12 -04:00
9321084092 v1.0.2 2024-08-09 14:17:15 -04:00
b1c92f00d7 Merge branch 'develop' into stable 2024-08-09 14:16:08 -04:00
eaa2269bf9 v1.0.1 pom 2024-08-09 13:45:19 -04:00
e867f0d807 Merge branch 'develop' into stable 2024-08-09 13:45:00 -04:00
85ec7aa307 v1.0.0 pom 2024-08-07 18:32:53 -04:00
95 changed files with 677 additions and 1434 deletions

View File

@@ -1,15 +0,0 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.inteligr8.alfresco</groupId>
<artifactId>annotations-platform-module</artifactId>
<version>1.0-SNAPSHOT</version>
<relativePath>../</relativePath>
</parent>
<artifactId>annotations-core-platform-module</artifactId>
<packaging>jar</packaging>
</project>

View File

@@ -1,30 +0,0 @@
package com.inteligr8.alfresco.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* This annotation tells the framework to only execute the annotated method if
* the ACS authenticated user is authorized.
*
* @see com.inteligr8.alfresco.annotations.Authorizable
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(IfAuthorizeds.class)
public @interface IfAuthorized {
/**
* The authorities (users and/or user groups) to constrain for the
* authorization context. Only one authority needs to match. To
* require multiple authorities, use multiple @IfAuthorized
* annotations.
*
* @return An array of ACS authorities; empty means any authenticated user.
*/
String[] value() default "";
}

View File

@@ -1,14 +0,0 @@
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 IfAuthorizeds {
IfAuthorized[] value();
}

View File

@@ -1,92 +0,0 @@
package com.inteligr8.alfresco.annotations.aspect;
import java.lang.reflect.Method;
import java.util.Set;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.util.collections.CollectionUtils;
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.beans.factory.annotation.Autowired;
import org.springframework.extensions.webscripts.Description.RequiredAuthentication;
import org.springframework.extensions.webscripts.WebScriptException;
import org.springframework.http.HttpStatus;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.server.ResponseStatusException;
import com.inteligr8.alfresco.annotations.IfAuthorized;
import com.inteligr8.alfresco.annotations.context.WebScriptContext;
import net.sf.acegisecurity.GrantedAuthority;
/**
* This aspect implements the IfAuthorized annotation.
*
* @see com.inteligr8.alfresco.annotations.IfAuthorized
*/
@Aspect
public class IfAuthorizedAspect {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired(required = false)
private WebScriptContext wscontext;
@Autowired(required = false)
private WebApplicationContext wacontext;
@Pointcut("@annotation(com.inteligr8.alfresco.annotations.IfAuthorized) && execution(* *(..))")
public void isIfAuthorizedAnnotated() {
}
@Around("isIfAuthorizedAnnotated()")
public Object ifAuthorized(ProceedingJoinPoint joinPoint) throws Throwable {
this.logger.trace("ifAuthorized({})", joinPoint);
if (this.wscontext != null && !RequiredAuthentication.user.equals(this.wscontext.getWebscript().getDescription().getRequiredAuthentication()))
return joinPoint.proceed();
if (!(joinPoint.getSignature() instanceof MethodSignature))
throw new IllegalStateException("The @IfAuthorized annotation must be on methods and methods have signatures");
MethodSignature methodSig = (MethodSignature) joinPoint.getSignature();
Method method = methodSig.getMethod();
IfAuthorized[] ifauths = method.getAnnotationsByType(IfAuthorized.class);
for (IfAuthorized ifauth : ifauths) {
if (ifauth.value() != null && ifauth.value().length > 0) {
if (!this.isAuthorized(ifauth.value()))
return this.unauthorized();
}
}
return joinPoint.proceed();
}
protected boolean isAuthorized(String[] permittedAuthorities) {
Set<String> permittedAuthoritiesSet = CollectionUtils.asSet(permittedAuthorities);
GrantedAuthority[] authenticatedAuthorities = AuthenticationUtil.getFullAuthentication().getAuthorities();
for (GrantedAuthority auth : authenticatedAuthorities) {
if (permittedAuthoritiesSet.contains(auth.getAuthority()))
return true;
}
return false;
}
protected Object unauthorized() {
if (this.wscontext != null) {
throw new WebScriptException(HttpStatus.FORBIDDEN.value(), "The authenticated user is not authorized to use this resource");
} else if (this.wacontext != null) {
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "The authenticated user is not authorized to use this resource");
} else {
return null;
}
}
}

View File

@@ -1,50 +0,0 @@
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.context.annotation.Bean;
import org.springframework.context.annotation.Scope;
import org.springframework.extensions.webscripts.WebScript;
import org.springframework.extensions.webscripts.WebScriptRequest;
import org.springframework.extensions.webscripts.WebScriptResponse;
import org.springframework.web.context.WebApplicationContext;
import com.inteligr8.alfresco.annotations.context.WebScriptContext;
/**
* This aspect captures the WebScript execution context.
*/
@Aspect
public class WebScriptAspect {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private ThreadLocal<WebScriptContext> context = new ThreadLocal<>();
@Pointcut("execution(public void org.springframework.extensions.webscripts.WebScript.execute(..))")
public void isWebScriptExecute() {
}
@Around("isWebScriptExecute()")
public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
this.logger.trace("execute({})", joinPoint);
WebScript ws = (WebScript) joinPoint.getTarget();
WebScriptRequest req = (WebScriptRequest) joinPoint.getArgs()[0];
WebScriptResponse res = (WebScriptResponse) joinPoint.getArgs()[1];
this.context.set(new WebScriptContext(ws, req, res));
return joinPoint.proceed();
}
@Bean
@Scope(WebApplicationContext.SCOPE_REQUEST)
public WebScriptContext getContext() {
return this.context.get();
}
}

View File

@@ -1,31 +0,0 @@
package com.inteligr8.alfresco.annotations.context;
import org.springframework.extensions.webscripts.WebScript;
import org.springframework.extensions.webscripts.WebScriptRequest;
import org.springframework.extensions.webscripts.WebScriptResponse;
public class WebScriptContext {
private final WebScript webscript;
private final WebScriptRequest request;
private final WebScriptResponse response;
public WebScriptContext(WebScript webscript, WebScriptRequest request, WebScriptResponse response) {
this.webscript = webscript;
this.request = request;
this.response = response;
}
public WebScript getWebscript() {
return webscript;
}
public WebScriptRequest getRequest() {
return request;
}
public WebScriptResponse getResponse() {
return response;
}
}

View File

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

View File

@@ -1,222 +0,0 @@
package com.inteligr8.alfresco.annotations;
import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
import org.alfresco.repo.transaction.AlfrescoTransactionSupport.TxnReadState;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationEvent;
import org.springframework.extensions.surf.util.AbstractLifecycleBean;
import org.springframework.transaction.IllegalTransactionStateException;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
public class AbstractTransactionalTest extends AbstractLifecycleBean {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Override
protected void onBootstrap(ApplicationEvent event) {
this.logger.info("Running test: " + this.getClass());
Assert.isNull(AlfrescoTransactionSupport.getTransactionId(), "An unexpected transaction: " + AlfrescoTransactionSupport.getTransactionId());
this.tryOutsideTx();
this.tryWithinTx();
this.tryWithinReadonlyTx();
}
@Override
protected void onShutdown(ApplicationEvent event) {
}
private void tryOutsideTx() {
this.logger.info("Running outside TX test");
this.tryDefaultTransactional(null, false);
this.tryReadOnlyTransactional(null, false);
this.tryRetryOnlyTransactional(null);
this.trySupportsTransactional(null, false);
this.tryRequiresNewTransactional(null, false);
this.tryRequiredTransactional(null, false);
this.tryNeverTransactional(null);
try {
this.tryNoSupportsTransactional();
} catch (IllegalTransactionStateException uoe) {
throw new IllegalStateException();
}
try {
this.tryMandatoryTransactional(null, false);
throw new IllegalStateException();
} catch (IllegalTransactionStateException itse) {
// suppress
}
}
@Transactional
private void tryWithinTx() {
this.logger.info("Running inside read/write TX test");
String txId = AlfrescoTransactionSupport.getTransactionId();
boolean readonly = false;
this.tryDefaultTransactional(txId, readonly);
this.tryReadOnlyTransactional(txId, readonly);
this.tryRetryOnlyTransactional(txId);
this.trySupportsTransactional(txId, readonly);
this.tryRequiresNewTransactional(txId, readonly);
this.tryRequiredTransactional(txId, readonly);
this.tryMandatoryTransactional(txId, readonly);
try {
this.tryNoSupportsTransactional();
throw new IllegalStateException();
} catch (IllegalTransactionStateException uoe) {
// suppress
}
try {
this.tryNeverTransactional(txId);
throw new IllegalStateException();
} catch (IllegalTransactionStateException itse) {
// suppress
}
}
@Transactional(readOnly = true)
private void tryWithinReadonlyTx() {
this.logger.info("Running inside read-only TX test");
String txId = AlfrescoTransactionSupport.getTransactionId();
boolean readonly = true;
this.tryDefaultTransactional(txId, readonly);
this.tryReadOnlyTransactional(txId, readonly);
this.tryRetryOnlyTransactional(txId);
this.trySupportsTransactional(txId, readonly);
this.tryRequiresNewTransactional(txId, readonly);
this.tryRequiredTransactional(txId, readonly);
try {
this.tryNoSupportsTransactional();
throw new IllegalStateException();
} catch (IllegalTransactionStateException uoe) {
// suppress
}
try {
this.tryMandatoryTransactional(txId, readonly);
throw new IllegalStateException();
} catch (IllegalTransactionStateException itse) {
// suppress
}
try {
this.tryNeverTransactional(txId);
throw new IllegalStateException();
} catch (IllegalTransactionStateException itse) {
// suppress
}
}
@Transactional
private void tryDefaultTransactional(String originTxId, boolean originReadonly) {
Assert.hasText(AlfrescoTransactionSupport.getTransactionId(), "Expected a transaction");
Assert.isTrue(TxnReadState.TXN_READ_WRITE.equals(AlfrescoTransactionSupport.getTransactionReadState()), "Expected a read/write transaction");
if (originTxId != null) {
if (originReadonly) {
// changed from readonly to read/write; need new TX
Assert.isTrue(!AlfrescoTransactionSupport.getTransactionId().equals(originTxId), "Expected a different transaction: " + AlfrescoTransactionSupport.getTransactionId() + " == " + originTxId);
} else {
// no changes; same TX
Assert.isTrue(AlfrescoTransactionSupport.getTransactionId().equals(originTxId), "Expected the same transaction: " + AlfrescoTransactionSupport.getTransactionId() + " != " + originTxId);
}
}
}
@Transactional(readOnly = true)
private void tryReadOnlyTransactional(String originTxId, boolean originReadonly) {
Assert.hasText(AlfrescoTransactionSupport.getTransactionId(), "Expected a transaction");
Assert.isTrue(TxnReadState.TXN_READ_ONLY.equals(AlfrescoTransactionSupport.getTransactionReadState()), "Expected a readonly transaction");
if (originTxId != null) {
if (originReadonly) {
// no changes; same TX
Assert.isTrue(AlfrescoTransactionSupport.getTransactionId().equals(originTxId), "Expected the same transaction: " + AlfrescoTransactionSupport.getTransactionId() + " != " + originTxId);
} else {
// changed from read/write to readonly; need new TX
Assert.isTrue(!AlfrescoTransactionSupport.getTransactionId().equals(originTxId), "Expected a different transaction: " + AlfrescoTransactionSupport.getTransactionId() + " == " + originTxId);
}
}
}
@Transactional(propagation = Propagation.SUPPORTS)
private void trySupportsTransactional(String originTxId, boolean originReadonly) {
if (originTxId == null) {
Assert.isNull(AlfrescoTransactionSupport.getTransactionId(), "Unexpected transaction");
} else {
Assert.hasText(AlfrescoTransactionSupport.getTransactionId(), "Expected a transaction");
Assert.isTrue(originReadonly == TxnReadState.TXN_READ_ONLY.equals(AlfrescoTransactionSupport.getTransactionReadState()), "Expected the same read-state transaction");
Assert.isTrue(AlfrescoTransactionSupport.getTransactionId().equals(originTxId), "Expected the same transaction: " + AlfrescoTransactionSupport.getTransactionId() + " != " + originTxId);
}
}
@Transactional(propagation = Propagation.REQUIRED)
private void tryRequiredTransactional(String originTxId, boolean originReadonly) {
Assert.hasText(AlfrescoTransactionSupport.getTransactionId(), "Expected a transaction");
Assert.isTrue(TxnReadState.TXN_READ_WRITE.equals(AlfrescoTransactionSupport.getTransactionReadState()), "Expected a read/write transaction");
if (originTxId != null) {
if (originReadonly) {
// changed from readonly to read/write; need new TX
Assert.isTrue(!AlfrescoTransactionSupport.getTransactionId().equals(originTxId), "Expected a different transaction: " + AlfrescoTransactionSupport.getTransactionId() + " == " + originTxId);
} else {
// no changes; same TX
Assert.isTrue(AlfrescoTransactionSupport.getTransactionId().equals(originTxId), "Expected the same transaction: " + AlfrescoTransactionSupport.getTransactionId() + " != " + originTxId);
}
}
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
private void tryRequiresNewTransactional(String originTxId, boolean originReadonly) {
Assert.hasText(AlfrescoTransactionSupport.getTransactionId(), "Expected a transaction");
Assert.isTrue(TxnReadState.TXN_READ_WRITE.equals(AlfrescoTransactionSupport.getTransactionReadState()), "Expected a read/write transaction");
if (originTxId != null)
Assert.isTrue(!AlfrescoTransactionSupport.getTransactionId().equals(originTxId), "Expected a different transaction: " + AlfrescoTransactionSupport.getTransactionId() + " == " + originTxId);
}
@Transactional(propagation = Propagation.MANDATORY)
private void tryMandatoryTransactional(String originTxId, boolean originReadonly) {
if (originTxId == null) {
throw new IllegalStateException();
} else {
Assert.hasText(AlfrescoTransactionSupport.getTransactionId(), "Expected a transaction");
Assert.isTrue(AlfrescoTransactionSupport.getTransactionId().equals(originTxId), "Expected the same transaction: " + AlfrescoTransactionSupport.getTransactionId() + " != " + originTxId);
Assert.isTrue(originReadonly == TxnReadState.TXN_READ_ONLY.equals(AlfrescoTransactionSupport.getTransactionReadState()), "Expected the same read-state transaction");
}
}
@Transactional(propagation = Propagation.NOT_SUPPORTED)
private void tryNoSupportsTransactional() {
Assert.isNull(AlfrescoTransactionSupport.getTransactionId(), "Expected no transaction");
Assert.isTrue(TxnReadState.TXN_NONE.equals(AlfrescoTransactionSupport.getTransactionReadState()), "Expected not transaction");
}
@Transactional(propagation = Propagation.NEVER)
private void tryNeverTransactional(String originTxId) {
if (originTxId == null) {
Assert.isNull(AlfrescoTransactionSupport.getTransactionId(), "Unexpected transaction");
} else {
throw new IllegalStateException();
}
}
@TransactionalRetryable
private void tryRetryOnlyTransactional(String originTxId) {
if (originTxId == null) {
Assert.isTrue(AlfrescoTransactionSupport.getTransactionId() != null, "Expected a new transaction");
} else {
Assert.isTrue(AlfrescoTransactionSupport.getTransactionId().equals(originTxId), "Expected the same transaction: " + AlfrescoTransactionSupport.getTransactionId() + " != " + originTxId);
}
}
}

View File

@@ -1,60 +0,0 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.inteligr8.alfresco</groupId>
<artifactId>annotations-platform-module</artifactId>
<version>1.0-SNAPSHOT</version>
<relativePath>../</relativePath>
</parent>
<artifactId>annotations-jakarta-platform-module</artifactId>
<packaging>jar</packaging>
<properties>
<alfresco.platform.version>23.2.1</alfresco.platform.version>
<alfresco.platform.war.version>23.2.0.60</alfresco.platform.war.version>
<tomcat-rad.version>10-2.1</tomcat-rad.version>
</properties>
<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>annotations-core-platform-module</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>jakarta.transaction</groupId>
<artifactId>jakarta.transaction-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>io.repaint.maven</groupId>
<artifactId>tiles-maven-plugin</artifactId>
<version>2.40</version>
<extensions>true</extensions>
<configuration>
<tiles>
<!-- Documentation: https://bitbucket.org/inteligr8/ootbee-beedk/src/stable/beedk-acs-platform-self-rad-tile -->
<tile>com.inteligr8.ootbee:beedk-acs-platform-self-rad-tile:[1.1.0,1.2.0)</tile>
<!-- Documentation: https://bitbucket.org/inteligr8/ootbee-beedk/src/stable/beedk-acs-platform-module-tile -->
<tile>com.inteligr8.ootbee:beedk-acs-platform-module-tile:[1.1.0,1.2.0)</tile>
<!-- Documentation: https://bitbucket.org/inteligr8/ootbee-beedk/src/stable/beedk-acs-platform-self-it-tile -->
<tile>com.inteligr8.ootbee:beedk-acs-platform-self-it-tile:[1.1.0,1.2.0)</tile>
</tiles>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -1,40 +0,0 @@
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 jakarta.transaction.Transactional;
/**
* @see jakarta.transaction.Transactional
*/
@Aspect
@DeclarePrecedence("com.inteligr8.alfresco.annotations.aspect.AuthorizedAspect, com.inteligr8.alfresco.annotations.aspect.RetryingTransactionAspect")
public class RetryingTransactionAspect extends AbstractRetryingTransactionAspect {
@Override
public String getJtaInterfaceName() {
return Transactional.class.getName();
}
@Pointcut("@annotation(org.springframework.transaction.annotation.Transactional) && execution(* *(..))")
public void isTransactionalAnnotated() {
}
@Pointcut("@annotation(jakarta.transaction.Transactional) && execution(* *(..))")
public void isJtaTransactionalAnnotated() {
}
@Pointcut("@annotation(com.inteligr8.alfresco.annotations.TransactionalRetryable) && execution(* *(..))")
public void isTransactionalRetryableAnnotated() {
}
@Around("isTransactionalAnnotated() || isJtaTransactionalAnnotated() || isTransactionalRetryableAnnotated()")
public Object retryingTransactional(ProceedingJoinPoint joinPoint) throws Throwable {
return super.retryingTransactional(joinPoint);
}
}

View File

@@ -1,5 +0,0 @@
<aspectj>
<aspects>
<aspect name="com.inteligr8.alfresco.annotations.aspect.RetryingTransactionAspect" />
</aspects>
</aspectj>

Binary file not shown.

View File

@@ -1,54 +0,0 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.inteligr8.alfresco</groupId>
<artifactId>annotations-platform-module</artifactId>
<version>1.0-SNAPSHOT</version>
<relativePath>../</relativePath>
</parent>
<artifactId>annotations-javax-platform-module</artifactId>
<packaging>jar</packaging>
<properties>
<alfresco.platform.war.version>22.22</alfresco.platform.war.version>
</properties>
<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>annotations-core-platform-module</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>javax.transaction</groupId>
<artifactId>javax.transaction-api</artifactId>
<version>1.3</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>io.repaint.maven</groupId>
<artifactId>tiles-maven-plugin</artifactId>
<version>2.40</version>
<extensions>true</extensions>
<configuration>
<tiles>
<!-- Documentation: https://bitbucket.org/inteligr8/ootbee-beedk/src/stable/beedk-acs-platform-self-rad-tile -->
<tile>com.inteligr8.ootbee:beedk-acs-platform-self-rad-tile:[1.1.0,1.2.0)</tile>
<!-- Documentation: https://bitbucket.org/inteligr8/ootbee-beedk/src/stable/beedk-acs-platform-module-tile -->
<tile>com.inteligr8.ootbee:beedk-acs-platform-module-tile:[1.1.0,1.2.0)</tile>
<!-- Documentation: https://bitbucket.org/inteligr8/ootbee-beedk/src/stable/beedk-acs-platform-self-it-tile -->
<tile>com.inteligr8.ootbee:beedk-acs-platform-self-it-tile:[1.1.0,1.2.0)</tile>
</tiles>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -1,74 +0,0 @@
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!"

View File

@@ -1,71 +0,0 @@
#!/bin/sh
discoverArtifactId() {
ARTIFACT_ID=`mvn -q -Dexpression=project.artifactId -DforceStdout help:evaluate | sed 's/\x1B\[[0-9;]\{1,\}[A-Za-z]//g'`
}
rebuild() {
echo "Rebuilding project ..."
mvn process-test-classes
}
start() {
echo "Rebuilding project and starting Docker containers to support rapid application development ..."
mvn -Drad process-test-classes
}
start_log() {
echo "Rebuilding project and starting Docker containers to support rapid application development ..."
mvn -Drad -Ddocker.showLogs process-test-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!"

View File

@@ -1,38 +0,0 @@
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;
/**
* @see javax.transaction.Transactional
*/
@Aspect
@DeclarePrecedence("com.inteligr8.alfresco.annotations.aspect.AuthorizedAspect, com.inteligr8.alfresco.annotations.aspect.RetryingTransactionAspect")
public class RetryingTransactionAspect extends AbstractRetryingTransactionAspect {
@Override
public String getJtaInterfaceName() {
return javax.transaction.Transactional.class.getName();
}
@Pointcut("@annotation(org.springframework.transaction.annotation.Transactional) && execution(* *(..))")
public void isTransactionalAnnotated() {
}
@Pointcut("@annotation(javax.transaction.Transactional) && execution(* *(..))")
public void isJtaTransactionalAnnotated() {
}
@Pointcut("@annotation(com.inteligr8.alfresco.annotations.TransactionalRetryable) && execution(* *(..))")
public void isTransactionalRetryableAnnotated() {
}
@Around("isTransactionalAnnotated() || isJtaTransactionalAnnotated() || isTransactionalRetryableAnnotated()")
public Object retryingTransactional(ProceedingJoinPoint joinPoint) throws Throwable {
return super.retryingTransactional(joinPoint);
}
}

View File

@@ -1,296 +0,0 @@
package com.inteligr8.alfresco.annotations.service.impl;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.regex.Matcher;
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 org.alfresco.service.cmr.repository.NodeRef;
import org.apache.activemq.ActiveMQConnectionFactory;
import org.apache.activemq.jms.pool.PooledConnectionFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import com.inteligr8.alfresco.annotations.AuthorizedAsSystem;
import com.inteligr8.alfresco.annotations.Threaded;
import com.inteligr8.alfresco.annotations.TransactionalRetryable;
import com.inteligr8.alfresco.annotations.service.AsyncProcessException;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
/**
* This class provides integration with MQ for the asynchronous method executions.
*
* @author brian@inteligr8.com
*/
@Component("async.mq")
public class MqAsyncService extends AbstractMqAsyncService {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private PooledConnectionFactory factory;
/**
* @PostConstruct does not work in ACS
*/
@PostConstruct
protected void init() {
if (!this.enabled)
return;
super.init();
ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(this.url);
PooledConnectionFactory pool = new PooledConnectionFactory();
pool.setConnectionFactory(factory);
pool.setMaxConnections(this.maxConnections);
pool.start();
this.factory = pool;
}
@PreDestroy
protected void uninit() {
super.uninit();
if (this.factory != null)
this.factory.stop();
}
@Override
@Transactional
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 + "-service-" + this.hostname);
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<String> 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);
this.pollMainThreaded(mqsession, mqqueue);
}
@Threaded(name = "mq-poll", join = true)
private void pollMainThreaded(Session mqsession, Queue mqqueue) throws JMSException {
MessageConsumer consumer = mqsession.createConsumer(mqqueue);
try {
while (!Thread.currentThread().isInterrupted()) {
pollTx(mqsession, consumer, null);
}
} finally {
consumer.close();
}
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
@TransactionalRetryable(maxRetries = 3)
@AuthorizedAsSystem
private Boolean pollTx(Session mqsession, MessageConsumer consumer, Set<String> 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 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 bean enum 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 bean 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 bean 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();
}
}
@Transactional
public void push(Object callbackBean, String callbackMethod, List<Object> args) throws AsyncProcessException {
this.logger.trace("push({}, {}, {})", callbackBean.getClass(), callbackMethod, args);
UUID msgId = UUID.randomUUID();
try {
Connection mqcon = this.factory.createConnection(this.username, this.password);
try {
mqcon.setClientID(this.clientId + "-client-" + this.hostname);
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);
} finally {
mqsession.close();
}
} finally {
mqcon.close();
}
} catch (JMSException je) {
throw new AsyncProcessException("A JMS messaging issue occurred", je);
}
}
}

View File

@@ -1,5 +0,0 @@
<aspectj>
<aspects>
<aspect name="com.inteligr8.alfresco.annotations.aspect.RetryingTransactionAspect" />
</aspects>
</aspectj>

View File

@@ -1,10 +0,0 @@
module.id=${project.groupId}.${project.artifactId}
module.alias=${project.groupId}.annotations-platform-module
module.title=${project.name}
module.description=${project.description}
module.version=${module.version}
module.repo.version.min=6.0
#module.repo.version.max=
module.depends.com.inteligr8.alfresco.aspectj-platform-module=1.0-*

View File

@@ -1,224 +0,0 @@
package com.inteligr8.alfresco.annotations;
import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
import org.alfresco.repo.transaction.AlfrescoTransactionSupport.TxnReadState;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationEvent;
import org.springframework.extensions.surf.util.AbstractLifecycleBean;
import org.springframework.stereotype.Component;
import org.springframework.transaction.IllegalTransactionStateException;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
@Component
public class TransactionalTest extends AbstractLifecycleBean {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Override
protected void onBootstrap(ApplicationEvent event) {
this.logger.info("Running test: " + this.getClass());
Assert.isNull(AlfrescoTransactionSupport.getTransactionId(), "An unexpected transaction: " + AlfrescoTransactionSupport.getTransactionId());
this.tryOutsideTx();
this.tryWithinTx();
this.tryWithinReadonlyTx();
}
@Override
protected void onShutdown(ApplicationEvent event) {
}
private void tryOutsideTx() {
this.logger.info("Running outside TX test");
this.tryDefaultTransactional(null, false);
this.tryReadOnlyTransactional(null, false);
this.tryRetryOnlyTransactional(null);
this.trySupportsTransactional(null, false);
this.tryRequiresNewTransactional(null, false);
this.tryRequiredTransactional(null, false);
this.tryNeverTransactional(null);
try {
this.tryNoSupportsTransactional();
} catch (IllegalTransactionStateException uoe) {
throw new IllegalStateException();
}
try {
this.tryMandatoryTransactional(null, false);
throw new IllegalStateException();
} catch (IllegalTransactionStateException itse) {
// suppress
}
}
@Transactional
private void tryWithinTx() {
this.logger.info("Running inside read/write TX test");
String txId = AlfrescoTransactionSupport.getTransactionId();
boolean readonly = false;
this.tryDefaultTransactional(txId, readonly);
this.tryReadOnlyTransactional(txId, readonly);
this.tryRetryOnlyTransactional(txId);
this.trySupportsTransactional(txId, readonly);
this.tryRequiresNewTransactional(txId, readonly);
this.tryRequiredTransactional(txId, readonly);
this.tryMandatoryTransactional(txId, readonly);
try {
this.tryNoSupportsTransactional();
throw new IllegalStateException();
} catch (IllegalTransactionStateException uoe) {
// suppress
}
try {
this.tryNeverTransactional(txId);
throw new IllegalStateException();
} catch (IllegalTransactionStateException itse) {
// suppress
}
}
@Transactional(readOnly = true)
private void tryWithinReadonlyTx() {
this.logger.info("Running inside read-only TX test");
String txId = AlfrescoTransactionSupport.getTransactionId();
boolean readonly = true;
this.tryDefaultTransactional(txId, readonly);
this.tryReadOnlyTransactional(txId, readonly);
this.tryRetryOnlyTransactional(txId);
this.trySupportsTransactional(txId, readonly);
this.tryRequiresNewTransactional(txId, readonly);
this.tryRequiredTransactional(txId, readonly);
try {
this.tryNoSupportsTransactional();
throw new IllegalStateException();
} catch (IllegalTransactionStateException uoe) {
// suppress
}
try {
this.tryMandatoryTransactional(txId, readonly);
throw new IllegalStateException();
} catch (IllegalTransactionStateException itse) {
// suppress
}
try {
this.tryNeverTransactional(txId);
throw new IllegalStateException();
} catch (IllegalTransactionStateException itse) {
// suppress
}
}
@Transactional
private void tryDefaultTransactional(String originTxId, boolean originReadonly) {
Assert.hasText(AlfrescoTransactionSupport.getTransactionId(), "Expected a transaction");
Assert.isTrue(TxnReadState.TXN_READ_WRITE.equals(AlfrescoTransactionSupport.getTransactionReadState()), "Expected a read/write transaction");
if (originTxId != null) {
if (originReadonly) {
// changed from readonly to read/write; need new TX
Assert.isTrue(!AlfrescoTransactionSupport.getTransactionId().equals(originTxId), "Expected a different transaction: " + AlfrescoTransactionSupport.getTransactionId() + " == " + originTxId);
} else {
// no changes; same TX
Assert.isTrue(AlfrescoTransactionSupport.getTransactionId().equals(originTxId), "Expected the same transaction: " + AlfrescoTransactionSupport.getTransactionId() + " != " + originTxId);
}
}
}
@Transactional(readOnly = true)
private void tryReadOnlyTransactional(String originTxId, boolean originReadonly) {
Assert.hasText(AlfrescoTransactionSupport.getTransactionId(), "Expected a transaction");
Assert.isTrue(TxnReadState.TXN_READ_ONLY.equals(AlfrescoTransactionSupport.getTransactionReadState()), "Expected a readonly transaction");
if (originTxId != null) {
if (originReadonly) {
// no changes; same TX
Assert.isTrue(AlfrescoTransactionSupport.getTransactionId().equals(originTxId), "Expected the same transaction: " + AlfrescoTransactionSupport.getTransactionId() + " != " + originTxId);
} else {
// changed from read/write to readonly; need new TX
Assert.isTrue(!AlfrescoTransactionSupport.getTransactionId().equals(originTxId), "Expected a different transaction: " + AlfrescoTransactionSupport.getTransactionId() + " == " + originTxId);
}
}
}
@Transactional(propagation = Propagation.SUPPORTS)
private void trySupportsTransactional(String originTxId, boolean originReadonly) {
if (originTxId == null) {
Assert.isNull(AlfrescoTransactionSupport.getTransactionId(), "Unexpected transaction");
} else {
Assert.hasText(AlfrescoTransactionSupport.getTransactionId(), "Expected a transaction");
Assert.isTrue(originReadonly == TxnReadState.TXN_READ_ONLY.equals(AlfrescoTransactionSupport.getTransactionReadState()), "Expected the same read-state transaction");
Assert.isTrue(AlfrescoTransactionSupport.getTransactionId().equals(originTxId), "Expected the same transaction: " + AlfrescoTransactionSupport.getTransactionId() + " != " + originTxId);
}
}
@Transactional(propagation = Propagation.REQUIRED)
private void tryRequiredTransactional(String originTxId, boolean originReadonly) {
Assert.hasText(AlfrescoTransactionSupport.getTransactionId(), "Expected a transaction");
Assert.isTrue(TxnReadState.TXN_READ_WRITE.equals(AlfrescoTransactionSupport.getTransactionReadState()), "Expected a read/write transaction");
if (originTxId != null) {
if (originReadonly) {
// changed from readonly to read/write; need new TX
Assert.isTrue(!AlfrescoTransactionSupport.getTransactionId().equals(originTxId), "Expected a different transaction: " + AlfrescoTransactionSupport.getTransactionId() + " == " + originTxId);
} else {
// no changes; same TX
Assert.isTrue(AlfrescoTransactionSupport.getTransactionId().equals(originTxId), "Expected the same transaction: " + AlfrescoTransactionSupport.getTransactionId() + " != " + originTxId);
}
}
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
private void tryRequiresNewTransactional(String originTxId, boolean originReadonly) {
Assert.hasText(AlfrescoTransactionSupport.getTransactionId(), "Expected a transaction");
Assert.isTrue(TxnReadState.TXN_READ_WRITE.equals(AlfrescoTransactionSupport.getTransactionReadState()), "Expected a read/write transaction");
if (originTxId != null)
Assert.isTrue(!AlfrescoTransactionSupport.getTransactionId().equals(originTxId), "Expected a different transaction: " + AlfrescoTransactionSupport.getTransactionId() + " == " + originTxId);
}
@Transactional(propagation = Propagation.MANDATORY)
private void tryMandatoryTransactional(String originTxId, boolean originReadonly) {
if (originTxId == null) {
throw new IllegalStateException();
} else {
Assert.hasText(AlfrescoTransactionSupport.getTransactionId(), "Expected a transaction");
Assert.isTrue(AlfrescoTransactionSupport.getTransactionId().equals(originTxId), "Expected the same transaction: " + AlfrescoTransactionSupport.getTransactionId() + " != " + originTxId);
Assert.isTrue(originReadonly == TxnReadState.TXN_READ_ONLY.equals(AlfrescoTransactionSupport.getTransactionReadState()), "Expected the same read-state transaction");
}
}
@Transactional(propagation = Propagation.NOT_SUPPORTED)
private void tryNoSupportsTransactional() {
Assert.isNull(AlfrescoTransactionSupport.getTransactionId(), "Expected no transaction");
Assert.isTrue(TxnReadState.TXN_NONE.equals(AlfrescoTransactionSupport.getTransactionReadState()), "Expected not transaction");
}
@Transactional(propagation = Propagation.NEVER)
private void tryNeverTransactional(String originTxId) {
if (originTxId == null) {
Assert.isNull(AlfrescoTransactionSupport.getTransactionId(), "Unexpected transaction");
} else {
throw new IllegalStateException();
}
}
@TransactionalRetryable
private void tryRetryOnlyTransactional(String originTxId) {
if (originTxId == null) {
Assert.isTrue(AlfrescoTransactionSupport.getTransactionId() != null, "Expected a new transaction");
} else {
Assert.isTrue(AlfrescoTransactionSupport.getTransactionId().equals(originTxId), "Expected the same transaction: " + AlfrescoTransactionSupport.getTransactionId() + " != " + originTxId);
}
}
}

90
pom.xml
View File

@@ -5,8 +5,8 @@
<groupId>com.inteligr8.alfresco</groupId> <groupId>com.inteligr8.alfresco</groupId>
<artifactId>annotations-platform-module</artifactId> <artifactId>annotations-platform-module</artifactId>
<version>1.0-SNAPSHOT</version> <version>1.0.5</version>
<packaging>pom</packaging> <packaging>jar</packaging>
<name>Annotations ACS Platform Module</name> <name>Annotations ACS Platform Module</name>
<description>A module to support annotation-based development for Alfresco Content Services modules.</description> <description>A module to support annotation-based development for Alfresco Content Services modules.</description>
@@ -43,8 +43,6 @@
<maven.compiler.target>8</maven.compiler.target> <maven.compiler.target>8</maven.compiler.target>
<alfresco.sdk.version>4.8.0</alfresco.sdk.version> <alfresco.sdk.version>4.8.0</alfresco.sdk.version>
<alfresco.platform.version>7.4.1</alfresco.platform.version>
<alfresco.platform.war.version>22.22</alfresco.platform.war.version>
<aspectj.version>1.9.19</aspectj.version> <aspectj.version>1.9.19</aspectj.version>
<acs-platform.tomcat.opts>-javaagent:/var/lib/tomcat/dev/lib/aspectjweaver-${aspectj.version}.jar</acs-platform.tomcat.opts> <acs-platform.tomcat.opts>-javaagent:/var/lib/tomcat/dev/lib/aspectjweaver-${aspectj.version}.jar</acs-platform.tomcat.opts>
</properties> </properties>
@@ -85,11 +83,48 @@
</dependency> </dependency>
</dependencies> </dependencies>
<modules> <build>
<module>core</module> <plugins>
<module>javax</module> <plugin>
<module>jakarta</module> <groupId>org.codehaus.mojo</groupId>
</modules> <artifactId>build-helper-maven-plugin</artifactId>
<version>3.6.0</version>
<executions>
<execution>
<id>add-source</id>
<goals><goal>add-source</goal></goals>
<configuration>
<sources>
<source>src/main/${source.tx.path}</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<classifier>${classifier.tx.name}</classifier>
</configuration>
</plugin>
<plugin>
<groupId>io.repaint.maven</groupId>
<artifactId>tiles-maven-plugin</artifactId>
<version>2.33</version>
<extensions>true</extensions>
<configuration>
<tiles>
<!-- Documentation: https://bitbucket.org/inteligr8/ootbee-beedk/src/stable/beedk-acs-platform-self-rad-tile -->
<tile>com.inteligr8.ootbee:beedk-acs-platform-self-rad-tile:[1.1.0,1.2.0)</tile>
<!-- Documentation: https://bitbucket.org/inteligr8/ootbee-beedk/src/stable/beedk-acs-platform-module-tile -->
<tile>com.inteligr8.ootbee:beedk-acs-platform-module-tile:[1.1.0,1.2.0)</tile>
<!-- Documentation: https://bitbucket.org/inteligr8/ootbee-beedk/src/stable/beedk-acs-platform-self-it-tile -->
<tile>com.inteligr8.ootbee:beedk-acs-platform-self-it-tile:[1.1.0,1.2.0)</tile>
</tiles>
</configuration>
</plugin>
</plugins>
</build>
<profiles> <profiles>
<profile> <profile>
@@ -98,6 +133,43 @@
<docker.showLogs>true</docker.showLogs> <docker.showLogs>true</docker.showLogs>
</properties> </properties>
</profile> </profile>
<profile>
<id>javax.transaction</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<source.tx.path>javax-tx</source.tx.path>
<classifier.tx.name>javax_tx</classifier.tx.name>
<alfresco.platform.version>7.4.2</alfresco.platform.version>
<alfresco.platform.war.version>22.22</alfresco.platform.war.version>
</properties>
<dependencies>
<dependency>
<groupId>javax.transaction</groupId>
<artifactId>javax.transaction-api</artifactId>
<version>1.3</version>
<scope>provided</scope>
</dependency>
</dependencies>
</profile>
<profile>
<id>jakarta.transaction</id>
<properties>
<source.tx.path>jakarta-tx</source.tx.path>
<classifier.tx.name>jakarta_tx</classifier.tx.name>
<alfresco.platform.version>23.2.1</alfresco.platform.version>
<alfresco.platform.war.version>23.2.0.60</alfresco.platform.war.version>
</properties>
<dependencies>
<dependency>
<groupId>jakarta.transaction</groupId>
<artifactId>jakarta.transaction-api</artifactId>
<version>2.0.1</version>
<scope>provided</scope>
</dependency>
</dependencies>
</profile>
<profile> <profile>
<id>ossrh-release</id> <id>ossrh-release</id>
<properties> <properties>

View File

@@ -97,12 +97,81 @@ import jakarta.jms.Session;
* @author brian@inteligr8.com * @author brian@inteligr8.com
*/ */
@Component("async.mq") @Component("async.mq")
public class MqAsyncService extends AbstractMqAsyncService { public class MqAsyncService extends AbstractLifecycleBean implements AsyncService, InitializingBean, DisposableBean, Threadable {
private final Logger logger = LoggerFactory.getLogger(this.getClass()); private final Logger logger = LoggerFactory.getLogger(this.getClass());
private final JobKey jobKey = new JobKey("mq-async", "inteligr8-annotations");
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.workerThreads}")
protected int workerThreads;
@Value("${inteligr8.async.mq.url}")
protected String url;
@Value("${inteligr8.async.mq.username}")
protected String username;
@Value("${inteligr8.async.mq.password}")
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;
private String hostname;
private JmsPoolConnectionFactory factory; private JmsPoolConnectionFactory factory;
private SimpleCache<Pair<Class<?>, String>, Method> methodCache;
private ThreadLocal<Boolean> isAsync = ThreadLocal.withInitial(new Supplier<Boolean>() {
@Override
public Boolean get() {
return false;
}
});
@Override
public void afterPropertiesSet() throws Exception {
this.init();
}
@Override
public void destroy() throws Exception {
this.uninit();
}
/** /**
* @PostConstruct does not work in ACS * @PostConstruct does not work in ACS
*/ */
@@ -111,7 +180,11 @@ public class MqAsyncService extends AbstractMqAsyncService {
if (!this.enabled) if (!this.enabled)
return; return;
super.init(); try {
this.hostname = InetAddress.getLocalHost().getHostName();
} catch (UnknownHostException uhe) {
this.hostname = "unknown";
}
ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(this.url); ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(this.url);
@@ -121,16 +194,65 @@ public class MqAsyncService extends AbstractMqAsyncService {
pool.start(); pool.start();
this.factory = pool; this.factory = pool;
if (this.workerThreads <= 0)
throw new AlfrescoRuntimeException("The 'inteligr8.async.mq.workerThreads' property must be positive");
} }
@PreDestroy @PreDestroy
protected void uninit() { protected void uninit() {
super.uninit();
if (this.factory != null) if (this.factory != null)
this.factory.stop(); this.factory.stop();
} }
@Override
protected void onBootstrap(ApplicationEvent event) {
if (!this.enabled)
return;
JobDetailImpl jobDetail = new JobDetailImpl();
jobDetail.setKey(this.jobKey);
jobDetail.setRequestsRecovery(true);
jobDetail.setJobClass(AsyncJob.class);
jobDetail.getJobDataMap().put("asyncService", this);
Trigger trigger = TriggerBuilder.newTrigger()
.startNow()
.build();
try {
StdSchedulerFactory.getDefaultScheduler()
.scheduleJob(jobDetail, trigger);
} catch (SchedulerException se) {
this.logger.error("The MQ async service job failed to start; no asynchronous executions will be processed!", se);
}
}
@Override
protected void onShutdown(ApplicationEvent event) {
try {
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
scheduler.deleteJob(this.jobKey);
} catch (SchedulerException se) {
this.logger.warn("The MQ async service job failed to stop", se);
}
}
@Override
public boolean isEnabled() {
return enabled;
}
@Override
public Integer getThreads() {
return this.workerThreads;
}
@Override
public boolean isCurrentThreadAsynchronous() {
return this.isAsync.get();
}
@Override @Override
@Transactional @Transactional
public void poll() throws AsyncProcessException { public void poll() throws AsyncProcessException {
@@ -307,6 +429,43 @@ public class MqAsyncService extends AbstractMqAsyncService {
} }
} }
private Method findMethod(Class<?> clazz, String methodName) {
Pair<Class<?>, 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");
Object bean = joinPoint.getThis();
this.logger.debug("Queuing for bean: {}", bean.getClass());
MethodSignature methodSig = (MethodSignature) joinPoint.getSignature();
Method method = methodSig.getMethod();
this.logger.debug("Queuing for method: {}", method);
this.push(bean, method.getName(), Arrays.asList(joinPoint.getArgs()));
}
@Transactional @Transactional
public void push(Object callbackBean, String callbackMethod, List<Object> args) throws AsyncProcessException { public void push(Object callbackBean, String callbackMethod, List<Object> args) throws AsyncProcessException {
this.logger.trace("push({}, {}, {})", callbackBean.getClass(), callbackMethod, args); this.logger.trace("push({}, {}, {})", callbackBean.getClass(), callbackMethod, args);
@@ -351,4 +510,130 @@ public class MqAsyncService extends AbstractMqAsyncService {
} }
} }
@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 (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);
Map<String, Object> argMap = (Map<String, Object>) this.om.convertValue(arg, Map.class);
Map<String, Serializable> versionPropertiesMap = (Map<String, Serializable>) 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<String, Object> argMap = (Map<String, Object>) 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);
try {
Constructor<?> cons = paramType.getConstructor(String.class);
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) {
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;
Map<String, Object> 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<String, Object> 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<Object> 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<Object, Object> 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 {
this.logger.trace("Marshaling Java object as JSON object: {}", arg);
return this.om.convertValue(arg, String.class);
}
}
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();
}
}
} }

View File

@@ -1,21 +1,21 @@
package com.inteligr8.alfresco.annotations.util; package com.inteligr8.alfresco.annotations.util;
import jakarta.transaction.Transactional;
import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Propagation;
import jakarta.transaction.Transactional; public class JtaTransactionalAnnotationAdapter implements TransactionalAnnotationAdapter {
public class JakartaTransactionalAnnotationAdapter implements JtaTransactionalAnnotationAdapter {
public static final String JTA_INTERFACE_NAME = "jakarta.transaction.Transactional"; public static final String JTA_INTERFACE_NAME = "jakarta.transaction.Transactional";
private final Transactional txl; private final Transactional txl;
public static JakartaTransactionalAnnotationAdapter cast(Object obj) { public static JtaTransactionalAnnotationAdapter cast(Object obj) {
return new JakartaTransactionalAnnotationAdapter((Transactional) obj); return new JtaTransactionalAnnotationAdapter((Transactional) obj);
} }
public JakartaTransactionalAnnotationAdapter(Transactional txl) { public JtaTransactionalAnnotationAdapter(Transactional txl) {
this.txl = txl; this.txl = txl;
} }

View File

@@ -9,11 +9,14 @@ import org.alfresco.repo.transaction.RetryingTransactionHelper;
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
import org.alfresco.service.transaction.TransactionService; import org.alfresco.service.transaction.TransactionService;
import org.aspectj.lang.ProceedingJoinPoint; 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.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.transaction.IllegalTransactionStateException; import org.springframework.transaction.IllegalTransactionStateException;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -38,36 +41,28 @@ import com.inteligr8.alfresco.annotations.util.TransactionalAnnotationAdapter;
* @see org.springframework.transaction.annotation.Transactional * @see org.springframework.transaction.annotation.Transactional
* @see com.inteligr8.alfresco.annotations.TransactionalRetryable * @see com.inteligr8.alfresco.annotations.TransactionalRetryable
*/ */
public abstract class AbstractRetryingTransactionAspect { @Aspect
@DeclarePrecedence("com.inteligr8.alfresco.annotations.aspect.AuthorizedAspect, com.inteligr8.alfresco.annotations.aspect.RetryingTransactionAspect")
public class RetryingTransactionAspect {
private final Logger logger = LoggerFactory.getLogger(this.getClass()); private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private ApplicationContext context;
@Autowired @Autowired
private TransactionService txService; private TransactionService txService;
public abstract String getJtaInterfaceName(); @Pointcut("@annotation(org.springframework.transaction.annotation.Transactional) && execution(* *(..))")
public void isTransactionalAnnotated() {
}
/** @Pointcut("@annotation(" + JtaTransactionalAnnotationAdapter.JTA_INTERFACE_NAME + ") && execution(* *(..))")
* A @Pointcunt annotation is not recognized in super classes. public void isJtaTransactionalAnnotated() {
*/ }
//@Pointcut("@annotation(org.springframework.transaction.annotation.Transactional) && execution(* *(..))")
public abstract void isTransactionalAnnotated();
public abstract void isJtaTransactionalAnnotated(); @Pointcut("@annotation(com.inteligr8.alfresco.annotations.TransactionalRetryable) && execution(* *(..))")
public void isTransactionalRetryableAnnotated() {
}
/** @Around("isTransactionalAnnotated() || isJtaTransactionalAnnotated() || isTransactionalRetryableAnnotated()")
* A @Pointcunt annotation is not recognized in super classes.
*/
//@Pointcut("@annotation(com.inteligr8.alfresco.annotations.TransactionalRetryable) && execution(* *(..))")
public abstract void isTransactionalRetryableAnnotated();
/**
* An @Around annotation is not recognized in super classes.
*/
//@Around("isTransactionalAnnotated() || isJtaTransactionalAnnotated() || 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);
@@ -91,9 +86,9 @@ public abstract class AbstractRetryingTransactionAspect {
if (txl != null) if (txl != null)
return new SpringTransactionalAnnotationAdapter((Transactional) txl); return new SpringTransactionalAnnotationAdapter((Transactional) txl);
txl = this.getOptionalAnnotation(method, this.getJtaInterfaceName()); txl = this.getOptionalAnnotation(method, JtaTransactionalAnnotationAdapter.JTA_INTERFACE_NAME);
if (txl != null) if (txl != null)
return this.context.getAutowireCapableBeanFactory().getBean(JtaTransactionalAnnotationAdapter.class, txl); return JtaTransactionalAnnotationAdapter.cast(txl);
return null; return null;
} }

View File

@@ -1,14 +1,9 @@
package com.inteligr8.alfresco.annotations.util; package com.inteligr8.alfresco.annotations.util;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@Component
@Scope(scopeName = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class SpringTransactionalAnnotationAdapter implements TransactionalAnnotationAdapter { public class SpringTransactionalAnnotationAdapter implements TransactionalAnnotationAdapter {
private final Transactional txl; private final Transactional txl;

View File

@@ -22,12 +22,24 @@ import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.Set;
import java.util.UUID;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern; 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 org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.model.ContentModel; import org.alfresco.model.ContentModel;
import org.alfresco.repo.cache.SimpleCache; import org.alfresco.repo.cache.SimpleCache;
@@ -45,6 +57,8 @@ import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName; import org.alfresco.service.namespace.QName;
import org.alfresco.service.transaction.TransactionService; import org.alfresco.service.transaction.TransactionService;
import org.alfresco.util.Pair; import org.alfresco.util.Pair;
import org.apache.activemq.ActiveMQConnectionFactory;
import org.apache.activemq.jms.pool.PooledConnectionFactory;
import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature; import org.aspectj.lang.reflect.MethodSignature;
import org.quartz.JobKey; import org.quartz.JobKey;
@@ -62,8 +76,15 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationEvent;
import org.springframework.extensions.surf.util.AbstractLifecycleBean; 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.fasterxml.jackson.databind.ObjectMapper;
import com.inteligr8.alfresco.annotations.AuthorizedAsSystem;
import com.inteligr8.alfresco.annotations.Threadable; import com.inteligr8.alfresco.annotations.Threadable;
import com.inteligr8.alfresco.annotations.Threaded;
import com.inteligr8.alfresco.annotations.TransactionalRetryable;
import com.inteligr8.alfresco.annotations.job.AsyncJob; import com.inteligr8.alfresco.annotations.job.AsyncJob;
import com.inteligr8.alfresco.annotations.service.AsyncProcessException; import com.inteligr8.alfresco.annotations.service.AsyncProcessException;
import com.inteligr8.alfresco.annotations.service.AsyncService; import com.inteligr8.alfresco.annotations.service.AsyncService;
@@ -72,14 +93,17 @@ import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy; import jakarta.annotation.PreDestroy;
/** /**
* This class provides integration with MQ for the asynchronous method executions.
*
* @author brian@inteligr8.com * @author brian@inteligr8.com
*/ */
public abstract class AbstractMqAsyncService extends AbstractLifecycleBean implements AsyncService, InitializingBean, DisposableBean, Threadable { @Component("async.mq")
public class MqAsyncService extends AbstractLifecycleBean implements AsyncService, InitializingBean, DisposableBean, Threadable {
private final Logger logger = LoggerFactory.getLogger(this.getClass()); private final Logger logger = LoggerFactory.getLogger(this.getClass());
private final JobKey jobKey = new JobKey("mq-async", "inteligr8-annotations"); private final JobKey jobKey = new JobKey("mq-async", "inteligr8-annotations");
protected final Pattern typePattern = Pattern.compile("v([0-9]+):([^:#]+)#(.+)"); private final Pattern typePattern = Pattern.compile("v([0-9]+):([^:#]+)#(.+)");
protected final ObjectMapper om = new ObjectMapper(); private final ObjectMapper om = new ObjectMapper();
@Value("${inteligr8.async.mq.enabled}") @Value("${inteligr8.async.mq.enabled}")
protected boolean enabled; protected boolean enabled;
@@ -126,11 +150,13 @@ public abstract class AbstractMqAsyncService extends AbstractLifecycleBean imple
@Autowired @Autowired
protected TransactionService txService; protected TransactionService txService;
protected String hostname; private String hostname;
protected SimpleCache<Pair<Class<?>, String>, Method> methodCache; private PooledConnectionFactory factory;
protected ThreadLocal<Boolean> isAsync = ThreadLocal.withInitial(new Supplier<Boolean>() { private SimpleCache<Pair<Class<?>, String>, Method> methodCache;
private ThreadLocal<Boolean> isAsync = ThreadLocal.withInitial(new Supplier<Boolean>() {
@Override @Override
public Boolean get() { public Boolean get() {
return false; return false;
@@ -138,12 +164,12 @@ public abstract class AbstractMqAsyncService extends AbstractLifecycleBean imple
}); });
@Override @Override
public final void afterPropertiesSet() throws Exception { public void afterPropertiesSet() throws Exception {
this.init(); this.init();
} }
@Override @Override
public final void destroy() throws Exception { public void destroy() throws Exception {
this.uninit(); this.uninit();
} }
@@ -161,12 +187,23 @@ public abstract class AbstractMqAsyncService extends AbstractLifecycleBean imple
this.hostname = "unknown"; this.hostname = "unknown";
} }
ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(this.url);
PooledConnectionFactory pool = new PooledConnectionFactory();
pool.setConnectionFactory(factory);
pool.setMaxConnections(this.maxConnections);
pool.start();
this.factory = pool;
if (this.workerThreads <= 0) if (this.workerThreads <= 0)
throw new AlfrescoRuntimeException("The 'inteligr8.async.mq.workerThreads' property must be positive"); throw new AlfrescoRuntimeException("The 'inteligr8.async.mq.workerThreads' property must be positive");
} }
@PreDestroy @PreDestroy
protected void uninit() { protected void uninit() {
if (this.factory != null)
this.factory.stop();
} }
@Override @Override
@@ -204,7 +241,7 @@ public abstract class AbstractMqAsyncService extends AbstractLifecycleBean imple
@Override @Override
public boolean isEnabled() { public boolean isEnabled() {
return this.enabled; return enabled;
} }
@Override @Override
@@ -217,7 +254,183 @@ public abstract class AbstractMqAsyncService extends AbstractLifecycleBean imple
return this.isAsync.get(); return this.isAsync.get();
} }
protected Method findMethod(Class<?> clazz, String methodName) { @Override
@Transactional
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 + "-service-" + this.hostname);
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<String> 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);
this.pollMainThreaded(mqsession, mqqueue);
}
@Threaded(name = "mq-poll", join = true)
private void pollMainThreaded(Session mqsession, Queue mqqueue) throws JMSException {
MessageConsumer consumer = mqsession.createConsumer(mqqueue);
try {
while (!Thread.currentThread().isInterrupted()) {
pollTx(mqsession, consumer, null);
}
} finally {
consumer.close();
}
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
@TransactionalRetryable(maxRetries = 3)
@AuthorizedAsSystem
private Boolean pollTx(Session mqsession, MessageConsumer consumer, Set<String> 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 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 bean enum 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 bean 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 bean 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<Class<?>, String> key = new Pair<>(clazz, methodName); Pair<Class<?>, String> key = new Pair<>(clazz, methodName);
Method method = this.methodCache.get(key); Method method = this.methodCache.get(key);
if (method != null) { if (method != null) {
@@ -254,10 +467,52 @@ public abstract class AbstractMqAsyncService extends AbstractLifecycleBean imple
this.push(bean, method.getName(), Arrays.asList(joinPoint.getArgs())); this.push(bean, method.getName(), Arrays.asList(joinPoint.getArgs()));
} }
public abstract void push(Object callbackBean, String callbackMethod, List<Object> args) throws AsyncProcessException; @Transactional
public void push(Object callbackBean, String callbackMethod, List<Object> args) throws AsyncProcessException {
this.logger.trace("push({}, {}, {})", callbackBean.getClass(), callbackMethod, args);
UUID msgId = UUID.randomUUID();
try {
Connection mqcon = this.factory.createConnection(this.username, this.password);
try {
mqcon.setClientID(this.clientId + "-client-" + this.hostname);
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);
} finally {
mqsession.close();
}
} finally {
mqcon.close();
}
} catch (JMSException je) {
throw new AsyncProcessException("A JMS messaging issue occurred", je);
}
}
@SuppressWarnings({ "unchecked" }) @SuppressWarnings({ "unchecked" })
protected Object unmarshal(Parameter param, Object arg) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { private Object unmarshal(Parameter param, Object arg) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Class<?> paramType = param.getType(); Class<?> paramType = param.getType();
this.logger.trace("Unmarshaling parameter of type: {}", paramType); this.logger.trace("Unmarshaling parameter of type: {}", paramType);
@@ -328,7 +583,7 @@ public abstract class AbstractMqAsyncService extends AbstractLifecycleBean imple
} }
} }
protected Object marshal(Object arg) { private Object marshal(Object arg) {
if (arg instanceof String || arg instanceof Number || arg instanceof Boolean) { if (arg instanceof String || arg instanceof Number || arg instanceof Boolean) {
return arg; return arg;
} else if (arg instanceof Temporal) { } else if (arg instanceof Temporal) {
@@ -372,7 +627,7 @@ public abstract class AbstractMqAsyncService extends AbstractLifecycleBean imple
} }
} }
protected M2Model loadModel(NodeRef nodeRef) throws IOException { private M2Model loadModel(NodeRef nodeRef) throws IOException {
ContentReader creader = this.contentService.getReader(nodeRef, ContentModel.PROP_CONTENT); ContentReader creader = this.contentService.getReader(nodeRef, ContentModel.PROP_CONTENT);
InputStream istream = creader.getContentInputStream(); InputStream istream = creader.getContentInputStream();
try { try {

View File

@@ -5,17 +5,17 @@ import javax.transaction.Transactional;
import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Propagation;
public class JavaxTransactionalAnnotationAdapter implements JtaTransactionalAnnotationAdapter { public class JtaTransactionalAnnotationAdapter implements TransactionalAnnotationAdapter {
public static final String JTA_INTERFACE_NAME = "javax.transaction.Transactional"; public static final String JTA_INTERFACE_NAME = "javax.transaction.Transactional";
private final Transactional txl; private final Transactional txl;
public static JavaxTransactionalAnnotationAdapter cast(Object obj) { public static JtaTransactionalAnnotationAdapter cast(Object obj) {
return new JavaxTransactionalAnnotationAdapter((Transactional) obj); return new JtaTransactionalAnnotationAdapter((Transactional) obj);
} }
public JavaxTransactionalAnnotationAdapter(Transactional txl) { public JtaTransactionalAnnotationAdapter(Transactional txl) {
this.txl = txl; this.txl = txl;
} }

View File

@@ -6,7 +6,6 @@
<aspect name="com.inteligr8.alfresco.annotations.aspect.ThreadedAspect" /> <aspect name="com.inteligr8.alfresco.annotations.aspect.ThreadedAspect" />
<aspect name="com.inteligr8.alfresco.annotations.aspect.AsyncAspect" /> <aspect name="com.inteligr8.alfresco.annotations.aspect.AsyncAspect" />
<aspect name="com.inteligr8.alfresco.annotations.aspect.AuthorizedAspect" /> <aspect name="com.inteligr8.alfresco.annotations.aspect.AuthorizedAspect" />
<aspect name="com.inteligr8.alfresco.annotations.aspect.IfAuthorizedAspect" />
<aspect name="com.inteligr8.alfresco.annotations.aspect.JobLockAspect" /> <aspect name="com.inteligr8.alfresco.annotations.aspect.JobLockAspect" />
<aspect name="com.inteligr8.alfresco.annotations.aspect.OperableNodeAspect" /> <aspect name="com.inteligr8.alfresco.annotations.aspect.OperableNodeAspect" />
@@ -14,6 +13,6 @@
<aspect name="com.inteligr8.alfresco.annotations.aspect.NodeAspectAspect" /> <aspect name="com.inteligr8.alfresco.annotations.aspect.NodeAspectAspect" />
<aspect name="com.inteligr8.alfresco.annotations.aspect.ChildIsPrimaryAspect" /> <aspect name="com.inteligr8.alfresco.annotations.aspect.ChildIsPrimaryAspect" />
<aspect name="com.inteligr8.alfresco.annotations.aspect.WebScriptAspect" /> <aspect name="com.inteligr8.alfresco.annotations.aspect.RetryingTransactionAspect" />
</aspects> </aspects>
</aspectj> </aspectj>

View File

@@ -1,8 +1,7 @@
module.id=${project.groupId}.${project.artifactId} module.id=${project.groupId}.${project.artifactId}
module.alias=${project.groupId}.annotations-platform-module
module.title=${project.name} module.title=${project.name}
module.description=${project.description} module.description=${project.description}
module.version=${module.version} module.version=${project.version}
module.repo.version.min=6.0 module.repo.version.min=6.0
#module.repo.version.max= #module.repo.version.max=