blob: 6ada8ef7fb4fd7a50661e46b4e98885fa3a48cc9 [file] [log] [blame]
/*
* $Id$
* $Revision$
* $Date$
*
* ==============================================================================
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package wicket.proxy;
import java.io.InvalidClassException;
import java.io.ObjectStreamException;
import java.io.Serializable;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import wicket.model.IModel;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
/**
* A factory class that creates lazy init proxies given a type and a
* {@link IProxyTargetLocator} used to retrieve the object the proxy will
* represent.
* <p>
* A lazy init proxy waits until the first method invocation before it uses the
* {@link IProxyTargetLocator} to retrieve the object to which the method
* invocation will be forwarded.
* <p>
* This factory creates two kinds of proxies: A standard dynamic proxy when the
* specified type is an interface, and a CGLib proxy when the specified type is
* a concrete class.
* <p>
* The general use case for such a proxy is to represent a dependency that
* should not be serialized with a wicket page or {@link IModel}. The solution
* is to serialize the proxy and the {@link IProxyTargetLocator} instead of the
* dependency, and be able to look up the target object again when the proxy is
* deserialized and accessed. A good strategy for achieving this is to have a
* static lookup in the {@link IProxyTargetLocator}, this keeps its size small
* and makes it safe to serialize.
* <p>
* Example:
*
* <pre>
* class UserServiceLocator implements IProxyTargetLocator
* {
*
* public static final IProxyTargetLocator INSTANCE = new UserServiceLocator();
*
* Object locateProxyObject()
* {
* MyApplication app = (MyApplication) Application.get();
* return app.getUserService();
* }
* }
*
* class UserDetachableModel extends LoadableModel
* {
* private UserService svc;
*
* private long userId;
*
* public UserDetachableModel(long userId, UserService svc)
* {
* this.userId = userId;
* this.svc = svc;
* }
*
* public Object load()
* {
* return svc.loadUser(userId);
* }
* }
*
* UserService service = LazyInitProxyFactory.createProxy(UserService.class,
* UserServiceLocator.INSTANCE);
*
* UserDetachableModel model = new UserDetachableModel(10, service);
*
* </pre>
*
* The detachable model in the example above follows to good citizen pattern and
* is easy to unit test. These are the advantages gained through the use of the
* lazy init proxies.
*
* @author Igor Vaynberg (ivaynberg)
*
*/
public class LazyInitProxyFactory
{
/**
* Create a lazy init proxy for the specified type. The target object will
* be located using the provided locator upon first method invocation.
*
* @param type
* type that proxy will represent
*
* @param locator
* object locator that will locate the object the proxy
* represents
*
* @return lazily initializable proxy
*/
public static Object createProxy(Class type, IProxyTargetLocator locator)
{
if (type.isInterface())
{
JdkHandler handler = new JdkHandler(type, locator);
return Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
new Class[] {type, Serializable.class, ILazyInitProxy.class,
IWriteReplace.class}, handler);
}
else
{
CGLibInterceptor handler = new CGLibInterceptor(type, locator);
Enhancer e = new Enhancer();
e.setInterfaces(new Class[] {Serializable.class, ILazyInitProxy.class,
IWriteReplace.class});
e.setSuperclass(type);
e.setCallback(handler);
return e.create();
}
}
/**
* This interface is used to make the proxy forward writeReplace() call to
* the handler instead of invoking it on itself. This allows us to serialize
* the replacement objet instead of the proxy itself in case the proxy
* subclass is deserialized on a VM that does not have it created.
*
* @see ProxyReplacement
*
* @author Igor Vaynberg (ivaynberg)
*
*/
protected static interface IWriteReplace
{
/**
* write replace method as defined by Serializable
*
* @return object that will replace this object in serialized state
* @throws ObjectStreamException
*/
Object writeReplace() throws ObjectStreamException;
}
/**
* Object that replaces the proxy when it is serialized. Upon
* deserialization this object will create a new proxy with the same
* locator.
*
* @author Igor Vaynberg (ivaynberg)
*
*/
static class ProxyReplacement implements Serializable
{
private IProxyTargetLocator locator;
private String type;
/**
* Constructor
*
* @param type
* @param locator
*/
public ProxyReplacement(String type, IProxyTargetLocator locator)
{
this.type = type;
this.locator = locator;
}
private Object readResolve() throws ObjectStreamException
{
Class clazz;
try
{
clazz = Class.forName(type);
}
catch (ClassNotFoundException e)
{
throw new InvalidClassException(type, "could not resolve class ["
+ type + "] when deserializing proxy");
}
return LazyInitProxyFactory.createProxy(clazz, locator);
}
}
/**
* Method interceptor for proxies representing concrete object not backed by
* an interface. These proxies are representing by cglib proxies.
*
* @author Igor Vaynberg (ivaynberg)
*
*/
private static class CGLibInterceptor implements MethodInterceptor, ILazyInitProxy,
Serializable, IWriteReplace
{
private IProxyTargetLocator locator;
private String typeName;
private transient Object target;
/**
* Constructor
*
* @param type
* class of the object this proxy was created for
*
* @param locator
* object locator used to locate the object this proxy
* represents
*/
public CGLibInterceptor(Class type, IProxyTargetLocator locator)
{
super();
this.typeName = type.getName();
this.locator = locator;
}
/**
* @see net.sf.cglib.proxy.MethodInterceptor#intercept(java.lang.Object,
* java.lang.reflect.Method, java.lang.Object[],
* net.sf.cglib.proxy.MethodProxy)
*/
public Object intercept(Object object, Method method, Object[] args,
MethodProxy proxy) throws Throwable
{
if (isFinalizeMethod(method))
{
// swallow finalize call
return null;
}
else if (isEqualsMethod(method))
{
return (equals(args[0])) ? Boolean.TRUE : Boolean.FALSE;
}
else if (isHashCodeMethod(method))
{
return new Integer(this.hashCode());
}
else if (isToStringMethod(method))
{
return toString();
}
else if (isWriteReplaceMethod(method))
{
return writeReplace();
}
else if (method.getDeclaringClass().equals(ILazyInitProxy.class))
{
return getObjectLocator();
}
if (target == null)
{
target = locator.locateProxyTarget();
}
return proxy.invoke(target, args);
}
/**
* @see wicket.proxy.ILazyInitProxy#getObjectLocator()
*/
public IProxyTargetLocator getObjectLocator()
{
return locator;
}
/**
* @see wicket.proxy.LazyInitProxyFactory.IWriteReplace#writeReplace()
*/
public Object writeReplace() throws ObjectStreamException
{
return new ProxyReplacement(typeName, locator);
}
}
/**
* Invocation handler for proxies representing interface based object. For
* interface backed objects dynamic jdk proxies are used.
*
* @author Igor Vaynberg (ivaynberg)
*
*/
private static class JdkHandler implements InvocationHandler, ILazyInitProxy,
Serializable, IWriteReplace
{
private IProxyTargetLocator locator;
private String typeName;
private transient Object target;
/**
* Constructor
*
* @param type
* class of object this handler will represent
*
* @param locator
* object locator used to locate the object this proxy
* represents
*/
public JdkHandler(Class type, IProxyTargetLocator locator)
{
super();
this.locator = locator;
this.typeName = type.getName();
}
/**
* @see java.lang.reflect.InvocationHandler#invoke(java.lang.Object,
* java.lang.reflect.Method, java.lang.Object[])
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
{
if (isFinalizeMethod(method))
{
// swallow finalize call
return null;
}
else if (isEqualsMethod(method))
{
return (equals(args[0])) ? Boolean.TRUE : Boolean.FALSE;
}
else if (isHashCodeMethod(method))
{
return new Integer(this.hashCode());
}
else if (isToStringMethod(method))
{
return toString();
}
else if (method.getDeclaringClass().equals(ILazyInitProxy.class))
{
return getObjectLocator();
}
else if (isWriteReplaceMethod(method))
{
return writeReplace();
}
if (target == null)
{
target = locator.locateProxyTarget();
}
try
{
return method.invoke(target, args);
}
catch (InvocationTargetException e)
{
throw e.getTargetException();
}
}
/**
* @see wicket.proxy.ILazyInitProxy#getObjectLocator()
*/
public IProxyTargetLocator getObjectLocator()
{
return locator;
}
/**
* @see wicket.proxy.LazyInitProxyFactory.IWriteReplace#writeReplace()
*/
public Object writeReplace() throws ObjectStreamException
{
return new ProxyReplacement(typeName, locator);
}
}
/**
* Checks if the method is derived from Object.equals()
*
* @param method
* method being tested
* @return true if the method is derived from Object.equals(), false
* otherwise
*/
protected static boolean isEqualsMethod(Method method)
{
return method.getReturnType() == boolean.class
&& method.getParameterTypes().length == 1
&& method.getParameterTypes()[0] == Object.class
&& method.getName().equals("equals");
}
/**
* Checks if the method is derived from Object.hashCode()
*
* @param method
* method being tested
* @return true if the method is defined from Object.hashCode(), false
* otherwise
*/
protected static boolean isHashCodeMethod(Method method)
{
return method.getReturnType() == int.class
&& method.getParameterTypes().length == 0
&& method.getName().equals("hashCode");
}
/**
* Checks if the method is derived from Object.toString()
*
* @param method
* method being tested
* @return true if the method is defined from Object.toString(), false
* otherwise
*/
protected static boolean isToStringMethod(Method method)
{
return method.getReturnType() == String.class
&& method.getParameterTypes().length == 0
&& method.getName().equals("toString");
}
/**
* Checks if the method is derived from Object.finalize()
*
* @param method
* method being tested
* @return true if the method is defined from Object.finalize(), false
* otherwise
*/
protected static boolean isFinalizeMethod(Method method)
{
return method.getReturnType() == void.class
&& method.getParameterTypes().length == 0
&& method.getName().equals("finalize");
}
/**
* Checks if the method is the writeReplace method
*
* @param method
* method being tested
* @return true if the method is the writeReplace method, false otherwise
*/
protected static boolean isWriteReplaceMethod(Method method)
{
return method.getReturnType() == Object.class
&& method.getParameterTypes().length == 0
&& method.getName().equals("writeReplace");
}
}