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.lang.reflect.InvocationTargetException;
45  import java.util.ArrayList;
46  import java.util.Collection;
47  import java.util.Collections;
48  import java.util.HashMap;
49  import java.util.LinkedHashMap;
50  import java.util.LinkedList;
51  import java.util.List;
52  import java.util.Map;
53  
54  import javax.inject.Inject;
55  
56  import org.apache.commons.beanutils.Converter;
57  import org.slf4j.Logger;
58  import org.slf4j.LoggerFactory;
59  
60  /**
61   * Transforms a map to a bean graph. Resolves types through {@link TypeMapping} and {@link PreConfiguredBeanUtils}
62   * to mimic what Node2BeanTransformer does.
63   */
64  public class Map2BeanTransformer {
65  
66      private static final Map<Class, Class> DEFAULT_TYPES = new HashMap<>();
67  
68      static {
69          DEFAULT_TYPES.put(Object.class, LinkedHashMap.class);
70          DEFAULT_TYPES.put(Map.class, LinkedHashMap.class);
71          DEFAULT_TYPES.put(Collection.class, LinkedList.class);
72          // TODO we need to have more (List, Queue, etc) and check for "isAssignableFrom" and/or isInterface
73      }
74  
75      private static final Logger log = LoggerFactory.getLogger(Map2BeanTransformer.class);
76  
77      private final ComponentProvider componentProvider;
78      private final TypeMapping mapping;
79      private final PreConfiguredBeanUtils beanUtils;
80  
81      @Inject
82      public Map2BeanTransformer(ComponentProvider componentProvider, TypeMapping mapping, PreConfiguredBeanUtils beanUtils) {
83          this.componentProvider = componentProvider;
84          this.mapping = mapping;
85          this.beanUtils = beanUtils;
86      }
87  
88      public <T> T toBean(Map<String, Object> map, Class<T> defaultRootType) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException, ConfigurationParsingException {
89          if (map == null) {
90              log.debug("toBean was invoked with a null map, will try to create bean out of an empty map.");
91              map = Collections.emptyMap();
92          }
93          TypeDescriptor defaultRootTypeDescriptor = mapping.getTypeDescriptor(defaultRootType);
94  
95          return readValue(map, defaultRootTypeDescriptor);
96      }
97  
98      private <T> T readValue(Object source, TypeDescriptor defaultTargetTypeDescriptor) throws IllegalAccessException, NoSuchMethodException, InvocationTargetException, InstantiationException, ConfigurationParsingException {
99          if (source == null) {
100             return null;
101         }
102 
103         TypeDescriptor sourceTypeDescriptor = mapping.getTypeDescriptor(source.getClass());
104 
105         if (needSimpleMapping(sourceTypeDescriptor, defaultTargetTypeDescriptor)) {
106             return readSimpleValue(source, defaultTargetTypeDescriptor);
107         } else {
108             return readComplexValue(source, defaultTargetTypeDescriptor);
109         }
110     }
111 
112     private boolean needSimpleMapping(TypeDescriptor sourceTypeDescriptor, TypeDescriptor defaultTargetTypeDescriptor) {
113         Converter converter = beanUtils.getConvertUtils().lookup(sourceTypeDescriptor.getType(), defaultTargetTypeDescriptor.getType());
114 
115         if (converter != null) {
116             // if direct conversion can be used no complex mapping is needed
117             return true;
118         } else {
119             if (defaultTargetTypeDescriptor.getType() == Object.class) { // untyped
120                 // if there the target is untyped but the source has a convertable type
121                 return beanUtils.getConvertUtils().lookup(sourceTypeDescriptor.getType()) != null;
122             }
123 
124             return false;
125         }
126     }
127 
128     private <T> T readSimpleValue(Object source, TypeDescriptor defaultTargetTypeDescriptor) {
129         return (T) beanUtils.getConvertUtils().convert(source, defaultTargetTypeDescriptor.getType());
130     }
131 
132     private <T> T readComplexValue(Object source, TypeDescriptor defaultTargetTypeDescriptor) throws IllegalAccessException, InstantiationException, InvocationTargetException, NoSuchMethodException, ConfigurationParsingException {
133         if (source instanceof Collection<?>) {
134             return (T) readOutList((Collection<Object>) source, mapping.getTypeDescriptor(Object.class));
135         }
136         Map<String, Object> sourceMap = (Map<String, Object>) source;
137         TypeDescriptor targetTypeDescriptor = resolveType(sourceMap, defaultTargetTypeDescriptor);
138         targetTypeDescriptor = resolveType(sourceMap, targetTypeDescriptor);
139         if (targetTypeDescriptor.getType() == Object.class || targetTypeDescriptor.isMap()) {
140             return (T) readOutMap(sourceMap, mapping.getTypeDescriptor(Object.class));
141         } else {
142             return (T) readOutObject(sourceMap, targetTypeDescriptor);
143         }
144     }
145 
146     private <T> T readOutObject(Map<String, Object> sourceMap, TypeDescriptor targetType) throws IllegalAccessException, NoSuchMethodException, InvocationTargetException, InstantiationException, ConfigurationParsingException {
147         T targetObj = (T) componentProvider.newInstance(targetType.getType());
148 
149         Map<String, String> targetDescription = beanUtils.describe(targetObj);
150 
151         for (Map.Entry<String, Object> sourceEntry : sourceMap.entrySet()) {
152             String sourcePropertyName = sourceEntry.getKey();
153             Object sourcePropertyValue = sourceEntry.getValue();
154 
155             if ("class".equals(sourcePropertyName)) {
156                 continue; // skip class property because its only meta data
157             }
158 
159             if (!targetDescription.containsKey(sourcePropertyName)) {
160                 // We should not just ignore this - see MAGNOLIA-6196
161                 log.warn("Property " + sourcePropertyName + " not found in class " + targetObj.getClass());
162                 continue;
163             }
164 
165             PropertyTypeDescriptor propertyTypeDescriptor = mapping.getPropertyTypeDescriptor(targetType.getType(), sourcePropertyName);
166 
167             Object value;
168             if (propertyTypeDescriptor.isCollection()) {
169                 value = readOutList((List<Object>) sourcePropertyValue, propertyTypeDescriptor.getCollectionEntryType());
170             } else if (propertyTypeDescriptor.isMap()) {
171                 value = readOutMap((Map<String, Object>) sourcePropertyValue, propertyTypeDescriptor.getCollectionEntryType());
172             } else {
173                 value = readValue(sourcePropertyValue, propertyTypeDescriptor.getType());
174             }
175 
176             beanUtils.setProperty(targetObj, sourcePropertyName, value);
177         }
178 
179         return targetObj;
180     }
181 
182     private Object readOutList(Collection<?> sourceList, TypeDescriptor mapValueTypeDescriptor) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException, ConfigurationParsingException {
183         if (sourceList == null) {
184             return null;
185         }
186 
187         List<Object> targetList = new ArrayList<>(sourceList.size());
188 
189         for (Object sourceElement : sourceList) {
190             targetList.add(readValue(sourceElement, mapValueTypeDescriptor));
191         }
192 
193         return targetList;
194     }
195 
196     private Object readOutMap(Map<String, Object> sourceMap, TypeDescriptor listElementTypeDescriptor) throws IllegalAccessException, InstantiationException, InvocationTargetException, NoSuchMethodException, ConfigurationParsingException {
197         if (sourceMap == null) {
198             return null;
199         }
200 
201         Map<String, Object> targetMap = new LinkedHashMap<>(sourceMap.size());
202 
203         for (Map.Entry<String, Object> sourceEntry : sourceMap.entrySet()) {
204             Object value = readValue(sourceEntry.getValue(), listElementTypeDescriptor);
205 
206             // feed name property from key if no name property is configured
207             Map<String, String> valueDescription = beanUtils.describe(value);
208             if (valueDescription.containsKey("name") && beanUtils.getProperty(value, "name") == null) {
209                 beanUtils.setProperty(value, "name", sourceEntry.getKey());
210             }
211 
212             targetMap.put(sourceEntry.getKey(), value);
213         }
214         return targetMap;
215     }
216 
217     private TypeDescriptor resolveType(Map<String, Object> map, TypeDescriptor defaultType) throws ConfigurationParsingException {
218         final TypeDescriptor targetType;
219         // 1. Resolve target type from configured class property in the map, fallback to defaultType otherwise
220         if (map.containsKey("class")) {
221             String className = (String) map.get("class");
222             ClassFactory classFactory = Classes.getClassFactory();
223             try {
224                 Class<?> loadClass = classFactory.forName(className);
225                 targetType = mapping.getTypeDescriptor(loadClass);
226             } catch (ClassNotFoundException e) {
227                 throw new ConfigurationParsingException("The classname [" + className + "] in the class attribute could not be found in the classpath", e);
228             }
229         } else {
230             targetType = defaultType;
231         }
232 
233         // 2. Get implementation according to type mappings
234         final TypeDescriptor mappedType;
235         try {
236             // ComponentProvider.getImplementation is actually where the type mapping happens (based on info from module descriptors)
237             // TypeMapping.getTypeDescriptor() _just_ gets us a descriptor of the given class, unlike its name suggests
238             final Class<?> implClass = componentProvider.getImplementation(targetType.getType());
239             mappedType = mapping.getTypeDescriptor(implClass);
240         } catch (ClassNotFoundException e) {
241             throw new ConfigurationParsingException("We don't know the implementation type to be used for [" + targetType.getType() + "]", e);
242         }
243 
244         // 3. If componentProvider has no mapping, we have a few defaults here ...
245         if (DEFAULT_TYPES.containsKey(mappedType.getType())) {
246             return mapping.getTypeDescriptor(DEFAULT_TYPES.get(mappedType.getType()));
247         }
248 
249         return mappedType;
250     }
251 }