I also hate writing getters and setters. I would prefer to use POJOs, even POJOs declared as nested classes.
There is another way to do this, even with old servers and technology and without introducing Springs (we use JBoss 4.2 and JBoss's incomplete EJB 3.0). Extending the org.apache.commons.beanutils.BeanMap, you can wrap the POJO in a bean map, and when you get or put you can manipulate the fields using reflection. If the getter or setter does not exist we just use field manipulation to get it. Obviously it's NOT a real bean, so that's perfectly OK.
package com.aaa.ejb.common;
import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashSet;
import java.util.Set;
import org.apache.commons.beanutils.BeanMap;
import org.apache.commons.collections.set.UnmodifiableSet;
import org.apache.log4j.Logger;
/**
* I want the bean map to be able to handle a POJO.
* @author gbishop
*/
public final class NoGetterBeanMap extends BeanMap {
private static final Logger LOG = Logger.getLogger(NoGetterBeanMap.class);
/**
* Gets a bean map that can handle writing to a pojo with no getters or setters.
* @param bean
*/
public NoGetterBeanMap(Object bean) {
super(bean);
}
/* (non-Javadoc)
* @see org.apache.commons.beanutils.BeanMap#get(java.lang.Object)
*/
public Object get(Object name) {
Object bean = getBean();
if ( bean != null ) {
Method method = getReadMethod( name );
if ( method != null ) {
try {
return method.invoke( bean, NULL_ARGUMENTS );
}
catch ( IllegalAccessException e ) {
logWarn( e );
}
catch ( IllegalArgumentException e ) {
logWarn( e );
}
catch ( InvocationTargetException e ) {
logWarn( e );
}
catch ( NullPointerException e ) {
logWarn( e );
}
} else {
if(name instanceof String) {
Class<?> c = bean.getClass();
try {
Field datafield = c.getDeclaredField( (String)name );
datafield.setAccessible(true);
return datafield.get(bean);
} catch (SecurityException e) {
throw new IllegalArgumentException( e.getMessage() );
} catch (NoSuchFieldException e) {
throw new IllegalArgumentException( e.getMessage() );
} catch (IllegalAccessException e) {
throw new IllegalArgumentException( e.getMessage() );
}
}
}
}
return null;
}
/* (non-Javadoc)
* @see org.apache.commons.beanutils.BeanMap#put(java.lang.Object, java.lang.Object)
*/
public Object put(Object name, Object value) throws IllegalArgumentException, ClassCastException {
Object bean = getBean();
if ( bean != null ) {
Object oldValue = get( name );
Method method = getWriteMethod( name );
Object newValue = null;
if ( method == null ) {
if(name instanceof String) {//I'm going to try setting the property directly on the bean.
Class<?> c = bean.getClass();
try {
Field datafield = c.getDeclaredField( (String)name );
datafield.setAccessible(true);
datafield.set(bean, value);
newValue = datafield.get(bean);
} catch (SecurityException e) {
throw new IllegalArgumentException( e.getMessage() );
} catch (NoSuchFieldException e) {
throw new IllegalArgumentException( e.getMessage() );
} catch (IllegalAccessException e) {
throw new IllegalArgumentException( e.getMessage() );
}
} else {
throw new IllegalArgumentException( "The bean of type: "+
bean.getClass().getName() + " has no property called: " + name );
}
} else {
try {
Object[] arguments = createWriteMethodArguments( method, value );
method.invoke( bean, arguments );
newValue = get( name );
} catch ( InvocationTargetException e ) {
logInfo( e );
throw new IllegalArgumentException( e.getMessage() );
} catch ( IllegalAccessException e ) {
logInfo( e );
throw new IllegalArgumentException( e.getMessage() );
}
firePropertyChange( name, oldValue, newValue );
}
return oldValue;
}
return null;
}
/* (non-Javadoc)
* @see org.apache.commons.beanutils.BeanMap#keySet()
*/
public Set keySet() {
Class<?> c = getBean().getClass();
Field[] fields = c.getDeclaredFields();
Set<String> keySet = new HashSet<String>(super.keySet());
for(Field f: fields){
if( Modifier.isPublic(f.getModifiers()) && !keySet.contains(f.getName())){
keySet.add(f.getName());
}
}
keySet.remove("class");
return UnmodifiableSet.decorate(keySet);
}
}
The tricky part is factoring up the POJOs to return back, but reflection can help you there:
/**
* Returns a new instance of the specified object. If the object is a bean,
* (serializable, with a default zero argument constructor), the default
* constructor is called. If the object is a Cloneable, it is cloned, if the
* object is a POJO or a nested POJO, it is cloned using the default
* zero argument constructor through reflection. Such objects should only be
* used as transfer objects since their constructors and initialization code
* (if any) have not have been called.
* @param obj
* @return A new copy of the object, it's fields are blank.
*/
public static Object constructBeanOrPOJO(final Object obj) {
Constructor<?> ctor = null;
Object retval = null;
//Try to invoke where it's Serializable and has a public zero argument constructor.
if(obj instanceof Serializable){
try {
ctor = obj.getClass().getConstructor((Class<?>)null);
if(ctor.isAccessible()){
retval = ctor.newInstance();
//LOG.info("Serializable class called with a public constructor.");
return retval;
}
} catch (Exception ignoredTryConeable) {
}
}
//Maybe it's Clonable.
if(obj instanceof Cloneable){
try {
Method clone = obj.getClass().getMethod("clone");
clone.setAccessible(true);
retval = clone.invoke(obj);
//LOG.info("Cloneable class called.");
return retval;
} catch (Exception ignoredTryUnNestedClass) {
}
}
try {
//Maybe it's not a nested class.
ctor = obj.getClass().getDeclaredConstructor((Class<?>)null);
ctor.setAccessible(true);
retval = ctor.newInstance();
//LOG.info("Class called with no public constructor.");
return retval;
} catch (Exception ignoredTryNestedClass) {
}
try {
Constructor[] cs = obj.getClass().getDeclaredConstructors();
for(Constructor<?> c: cs){
if(c.getTypeParameters().length==0){
ctor = c;
ctor.setAccessible(true);
retval = ctor.newInstance();
return retval;
}
}
//Try a nested class class.
Field parent = obj.getClass().getDeclaredField("this$0");
parent.setAccessible(true);
Object outer = (Object) parent.get(obj);
//ctor = (Constructor<? extends Object>) obj.getClass().getConstructors()[0];//NO, getDECLAREDConstructors!!!
ctor = (Constructor<? extends Object>) obj.getClass().getDeclaredConstructor(parent.get(obj).getClass());
ctor.setAccessible(true);
retval = ctor.newInstance(outer);
//LOG.info("Nested class called with no public constructor.");
return retval;
} catch (Exception failure) {
throw new IllegalArgumentException(failure);
}
}
Example code to get a generic bean from a bean, a cloneable, or a POJO:
public List<Object> getGenericEJBData(String tableName, Object desiredFields, Object beanCriteria){
NoGetterBeanMap desiredFieldMap = new NoGetterBeanMap(desiredFields);
NoGetterBeanMap criteriaMap = new NoGetterBeanMap(beanCriteria);
List<Object> data = new ArrayList<Object>();
List<Map<String, Object>> mapData = getGenericEJBData(tableName, desiredFieldMap, criteriaMap);
for (Map<String,Object> row: mapData) {
Object bean = NoGetterBeanMap.constructBeanOrPOJO(desiredFields);//Cool eh?
new NoGetterBeanMap(bean).putAll(row);//Put the data back in too!
data.add(bean);
}
return data;
}
Example usage with EJB:
IGenericBean genericRemote = BeanLocator.lookup(IGenericBean.class);
//This is the minimum required typing.
class DesiredDataPOJO {
public String makename="";//Name matches column and return type.
}
class CriteriaPOJO {
//Names match column and contains criteria values.
public String modelname=model,yearid=year;
}
List<DesiredDataPOJO> data =
genericRemote.getGenericEJBData(ACES_VEHICLE_TABLE, new DesiredDataPOJO(), new CriteriaPOJO() );
for (DesiredDataPOJO o: data) {
makes.add(o.makename);
}
The EJB has an interface like this:
package com.aaa.ejb.common.interfaces;
import java.util.List;
import java.util.Map;
import javax.ejb.Local;
import javax.ejb.Remote;
/**
* @see
* http://trycatchfinally.blogspot.com/2006/03/remote-or-local-interface.html
*
* Note that the local and remote interfaces extend a common business interface.
* Also note that the local and remote interfaces are nested within the business
* interface. I like this model because it reduces the clutter, keeps related
* interfaces together, and eases understanding.
*
* When using dependency injection, you can specify explicitly whether you want
* the remote or local interface. For example:
* @EJB(beanInterface=services.DistrictService.IRemote.class)
* public final void setDistrictService(DistrictService districtService) {
* this.districtService = districtService;
* }
*/
public interface IGenericBean {
@Remote
public interface IRemote extends IGenericBean {
}
@Local
public interface ILocal extends IGenericBean {
}
/**
* Gets a list of beans containing data.
* Requires a table name and pair of beans containing the fields
* to return and the criteria to use.
*
* You can even use anonymous inner classes for the criteria.
* EX: new Object() { public String modelname=model,yearid=year; }
*
* @param tableName
* @param fields
* @param criteria
* @return
*/
public <DesiredFields> List<DesiredFields> getGenericEJBData(String tableName, DesiredFields desiredFields, Object beanCriteria);
}
You can imagine what the ejb implementation looks like, right now we are building prepared statements and calling them but we could use criteria, or something cooler like hibernate or whatever if we wanted.
Here is a rough example (some are NOT going to like this part). In the example we have a single table with data in 3rd normal form. For this to work the bean fields have to match the table column names. The toLowerCase() calls are in case this is a REAL bean being used, which will screw up the name matching (MyField vs. getMyfield). This part could probably be polished quite a bit better. In particular the order by and distinct ought to be flags or something. There are probably other edge conditions that can also happen. Of course I only have to write this ONCE, and for performance, nothing prevents you from having a much more precise receiver of the data for performance.
@Stateless
public class GenericBean implements ILocal, IRemote {
...
/* (non-Javadoc)
* @see com.aaa.ejb.acesvehicle.beans.interfaces.IAcesVehicleBean#getGenericEJBData(java.lang.String, java.util.Map, java.util.Map)
*/
@Override
public List<Map<String, Object>> getGenericEJBData(String tableName,Map<String, Object> desiredFields, Map<String, Object> criteria){
try {
List<Map<String,Object>> dataFieldKeyValuePairs = new ArrayList<Map<String,Object>>();
StringBuilder sql = new StringBuilder("SELECT DISTINCT ");
int selectDistinctLength = sql.length();
for(Object key : desiredFields.keySet()){
if(desiredFields.get(key)!=null) {
sql.append(key).append(", ");
}
}
sql.setLength(sql.length()-2);//Remove last COMMA.
int fieldsLength = sql.length();
sql.append(" FROM ").append(tableName).append(" WHERE ");
String sep = "";//I like this, I like it a lot.
for(Object key : criteria.keySet()){
sql.append(sep);
sql.append(key).append(" = COALESCE(?,").append(key).append(") ");
sep = "AND ";
}
sql.append(" ORDER BY ").append(sql.substring(selectDistinctLength, fieldsLength));
PreparedStatement ps = connection.prepareStatement(sql.toString(), ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
int criteriaCounter=1;
for(Object key : criteria.keySet()){
ps.setObject(criteriaCounter++, criteria.get(key));
}
ResultSet rs = ps.executeQuery();
while (rs.next()) {
Map<String,Object> data = new HashMap<String,Object>();
int columnIndex = rs.getMetaData().getColumnCount();
for(int x=0;x<columnIndex;x++){
String columnName = rs.getMetaData().getColumnName(x+1);
if(desiredFields.keySet().contains(columnName.toLowerCase())){
//Handle bean getters and setters with different case than metadata case.
data.put(columnName.toLowerCase(), rs.getObject(x+1));
} else {
data.put(columnName, rs.getObject(x+1));
}
}
dataFieldKeyValuePairs.add(data);
}
rs.close();
ps.close();
return dataFieldKeyValuePairs;
} catch (SQLException sqle) {
LOG.debug("National database access failed.", sqle);
throw new EJBException(new DataSourceException("Database access failed. \n"
+ "getGenericEJBData()", sqle.getMessage()));
}
}