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.Classes;
44 import info.magnolia.objectfactory.ComponentProvider;
45 import info.magnolia.transformer.BeanTypeResolver;
46 import info.magnolia.transformer.ToBeanTransformer;
47 import info.magnolia.transformer.TransformationProblem;
48 import info.magnolia.transformer.TransformationResult;
49 import info.magnolia.util.DeprecationUtil;
50
51 import java.beans.FeatureDescriptor;
52 import java.lang.reflect.Array;
53 import java.lang.reflect.InvocationTargetException;
54 import java.lang.reflect.Method;
55 import java.util.ArrayDeque;
56 import java.util.ArrayList;
57 import java.util.Arrays;
58 import java.util.Collection;
59 import java.util.Collections;
60 import java.util.HashMap;
61 import java.util.LinkedHashMap;
62 import java.util.LinkedHashSet;
63 import java.util.LinkedList;
64 import java.util.List;
65 import java.util.Map;
66 import java.util.Objects;
67 import java.util.Optional;
68 import java.util.Queue;
69 import java.util.Set;
70 import java.util.stream.Stream;
71
72 import javax.inject.Inject;
73
74 import org.apache.commons.beanutils.ConversionException;
75 import org.apache.commons.beanutils.Converter;
76 import org.apache.commons.beanutils.PropertyUtilsBean;
77 import org.apache.commons.lang3.StringUtils;
78 import org.slf4j.Logger;
79 import org.slf4j.LoggerFactory;
80
81
82
83
84
85 @SuppressWarnings("unchecked")
86 public class Map2BeanTransformer implements ToBeanTransformer<Map<String, Object>> {
87
88 private static final Logger log = LoggerFactory.getLogger(Map2BeanTransformer.class);
89
90 private static final Collection<String> IGNORED_DEPRECATED_PACKAGES = Arrays.asList(
91 "info.magnolia.ui.form.field.definition.",
92 "info.magnolia.ui.workbench.column.definition.",
93 "info.magnolia.ui.framework.tools."
94 );
95
96 private static final Map<Class, Class> DEFAULT_TYPES = new HashMap<>();
97
98
99
100
101 public static final String METADATA_PREFIX = "$";
102
103 static {
104 DEFAULT_TYPES.put(Object.class, LinkedHashMap.class);
105 DEFAULT_TYPES.put(Map.class, LinkedHashMap.class);
106 DEFAULT_TYPES.put(Set.class, LinkedHashSet.class);
107 DEFAULT_TYPES.put(List.class, ArrayList.class);
108 DEFAULT_TYPES.put(Queue.class, ArrayDeque.class);
109 DEFAULT_TYPES.put(Collection.class, LinkedList.class);
110 }
111
112 private final ComponentProvider componentProvider;
113 private final TypeMapping mapping;
114 private final PreConfiguredBeanUtils beanUtils;
115 private final BeanTypeResolver beanTypeResolver;
116 private final PropertyUtilsBean propertyUtils;
117
118 @Inject
119 public Map2BeanTransformer(ComponentProvider componentProvider, TypeMapping mapping, PreConfiguredBeanUtils beanUtils,
120 BeanTypeResolver beanTypeResolver) {
121 this.componentProvider = componentProvider;
122 this.mapping = mapping;
123 this.beanUtils = beanUtils;
124 this.beanTypeResolver = beanTypeResolver;
125 this.propertyUtils = new PropertyUtilsBean();
126 }
127
128
129
130
131 @Deprecated
132 public Map2BeanTransformer(ComponentProvider componentProvider, TypeMapping mapping, PreConfiguredBeanUtils beanUtils) {
133 this(componentProvider, mapping, beanUtils, new BeanTypeResolver());
134 }
135
136 @Override
137 public <T> TransformationResult<T> transform(Map<String, Object> map, Class<T> targetType) {
138
139 final TransformationStatetate.html#TransformationState">TransformationState state = new TransformationState();
140
141 if (map == null) {
142 state.trackProblem(warning("Map2Bean#transform() has been invoked with a null input, bean will be created out of an empty map"));
143 map = Collections.emptyMap();
144 }
145
146 state.pushEntry("", map, mapping.getTypeDescriptor(targetType));
147
148 final T bean = (T) readValue(state);
149
150 return TransformationResult.transformationResult(bean, state.getProblems());
151 }
152
153 public <T> T toBean(Map<String, Object> map, Class<T> targetType) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException, ConfigurationParsingException {
154 final TransformationResult<T> result = transform(map, targetType);
155
156
157
158
159
160 for (final TransformationProblem problem : result.getProblems()) {
161 switch (problem.getSeverityType()) {
162 case ERROR:
163 log.warn(problem.getMessage(), problem.getException());
164 break;
165 case WARNING:
166 log.info(problem.getMessage());
167 break;
168 }
169 }
170
171 return result.get();
172 }
173
174 protected Object readValue(TransformationState state) {
175 final Object source = state.peekValue();
176
177 if (source == null) {
178 return null;
179 }
180 Class<?> targetType = state.peekTypeDescriptor().getType();
181 if (DeprecationUtil.isDeprecated(targetType)) {
182 reportDeprecation(state, targetType);
183 } else {
184 DeprecationUtil.findFirstEncounteredDeprecatedSuperType(targetType)
185 .ifPresent(deprecatedSuperType -> reportDeprecation(state, targetType, deprecatedSuperType));
186 }
187
188 if (isSimpleConverterAvailable(source.getClass(), targetType)) {
189 return readSimpleValue(state);
190 } else {
191 return readComplexValue(state);
192 }
193 }
194
195 private Object readSimpleValue(TransformationState state) {
196 final Object source = state.peekValue();
197 final TypeDescriptor targetTypeDescriptor = state.peekTypeDescriptor();
198 final Class<?> targetType = targetTypeDescriptor.getType();
199
200
201
202 if (Class.class.isAssignableFrom(targetType)) {
203 try {
204 return Classes.getClassFactory().forName((String) source);
205 } catch (ClassNotFoundException e) {
206 state.trackProblem(warning("Failed to resolve a class property due to a missing class: [%s]", e.getMessage()).withException(e));
207 return null;
208 }
209 }
210
211 Object result = source;
212 if (!targetType.isAssignableFrom(result.getClass())) {
213 try {
214 result = beanUtils.getConvertUtils().convert(result, targetType);
215 } catch (ConversionException e) {
216 state.trackProblem(error("Failed to convert an instance of [%s] to [%s] due to [%s]",
217 result.getClass().getName(), targetType, e.getMessage()).withException(e));
218 return null;
219 }
220 }
221
222 return result;
223 }
224
225 private Object readComplexValue(TransformationState state) {
226 final TypeDescriptor targetTypeDescriptor = elaborateCurrentTargetType(state);
227
228 if (Objects.equals(targetTypeDescriptor.getType(), Object.class) || targetTypeDescriptor.isMap()) {
229 return readMap(state);
230 }
231
232 if (targetTypeDescriptor.isCollection()) {
233 return readCollection(state);
234 }
235
236 if (targetTypeDescriptor.isArray()) {
237 Collection collection = readCollection(state);
238 return collection == null ? null : collection.toArray((Object[]) Array.newInstance(targetTypeDescriptor.getType().getComponentType(), collection.size()));
239 }
240
241 return readBean(state);
242 }
243
244 private Object readBean(TransformationState state) {
245 final Class<?> targetType = state.peekTypeDescriptor().getType();
246
247 final Object result;
248 try {
249 result = createInstance(targetType);
250 } catch (Exception e) {
251 if (!isSimpleConstructorPresent(targetType)) {
252 state.trackProblem(error("No simple constructor found for type [%s], null is returned", targetType).withException(e));
253 } else {
254 state.trackProblem(error("Failed to instantiate an object of type [%s] due to [%s], null is returned", targetType, e.getMessage()).withException(e));
255 }
256 return null;
257 }
258
259 final Map<String, Object> sourceMap = state.peekMap();
260 if (sourceMap == null) {
261 state.trackProblem(error("Skipping bean of type [%s] due to invalid source map [%s], null is returned", targetType, state.peekValue()));
262 return null;
263 }
264
265 final List<String> propertyNames = Stream
266 .of(propertyUtils.getPropertyDescriptors(result))
267 .map(FeatureDescriptor::getName)
268 .collect(toList());
269
270 for (Map.Entry<String, Object> sourceEntry : sourceMap.entrySet()) {
271 String sourcePropertyName = sourceEntry.getKey();
272 Object sourcePropertyValue = sourceEntry.getValue();
273
274 if ("class".equals(sourcePropertyName) || sourcePropertyName.startsWith(METADATA_PREFIX)) {
275 continue;
276 }
277
278 if (!propertyNames.contains(sourcePropertyName)) {
279 final String missingPropertyMessage = String.format("Property [%s] not found in class [%s], property is not assigned", sourcePropertyName, result.getClass().getName());
280 if (!"name".equalsIgnoreCase(sourcePropertyName)) {
281 handleMissingProperty(state, result, sourcePropertyName, sourcePropertyValue, missingPropertyMessage);
282 } else {
283 log.debug(missingPropertyMessage);
284 }
285 continue;
286 }
287
288 try {
289
290 if (sourcePropertyValue != null) {
291 Collection<Method> deprecatedMethods = DeprecationUtil.getDeprecatedReadMethods(mapping, targetType, sourcePropertyName);
292 deprecatedMethods.forEach(method -> reportDeprecation(state, method));
293 }
294
295 final PropertyTypeDescriptor propertyTypeDescriptor = mapping.getPropertyTypeDescriptor(targetType, sourcePropertyName);
296 state.pushEntry(sourcePropertyName, sourcePropertyValue, propertyTypeDescriptor);
297 if (sourcePropertyValue == null) {
298 state.trackProblem(warning("Property [%s] is set to null in definition.", sourcePropertyName));
299 }
300 beanUtils.setProperty(result, sourcePropertyName, readValue(state));
301 } catch (ReflectiveOperationException e) {
302 state.trackProblem(error("Failed to resolve and set bean property [%s] due to a reflection operation issue: [%s], bean property is skipped",
303 sourcePropertyName, Optional.ofNullable(e.getMessage()).orElse(e.getCause().getMessage())
304 ).withException(e));
305 } catch (Exception e) {
306 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));
307 } finally {
308 state.popCurrentEntry();
309 }
310 }
311
312
313 if (propertyNames.contains("name")) {
314 try {
315 final String currentTransformedObjectName = state.peekName();
316 if (StringUtils.isBlank(beanUtils.getProperty(result, "name")) && StringUtils.isNotBlank(currentTransformedObjectName)) {
317 beanUtils.setProperty(result, "name", currentTransformedObjectName);
318 }
319 } catch (Exception e) {
320 state.trackProblem(warning("Failed to set a 'name' property from context due to [%s]", e.getMessage()).withException(e));
321 }
322 }
323
324 try {
325 initBean(result);
326 } catch (Exception e) {
327 state.trackProblem(warning("Failed to invoke bean init() method due to %s", e.getMessage()).withException(e));
328 }
329
330 return result;
331 }
332
333 protected void handleMissingProperty(TransformationState state, Object bean, String sourcePropertyName, Object sourcePropertyValue, String missingPropertyMessage) {
334 state.trackProblem(warning(missingPropertyMessage));
335 }
336
337 protected Object createInstance(Class<?> targetType) {
338 return componentProvider.newInstance(targetType);
339 }
340
341 protected Collection<Object> readCollection(TransformationState state) {
342 final List<?> sourceList = state.peekList();
343
344 if (sourceList == null) {
345 if (state.peekValue() != null) {
346 state.trackProblem(warning("Failed to resolve property [%s], expected a collection", state.peekName()));
347 }
348 return null;
349 }
350
351 TypeDescriptor genericTypeDescriptor = state.peek().genericTypeDescriptor;
352 if (genericTypeDescriptor == null) {
353 genericTypeDescriptor = mapping.getTypeDescriptor(Object.class);
354 }
355
356 final Class<?> collectionType = state.peekTypeDescriptor().getType();
357 final Collection<Object> result = instantiateCollection(collectionType);
358 int index = 0;
359
360 for (Object sourceElement : sourceList) {
361 state.pushEntry(String.valueOf(index), sourceElement, genericTypeDescriptor);
362
363 try {
364 Class<?> targetType = state.peekTypeDescriptor().getType();
365 Object rawValue = state.peekValue();
366 Class<?> sourceType = rawValue.getClass();
367
368 if (String.class.equals(targetType) && !isSimpleConverterAvailable(sourceType, targetType)) {
369 state.trackProblem(warning("Ignoring element [%s] of type [%s], because it is incompatible with " +
370 "collection of type [%s]", rawValue, sourceType.getName(), targetType.getName()));
371 continue;
372 }
373
374 Object value = readValue(state);
375
376
377 if (value == null) {
378 continue;
379 }
380
381 if (genericTypeDescriptor.getType().isAssignableFrom(value.getClass())) {
382 result.add(value);
383 } else {
384 state.trackProblem(warning("Element [%s] of type [%s] may not be added to the collection of type [%s]", value, value.getClass().getName(), genericTypeDescriptor.getType()));
385 }
386 } catch (Exception e) {
387 state.trackProblem(error("Failed to process collection entry due to [%s]", e.getMessage() != null ? e.getMessage() : e)
388 .withException(e));
389 } finally {
390 ++index;
391 state.popCurrentEntry();
392 }
393 }
394
395 return result;
396 }
397
398 private Collection<Object> instantiateCollection(Class<?> collectionType) {
399 try {
400 return (Collection<Object>) DEFAULT_TYPES.getOrDefault(collectionType, collectionType).newInstance();
401 } catch (Exception e) {
402 log.debug("Failed to instantiate a collection of type {}, falling back to an ArrayList", collectionType, e);
403 return new ArrayList<>();
404 }
405 }
406
407 protected Map<String, Object> readMap(TransformationState state) {
408 final Map<String, Object> sourceMap = state.peekMap();
409
410 if (sourceMap == null) {
411 if (state.peekValue() != null) {
412 state.trackProblem(warning("Failed to resolve property [%s], expected a map", state.peekName()));
413 }
414 return null;
415 }
416
417 TypeDescriptor valueTypeDescriptor = state.peek().genericTypeDescriptor;
418 if (valueTypeDescriptor == null) {
419 valueTypeDescriptor = mapping.getTypeDescriptor(Object.class);
420 }
421
422 final Map<String, Object> result = new LinkedHashMap<>(sourceMap.size());
423
424 for (Map.Entry<String, Object> sourceEntry : sourceMap.entrySet()) {
425
426
427
428 String entryKey = String.valueOf(sourceEntry.getKey());
429 state.pushEntry(entryKey, sourceEntry.getValue(), valueTypeDescriptor);
430
431 try {
432 Object value = readValue(state);
433
434
435 if (value == null) {
436 continue;
437 }
438
439 result.put(entryKey, value);
440 } catch (Exception e) {
441 state.trackProblem(error("Failed to process map entry due to [%s]", e.getMessage()).withException(e));
442 } finally {
443 state.popCurrentEntry();
444 }
445 }
446
447 return result;
448 }
449
450 private TypeDescriptor elaborateCurrentTargetType(TransformationState state) {
451 final TransformationState.Entry currentEntry = state.peek();
452
453 TypeDescriptor targetType = currentEntry.typeDescriptor;
454
455 if (currentEntry.value instanceof Map) {
456 final Map<String, Object> map = state.peekMap();
457
458
459 targetType = beanTypeResolver.resolve(targetType, map).map(mapping::getTypeDescriptor).orElse(currentEntry.typeDescriptor);
460 }
461
462
463 TypeDescriptor result = targetType;
464 try {
465
466
467 final Class<?> implClass = componentProvider.getImplementation(targetType.getType());
468 result = mapping.getTypeDescriptor(implClass);
469 } catch (ClassNotFoundException e) {
470 state.trackProblem(error("Implementation type of [%s] is unknown", targetType.getType()).withException(e));
471 }
472
473
474 if (DEFAULT_TYPES.containsKey(result.getType())) {
475 result = mapping.getTypeDescriptor(DEFAULT_TYPES.get(result.getType()));
476 }
477
478 currentEntry.typeDescriptor = result;
479
480 return result;
481 }
482
483 private boolean isSimpleConverterAvailable(Class<?> sourceType, Class<?> targetType) {
484 final Converter converter = beanUtils.getConvertUtils().lookup(sourceType, targetType);
485
486 if (converter != null) {
487
488 boolean isCollectionOrMap = Collection.class.isAssignableFrom(sourceType) || Map.class.isAssignableFrom(sourceType);
489
490 return !(String.class.equals(targetType) && isCollectionOrMap && isStringConverter(converter));
491 } else {
492
493 if (Objects.equals(targetType, Object.class)) {
494
495 return beanUtils.getConvertUtils().lookup(sourceType) != null;
496 } else if (Objects.equals(targetType, Class.class)) {
497 return true;
498 }
499
500 return false;
501 }
502 }
503
504 private boolean isStringConverter(Converter converter) {
505
506
507
508 return converter.toString().contains("StringConverter");
509 }
510
511 private void initBean(Object bean) throws Exception {
512 try {
513 Method initMethod = bean.getClass().getMethod("init");
514 initMethod.invoke(bean);
515 log.debug("{} is initialized", bean);
516 } catch (NoSuchMethodException ignored) {
517 } catch (SecurityException | InvocationTargetException | IllegalAccessException e) {
518 throw new Exception(e);
519 }
520 }
521
522 private void reportDeprecation(TransformationState state, Class<?> actualClass, Class<?> deprecatedParentClass) {
523 String deprecationWarning = DeprecationUtil.getDeprecationMessage(actualClass, deprecatedParentClass);
524 state.trackProblem(deprecated(deprecationWarning));
525 }
526
527 private void reportDeprecation(TransformationState state, Class<?> deprecatedClass) {
528 if (IGNORED_DEPRECATED_PACKAGES.stream().noneMatch(packageName -> deprecatedClass.getName().startsWith(packageName))) {
529 String deprecationWarning = DeprecationUtil.getDeprecationMessage(deprecatedClass);
530 state.trackProblem(deprecated(deprecationWarning));
531 }
532 }
533
534 private void reportDeprecation(TransformationState state, Method deprecatedMethod) {
535 String deprecationWarning = DeprecationUtil.getDeprecationMessage(deprecatedMethod);
536 state.trackProblem(deprecated(deprecationWarning));
537 }
538
539 private boolean isSimpleConstructorPresent(Class<?> targetType) {
540 return Arrays.stream(targetType.getConstructors()).anyMatch(constructor -> constructor.getParameterCount() == 0);
541 }
542
543 }