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