/*
* JBoss, Home of Professional Open Source
* Copyright 2006, JBoss Inc., and individual contributors as indicated
* by the @authors tag. See the copyright.txt in the distribution for a
* full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.jboss.soa.esb.listeners.message;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;
import org.apache.log4j.Logger;
import org.jboss.internal.soa.esb.assertion.AssertArgument;
import org.jboss.soa.esb.ConfigurationException;
import org.jboss.soa.esb.actions.AbstractActionPipelineProcessor;
import org.jboss.soa.esb.actions.ActionLifecycleException;
import org.jboss.soa.esb.actions.ActionProcessingException;
import org.jboss.soa.esb.actions.annotation.AttachmentParam;
import org.jboss.soa.esb.actions.annotation.BodyParam;
import org.jboss.soa.esb.actions.annotation.OnException;
import org.jboss.soa.esb.actions.annotation.OnSuccess;
import org.jboss.soa.esb.actions.annotation.Process;
import org.jboss.soa.esb.actions.annotation.PropertyParam;
import org.jboss.soa.esb.configure.AnnotationUtil;
import org.jboss.soa.esb.configure.Configurator;
import org.jboss.soa.esb.helpers.ConfigTree;
import org.jboss.soa.esb.listeners.ListenerTagNames;
import org.jboss.soa.esb.message.Attachment;
import org.jboss.soa.esb.message.Body;
import org.jboss.soa.esb.message.Message;
import org.jboss.soa.esb.message.MessagePayloadProxy;
import org.jboss.soa.esb.message.Properties;
/**
* Bean Action Container.
* <p/>
* Container Action class for @ProcessMethod annotated bean action.
*
* @author <a href="mailto:tom.fennelly@jboss.com">tom.fennelly@jboss.com</a>
*/
public class BeanContainerAction extends AbstractActionPipelineProcessor {
private final static Logger logger = Logger.getLogger(BeanContainerAction.class);
private String actionName;
private Object bean;
private ConfigTree configTree;
private MessagePayloadProxy payloadProxy;
private Method processMethod;
private ParamResolver[] paramResolvers;
private Map<String, Method> onSuccessMethods;
private Map<String, Method> onExceptionMethods;
public BeanContainerAction(Object bean, ConfigTree configTree) throws ConfigurationException {
AssertArgument.isNotNull(bean, "bean");
AssertArgument.isNotNull(configTree, "configTree");
actionName = configTree.getAttribute(ListenerTagNames.ACTION_ELEMENT_TAG);
this.bean = bean;
this.configTree = configTree;
payloadProxy = new MessagePayloadProxy(configTree);
Map<String, Method> processMethods = getAnnotatedMethods(bean.getClass(), Process.class);
if(processMethods.isEmpty()) {
throw new IllegalArgumentException("Invalid Bean Action type '" + bean.getClass().getName() + "'. An Action bean must contain at least one public method annotated with the @ProcessMethod annotation.");
}
if(processMethods.size() == 1) {
processMethod = processMethods.values().iterator().next();
} else {
String processMethodName = configTree.getAttribute(ListenerTagNames.PROCESS_METHOD_TAG);
if(processMethodName == null) {
throw new ConfigurationException("Invalid configuration for Bean Action '" + actionName + "' (bean type'" + bean.getClass().getName() + "'). Bean contain 1+ public methods annotated with the @ProcessMethod annotation. The <action> '" + ListenerTagNames.PROCESS_METHOD_TAG + "' attribute must be specified.");
} else if (!processMethods.containsKey(processMethodName)) {
throw new ConfigurationException("Invalid configuration for Bean Action '" + actionName + "' (bean type'" + bean.getClass().getName() + "'). Bean does not contain a public method named '" + processMethodName + "', annotated with the @ProcessMethod annotation.");
}
processMethod = processMethods.get(processMethodName);
}
Class<?>[] args = processMethod.getParameterTypes();
Annotation[][] paramAnnotations = processMethod.getParameterAnnotations();
paramResolvers = new ParamResolver[args.length];
for(int i = 0; i < args.length; i++) {
BodyParam bodyParam = findAnnotation(BodyParam.class, paramAnnotations[i]);
PropertyParam propertyParam = findAnnotation(PropertyParam.class, paramAnnotations[i]);
AttachmentParam attachmentParam = findAnnotation(AttachmentParam.class, paramAnnotations[i]);
int annotationCount = 0;
if(bodyParam != null) {
annotationCount++;
}
if(propertyParam != null) {
annotationCount++;
}
if(attachmentParam != null) {
annotationCount++;
}
if(annotationCount == 0) {
paramResolvers[i] = new CascadingParamResolver(args[i]);
} else if(annotationCount == 1) {
if(bodyParam != null) {
paramResolvers[i] = new BodyResolver(args[i], bodyParam);
} else if(propertyParam != null) {
paramResolvers[i] = new PropertyResolver(args[i], propertyParam);
} else if(attachmentParam != null) {
paramResolvers[i] = new AttachmentResolver(args[i], attachmentParam);
}
} else {
throw new IllegalArgumentException("Invalid Bean Action type '" + bean.getClass().getName() + "'. The @ProcessMethod annotated method '" + processMethod.getName() + "' contains an argument that conflicting ParamResolver annotation.");
}
}
onSuccessMethods = getAnnotatedMethods(bean.getClass(), OnSuccess.class);
onExceptionMethods = getAnnotatedMethods(bean.getClass(), OnException.class);
Configurator.configure(bean, configTree);
}
private <T extends Annotation> T findAnnotation(Class<T> annotation, Annotation[] annotations) {
for(Annotation annotationEntry : annotations) {
if(annotation.isInstance(annotationEntry)) {
return annotation.cast(annotationEntry);
}
}
return null;
}
/* (non-Javadoc)
* @see org.jboss.soa.esb.actions.AbstractActionLifecycle#initialise()
*/
@Override
public void initialise() throws ActionLifecycleException {
Configurator.initialise(bean, configTree);
}
/* (non-Javadoc)
* @see org.jboss.soa.esb.actions.ActionPipelineProcessor#process(org.jboss.soa.esb.message.Message)
*/
public Message process(Message message) throws ActionProcessingException {
Object processResult = null;
try {
Object[] params = new Object[paramResolvers.length];
for(int i = 0; i < paramResolvers.length; i++) {
try {
params[i] = paramResolvers[i].getParam(message);
} catch (MessageDeliverException e) {
throw new ActionProcessingException("Error resolving method parameter from ESB message.", e);
}
}
processResult = processMethod.invoke(bean, params);
} catch (IllegalArgumentException e) {
throw new ActionProcessingException("Bean Action '" + actionName + "' exception.", e);
} catch (IllegalAccessException e) {
throw new ActionProcessingException("Bean Action '" + actionName + "' exception.", e);
} catch (InvocationTargetException e) {
Throwable targetException = e.getTargetException();
if(targetException instanceof ActionProcessingException) {
throw (ActionProcessingException) targetException;
} else if (targetException instanceof RuntimeException) {
throw (RuntimeException) targetException;
} else {
throw new ActionProcessingException("Bean Action '" + actionName + "' exception.", targetException);
}
}
if(processResult == null && processMethod.getReturnType() != void.class) {
// Terminate the pipeline...
return null;
}
if(processResult instanceof Message) {
return (Message) processResult;
} else if(processResult != null) {
try {
payloadProxy.setPayload(message, processResult);
} catch (MessageDeliverException e) {
throw new ActionProcessingException("Error injecting 'out' payload into ESB message.", e);
}
}
return message;
}
/* (non-Javadoc)
* @see org.jboss.soa.esb.actions.AbstractActionPipelineProcessor#processSuccess(org.jboss.soa.esb.message.Message)
*/
@Override
public void processSuccess(Message message) {
if(!onSuccessMethods.isEmpty()) {
for(Method method : onSuccessMethods.values()) {
Class<?>[] args = method.getParameterTypes();
try {
if(args.length == 0) {
method.invoke(bean, new Object[] {});
} else if(args.length == 1 && args[0] == Message.class) {
method.invoke(bean, new Object[] {message});
} else {
logger.debug("@OnSuccessMethod '"+ method.getName() + "' has too many arguments, or contains the wrong combination of args [" + Arrays.asList(args) + "].");
}
} catch (IllegalArgumentException e) {
logger.error("Exception while invoking @OnSuccessMethod '"+ method.getName() + "'.", e);
} catch (IllegalAccessException e) {
logger.error("Exception while invoking @OnSuccessMethod '"+ method.getName() + "'.", e);
} catch (InvocationTargetException e) {
logger.error("Exception while invoking @OnSuccessMethod '"+ method.getName() + "'.", e.getTargetException());
}
}
}
}
/* (non-Javadoc)
* @see org.jboss.soa.esb.actions.AbstractActionPipelineProcessor#processException(org.jboss.soa.esb.message.Message, java.lang.Throwable)
*/
@Override
public void processException(Message message, Throwable th) {
if(!onExceptionMethods.isEmpty()) {
for(Method method : onExceptionMethods.values()) {
Class<?>[] args = method.getParameterTypes();
try {
if(args.length == 0) {
method.invoke(bean, new Object[] {});
} else if(args.length == 1 && args[0] == Message.class) {
method.invoke(bean, new Object[] {message});
} else if(args.length == 2 && args[0] == Message.class && args[1] == Throwable.class) {
method.invoke(bean, new Object[] {message, th});
} else {
logger.debug("@OnExceptionMethod '"+ method.getName() + "' has too many arguments, or contains the wrong combination of args [" + Arrays.asList(args) + "].");
}
} catch (IllegalArgumentException e) {
logger.error("Exception while invoking @OnExceptionMethod '"+ method.getName() + "'.", e);
} catch (IllegalAccessException e) {
logger.error("Exception while invoking @OnExceptionMethod '"+ method.getName() + "'.", e);
} catch (InvocationTargetException e) {
logger.error("Exception while invoking @OnExceptionMethod '"+ method.getName() + "'.", e.getTargetException());
}
}
}
}
/* (non-Javadoc)
* @see org.jboss.soa.esb.actions.AbstractActionLifecycle#destroy()
*/
@Override
public void destroy() throws ActionLifecycleException {
Configurator.destroy(bean);
}
public static boolean isAnnotatedActionClass(Class<?> runtimeClass) {
// It's an Action bean if it has one or more @ProcessMethod annotated methods...
return (!getAnnotatedMethods(runtimeClass, Process.class).isEmpty());
}
private static Map<String, Method> getAnnotatedMethods(Class<?> runtimeClass, Class<? extends Annotation> annotation) {
AssertArgument.isNotNull(runtimeClass, "runtimeClass");
Method[] publicMethods = runtimeClass.getMethods();
Map<String, Method> processMethods = new LinkedHashMap<String, Method>();
for(Method method : publicMethods) {
if(method.isAnnotationPresent(annotation)) {
processMethods.put(method.getName(), method);
}
}
return processMethods;
}
private interface ParamResolver {
Object getParam(Message message) throws MessageDeliverException;
}
private class CascadingParamResolver implements ParamResolver {
private Class<?> paramType;
private BodyResolver bodyResolver;
private PropertyResolver propertyResolver;
private AttachmentResolver attachmentResolver;
private CascadingParamResolver(Class<?> paramType) {
this.paramType = paramType;
bodyResolver = new BodyResolver(paramType, null);
propertyResolver = new PropertyResolver(paramType, null);
attachmentResolver = new AttachmentResolver(paramType, null);
}
public Object getParam(Message message) throws MessageDeliverException {
if(Message.class.isAssignableFrom(paramType)) {
return message;
}
Object payload = payloadProxy.getPayload(message);
if(paramType.isInstance(payload)) {
return payload;
}
// Now lets cascade, trying the different message parts to find the first match....
Object param = bodyResolver.getParam(message);
if(param == null) {
param = propertyResolver.getParam(message);
if(param == null) {
param = attachmentResolver.getParam(message);
}
}
return param;
}
}
private class PropertyResolver implements ParamResolver {
private Class<?> paramType;
private PropertyParam property;
private PropertyResolver(Class<?> paramType, PropertyParam property) {
this.paramType = paramType;
this.property = property;
}
public Object getParam(Message message) throws MessageDeliverException {
Properties properties = message.getProperties();
String propertyName = AnnotationUtil.getPropertyName(property);
if(propertyName != null) {
Object param = properties.getProperty(propertyName);
if(param != null && !paramType.isInstance(param)) {
throw new MessageDeliverException("Named property '" + propertyName + "' exists on ESB message but is not of type '" + paramType.getName() + "'.");
}
return param;
} else {
for(String propertyNameOnMessage : properties.getNames()) {
Object param = properties.getProperty(propertyNameOnMessage);
if(param != null && paramType.isInstance(param)) {
return param;
}
}
}
return null;
}
}
private class BodyResolver implements ParamResolver {
private Class<?> paramType;
private BodyParam bodyParam;
private BodyResolver(Class<?> paramType, BodyParam bodyParam) {
this.paramType = paramType;
this.bodyParam = bodyParam;
}
public Object getParam(Message message) throws MessageDeliverException {
Body body = message.getBody();
String bodyPartName = AnnotationUtil.getBodyName(bodyParam);
if(bodyPartName != null) {
Object param = body.get(bodyPartName);
if(param != null && !paramType.isInstance(param)) {
throw new MessageDeliverException("Named Body part '" + bodyPartName + "' exists on ESB message but is not of type '" + paramType.getName() + "'.");
}
return param;
} else {
for(String bodyPartNameOnMessage : body.getNames()) {
Object param = body.get(bodyPartNameOnMessage);
if(param != null && paramType.isInstance(param)) {
return param;
}
}
}
return null;
}
}
private class AttachmentResolver implements ParamResolver {
private Class<?> paramType;
private AttachmentParam attachmentParam;
private AttachmentResolver(Class<?> paramType, AttachmentParam attachmentParam) {
this.paramType = paramType;
this.attachmentParam = attachmentParam;
}
public Object getParam(Message message) throws MessageDeliverException {
Attachment attachment = message.getAttachment();
String attachPartName = AnnotationUtil.getAttachmentName(attachmentParam);
if(attachPartName != null) {
Object param = attachment.get(attachPartName);
if(param != null && !paramType.isInstance(param)) {
throw new MessageDeliverException("Named Attachment part '" + attachPartName + "' exists on ESB message but is not of type '" + paramType.getName() + "'.");
}
return param;
} else {
for(String attachPartNameOnMessage : attachment.getNames()) {
Object param = attachment.get(attachPartNameOnMessage);
if(param != null && paramType.isInstance(param)) {
return param;
}
}
}
return null;
}
}
}