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.Arrays;
58  import java.util.Collection;
59  import java.util.Collections;
60  import java.util.HashMap;
61  import java.util.LinkedHashMap;
62  import java.util.LinkedHashSet;
63  import java.util.LinkedList;
64  import java.util.List;
65  import java.util.Map;
66  import java.util.Objects;
67  import java.util.Optional;
68  import java.util.Queue;
69  import java.util.Set;
70  import java.util.stream.Stream;
71  
72  import javax.inject.Inject;
73  
74  import org.apache.commons.beanutils.ConversionException;
75  import org.apache.commons.beanutils.Converter;
76  import org.apache.commons.beanutils.PropertyUtilsBean;
77  import org.apache.commons.lang3.StringUtils;
78  import org.slf4j.Logger;
79  import org.slf4j.LoggerFactory;
80  
81  /**
82   * Transforms a map to a bean graph. Resolves types through {@link TypeMapping} and {@link PreConfiguredBeanUtils}
83   * to mimic what Node2BeanTransformer does.
84   */
85  @SuppressWarnings("unchecked")
86  public class Map2BeanTransformer implements ToBeanTransformer<Map<String, Object>> {
87  
88      private static final Logger log = LoggerFactory.getLogger(Map2BeanTransformer.class);
89  
90      private static final Collection<String> IGNORED_DEPRECATED_PACKAGES = Arrays.asList(
91              "info.magnolia.ui.form.field.definition.",
92              "info.magnolia.ui.workbench.column.definition.",
93              "info.magnolia.ui.framework.tools."
94      );
95  
96      private static final Map<Class, Class> DEFAULT_TYPES = new HashMap<>();
97  
98      /**
99       * Properties starting with this prefix are considered metadata and are skipped during transformation.
100      */
101     public static final String METADATA_PREFIX = "$";
102 
103     static {
104         DEFAULT_TYPES.put(Object.class, LinkedHashMap.class);
105         DEFAULT_TYPES.put(Map.class, LinkedHashMap.class);
106         DEFAULT_TYPES.put(Set.class, LinkedHashSet.class);
107         DEFAULT_TYPES.put(List.class, ArrayList.class);
108         DEFAULT_TYPES.put(Queue.class, ArrayDeque.class);
109         DEFAULT_TYPES.put(Collection.class, LinkedList.class);
110     }
111 
112     private final ComponentProvider componentProvider;
113     private final TypeMapping mapping;
114     private final PreConfiguredBeanUtils beanUtils;
115     private final BeanTypeResolver beanTypeResolver;
116     private final PropertyUtilsBean propertyUtils;
117 
118     @Inject
119     public Map2BeanTransformer(ComponentProvider componentProvider, TypeMapping mapping, PreConfiguredBeanUtils beanUtils,
120                                BeanTypeResolver beanTypeResolver) {
121         this.componentProvider = componentProvider;
122         this.mapping = mapping;
123         this.beanUtils = beanUtils;
124         this.beanTypeResolver = beanTypeResolver;
125         this.propertyUtils = new PropertyUtilsBean();
126     }
127 
128     /**
129      * @deprecated since 5.5.1 - Please use {@link #Map2BeanTransformer(ComponentProvider, TypeMapping, PreConfiguredBeanUtils, BeanTypeResolver)} instead.
130      */
131     @Deprecated
132     public Map2BeanTransformer(ComponentProvider componentProvider, TypeMapping mapping, PreConfiguredBeanUtils beanUtils) {
133         this(componentProvider, mapping, beanUtils, new BeanTypeResolver());
134     }
135 
136     @Override
137     public <T> TransformationResult<T> transform(Map<String, Object> map, Class<T> targetType) {
138 
139         final TransformationStatetate.html#TransformationState">TransformationState state = new TransformationState();
140 
141         if (map == null) {
142             state.trackProblem(warning("Map2Bean#transform() has been invoked with a null input, bean will be created out of an empty map"));
143             map = Collections.emptyMap();
144         }
145 
146         state.pushEntry("", map, mapping.getTypeDescriptor(targetType));
147 
148         final T bean = (T) readValue(state);
149 
150         return TransformationResult.transformationResult(bean, state.getProblems());
151     }
152 
153     public <T> T toBean(Map<String, Object> map, Class<T> targetType) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException, ConfigurationParsingException {
154         final TransformationResult<T> result = transform(map, targetType);
155 
156         /**
157          * Since this method returns only an object, thus, swallowing the issues
158          * let us spit the problems in the log at least.
159          */
160         for (final TransformationProblem problem : result.getProblems()) {
161             switch (problem.getSeverityType()) {
162             case ERROR:
163                 log.warn(problem.getMessage(), problem.getException());
164                 break;
165             case WARNING:
166                 log.info(problem.getMessage());
167                 break;
168             }
169         }
170 
171         return result.get();
172     }
173 
174     protected Object readValue(TransformationState state) {
175         final Object source = state.peekValue();
176 
177         if (source == null) {
178             return null;
179         }
180         Class<?> targetType = state.peekTypeDescriptor().getType();
181         if (DeprecationUtil.isDeprecated(targetType)) {
182             reportDeprecation(state, targetType);
183         } else {
184             DeprecationUtil.findFirstEncounteredDeprecatedSuperType(targetType)
185                     .ifPresent(deprecatedSuperType -> reportDeprecation(state, targetType, deprecatedSuperType));
186         }
187 
188         if (isSimpleConverterAvailable(source.getClass(), targetType)) {
189             return readSimpleValue(state);
190         } else {
191             return readComplexValue(state);
192         }
193     }
194 
195     private Object readSimpleValue(TransformationState state) {
196         final Object source = state.peekValue();
197         final TypeDescriptor targetTypeDescriptor = state.peekTypeDescriptor();
198         final Class<?> targetType = targetTypeDescriptor.getType();
199 
200         // 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)
201         // and we do the conversion ourselves. Basically we need to do here something similar to what is done at Node2BeanTransformerImpl.convertPropertyValue(..)
202         if (Class.class.isAssignableFrom(targetType)) {
203             try {
204                 return Classes.getClassFactory().forName((String) source);
205             } catch (ClassNotFoundException e) {
206                 state.trackProblem(warning("Failed to resolve a class property due to a missing class: [%s]", e.getMessage()).withException(e));
207                 return null;
208             }
209         }
210 
211         Object result = source;
212         if (!targetType.isAssignableFrom(result.getClass())) {
213             try {
214                 result = beanUtils.getConvertUtils().convert(result, targetType);
215             } catch (ConversionException e) {
216                 state.trackProblem(error("Failed to convert an instance of [%s] to [%s] due to [%s]",
217                         result.getClass().getName(), targetType, e.getMessage()).withException(e));
218                 return null;
219             }
220         }
221 
222         return result;
223     }
224 
225     private Object readComplexValue(TransformationState state) {
226         final TypeDescriptor targetTypeDescriptor = elaborateCurrentTargetType(state);
227 
228         if (Objects.equals(targetTypeDescriptor.getType(), Object.class) || targetTypeDescriptor.isMap()) {
229             return readMap(state);
230         }
231 
232         if (targetTypeDescriptor.isCollection()) {
233             return readCollection(state);
234         }
235 
236         if (targetTypeDescriptor.isArray()) {
237             Collection collection = readCollection(state);
238             return collection == null ? null : collection.toArray((Object[]) Array.newInstance(targetTypeDescriptor.getType().getComponentType(), collection.size()));
239         }
240 
241         return readBean(state);
242     }
243 
244     private Object readBean(TransformationState state) {
245         final Class<?> targetType = state.peekTypeDescriptor().getType();
246 
247         final Object result;
248         try {
249             result = createInstance(targetType);
250         } catch (Exception e) {
251             if (!isSimpleConstructorPresent(targetType)) {
252                 state.trackProblem(error("No simple constructor found for type [%s], null is returned", targetType).withException(e));
253             } else {
254                 state.trackProblem(error("Failed to instantiate an object of type [%s] due to [%s], null is returned", targetType, e.getMessage()).withException(e));
255             }
256             return null;
257         }
258 
259         final Map<String, Object> sourceMap = state.peekMap();
260         if (sourceMap == null) {
261             state.trackProblem(error("Skipping bean of type [%s] due to invalid source map [%s], null is returned", targetType, state.peekValue()));
262             return null;
263         }
264 
265         final List<String> propertyNames = Stream
266                 .of(propertyUtils.getPropertyDescriptors(result))
267                 .map(FeatureDescriptor::getName)
268                 .collect(toList());
269 
270         for (Map.Entry<String, Object> sourceEntry : sourceMap.entrySet()) {
271             String sourcePropertyName = sourceEntry.getKey();
272             Object sourcePropertyValue = sourceEntry.getValue();
273 
274             if ("class".equals(sourcePropertyName) || sourcePropertyName.startsWith(METADATA_PREFIX)) {
275                 continue; // skip class property and meta properties
276             }
277 
278             if (!propertyNames.contains(sourcePropertyName)) {
279                 final String missingPropertyMessage = String.format("Property [%s] not found in class [%s], property is not assigned", sourcePropertyName, result.getClass().getName());
280                 if (!"name".equalsIgnoreCase(sourcePropertyName)) {
281                     handleMissingProperty(state, result, sourcePropertyName, sourcePropertyValue, missingPropertyMessage);
282                 } else {
283                     log.debug(missingPropertyMessage);
284                 }
285                 continue;
286             }
287 
288             try {
289                 // We only want to report the deprecation when property value is present (used).
290                 if (sourcePropertyValue != null) {
291                     Collection<Method> deprecatedMethods = DeprecationUtil.getDeprecatedReadMethods(mapping, targetType, sourcePropertyName);
292                     deprecatedMethods.forEach(method -> reportDeprecation(state, method));
293                 }
294 
295                 final PropertyTypeDescriptor propertyTypeDescriptor = mapping.getPropertyTypeDescriptor(targetType, sourcePropertyName);
296                 state.pushEntry(sourcePropertyName, sourcePropertyValue, propertyTypeDescriptor);
297                 if (sourcePropertyValue == null) {
298                     state.trackProblem(warning("Property [%s] is set to null in definition.", sourcePropertyName));
299                 }
300                 beanUtils.setProperty(result, sourcePropertyName, readValue(state));
301             } catch (ReflectiveOperationException e) {
302                 state.trackProblem(error("Failed to resolve and set bean property [%s] due to a reflection operation issue: [%s], bean property is skipped",
303                         sourcePropertyName, Optional.ofNullable(e.getMessage()).orElse(e.getCause().getMessage())
304                 ).withException(e));
305             } catch (Exception e) {
306                 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));
307             } finally {
308                 state.popCurrentEntry();
309             }
310         }
311 
312         // feed name property from key if no name property is configured
313         if (propertyNames.contains("name")) {
314             try {
315                 final String currentTransformedObjectName = state.peekName();
316                 if (StringUtils.isBlank(beanUtils.getProperty(result, "name")) && StringUtils.isNotBlank(currentTransformedObjectName)) {
317                     beanUtils.setProperty(result, "name", currentTransformedObjectName);
318                 }
319             } catch (Exception e) {
320                 state.trackProblem(warning("Failed to set a 'name' property from context due to [%s]", e.getMessage()).withException(e));
321             }
322         }
323 
324         try {
325             initBean(result);
326         } catch (Exception e) {
327             state.trackProblem(warning("Failed to invoke bean init() method due to %s", e.getMessage()).withException(e));
328         }
329 
330         return result;
331     }
332 
333     protected void handleMissingProperty(TransformationState state, Object bean, String sourcePropertyName, Object sourcePropertyValue, String missingPropertyMessage) {
334         state.trackProblem(warning(missingPropertyMessage));
335     }
336 
337     protected Object createInstance(Class<?> targetType) {
338         return componentProvider.newInstance(targetType);
339     }
340 
341     protected Collection<Object> readCollection(TransformationState state) {
342         final List<?> sourceList = state.peekList();
343 
344         if (sourceList == null) {
345             if (state.peekValue() != null) {
346                 state.trackProblem(warning("Failed to resolve property [%s], expected a collection", state.peekName()));
347             }
348             return null;
349         }
350 
351         TypeDescriptor genericTypeDescriptor = state.peek().genericTypeDescriptor;
352         if (genericTypeDescriptor == null) {
353             genericTypeDescriptor = mapping.getTypeDescriptor(Object.class);
354         }
355 
356         final Class<?> collectionType = state.peekTypeDescriptor().getType();
357         final Collection<Object> result = instantiateCollection(collectionType);
358         int index = 0;
359 
360         for (Object sourceElement : sourceList) {
361             state.pushEntry(String.valueOf(index), sourceElement, genericTypeDescriptor);
362 
363             try {
364                 Class<?> targetType = state.peekTypeDescriptor().getType();
365                 Object rawValue = state.peekValue();
366                 Class<?> sourceType = rawValue.getClass();
367 
368                 if (String.class.equals(targetType) && !isSimpleConverterAvailable(sourceType, targetType)) {
369                     state.trackProblem(warning("Ignoring element [%s] of type [%s], because it is incompatible with " +
370                             "collection of type [%s]", rawValue, sourceType.getName(), targetType.getName()));
371                     continue;
372                 }
373 
374                 Object value = readValue(state);
375 
376                 // A problem with reading a child value occurred - skip it assuming it is logged already
377                 if (value == null) {
378                     continue;
379                 }
380 
381                 if (genericTypeDescriptor.getType().isAssignableFrom(value.getClass())) {
382                     result.add(value);
383                 } else {
384                     state.trackProblem(warning("Element [%s] of type [%s] may not be added to the collection of type [%s]", value, value.getClass().getName(), genericTypeDescriptor.getType()));
385                 }
386             } catch (Exception e) {
387                 state.trackProblem(error("Failed to process collection entry due to [%s]", e.getMessage() != null ? e.getMessage() : e)
388                         .withException(e));
389             } finally {
390                 ++index;
391                 state.popCurrentEntry();
392             }
393         }
394 
395         return result;
396     }
397 
398     private Collection<Object> instantiateCollection(Class<?> collectionType) {
399         try {
400             return (Collection<Object>) DEFAULT_TYPES.getOrDefault(collectionType, collectionType).newInstance();
401         } catch (Exception e) {
402             log.debug("Failed to instantiate a collection of type {}, falling back to an ArrayList", collectionType, e);
403             return new ArrayList<>();
404         }
405     }
406 
407     protected Map<String, Object> readMap(TransformationState state) {
408         final Map<String, Object> sourceMap = state.peekMap();
409 
410         if (sourceMap == null) {
411             if (state.peekValue() != null) {
412                 state.trackProblem(warning("Failed to resolve property [%s], expected a map", state.peekName()));
413             }
414             return null;
415         }
416 
417         TypeDescriptor valueTypeDescriptor = state.peek().genericTypeDescriptor;
418         if (valueTypeDescriptor == null) {
419             valueTypeDescriptor = mapping.getTypeDescriptor(Object.class);
420         }
421 
422         final Map<String, Object> result = new LinkedHashMap<>(sourceMap.size());
423 
424         for (Map.Entry<String, Object> sourceEntry : sourceMap.entrySet()) {
425             // Key may actually not be a String:
426             // * typing comes from an unchecked cast in `TransformationState#peekMap`
427             // * at runtime `YamlReader` may infer another "tag", hence different type, so we avoid the CCE
428             String entryKey = String.valueOf(sourceEntry.getKey());
429             state.pushEntry(entryKey, sourceEntry.getValue(), valueTypeDescriptor);
430 
431             try {
432                 Object value = readValue(state);
433 
434                 // A problem with reading a child value occurred - skip it assuming it is logged already
435                 if (value == null) {
436                     continue;
437                 }
438 
439                 result.put(entryKey, value);
440             } catch (Exception e) {
441                 state.trackProblem(error("Failed to process map entry due to [%s]", e.getMessage()).withException(e));
442             } finally {
443                 state.popCurrentEntry();
444             }
445         }
446 
447         return result;
448     }
449 
450     private TypeDescriptor elaborateCurrentTargetType(TransformationState state) {
451         final TransformationState.Entry currentEntry = state.peek();
452 
453         TypeDescriptor targetType = currentEntry.typeDescriptor;
454 
455         if (currentEntry.value instanceof Map) {
456             final Map<String, Object> map = state.peekMap();
457 
458             // 1. Resolve target type from configured class property in the map, fallback to defaultType otherwise
459             targetType = beanTypeResolver.resolve(targetType, map).map(mapping::getTypeDescriptor).orElse(currentEntry.typeDescriptor);
460         }
461 
462         // 2. Get implementation according to type mappings
463         TypeDescriptor result = targetType;
464         try {
465             // ComponentProvider.getImplementation is actually where the type mapping happens (based on info from module descriptors)
466             // TypeMapping.getTypeDescriptor() _just_ gets us a descriptor of the given class, unlike its name suggests
467             final Class<?> implClass = componentProvider.getImplementation(targetType.getType());
468             result = mapping.getTypeDescriptor(implClass);
469         } catch (ClassNotFoundException e) {
470             state.trackProblem(error("Implementation type of [%s] is unknown", targetType.getType()).withException(e));
471         }
472 
473         // 3. If componentProvider has no mapping, we have a few defaults here ...
474         if (DEFAULT_TYPES.containsKey(result.getType())) {
475             result = mapping.getTypeDescriptor(DEFAULT_TYPES.get(result.getType()));
476         }
477 
478         currentEntry.typeDescriptor = result;
479 
480         return result;
481     }
482 
483     private boolean isSimpleConverterAvailable(Class<?> sourceType, Class<?> targetType) {
484         final Converter converter = beanUtils.getConvertUtils().lookup(sourceType, targetType);
485 
486         if (converter != null) {
487             // if direct conversion can be used no complex mapping is needed
488             boolean isCollectionOrMap = Collection.class.isAssignableFrom(sourceType) || Map.class.isAssignableFrom(sourceType);
489 
490             return !(String.class.equals(targetType) && isCollectionOrMap && isStringConverter(converter));
491         } else {
492             // untyped
493             if (Objects.equals(targetType, Object.class)) {
494                 // if there the target is untyped but the source has a convertible type
495                 return beanUtils.getConvertUtils().lookup(sourceType) != null;
496             } else if (Objects.equals(targetType, Class.class)) {// we resolve class types ourselves
497                 return true;
498             }
499 
500             return false;
501         }
502     }
503 
504     private boolean isStringConverter(Converter converter) {
505         // Need to use converter.toString().contains("StringConverter") to test that the converter is a
506         // StringConverter, because converter.getClass().getName() returns only
507         // the Facade (org.apache.commons.beanutils.converters.ConverterFacade).
508         return converter.toString().contains("StringConverter");
509     }
510 
511     private void initBean(Object bean) throws Exception {
512         try {
513             Method initMethod = bean.getClass().getMethod("init");
514             initMethod.invoke(bean);
515             log.debug("{} is initialized", bean);
516         } catch (NoSuchMethodException ignored) {
517         } catch (SecurityException | InvocationTargetException | IllegalAccessException e) {
518             throw new Exception(e);
519         }
520     }
521 
522     private void reportDeprecation(TransformationState state, Class<?> actualClass, Class<?> deprecatedParentClass) {
523         String deprecationWarning = DeprecationUtil.getDeprecationMessage(actualClass, deprecatedParentClass);
524         state.trackProblem(deprecated(deprecationWarning));
525     }
526 
527     private void reportDeprecation(TransformationState state, Class<?> deprecatedClass) {
528         if (IGNORED_DEPRECATED_PACKAGES.stream().noneMatch(packageName -> deprecatedClass.getName().startsWith(packageName))) {
529             String deprecationWarning = DeprecationUtil.getDeprecationMessage(deprecatedClass);
530             state.trackProblem(deprecated(deprecationWarning));
531         }
532     }
533 
534     private void reportDeprecation(TransformationState state, Method deprecatedMethod) {
535         String deprecationWarning = DeprecationUtil.getDeprecationMessage(deprecatedMethod);
536         state.trackProblem(deprecated(deprecationWarning));
537     }
538 
539     private boolean isSimpleConstructorPresent(Class<?> targetType) {
540         return Arrays.stream(targetType.getConstructors()).anyMatch(constructor -> constructor.getParameterCount() == 0);
541     }
542 
543 }