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.map2bean;
35
36 import static info.magnolia.transformer.TransformationProblem.*;
37 import static java.util.stream.Collectors.toList;
38
39 import info.magnolia.jcr.node2bean.PropertyTypeDescriptor;
40 import info.magnolia.jcr.node2bean.TypeDescriptor;
41 import info.magnolia.jcr.node2bean.TypeMapping;
42 import info.magnolia.jcr.node2bean.impl.PreConfiguredBeanUtils;
43 import info.magnolia.objectfactory.ClassFactory;
44 import info.magnolia.objectfactory.Classes;
45 import info.magnolia.objectfactory.ComponentProvider;
46 import info.magnolia.transformer.ToBeanTransformer;
47 import info.magnolia.transformer.TransformationProblem;
48 import info.magnolia.transformer.TransformationResult;
49
50 import java.beans.FeatureDescriptor;
51 import java.lang.reflect.InvocationTargetException;
52 import java.lang.reflect.Method;
53 import java.util.ArrayList;
54 import java.util.Collection;
55 import java.util.Collections;
56 import java.util.HashMap;
57 import java.util.LinkedHashMap;
58 import java.util.LinkedList;
59 import java.util.List;
60 import java.util.Map;
61 import java.util.Objects;
62 import java.util.stream.Stream;
63
64 import javax.inject.Inject;
65
66 import org.apache.commons.beanutils.ConversionException;
67 import org.apache.commons.beanutils.Converter;
68 import org.apache.commons.beanutils.PropertyUtilsBean;
69 import org.apache.commons.lang3.StringUtils;
70 import org.slf4j.Logger;
71 import org.slf4j.LoggerFactory;
72
73
74
75
76
77 @SuppressWarnings("unchecked")
78 public class Map2BeanTransformer implements ToBeanTransformer<Map<String, Object>> {
79
80 private static final Logger log = LoggerFactory.getLogger(Map2BeanTransformer.class);
81
82 private static final Map<Class, Class> DEFAULT_TYPES = new HashMap<>();
83
84 static {
85 DEFAULT_TYPES.put(Object.class, LinkedHashMap.class);
86 DEFAULT_TYPES.put(Map.class, LinkedHashMap.class);
87 DEFAULT_TYPES.put(Collection.class, LinkedList.class);
88
89 }
90
91 private final ComponentProvider componentProvider;
92 private final TypeMapping mapping;
93 private final PreConfiguredBeanUtils beanUtils;
94 private final PropertyUtilsBean propertyUtils;
95
96 @Inject
97 public Map2BeanTransformer(ComponentProvider componentProvider, TypeMapping mapping, PreConfiguredBeanUtils beanUtils) {
98 this.componentProvider = componentProvider;
99 this.mapping = mapping;
100 this.beanUtils = beanUtils;
101 this.propertyUtils = new PropertyUtilsBean();
102 }
103
104 @Override
105 public <T> TransformationResult<T> transform(Map<String, Object> map, Class<T> targetType) {
106
107 final TransformationState state = new TransformationState();
108
109 if (map == null) {
110 state.trackProblem(warning("Map2Bean#transform() has been invoked with a null input, bean will be created out of an empty map"));
111 map = Collections.emptyMap();
112 }
113
114 state.pushEntry("", map, mapping.getTypeDescriptor(targetType));
115
116 final T bean = (T) readValue(state);
117
118 return TransformationResult.transformationResult(bean, state.getProblems());
119 }
120
121 public <T> T toBean(Map<String, Object> map, Class<T> targetType) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException, ConfigurationParsingException {
122 final TransformationResult<T> result = transform(map, targetType);
123
124
125
126
127
128 for (final TransformationProblem problem : result.getProblems()) {
129 switch (problem.getSeverityType()) {
130 case ERROR:
131 log.warn(problem.getMessage(), problem.getException());
132 break;
133 case WARNING:
134 log.info(problem.getMessage());
135 break;
136 }
137 }
138
139 return result.get();
140 }
141
142 private Object readValue(TransformationState state) {
143 final Object source = state.peekValue();
144
145 if (source == null) {
146 return null;
147 }
148
149 if (isSimpleConverterAvailable(source.getClass(), state.peekTypeDescriptor().getType())) {
150 return readSimpleValue(state);
151 } else {
152 return readComplexValue(state);
153 }
154 }
155
156 private Object readSimpleValue(TransformationState state) {
157 final Object source = state.peekValue();
158 final TypeDescriptor targetTypeDescriptor = state.peekTypeDescriptor();
159
160
161
162 if (Class.class.isAssignableFrom(targetTypeDescriptor.getType())) {
163 try {
164 return Classes.getClassFactory().forName((String) source);
165 } catch (ClassNotFoundException e) {
166 state.trackProblem(warning("Failed to resolve a class property due to a missing class: [%s]", e.getMessage()).withException(e));
167 return null;
168 }
169 }
170
171 Object result = source;
172 if (!targetTypeDescriptor.getType().isAssignableFrom(result.getClass())) {
173 try {
174 result = beanUtils.getConvertUtils().convert(result, targetTypeDescriptor.getType());
175 } catch (ConversionException e) {
176 state.trackProblem(error("Failed to convert an instance of [%s] to [%s] due to [%s]", result.getClass().getName(), state.peekTypeDescriptor().getType(), e.getMessage()).withException(e));
177 return null;
178 }
179 }
180
181 return result;
182 }
183
184 private Object readComplexValue(TransformationState state) {
185 final TypeDescriptor targetTypeDescriptor = elaborateCurrentTargetType(state);
186
187 if (Objects.equals(targetTypeDescriptor.getType(), Object.class) || targetTypeDescriptor.isMap()) {
188 return readMap(state);
189 }
190
191 if (targetTypeDescriptor.isCollection()) {
192 return readCollection(state);
193 }
194
195 return readBean(state);
196 }
197
198 private Object readBean(TransformationState state) {
199 final Class<?> targetType = state.peekTypeDescriptor().getType();
200
201 final Object result;
202 try {
203 result = componentProvider.newInstance(targetType);
204 } catch (Exception e) {
205 state.trackProblem(error("Failed to instantiate an object of type [%s] due to [%s], null is returned", targetType, e.getMessage()).withException(e));
206 return null;
207 }
208
209 final List<String> propertyNames =
210 Stream
211 .of(propertyUtils.getPropertyDescriptors(result))
212 .map(FeatureDescriptor::getName)
213 .collect(toList());
214
215 final Map<String, Object> sourceMap = state.peekMap();
216 for (Map.Entry<String, Object> sourceEntry : sourceMap.entrySet()) {
217 String sourcePropertyName = sourceEntry.getKey();
218 Object sourcePropertyValue = sourceEntry.getValue();
219
220 if ("class".equals(sourcePropertyName)) {
221 continue;
222 }
223
224 if (!propertyNames.contains(sourcePropertyName)) {
225 final String missingPropertyMessage = String.format("Property [%s] not found in class [%s], property is not assigned", sourcePropertyName, result.getClass().getName());
226 if (!"name".equalsIgnoreCase(sourcePropertyName)) {
227 state.trackProblem(warning(missingPropertyMessage));
228 } else {
229 log.debug(missingPropertyMessage);
230 }
231
232 continue;
233 }
234
235 try {
236 final PropertyTypeDescriptor propertyTypeDescriptor = mapping.getPropertyTypeDescriptor(targetType, sourcePropertyName);
237 state.pushEntry(sourcePropertyName, sourcePropertyValue, propertyTypeDescriptor);
238 beanUtils.setProperty(result, sourcePropertyName, readValue(state));
239 } catch (ReflectiveOperationException e) {
240 state.trackProblem(error("Failed to resolve and set bean property [%s] due to a reflection operation issue: [%s], bean property is skipped", sourcePropertyName, e.getMessage()).withException(e));
241 } catch (Exception e) {
242 state.trackProblem(error("Failed to resolve and set property [%s] due to an unexpected issue: [%s], bean property is skipped", sourcePropertyName, e.getMessage()).withException(e));
243 } finally {
244 state.popCurrentEntry();
245 }
246 }
247
248
249 if (propertyNames.contains("name")) {
250 try {
251 final String currentTransformedObjectName = state.peekName();
252 if (StringUtils.isBlank(beanUtils.getProperty(result, "name")) && StringUtils.isNotBlank(currentTransformedObjectName)) {
253 beanUtils.setProperty(result, "name", currentTransformedObjectName);
254 }
255 } catch (Exception e) {
256 state.trackProblem(warning("Failed to set a 'name' property from context due to [%s]", e.getMessage()).withException(e));
257 }
258 }
259
260 try {
261 initBean(result);
262 } catch (Exception e) {
263 state.trackProblem(warning("Failed to invoke bean init() method due to %s", e.getMessage()).withException(e));
264 }
265
266 return result;
267 }
268
269
270 private List<Object> readCollection(TransformationState state) {
271 final List<?> sourceList = state.peekList();
272
273 if (sourceList == null) {
274 return null;
275 }
276
277 TypeDescriptor genericTypeDescriptor = state.peek().genericTypeDescriptor;
278 if (genericTypeDescriptor == null) {
279 genericTypeDescriptor = mapping.getTypeDescriptor(Object.class);
280 }
281
282 final List<Object> result = new ArrayList<>(sourceList.size());
283 int index = 0;
284
285 for (Object sourceElement : sourceList) {
286 state.pushEntry(String.valueOf(index), sourceElement, genericTypeDescriptor);
287
288 try {
289 Object value = readValue(state);
290
291
292 if (value == null) {
293 continue;
294 }
295
296 if (genericTypeDescriptor.getType().isAssignableFrom(value.getClass())) {
297 result.add(value);
298 } else {
299 state.trackProblem(warning("Element [%s] of type [%s] may not be added to the collection of type [%s]", sourceElement, sourceElement.getClass().getName(), genericTypeDescriptor.getType()));
300 }
301 } catch (Exception e) {
302 state.trackProblem(error("Failed to process collection entry due to [%s]", e.getMessage()).withException(e));
303 } finally {
304 ++index;
305 state.popCurrentEntry();
306 }
307 }
308
309 return result;
310 }
311
312 private Map<String, Object> readMap(TransformationState state) {
313 final Map<String, Object> sourceMap = state.peekMap();
314
315 if (sourceMap == null) {
316 return null;
317 }
318
319 TypeDescriptor valueTypeDescriptor = state.peek().genericTypeDescriptor;
320 if (valueTypeDescriptor == null) {
321 valueTypeDescriptor = mapping.getTypeDescriptor(Object.class);
322 }
323
324 final Map<String, Object> result = new LinkedHashMap<>(sourceMap.size());
325
326 for (Map.Entry<String, Object> sourceEntry : sourceMap.entrySet()) {
327 state.pushEntry(sourceEntry.getKey(), sourceEntry.getValue(), valueTypeDescriptor);
328
329 try {
330 Object value = readValue(state);
331
332
333 if (value == null) {
334 continue;
335 }
336
337 result.put(sourceEntry.getKey(), value);
338 } catch (Exception e) {
339 state.trackProblem(error("Failed to process map entry due to [%s]", e.getMessage()).withException(e));
340 } finally {
341 state.popCurrentEntry();
342 }
343 }
344
345 return result;
346 }
347
348 private TypeDescriptor elaborateCurrentTargetType(TransformationState state) {
349 final TransformationState.Entry currentEntry = state.peek();
350
351 TypeDescriptor targetType = currentEntry.typeDescriptor;
352
353 if (currentEntry.value instanceof Map) {
354 final Map<String, Object> map = state.peekMap();
355
356
357 if (map.containsKey("class")) {
358 String className = (String) map.get("class");
359 ClassFactory classFactory = Classes.getClassFactory();
360 try {
361 Class<?> loadClass = classFactory.forName(className);
362 targetType = mapping.getTypeDescriptor(loadClass);
363 } catch (ClassNotFoundException e) {
364 state.trackProblem(error("Configured class [%s] is not found in classpath", className).withException(e));
365 targetType = currentEntry.typeDescriptor;
366 }
367 } else {
368 targetType = currentEntry.typeDescriptor;
369 }
370 }
371
372
373 TypeDescriptor result = targetType;
374 try {
375
376
377 final Class<?> implClass = componentProvider.getImplementation(targetType.getType());
378 result = mapping.getTypeDescriptor(implClass);
379 } catch (ClassNotFoundException e) {
380 state.trackProblem(error("Implementation type of [%s] is unknown", targetType.getType()).withException(e));
381 }
382
383
384 if (DEFAULT_TYPES.containsKey(result.getType())) {
385 result = mapping.getTypeDescriptor(DEFAULT_TYPES.get(result.getType()));
386 }
387
388 currentEntry.typeDescriptor = result;
389
390 return result;
391 }
392
393 private boolean isSimpleConverterAvailable(Class<?> sourceType, Class<?> targetType) {
394 final Converter converter = beanUtils.getConvertUtils().lookup(sourceType, targetType);
395
396 if (converter != null) {
397
398 return true;
399 } else {
400
401 if (Objects.equals(targetType, Object.class)) {
402
403 return beanUtils.getConvertUtils().lookup(sourceType) != null;
404 } else if (Objects.equals(targetType, Class.class)) {
405 return true;
406 }
407
408 return false;
409 }
410 }
411
412 private void initBean(Object bean) throws Exception {
413 try {
414 Method initMethod = bean.getClass().getMethod("init");
415 initMethod.invoke(bean);
416 log.debug("{} is initialized", bean);
417 } catch (NoSuchMethodException ignored) {
418 } catch (SecurityException | InvocationTargetException | IllegalAccessException e) {
419 throw new Exception(e);
420 }
421 }
422 }