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