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 state.trackProblem(error("Failed to instantiate an object of type [%s] due to [%s], null is returned", targetType, e.getMessage()).withException(e));
252 return null;
253 }
254
255 final Map<String, Object> sourceMap = state.peekMap();
256 if (sourceMap == null) {
257 state.trackProblem(error("Skipping bean of type [%s] due to invalid source map [%s], null is returned", targetType, state.peekValue()));
258 return null;
259 }
260
261 final List<String> propertyNames = Stream
262 .of(propertyUtils.getPropertyDescriptors(result))
263 .map(FeatureDescriptor::getName)
264 .collect(toList());
265
266 for (Map.Entry<String, Object> sourceEntry : sourceMap.entrySet()) {
267 String sourcePropertyName = sourceEntry.getKey();
268 Object sourcePropertyValue = sourceEntry.getValue();
269
270 if ("class".equals(sourcePropertyName) || sourcePropertyName.startsWith(METADATA_PREFIX)) {
271 continue;
272 }
273
274 if (!propertyNames.contains(sourcePropertyName)) {
275 final String missingPropertyMessage = String.format("Property [%s] not found in class [%s], property is not assigned", sourcePropertyName, result.getClass().getName());
276 if (!"name".equalsIgnoreCase(sourcePropertyName)) {
277 handleMissingProperty(state, result, sourcePropertyName, sourcePropertyValue, missingPropertyMessage);
278 } else {
279 log.debug(missingPropertyMessage);
280 }
281 continue;
282 }
283
284 try {
285
286 if (sourcePropertyValue != null) {
287 Collection<Method> deprecatedMethods = DeprecationUtil.getDeprecatedReadMethods(mapping, targetType, sourcePropertyName);
288 deprecatedMethods.forEach(method -> reportDeprecation(state, method));
289 }
290
291 final PropertyTypeDescriptor propertyTypeDescriptor = mapping.getPropertyTypeDescriptor(targetType, sourcePropertyName);
292 state.pushEntry(sourcePropertyName, sourcePropertyValue, propertyTypeDescriptor);
293 if (sourcePropertyValue == null) {
294 state.trackProblem(warning("Property [%s] is set to null in definition.", sourcePropertyName));
295 }
296 beanUtils.setProperty(result, sourcePropertyName, readValue(state));
297 } catch (ReflectiveOperationException e) {
298 state.trackProblem(error("Failed to resolve and set bean property [%s] due to a reflection operation issue: [%s], bean property is skipped",
299 sourcePropertyName, Optional.ofNullable(e.getMessage()).orElse(e.getCause().getMessage())
300 ).withException(e));
301 } catch (Exception e) {
302 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));
303 } finally {
304 state.popCurrentEntry();
305 }
306 }
307
308
309 if (propertyNames.contains("name")) {
310 try {
311 final String currentTransformedObjectName = state.peekName();
312 if (StringUtils.isBlank(beanUtils.getProperty(result, "name")) && StringUtils.isNotBlank(currentTransformedObjectName)) {
313 beanUtils.setProperty(result, "name", currentTransformedObjectName);
314 }
315 } catch (Exception e) {
316 state.trackProblem(warning("Failed to set a 'name' property from context due to [%s]", e.getMessage()).withException(e));
317 }
318 }
319
320 try {
321 initBean(result);
322 } catch (Exception e) {
323 state.trackProblem(warning("Failed to invoke bean init() method due to %s", e.getMessage()).withException(e));
324 }
325
326 return result;
327 }
328
329 protected void handleMissingProperty(TransformationState state, Object bean, String sourcePropertyName, Object sourcePropertyValue, String missingPropertyMessage) {
330 state.trackProblem(warning(missingPropertyMessage));
331 }
332
333 protected Object createInstance(Class<?> targetType) {
334 return componentProvider.newInstance(targetType);
335 }
336
337
338 protected Collection<Object> readCollection(TransformationState state) {
339 final List<?> sourceList = state.peekList();
340
341 if (sourceList == null) {
342 if (state.peekValue() != null) {
343 state.trackProblem(warning("Failed to resolve property [%s], expected a collection", state.peekName()));
344 }
345 return null;
346 }
347
348 TypeDescriptor genericTypeDescriptor = state.peek().genericTypeDescriptor;
349 if (genericTypeDescriptor == null) {
350 genericTypeDescriptor = mapping.getTypeDescriptor(Object.class);
351 }
352
353 final Class<?> collectionType = state.peekTypeDescriptor().getType();
354 final Collection<Object> result = instantiateCollection(collectionType);
355 int index = 0;
356
357 for (Object sourceElement : sourceList) {
358 state.pushEntry(String.valueOf(index), sourceElement, genericTypeDescriptor);
359
360 try {
361 Class<?> targetType = state.peekTypeDescriptor().getType();
362 Object rawValue = state.peekValue();
363 Class<?> sourceType = rawValue.getClass();
364
365 if (String.class.equals(targetType) && !isSimpleConverterAvailable(sourceType, targetType)) {
366 state.trackProblem(warning("Ignoring element [%s] of type [%s], because it is incompatible with " +
367 "collection of type [%s]", rawValue, sourceType.getName(), targetType.getName()));
368 continue;
369 }
370
371 Object value = readValue(state);
372
373
374 if (value == null) {
375 continue;
376 }
377
378 if (genericTypeDescriptor.getType().isAssignableFrom(value.getClass())) {
379 result.add(value);
380 } else {
381 state.trackProblem(warning("Element [%s] of type [%s] may not be added to the collection of type [%s]", value, value.getClass().getName(), genericTypeDescriptor.getType()));
382 }
383 } catch (Exception e) {
384 state.trackProblem(error("Failed to process collection entry due to [%s]", e.getMessage() != null ? e.getMessage() : e)
385 .withException(e));
386 } finally {
387 ++index;
388 state.popCurrentEntry();
389 }
390 }
391
392 return result;
393 }
394
395 private Collection<Object> instantiateCollection(Class<?> collectionType) {
396 try {
397 return (Collection<Object>) DEFAULT_TYPES.getOrDefault(collectionType, collectionType).newInstance();
398 } catch (Exception e) {
399 log.debug("Failed to instantiate a collection of type {}, falling back to an ArrayList", collectionType, e);
400 return new ArrayList<>();
401 }
402 }
403
404 protected Map<String, Object> readMap(TransformationState state) {
405 final Map<String, Object> sourceMap = state.peekMap();
406
407 if (sourceMap == null) {
408 if (state.peekValue() != null) {
409 state.trackProblem(warning("Failed to resolve property [%s], expected a map", state.peekName()));
410 }
411 return null;
412 }
413
414 TypeDescriptor valueTypeDescriptor = state.peek().genericTypeDescriptor;
415 if (valueTypeDescriptor == null) {
416 valueTypeDescriptor = mapping.getTypeDescriptor(Object.class);
417 }
418
419 final Map<String, Object> result = new LinkedHashMap<>(sourceMap.size());
420
421 for (Map.Entry<String, Object> sourceEntry : sourceMap.entrySet()) {
422
423
424
425 String entryKey = String.valueOf(sourceEntry.getKey());
426 state.pushEntry(entryKey, sourceEntry.getValue(), valueTypeDescriptor);
427
428 try {
429 Object value = readValue(state);
430
431
432 if (value == null) {
433 continue;
434 }
435
436 result.put(entryKey, value);
437 } catch (Exception e) {
438 state.trackProblem(error("Failed to process map entry due to [%s]", e.getMessage()).withException(e));
439 } finally {
440 state.popCurrentEntry();
441 }
442 }
443
444 return result;
445 }
446
447 private TypeDescriptor elaborateCurrentTargetType(TransformationState state) {
448 final TransformationState.Entry currentEntry = state.peek();
449
450 TypeDescriptor targetType = currentEntry.typeDescriptor;
451
452 if (currentEntry.value instanceof Map) {
453 final Map<String, Object> map = state.peekMap();
454
455
456 targetType = beanTypeResolver.resolve(targetType, map).map(mapping::getTypeDescriptor).orElse(currentEntry.typeDescriptor);
457 }
458
459
460 TypeDescriptor result = targetType;
461 try {
462
463
464 final Class<?> implClass = componentProvider.getImplementation(targetType.getType());
465 result = mapping.getTypeDescriptor(implClass);
466 } catch (ClassNotFoundException e) {
467 state.trackProblem(error("Implementation type of [%s] is unknown", targetType.getType()).withException(e));
468 }
469
470
471 if (DEFAULT_TYPES.containsKey(result.getType())) {
472 result = mapping.getTypeDescriptor(DEFAULT_TYPES.get(result.getType()));
473 }
474
475 currentEntry.typeDescriptor = result;
476
477 return result;
478 }
479
480 private boolean isSimpleConverterAvailable(Class<?> sourceType, Class<?> targetType) {
481 final Converter converter = beanUtils.getConvertUtils().lookup(sourceType, targetType);
482
483 if (converter != null) {
484
485 boolean isCollectionOrMap = Collection.class.isAssignableFrom(sourceType) || Map.class.isAssignableFrom(sourceType);
486
487 return !(String.class.equals(targetType) && isCollectionOrMap && isStringConverter(converter));
488 } else {
489
490 if (Objects.equals(targetType, Object.class)) {
491
492 return beanUtils.getConvertUtils().lookup(sourceType) != null;
493 } else if (Objects.equals(targetType, Class.class)) {
494 return true;
495 }
496
497 return false;
498 }
499 }
500
501 private boolean isStringConverter(Converter converter) {
502
503
504
505 return converter.toString().contains("StringConverter");
506 }
507
508 private void initBean(Object bean) throws Exception {
509 try {
510 Method initMethod = bean.getClass().getMethod("init");
511 initMethod.invoke(bean);
512 log.debug("{} is initialized", bean);
513 } catch (NoSuchMethodException ignored) {
514 } catch (SecurityException | InvocationTargetException | IllegalAccessException e) {
515 throw new Exception(e);
516 }
517 }
518
519 private void reportDeprecation(TransformationState state, Class<?> actualClass, Class<?> deprecatedParentClass) {
520 String deprecationWarning = DeprecationUtil.getDeprecationMessage(actualClass, deprecatedParentClass);
521 state.trackProblem(deprecated(deprecationWarning));
522 }
523
524 private void reportDeprecation(TransformationState state, Class<?> deprecatedClass) {
525 if (IGNORED_DEPRECATED_PACKAGES.stream().noneMatch(packageName -> deprecatedClass.getName().startsWith(packageName))) {
526 String deprecationWarning = DeprecationUtil.getDeprecationMessage(deprecatedClass);
527 state.trackProblem(deprecated(deprecationWarning));
528 }
529 }
530
531 private void reportDeprecation(TransformationState state, Method deprecatedMethod) {
532 String deprecationWarning = DeprecationUtil.getDeprecationMessage(deprecatedMethod);
533 state.trackProblem(deprecated(deprecationWarning));
534 }
535 }