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