View Javadoc
1   /**
2    * This file Copyright (c) 2014-2018 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
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   * Transforms a map to a bean graph. Resolves types through {@link TypeMapping} and {@link PreConfiguredBeanUtils}
81   * to mimic what Node2BeanTransformer does.
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       * Properties starting with this prefix are considered metadata and are skipped during transformation.
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      * @deprecated since 5.5.1 - Please use {@link #Map2BeanTransformer(ComponentProvider, TypeMapping, PreConfiguredBeanUtils, BeanTypeResolver)} instead.
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 TransformationStatetate.html#TransformationState">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          * Since this method returns only an object, thus, swallowing the issues
150          * let us spit the problems in the log at least.
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         // org.apache.commons.beanutils.converters.ClassConverter (which isn't aware of custom class loaders, e.g. our Groovy one) has been de-registered (see PreConfiguredBeanUtils)
192         // and we do the conversion ourselves. Basically we need to do here something similar to what is done at Node2BeanTransformerImpl.convertPropertyValue(..)
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; // skip class property and meta properties
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                 // We only want to report the deprecation when property value is present (used).
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         // feed name property from key if no name property is configured
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                 // A problem with reading a child value occurred - skip it assuming it is logged already
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             // Key may actually not be a String:
395             // * typing comes from an unchecked cast in `TransformationState#peekMap`
396             // * at runtime `YamlReader` may infer another "tag", hence different type, so we avoid the CCE
397             String entryKey = String.valueOf(sourceEntry.getKey());
398             state.pushEntry(entryKey, sourceEntry.getValue(), valueTypeDescriptor);
399 
400             try {
401                 Object value = readValue(state);
402 
403                 // A problem with reading a child value occurred - skip it assuming it is logged already
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             // 1. Resolve target type from configured class property in the map, fallback to defaultType otherwise
428             targetType = beanTypeResolver.resolve(targetType, map).map(mapping::getTypeDescriptor).orElse(currentEntry.typeDescriptor);
429         }
430 
431         // 2. Get implementation according to type mappings
432         TypeDescriptor result = targetType;
433         try {
434             // ComponentProvider.getImplementation is actually where the type mapping happens (based on info from module descriptors)
435             // TypeMapping.getTypeDescriptor() _just_ gets us a descriptor of the given class, unlike its name suggests
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         // 3. If componentProvider has no mapping, we have a few defaults here ...
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             // if direct conversion can be used no complex mapping is needed
457             return true;
458         } else {
459             // untyped
460             if (Objects.equals(targetType, Object.class)) {
461                 // if there the target is untyped but the source has a convertible type
462                 return beanUtils.getConvertUtils().lookup(sourceType) != null;
463             } else if (Objects.equals(targetType, Class.class)) {// we resolve class types ourselves
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 }