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