/*
* This file is part of the WfMOpen project.
* Copyright (C) 2001-2003 Danet GmbH (www.danet.de), GS-AN.
* All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
* $Id: EJBUtil.java 2550 2007-10-22 14:00:54Z $
*
* $Log$
* Revision 1.7 2006/10/11 09:42:36 drmlipp
* Comepleted adaption to local KeyGen EJB.
*
* Revision 1.6 2006/10/11 09:06:08 drmlipp
* Fixed EJB naming.
*
* Revision 1.5 2006/10/11 07:47:02 drmlipp
* Adapted properties file to new convention.
*
* Revision 1.4 2006/10/07 20:41:34 mlipp
* Merged J2EE 1.4 adaptions from test branch.
*
* Revision 1.3 2006/09/29 12:32:08 drmlipp
* Consistently using WfMOpen as projct name now.
*
* Revision 1.2 2005/09/10 21:44:18 mlipp
* Fixed JDK 5.0 warnings.
*
* Revision 1.1.1.3 2004/08/18 15:17:34 drmlipp
* Update to 1.2
*
* Revision 1.19 2004/02/13 10:32:22 lipp
* Added support for local home interfaces.
*
* Revision 1.18 2003/10/27 15:28:51 lipp
* Fixed repeat behaviour.
*
* Revision 1.17 2003/10/27 14:33:37 lipp
* Added debug statements.
*
* Revision 1.16 2003/10/24 11:08:01 lipp
* Added retrievJNDIEntry.
*
* Revision 1.15 2003/10/22 15:58:45 lipp
* Fixed synchronization problem.
*
* Revision 1.14 2003/09/09 13:38:47 lipp
* Added support for local home and business interfaces.
*
* Revision 1.13 2003/06/29 19:50:07 lipp
* Moved primary key generator fropm JDBCUtil to EJBUtil and made some
* fixes.
*
* Revision 1.12 2003/06/27 08:51:47 lipp
* Fixed copyright/license information.
*
* Revision 1.11 2003/05/23 15:42:41 lipp
* Fixed deployment unit dependencies.
*
* Revision 1.10 2003/03/31 16:50:27 huaiyang
* Logging using common-logging.
*
* Revision 1.9 2002/09/26 15:05:13 lipp
* Added method and extended comments.
*
* Revision 1.8 2002/08/30 13:37:04 lipp
* Using Workflow engine facade now.
*
* Revision 1.7 2002/07/29 21:03:57 lipp
* Some JavaDoc fixes.
*
* Revision 1.6 2002/07/24 05:48:16 huaiyang
* javadocs added.
*
* Revision 1.5 2002/07/03 11:02:17 lipp
* New session ejb handling support.
*
* Revision 1.4 2002/02/06 16:02:29 lipp
* Added caching for home interfaces.
*
* Revision 1.3 2001/12/12 14:11:45 lipp
* Added environment lookup.
*
* Revision 1.2 2001/10/25 17:02:13 robert
* javadoc
*
* Revision 1.1 2001/10/25 07:41:23 lipp
* Moved EJBUtil to de.danet.an.util
*
* Revision 1.1 2001/10/21 20:03:00 lipp
* Initial version
*
*/
package de.danet.an.util;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.rmi.RemoteException;
import javax.ejb.CreateException;
import javax.ejb.EJBException;
import javax.ejb.EJBHome;
import javax.ejb.EJBLocalHome;
import javax.ejb.EJBLocalObject;
import javax.ejb.EJBObject;
import javax.ejb.RemoveException;
import javax.naming.CommunicationException;
import javax.naming.InitialContext;
import javax.naming.InterruptedNamingException;
import javax.naming.NamingException;
import javax.rmi.PortableRemoteObject;
/**
* Collection of EJB utilities.
* This class adds common utilitites for an EJB environment.
*/
public class EJBUtil {
private static final org.apache.commons.logging.Log logger
= org.apache.commons.logging.LogFactory.getLog(EJBUtil.class);
/**
* The initial context used for lookup.
*/
private static InitialContext initialContext = null;
/** Size of low key. */
private static int lowKeySize = 64;
/** Key generation EJB name. */
private static String keyGenEjbName = null;
/** Key generation high table. */
private static String keyGenTable = null;
/**
* Lookup a JNDI entry. This method implements caching as not all
* application servers implement JNDI efficiently. As a
* consequence, this method can only be used for read-only entries
* that do not change over time.
*
* @param entry the JNDI name of the entry
* @return the object found object
* @throws NamingException if the JNDI lookup fails.
*/
public static Object lookupJNDIEntry(String entry)
throws NamingException{
if (initialContext == null) {
initialContext = new InitialContext();
}
Object objref = null;
try {
objref = initialContext.lookup(entry);
} catch (NamingException ne) {
// maybe naming context is no longer valid (e.g. cluster), retry
initialContext = new InitialContext();
objref = initialContext.lookup(entry);
}
return objref;
}
/**
* Lookup a JNDI entry using {@link #lookupJNDIEntry
* <code>lookupJNDIEntry</code>}. This should be called from a
* context where the JNDI name is expected
* to exist. <code>NamingException</code>s should thus only occur
* if the service is not available due to dynamic error conditions
* (network failure etc.). These exceptions are mapped to a
* <code>ResourceNotAvailable</code> exception.<P>
*
* If, nevertheless, the <code>NamingException</code> indicates
* that the JNDI entry does not exists, we assume a configuration
* error and throw an <code>IllegalStateException</code>.
*
* @param entry the JNDI name
* @return an EJBHome object
* @throws ResourceNotAvailableException if a problem occurs that
* should be temporary.
* @throws IllegalStateException if the given
* <code>jndiName</code> is not registered.
*/
public static Object retrieveJNDIEntry(String entry)
throws ResourceNotAvailableException, IllegalStateException {
try {
return lookupJNDIEntry (entry);
} catch (CommunicationException e) {
logger.error (e.getMessage(), e);
throw new ResourceNotAvailableException (e.getMessage());
} catch (InterruptedNamingException e) {
logger.error (e.getMessage(), e);
throw new ResourceNotAvailableException (e.getMessage());
} catch (NamingException e) {
logger.error (e.getMessage(), e);
throw new IllegalStateException (e.getMessage());
}
}
private static Map homeCache = new HashMap ();
/**
* Lookup a home interface using {@link #lookupJNDIEntry
* <code>lookupJNDIEntry</code>}.
*
* The method provides caching of looked up home interface
* if the JNDI name is global (i.e. does not start with
* "<code>java:comp/</code>").
*
* @param ejbClass the class of the home interface
* @param jndiName the JNDI name of the home interface
* @return an EJBHome object
* @throws NamingException if the JNDI lookup fails.
*/
public static EJBHome lookupEJBHome(Class ejbClass, String jndiName)
throws NamingException {
if (jndiName.startsWith ("java:comp/")) {
Object objref = lookupJNDIEntry (jndiName);
return (EJBHome)PortableRemoteObject.narrow (objref, ejbClass);
}
synchronized (homeCache) {
EJBHome ejbHome = (EJBHome)homeCache.get (jndiName);
if (ejbHome == null) {
ejbHome = (EJBHome)PortableRemoteObject.narrow
(lookupJNDIEntry (jndiName), ejbClass);
homeCache.put (jndiName, ejbHome);
}
return ejbHome;
}
}
/**
* Return an EJB's home interface using {@link #lookupEJBHome
* <code>lookupEJBHome</code>}. This should be called from a
* context where the JNDI name of the home interface is expected
* to exist. <code>NamingException</code>s should thus only occur
* if the service is not available due to dynamic error conditions
* (network failure etc.). These exceptions are mapped to a
* <code>ResourceNotAvailable</code> exception.<P>
*
* If, nevertheless, the <code>NamingException</code> indicates
* that the JNDI entry does not exists, we assume a configuration
* error and throw an <code>IllegalStateException</code>.
*
* @param ejbClass the class of the home interface
* @param jndiName the JNDI name of the home interface
* @return an EJBHome object
* @throws ResourceNotAvailableException if a problem occurs that
* should be temporary.
* @throws IllegalStateException if the given
* <code>jndiName</code> is not registered.
*/
public static EJBHome retrieveEJBHome(Class ejbClass, String jndiName)
throws ResourceNotAvailableException, IllegalStateException {
try {
return lookupEJBHome (ejbClass, jndiName);
} catch (CommunicationException e) {
logger.error (e.getMessage(), e);
throw new ResourceNotAvailableException (e.getMessage());
} catch (InterruptedNamingException e) {
logger.error (e.getMessage(), e);
throw new ResourceNotAvailableException (e.getMessage());
} catch (NamingException e) {
logger.error (e.getMessage(), e);
throw new IllegalStateException (e.getMessage());
}
}
/**
* Lookup a local home interface using {@link #lookupJNDIEntry
* <code>lookupJNDIEntry</code>}.
*
* @param ejbClass the class of the home interface
* @param jndiName the JNDI name of the home interface
* @return an EJBHome object
* @throws NamingException if the JNDI lookup fails.
*/
public static EJBLocalHome lookupEJBLocalHome
(Class ejbClass, String jndiName)
throws NamingException {
Object objref = lookupJNDIEntry (jndiName);
return (EJBLocalHome)PortableRemoteObject.narrow (objref, ejbClass);
}
/**
* Return an EJB's local home interface using {@link
* #lookupEJBHome <code>lookupEJBLocalHome</code>}. This should be
* called from a context where the JNDI name of the home interface
* is expected to exist. <code>NamingException</code>s should thus
* only occur if the service is not available due to dynamic error
* conditions (network failure etc.). These exceptions are mapped
* to a <code>ResourceNotAvailable</code> exception.<P>
*
* If, nevertheless, the <code>NamingException</code> indicates
* that the JNDI entry does not exists, we assume a configuration
* error and throw an <code>IllegalStateException</code>.
*
* @param ejbClass the class of the home interface
* @param jndiName the JNDI name of the home interface
* @return an EJBHome object
* @throws ResourceNotAvailableException if a problem occurs that
* should be temporary.
* @throws IllegalStateException if the given
* <code>jndiName</code> is not registered.
*/
public static EJBLocalHome retrieveEJBLocalHome
(Class ejbClass, String jndiName)
throws ResourceNotAvailableException, IllegalStateException {
try {
return lookupEJBLocalHome (ejbClass, jndiName);
} catch (CommunicationException e) {
logger.error (e.getMessage(), e);
throw new ResourceNotAvailableException (e.getMessage());
} catch (InterruptedNamingException e) {
logger.error (e.getMessage(), e);
throw new ResourceNotAvailableException (e.getMessage());
} catch (NamingException e) {
logger.error (e.getMessage(), e);
throw new IllegalStateException (e.getMessage());
}
}
/**
* Create a session EJB. This method can be used for the special
* — but very common — case when you want to create a
* session EJB and the corresponding home interface has a create
* method without any paramaters.<P>
*
* The given home interface is looked up using {@link
* #retrieveEJBHome <code>retrieveEJBHome</code>} or {@link
* #retrieveEJBLocalHome <code>retrieveEJBLocalHome</code>} (as
* appropriate for the given class) and then the create session
* method is called.
*
* @param ejbClass the class of the home interface which must define a
* <code>create()</code> method.
* @param jndiName the JNDI name of the home interface
* @return the new <code>EJBObject</code>.
* @throws IllegalArgumentException if no <code>create</code>
* method without arguments exists.
* @throws ResourceNotAvailableException if a problem occurs that
* should be temporary.
*/
public static Object createSession (Class ejbClass, String jndiName)
throws IllegalArgumentException, ResourceNotAvailableException {
if (EJBHome.class.isAssignableFrom (ejbClass)) {
EJBHome home = retrieveEJBHome (ejbClass, jndiName);
return createSession (home);
} else {
EJBLocalHome home = retrieveEJBLocalHome (ejbClass, jndiName);
return createSession (home);
}
}
/**
* Create a session EJB. This method can be used for the special -
* but very common - case when you want to create a session EJB
* and the corresponding home interface has a create method
* without any paramaters.<P>
*
* @param home the home interface
* @throws IllegalArgumentException if no <code>create</code>
* method without arguments exists.
* @throws ResourceNotAvailableException if a problem occurs that
* should be temporary.
* @return the new <code>EJBObject</code>.
*/
public static EJBObject createSession (EJBHome home)
throws IllegalArgumentException, ResourceNotAvailableException {
Class homeClass = null;
try {
homeClass = home.getClass();
Method createMethod = homeClass.getMethod("create", (Class[])null);
return (EJBObject)createMethod.invoke(home, new Object[0]);
} catch (NoSuchMethodException nm) {
throw new IllegalArgumentException
(homeClass.getName() + " does not define a create() method.");
} catch (IllegalAccessException ia) {
throw new IllegalArgumentException
("Calling " + homeClass.getName() + ".create() not allowed: "
+ ia.getMessage());
} catch (InvocationTargetException ie) {
Throwable tex = ie.getTargetException();
logger.warn ("Exception in " + homeClass.getName() + ".create():",
unwrapEJBException(tex));
logger.warn ("Unwrapped exception is:", tex);
if ((tex instanceof RemoteException)
|| (tex instanceof CreateException)
|| (tex instanceof EJBException)) {
// these should be temporary conditions
throw new ResourceNotAvailableException (tex.getMessage());
}
throw new IllegalArgumentException (tex.getMessage());
}
}
/**
* Create a session EJB. This method can be used for the special -
* but very common - case when you want to create a session EJB
* and the corresponding home interface has a create method
* without any paramaters.<P>
*
* @param home the home interface
* @throws IllegalArgumentException if no <code>create</code>
* method without arguments exists.
* @throws ResourceNotAvailableException if a problem occurs that
* should be temporary.
* @return the new <code>EJBObject</code>.
*/
public static EJBLocalObject createSession (EJBLocalHome home)
throws IllegalArgumentException, ResourceNotAvailableException {
Class homeClass = null;
try {
homeClass = home.getClass();
Method createMethod = homeClass.getMethod("create", (Class[])null);
return (EJBLocalObject)createMethod.invoke(home, new Object[0]);
} catch (NoSuchMethodException nm) {
throw new IllegalArgumentException
(homeClass.getName() + " does not define a create() method.");
} catch (IllegalAccessException ia) {
throw new IllegalArgumentException
("Calling " + homeClass.getName() + ".create() not allowed: "
+ ia.getMessage());
} catch (InvocationTargetException ie) {
Throwable tex = ie.getTargetException();
logger.warn ("Exception in " + homeClass.getName() + ".create():",
unwrapEJBException(tex));
logger.warn ("Unwrapped exception is:", tex);
if ((tex instanceof RemoteException)
|| (tex instanceof CreateException)
|| (tex instanceof EJBException)) {
// these should be temporary conditions
throw new ResourceNotAvailableException (tex.getMessage());
}
throw new IllegalArgumentException (tex.getMessage());
}
}
/**
* Convenience method to clean up a session connection after
* usage. If the given parameter is an instance of
* <code>EJBObject</code> it simply calls <code>remove()</code> on
* the given object. If the parameter is <code>null</code>
* (indicating that the previous session creation propably failed)
* nothing happens. If an error occurs when calling
* <code>remove()</code>, it will be logged as warning.
* @param handle the — maybe — ejb session object.
*/
public static void removeSession (Object handle) {
if (handle == null || !(handle instanceof EJBObject)) {
return;
}
try {
((EJBObject)handle).remove();
} catch (RemoveException rex) {
logger.warn ("Error in remove(): " + rex.getMessage(), rex);
} catch (RemoteException rex) {
logger.warn ("Error in remove(): " + rex.getMessage(), rex);
}
}
/**
* Unwrap the root cuase of an <code>EJBException</code>. The
* <code>EJBException</code> wrappers often hide the root cause of
* an exception. This method calls
* <code>getCausedByException()</code> until it finds an exception
* that is not of type <code>EJBException</code>. This exception
* is returned.
*
* @param ex the exception to be unwrapped.
* @return the root cause exception.
*/
public static Throwable unwrapEJBException (Throwable ex) {
while (ex instanceof EJBException) {
ex = ((EJBException)ex).getCausedByException ();
}
return ex;
}
/**
* Included database id values of each database tables. If a table is
* not present it is added.
*/
private static Map counters = new HashMap();
private static class HighLow {
public long high;
public int low;
}
/**
* Gets the next unique primary key for the given database table
* observing a minimum value.<P>
*
* This method uses a high/low algorithm. The high values are
* obtained from an EJB that maintains a table in a database.
* The JNDI name of the ejb used can be set in a properties file
* "<code>/de.danet.an.util.jdbcKeyGen.properties</code> with the
* entry "<code>generatorEjbJndiName</code>". <P>
*
* The JNDI name defaults to
* "<code>java:comp/env/ejb/JdbcKeyGenLocal</code>". This reflects the
* assumption that this method is usually called from an EJB or
* servlet. Of course, the deployment descriptor of the calling
* EJB or servlet must include an <code><ejb-ref></code>
* entry that links to the local home of EJB "KeyGen".<P>
*
* The table used by the EJB can be set with the property
* "<code>highKeyTable</code>". It defaults to
* "<code>KeyGeneratorHighs</code>". This table must be created as:
* <pre>create table KeyGeneratorHighs (
* TabName VARCHAR(50) NOT NULL,
* NextKey INTEGER NOT NULL
* )/</pre><P>
*
* Another property that can be set is "<code>highFactor</code>".
* It determines the number of low keys used before a new
* high key is requested. This value defaults to 64.
*
* @param table the name of the table.
* @param min the minimum value of the returned key.
* @return the new primary key.
* @throws ResourceNotAvailableException if an error occurs.
*/
public static long newPrimaryKey (String table, long min)
throws ResourceNotAvailableException {
try {
if (keyGenEjbName == null) {
Properties props = new Properties();
try {
InputStream is = JDBCUtil.class.getResourceAsStream
("/de.danet.an.util.jdbcKeyGen.properties");
if (is != null) {
props.load (is);
}
} catch (IOException ex) {
logger.error ("Cannot read jdbcKeyGen.properties: "
+ ex.getMessage (), ex);
}
keyGenTable = props.getProperty
("highKeyTable", "KeyGeneratorHighs");
lowKeySize = Integer.parseInt
(props.getProperty ("highFactor", "64"));
keyGenEjbName = props.getProperty
("generatorEjbJndiName",
"java:comp/env/ejb/JdbcKeyGenLocal");
}
// now do the work
HighLow hl = null;
synchronized (counters) {
hl = (HighLow)counters.get (table);
if (hl == null) {
hl = new HighLow ();
hl.low = lowKeySize - 1;
counters.put (table, hl);
}
}
synchronized (hl) {
if (logger.isDebugEnabled ()) {
logger.debug ("Finding new key for " + table
+ " using HighLow " + hl);
}
if (hl.low == (lowKeySize - 1)) {
KeyGenLocal keyGen = (KeyGenLocal)EJBUtil.createSession
(KeyGenLocalHome.class, keyGenEjbName);
while (true) {
try {
hl.high = keyGen.newHigh
(keyGenTable, table,
(min+lowKeySize-1) / lowKeySize);
break;
} catch (EJBException e) {
logger.info ("Problem calling newHigh (will be "
+ "repeated): " + e.getMessage ());
}
}
EJBUtil.removeSession (keyGen);
hl.low = 0;
}
return hl.high * lowKeySize + hl.low++;
}
} catch (RemoteException e) {
logger.error ("Cannot generate key: " + e.getMessage (), e);
throw new ResourceNotAvailableException (e.getMessage ());
}
}
/**
* Gets the next unique primary key for the given database table.
* @param table the name of the table.
* @return the new primary key.
* @throws ResourceNotAvailableException if an error occurs.
* @see #newPrimaryKey(String,long)
*/
public static long newPrimaryKey (String table)
throws ResourceNotAvailableException {
return newPrimaryKey (table, 0);
}
}