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 java.lang.reflect.Field;
39  import java.lang.reflect.InvocationTargetException;
40  import java.lang.reflect.Method;
41  import java.lang.reflect.Proxy;
42  import java.util.ArrayList;
43  import java.util.Collection;
44  import java.util.HashMap;
45  import java.util.HashSet;
46  import java.util.LinkedHashMap;
47  import java.util.LinkedHashSet;
48  import java.util.List;
49  import java.util.Map;
50  import java.util.Set;
51  import java.util.regex.Matcher;
52  import java.util.regex.Pattern;
53  
54  import org.apache.commons.lang3.StringUtils;
55  import org.slf4j.Logger;
56  import org.slf4j.LoggerFactory;
57  
58  import com.google.common.base.Joiner;
59  import com.google.common.base.Objects;
60  import com.thoughtworks.proxy.factory.CglibProxyFactory;
61  import com.thoughtworks.proxy.factory.InvokerReference;
62  import com.thoughtworks.proxy.kit.ReflectionUtils;
63  import com.thoughtworks.proxy.kit.SimpleInvoker;
64  
65  import net.sf.cglib.proxy.Enhancer;
66  
67  /**
68   * Provides a mutable wrapper.
69   * <p>
70   * For a given object (normally a POJO) this utility generates a wrapper (via Java Dynamic Proxies or CGLIB) which initially
71   * 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
72   * 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.
73   * </p>
74   * <p>
75   * Besides merely acting as a wrapper, the proxy object is also supplied with an implementation of the {@link Mutable} interface, which simplifies
76   * dynamic mutator implementations.
77   * </p>
78   *
79   * @see Mutator
80   * @param <T> type of the object being wrapped
81   * @param <U> type of the resulting wrapper (must be extending {@code T})
82   */
83  @SuppressWarnings("unchecked")
84  public class MutableWrapper<T, U extends T> {
85  
86      private static final Logger log = LoggerFactory.getLogger(MutableWrapper.class);
87  
88  
89      /**
90       * Takes an arbitrary source object {@code T} and produces a proxied wrapper of type {@code U} which may extend {@code T}.
91       * <p>
92       * Once the source object is wrapped via this API - all the sub-objects retrieved via getters will get automatically (and recursively)
93       * wrapped as well.
94       * </p>
95       * <p>
96       * Returned instance's state can be modified without a risk of affecting the {@code source} instance's state.
97       * Modification can be done via setters (should {@code T} have any) or via {@link Mutable} interface which
98       * the returned wrapper implements.
99       * </p>
100      */
101     public static <T, U extends T> U wrapAs(T source, Class<U> type) {
102         return new MutableWrapper<T, U>(type).createWrapper(source);
103     }
104 
105     /**
106      * Does the same as {@link Mutable#wrapAs(Object, Class)} but uses a provided instance's type
107      * as a default target type.
108      */
109     public static <U> U wrap(U source) {
110         if (source == null) {
111             return null;
112         }
113         return MutableWrapper.wrapAs(source, (Class<U>) source.getClass());
114     }
115 
116     /**
117      * Merely attempts to cast an instance to {@link Mutable}.
118      * @throws IllegalArgumentException if {@code instance} does not implement {@link Mutable}
119      * (i.e. if instance has not been wrapped with {@link MutableWrapper} previously).
120      *
121      */
122     public static <U> Mutable<U> asMutable(U instance) {
123         if (instance instanceof Mutable) {
124             return (Mutable<U>) instance;
125         }
126 
127         throw new IllegalArgumentException("Provided instance is not instance of Mutable: " + String.valueOf(instance));
128     }
129 
130     protected static Method setProperty;
131     protected static Method getObject;
132     protected static Method getInvoker;
133 
134     static {
135         try {
136             setProperty = Mutable.class.getMethod("setProperty", String.class, Object.class);
137             getObject = Mutable.class.getMethod("getObject");
138             getInvoker = ProxyWithBeanPropertyMethodInvoker.class.getMethod("getInvoker");
139         } catch (NoSuchMethodException e) {
140             throw new ExceptionInInitializerError(e.toString());
141         }
142     }
143 
144     // Prevent a wrapped class c-tor from being invoked for the proxy by passing false to CglibProxyFactory
145     private final CglibProxyFactory proxyFactory = new CglibProxyFactory(false);
146 
147     private final Class<U> type;
148 
149     MutableWrapper(Class<U> type) {
150         this.type = type;
151     }
152 
153     U createWrapper(T source) {
154         if (source == null) {
155             return null;
156         }
157 
158         final Class[] typesToProxy = getTargetTypes(type);
159         for (final Class<?> type : typesToProxy) {
160             if (!proxyFactory.canProxy(type)) {
161                 log.debug("{} may not be proxied, returning unwrapped object...", type);
162                 return (U) source;
163             }
164         }
165 
166         return proxyFactory.createProxy(new BeanPropertyMethodInvoker(source), typesToProxy);
167     }
168 
169     // Last resort of proxy detection attempt. Normally Enhancer#isEnhanced would indicate that the class is proxied with CGLIB,
170     // but some frameworks like Mockito re-package CGLIB which produces proxies not detectable by Enhancer
171     private static final Pattern CGLIB_FOOTPRINT = Pattern.compile("\\$\\$EnhancerBy.*CGLIB\\$\\$");
172 
173     private Class[] getTargetTypes(Class<U> initialType) {
174 
175         Class<? super U> type = initialType;
176 
177         while (!type.isInterface() &&
178                 (Enhancer.isEnhanced(type) ||
179                  proxyFactory.isProxyClass(type) ||
180                  Proxy.class.isAssignableFrom(type) ||
181                  CGLIB_FOOTPRINT.matcher(type.getName()).find())) {
182             type = type.getSuperclass();
183         }
184 
185         final HashSet<Class<?>> types = new HashSet<>();
186         types.add(Mutable.class);
187         types.add(ProxyWithBeanPropertyMethodInvoker.class);
188         types.addAll(ReflectionUtils.getAllInterfaces(initialType));
189         types.add(type);
190 
191         return types.toArray(new Class[types.size()]);
192     }
193 
194 
195     /**
196      * Generic interface which allows to set the arbitrary object properties via {@link #setProperty(String, Object)}.
197      * By design this interface is appended to the wrappers produced by {@link MutableWrapper}.
198      *
199      * @param <T> type of the managed object
200      */
201     public interface Mutable<T> {
202         T getObject();
203         void setProperty(String propertyName, Object value);
204     }
205 
206     /**
207      * Provides access to the mutable wrapper invoker.
208      */
209     public interface ProxyWithBeanPropertyMethodInvoker extends InvokerReference {
210         @Override
211         BeanPropertyMethodInvoker getInvoker();
212     }
213 
214     /**
215      * Invoker which is backing up the proxies produced by {@link MutableWrapper}. The following is provided:
216      * <ul>
217      * <li>Simple map based mutable state</li>
218      * <li>Support for getter invocations whose return values are delegated to the wrapped object, automatically cached in the state and wrapped with {@link MutableWrapper},
219      * collections and maps are copied.</li>
220      * <li>Support for setter invocations which are able to change the state.</li>
221      * <li>{@link Mutable} interface support - a universal setter for interfaces that are immutable by default</li>
222      * <li>Basic implementations of {@link Object#toString()}, {@link Object#equals(Object)} and {@link Object#hashCode()}</li>
223      * </ul>
224      */
225     @SuppressWarnings("rawtypes")
226     public static class BeanPropertyMethodInvoker extends SimpleInvoker {
227         private static final Logger log = LoggerFactory.getLogger(BeanPropertyMethodInvoker.class);
228         private static final Pattern getter = compile("^(?:get|is)(.+)$");
229         private static final Pattern setter = compile("^set(.+)$");
230 
231         private final Set<String> modifiedPropertyNames = new HashSet<>();
232 
233         private final Map<String, Object> propertyValueCache = new HashMap<>();
234 
235         BeanPropertyMethodInvoker(Object delegate) {
236             super(delegate);
237         }
238 
239         @Override
240         public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
241             if (getInvoker.equals(method)) {
242                 return this;
243             }
244 
245             if (ReflectionUtils.toString.getName().equals(method.getName()) && args.length == 0) {
246                 return invokeToString();
247             }
248 
249             if (ReflectionUtils.equals.getName().equals(method.getName()) && args.length == 1) {
250                 return invokeEquals(args[0]);
251             }
252 
253             if (ReflectionUtils.hashCode.getName().equals(method.getName()) && args.length == 0) {
254                 return invokeHashCode();
255             }
256 
257             if (method.equals(setProperty)) {
258                 invokeSetter(String.valueOf(args[0]), args[1]);
259                 return null;
260             }
261 
262             if (method.equals(getObject)) {
263                 return proxy;
264             }
265 
266             final Matcher setterInvocationMatcher = setter.matcher(method.getName());
267             if (setterInvocationMatcher.matches()) {
268                 if (args.length < 1) {
269                     log.debug("Encountered [{}] setter invocation without arguments, related type: [{}]", method.getName(), proxy.getClass().getName());
270                 } else {
271                     invokeSetter(StringUtils.uncapitalize(setterInvocationMatcher.group(1)), args[0]);
272                 }
273                 return null;
274             }
275 
276             final Matcher getterInvocationMatcher = getter.matcher(method.getName());
277             if (getterInvocationMatcher.matches()) {
278                 return invokeGetter(StringUtils.uncapitalize(getterInvocationMatcher.group(1)), method);
279             }
280 
281             final Map<String, Object> currentValues = applyFieldValues(this.propertyValueCache);
282             try {
283                 return super.invoke(getTarget(), method, args);
284             } finally {
285                 applyFieldValues(currentValues);
286             }
287         }
288 
289         private Map<String, Object> applyFieldValues(Map<String, Object> valuesToApply) {
290             final Map<String, Object> oldValues = new HashMap<>();
291             valuesToApply.forEach((propertyName, value) -> {
292                 try {
293                     final Field property = getTarget().getClass().getDeclaredField(propertyName);
294                     property.setAccessible(true);
295                     oldValues.put(propertyName, property.get(getTarget()));
296                     property.set(getTarget(), value);
297                 } catch (NoSuchFieldException | IllegalAccessException e) {
298                     log.debug("Reflective setting of the property: '{}' has failed.", propertyName, e);
299                 }
300             });
301 
302             return oldValues;
303         }
304 
305         /**
306          * Provides the hashcode value based on the wrapped object's hashcode plus combined hashcode of the modified properties' values.
307          */
308         private int invokeHashCode() {
309             int hashCode = getTarget().hashCode();
310             for (final String modifiedPropertyName : modifiedPropertyNames) {
311                 final Object modifiedValue = propertyValueCache.get(modifiedPropertyName);
312                 hashCode += modifiedValue == null ? 0 : modifiedValue.hashCode();
313             }
314             return hashCode;
315         }
316 
317         /**
318          * {@link Object#toString()} implementation which prints the wrapped object plus the modified properties.
319          */
320         private String invokeToString() {
321             final StringBuilder sb = new StringBuilder();
322             sb.append("Mutable wrapper of [")
323               .append(getTarget().toString())
324               .append("]");
325 
326             if (!modifiedPropertyNames.isEmpty()) {
327                 sb.append(" with modified properties: ");
328                 List<String> modifiedPropertyStatements = new ArrayList<>(modifiedPropertyNames.size());
329                 for (final String modifiedPropertyName : modifiedPropertyNames) {
330                     modifiedPropertyStatements.add(String.format("{%s : %s}", modifiedPropertyName, propertyValueCache.get(modifiedPropertyName)));
331                 }
332                 sb.append(Joiner.on(", ").join(modifiedPropertyStatements));
333             }
334 
335             return sb.toString();
336         }
337 
338         /**
339          * Provides support for equality check. The strategy is as follows:
340          * - it is only possible that this proxy is equal to a proxy of the same kind
341          * - source objects are equal
342          * - all the values of modified properties are equal
343          */
344         private boolean invokeEquals(Object other) {
345             if (other instanceof MutableWrapper.ProxyWithBeanPropertyMethodInvoker) {
346                 final BeanPropertyMethodInvoker otherProxy = ((ProxyWithBeanPropertyMethodInvoker) other).getInvoker();
347                 boolean sameTarget = otherProxy.getTarget().equals(getTarget());
348                 if (sameTarget) {
349                     if (otherProxy.modifiedPropertyNames.equals(modifiedPropertyNames)) {
350                         for (final String modifiedPropertyName : modifiedPropertyNames) {
351                             if (!Objects.equal(propertyValueCache.get(modifiedPropertyName), otherProxy.propertyValueCache.get(modifiedPropertyName))) {
352                                 return false;
353                             }
354                         }
355                         return true;
356                     }
357                 }
358             }
359             return false;
360         }
361 
362         private void invokeSetter(String propertyName, Object value) {
363             // track modified property
364             modifiedPropertyNames.add(propertyName);
365             // Remember the new explicit property value
366             propertyValueCache.put(propertyName, value);
367         }
368 
369         private Object invokeGetter(String propertyName, Method getterMethod) {
370             if (propertyValueCache.containsKey(propertyName)) {
371                 return propertyValueCache.get(propertyName);
372             }
373 
374             final Object fallbackValue;
375             try {
376                 fallbackValue = getterMethod.invoke(getTarget());
377             } catch (IllegalAccessException | InvocationTargetException e) {
378                 log.warn("Failed to invoke a fallback {} call due to a reflection operation problem: {}, returning null", getterMethod.getName(), e.getMessage(), e);
379                 return null;
380             }
381 
382             if (fallbackValue == null) {
383                 return null;
384             }
385 
386             Object wrappedValue;
387 
388             if (fallbackValue instanceof Collection) {
389                 wrappedValue = wrapCollection((Collection) fallbackValue);
390             } else if (fallbackValue instanceof Map) {
391                 wrappedValue = wrapMap((Map) fallbackValue);
392             } else {
393                 wrappedValue = MutableWrapper.wrap(fallbackValue);
394             }
395 
396             propertyValueCache.put(propertyName, wrappedValue);
397             return wrappedValue;
398         }
399 
400         private Map<?, ?> wrapMap(Map<?, ?> sourceMap) {
401             final Map<Object, Object> mapCopy = new LinkedHashMap<>();
402             for (final Map.Entry<?, ?> entry : sourceMap.entrySet()) {
403                 mapCopy.put(entry.getKey(), MutableWrapper.wrap(entry.getValue()));
404             }
405             return mapCopy;
406         }
407 
408         private Collection<?> wrapCollection(Collection<?> sourceCollection) {
409             final Collection<Object> collectionCopy = sourceCollection instanceof List ? new ArrayList<>() : new LinkedHashSet<>();
410             for (final Object element : sourceCollection) {
411                 collectionCopy.add(MutableWrapper.wrap(element));
412             }
413             return collectionCopy;
414         }
415     }
416 }