1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 package info.magnolia.config;
35
36 import static java.util.regex.Pattern.compile;
37
38 import java.lang.reflect.InvocationTargetException;
39 import java.lang.reflect.Method;
40 import java.lang.reflect.Proxy;
41 import java.util.ArrayList;
42 import java.util.Collection;
43 import java.util.HashMap;
44 import java.util.HashSet;
45 import java.util.LinkedHashMap;
46 import java.util.LinkedHashSet;
47 import java.util.List;
48 import java.util.Map;
49 import java.util.Set;
50 import java.util.regex.Matcher;
51 import java.util.regex.Pattern;
52
53 import org.apache.commons.lang3.StringUtils;
54 import org.slf4j.Logger;
55 import org.slf4j.LoggerFactory;
56
57 import com.google.common.base.Joiner;
58 import com.google.common.base.Objects;
59 import com.thoughtworks.proxy.factory.CglibProxyFactory;
60 import com.thoughtworks.proxy.factory.InvokerReference;
61 import com.thoughtworks.proxy.kit.ReflectionUtils;
62 import com.thoughtworks.proxy.kit.SimpleInvoker;
63
64 import net.sf.cglib.proxy.Enhancer;
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82 @SuppressWarnings("unchecked")
83 public class MutableWrapper<T, U extends T> {
84
85 private static final Logger log = LoggerFactory.getLogger(MutableWrapper.class);
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100 public static <T, U extends T> U wrapAs(T source, Class<U> type) {
101 return new MutableWrapper<T, U>(type).createWrapper(source);
102 }
103
104
105
106
107
108 public static <U> U wrap(U source) {
109 if (source == null) {
110 return null;
111 }
112 return MutableWrapper.wrapAs(source, (Class<U>) source.getClass());
113 }
114
115
116
117
118
119
120
121 public static <U> Mutable<U> asMutable(U instance) {
122 if (instance instanceof Mutable) {
123 return (Mutable<U>) instance;
124 }
125
126 throw new IllegalArgumentException("Provided instance is not instance of Mutable: " + String.valueOf(instance));
127 }
128
129 protected static Method setProperty;
130 protected static Method getObject;
131 protected static Method getInvoker;
132
133 static {
134 try {
135 setProperty = Mutable.class.getMethod("setProperty", String.class, Object.class);
136 getObject = Mutable.class.getMethod("getObject");
137 getInvoker = ProxyWithBeanPropertyMethodInvoker.class.getMethod("getInvoker");
138 } catch (NoSuchMethodException e) {
139 throw new ExceptionInInitializerError(e.toString());
140 }
141 }
142
143
144 private final CglibProxyFactory proxyFactory = new CglibProxyFactory(false);
145
146 private final Class<U> type;
147
148 MutableWrapper(Class<U> type) {
149 this.type = type;
150 }
151
152 U createWrapper(T source) {
153 if (source == null) {
154 return null;
155 }
156
157 final Class[] typesToProxy = getTargetTypes(type);
158 for (final Class<?> type : typesToProxy) {
159 if (!proxyFactory.canProxy(type)) {
160 log.debug("{} may not be proxied, returning unwrapped object...", type);
161 return (U) source;
162 }
163 }
164
165 return proxyFactory.createProxy(new BeanPropertyMethodInvoker(source), typesToProxy);
166 }
167
168
169
170 private static final Pattern CGLIB_FOOTPRINT = Pattern.compile("\\$\\$EnhancerBy.*CGLIB\\$\\$");
171
172 private Class[] getTargetTypes(Class<U> initialType) {
173
174 Class<? super U> type = initialType;
175
176 while (!type.isInterface() &&
177 (Enhancer.isEnhanced(type) ||
178 proxyFactory.isProxyClass(type) ||
179 Proxy.class.isAssignableFrom(type) ||
180 CGLIB_FOOTPRINT.matcher(type.getName()).find())) {
181 type = type.getSuperclass();
182 }
183
184 final HashSet<Class<?>> types = new HashSet<>();
185 types.add(Mutable.class);
186 types.add(ProxyWithBeanPropertyMethodInvoker.class);
187 types.addAll(ReflectionUtils.getAllInterfaces(initialType));
188 types.add(type);
189
190 return types.toArray(new Class[types.size()]);
191 }
192
193
194
195
196
197
198
199
200 public interface Mutable<T> {
201 T getObject();
202 void setProperty(String propertyName, Object value);
203 }
204
205
206
207
208 public interface ProxyWithBeanPropertyMethodInvoker extends InvokerReference {
209 @Override
210 BeanPropertyMethodInvoker getInvoker();
211 }
212
213
214
215
216
217
218
219
220
221
222
223
224 @SuppressWarnings("rawtypes")
225 public static class BeanPropertyMethodInvoker extends SimpleInvoker {
226 private static final Logger log = LoggerFactory.getLogger(BeanPropertyMethodInvoker.class);
227 private static final Pattern getter = compile("^(?:get|is)(.+)$");
228 private static final Pattern setter = compile("^set(.+)$");
229
230 private final Set<String> modifiedPropertyNames = new HashSet<>();
231
232 private final Map<String, Object> propertyValueCache = new HashMap<>();
233
234 BeanPropertyMethodInvoker(Object delegate) {
235 super(delegate);
236 }
237
238 @Override
239 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
240 if (getInvoker.equals(method)) {
241 return this;
242 }
243
244 if (ReflectionUtils.toString.getName().equals(method.getName()) && args.length == 0) {
245 return invokeToString();
246 }
247
248 if (ReflectionUtils.equals.getName().equals(method.getName()) && args.length == 1) {
249 return invokeEquals(args[0]);
250 }
251
252 if (ReflectionUtils.hashCode.getName().equals(method.getName()) && args.length == 0) {
253 return invokeHashCode();
254 }
255
256 if (method.equals(setProperty)) {
257 invokeSetter(String.valueOf(args[0]), args[1]);
258 return null;
259 }
260
261 if (method.equals(getObject)) {
262 return proxy;
263 }
264
265 final Matcher setterInvocationMatcher = setter.matcher(method.getName());
266 if (setterInvocationMatcher.matches()) {
267 if (args.length < 1) {
268 log.debug("Encountered [{}] setter invocation without arguments, related type: [{}]", method.getName(), proxy.getClass().getName());
269 } else {
270 invokeSetter(StringUtils.uncapitalize(setterInvocationMatcher.group(1)), args[0]);
271 }
272 return null;
273 }
274
275 final Matcher getterInvocationMatcher = getter.matcher(method.getName());
276 if (getterInvocationMatcher.matches()) {
277 return invokeGetter(StringUtils.uncapitalize(getterInvocationMatcher.group(1)), method);
278 }
279
280 return super.invoke(proxy, method, args);
281 }
282
283
284
285
286 private int invokeHashCode() {
287 int hashCode = getTarget().hashCode();
288 for (final String modifiedPropertyName : modifiedPropertyNames) {
289 final Object modifiedValue = propertyValueCache.get(modifiedPropertyName);
290 hashCode += modifiedValue == null ? 0 : modifiedValue.hashCode();
291 }
292 return hashCode;
293 }
294
295
296
297
298 private String invokeToString() {
299 final StringBuilder sb = new StringBuilder();
300 sb.append("Mutable wrapper of [")
301 .append(getTarget().toString())
302 .append("]");
303
304 if (!modifiedPropertyNames.isEmpty()) {
305 sb.append(" with modified properties: ");
306 List<String> modifiedPropertyStatements = new ArrayList<>(modifiedPropertyNames.size());
307 for (final String modifiedPropertyName : modifiedPropertyNames) {
308 modifiedPropertyStatements.add(String.format("{%s : %s}", modifiedPropertyName, propertyValueCache.get(modifiedPropertyName)));
309 }
310 sb.append(Joiner.on(", ").join(modifiedPropertyStatements));
311 }
312
313 return sb.toString();
314 }
315
316
317
318
319
320
321
322 private boolean invokeEquals(Object other) {
323 if (other instanceof MutableWrapper.ProxyWithBeanPropertyMethodInvoker) {
324 final BeanPropertyMethodInvoker otherProxy = ((ProxyWithBeanPropertyMethodInvoker) other).getInvoker();
325 boolean sameTarget = otherProxy.getTarget().equals(getTarget());
326 if (sameTarget) {
327 if (otherProxy.modifiedPropertyNames.equals(modifiedPropertyNames)) {
328 for (final String modifiedPropertyName : modifiedPropertyNames) {
329 if (!Objects.equal(propertyValueCache.get(modifiedPropertyName), otherProxy.propertyValueCache.get(modifiedPropertyName))) {
330 return false;
331 }
332 }
333 return true;
334 }
335 }
336 }
337 return false;
338 }
339
340 private void invokeSetter(String propertyName, Object value) {
341
342 modifiedPropertyNames.add(propertyName);
343
344 propertyValueCache.put(propertyName, value);
345 }
346
347 private Object invokeGetter(String propertyName, Method getterMethod) {
348 if (propertyValueCache.containsKey(propertyName)) {
349 return propertyValueCache.get(propertyName);
350 }
351
352 final Object fallbackValue;
353 try {
354 fallbackValue = getterMethod.invoke(getTarget());
355 } catch (IllegalAccessException | InvocationTargetException e) {
356 log.warn("Failed to invoke a fallback {} call due to a reflection operation problem: {}, returning null", getterMethod.getName(), e.getMessage(), e);
357 return null;
358 }
359
360 if (fallbackValue == null) {
361 return null;
362 }
363
364 Object wrappedValue;
365
366 if (fallbackValue instanceof Collection) {
367 wrappedValue = wrapCollection((Collection) fallbackValue);
368 } else if (fallbackValue instanceof Map) {
369 wrappedValue = wrapMap((Map) fallbackValue);
370 } else {
371 wrappedValue = MutableWrapper.wrap(fallbackValue);
372 }
373
374 propertyValueCache.put(propertyName, wrappedValue);
375 return wrappedValue;
376 }
377
378 private Map<?, ?> wrapMap(Map<?, ?> sourceMap) {
379 final Map<Object, Object> mapCopy = new LinkedHashMap<>();
380 for (final Map.Entry<?, ?> entry : sourceMap.entrySet()) {
381 mapCopy.put(entry.getKey(), MutableWrapper.wrap(entry.getValue()));
382 }
383 return mapCopy;
384 }
385
386 private Collection<?> wrapCollection(Collection<?> sourceCollection) {
387 final Collection<Object> collectionCopy = sourceCollection instanceof List ? new ArrayList<>() : new LinkedHashSet<>();
388 for (final Object element : sourceCollection) {
389 collectionCopy.add(MutableWrapper.wrap(element));
390 }
391 return collectionCopy;
392 }
393 }
394 }