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