View Javadoc
1   /**
2    * This file Copyright (c) 2014-2015 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.map2bean;
35  
36  import info.magnolia.jcr.node2bean.PropertyTypeDescriptor;
37  import info.magnolia.jcr.node2bean.TypeDescriptor;
38  import info.magnolia.jcr.node2bean.TypeMapping;
39  import info.magnolia.jcr.node2bean.impl.PreConfiguredBeanUtils;
40  import info.magnolia.objectfactory.ClassFactory;
41  import info.magnolia.objectfactory.Classes;
42  import info.magnolia.objectfactory.ComponentProvider;
43  
44  import java.beans.PropertyDescriptor;
45  import java.lang.reflect.InvocationTargetException;
46  import java.util.ArrayList;
47  import java.util.Collection;
48  import java.util.Collections;
49  import java.util.HashMap;
50  import java.util.LinkedHashMap;
51  import java.util.LinkedList;
52  import java.util.List;
53  import java.util.Map;
54  
55  import javax.annotation.Nonnull;
56  import javax.inject.Inject;
57  
58  import org.apache.commons.beanutils.Converter;
59  import org.apache.commons.beanutils.PropertyUtilsBean;
60  import org.slf4j.Logger;
61  import org.slf4j.LoggerFactory;
62  
63  import com.google.common.base.Function;
64  import com.google.common.collect.FluentIterable;
65  import com.google.common.collect.Lists;
66  
67  /**
68   * Transforms a map to a bean graph. Resolves types through {@link TypeMapping} and {@link PreConfiguredBeanUtils}
69   * to mimic what Node2BeanTransformer does.
70   */
71  @SuppressWarnings("unchecked")
72  public class Map2BeanTransformer {
73  
74      private static final Map<Class, Class> DEFAULT_TYPES = new HashMap<>();
75      private final PropertyUtilsBean propertyUtils;
76  
77      static {
78          DEFAULT_TYPES.put(Object.class, LinkedHashMap.class);
79          DEFAULT_TYPES.put(Map.class, LinkedHashMap.class);
80          DEFAULT_TYPES.put(Collection.class, LinkedList.class);
81          // TODO we need to have more (List, Queue, etc) and check for "isAssignableFrom" and/or isInterface
82      }
83  
84      private static final Logger log = LoggerFactory.getLogger(Map2BeanTransformer.class);
85  
86      private final ComponentProvider componentProvider;
87      private final TypeMapping mapping;
88      private final PreConfiguredBeanUtils beanUtils;
89  
90      @Inject
91      public Map2BeanTransformer(ComponentProvider componentProvider, TypeMapping mapping, PreConfiguredBeanUtils beanUtils) {
92          this.componentProvider = componentProvider;
93          this.mapping = mapping;
94          this.beanUtils = beanUtils;
95          propertyUtils = new PropertyUtilsBean();
96      }
97  
98      public <T> T toBean(Map<String, Object> map, Class<T> defaultRootType) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException, ConfigurationParsingException {
99          if (map == null) {
100             log.debug("toBean was invoked with a null map, will try to create bean out of an empty map.");
101             map = Collections.emptyMap();
102         }
103         TypeDescriptor defaultRootTypeDescriptor = mapping.getTypeDescriptor(defaultRootType);
104 
105         return readValue(map, defaultRootTypeDescriptor);
106     }
107 
108     private <T> T readValue(Object source, TypeDescriptor defaultTargetTypeDescriptor) throws IllegalAccessException, NoSuchMethodException, InvocationTargetException, InstantiationException, ConfigurationParsingException {
109         if (source == null) {
110             return null;
111         }
112 
113         TypeDescriptor sourceTypeDescriptor = mapping.getTypeDescriptor(source.getClass());
114 
115         if (needSimpleMapping(sourceTypeDescriptor, defaultTargetTypeDescriptor)) {
116             return readSimpleValue(source, defaultTargetTypeDescriptor);
117         } else {
118             return readComplexValue(source, defaultTargetTypeDescriptor);
119         }
120     }
121 
122     private boolean needSimpleMapping(TypeDescriptor sourceTypeDescriptor, TypeDescriptor defaultTargetTypeDescriptor) {
123         Converter converter = beanUtils.getConvertUtils().lookup(sourceTypeDescriptor.getType(), defaultTargetTypeDescriptor.getType());
124 
125         if (converter != null) {
126             // if direct conversion can be used no complex mapping is needed
127             return true;
128         } else {
129             if (defaultTargetTypeDescriptor.getType() == Object.class) { // untyped
130                 // if there the target is untyped but the source has a convertable type
131                 return beanUtils.getConvertUtils().lookup(sourceTypeDescriptor.getType()) != null;
132             }
133 
134             return false;
135         }
136     }
137 
138     private <T> T readSimpleValue(Object source, TypeDescriptor defaultTargetTypeDescriptor) {
139         //not needed to convert here, convert is done later by beanUtils.setProperty()
140         return (T) source;
141     }
142 
143     private <T> T readComplexValue(Object source, TypeDescriptor defaultTargetTypeDescriptor) throws IllegalAccessException, InstantiationException, InvocationTargetException, NoSuchMethodException, ConfigurationParsingException {
144         if (source instanceof Collection<?>) {
145             return (T) readOutList((Collection<Object>) source, mapping.getTypeDescriptor(Object.class));
146         }
147         Map<String, Object> sourceMap = (Map<String, Object>) source;
148         TypeDescriptor targetTypeDescriptor = resolveType(sourceMap, defaultTargetTypeDescriptor);
149         targetTypeDescriptor = resolveType(sourceMap, targetTypeDescriptor);
150         if (targetTypeDescriptor.getType() == Object.class || targetTypeDescriptor.isMap()) {
151             return (T) readOutMap(sourceMap, mapping.getTypeDescriptor(Object.class));
152         } else if (targetTypeDescriptor.isCollection()) {
153             return (T) readOutList(Lists.newLinkedList(prepareListValue(sourceMap.values())), mapping.getTypeDescriptor(Object.class));
154         }
155         return (T) readOutObject(sourceMap, targetTypeDescriptor);
156     }
157 
158     private <T> T readOutObject(Map<String, Object> sourceMap, TypeDescriptor targetType) throws IllegalAccessException, NoSuchMethodException, InvocationTargetException, InstantiationException, ConfigurationParsingException {
159         T targetObj = (T) componentProvider.newInstance(targetType.getType());
160 
161         List<PropertyDescriptor> propertyDescriptors = Lists.newArrayList(propertyUtils.getPropertyDescriptors(targetObj));
162         List<String> propertyNames = Lists.transform(propertyDescriptors, new Function<PropertyDescriptor, String>() {
163             @Override
164             public String apply(PropertyDescriptor pd) {
165                 return pd.getName();
166             }
167         });
168 
169 
170         for (Map.Entry<String, Object> sourceEntry : sourceMap.entrySet()) {
171             String sourcePropertyName = sourceEntry.getKey();
172             Object sourcePropertyValue = sourceEntry.getValue();
173 
174             if ("class".equals(sourcePropertyName)) {
175                 continue; // skip class property because its only meta data
176             }
177 
178             if (!propertyNames.contains(sourcePropertyName)) {
179                 // We should not just ignore this - see MAGNOLIA-6196
180                 log.warn("Property " + sourcePropertyName + " not found in class " + targetObj.getClass());
181                 continue;
182             }
183 
184             PropertyTypeDescriptor propertyTypeDescriptor = mapping.getPropertyTypeDescriptor(targetType.getType(), sourcePropertyName);
185 
186             Object value;
187             if (propertyTypeDescriptor.isCollection()) {
188                 value = readOutList(prepareListValue(sourcePropertyValue), propertyTypeDescriptor.getCollectionEntryType());
189             } else if (propertyTypeDescriptor.isMap()) {
190                 value = readOutMap((Map<String, Object>) sourcePropertyValue, propertyTypeDescriptor.getCollectionEntryType());
191             } else {
192                 value = readValue(sourcePropertyValue, propertyTypeDescriptor.getType());
193             }
194 
195             beanUtils.setProperty(targetObj, sourcePropertyName, value);
196         }
197 
198         return targetObj;
199     }
200 
201     private List<Object> prepareListValue(Object value) {
202         if (value instanceof List) {
203             return (List<Object>) value;
204         }
205 
206         if (value instanceof Map) {
207             final Map<String, Object> map = (Map) value;
208             final Function<Map.Entry<String, Object>, Object> entry2Value = new Function<Map.Entry<String, Object>, Object>() {
209                 @Nonnull
210                 @Override
211                 public Object apply(Map.Entry<String, Object> input) {
212                     final Object returnValue = input.getValue();
213                     if (returnValue instanceof Map) {
214                         final Map mapValue = (Map) returnValue;
215                         if (!mapValue.containsKey("name")) {
216                             mapValue.put("name", input.getKey());
217                         }
218                     }
219                     return returnValue;
220                 }
221             };
222             return FluentIterable.from(map.entrySet()).transform(entry2Value).toList();
223         }
224 
225         return null;
226     }
227 
228     private Object readOutList(Collection<?> sourceList, TypeDescriptor mapValueTypeDescriptor) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException, ConfigurationParsingException {
229         if (sourceList == null) {
230             return null;
231         }
232 
233         Class expectedElementType = mapValueTypeDescriptor.getType();
234         List<Object> targetList = new ArrayList<>(sourceList.size());
235         for (Object sourceElement : sourceList) {
236             Object value = readValue(sourceElement, mapValueTypeDescriptor);
237             if (expectedElementType.isAssignableFrom(value.getClass())) {
238                 targetList.add(value);
239             } else {
240                 log.warn(String.format("The element %s is incompatible with the collection's generic signature and will be ignored, expected %s but encountered %s", sourceElement, expectedElementType, value.getClass()));
241             }
242         }
243 
244         return targetList;
245     }
246 
247     private Object readOutMap(Map<String, Object> sourceMap, TypeDescriptor listElementTypeDescriptor) throws IllegalAccessException, InstantiationException, InvocationTargetException, NoSuchMethodException, ConfigurationParsingException {
248         if (sourceMap == null) {
249             return null;
250         }
251 
252         Map<String, Object> targetMap = new LinkedHashMap<>(sourceMap.size());
253 
254         for (Map.Entry<String, Object> sourceEntry : sourceMap.entrySet()) {
255             Object value = readValue(sourceEntry.getValue(), listElementTypeDescriptor);
256             if (value != null) {
257 
258                 // feed name property from key if no name property is configured
259                 PropertyDescriptor namePropertyDescriptor = propertyUtils.getPropertyDescriptor(value, "name");
260                 if (namePropertyDescriptor != null && beanUtils.getProperty(value, "name") == null) {
261                     beanUtils.setProperty(value, "name", sourceEntry.getKey());
262                 }
263             }
264 
265             targetMap.put(sourceEntry.getKey(), value);
266         }
267         return targetMap;
268     }
269 
270     private TypeDescriptor resolveType(Map<String, Object> map, TypeDescriptor defaultType) throws ConfigurationParsingException {
271         final TypeDescriptor targetType;
272         // 1. Resolve target type from configured class property in the map, fallback to defaultType otherwise
273         if (map.containsKey("class")) {
274             String className = (String) map.get("class");
275             ClassFactory classFactory = Classes.getClassFactory();
276             try {
277                 Class<?> loadClass = classFactory.forName(className);
278                 targetType = mapping.getTypeDescriptor(loadClass);
279             } catch (ClassNotFoundException e) {
280                 throw new ConfigurationParsingException("The classname [" + className + "] in the class attribute could not be found in the classpath", e);
281             }
282         } else {
283             targetType = defaultType;
284         }
285 
286         // 2. Get implementation according to type mappings
287         final TypeDescriptor mappedType;
288         try {
289             // ComponentProvider.getImplementation is actually where the type mapping happens (based on info from module descriptors)
290             // TypeMapping.getTypeDescriptor() _just_ gets us a descriptor of the given class, unlike its name suggests
291             final Class<?> implClass = componentProvider.getImplementation(targetType.getType());
292             mappedType = mapping.getTypeDescriptor(implClass);
293         } catch (ClassNotFoundException e) {
294             throw new ConfigurationParsingException("We don't know the implementation type to be used for [" + targetType.getType() + "]", e);
295         }
296 
297         // 3. If componentProvider has no mapping, we have a few defaults here ...
298         if (DEFAULT_TYPES.containsKey(mappedType.getType())) {
299             return mapping.getTypeDescriptor(DEFAULT_TYPES.get(mappedType.getType()));
300         }
301 
302         return mappedType;
303     }
304 }