1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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
69
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
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
127 return true;
128 } else {
129 if (defaultTargetTypeDescriptor.getType() == Object.class) {
130
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
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;
176 }
177
178 if (!propertyNames.contains(sourcePropertyName)) {
179
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
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
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
287 final TypeDescriptor mappedType;
288 try {
289
290
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
298 if (DEFAULT_TYPES.containsKey(mappedType.getType())) {
299 return mapping.getTypeDescriptor(DEFAULT_TYPES.get(mappedType.getType()));
300 }
301
302 return mappedType;
303 }
304 }