View Javadoc
1   /**
2    * This file Copyright (c) 2016-2018 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
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   * Provides a mutable wrapper.
71   * <p>
72   * For a given object (normally a POJO) this utility generates a wrapper (via Java Dynamic Proxies or CGLIB) which initially
73   * has the state identical to the one of the source due to the default method call delegation. However, the wrapper can be deliberately modified without
74   * a fear of affecting the source object thanks to the wrappers internal state which automatically absorbs all the changes coming through setter calls or some such.
75   * </p>
76   * <p>
77   * Besides merely acting as a wrapper, the proxy object is also supplied with an implementation of the {@link Mutable} interface, which simplifies
78   * dynamic mutator implementations.
79   * </p>
80   *
81   * @see Mutator
82   * @param <T> type of the object being wrapped
83   * @param <U> type of the resulting wrapper (must be extending {@code T})
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       * Takes an arbitrary source object {@code T} and produces a proxied wrapper of type {@code U} which may extend {@code T}.
92       * <p>
93       * Once the source object is wrapped via this API - all the sub-objects retrieved via getters will get automatically (and recursively)
94       * wrapped as well.
95       * </p>
96       * <p>
97       * Returned instance's state can be modified without a risk of affecting the {@code source} instance's state.
98       * Modification can be done via setters (should {@code T} have any) or via {@link Mutable} interface which
99       * the returned wrapper implements.
100      * </p>
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      * Does the same as {@link Mutable#wrapAs(Object, Class)} but uses a provided instance's type
108      * as a default target type.
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      * Merely attempts to cast an instance to {@link Mutable}.
119      * @throws IllegalArgumentException if {@code instance} does not implement {@link Mutable}
120      * (i.e. if instance has not been wrapped with {@link MutableWrapper} previously).
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     // Last resort of proxy detection attempt. Normally Enhancer#isEnhanced would indicate that the class is proxied with CGLIB,
145     // but some frameworks like Mockito re-package CGLIB which produces proxies not detectable by Enhancer
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      * Generic interface which allows to set the arbitrary object properties via {@link #setProperty(String, Object)}.
168      * By design this interface is appended to the wrappers produced by {@link MutableWrapper}.
169      *
170      * @param <T> type of the managed object
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     // Prevent a wrapped class c-tor from being invoked for the proxy by passing false to CglibProxyFactory
193     private final CglibProxyFactory proxyFactory = new CglibProxyFactory(false);
194 
195     /**
196      * Provides access to the mutable wrapper invoker.
197      *
198      * @deprecated since 6.1. Use {@link ByteBuddyMutableWrapperHelper} instead.
199      */
200     @Deprecated
201     public interface ProxyWithBeanPropertyMethodInvoker extends InvokerReference {
202         @Override
203         BeanPropertyMethodInvoker getInvoker();
204     }
205 
206     /**
207      * Invoker which is backing up the proxies produced by {@link MutableWrapper}. The following is provided:
208      * <ul>
209      * <li>Simple map based mutable state</li>
210      * <li>Support for getter invocations whose return values are delegated to the wrapped object, automatically cached in the state and wrapped with {@link MutableWrapper},
211      * collections and maps are copied.</li>
212      * <li>Support for setter invocations which are able to change the state.</li>
213      * <li>{@link Mutable} interface support - a universal setter for interfaces that are immutable by default</li>
214      * <li>Basic implementations of {@link Object#toString()}, {@link Object#equals(Object)} and {@link Object#hashCode()}</li>
215      * </ul>
216      *
217      * @deprecated since 6.1. Use {@link ByteBuddyMutableWrapperHelper} instead.
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          * Provides the hashcode value based on the wrapped object's hashcode plus combined hashcode of the modified properties' values.
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          * {@link Object#toString()} implementation which prints the wrapped object plus the modified properties.
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          * Provides support for equality check. The strategy is as follows:
335          * - it is only possible that this proxy is equal to a proxy of the same kind
336          * - source objects are equal
337          * - all the values of modified properties are equal
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             // track modified property
359             modifiedPropertyNames.add(propertyName);
360             // Remember the new explicit property value
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 }