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}