001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.wicket.proxy;
018
019import java.io.ObjectStreamException;
020import java.lang.reflect.Method;
021import java.util.Arrays;
022import java.util.List;
023
024import org.apache.wicket.WicketRuntimeException;
025import org.apache.wicket.core.util.lang.WicketObjects;
026import org.apache.wicket.model.IModel;
027import org.apache.wicket.proxy.bytebuddy.ByteBuddyProxyFactory;
028import org.apache.wicket.proxy.jdk.JdkProxyFactory;
029import org.apache.wicket.util.io.IClusterable;
030
031/**
032 * A factory class that creates lazy init proxies given a type and a {@link IProxyTargetLocator}
033 * used to retrieve the object the proxy will represent.
034 * <p>
035 * A lazy init proxy waits until the first method invocation before it uses the
036 * {@link IProxyTargetLocator} to retrieve the object to which the method invocation will be
037 * forwarded.
038 * <p>  
039 * This factory creates two kinds of proxies: A standard dynamic proxy when the specified type is an
040 * interface, and a ByteBuddy proxy when the specified type is a concrete class.
041 * <p>
042 * The general use case for such a proxy is to represent a dependency that should not be serialized
043 * with a wicket page or {@link IModel}. The solution is to serialize the proxy and the
044 * {@link IProxyTargetLocator} instead of the dependency, and be able to look up the target object
045 * again when the proxy is deserialized and accessed. A good strategy for achieving this is to have
046 * a static lookup in the {@link IProxyTargetLocator}, this keeps its size small and makes it safe
047 * to serialize.
048 * <p>
049 * Example:
050 * 
051 * <pre>
052 * class UserServiceLocator implements IProxyTargetLocator
053 * {
054 *      public static final IProxyTargetLocator INSTANCE = new UserServiceLocator();
055 * 
056 *      Object locateProxyObject()
057 *      {
058 *              MyApplication app = (MyApplication)Application.get();
059 *              return app.getUserService();
060 *      }
061 * }
062 * 
063 * class UserDetachableModel extends LoadableDetachableModel
064 * {
065 *      private UserService svc;
066 * 
067 *      private long userId;
068 * 
069 *      public UserDetachableModel(long userId, UserService svc)
070 *      {
071 *              this.userId = userId;
072 *              this.svc = svc;
073 *      }
074 * 
075 *      public Object load()
076 *      {
077 *              return svc.loadUser(userId);
078 *      }
079 * }
080 * 
081 * UserService service = LazyInitProxyFactory.createProxy(UserService.class,
082 *      UserServiceLocator.INSTANCE);
083 * 
084 * UserDetachableModel model = new UserDetachableModel(10, service);
085 * 
086 * </pre>
087 * 
088 * The detachable model in the example above follows to good citizen pattern and is easy to unit
089 * test. These are the advantages gained through the use of the lazy init proxies.
090 * 
091 * @author Igor Vaynberg (ivaynberg)
092 * 
093 */
094public class LazyInitProxyFactory
095{
096        /**
097         * Primitive java types and their object wrappers
098         */
099        private static final List<Class<?>> PRIMITIVES = Arrays.asList(String.class, byte.class, Byte.class,
100                short.class, Short.class, int.class, Integer.class, long.class, Long.class, float.class,
101                Float.class, double.class, Double.class, char.class, Character.class, boolean.class,
102                Boolean.class);
103        
104        private static final IProxyFactory JDK_PROXY_FACTORY = new JdkProxyFactory();
105        private static final IProxyFactory CLASS_PROXY_FACTORY = new ByteBuddyProxyFactory();
106
107        /**
108         * Create a lazy init proxy for the specified type. The target object will be located using the
109         * provided locator upon first method invocation.
110         * 
111         * @param type
112         *            type that proxy will represent
113         * 
114         * @param locator
115         *            object locator that will locate the object the proxy represents
116         * 
117         * @return lazily initializable proxy
118         */
119        public static <T> T createProxy(final Class<T> type, final IProxyTargetLocator locator)
120        {
121                if (PRIMITIVES.contains(type) || Enum.class.isAssignableFrom(type))
122                {
123                        // We special-case primitives as sometimes people use these as
124                        // SpringBeans (WICKET-603, WICKET-906). Go figure.
125                        return (T) locator.locateProxyTarget();
126                }
127                else if (type.isInterface())
128                {
129                        return JDK_PROXY_FACTORY.createProxy(type, locator);
130                }
131                else
132                {
133                        return CLASS_PROXY_FACTORY.createProxy(type, locator);
134                }
135        }
136        
137        /**
138         * This interface is used to make the proxy forward writeReplace() call to the handler instead
139         * of invoking it on itself. This allows us to serialize the replacement object instead of the
140         * proxy itself in case the proxy subclass is deserialized on a VM that does not have it
141         * created.
142         * 
143         * @see LazyInitProxyFactory.ProxyReplacement
144         * 
145         * @author Igor Vaynberg (ivaynberg)
146         * 
147         */
148        public interface IWriteReplace
149        {
150                /**
151                 * write replace method as defined by Serializable
152                 * 
153                 * @return object that will replace this object in serialized state
154                 * @throws ObjectStreamException
155                 */
156                Object writeReplace() throws ObjectStreamException;
157        }
158        
159        /**
160         * Object that replaces the proxy when it is serialized. Upon deserialization this object will
161         * create a new proxy with the same locator.
162         * 
163         * @author Igor Vaynberg (ivaynberg)
164         * 
165         */
166        public static final class ProxyReplacement implements IClusterable
167        {
168                private static final long serialVersionUID = 1L;
169
170                private final IProxyTargetLocator locator;
171
172                private final String type;
173
174                /**
175                 * Constructor
176                 * 
177                 * @param type
178                 * @param locator
179                 */
180                public ProxyReplacement(final String type, final IProxyTargetLocator locator)
181                {
182                        this.type = type;
183                        this.locator = locator;
184                }
185
186                private Object readResolve() throws ObjectStreamException
187                {
188                        Class<?> clazz = WicketObjects.resolveClass(type);
189                        if (clazz == null)
190                        {
191                                try
192                                {
193                                        clazz = Class.forName(type, false, Thread.currentThread().getContextClassLoader());
194                                }
195                                catch (ClassNotFoundException ignored1)
196                                {
197                                        try
198                                        {
199                                                clazz = Class.forName(type, false, LazyInitProxyFactory.class.getClassLoader());
200                                        }
201                                        catch (ClassNotFoundException ignored2)
202                                        {
203                                                ClassNotFoundException cause = new ClassNotFoundException(
204                                                                "Could not resolve type [" + type +
205                                                                                "] with the currently configured org.apache.wicket.application.IClassResolver");
206                                                throw new WicketRuntimeException(cause);
207                                        }
208                                }
209                        }
210                        return LazyInitProxyFactory.createProxy(clazz, locator);
211                }
212        }
213
214        /**
215         * Checks if the method is derived from Object.equals()
216         * 
217         * @param method
218         *            method being tested
219         * @return true if the method is derived from Object.equals(), false otherwise
220         */
221        public static boolean isEqualsMethod(final Method method)
222        {
223                return (method.getReturnType() == boolean.class) &&
224                        (method.getParameterTypes().length == 1) &&
225                        (method.getParameterTypes()[0] == Object.class) && method.getName().equals("equals");
226        }
227
228        /**
229         * Checks if the method is derived from Object.hashCode()
230         * 
231         * @param method
232         *            method being tested
233         * @return true if the method is defined from Object.hashCode(), false otherwise
234         */
235        public static boolean isHashCodeMethod(final Method method)
236        {
237                return (method.getReturnType() == int.class) && (method.getParameterTypes().length == 0) &&
238                        method.getName().equals("hashCode");
239        }
240
241        /**
242         * Checks if the method is derived from Object.toString()
243         * 
244         * @param method
245         *            method being tested
246         * @return true if the method is defined from Object.toString(), false otherwise
247         */
248        public static boolean isToStringMethod(final Method method)
249        {
250                return (method.getReturnType() == String.class) &&
251                        (method.getParameterTypes().length == 0) && method.getName().equals("toString");
252        }
253
254        /**
255         * Checks if the method is derived from Object.finalize()
256         * 
257         * @param method
258         *            method being tested
259         * @return true if the method is defined from Object.finalize(), false otherwise
260         */
261        public static boolean isFinalizeMethod(final Method method)
262        {
263                return (method.getReturnType() == void.class) && (method.getParameterTypes().length == 0) &&
264                        method.getName().equals("finalize");
265        }
266
267        /**
268         * Checks if the method is the writeReplace method
269         * 
270         * @param method
271         *            method being tested
272         * @return true if the method is the writeReplace method, false otherwise
273         */
274        public static boolean isWriteReplaceMethod(final Method method)
275        {
276                return (method.getReturnType() == Object.class) &&
277                        (method.getParameterTypes().length == 0) && method.getName().equals("writeReplace");
278        }       
279}