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