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.io.Serializable;
021import java.lang.reflect.Method;
022import java.util.Arrays;
023import java.util.List;
024
025import org.apache.wicket.WicketRuntimeException;
026import org.apache.wicket.core.util.lang.WicketObjects;
027import org.apache.wicket.model.IModel;
028import org.apache.wicket.proxy.bytebuddy.ByteBuddyProxyFactory;
029import org.apache.wicket.proxy.cglib.CglibProxyFactory;
030import org.apache.wicket.proxy.jdk.JdkProxyFactory;
031import org.apache.wicket.util.io.IClusterable;
032import org.slf4j.Logger;
033import org.slf4j.LoggerFactory;
034
035import net.sf.cglib.core.DefaultNamingPolicy;
036import net.sf.cglib.proxy.MethodInterceptor;
037import net.sf.cglib.proxy.MethodProxy;
038import net.sf.cglib.proxy.NoOp;
039
040/**
041 * A factory class that creates lazy init proxies given a type and a {@link IProxyTargetLocator}
042 * used to retrieve the object the proxy will represent.
043 * <p>
044 * A lazy init proxy waits until the first method invocation before it uses the
045 * {@link IProxyTargetLocator} to retrieve the object to which the method invocation will be
046 * forwarded.
047 * <p>
048 * This factory creates two kinds of proxies: A standard dynamic proxy when the specified type is an
049 * interface, and a cglib proxy when the specified type is a concrete class.
050 * <p>
051 * The general use case for such a proxy is to represent a dependency that should not be serialized
052 * with a wicket page or {@link IModel}. The solution is to serialize the proxy and the
053 * {@link IProxyTargetLocator} instead of the dependency, and be able to look up the target object
054 * again when the proxy is deserialized and accessed. A good strategy for achieving this is to have
055 * a static lookup in the {@link IProxyTargetLocator}, this keeps its size small and makes it safe
056 * to serialize.
057 * <p>
058 * Example:
059 * 
060 * <pre>
061 * class UserServiceLocator implements IProxyTargetLocator
062 * {
063 *      public static final IProxyTargetLocator INSTANCE = new UserServiceLocator();
064 * 
065 *      Object locateProxyObject()
066 *      {
067 *              MyApplication app = (MyApplication)Application.get();
068 *              return app.getUserService();
069 *      }
070 * }
071 * 
072 * class UserDetachableModel extends LoadableDetachableModel
073 * {
074 *      private UserService svc;
075 * 
076 *      private long userId;
077 * 
078 *      public UserDetachableModel(long userId, UserService svc)
079 *      {
080 *              this.userId = userId;
081 *              this.svc = svc;
082 *      }
083 * 
084 *      public Object load()
085 *      {
086 *              return svc.loadUser(userId);
087 *      }
088 * }
089 * 
090 * UserService service = LazyInitProxyFactory.createProxy(UserService.class,
091 *      UserServiceLocator.INSTANCE);
092 * 
093 * UserDetachableModel model = new UserDetachableModel(10, service);
094 * 
095 * </pre>
096 * 
097 * The detachable model in the example above follows to good citizen pattern and is easy to unit
098 * test. These are the advantages gained through the use of the lazy init proxies.
099 * 
100 * @author Igor Vaynberg (ivaynberg)
101 * 
102 */
103public class LazyInitProxyFactory
104{
105        private static final Logger log = LoggerFactory.getLogger(LazyInitProxyFactory.class);
106
107        /**
108         * Primitive java types and their object wrappers
109         */
110        private static final List<Class<?>> PRIMITIVES = Arrays.asList(String.class, byte.class, Byte.class,
111                short.class, Short.class, int.class, Integer.class, long.class, Long.class, float.class,
112                Float.class, double.class, Double.class, char.class, Character.class, boolean.class,
113                Boolean.class);
114        
115        private static final IProxyFactory JDK_PROXY_FACTORY = new JdkProxyFactory();
116        private static final IProxyFactory CLASS_PROXY_FACTORY = initProxyFactory();
117
118    private static IProxyFactory initProxyFactory() {
119                IProxyFactory proxyFactory;
120
121                if (Boolean.getBoolean("wicket.ioc.useByteBuddy"))
122                {
123                        log.info("Using Byte Buddy proxy factory");
124                        proxyFactory = new ByteBuddyProxyFactory();
125                }
126                else
127                {
128                        proxyFactory = new CglibProxyFactory();
129                }
130
131                return proxyFactory;
132        }
133
134        /**
135         * Create a lazy init proxy for the specified type. The target object will be located using the
136         * provided locator upon first method invocation.
137         * 
138         * @param type
139         *            type that proxy will represent
140         * 
141         * @param locator
142         *            object locator that will locate the object the proxy represents
143         * 
144         * @return lazily initializable proxy
145         */
146        public static <T> T createProxy(final Class<T> type, final IProxyTargetLocator locator)
147        {
148                if (PRIMITIVES.contains(type) || Enum.class.isAssignableFrom(type))
149                {
150                        // We special-case primitives as sometimes people use these as
151                        // SpringBeans (WICKET-603, WICKET-906). Go figure.
152                        return (T) locator.locateProxyTarget();
153                }
154                else if (type.isInterface())
155                {
156                        return JDK_PROXY_FACTORY.createProxy(type, locator);
157                }
158                else
159                {
160                        return CLASS_PROXY_FACTORY.createProxy(type, locator);
161                }
162        }
163        
164        /**
165         * This interface is used to make the proxy forward writeReplace() call to the handler instead
166         * of invoking it on itself. This allows us to serialize the replacement object instead of the
167         * proxy itself in case the proxy subclass is deserialized on a VM that does not have it
168         * created.
169         * 
170         * @see LazyInitProxyFactory.ProxyReplacement
171         * 
172         * @author Igor Vaynberg (ivaynberg)
173         * 
174         */
175        public interface IWriteReplace
176        {
177                /**
178                 * write replace method as defined by Serializable
179                 * 
180                 * @return object that will replace this object in serialized state
181                 * @throws ObjectStreamException
182                 */
183                Object writeReplace() throws ObjectStreamException;
184        }
185
186        /**
187         * Object that replaces the proxy when it is serialized. Upon deserialization this object will
188         * create a new proxy with the same locator.
189         * 
190         * @author Igor Vaynberg (ivaynberg)
191         * 
192         */
193        public static class ProxyReplacement implements IClusterable
194        {
195                private static final long serialVersionUID = 1L;
196
197                private final IProxyTargetLocator locator;
198
199                private final String type;
200
201                /**
202                 * Constructor
203                 * 
204                 * @param type
205                 * @param locator
206                 */
207                public ProxyReplacement(final String type, final IProxyTargetLocator locator)
208                {
209                        this.type = type;
210                        this.locator = locator;
211                }
212
213                private Object readResolve() throws ObjectStreamException
214                {
215                        Class<?> clazz = WicketObjects.resolveClass(type);
216                        if (clazz == null)
217                        {
218                                try
219                                {
220                                        clazz = Class.forName(type, false, Thread.currentThread().getContextClassLoader());
221                                }
222                                catch (ClassNotFoundException ignored1)
223                                {
224                                        try
225                                        {
226                                                clazz = Class.forName(type, false, LazyInitProxyFactory.class.getClassLoader());
227                                        }
228                                        catch (ClassNotFoundException ignored2)
229                                        {
230                                                ClassNotFoundException cause = new ClassNotFoundException(
231                                                                "Could not resolve type [" + type +
232                                                                                "] with the currently configured org.apache.wicket.application.IClassResolver");
233                                                throw new WicketRuntimeException(cause);
234                                        }
235                                }
236                        }
237                        return LazyInitProxyFactory.createProxy(clazz, locator);
238                }
239        }
240
241        /**
242         * Method interceptor for proxies representing concrete object not backed by an interface. These
243         * proxies are represented by cglib proxies.
244         * 
245         * @author Igor Vaynberg (ivaynberg)
246         * 
247         */
248        @Deprecated(forRemoval = true)
249        public abstract static class AbstractCGLibInterceptor
250                implements
251                        MethodInterceptor,
252                        ILazyInitProxy,
253                        Serializable,
254                        IWriteReplace
255        {
256                private static final long serialVersionUID = 1L;
257
258                protected final IProxyTargetLocator locator;
259
260                protected final String typeName;
261
262                private transient Object target;
263
264                /**
265                 * Constructor
266                 * 
267                 * @param type
268                 *            class of the object this proxy was created for
269                 * 
270                 * @param locator
271                 *            object locator used to locate the object this proxy represents
272                 */
273                public AbstractCGLibInterceptor(final Class<?> type, final IProxyTargetLocator locator)
274                {
275                        super();
276                        typeName = type.getName();
277                        this.locator = locator;
278                }
279
280                /**
281                 * @see net.sf.cglib.proxy.MethodInterceptor#intercept(java.lang.Object,
282                 *      java.lang.reflect.Method, java.lang.Object[], net.sf.cglib.proxy.MethodProxy)
283                 */
284                @Override
285                public Object intercept(final Object object, final Method method, final Object[] args,
286                        final MethodProxy proxy) throws Throwable
287                {
288                        if (isFinalizeMethod(method))
289                        {
290                                // swallow finalize call
291                                return null;
292                        }
293                        else if (isEqualsMethod(method))
294                        {
295                                return (equals(args[0])) ? Boolean.TRUE : Boolean.FALSE;
296                        }
297                        else if (isHashCodeMethod(method))
298                        {
299                                return hashCode();
300                        }
301                        else if (isToStringMethod(method))
302                        {
303                                return toString();
304                        }
305                        else if (isWriteReplaceMethod(method))
306                        {
307                                return writeReplace();
308                        }
309                        else if (method.getDeclaringClass().equals(ILazyInitProxy.class))
310                        {
311                                return getObjectLocator();
312                        }
313
314                        if (target == null)
315                        {
316                                target = locator.locateProxyTarget();
317                        }
318                        return proxy.invoke(target, args);
319                }
320
321                /**
322                 * @see org.apache.wicket.proxy.ILazyInitProxy#getObjectLocator()
323                 */
324                @Override
325                public IProxyTargetLocator getObjectLocator()
326                {
327                        return locator;
328                }
329
330                @Override
331                public Object writeReplace() throws ObjectStreamException
332                {
333                        return new ProxyReplacement(typeName, locator);
334                }
335        }
336
337        /**
338         * Method interceptor for proxies representing concrete object not backed by an interface. These
339         * proxies are represented by cglib proxies.
340         * 
341         * @author Igor Vaynberg (ivaynberg)
342         * 
343         */
344        public static class CGLibInterceptor extends AbstractCGLibInterceptor
345        {
346                private static final long serialVersionUID = 1L;
347
348                /**
349                 * Constructor
350                 * 
351                 * @param type
352                 *            class of the object this proxy was created for
353                 * 
354                 * @param locator
355                 *            object locator used to locate the object this proxy represents
356                 */
357                public CGLibInterceptor(final Class<?> type, final IProxyTargetLocator locator)
358                {
359                        super(type, locator);
360                }
361        }
362        
363        /**
364         * @deprecated no longer used
365         */
366        @Deprecated(forRemoval = true)
367        public static class SerializableNoOpCallback implements NoOp, Serializable
368        {
369                public static final NoOp INSTANCE = new SerializableNoOpCallback();
370        }
371        
372        /**
373         * Checks if the method is derived from Object.equals()
374         * 
375         * @param method
376         *            method being tested
377         * @return true if the method is derived from Object.equals(), false otherwise
378         */
379        public static boolean isEqualsMethod(final Method method)
380        {
381                return (method.getReturnType() == boolean.class) &&
382                        (method.getParameterTypes().length == 1) &&
383                        (method.getParameterTypes()[0] == Object.class) && method.getName().equals("equals");
384        }
385
386        /**
387         * Checks if the method is derived from Object.hashCode()
388         * 
389         * @param method
390         *            method being tested
391         * @return true if the method is defined from Object.hashCode(), false otherwise
392         */
393        public static boolean isHashCodeMethod(final Method method)
394        {
395                return (method.getReturnType() == int.class) && (method.getParameterTypes().length == 0) &&
396                        method.getName().equals("hashCode");
397        }
398
399        /**
400         * Checks if the method is derived from Object.toString()
401         * 
402         * @param method
403         *            method being tested
404         * @return true if the method is defined from Object.toString(), false otherwise
405         */
406        public static boolean isToStringMethod(final Method method)
407        {
408                return (method.getReturnType() == String.class) &&
409                        (method.getParameterTypes().length == 0) && method.getName().equals("toString");
410        }
411
412        /**
413         * Checks if the method is derived from Object.finalize()
414         * 
415         * @param method
416         *            method being tested
417         * @return true if the method is defined from Object.finalize(), false otherwise
418         */
419        public static boolean isFinalizeMethod(final Method method)
420        {
421                return (method.getReturnType() == void.class) && (method.getParameterTypes().length == 0) &&
422                        method.getName().equals("finalize");
423        }
424
425        /**
426         * Checks if the method is the writeReplace method
427         * 
428         * @param method
429         *            method being tested
430         * @return true if the method is the writeReplace method, false otherwise
431         */
432        public static boolean isWriteReplaceMethod(final Method method)
433        {
434                return (method.getReturnType() == Object.class) &&
435                        (method.getParameterTypes().length == 0) && method.getName().equals("writeReplace");
436        }
437        
438        /**
439         * @deprecated no longer used
440         */
441        @Deprecated(forRemoval = true)
442        public static class WicketNamingPolicy extends DefaultNamingPolicy
443        {
444                public static final WicketNamingPolicy INSTANCE = new WicketNamingPolicy();
445
446                private WicketNamingPolicy()
447                {
448                }
449        }
450}