/*
* JBoss, Home of Professional Open Source
* Copyright 2006, JBoss Inc., and others contributors as indicated
* by the @authors tag. All rights reserved.
* See the copyright.txt in the distribution for a
* full listing of individual contributors.
* This copyrighted material is made available to anyone wishing to use,
* modify, copy, or redistribute it subject to the terms and conditions
* of the GNU Lesser General Public License, v. 2.1.
* This program is distributed in the hope that it will be useful, but WITHOUT A
* 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,
* v.2.1 along with this distribution; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*
* (C) 2008, JBoss Inc.
*/
/*
* Stateless Session Bean Action provided by Dominik Kunz.
*
* http://jira.jboss.com/jira/browse/JBESB-1350
*/
package org.jboss.soa.esb.actions;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.Map.Entry;
import javax.ejb.EJBHome;
import javax.ejb.EJBMetaData;
import javax.ejb.EJBObject;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.rmi.PortableRemoteObject;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;
import org.apache.log4j.Logger;
import org.jboss.security.auth.callback.AppCallbackHandler;
import org.jboss.soa.esb.ConfigurationException;
import org.jboss.soa.esb.helpers.ConfigTree;
import org.jboss.soa.esb.message.Message;
import org.jboss.soa.esb.util.ClassUtil;
/**
* EJBProcessor is an action that can call stateless session beans
* deployed in an application server.
* <p/>
* This implementation currently supports EJB2.x and EJB3.x session beans.
*
* Example EJB 2.x configuration:
* <pre>{@code
* <action name="EJBTest" class="org.jboss.soa.esb.actions.EJBProcessor">
* <property name="ejb-name" value="MyBean" />
* <property name="jndi-name" value="ejb/MyBean" />
* <property name="initial-context-factory" value="org.jnp.interfaces.NamingContextFactory" />
* <property name="security-principal" value="username" />
* <property name="security-credentials" value="password" />
* <!-- Optional property for JAAS login -->"
* <property name="security-login-module" value="securityDomain" />
* <property name="provider-url" value="localhost:1099" />
* <property name="method" value="login" />
*
* <!-- Optional output location, defaults to "DEFAULT_EJB_OUT"
* <property name="esb-out-var" value="MY_OUT_LOCATION"/> -->
* <property name="ejb-params">
* <!-- arguments of the operation and where to find them in the message -->
* <arg0 type="java.lang.String">username</arg0>
* <arg1 type="java.lang.String">password</arg1>
* </property>
* </action>
* }</pre>
*
* Example EJB 3.x configuration:
* <pre>{@code
* <action name="EJBTest" class="org.jboss.soa.esb.actions.EJBProcessor">
* <property name="ejb3" value="true" />
* <property name="jndi-name" value="ejb/MyBean" />
* <property name="initial-context-factory" value="org.jnp.interfaces.NamingContextFactory" />
* <property name="security-principal" value="username" />
* <property name="security-credentials" value="password" />
* <!-- Optional property for JAAS login -->"
* <property name="security-login-module" value="securityDomain" />
* <property name="provider-url" value="localhost:1099" />
* <property name="method" value="login" />
*
* <!-- Optional output location, defaults to "DEFAULT_EJB_OUT"
* <property name="esb-out-var" value="MY_OUT_LOCATION"/> -->
* <property name="ejb-params">
* <!-- arguments of the operation and where to find them in the message -->
* <arg0 type="java.lang.String">username</arg0>
* <arg1 type="java.lang.String">password</arg1>
* </property>
* </action>
* }</pre>
*
*/
public class EJBProcessor extends AbstractActionLifecycle
{
private static final Logger log = Logger.getLogger(EJBProcessor.class);
public static final String EJB_NAME = "ejb-name";
public static final String JNDI_NAME = "jndi-name";
public static final String EJB_METHOD = "method";
public static final String JAVA_TYPE = "type";
public static final String INICTXFACTORY = "initial-context-factory";
public static final String PROVIDERURL = "provider-url";
public static final String OUT_VAR = "esb-out-var";
public static final String DEFAULT_OUT = "DEFAULT_EJB_OUT";
public static final String EJB3_ATTRIBUTE = "ejb3";
public static final String SECURITY_PRINCIPAL = "security-principal";
public static final String SECURITY_CREDENTIALS = "security-credentials";
public static final String SECURITY_LOGIN_MODULE = "security-login-module";
public static final int ARG_PREFIX_LENGTH = 3;
protected ConfigTree configTree;
private Map<String, String> ejbRef;
private Map<Integer, Argument> ejbParams;
private List<String> ejbParamTypeNames;
private EJBHome ejbHome;
private EJBObject ejbObject;
private Object ejb3Interface;
private boolean ejb3;
private LoginContext loginContext;
public EJBProcessor(ConfigTree config)
{
configTree = config;
}
public Message process (Message msg) throws ActionProcessingException, ConfigurationException
{
try
{
// Assemble parameter array
Object[] param = new Object[ejbParams.size()];
for (int i = 0; i < ejbParams.size(); i++)
{
// get the parameter from the esb message and
// cast it to the in the jboss-esb.xml specified type
param[i] = ClassUtil.forName(ejbParams.get(i).getType(), getClass()).cast(
msg.getBody().get(ejbParams.get(i).getLoc()));
}
Object ret;
if ( ejb3 )
{
// invoke EJB3.x
ret = invoke(ejb3Interface.getClass(), ejb3Interface, ejbRef.get(EJB_METHOD), param);
}
else
{
// invoke EJB2.x
ret = invoke( ejbHome.getEJBMetaData().getRemoteInterfaceClass(), ejbObject, ejbRef.get(EJB_METHOD), param);
}
// add return object to messages output location
if ( ret != null )
{
msg.getBody().add( ejbRef.get(OUT_VAR), ret);
}
log.debug("###########################################");
log.debug(msg);
log.debug("###########################################");
}
catch (Exception e)
{
throw new ActionProcessingException( "Got an error while processing EJB method [" + ejbRef.get(EJB_METHOD) + "]", e);
}
return msg;
}
@Override
public void initialise () throws ActionLifecycleException
{
/*
* Only do the lookup once. We can do this because
* all of this data is statically defined and not
* modified by the incoming Message during process execution.
*/
ejbRef = new HashMap<String, String>();
ejbParams = new HashMap<Integer, Argument>();
ejbParamTypeNames = new ArrayList<String>();
ejb3 = Boolean.parseBoolean(configTree.getAttribute(EJB3_ATTRIBUTE, "false"));
// get properties common to both EJB2.x and EJB3.x
ejbRef.put(JNDI_NAME, configTree.getAttribute(JNDI_NAME));
ejbRef.put(EJB_METHOD, configTree.getAttribute(EJB_METHOD));
ejbRef.put(INICTXFACTORY, configTree.getAttribute(INICTXFACTORY));
ejbRef.put(PROVIDERURL, configTree.getAttribute(PROVIDERURL));
if ( !ejb3 )
ejbRef.put(EJB_NAME, configTree.getAttribute(EJB_NAME));
if (configTree.getAttribute(OUT_VAR) != null)
{
ejbRef.put(OUT_VAR, configTree.getAttribute(OUT_VAR));
}
else
{
ejbRef.put(OUT_VAR, DEFAULT_OUT);
}
// Get all parameters for the EJB method, defined in jboss-esb.xml
ConfigTree[] subElements = configTree.getAllChildren();
for (ConfigTree child : subElements)
{
Integer argNum;
String jType;
String esbLocation;
argNum = Integer.parseInt(child.getName().substring(ARG_PREFIX_LENGTH));
jType = child.getAttribute(JAVA_TYPE);
esbLocation = child.getWholeText();
ejbParams.put(argNum, new Argument(jType, esbLocation));
ejbParamTypeNames.add(jType);
}
// Check for missing configuration values
Set<Entry<String, String>> entrySet = ejbRef.entrySet();
for (Entry<String, String> entry : entrySet)
{
if ( entry.getValue() == null )
{
throw new ActionLifecycleException( "Error configuring EJBProcessor.[" + entry.getKey() + "] must not be null");
}
}
// Build Properties for InitialContext lookup
Properties props = new Properties();
props.put(Context.INITIAL_CONTEXT_FACTORY, ejbRef.get(INICTXFACTORY));
props.put(Context.PROVIDER_URL, ejbRef.get(PROVIDERURL));
// extract security principal from config
final String username = configTree.getAttribute(SECURITY_PRINCIPAL);
if (username != null)
{
// extract security credential from config
final String password = configTree.getAttribute(SECURITY_CREDENTIALS);
if (password == null)
{
throw new ActionLifecycleException("'" + SECURITY_CREDENTIALS + "' configuration property is missing from esb configuration. It is required when '" + SECURITY_PRINCIPAL + "' is specified");
}
// Check if a jaas login module was specified. If so use a jaas login.
final String loginModuleName = configTree.getAttribute(SECURITY_LOGIN_MODULE);
if (loginModuleName != null)
{
login(loginModuleName, createCallbackHandler(configTree));
}
// assume that the principal and credential are to be used by the initial context factory.
else
{
props.setProperty(Context.SECURITY_CREDENTIALS, password);
props.setProperty(Context.SECURITY_PRINCIPAL, username);
}
}
InitialContext initCtx = getInitialContext(props);
if ( ejb3 )
{
ejb3Interface = getEjb3FromJndi(initCtx);
}
else
{
try
{
// Lookup and narrow
ejbHome = (EJBHome) PortableRemoteObject.narrow( (EJBHome) initCtx.lookup(ejbRef.get(JNDI_NAME)), EJBHome.class);
// Get the EJB metadata
EJBMetaData metaData = ejbHome.getEJBMetaData();
Class<?> homeClass = metaData.getHomeInterfaceClass();
// convert handle to real home type
ejbHome = (EJBHome) javax.rmi.PortableRemoteObject.narrow(ejbHome, homeClass);
if (!(metaData.isSession() && metaData.isStatelessSession()))
{
throw new ActionLifecycleException("Only SLSBs are supported!");
}
ejbObject = (EJBObject) EJBProcessor.create(homeClass, ejbHome);
}
catch (Exception e)
{
throw new ActionLifecycleException( "Got an error while processing EJB " + ejbRef.get(EJB_METHOD), e);
}
}
}
InitialContext getInitialContext(final Properties props) throws ActionLifecycleException
{
InitialContext context;
try
{
context = new InitialContext(props);
} catch (final NamingException e)
{
throw new ActionLifecycleException("Could not create a new InitialContext with properties : " + props, e);
}
return context;
}
Object getEjb3FromJndi(final Context context) throws ActionLifecycleException
{
Object ejb3Interface;
try
{
ejb3Interface = context.lookup(ejbRef.get(JNDI_NAME));
} catch (final NamingException e)
{
throw new ActionLifecycleException("Could not lookup " + ejbRef.get(JNDI_NAME), e);
}
return ejb3Interface;
}
/**
* Creates an {@link AppCallbackHandler} that takes a username and password.
* This method is protected to let subclasses override it to implement other
* security authentication mechanisms.
*
* @param config The configuration for this action. Gives access to all config parameters.
* @return CallbackHandler A callback handler that suitable for the login module configured.
*/
protected CallbackHandler createCallbackHandler(final ConfigTree config)
{
final String username = configTree.getAttribute(SECURITY_PRINCIPAL);
return new AppCallbackHandler(username, ((String)configTree.getAttribute(SECURITY_CREDENTIALS)).toCharArray());
}
void login(final String loginModuleName, final CallbackHandler callbackHandler) throws ActionLifecycleException
{
try
{
loginContext = new LoginContext (loginModuleName, callbackHandler);
loginContext.login();
}
catch (final Exception e)
{
throw new ActionLifecycleException(e.getMessage(), e);
}
}
@Override
public void destroy()
{
if (loginContext != null)
{
try
{
loginContext.logout();
}
catch (LoginException e)
{
log.warn(e.getMessage(), e);
}
}
}
private static Object create (Class<?> c, Object obj) throws Exception
{
Object ret = null;
Method create = c.getMethod("create");
ret = create.invoke(obj);
return ret;
}
private Object invoke (Class<?> c, Object obj, String mname, Object[] params) throws ClassNotFoundException, SecurityException, NoSuchMethodException, IllegalArgumentException, IllegalAccessException, InvocationTargetException
{
// The return Object
Object r = null;
// Assemble method signature array
Class<?>[] sigArray = new Class[ejbParams.size()];
for (int i = 0; i < ejbParams.size(); i++)
{
sigArray[i] = ClassUtil.forName(ejbParams.get(i).getType(), getClass());
}
// Get the specified method
Method method = c.getMethod(mname, sigArray);
// finally invoke it...
r = method.invoke(obj, params);
return r;
}
// Helper inner class for method arguments and where to find it in the esb
// message
private static class Argument
{
private String type;
private String loc;
public Argument(String javaType, String esbLoc)
{
type = javaType;
loc = esbLoc;
}
public String getType ()
{
return type;
}
public void setType (String type)
{
this.type = type;
}
public String getLoc ()
{
return loc;
}
public void setLoc (String loc)
{
this.loc = loc;
}
}
}