Monday, July 11, 2005

POJO Binding

I managed to make JGoodies Binding works with simple POJO. I create JavaBeanAdapter class to do that. Suppose that you have this POJO:
<code>
public class Pojo {
private String name;
public void setName(String name) { this.name = name; }
public String getName() { return name; }
}
</code>
Then, wrap the POJO:
<code>
Pojo javaBean = JavaBeanAdapter.wrap(new Pojo());
</code>
That's all. I usually do this in my projects that use JGoodies Binding.
<code>
PresentationModel model = new PresentationModel(JavaBeanAdapter.wrap(new Pojo());
</code>
Here is the complete source code for JavaBeanAdapter.
<code>
package com.katalis.commons.util;

import net.sf.cglib.core.DefaultNamingPolicy;
import net.sf.cglib.core.NamingPolicy;
import net.sf.cglib.core.Predicate;
import net.sf.cglib.proxy.Callback;
import net.sf.cglib.proxy.CallbackFilter;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import org.apache.commons.beanutils.PropertyUtils;

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
* Java Bean adapter using CGLIB. Adapted from <a href="http://cglib.sourceforge.net/xref/samples/Beans.html">http://cglib.sourceforge.net/xref/samples/Beans.html</a>.
* Adapt POJO to meet JavaBean specification. All generated objects will implement {@link Observable}.
* <p/>
* Expected use of this adapter is as follow:<pre>
*      Foo javaBean = JavaBeanAdapter.wrap(new Foo());
* </pre>
* <p/>
* All object graph of the pojo will be wrapped. Exceptions are those of standard java packages.
* <p/>
* Note: Great care must be excercised if you modify the pojo after wrapping. The most obvious is that
* the symetric contract of {@link Object#equals(Object)} will be violated. Example:<pre>
*      javaBean.setFoo("foo");
*      pojo.setFoo("another");
*      javaBean.equals(pojo) //true
*      pojo.equals(javaBean) // false
* </pre>
* The safest approach is to use the generated java bean instead.
*
* @author Thomas Edwin Santosa
*/
public class JavaBeanAdapter {
// ------------------------------ FIELDS ------------------------------

private static final Logger log = Logger.getLogger(JavaBeanAdapter.class.getPackage().getName());
private static final Map cache = Collections.synchronizedMap(new WeakHashMap());
private static final InternalCallbackFilter INTERNAL_CALLBACK_FILTER = new InternalCallbackFilter();
private static final NamingPolicy namingPolicy = new JBNamingPolicy();
private static final Class[] INTERFACES = new Class[]{Observable.class};
private Object pojo;
private final PropertyChangeSupport propertySupport;
private final GetterInterceptor getterInterceptor = new GetterInterceptor();
private final SetterInterceptor setterInterceptor = new SetterInterceptor();
private final AddListenerInterceptor addListenerInterceptor = new AddListenerInterceptor();
private final RemoveListenerInterceptor removeListenerInterceptor = new RemoveListenerInterceptor();
private final DelegateInterceptor delegateInterceptor = new DelegateInterceptor();
private final boolean multicast;

// -------------------------- STATIC METHODS --------------------------

/**
* Wrap the pojo using with no multicast .
*
* @param pojo plain old java object
* @return java bean
* @see #wrap(Object, boolean)
*/
public static Object wrap(Object pojo) {
return wrap(pojo, false);
}

/**
* Wrap the pojo. The pojo will be enhanced so that all setters will fire property change support. This method will try
* its best to synchronize the generated java bean's contents with those of the pojo. If that fails, it will log a
* warning.
*
* @param pojo      plain old java object
* @param multicast whether the property change event will be multicast or not
* @return JavaBean
* @throws IllegalArgumentException if the pojo have been wrapped already
*/
public static Object wrap(Object pojo, boolean multicast) {
if (Enhancer.isEnhanced(pojo.getClass()) && pojo.getClass().getName().endsWith("JavaBeanAdapter")) {
throw new IllegalArgumentException("The pojo is already wrapped.");
}

Object bean;

IdentitySupport key = new IdentitySupport(pojo);
bean = cache.get(key);
if (bean == null) {
JavaBeanAdapter adapter = new JavaBeanAdapter(pojo, multicast);
Enhancer e = new Enhancer();
e.setSuperclass(pojo.getClass());
e.setNamingPolicy(namingPolicy);
e.setInterfaces(INTERFACES);
e.setCallbacks(
        new Callback[]{
        adapter.addListenerInterceptor,
        adapter.removeListenerInterceptor,
        adapter.getterInterceptor,
        adapter.setterInterceptor,
        adapter.delegateInterceptor
        });
e.setCallbackFilter(INTERNAL_CALLBACK_FILTER);
bean = e.create();
try {
PropertyUtils.copyProperties(bean, pojo);
} catch (IllegalAccessException e1) {
log.log(Level.WARNING, "The caller does not have access to the property accessor method.", e1);
} catch (InvocationTargetException e1) {
log.log(Level.WARNING, "The property accessor method throws an exception.", e1);
} catch (NoSuchMethodException e1) {
log.log(Level.WARNING, "an accessor method for this propety cannot be found.", e1);
}
cache.put(key, bean);
}
return bean;
}

private static boolean isSetter(Method method) {
String name = method.getName();
return name.startsWith("set") &&
        isVoidAndSingleArgument(method);
}

private static boolean isVoidAndSingleArgument(Method method) {
return method.getParameterTypes().length == 1 && method.getReturnType() == Void.TYPE;
}

private static boolean isNonJavaGetter(Method method) {
String name = method.getName();
boolean possibleGetter = name.startsWith("get") && method.getParameterTypes().length == 0;
if (possibleGetter) {
Class returnType = method.getReturnType();
return !(returnType.isPrimitive() || returnType.isArray() || returnType.getName().startsWith("java"));
}
return false;
}

private static String extractPropertyName(String name) {
char[] propName = name.substring(3).toCharArray();
propName[0] = Character.toLowerCase(propName[0]);
String propNameStr = String.valueOf(propName);
return propNameStr;
}

// --------------------------- CONSTRUCTORS ---------------------------

private JavaBeanAdapter(Object delegate, boolean multicast) {
assert delegate != null : "Delegate must be not null";
pojo = delegate;
propertySupport = new PropertyChangeSupport(delegate);
this.multicast = multicast;
}

// -------------------------- INNER CLASSES --------------------------

private static class IdentitySupport {
private Object object;

IdentitySupport(Object object) {
this.object = object;
}

public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof IdentitySupport)) return false;

final IdentitySupport identitySupport = (IdentitySupport) o;

return object == identitySupport.object;
}

public int hashCode() {
return System.identityHashCode(object);
}
}

private class GetterInterceptor
        implements MethodInterceptor {
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy)
        throws Throwable {
Object retValFromSuper;
Object getterResult = methodProxy.invoke(pojo, objects);
if (getterResult != null && !Enhancer.isEnhanced(getterResult.getClass())) {
retValFromSuper = wrap(getterResult, false);
} else {
retValFromSuper = getterResult;
}
return retValFromSuper;
}
}

private class SetterInterceptor
        implements MethodInterceptor {
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy)
        throws Throwable {
String name = method.getName();
String propNameStr = null;
Object oldValue = null;
Object newValue = null;
if (!multicast) {
propNameStr = extractPropertyName(name);
if (PropertyUtils.isReadable(pojo, propNameStr)) {
oldValue = PropertyUtils.getProperty(pojo, propNameStr);
}
newValue = objects[0];
}
methodProxy.invoke(pojo, objects);
// we try to minimize the symetric issue
methodProxy.invokeSuper(o, objects);

propertySupport.firePropertyChange(propNameStr, oldValue, newValue);
if (log.isLoggable(Level.FINE)) {
log.fine(name + " called");
}
return null;
}
}

private class AddListenerInterceptor
        implements MethodInterceptor {
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy)
        throws Throwable {
propertySupport.addPropertyChangeListener((PropertyChangeListener) objects[0]);
return null;
}
}

private class RemoveListenerInterceptor
        implements MethodInterceptor {
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy)
        throws Throwable {
propertySupport.removePropertyChangeListener((PropertyChangeListener) objects[0]);
return null;
}
}

private class DelegateInterceptor
        implements MethodInterceptor {
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy)
        throws Throwable {
return methodProxy.invoke(pojo, objects);
}
}

private static class InternalCallbackFilter
        implements CallbackFilter {
public int accept(Method method) {
int result;
String name = method.getName();

if ("addPropertyChangeListener".equals(name) && isVoidAndSingleArgument(method)) {
result = 0;
} else if ("removePropertyChangeListener".equals(name) && isVoidAndSingleArgument(method)) {
result = 1;
} else if (isNonJavaGetter(method)) {
result = 2;
} else if (isSetter(method)) {
result = 3;
} else {
result = 4;
}
if (log.isLoggable(Level.FINE)) {
log.fine(method + ":" + result);
}
return result;
}
}

private static class JBNamingPolicy
        extends DefaultNamingPolicy {
public String getClassName(String prefix, String source, Object key, Predicate predicate) {
return super.getClassName(prefix, source, key, predicate) + "JavaBeanAdapter";
}
}
}
</code>
Here is the code for Observable:
<code>
package com.katalis.commons.util;

import java.beans.PropertyChangeListener;

/**
* Describes objects that provide bound properties as specified in the
* <a href="http://java.sun.com/products/javabeans/docs/spec.html">Java
* Bean Secification</a>.
*
* @author  Karsten Lentzsch
*
* @see     java.beans.PropertyChangeListener
* @see     java.beans.PropertyChangeEvent
* @see     java.beans.PropertyChangeSupport
*/

public interface Observable {
   

    /**
     * Adds a <code>PropertyChangeListener</code> to the listener list.
     * The listener is registered for all bound properties of this class.
     *
     * @param listener the PropertyChangeListener to be added
     *
     * @see #removePropertyChangeListener(java.beans.PropertyChangeListener)
     */
    void addPropertyChangeListener(PropertyChangeListener listener);


    /**
     * Removes a <code>PropertyChangeListener</code> from the listener list.
     * This method should be used to remove PropertyChangeListeners that were
     * registered for all bound properties of this class.
     *
     * @param listener the PropertyChangeListener to be removed
     *
     * @see #addPropertyChangeListener(PropertyChangeListener)
     */
    void removePropertyChangeListener(PropertyChangeListener listener);


}
</code>
You will need Commons-Beanutils from Apache Jakarta on the, I forgot the site. You can google them. I think you will need commons-logging as well. Don't forget to add CGLIB!

No comments: