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 import static net.bytebuddy.dynamic.loading.ClassLoadingStrategy.Default.WRAPPER;
38 import static net.bytebuddy.implementation.MethodDelegation.to;
39 import static net.bytebuddy.matcher.ElementMatchers.not;
40 import static net.bytebuddy.matcher.ElementMatchers.isDeclaredBy;
41 import static net.bytebuddy.matcher.ElementMatchers.isPublic;
42 import static net.bytebuddy.matcher.ElementMatchers.isEquals;
43 import static net.bytebuddy.matcher.ElementMatchers.isToString;
44 import static net.bytebuddy.matcher.ElementMatchers.isHashCode;
45
46 import info.magnolia.dynamic.MagnoliaProxy;
47
48 import java.lang.reflect.Field;
49 import java.lang.reflect.InvocationTargetException;
50 import java.lang.reflect.Method;
51 import java.lang.reflect.Modifier;
52 import java.util.ArrayList;
53 import java.util.Collection;
54 import java.util.Collections;
55 import java.util.HashMap;
56 import java.util.HashSet;
57 import java.util.LinkedHashMap;
58 import java.util.LinkedHashSet;
59 import java.util.List;
60 import java.util.Map;
61 import java.util.Set;
62 import java.util.regex.Matcher;
63 import java.util.regex.Pattern;
64
65 import net.bytebuddy.TypeCache;
66 import org.apache.commons.lang3.StringUtils;
67 import org.objenesis.Objenesis;
68 import org.objenesis.ObjenesisStd;
69 import org.slf4j.Logger;
70 import org.slf4j.LoggerFactory;
71
72 import com.google.common.base.Joiner;
73 import com.google.common.base.Objects;
74
75 import net.bytebuddy.implementation.bind.annotation.AllArguments;
76 import net.bytebuddy.implementation.bind.annotation.Origin;
77 import net.bytebuddy.implementation.bind.annotation.RuntimeType;
78 import net.bytebuddy.implementation.bind.annotation.This;
79 import net.bytebuddy.implementation.bind.annotation.FieldValue;
80 import net.bytebuddy.ByteBuddy;
81 import net.bytebuddy.implementation.FieldAccessor;
82
83
84
85
86
87
88
89 class ByteBuddyMutableWrapperHelper<T, U extends T> {
90
91 private static final String INVOKER = "_INVOKER";
92
93 private static final TypeCache<Class> typeCache = new TypeCache.WithInlineExpunction<>(TypeCache.Sort.SOFT);
94
95 private static final Objenesis objenesis = new ObjenesisStd(true);
96
97 U createWrapper(T source, Class<?> type, Set<Class<?>> interfaces) {
98 if (source == null) {
99 return null;
100 } else if (Modifier.isFinal(type.getModifiers())) {
101 return (U) source;
102 }
103
104 BeanPropertyMethodInvoker invoker = new BeanPropertyMethodInvoker(source);
105
106
107
108 Class cacheKey = Object.class.equals(type) ? source.getClass() : type;
109 Class clazz = typeCache.findOrInsert(getClass().getClassLoader(), cacheKey, () -> {
110
111 Collections.addAll(interfaces, MagnoliaProxy.class, MutableWrapper.Mutable.class, InvokerResolver.class);
112 ArrayList<Class<?>> interfaceTypes = new ArrayList<>(interfaces);
113
114 return new ByteBuddy()
115 .subclass(type)
116 .implement(interfaceTypes)
117 .method(not(isDeclaredBy(Object.class).or(isEquals()).or(isToString()))
118 .and(isPublic()))
119 .intercept(to(new DefaultInterceptor()))
120 .defineField(INVOKER, BeanPropertyMethodInvoker.class)
121 .method(isDeclaredBy(InvokerResolver.class)).intercept(FieldAccessor.ofField(INVOKER))
122 .method(isEquals()).intercept(to(new EqualsInterceptor()))
123 .method(isToString()).intercept(to(new ToStringInterceptor()))
124 .method(isHashCode()).intercept(to(new HashCodeInterceptor()))
125 .make()
126 .load(getClass().getClassLoader(), WRAPPER).getLoaded();
127 });
128
129 InvokerResolver instance = (InvokerResolver) objenesis.newInstance(clazz);
130 instance.setInvoker(invoker);
131 return (U) instance;
132 }
133
134
135
136
137 public interface InvokerResolver {
138
139 BeanPropertyMethodInvoker getInvoker();
140
141 void setInvoker(BeanPropertyMethodInvoker invoker);
142 }
143
144
145
146
147
148
149
150 public class EqualsInterceptor {
151
152 @RuntimeType
153 public Object intercept(@This InvokerResolver that, @AllArguments Object[] args) {
154 if (args.length == 1 && args[0] instanceof InvokerResolver) {
155 BeanPropertyMethodInvoker invoker = that.getInvoker();
156 BeanPropertyMethodInvoker otherInvoker = ((InvokerResolver) args[0]).getInvoker();
157 boolean sameTarget = otherInvoker.getTarget().equals(invoker.getTarget());
158 if (sameTarget) {
159 if (invoker.modifiedPropertyNames.equals(otherInvoker.modifiedPropertyNames)) {
160 for (final String modifiedPropertyName : invoker.modifiedPropertyNames) {
161 if (!Objects.equal(invoker.propertyValueCache.get(modifiedPropertyName), otherInvoker.propertyValueCache.get(modifiedPropertyName))) {
162 return false;
163 }
164 }
165 return true;
166 }
167 }
168 }
169 return false;
170 }
171 }
172
173
174
175
176 public class ToStringInterceptor {
177
178 @RuntimeType
179 public Object intercept(@This InvokerResolver that) {
180 final StringBuilder sb = new StringBuilder();
181 sb.append("Mutable wrapper of [")
182 .append(that.getInvoker().getTarget())
183 .append("]");
184
185 if (!that.getInvoker().modifiedPropertyNames.isEmpty()) {
186 sb.append(" with modified properties: ");
187 List<String> modifiedPropertyStatements = new ArrayList<>(that.getInvoker().modifiedPropertyNames.size());
188 for (final String modifiedPropertyName : that.getInvoker().modifiedPropertyNames) {
189 modifiedPropertyStatements.add(String.format("{%s : %s}", modifiedPropertyName, that.getInvoker().propertyValueCache.get(modifiedPropertyName)));
190 }
191 sb.append(Joiner.on(", ").join(modifiedPropertyStatements));
192 }
193
194 return sb.toString();
195 }
196 }
197
198
199
200
201 public class HashCodeInterceptor {
202
203 @RuntimeType
204 public Object intercept(@This InvokerResolver that) {
205 BeanPropertyMethodInvoker invoker = that.getInvoker();
206 int hashCode = invoker.getTarget().hashCode();
207 for (final String modifiedPropertyName : invoker.modifiedPropertyNames) {
208 final Object modifiedValue = invoker.propertyValueCache.get(modifiedPropertyName);
209 hashCode += modifiedValue == null ? 0 : modifiedValue.hashCode();
210 }
211 return hashCode;
212 }
213 }
214
215
216
217
218
219 public class DefaultInterceptor {
220
221 @RuntimeType
222 public Object intercept(@This InvokerResolver that, @Origin Method method, @AllArguments Object[] args, @FieldValue(INVOKER) BeanPropertyMethodInvoker invoker) {
223 try {
224 return invoker.invoke(invoker, method, args);
225 } catch (Throwable e) {
226 throw new RuntimeException(e);
227 }
228 }
229 }
230
231
232
233
234
235
236
237
238
239
240
241 public static class BeanPropertyMethodInvoker {
242 private static final Logger log = LoggerFactory.getLogger(BeanPropertyMethodInvoker.class);
243 private static final Pattern getter = compile("^(?:get|is)(.+)$");
244 private static final Pattern setter = compile("^set(.+)$");
245
246 final Set<String> modifiedPropertyNames = new HashSet<>();
247
248 final Map<String, Object> propertyValueCache = new HashMap<>();
249
250 private final Object target;
251
252 private BeanPropertyMethodInvoker(Object proxyDelegate) {
253 target = proxyDelegate;
254 }
255
256 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
257 if (method.equals(MutableWrapper.Mutable.class.getMethod("setProperty", String.class, Object.class))) {
258 invokeSetter(String.valueOf(args[0]), args[1]);
259 return null;
260 }
261
262 final Matcher setterInvocationMatcher = setter.matcher(method.getName());
263 if (setterInvocationMatcher.matches()) {
264 if (args.length < 1) {
265 log.debug("Encountered [{}] setter invocation without arguments, related type: [{}]", method.getName(), proxy.getClass().getName());
266 } else {
267 invokeSetter(StringUtils.uncapitalize(setterInvocationMatcher.group(1)), args[0]);
268 }
269 return null;
270 }
271
272 final Matcher getterInvocationMatcher = getter.matcher(method.getName());
273 if (getterInvocationMatcher.matches() && args.length == 0) {
274 return invokeGetter(StringUtils.uncapitalize(getterInvocationMatcher.group(1)), method);
275 }
276
277 final Map<String, Object> currentValues = applyFieldValues(this.propertyValueCache);
278 try {
279 return method.invoke(getTarget(), args);
280 } finally {
281 applyFieldValues(currentValues);
282 }
283 }
284
285 private Map<String, Object> applyFieldValues(Map<String, Object> valuesToApply) {
286 final Map<String, Object> oldValues = new HashMap<>();
287 valuesToApply.forEach((propertyName, value) -> {
288 try {
289 final Field property = getTarget().getClass().getDeclaredField(propertyName);
290 property.setAccessible(true);
291 oldValues.put(propertyName, property.get(getTarget()));
292 property.set(getTarget(), value);
293 } catch (NoSuchFieldException | IllegalAccessException e) {
294 log.debug("Reflective setting of the property: '{}' has failed.", propertyName, e);
295 }
296 });
297
298 return oldValues;
299 }
300
301 Object getTarget() {
302 return target;
303 }
304
305 private void invokeSetter(String propertyName, Object value) {
306
307 modifiedPropertyNames.add(propertyName);
308
309 propertyValueCache.put(propertyName, value);
310 }
311
312 private Object invokeGetter(String propertyName, Method getterMethod) {
313 if (propertyValueCache.containsKey(propertyName)) {
314 return propertyValueCache.get(propertyName);
315 }
316
317 final Object fallbackValue;
318 try {
319 fallbackValue = getterMethod.invoke(getTarget());
320 } catch (IllegalAccessException | InvocationTargetException e) {
321 log.warn("Failed to invoke a fallback {} call due to a reflection operation problem: {}, returning null", getterMethod.getName(), e.getMessage(), e);
322 return null;
323 }
324
325 if (fallbackValue == null) {
326 return null;
327 }
328
329 Object wrappedValue;
330
331 if (fallbackValue instanceof Collection) {
332 wrappedValue = wrapCollection((Collection) fallbackValue);
333 } else if (fallbackValue instanceof Map) {
334 wrappedValue = wrapMap((Map) fallbackValue);
335 } else {
336 wrappedValue = MutableWrapper.wrap(fallbackValue);
337 }
338
339 propertyValueCache.put(propertyName, wrappedValue);
340 return wrappedValue;
341 }
342
343 private Map<?, ?> wrapMap(Map<?, ?> sourceMap) {
344 final Map<Object, Object> mapCopy = new LinkedHashMap<>();
345 for (final Map.Entry<?, ?> entry : sourceMap.entrySet()) {
346 mapCopy.put(entry.getKey(), MutableWrapper.wrap(entry.getValue()));
347 }
348 return mapCopy;
349 }
350
351 private Collection<?> wrapCollection(Collection<?> sourceCollection) {
352 final Collection<Object> collectionCopy = sourceCollection instanceof List ? new ArrayList<>() : new LinkedHashSet<>();
353 for (final Object element : sourceCollection) {
354 collectionCopy.add(MutableWrapper.wrap(element));
355 }
356 return collectionCopy;
357 }
358 }
359 }