/*
* Copyright 2010 the original author or authors
*
* Licensed 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.
*/
package org.activiti.spring.components.scope;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Logger;
import org.activiti.engine.ProcessEngine;
import org.activiti.engine.RuntimeService;
import org.activiti.engine.impl.context.Context;
import org.activiti.engine.impl.persistence.entity.ExecutionEntity;
import org.activiti.engine.runtime.ProcessInstance;
import org.activiti.spring.components.aop.util.Scopifier;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.scope.ScopedObject;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.Scope;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/**
* binds variables to a currently executing Activiti business process (a {@link org.activiti.engine.runtime.ProcessInstance}).
* <p/>
* Parts of this code are lifted wholesale from Dave Syer's work on the Spring 3.1 RefreshScope.
*
* @author Josh Long
* @since 5.3
*/
public class ProcessScope implements Scope, InitializingBean, BeanFactoryPostProcessor, DisposableBean {
/**
* Map of the processVariables. Supports correct, scoped access to process variables so that
* <code>
*
* @Value("#{ processVariables['customerId'] }") long customerId;
* </code>
* <p/>
* works in any bean - scoped or not
*/
public final static String PROCESS_SCOPE_PROCESS_VARIABLES_SINGLETON = "processVariables";
public final static String PROCESS_SCOPE_NAME = "process";
private ClassLoader classLoader = ClassUtils.getDefaultClassLoader();
private boolean proxyTargetClass = true;
private Logger logger = Logger.getLogger(getClass().getName());
private ProcessEngine processEngine;
private RuntimeService runtimeService;
// set through Namespace reflection if nothing else
@SuppressWarnings("unused")
public void setProcessEngine(ProcessEngine processEngine) {
this.processEngine = processEngine;
}
public Object get(String name, ObjectFactory<?> objectFactory) {
ExecutionEntity executionEntity = null;
try {
logger.fine("returning scoped object having beanName '" + name + "' for conversation ID '" + this.getConversationId() + "'. ");
ProcessInstance processInstance = Context.getExecutionContext().getProcessInstance();
executionEntity = (ExecutionEntity) processInstance;
Object scopedObject = executionEntity.getVariable(name);
if (scopedObject == null) {
scopedObject = objectFactory.getObject();
if (scopedObject instanceof ScopedObject) {
ScopedObject sc = (ScopedObject) scopedObject;
scopedObject = sc.getTargetObject();
logger.fine("de-referencing " + ScopedObject.class.getName() + "#targetObject before persisting variable");
}
persistVariable(name, scopedObject);
}
return createDirtyCheckingProxy(name, scopedObject);
} catch (Throwable th) {
logger.warning("couldn't return value from process scope! " + ExceptionUtils.getFullStackTrace(th));
} finally {
if (executionEntity != null) {
logger.fine("set variable '" + name + "' on executionEntity# " + executionEntity.getId());
}
}
return null;
}
public void registerDestructionCallback(String name, Runnable callback) {
logger.fine("no support for registering descruction callbacks implemented currently. registerDestructionCallback('" + name + "',callback) will do nothing.");
}
private String getExecutionId() {
return Context.getExecutionContext().getExecution().getId();
}
public Object remove(String name) {
logger.fine("remove '" + name + "'");
return runtimeService.getVariable(getExecutionId(), name);
}
public Object resolveContextualObject(String key) {
if ("executionId".equalsIgnoreCase(key))
return Context.getExecutionContext().getExecution().getId();
if ("processInstance".equalsIgnoreCase(key))
return Context.getExecutionContext().getProcessInstance();
if ("processInstanceId".equalsIgnoreCase(key))
return Context.getExecutionContext().getProcessInstance().getId();
return null;
}
/**
* creates a proxy that dispatches invocations to the currently bound {@link ProcessInstance}
*
* @return shareable {@link ProcessInstance}
*/
private Object createSharedProcessInstance() {
ProxyFactory proxyFactoryBean = new ProxyFactory(ProcessInstance.class, new MethodInterceptor() {
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
String methodName = methodInvocation.getMethod().getName() ;
logger.info("method invocation for " + methodName+ ".");
if(methodName.equals("toString"))
return "SharedProcessInstance";
ProcessInstance processInstance = Context.getExecutionContext().getProcessInstance();
Method method = methodInvocation.getMethod();
Object[] args = methodInvocation.getArguments();
Object result = method.invoke(processInstance, args);
return result;
}
});
return proxyFactoryBean.getProxy(this.classLoader);
}
public String getConversationId() {
return getExecutionId();
}
private final ConcurrentHashMap<String, Object> processVariablesMap = new ConcurrentHashMap<String, Object>() {
@Override
public java.lang.Object get(java.lang.Object o) {
Assert.isInstanceOf(String.class, o, "the 'key' must be a String");
String varName = (String) o;
ProcessInstance processInstance = Context.getExecutionContext().getProcessInstance();
ExecutionEntity executionEntity = (ExecutionEntity) processInstance;
if (executionEntity.getVariableNames().contains(varName)) {
return executionEntity.getVariable(varName);
}
throw new RuntimeException("no processVariable by the name of '" + varName + "' is available!");
}
};
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
beanFactory.registerScope(ProcessScope.PROCESS_SCOPE_NAME, this);
Assert.isInstanceOf(BeanDefinitionRegistry.class, beanFactory, "BeanFactory was not a BeanDefinitionRegistry, so ProcessScope cannot be used.");
BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory;
for (String beanName : beanFactory.getBeanDefinitionNames()) {
BeanDefinition definition = beanFactory.getBeanDefinition(beanName);
// Replace this or any of its inner beans with scoped proxy if it has this scope
boolean scoped = PROCESS_SCOPE_NAME.equals(definition.getScope());
Scopifier scopifier = new Scopifier(registry, PROCESS_SCOPE_NAME, proxyTargetClass, scoped);
scopifier.visitBeanDefinition(definition);
if (scoped) {
Scopifier.createScopedProxy(beanName, definition, registry, proxyTargetClass);
}
}
beanFactory.registerSingleton(ProcessScope.PROCESS_SCOPE_PROCESS_VARIABLES_SINGLETON, this.processVariablesMap);
beanFactory.registerResolvableDependency(ProcessInstance.class, createSharedProcessInstance());
}
public void destroy() throws Exception {
logger.info(ProcessScope.class.getName() + "#destroy() called ...");
}
public void afterPropertiesSet() throws Exception {
Assert.notNull(this.processEngine, "the 'processEngine' must not be null!");
this.runtimeService = this.processEngine.getRuntimeService();
}
private Object createDirtyCheckingProxy(final String name, final Object scopedObject) throws Throwable {
ProxyFactory proxyFactoryBean = new ProxyFactory(scopedObject);
proxyFactoryBean.setProxyTargetClass(this.proxyTargetClass);
proxyFactoryBean.addAdvice(new MethodInterceptor() {
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
Object result = methodInvocation.proceed();
persistVariable(name, scopedObject);
return result;
}
});
return proxyFactoryBean.getProxy(this.classLoader);
}
private void persistVariable(String variableName, Object scopedObject) {
ProcessInstance processInstance = Context.getExecutionContext().getProcessInstance();
ExecutionEntity executionEntity = (ExecutionEntity) processInstance;
Assert.isTrue(scopedObject instanceof Serializable, "the scopedObject is not " + Serializable.class.getName() + "!");
executionEntity.setVariable(variableName, scopedObject);
}
}