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