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  
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   * Transforms a map to a bean graph. Resolves types through {@link TypeMapping} and {@link PreConfiguredBeanUtils}
75   * to mimic what Node2BeanTransformer does.
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          // TODO we need to have more (List, Queue, etc) and check for "isAssignableFrom" and/or isInterface
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      * @deprecated since 5.5.1 - Please use {@link #Map2BeanTransformer(ComponentProvider, TypeMapping, PreConfiguredBeanUtils, BeanTypeResolver)} instead.
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          * Since this method returns only an object, thus, swallowing the issues
137          * let us spit the problems in the log at least.
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         // 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)
172         // and we do the conversion ourselves. Basically we need to do here something similar to what is done at Node2BeanTransformerImpl.convertPropertyValue(..)
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; // skip class property because its only meta data
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         // feed name property from key if no name property is configured
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                 // A problem with reading a child value occurred - skip it assuming it is logged already
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                 // A problem with reading a child value occurred - skip it assuming it is logged already
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             // 1. Resolve target type from configured class property in the map, fallback to defaultType otherwise
368             targetType = beanTypeResolver.resolve(targetType, map).map(mapping::getTypeDescriptor).orElse(currentEntry.typeDescriptor);
369         }
370 
371         // 2. Get implementation according to type mappings
372         TypeDescriptor result = targetType;
373         try {
374             // ComponentProvider.getImplementation is actually where the type mapping happens (based on info from module descriptors)
375             // TypeMapping.getTypeDescriptor() _just_ gets us a descriptor of the given class, unlike its name suggests
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         // 3. If componentProvider has no mapping, we have a few defaults here ...
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             // if direct conversion can be used no complex mapping is needed
397             return true;
398         } else {
399             // untyped
400             if (Objects.equals(targetType, Object.class)) {
401                 // if there the target is untyped but the source has a convertible type
402                 return beanUtils.getConvertUtils().lookup(sourceType) != null;
403             } else if (Objects.equals(targetType, Class.class)) {// we resolve class types ourselves
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 }