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