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