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