org.apache.wicket.proxy.LazyInitProxyFactory.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.wicket.proxy.LazyInitProxyFactory.java

Source

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 org.apache.wicket.proxy;

import java.io.ObjectStreamException;
import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.List;

import net.sf.cglib.core.DefaultNamingPolicy;
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 net.sf.cglib.proxy.NoOp;

import org.apache.wicket.Application;
import org.apache.wicket.WicketRuntimeException;
import org.apache.wicket.core.util.lang.WicketObjects;
import org.apache.wicket.model.IModel;
import org.apache.wicket.proxy.objenesis.ObjenesisProxyFactory;
import org.apache.wicket.util.io.IClusterable;
import org.apache.wicket.util.string.Strings;

/**
 * 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 LoadableDetachableModel
 * {
 *    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 {
    /**
     * Primitive java types and their object wrappers
     */
    @SuppressWarnings({ "unchecked", "rawtypes" })
    private static final List PRIMITIVES = Arrays.asList(String.class, byte.class, Byte.class, short.class,
            Short.class, int.class, Integer.class, long.class, Long.class, float.class, Float.class, double.class,
            Double.class, char.class, Character.class, boolean.class, Boolean.class);

    private static final int CGLIB_CALLBACK_NO_OVERRIDE = 0;
    private static final int CGLIB_CALLBACK_HANDLER = 1;

    private static final boolean IS_OBJENESIS_AVAILABLE = isObjenesisAvailable();

    /**
     * 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(final Class<?> type, final IProxyTargetLocator locator) {
        if (PRIMITIVES.contains(type) || Enum.class.isAssignableFrom(type)) {
            // We special-case primitives as sometimes people use these as
            // SpringBeans (WICKET-603, WICKET-906). Go figure.
            return locator.locateProxyTarget();
        } else if (type.isInterface()) {
            JdkHandler handler = new JdkHandler(type, locator);

            try {
                return Proxy.newProxyInstance(resolveClassLoader(),
                        new Class[] { type, Serializable.class, ILazyInitProxy.class, IWriteReplace.class },
                        handler);
            } catch (IllegalArgumentException e) {
                /*
                 * STW: In some clustering environments it appears the context classloader fails to
                 * load the proxied interface (currently seen in BEA WLS 9.x clusters). If this
                 * happens, we can try and fall back to the classloader (current) that actually
                 * loaded this class.
                 */
                return Proxy.newProxyInstance(LazyInitProxyFactory.class.getClassLoader(),
                        new Class[] { type, Serializable.class, ILazyInitProxy.class, IWriteReplace.class },
                        handler);
            }

        } else if (IS_OBJENESIS_AVAILABLE && !hasNoArgConstructor(type)) {
            return ObjenesisProxyFactory.createProxy(type, locator, WicketNamingPolicy.INSTANCE);
        } else {
            CGLibInterceptor handler = new CGLibInterceptor(type, locator);

            Callback[] callbacks = new Callback[2];
            callbacks[CGLIB_CALLBACK_NO_OVERRIDE] = SerializableNoOpCallback.INSTANCE;
            callbacks[CGLIB_CALLBACK_HANDLER] = handler;

            Enhancer e = new Enhancer();
            e.setClassLoader(resolveClassLoader());
            e.setInterfaces(new Class[] { Serializable.class, ILazyInitProxy.class, IWriteReplace.class });
            e.setSuperclass(type);
            e.setCallbackFilter(NoOpForProtectedMethodsCGLibCallbackFilter.INSTANCE);
            e.setCallbacks(callbacks);
            e.setNamingPolicy(WicketNamingPolicy.INSTANCE);

            return e.create();
        }
    }

    private static ClassLoader resolveClassLoader() {
        ClassLoader classLoader = null;
        if (Application.exists()) {
            classLoader = Application.get().getApplicationSettings().getClassResolver().getClassLoader();
        }

        if (classLoader == null) {
            classLoader = Thread.currentThread().getContextClassLoader();
        }

        return classLoader;
    }

    /**
    * 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 object instead of the
    * proxy itself in case the proxy subclass is deserialized on a VM that does not have it
    * created.
    * 
    * @see LazyInitProxyFactory.ProxyReplacement
    * 
    * @author Igor Vaynberg (ivaynberg)
    * 
    */
    public 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 IClusterable {
        private static final long serialVersionUID = 1L;

        private final IProxyTargetLocator locator;

        private final String type;

        /**
         * Constructor
         * 
         * @param type
         * @param locator
         */
        public ProxyReplacement(final String type, final IProxyTargetLocator locator) {
            this.type = type;
            this.locator = locator;
        }

        private Object readResolve() throws ObjectStreamException {
            Class<?> clazz = WicketObjects.resolveClass(type);
            if (clazz == null) {
                try {
                    clazz = Class.forName(type, false, Thread.currentThread().getContextClassLoader());
                } catch (ClassNotFoundException ignored1) {
                    try {
                        clazz = Class.forName(type, false, LazyInitProxyFactory.class.getClassLoader());
                    } catch (ClassNotFoundException ignored2) {
                        ClassNotFoundException cause = new ClassNotFoundException("Could not resolve type [" + type
                                + "] with the currently configured org.apache.wicket.application.IClassResolver");
                        throw new WicketRuntimeException(cause);
                    }
                }
            }
            return LazyInitProxyFactory.createProxy(clazz, locator);
        }
    }

    /**
     * Method interceptor for proxies representing concrete object not backed by an interface. These
     * proxies are represented by cglib proxies.
     * 
     * @author Igor Vaynberg (ivaynberg)
     * 
     */
    public abstract static class AbstractCGLibInterceptor
            implements MethodInterceptor, ILazyInitProxy, Serializable, IWriteReplace {
        private static final long serialVersionUID = 1L;

        protected final IProxyTargetLocator locator;

        protected final 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 AbstractCGLibInterceptor(final Class<?> type, final IProxyTargetLocator locator) {
            super();
            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)
         */
        @Override
        public Object intercept(final Object object, final Method method, final Object[] args,
                final 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 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 org.apache.wicket.proxy.ILazyInitProxy#getObjectLocator()
         */
        @Override
        public IProxyTargetLocator getObjectLocator() {
            return locator;
        }
    }

    /**
     * Method interceptor for proxies representing concrete object not backed by an interface. These
     * proxies are representing by cglib proxies.
     *
     * @author Igor Vaynberg (ivaynberg)
     */
    protected static class CGLibInterceptor extends AbstractCGLibInterceptor {
        public CGLibInterceptor(Class<?> type, IProxyTargetLocator locator) {
            super(type, locator);
        }

        /**
         * @see org.apache.wicket.proxy.LazyInitProxyFactory.IWriteReplace#writeReplace()
         */
        @Override
        public Object writeReplace() throws ObjectStreamException {
            return new ProxyReplacement(typeName, locator);
        }
    }

    /**
     * Serializable implementation of the NoOp callback.
     */
    public static class SerializableNoOpCallback implements NoOp, Serializable {
        private static final long serialVersionUID = 1L;

        private static final NoOp INSTANCE = new SerializableNoOpCallback();
    }

    /**
     * CGLib callback filter which does not intercept protected methods.
     * 
     * Protected methods need to be called with invokeSuper() instead of invoke().
     * When invoke() is called on a protected method, it throws an "IllegalArgumentException:
     * Protected method" exception.
     * That being said, we do not need to intercept the protected methods so this callback filter
     * is designed to use a NoOp callback for protected methods.
     * 
     * @see <a href="http://comments.gmane.org/gmane.comp.java.cglib.devel/720">Discussion about
     * this very issue in Spring AOP</a>
     * @see <a href="https://github.com/wicketstuff/core/wiki/SpringReference">The WicketStuff
     * SpringReference project which worked around this issue</a>
     */
    private static class NoOpForProtectedMethodsCGLibCallbackFilter implements CallbackFilter {
        private static final CallbackFilter INSTANCE = new NoOpForProtectedMethodsCGLibCallbackFilter();

        @Override
        public int accept(Method method) {
            if (Modifier.isProtected(method.getModifiers())) {
                return CGLIB_CALLBACK_NO_OVERRIDE;
            } else {
                return CGLIB_CALLBACK_HANDLER;
            }
        }
    }

    /**
     * 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 static final long serialVersionUID = 1L;

        private final IProxyTargetLocator locator;

        private final 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(final Class<?> type, final IProxyTargetLocator locator) {
            super();
            this.locator = locator;
            typeName = type.getName();
        }

        /**
         * @see java.lang.reflect.InvocationHandler#invoke(java.lang.Object,
         *      java.lang.reflect.Method, java.lang.Object[])
         */
        @Override
        public Object invoke(final Object proxy, final Method method, final 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 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 {
                method.setAccessible(true);
                return method.invoke(target, args);
            } catch (InvocationTargetException e) {
                throw e.getTargetException();
            }
        }

        /**
         * @see org.apache.wicket.proxy.ILazyInitProxy#getObjectLocator()
         */
        @Override
        public IProxyTargetLocator getObjectLocator() {
            return locator;
        }

        /**
         * @see org.apache.wicket.proxy.LazyInitProxyFactory.IWriteReplace#writeReplace()
         */
        @Override
        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
     */
    public static boolean isEqualsMethod(final 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
     */
    public static boolean isHashCodeMethod(final 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
     */
    public static boolean isToStringMethod(final 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
     */
    public static boolean isFinalizeMethod(final 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
     */
    public static boolean isWriteReplaceMethod(final Method method) {
        return (method.getReturnType() == Object.class) && (method.getParameterTypes().length == 0)
                && method.getName().equals("writeReplace");
    }

    public static final class WicketNamingPolicy extends DefaultNamingPolicy {
        public static final WicketNamingPolicy INSTANCE = new WicketNamingPolicy();

        private WicketNamingPolicy() {
            super();
        }

        @Override
        public String getClassName(final String prefix, final String source, final Object key,
                final Predicate names) {
            int lastIdxOfDot = prefix.lastIndexOf('.');
            String packageName = prefix.substring(0, lastIdxOfDot);
            String className = prefix.substring(lastIdxOfDot + 1);
            String newPrefix = packageName + ".Wicket_Proxy_" + className;
            return super.getClassName(newPrefix, source, key, names);
        }
    }

    private static boolean hasNoArgConstructor(Class<?> type) {
        for (Constructor<?> constructor : type.getDeclaredConstructors()) {
            if (constructor.getParameterTypes().length == 0)
                return true;
        }

        return false;
    }

    private static boolean isObjenesisAvailable() {
        try {
            Class.forName("org.objenesis.ObjenesisStd");
            return true;
        } catch (Exception ignored) {
            return false;
        }
    }
}