initial checkin

This commit is contained in:
Brian Long 2023-04-02 15:38:28 -04:00
commit 85ff365031
37 changed files with 2057 additions and 0 deletions

9
.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
# Maven
target
pom.xml.versionsBackup
# Eclipse
.settings
.project
.classpath

90
pom.xml Normal file
View File

@ -0,0 +1,90 @@
<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>
<groupId>com.inteligr8.alfresco</groupId>
<artifactId>annotations-platform-module</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>Annotations ACS Platform Module</name>
<description>A module to support annotation-based development for Alfresco Content Services modules.</description>
<scm>
<connection>scm:git:https://bitbucket.org/inteligr8/annotations-platform-module.git</connection>
<developerConnection>scm:git:git@bitbucket.org:inteligr8/annotations-platform-module.git</developerConnection>
<url>https://bitbucket.org/inteligr8/annotations-platform-module</url>
</scm>
<organization>
<name>Inteligr8</name>
<url>https://www.inteligr8.com</url>
</organization>
<developers>
<developer>
<id>brian.long</id>
<name>Brian Long</name>
<email>brian@inteligr8.com</email>
<url>https://twitter.com/brianmlong</url>
</developer>
</developers>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<alfresco.sdk.version>4.2.0</alfresco.sdk.version>
<alfresco.platform.version>6.2.0-ga</alfresco.platform.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.alfresco</groupId>
<artifactId>acs-community-packaging</artifactId>
<version>${alfresco.platform.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- Very popular, but not required, dependency -->
<!-- Provided as an example -->
<dependency>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-repository</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>io.repaint.maven</groupId>
<artifactId>tiles-maven-plugin</artifactId>
<version>2.26</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.0.0,2.0.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.0.0,2.0.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.0.0,2.0.0)</tile>
</tiles>
</configuration>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>alfresco-public</id>
<url>https://artifacts.alfresco.com/nexus/content/groups/public</url>
</repository>
</repositories>
</project>

74
rad.ps1 Normal file
View File

@ -0,0 +1,74 @@
function discoverArtifactId {
$script:ARTIFACT_ID=(mvn -q -Dexpression=project"."artifactId -DforceStdout help:evaluate)
}
function rebuild {
echo "Rebuilding project ..."
mvn process-classes
}
function start_ {
echo "Rebuilding project and starting Docker containers to support rapid application development ..."
mvn -Drad process-classes
}
function start_log {
echo "Rebuilding project and starting Docker containers to support rapid application development ..."
mvn -Drad "-Ddocker.showLogs" process-classes
}
function stop_ {
discoverArtifactId
echo "Stopping Docker containers that supported rapid application development ..."
docker container ls --filter name=${ARTIFACT_ID}-*
echo "Stopping containers ..."
docker container stop (docker container ls -q --filter name=${ARTIFACT_ID}-*)
echo "Removing containers ..."
docker container rm (docker container ls -aq --filter name=${ARTIFACT_ID}-*)
}
function tail_logs {
param (
$container
)
discoverArtifactId
docker container logs -f (docker container ls -q --filter name=${ARTIFACT_ID}-${container})
}
function list {
discoverArtifactId
docker container ls --filter name=${ARTIFACT_ID}-*
}
switch ($args[0]) {
"start" {
start_
}
"start_log" {
start_log
}
"stop" {
stop_
}
"restart" {
stop_
start_
}
"rebuild" {
rebuild
}
"tail" {
tail_logs $args[1]
}
"containers" {
list
}
default {
echo "Usage: .\rad.ps1 [ start | start_log | stop | restart | rebuild | tail {container} | containers ]"
}
}
echo "Completed!"

71
rad.sh Normal file
View File

@ -0,0 +1,71 @@
#!/bin/sh
discoverArtifactId() {
ARTIFACT_ID=`mvn -q -Dexpression=project.artifactId -DforceStdout help:evaluate`
}
rebuild() {
echo "Rebuilding project ..."
mvn process-classes
}
start() {
echo "Rebuilding project and starting Docker containers to support rapid application development ..."
mvn -Drad process-classes
}
start_log() {
echo "Rebuilding project and starting Docker containers to support rapid application development ..."
mvn -Drad -Ddocker.showLogs process-classes
}
stop() {
discoverArtifactId
echo "Stopping Docker containers that supported rapid application development ..."
docker container ls --filter name=${ARTIFACT_ID}-*
echo "Stopping containers ..."
docker container stop `docker container ls -q --filter name=${ARTIFACT_ID}-*`
echo "Removing containers ..."
docker container rm `docker container ls -aq --filter name=${ARTIFACT_ID}-*`
}
tail_logs() {
discoverArtifactId
docker container logs -f `docker container ls -q --filter name=${ARTIFACT_ID}-$1`
}
list() {
discoverArtifactId
docker container ls --filter name=${ARTIFACT_ID}-*
}
case "$1" in
start)
start
;;
start_log)
start_log
;;
stop)
stop
;;
restart)
stop
start
;;
rebuild)
rebuild
;;
tail)
tail_logs $2
;;
containers)
list
;;
*)
echo "Usage: ./rad.sh [ start | start_log | stop | restart | rebuild | tail {container} | containers ]"
exit 1
esac
echo "Completed!"

View File

@ -0,0 +1,42 @@
package com.inteligr8.alfresco.annotations;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.alfresco.service.namespace.QNamePattern;
import org.alfresco.service.namespace.RegexQNamePattern;
import org.springframework.beans.factory.BeanNameAware;
public interface AspectConstrainable extends BeanNameAware {
String getBeanName();
NamespaceService getNamespaceService();
default Collection<String> constrainedPrefixedAspects() {
return Collections.emptySet();
}
default Collection<String> constrainedRegexedAspects() {
return Collections.emptySet();
}
default Collection<? extends QNamePattern> constrainedAspects() {
Set<QNamePattern> aspects = new HashSet<>();
Collection<String> prefixedAspects = this.constrainedPrefixedAspects();
for (String prefixedAspect : prefixedAspects)
aspects.add(QName.createQName(prefixedAspect, this.getNamespaceService()));
Collection<String> regexedAspects = this.constrainedRegexedAspects();
for (String regexedAspect : regexedAspects)
aspects.add(new RegexQNamePattern(regexedAspect));
return aspects;
}
}

View File

@ -0,0 +1,11 @@
package com.inteligr8.alfresco.annotations;
import java.util.Collection;
import org.alfresco.service.namespace.QNamePattern;
public interface AssociationTypeConstrainable {
Collection<? extends QNamePattern> constrainedNodeTypes();
}

View File

@ -0,0 +1,12 @@
package com.inteligr8.alfresco.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Asynchronous {
}

View File

@ -0,0 +1,7 @@
package com.inteligr8.alfresco.annotations;
public interface Authorizable {
String authorizeAsUser();
}

View File

@ -0,0 +1,14 @@
package com.inteligr8.alfresco.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Authorized {
String value() default "";
}

View File

@ -0,0 +1,12 @@
package com.inteligr8.alfresco.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AuthorizedAsSystem {
}

View File

@ -0,0 +1,16 @@
package com.inteligr8.alfresco.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface BehaviorBean {
String defaultType() default "sys:base";
String defaultAssocType() default "";
}

View File

@ -0,0 +1,17 @@
package com.inteligr8.alfresco.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({
ElementType.METHOD,
ElementType.PARAMETER
})
public @interface IfAspect {
String aspect() default "";
}

View File

@ -0,0 +1,15 @@
package com.inteligr8.alfresco.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({
ElementType.METHOD,
ElementType.PARAMETER
})
public @interface IfChildAssociationIsPrimary {
}

View File

@ -0,0 +1,15 @@
package com.inteligr8.alfresco.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({
ElementType.METHOD,
ElementType.PARAMETER
})
public @interface IfNodeExists {
}

View File

@ -0,0 +1,17 @@
package com.inteligr8.alfresco.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({
ElementType.METHOD,
ElementType.PARAMETER
})
public @interface IfNodeType {
String type() default "";
}

View File

@ -0,0 +1,42 @@
package com.inteligr8.alfresco.annotations;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.alfresco.service.namespace.QNamePattern;
import org.alfresco.service.namespace.RegexQNamePattern;
import org.springframework.beans.factory.BeanNameAware;
public interface NodeTypeConstrainable extends BeanNameAware {
String getBeanName();
NamespaceService getNamespaceService();
default Collection<String> constrainedPrefixedNodeTypes() {
return Collections.emptySet();
}
default Collection<String> constrainedRegexedNodeTypes() {
return Collections.emptySet();
}
default Collection<? extends QNamePattern> constrainedNodeTypes() {
Set<QNamePattern> nodeTypes = new HashSet<>();
Collection<String> prefixedNodeTypes = this.constrainedPrefixedNodeTypes();
for (String prefixedNodeType : prefixedNodeTypes)
nodeTypes.add(QName.createQName(prefixedNodeType, this.getNamespaceService()));
Collection<String> regexedNodeTypes = this.constrainedRegexedNodeTypes();
for (String regexedNodeType : regexedNodeTypes)
nodeTypes.add(new RegexQNamePattern(regexedNodeType));
return nodeTypes;
}
}

View File

@ -0,0 +1,20 @@
package com.inteligr8.alfresco.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface TransactionalRetryable {
int maxRetries() default -1;
int minRetryWaitInMillis() default -1;
int maxRetryWaitInMillis() default -1;
int incRetryWaitInMillis() default -1;
}

View File

@ -0,0 +1,12 @@
package com.inteligr8.alfresco.annotations.aspect;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import org.aspectj.lang.ProceedingJoinPoint;
public interface ApplicableParameterCallback<T extends Annotation> {
boolean checkParameter(ProceedingJoinPoint joinPoint, Method method, T annotation, int parameterIndex);
}

View File

@ -0,0 +1,145 @@
package com.inteligr8.alfresco.annotations.aspect;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
import org.alfresco.service.cmr.dictionary.ClassDefinition;
import org.alfresco.service.cmr.dictionary.DictionaryService;
import org.alfresco.service.cmr.repository.AssociationRef;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.namespace.QName;
import org.alfresco.service.namespace.QNamePattern;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import com.inteligr8.alfresco.annotations.AspectConstrainable;
import com.inteligr8.alfresco.annotations.IfAspect;
@Aspect
@Component
public class AspectAspect extends QNameBasedAspect<IfAspect> {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private DictionaryService dictionaryService;
@Autowired
private NodeService nodeService;
@Value("${inteligr8.cache.aspectConstrainable.maxBeans}")
private int maxBeans;
@Override
public int getMaxBeansCache() {
return this.maxBeans;
}
@Override
public Class<IfAspect> getAnnotationClass() {
return IfAspect.class;
}
@Pointcut("@annotation(com.inteligr8.alfresco.annotations.IfAspect)")
public void isAspectMethod() {
}
@Around("isAspectMethod() or execution(* *(@com.inteligr8.alfresco.annotations.IfAspect (*), ..))")
public void isAspect(ProceedingJoinPoint joinPoint) throws Throwable {
this.checkParameters(joinPoint, new ApplicableParameterCallback<IfAspect>() {
@Override
public boolean checkParameter(ProceedingJoinPoint joinPoint, Method method, IfAspect annotation, int parameterIndex) {
Object arg = joinPoint.getArgs()[parameterIndex];
Collection<NodeRef> nodeRefs = extractNodeRefs(arg);
if (nodeRefs == null)
return true;
QNameBasedCallback<IfAspect> callback = new QNameBasedCallback<IfAspect>() {
@Override
public boolean isConstrained() {
return joinPoint.getThis() instanceof AspectConstrainable;
}
@Override
public String getConstrainableClassSimpleName() {
return AspectConstrainable.class.getSimpleName();
}
@Override
public String getBeanName() {
return ((AspectConstrainable) joinPoint.getThis()).getBeanName();
}
@Override
public Collection<? extends QNamePattern> constrainedQNames() {
return ((AspectConstrainable) joinPoint.getThis()).constrainedAspects();
}
@Override
public Collection<QName> allPossibleQNames() {
return dictionaryService.getAllAspects();
}
@Override
public void addAllAncestors(Set<QName> qnames, QName qname) {
ClassDefinition aspectDef = dictionaryService.getAspect(qname);
while (aspectDef != null) {
qnames.add(aspectDef.getName());
aspectDef = aspectDef.getParentClassDefinition();
}
}
@Override
public String getAnnotationValue(IfAspect annotation) {
return annotation.aspect();
}
};
Set<QName> aspects = getQNameCache(joinPoint, annotation, callback);
for (NodeRef nodeRef : nodeRefs) {
Set<QName> nodeAspects = nodeService.getAspects(nodeRef);
if (Collections.disjoint(aspects, nodeAspects)) {
logger.debug("The node '{}' aspects {} are not applicable; skipping method: {}", nodeRef, aspects, method);
return false;
}
}
return true;
}
});
}
private Collection<NodeRef> extractNodeRefs(Object obj) {
if (obj instanceof NodeRef) {
NodeRef nodeRef = (NodeRef) obj;
return Collections.singleton(nodeRef);
} else if (obj instanceof ChildAssociationRef) {
ChildAssociationRef childAssocRef = (ChildAssociationRef) obj;
return Collections.singleton(childAssocRef.getChildRef());
} else if (obj instanceof AssociationRef) {
AssociationRef assocRef = (AssociationRef) obj;
return Arrays.asList(assocRef.getSourceRef(), assocRef.getTargetRef());
} else if (obj instanceof Collection<?>) {
Set<NodeRef> nodeRefs = new LinkedHashSet<>();
for (Object o : ((Collection<?>) obj))
nodeRefs.addAll(this.extractNodeRefs(o));
return nodeRefs;
} else {
return null;
}
}
}

View File

@ -0,0 +1,38 @@
package com.inteligr8.alfresco.annotations.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.inteligr8.alfresco.annotations.service.AsyncService;
@Aspect
@Component
public class AsyncAspect {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private AsyncService asyncService;
@Pointcut("@annotation(com.inteligr8.alfresco.annotations.Asynchronous)")
public void asyncMethod() {
}
@Around("asyncMethod()")
public void async(ProceedingJoinPoint joinPoint) throws Throwable {
if (this.asyncService.isCurrentThreadAsynchronous()) {
this.logger.trace("Intercepted an @Async method call while already asynchronous; executing synchronously");
joinPoint.proceed();
} else {
this.logger.trace("Intercepted an @Async method call; redirecting to Async service");
this.asyncService.push(joinPoint);
}
}
}

View File

@ -0,0 +1,103 @@
package com.inteligr8.alfresco.annotations.aspect;
import java.lang.reflect.Method;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import com.inteligr8.alfresco.annotations.Authorized;
import com.inteligr8.alfresco.annotations.AuthorizedAsSystem;
import com.inteligr8.alfresco.annotations.Authorizable;
@Aspect
@Order(Ordered.HIGHEST_PRECEDENCE + Short.MAX_VALUE)
@Component
public class AuthorizedAspect {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Pointcut("@annotation(com.inteligr8.alfresco.annotations.Authorized || com.inteligr8.alfresco.annotations.AuthorizedAsSystem)")
public void runAsableMethod() {
}
@Around("runAsableMethod()")
public void runAs(ProceedingJoinPoint joinPoint) throws Throwable {
String runAsUser = this.getRunAsUser(joinPoint);
String currentRunAsUser = AuthenticationUtil.getRunAsUser();
if (currentRunAsUser != null && currentRunAsUser.equals(runAsUser)) {
this.logger.trace("The current context is already running as the specified user: {}", currentRunAsUser);
joinPoint.proceed();
} else {
this.logger.debug("Changing runAs context: {} => {}", currentRunAsUser, runAsUser);
this.runAs(joinPoint, runAsUser);
}
}
private String getRunAsUser(ProceedingJoinPoint joinPoint) {
if (!(joinPoint.getSignature() instanceof MethodSignature))
throw new IllegalStateException("The @Authorized annotation must be on methods and methods have signatures");
MethodSignature methodSig = (MethodSignature) joinPoint.getSignature();
Method method = methodSig.getMethod();
if (method.getAnnotation(AuthorizedAsSystem.class) != null) {
String runAs = AuthenticationUtil.getSystemUserName();
this.logger.trace("The @AuthorizedAsSystem method {}#{} will run as: {}", joinPoint.getThis().getClass(), method, runAs);
return runAs;
}
if (joinPoint.getThis() instanceof Authorizable) {
String runAs = StringUtils.trimToNull(((Authorizable) joinPoint.getThis()).authorizeAsUser());
this.logger.trace("The @Authorized method {}#{} is Authorizable: {}", joinPoint.getThis().getClass(), method, runAs);
return runAs;
}
Authorized runAsAnnotation = method.getAnnotation(Authorized.class);
String runAs = StringUtils.trimToNull(runAsAnnotation.value());
if (runAs != null) {
this.logger.trace("The @Authorized method {}#{} must run as: {}", joinPoint.getThis().getClass(), method, runAs);
return runAs;
}
this.logger.trace("The @Authorized method {}#{} must run as system", joinPoint.getThis().getClass(), method);
return AuthenticationUtil.getSystemUserName();
}
private void runAs(final ProceedingJoinPoint joinPoint, String runAsUser) throws Throwable {
RunAsWork<Object> work = new RunAsWork<Object>() {
public Object doWork() throws Exception {
try {
return joinPoint.proceed();
} catch (Exception | Error e) {
throw e;
} catch (Throwable t) {
throw new RuntimeException("This should never happen", t);
}
}
};
try {
AuthenticationUtil.runAs(work, runAsUser);
} catch (RuntimeException re) {
// attempt to unwrap the exception
if (re.getMessage() != null && re.getMessage().equals("Error during run as.")) {
throw re.getCause();
} else {
throw re;
}
}
}
}

View File

@ -0,0 +1,52 @@
package com.inteligr8.alfresco.annotations.aspect;
import java.lang.reflect.Method;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import com.inteligr8.alfresco.annotations.IfChildAssociationIsPrimary;
@Aspect
@Order(Ordered.HIGHEST_PRECEDENCE + 100) // ordering before transaction/authorized
@Component
public class ChildIsPrimaryAspect extends MethodOrParameterAspect<IfChildAssociationIsPrimary> {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Override
public Class<IfChildAssociationIsPrimary> getAnnotationClass() {
return IfChildAssociationIsPrimary.class;
}
@Pointcut("@annotation(com.inteligr8.alfresco.annotations.IfChildAssociationIsPrimary)")
public void isChildAssocPrimaryMethod() {
}
@Around("isChildAssocPrimaryMethod() or execution(* *(@com.inteligr8.alfresco.annotations.IfChildAssociationIsPrimary (*), ..))")
public void isChildAssocPrimary(ProceedingJoinPoint joinPoint) throws Throwable {
this.checkParameters(joinPoint, new ApplicableParameterCallback<IfChildAssociationIsPrimary>() {
@Override
public boolean checkParameter(ProceedingJoinPoint joinPoint, Method method, IfChildAssociationIsPrimary annotation, int parameterIndex) {
Object arg = joinPoint.getArgs()[parameterIndex];
if (arg instanceof ChildAssociationRef) {
if (!((ChildAssociationRef)arg).isPrimary()) {
logger.debug("The child association '{}' is not primary; skipping method: {}", arg, method);
return false;
}
}
return true;
}
});
}
}

View File

@ -0,0 +1,37 @@
package com.inteligr8.alfresco.annotations.aspect;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public abstract class MethodOrParameterAspect<T extends Annotation> {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
public abstract Class<T> getAnnotationClass();
public void checkParameters(ProceedingJoinPoint joinPoint, ApplicableParameterCallback<T> callback) throws Throwable {
MethodSignature methodSig = (MethodSignature) joinPoint.getSignature();
Method method = methodSig.getMethod();
T methodAnnotation = method.getAnnotation(this.getAnnotationClass());
for (int p = 0; p < method.getParameterCount(); p++) {
T annotation = method.getParameters()[p].getAnnotation(this.getAnnotationClass());
if (annotation == null)
annotation = methodAnnotation;
if (annotation != null) {
if (!callback.checkParameter(joinPoint, method, annotation, p)) {
this.logger.debug("The parameter '{}' condition is false; skipping method: {}", method.getParameters()[p].getName(), method);
return;
}
}
}
joinPoint.proceed();
}
}

View File

@ -0,0 +1,145 @@
package com.inteligr8.alfresco.annotations.aspect;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
import org.alfresco.service.cmr.dictionary.ClassDefinition;
import org.alfresco.service.cmr.dictionary.DictionaryService;
import org.alfresco.service.cmr.repository.AssociationRef;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.namespace.QName;
import org.alfresco.service.namespace.QNamePattern;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import com.inteligr8.alfresco.annotations.IfNodeType;
import com.inteligr8.alfresco.annotations.NodeTypeConstrainable;
@Aspect
@Component
public class NodeTypeAspect extends QNameBasedAspect<IfNodeType> {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private DictionaryService dictionaryService;
@Autowired
private NodeService nodeService;
@Value("${inteligr8.cache.nodeTypeConstrainable.maxBeans}")
private int maxBeans;
@Override
public int getMaxBeansCache() {
return this.maxBeans;
}
@Override
public Class<IfNodeType> getAnnotationClass() {
return IfNodeType.class;
}
@Pointcut("@annotation(com.inteligr8.alfresco.annotations.IfNodeType)")
public void isNodeTypeMethod() {
}
@Around("isNodeTypeMethod() or execution(* *(@com.inteligr8.alfresco.annotations.IfNodeType (*), ..))")
public void isNodeType(ProceedingJoinPoint joinPoint) throws Throwable {
this.checkParameters(joinPoint, new ApplicableParameterCallback<IfNodeType>() {
@Override
public boolean checkParameter(ProceedingJoinPoint joinPoint, Method method, IfNodeType annotation, int parameterIndex) {
Object arg = joinPoint.getArgs()[parameterIndex];
Collection<NodeRef> nodeRefs = extractNodeRefs(arg);
if (nodeRefs == null)
return true;
QNameBasedCallback<IfNodeType> callback = new QNameBasedCallback<IfNodeType>() {
@Override
public boolean isConstrained() {
return joinPoint.getThis() instanceof NodeTypeConstrainable;
}
@Override
public String getConstrainableClassSimpleName() {
return NodeTypeConstrainable.class.getSimpleName();
}
@Override
public String getBeanName() {
return ((NodeTypeConstrainable) joinPoint.getThis()).getBeanName();
}
@Override
public Collection<? extends QNamePattern> constrainedQNames() {
return ((NodeTypeConstrainable) joinPoint.getThis()).constrainedNodeTypes();
}
@Override
public Collection<QName> allPossibleQNames() {
return dictionaryService.getAllTypes();
}
@Override
public void addAllAncestors(Set<QName> qnames, QName qname) {
ClassDefinition typeDef = dictionaryService.getType(qname);
while (typeDef != null) {
qnames.add(typeDef.getName());
typeDef = typeDef.getParentClassDefinition();
}
}
@Override
public String getAnnotationValue(IfNodeType annotation) {
return annotation.type();
}
};
Set<QName> nodeTypes = getQNameCache(joinPoint, annotation, callback);
for (NodeRef nodeRef : nodeRefs) {
QName nodeType = nodeService.getType(nodeRef);
if (!nodeTypes.contains(nodeType)) {
logger.debug("The node '{}' type '{}' is not applicable; skipping method: {}", nodeRef, nodeType, method);
return false;
}
}
return true;
}
});
}
private Collection<NodeRef> extractNodeRefs(Object obj) {
if (obj instanceof NodeRef) {
NodeRef nodeRef = (NodeRef) obj;
return Collections.singleton(nodeRef);
} else if (obj instanceof ChildAssociationRef) {
ChildAssociationRef childAssocRef = (ChildAssociationRef) obj;
return Collections.singleton(childAssocRef.getChildRef());
} else if (obj instanceof AssociationRef) {
AssociationRef assocRef = (AssociationRef) obj;
return Arrays.asList(assocRef.getSourceRef(), assocRef.getTargetRef());
} else if (obj instanceof Collection<?>) {
Set<NodeRef> nodeRefs = new LinkedHashSet<>();
for (Object o : ((Collection<?>) obj))
nodeRefs.addAll(this.extractNodeRefs(o));
return nodeRefs;
} else {
return null;
}
}
}

View File

@ -0,0 +1,97 @@
package com.inteligr8.alfresco.annotations.aspect;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
import org.alfresco.service.cmr.repository.AssociationRef;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.inteligr8.alfresco.annotations.IfNodeExists;
@Aspect
@Component
public class OperableNodeAspect extends MethodOrParameterAspect<IfNodeExists> {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private NodeService nodeService;
@Override
public Class<IfNodeExists> getAnnotationClass() {
return IfNodeExists.class;
}
@Pointcut("@annotation(com.inteligr8.alfresco.annotations.IfNodeExists)")
public void isNodeOperableMethod() {
}
@Around("isNodeOperableMethod() or execution(* *(@com.inteligr8.alfresco.annotations.IfNodeExists (*), ..))")
public void isNodeOperable(ProceedingJoinPoint joinPoint) throws Throwable {
this.checkParameters(joinPoint, new ApplicableParameterCallback<IfNodeExists>() {
@Override
public boolean checkParameter(ProceedingJoinPoint joinPoint, Method method, IfNodeExists annotation, int parameterIndex) {
Object arg = joinPoint.getArgs()[parameterIndex];
Collection<NodeRef> nodeRefs = extractNodeRefs(arg);
if (nodeRefs == null)
return true;
for (NodeRef nodeRef : nodeRefs) {
if (!isOperableNode(nodeRef)) {
logger.debug("The node '{}' does not exist; skipping method: {}", nodeRef, method);
return false;
}
}
return true;
}
});
}
private Collection<NodeRef> extractNodeRefs(Object obj) {
if (obj instanceof NodeRef) {
NodeRef nodeRef = (NodeRef) obj;
return Collections.singleton(nodeRef);
} else if (obj instanceof ChildAssociationRef) {
ChildAssociationRef childAssocRef = (ChildAssociationRef) obj;
return Collections.singleton(childAssocRef.getChildRef());
} else if (obj instanceof AssociationRef) {
AssociationRef assocRef = (AssociationRef) obj;
return Arrays.asList(assocRef.getSourceRef(), assocRef.getTargetRef());
} else if (obj instanceof Collection<?>) {
Set<NodeRef> nodeRefs = new LinkedHashSet<>();
for (Object o : ((Collection<?>) obj))
nodeRefs.addAll(this.extractNodeRefs(o));
return nodeRefs;
} else {
return null;
}
}
private boolean isOperableNode(NodeRef nodeRef) {
if (!this.nodeService.exists(nodeRef)) {
this.logger.debug("The node '{}' does not exist", nodeRef);
return false;
} else if (this.nodeService.getNodeStatus(nodeRef).isDeleted()) {
this.logger.debug("The node '{}' was already deleted", nodeRef);
return false;
}
return true;
}
}

View File

@ -0,0 +1,99 @@
package com.inteligr8.alfresco.annotations.aspect;
import java.lang.annotation.Annotation;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import javax.annotation.PostConstruct;
import org.alfresco.repo.cache.DefaultSimpleCache;
import org.alfresco.repo.cache.SimpleCache;
import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.alfresco.service.namespace.QNamePattern;
import org.aspectj.lang.ProceedingJoinPoint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
public abstract class QNameBasedAspect<T extends Annotation> extends MethodOrParameterAspect<T> {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private NamespaceService namespaceService;
private SimpleCache<String, Set<QName>> qnameCache;
public abstract int getMaxBeansCache();
@PostConstruct
public void init() {
this.qnameCache = new DefaultSimpleCache<>(this.getMaxBeansCache(), this.getClass().getName());
}
public Set<QName> getQNameCache(ProceedingJoinPoint joinPoint, T annotation, QNameBasedCallback<T> callback) {
if (callback.isConstrained()) {
Set<QName> qnames = this.qnameCache.get(callback.getBeanName());
if (qnames != null) {
this.logger.trace("Using cache of qnames for bean: {}", callback.getBeanName());
return qnames;
}
// caching all types; expensive now; fast at runtime
qnames = new HashSet<>();
for (QNamePattern qnamePattern : callback.constrainedQNames()) {
if (qnamePattern instanceof QName) {
callback.addAllAncestors(qnames, (QName) qnamePattern);
} else {
for (QName qname : callback.allPossibleQNames()) {
if (qnamePattern.isMatch(qname)) {
callback.addAllAncestors(qnames, qname);
}
}
}
}
this.logger.debug("Caching @{} qnames for bean: {}: {}", annotation.getClass().getSimpleName(), callback.getBeanName(), qnames);
this.qnameCache.put(callback.getBeanName(), qnames);
return qnames;
} else if (callback.getAnnotationValue(annotation).length() > 0) {
Set<QName> qnames = this.qnameCache.get(joinPoint.getThis().getClass().getName());
if (qnames != null) {
this.logger.trace("Using cache of qnames for bean: {}", callback.getBeanName());
return qnames;
}
qnames = new HashSet<>();
callback.addAllAncestors(qnames, QName.createQName(callback.getAnnotationValue(annotation), this.namespaceService));
this.logger.debug("Caching @{} node types for singleton: {}: {}", annotation.getClass().getSimpleName(), joinPoint.getThis().getClass(), qnames);
this.qnameCache.put(joinPoint.getThis().getClass().getName(), qnames);
return qnames;
} else {
throw new IllegalStateException("An @" + annotation.getClass().getSimpleName() + " must have a value or the class must implement " + callback.getConstrainableClassSimpleName());
}
}
public interface QNameBasedCallback<T> {
boolean isConstrained();
String getBeanName();
String getConstrainableClassSimpleName();
Collection<? extends QNamePattern> constrainedQNames();
void addAllAncestors(Set<QName> qnames, QName qname);
Collection<QName> allPossibleQNames();
String getAnnotationValue(T annotation);
}
}

View File

@ -0,0 +1,154 @@
package com.inteligr8.alfresco.annotations.aspect;
import java.lang.reflect.Method;
import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
import org.alfresco.repo.transaction.RetryingTransactionHelper;
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.transaction.IllegalTransactionStateException;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import com.inteligr8.alfresco.annotations.TransactionalRetryable;
@Aspect
@Order(Ordered.LOWEST_PRECEDENCE - Short.MAX_VALUE)
@Component
public class RetryingTransactionAspect {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Pointcut("@annotation(org.springframework.transaction.annotation.Transactional)")
public void transactionalMethod() {
}
@Around("transactionalMethod()")
public void retryingTransactional(ProceedingJoinPoint joinPoint) throws Throwable {
this.logger.trace("tx: {}", AlfrescoTransactionSupport.getTransactionId());
Method method = this.getMethod(joinPoint);
Transactional txl = method.getAnnotation(Transactional.class);
if (this.doCreateNewTxContext(txl) || this.isReadStateChange(txl)) {
TransactionalRetryable txtry = method.getAnnotation(TransactionalRetryable.class);
this.logger.debug("Changing TX context: {} => [ro: {}, new: {}]", AlfrescoTransactionSupport.getTransactionReadState(), txl.readOnly(), txl.propagation());
this.execute(joinPoint, txl, txtry);
}
}
private Method getMethod(ProceedingJoinPoint joinPoint) {
if (!(joinPoint.getSignature() instanceof MethodSignature))
throw new IllegalStateException("The @Transactional or @TransactionalRetryable annotations must be on methods");
MethodSignature methodSig = (MethodSignature) joinPoint.getSignature();
return methodSig.getMethod();
}
private boolean isReadStateChange(Transactional txl) {
switch (AlfrescoTransactionSupport.getTransactionReadState()) {
case TXN_NONE:
switch (txl.propagation()) {
case NEVER:
case NOT_SUPPORTED:
case SUPPORTS:
return false;
default:
return true;
}
case TXN_READ_ONLY:
return !txl.readOnly();
case TXN_READ_WRITE:
return txl.readOnly();
default:
throw new IllegalStateException();
}
}
private boolean doCreateNewTxContext(Transactional txl) {
switch (txl.propagation()) {
case NEVER:
switch (AlfrescoTransactionSupport.getTransactionReadState()) {
case TXN_NONE:
return false;
default:
throw new IllegalTransactionStateException("A transaction exists where one is not allowed");
}
case MANDATORY:
switch (AlfrescoTransactionSupport.getTransactionReadState()) {
case TXN_NONE:
throw new IllegalTransactionStateException("A transaction does not exist where one is mandatory");
default:
return false;
}
case SUPPORTS:
return false;
case REQUIRED:
switch (AlfrescoTransactionSupport.getTransactionReadState()) {
case TXN_NONE:
return true;
default:
return false;
}
case REQUIRES_NEW:
return true;
default:
throw new UnsupportedOperationException();
}
}
private void execute(final ProceedingJoinPoint joinPoint, Transactional txl, TransactionalRetryable txtry) throws Throwable {
RetryingTransactionCallback<Object> rtcallback = new RetryingTransactionCallback<Object>() {
@Override
public Object execute() throws Throwable {
logger.trace("tx: {}", AlfrescoTransactionSupport.getTransactionId());
try {
return joinPoint.proceed();
} catch (Exception | Error e) {
throw e;
} catch (Throwable t) {
throw new RuntimeException("This should never happen", t);
}
}
};
RetryingTransactionHelper rthelper = new RetryingTransactionHelper();
if (txtry.maxRetries() > 0)
rthelper.setMaxRetries(txtry.maxRetries());
if (txtry.minRetryWaitInMillis() > 0)
rthelper.setMinRetryWaitMs(txtry.minRetryWaitInMillis());
if (txtry.maxRetryWaitInMillis() > 0)
rthelper.setMaxRetryWaitMs(txtry.maxRetryWaitInMillis());
if (txtry.incRetryWaitInMillis() > 0)
rthelper.setRetryWaitIncrementMs(txtry.incRetryWaitInMillis());
if (txl.timeout() > 0)
rthelper.setMaxExecutionMs(txl.timeout() * 1000L);
try {
rthelper.doInTransaction(rtcallback, txl.readOnly(), txl.propagation().equals(Propagation.REQUIRES_NEW));
} catch (RuntimeException re) {
// attempt to unwrap the exception
if (re.getMessage() == null) {
throw re;
} else if (re.getMessage().startsWith("Exception from transactional callback")) {
throw re.getCause();
} else if (re.getMessage().startsWith("Exception in Transaction")) {
throw re.getCause();
} else {
throw re;
}
}
}
}

View File

@ -0,0 +1,25 @@
package com.inteligr8.alfresco.annotations.job;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import com.inteligr8.alfresco.annotations.service.AsyncProcessException;
import com.inteligr8.alfresco.annotations.service.AsyncService;
public class AsyncJob implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
AsyncService asyncService = (AsyncService) context.getMergedJobDataMap().get("asyncService");
if (asyncService == null)
throw new JobExecutionException("An 'asyncService' object is required in the job map");
try {
asyncService.poll();
} catch (AsyncProcessException ape) {
throw new JobExecutionException(ape, true);
}
}
}

View File

@ -0,0 +1,11 @@
package com.inteligr8.alfresco.annotations.service;
public class AsyncProcessException extends Exception {
private static final long serialVersionUID = 8254359296736253436L;
public AsyncProcessException(String message, Throwable t) {
super(message, t);
}
}

View File

@ -0,0 +1,18 @@
package com.inteligr8.alfresco.annotations.service;
import org.aspectj.lang.ProceedingJoinPoint;
/**
* @author brian@inteligr8.com
*/
public interface AsyncService {
boolean isEnabled();
boolean isCurrentThreadAsynchronous();
void poll() throws AsyncProcessException;
void push(ProceedingJoinPoint joinPoint) throws AsyncProcessException;
}

View File

@ -0,0 +1,538 @@
package com.inteligr8.alfresco.annotations.service.impl;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.UUID;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.jms.Connection;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageConsumer;
import javax.jms.MessageProducer;
import javax.jms.Queue;
import javax.jms.Session;
import javax.transaction.TransactionManager;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.cache.SimpleCache;
import org.alfresco.repo.dictionary.M2Model;
import org.alfresco.repo.version.common.VersionImpl;
import org.alfresco.service.cmr.action.Action;
import org.alfresco.service.cmr.action.ActionService;
import org.alfresco.service.cmr.dictionary.CustomModelService;
import org.alfresco.service.cmr.dictionary.DictionaryService;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentService;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.version.Version;
import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.alfresco.service.transaction.TransactionService;
import org.alfresco.util.Pair;
import org.apache.activemq.ActiveMQXAConnectionFactory;
import org.apache.activemq.jms.pool.XaPooledConnectionFactory;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.impl.JobDetailImpl;
import org.quartz.impl.StdSchedulerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEvent;
import org.springframework.extensions.surf.util.AbstractLifecycleBean;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.inteligr8.alfresco.annotations.AuthorizedAsSystem;
import com.inteligr8.alfresco.annotations.TransactionalRetryable;
import com.inteligr8.alfresco.annotations.job.AsyncJob;
import com.inteligr8.alfresco.annotations.service.AsyncProcessException;
import com.inteligr8.alfresco.annotations.service.AsyncService;
/**
* This class provides integration with MQ for the asynchronous filing of nodes.
*
* @author brian@inteligr8.com
*/
@Component
public class MqAsyncService extends AbstractLifecycleBean implements AsyncService {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private final Pattern typePattern = Pattern.compile("v([0-9]+):([^:#]+)#(.+)");
private final ObjectMapper om = new ObjectMapper();
@Value("${inteligr8.async.mq.enabled}")
protected boolean enabled;
@Value("${inteligr8.async.mq.url:#{null}}")
protected String url;
@Value("${inteligr8.async.mq.username:#{null}}")
protected String username;
@Value("${inteligr8.async.mq.password:#{null}}")
protected String password;
@Value("${inteligr8.async.mq.queue}")
protected String queueName;
@Value("${inteligr8.async.mq.errorQueue}")
protected String errorQueueName;
@Value("${inteligr8.async.mq.clientId}")
protected String clientId;
@Value("${inteligr8.async.mq.pool.max}")
protected short maxConnections;
@Autowired
protected ActionService actionService;
@Autowired
protected ContentService contentService;
@Autowired
protected CustomModelService modelService;
@Autowired
protected DictionaryService dictionaryService;
@Autowired
protected NamespaceService namespaceService;
@Autowired
protected TransactionService txService;
@Autowired
protected TransactionManager txManager;
private XaPooledConnectionFactory factory;
private SimpleCache<Pair<Class<?>, String>, Method> methodCache;
private ThreadLocal<Boolean> isAsync = ThreadLocal.withInitial(new Supplier<Boolean>() {
@Override
public Boolean get() {
return false;
}
});
@Override
protected void onBootstrap(ApplicationEvent event) {
if (!this.enabled)
return;
ActiveMQXAConnectionFactory factory = new ActiveMQXAConnectionFactory(this.url);
XaPooledConnectionFactory pool = new XaPooledConnectionFactory();
pool.setConnectionFactory(factory);
pool.setMaxConnections(this.maxConnections);
pool.setTransactionManager(this.txManager);
pool.start();
this.factory = pool;
JobKey jobKey = new JobKey("behaviour-async", "inteligr8-annotations");
JobDetailImpl jobDetail = new JobDetailImpl();
jobDetail.setKey(jobKey);
jobDetail.setRequestsRecovery(true);
jobDetail.setJobClass(AsyncJob.class);
jobDetail.getJobDataMap().put("asyncService", this);
try {
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
scheduler.addJob(jobDetail, false);
scheduler.triggerJob(jobKey);
} catch (SchedulerException se) {
this.logger.error("The behavior policy async service failed to start; no asynchronous policies will be processed!", se);
}
}
@Override
protected void onShutdown(ApplicationEvent event) {
this.factory.stop();
}
@Override
public boolean isEnabled() {
return enabled;
}
@Override
public boolean isCurrentThreadAsynchronous() {
return this.isAsync.get();
}
@Override
public void poll() throws AsyncProcessException {
this.logger.trace("poll()");
this.isAsync.set(true);
try {
Connection mqcon = this.factory.createConnection(this.username, this.password);
try {
mqcon.setClientID(this.clientId);
Session mqsession = mqcon.createSession(true, Session.CLIENT_ACKNOWLEDGE);
try {
this.logger.debug("Polling messages for asynchronous policy execution");
this.pollErrors(mqsession);
this.pollMain(mqsession);
} finally {
mqsession.close();
}
} finally {
mqcon.close();
}
} catch (JMSException je) {
throw new AsyncProcessException("A JMS messaging issue occurred", je);
}
}
private void pollErrors(Session mqsession) throws JMSException {
this.logger.debug("Polling previously errored messages");
Queue mqqueue = mqsession.createQueue(this.errorQueueName);
Set<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);
MessageConsumer consumer = mqsession.createConsumer(mqqueue);
try {
while (!Thread.currentThread().isInterrupted()) {
this.pollTx(mqsession, consumer, null);
}
} finally {
consumer.close();
}
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
@TransactionalRetryable(maxRetries = 3)
@AuthorizedAsSystem
private Boolean pollTx(Session mqsession, MessageConsumer consumer, Set<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 policy bean could not be found; will try on next restart");
this.logger.error("The bean '{}' could not be found: {}", matcher.group(2), cnfe.getMessage());
if (isErrorQueue)
return false;
this.moveToErrorQueue(mqsession, mqmsg);
} catch (IOException ie) {
this.logger.warn("This should never happen: " + ie.getMessage());
// return to queue and retry indefinitely
return false;
} catch (NoSuchMethodException nsme) {
this.logger.error("A policy enumeration argument could not be constructed; will try on next restart");
this.logger.error("An argument could not be The bean '{}' could not be found: {}", matcher.group(2), nsme.getMessage());
if (isErrorQueue)
return false;
this.moveToErrorQueue(mqsession, mqmsg);
} catch (IllegalAccessException iae) {
this.logger.error("A policy method was not accessible (public); will try on next restart");
this.logger.warn("The bean '{}' method '{}' is not accessible: {}", matcher.group(2), matcher.group(3), iae.getMessage());
if (isErrorQueue)
return false;
this.moveToErrorQueue(mqsession, mqmsg);
} catch (InstantiationException | InvocationTargetException ie) {
this.logger.error("A policy method execution failed; will try on next restart");
this.logger.warn("The bean '{}' method '{}' execution failed: {}", matcher.group(2), matcher.group(3), ie.getMessage());
if (isErrorQueue)
return false;
this.moveToErrorQueue(mqsession, mqmsg);
}
return true;
}
private void moveToErrorQueue(Session mqsession, Message mqmsg) throws JMSException {
Queue mqqueue = mqsession.createQueue(this.errorQueueName);
MessageProducer producer = mqsession.createProducer(mqqueue);
try {
producer.send(mqmsg);
} finally {
producer.close();
}
}
private Method findMethod(Class<?> clazz, String methodName) {
Pair<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");
Class<?> beanType = joinPoint.getThis().getClass();
this.logger.debug("Queuing for bean: {}", beanType);
MethodSignature methodSig = (MethodSignature) joinPoint.getSignature();
Method method = methodSig.getMethod();
this.logger.debug("Queuing for method: {}", method);
this.push(beanType, method.getName(), Arrays.asList(joinPoint.getArgs()));
}
@Transactional
public void push(Object callbackBean, String callbackMethod, List<Object> args) throws AsyncProcessException {
this.logger.trace("push({}, {}, {})", callbackBean, callbackMethod, args);
UUID msgId = UUID.randomUUID();
try {
Connection mqcon = this.factory.createConnection(this.username, this.password);
try {
mqcon.setClientID(this.clientId);
Session mqsession = mqcon.createSession(true, Session.AUTO_ACKNOWLEDGE);
try {
this.logger.trace("Sending policy as message: {} => {}", callbackMethod, msgId);
Queue mqqueue = mqsession.createQueue(this.queueName);
Message mqmsg = mqsession.createMessage();
mqmsg.setJMSMessageID(msgId.toString());
mqmsg.setJMSType("v1:" + callbackBean.getClass() + "#" + callbackMethod);
int i = 0;
for (Object arg : args)
mqmsg.setObjectProperty("arg" + (i++), this.marshal(arg));
MessageProducer producer = mqsession.createProducer(mqqueue);
try {
producer.send(mqmsg);
} finally {
producer.close();
}
this.logger.debug("Sent node as message: {} => {}", callbackMethod, msgId);
mqsession.commit();
} finally {
mqsession.close();
}
} finally {
mqcon.close();
}
} catch (JMSException je) {
throw new AsyncProcessException("A JMS messaging issue occurred", je);
}
}
@SuppressWarnings({ "unchecked" })
private Object unmarshal(Parameter param, Object arg) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Class<?> paramType = param.getType();
this.logger.trace("Unmarshaling parameter of type: {}", paramType);
if (Version.class.isAssignableFrom(paramType)) {
this.logger.trace("Unmarshaling as JSON object: {}", arg);
Map<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);
Constructor<?> cons = paramType.getConstructor(String.class);
return cons.newInstance(arg.toString());
}
}
private Object marshal(Object arg) {
if (arg instanceof Version) {
Version version = (Version) arg;
Map<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 {
return arg;
}
}
private M2Model loadModel(NodeRef nodeRef) throws IOException {
ContentReader creader = this.contentService.getReader(nodeRef, ContentModel.PROP_CONTENT);
InputStream istream = creader.getContentInputStream();
try {
return M2Model.createModel(istream);
} finally {
istream.close();
}
}
}

View File

@ -0,0 +1,10 @@
inteligr8.async.mq.enabled=false
inteligr8.async.mq.url=${messaging.broker.url}
inteligr8.async.mq.username=${messaging.broker.username}
inteligr8.async.mq.password=${messaging.broker.password}
inteligr8.async.mq.queuePrefix=inteligr8.acs.
inteligr8.async.mq.clientId=acs
inteligr8.async.mq.pool.max=5
inteligr8.cache.nodeTypeConstrainable.maxBeans=32

View File

@ -0,0 +1 @@
log4j.logger.com.inteligr8.alfresco.annotations=info

View File

@ -0,0 +1,13 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Use this file for beans to be loaded in whatever order Alfresco/Spring decides -->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">
<!-- Enable Spring annotation scanning for classes in package -->
<context:component-scan base-package="com.inteligr8.alfresco.annotations" />
</beans>

View File

@ -0,0 +1,4 @@
module.id=${project.artifactId}
module.title=${project.name}
module.description=${project.description}
module.version=${project.version}

View File

@ -0,0 +1,8 @@
# Module debugging
log4j.logger.com.inteligr8.alfresco.foldering=trace
# WebScript debugging
log4j.logger.org.springframework.extensions.webscripts.ScriptLogger=debug
# non-WebScript JavaScript execution debugging
log4j.logger.org.alfresco.repo.jscript.ScriptLogger=debug

View File

@ -0,0 +1,63 @@
<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE beans PUBLIC '-//SPRING//DTD BEAN//EN' 'http://www.springframework.org/dtd/spring-beans.dtd'>
<!--
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<beans>
<!--
To support hot reloading of server side Javascript files in Share, we have to turn on development mode.
This setting will tell the Rhinoscript Processor not to compile and cache the JS files.
Cool, we can now change server side JS files and have the changes picked up,
without having to restart or refresh web scripts.
But… Due to a known bug in the Surf framework (ALF-9970) this will break the admin consoles in Share.
Override this bean and disable javascript compilation so that webscripts can be hot reloaded.
We have changed the 'compile' property from true to false.
-->
<bean id="javaScriptProcessor" class="org.alfresco.repo.jscript.RhinoScriptProcessor" init-method="register">
<property name="name">
<value>javascript</value>
</property>
<property name="extension">
<value>js</value>
</property>
<!-- Do not "compile javascript and cache compiled scripts" -->
<property name="compile">
<value>false</value>
</property>
<!-- allow sharing of sealed scopes for performance -->
<!-- disable to give each script it's own new scope which can be extended -->
<property name="shareSealedScopes">
<value>true</value>
</property>
<property name="scriptService">
<ref bean="scriptService"/>
</property>
<!-- Creates ScriptNodes which require the ServiceRegistry -->
<property name="serviceRegistry">
<ref bean="ServiceRegistry"/>
</property>
<property name="storeUrl">
<value>${spaces.store}</value>
</property>
<property name="storePath">
<value>${spaces.company_home.childname}</value>
</property>
</bean>
</beans>