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.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   * Transforms a map to a bean graph. Resolves types through {@link TypeMapping} and {@link PreConfiguredBeanUtils}
76   * to mimic what Node2BeanTransformer does.
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          // TODO we need to have more (List, Queue, etc) and check for "isAssignableFrom" and/or isInterface
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      * @deprecated since 5.5.1 - Please use {@link #Map2BeanTransformer(ComponentProvider, TypeMapping, PreConfiguredBeanUtils, BeanTypeResolver)} instead.
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          * Since this method returns only an object, thus, swallowing the issues
138          * let us spit the problems in the log at least.
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         // 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)
180         // and we do the conversion ourselves. Basically we need to do here something similar to what is done at Node2BeanTransformerImpl.convertPropertyValue(..)
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; // skip class property because its only meta data
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                 // We only want to report the deprecation when property value is present (used).
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         // feed name property from key if no name property is configured
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                 // A problem with reading a child value occurred - skip it assuming it is logged already
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             // Key may actually not be a String:
359             // * typing comes from an unchecked cast in `TransformationState#peekMap`
360             // * at runtime `YamlReader` may infer another "tag", hence different type, so we avoid the CCE
361             String entryKey = String.valueOf(sourceEntry.getKey());
362             state.pushEntry(entryKey, sourceEntry.getValue(), valueTypeDescriptor);
363 
364             try {
365                 Object value = readValue(state);
366 
367                 // A problem with reading a child value occurred - skip it assuming it is logged already
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             // 1. Resolve target type from configured class property in the map, fallback to defaultType otherwise
392             targetType = beanTypeResolver.resolve(targetType, map).map(mapping::getTypeDescriptor).orElse(currentEntry.typeDescriptor);
393         }
394 
395         // 2. Get implementation according to type mappings
396         TypeDescriptor result = targetType;
397         try {
398             // ComponentProvider.getImplementation is actually where the type mapping happens (based on info from module descriptors)
399             // TypeMapping.getTypeDescriptor() _just_ gets us a descriptor of the given class, unlike its name suggests
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         // 3. If componentProvider has no mapping, we have a few defaults here ...
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             // if direct conversion can be used no complex mapping is needed
421             return true;
422         } else {
423             // untyped
424             if (Objects.equals(targetType, Object.class)) {
425                 // if there the target is untyped but the source has a convertible type
426                 return beanUtils.getConvertUtils().lookup(sourceType) != null;
427             } else if (Objects.equals(targetType, Class.class)) {// we resolve class types ourselves
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 }