View Javadoc
1   /**
2    * This file Copyright (c) 2012-2016 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.jcr.node2bean.impl;
35  
36  import static info.magnolia.transformer.TransformationProblem.*;
37  
38  import info.magnolia.cms.util.ContentUtil;
39  import info.magnolia.cms.util.SystemContentWrapper;
40  import info.magnolia.jcr.iterator.FilteringNodeIterator;
41  import info.magnolia.jcr.node2bean.Node2BeanException;
42  import info.magnolia.jcr.node2bean.Node2BeanTransformer;
43  import info.magnolia.jcr.node2bean.PropertyTypeDescriptor;
44  import info.magnolia.jcr.node2bean.TransformationState;
45  import info.magnolia.jcr.node2bean.TypeDescriptor;
46  import info.magnolia.jcr.node2bean.TypeMapping;
47  import info.magnolia.jcr.predicate.AbstractPredicate;
48  import info.magnolia.jcr.util.NodeTypes;
49  import info.magnolia.objectfactory.Classes;
50  import info.magnolia.objectfactory.ComponentProvider;
51  import info.magnolia.objectfactory.Components;
52  
53  import java.lang.reflect.Array;
54  import java.lang.reflect.Constructor;
55  import java.lang.reflect.InvocationTargetException;
56  import java.lang.reflect.Method;
57  import java.util.ArrayList;
58  import java.util.Collection;
59  import java.util.HashSet;
60  import java.util.Iterator;
61  import java.util.LinkedHashMap;
62  import java.util.LinkedList;
63  import java.util.List;
64  import java.util.Locale;
65  import java.util.Map;
66  import java.util.Queue;
67  import java.util.Set;
68  
69  import javax.inject.Inject;
70  import javax.jcr.Node;
71  import javax.jcr.NodeIterator;
72  import javax.jcr.RepositoryException;
73  
74  import org.apache.commons.beanutils.BeanUtilsBean;
75  import org.apache.commons.beanutils.Converter;
76  import org.apache.commons.beanutils.MethodUtils;
77  import org.apache.commons.beanutils.PropertyUtils;
78  import org.apache.commons.lang3.LocaleUtils;
79  import org.apache.commons.lang3.StringUtils;
80  import org.slf4j.Logger;
81  import org.slf4j.LoggerFactory;
82  
83  import com.google.common.collect.Iterables;
84  
85  /**
86   * Concrete implementation using reflection, generics and setter methods.
87   */
88  public class Node2BeanTransformerImpl implements Node2BeanTransformer {
89  
90      private static final Logger log = LoggerFactory.getLogger(Node2BeanTransformerImpl.class);
91  
92      private final BeanUtilsBean beanUtilsBean;
93  
94      private final Class<?> defaultListImpl;
95  
96      private final Class<?> defaultSetImpl;
97  
98      private final Class<?> defaultQueueImpl;
99  
100     /**
101      * @deprecated since 5.4 - use IoC, don't instantiate directly. You should not have to instantiate this if you
102      * use magnolia-configuration mechanisms introduced in 5.4.
103      */
104     @Deprecated
105     public Node2BeanTransformerImpl() {
106         this(Components.getComponent(PreConfiguredBeanUtils.class));
107     }
108 
109     @Inject
110     public Node2BeanTransformerImpl(PreConfiguredBeanUtils beanUtilsBean) {
111         this(beanUtilsBean, LinkedList.class, HashSet.class, LinkedList.class);
112     }
113 
114     /**
115      * @deprecated since 5.4 - use IoC, don't instantiate directly. You should not have to instantiate this if you
116      * use magnolia-configuration mechanisms introduced in 5.4.
117      */
118     @Deprecated
119     public Node2BeanTransformerImpl(Class<?> defaultListImpl, Class<?> defaultSetImpl, Class<?> defaultQueueImpl) {
120         this(Components.getComponent(PreConfiguredBeanUtils.class), defaultListImpl, defaultSetImpl, defaultQueueImpl);
121     }
122 
123     protected Node2BeanTransformerImpl(PreConfiguredBeanUtils beanUtilsBean, Class<?> defaultListImpl, Class<?> defaultSetImpl, Class<?> defaultQueueImpl) {
124         this.beanUtilsBean = beanUtilsBean;
125         this.defaultListImpl = defaultListImpl;
126         this.defaultSetImpl = defaultSetImpl;
127         this.defaultQueueImpl = defaultQueueImpl;
128 
129         beanUtilsBean.getConvertUtils().deregister(Class.class);
130     }
131 
132     @Override
133     public TransformationState newState() {
134         return new TransformationStateImpl();
135     }
136 
137     @Override
138     public TypeDescriptor resolveType(TypeMapping typeMapping, TransformationState state, ComponentProvider componentProvider) throws ClassNotFoundException, RepositoryException {
139         TypeDescriptor typeDscr = null;
140         Node node = state.getCurrentNode();
141 
142         try {
143             if (node.hasProperty("class")) {
144                 String className = StringUtils.trim(node.getProperty("class").getString());
145                 if (StringUtils.isBlank(className)) {
146                     state.trackProblem(error("Cannot resolve type for node [%s] because class property has empty value", node.getPath()));
147                 } else {
148                     Class<?> clazz = Classes.getClassFactory().forName(className);
149                     typeDscr = typeMapping.getTypeDescriptor(clazz);
150                 }
151             }
152         } catch (RepositoryException e) {
153             state.trackProblem(error("Failed to read a class property value from the node [%d]", node.getPath()));
154         }
155 
156         if (typeDscr == null && state.getLevel() > 1) {
157             TypeDescriptor parentTypeDscr = state.getCurrentType();
158             PropertyTypeDescriptor propDscr;
159 
160             if (parentTypeDscr.isMap() || parentTypeDscr.isCollection()) {
161                 if (state.getLevel() > 2) {
162                     // this is not necessarily the parent node of the current
163                     String mapProperyName = state.peekNode(1).getName();
164                     propDscr = state.peekType(1).getPropertyTypeDescriptor(mapProperyName, typeMapping);
165                     if (propDscr != null) {
166                         typeDscr = propDscr.getCollectionEntryType();
167                     }
168                 }
169             } else {
170                 propDscr = state.getCurrentType().getPropertyTypeDescriptor(node.getName(), typeMapping);
171                 if (propDscr != null) {
172                     typeDscr = propDscr.getType();
173                 }
174             }
175         }
176 
177         typeDscr = onResolveType(typeMapping, state, typeDscr, componentProvider);
178 
179         if (typeDscr != null) {
180             // might be that the factory util defines a default implementation for interfaces
181             final Class<?> type = typeDscr.getType();
182             typeDscr = typeMapping.getTypeDescriptor(componentProvider.getImplementation(type));
183 
184             // now that we know the property type we should delegate to the custom transformer if any defined
185             Node2BeanTransformer customTransformer = typeDscr.getTransformer();
186             if (customTransformer != null && customTransformer != this) {
187                 TypeDescriptor typeFoundByCustomTransformer = customTransformer.resolveType(typeMapping, state, componentProvider);
188                 // if no specific type has been provided by the
189                 // TODO - TypeDescriptor - equals and hashCode impl and use
190                 // not equals instead of !=
191                 if (typeFoundByCustomTransformer != TypeMapping.MAP_TYPE) {
192                     // might be that the factory util defines a default implementation for interfaces
193                     Class<?> implementation = componentProvider.getImplementation(typeFoundByCustomTransformer.getType());
194                     typeDscr = typeMapping.getTypeDescriptor(implementation);
195                 }
196             }
197         }
198 
199         if (typeDscr == null || typeDscr.needsDefaultMapping()) {
200             if (typeDscr == null) {
201                 log.debug("Was not able to resolve type for node [{}] will use a map", node);
202             }
203             typeDscr = TypeMapping.MAP_TYPE;
204         }
205         log.debug("Resolved type [{}] for node [{}]", typeDscr.getType(), node.getPath());
206 
207         return typeDscr;
208     }
209 
210     @Override
211     public NodeIterator getChildren(Node node) throws RepositoryException {
212         // TODO create predicate into separate class, <? extends Item> ItemHidingPredicate (regexp)
213         return new FilteringNodeIterator(node.getNodes(), new AbstractPredicate<Node>() {
214             @Override
215             public boolean evaluateTyped(Node t) {
216                 try {
217                     return !(t.getName().startsWith(NodeTypes.JCR_PREFIX) || t.isNodeType(NodeTypes.MetaData.NAME));
218                 } catch (RepositoryException e) {
219                     return false;
220                 }
221             }
222         });
223     }
224 
225     @Override
226     public Object newBeanInstance(TransformationState state, Map<String, Object> values, ComponentProvider componentProvider) throws Node2BeanException {
227         // we try first to use conversion (Map --> primitive type)
228         // this is the case when we flattening the hierarchy?
229         final Object bean = convertPropertyValue(state.getCurrentType().getType(), values);
230         // were the properties transformed?
231         if (bean == values) {
232             try {
233                 // is this property remove necessary?
234                 values.remove("class");
235                 final Class<?> type = state.getCurrentType().getType();
236                 if (LinkedHashMap.class.equals(type)) {
237                     // TODO - as far as I can tell, "bean" and "properties" are already the same instance of a
238                     // LinkedHashMap, so what are we doing in here ?
239                     return new LinkedHashMap();
240                 } else if (Map.class.isAssignableFrom(type)) {
241                     // TODO ?
242                     log.warn("someone wants another type of map ? {}", type);
243                 } else if (Collection.class.isAssignableFrom(type)) {
244                     // someone wants specific collection
245                     return type.newInstance();
246                 }
247                 return componentProvider.newInstance(type);
248             } catch (Exception e) {
249                 // This error is not tracked here because by current design the N2B conversion is supossed to blow up.
250                 // The N2B exception will bubble upwards to the N2BProcessor where we either handle it (the new API will catch the exception and record the error)
251                 // or, in case of the old API - we don't care about error tracking that much and will just let exception fly.
252                 throw new Node2BeanException(
253                         String.format("Failed to instantiate a bean of type [%s] due to: [%s]", state.getCurrentType().getType().getName(), e.getMessage()), e);
254             }
255         }
256         return bean;
257     }
258 
259     @Override
260     public void initBean(TransformationState state, Map values) throws Node2BeanException {
261         Object bean = state.getCurrentBean();
262 
263         Method init;
264         try {
265             init = bean.getClass().getMethod("init");
266             try {
267                 init.invoke(bean); // no parameters
268             } catch (Exception e) {
269                 throw new Node2BeanException("can't call init method", e);
270             }
271         } catch (SecurityException | NoSuchMethodException e) {
272             return;
273         }
274         log.debug("{} is initialized", bean);
275 
276     }
277 
278     @Override
279     public Object convertPropertyValue(Class<?> propertyType, Object value) throws Node2BeanException {
280         if (Class.class.equals(propertyType)) {
281             try {
282                 return Classes.getClassFactory().forName(value.toString());
283             } catch (ClassNotFoundException e) {
284                 // we let this error to be tracked by handleSetPropertyException, which is a common funnel for property-related failures
285                 throw new Node2BeanException(String.format("Can't convert property. Class for type [%s] not found.", propertyType), e);
286             }
287         }
288 
289         if (Locale.class.equals(propertyType)) {
290             if (value instanceof String) {
291                 String localeStr = (String) value;
292                 if (StringUtils.isNotEmpty(localeStr)) {
293                     return LocaleUtils.toLocale(localeStr);
294                 }
295             }
296         }
297 
298         if (Collection.class.equals(propertyType) && value instanceof Map) {
299             // TODO never used ?
300             return ((Map) value).values();
301         }
302 
303         // this is mainly the case when we are flattening node hierarchies
304         if (String.class.equals(propertyType) && value instanceof Map && ((Map) value).size() == 1) {
305             return ((Map) value).values().iterator().next();
306         }
307 
308         return value;
309     }
310 
311     /**
312      * Called once the type should have been resolved. The resolvedType might be
313      * null if no type has been resolved. Every subclass should override this method.
314      */
315     protected TypeDescriptor onResolveType(TypeMapping typeMapping, TransformationState state, TypeDescriptor resolvedType, ComponentProvider componentProvider) {
316         return resolvedType;
317     }
318 
319     @Override
320     public void setProperty(TypeMapping mapping, TransformationState state, PropertyTypeDescriptor descriptor, Map<String, Object> values) throws RepositoryException {
321         String propertyName = descriptor.getName();
322         if (propertyName.equals("class")) {
323             return;
324         }
325         Object value = values.get(propertyName);
326         Object bean = state.getCurrentBean();
327 
328         if (propertyName.equals("content") && value == null) {
329             PropertyTypeDescriptor dscr = mapping.getPropertyTypeDescriptor(bean.getClass(), propertyName);
330             // For backwards compatibility with before this type check was present we support all the base classes of
331             // SystemContentWrapper and all the interfaces it implements
332             if (dscr.getType().getType().isAssignableFrom(SystemContentWrapper.class)) {
333                 value = new SystemContentWrapper(ContentUtil.asContent(state.getCurrentNode()));
334             } else  if (dscr.getType().getType().isAssignableFrom(Node.class)) {
335                 value = state.getCurrentNode();
336             }
337 
338         } else if (propertyName.equals("name") && value == null) {
339             value = state.getCurrentNode().getName();
340         } else if (propertyName.equals("className") && value == null) {
341             value = values.get("class");
342         }
343 
344         // do no try to set a bean-property that has no corresponding node-property
345         if (value == null) {
346             return;
347         }
348 
349         log.debug("try to set {}.{} with value {}", bean, propertyName, value);
350         // if the parent bean is a map, we can't guess the types.
351         if (!(bean instanceof Map)) {
352             try {
353                 PropertyTypeDescriptor dscr = mapping.getPropertyTypeDescriptor(bean.getClass(), propertyName);
354                 if (dscr.getType() != null) {
355                     // try to use a setter method for a Collection property of
356                     // the bean
357                     if (dscr.isCollection() || dscr.isMap() || dscr.isArray()) {
358                         log.debug("{} is of type collection, map or array", propertyName);
359                         if (dscr.getWriteMethod() != null) {
360                             Method method = dscr.getWriteMethod();
361                             clearCollection(bean, propertyName);
362                             filterOrConvertCollectionsAndMaps(dscr, value);
363                             if (dscr.isMap()) {
364                                 method.invoke(bean, value);
365                             } else if (dscr.isArray()) {
366                                 Class<?> entryClass = dscr.getCollectionEntryType().getType();
367                                 Collection<Object> list = new LinkedList<Object>(((Map<Object, Object>) value).values());
368 
369                                 Object[] arr = (Object[]) Array.newInstance(entryClass, list.size());
370                                 for (int i = 0; i < arr.length; i++) {
371                                     arr[i] = Iterables.get(list, i);
372                                 }
373                                 method.invoke(bean, new Object[]{arr});
374                             } else if (dscr.isCollection()) {
375                                 if (value instanceof Map) {
376                                     value = createCollectionFromMap((Map<Object, Object>) value, dscr.getType().getType());
377                                 }
378                                 method.invoke(bean, value);
379                             }
380                             return;
381                         } else if (dscr.getAddMethod() != null) {
382                             Method method = dscr.getAddMethod();
383                             clearCollection(bean, propertyName);
384                             Class<?> entryClass = dscr.getCollectionEntryType().getType();
385 
386                             log.warn("Will use deprecated add method [{}] to populate [{}] in bean class [{}].", method.getName(), propertyName, bean.getClass().getName());
387                             for (Iterator<Object> iter = ((Map<Object, Object>) value).keySet().iterator(); iter.hasNext(); ) {
388                                 Object key = iter.next();
389                                 Object entryValue = ((Map<Object, Object>) value).get(key);
390                                 entryValue = convertPropertyValue(entryClass, entryValue);
391                                 if (entryClass.isAssignableFrom(entryValue.getClass())) {
392                                     if (dscr.isCollection() || dscr.isArray()) {
393                                         log.debug("will add value {}", entryValue);
394                                         method.invoke(bean, entryValue);
395                                     }
396                                     // is a map
397                                     else {
398                                         log.debug("will add key {} with value {}", key, entryValue);
399                                         method.invoke(bean, key, entryValue);
400                                     }
401                                 }
402                             }
403                             return;
404                         }
405                         if (dscr.isCollection()) {
406                             log.debug("transform the values to a collection", propertyName);
407                             value = ((Map<Object, Object>) value).values();
408                         }
409                     } else {
410                         value = convertPropertyValue(dscr.getType().getType(), value);
411                     }
412                 }
413             } catch (Exception e) {
414                 handleSetPropertyException(state, propertyName, value, bean, e);
415                 return;
416             }
417         }
418 
419         try {
420             // This uses the converters registered in beanUtilsBean.convertUtilsBean (see constructor of this class)
421             // If a converter is registered, beanutils will determineBestMatch value.toString(), not the value object as-is.
422             // If no converter is registered, then the value Object is set as-is.
423             // If convertPropertyValue() already converted this value, you'll probably want to unregister the beanutils
424             // converter.
425             // some conversions like string to class. Performance of PropertyUtils.setProperty() would be better
426             beanUtilsBean.setProperty(bean, propertyName, value);
427         } catch (Exception e) {
428             handleSetPropertyException(state, propertyName, value, bean, e);
429         }
430     }
431 
432     protected boolean isBeanEnabled(Object bean) {
433         Method method = null;
434         Object isEnabled = null;
435         try {
436             method = bean.getClass().getMethod("isEnabled");
437             // Check if return type is *not primitive* Boolean and if so return always true
438             // See ConfiguredAreaDefinition
439             if (method.getReturnType().equals(Boolean.class)) {
440                 return true;
441             }
442             isEnabled = method.invoke(bean);
443         } catch (NoSuchMethodException e) {
444             // this is ok, enabled property is optional
445             return true;
446         } catch (IllegalArgumentException e) {
447             // this should never happen
448             return true;
449         } catch (IllegalAccessException e) {
450             log.warn("Can't access method [{}#isEnabled]. Maybe it's private/protected?", bean.getClass());
451             return true;
452         } catch (InvocationTargetException e) {
453             log.error("An exception was thrown by [{}]#isEnabled method.", bean.getClass(), e);
454             return true;
455         }
456         return (Boolean) isEnabled;
457     }
458 
459     /**
460      * Filters out not matching values and converts values if required.
461      *
462      * @param dscr descriptor keeping information about the properties
463      * @param value object from which some element might be filtered or converted
464      */
465     private void filterOrConvertCollectionsAndMaps(final PropertyTypeDescriptor dscr, final Object value) {
466         if (dscr.getCollectionEntryType() != null) {
467             Class<?> entryClass = dscr.getCollectionEntryType().getType();
468             if (dscr.getType().isCollection() && value instanceof Collection) {
469                 final Collection<?> collection = ((Collection) value);
470                 final Collection convertedCollection = filterOrConvert(collection, entryClass);
471                 collection.clear();
472                 collection.addAll(convertedCollection);
473             } else if (value instanceof Map) {
474                 final Map map = (Map) value;
475                 final Map convertedMap = filterOrConvert(map, entryClass);
476                 map.clear();
477                 map.putAll(convertedMap);
478             }
479         }
480     }
481 
482     private Collection filterOrConvert(final Collection collection, final Class entryClass) {
483         final Collection converted = new ArrayList();
484         final Iterator<?> it = collection.iterator();
485         while (it.hasNext()) {
486             Object obj = it.next();
487             if (isBeanEnabled(obj)) {
488                 if (!entryClass.isAssignableFrom(obj.getClass())) {
489                     final Converter converter = beanUtilsBean.getConvertUtils().lookup(entryClass);
490                     if (converter != null) {
491                         converted.add(converter.convert(entryClass, obj));
492                     }
493                 } else {
494                     converted.add(obj);
495                 }
496             }
497         }
498         return converted;
499     }
500 
501     private Map filterOrConvert(final Map map, final Class entryClass) {
502         final Map converted = new LinkedHashMap();
503         for (Object key : map.keySet()) {
504             Object obj = map.get(key);
505             if (isBeanEnabled(obj)) {
506                 if (!entryClass.isAssignableFrom(obj.getClass())) {
507                     final Converter converter = beanUtilsBean.getConvertUtils().lookup(entryClass);
508                     if (converter != null) {
509                         converted.put(key, converter.convert(entryClass, obj));
510                     }
511                 } else {
512                     converted.put(key, obj);
513                 }
514             }
515         }
516         return converted;
517     }
518 
519     private void clearCollection(Object bean, String propertyName) {
520         log.debug("clearing the current content of the collection/map");
521         try {
522             Object col = PropertyUtils.getProperty(bean, propertyName);
523             if (col != null) {
524                 MethodUtils.invokeExactMethod(col, "clear", new Object[]{});
525             }
526         } catch (Exception e) {
527             log.debug("no clear method found on collection {}", propertyName);
528         }
529     }
530 
531     /**
532      * Creates collection from map. Collection type depends on passed class parameter. If passed class parameter is
533      * interface, then default implementation will be used for creating collection.<br/>
534      * By default
535      * <ul>
536      * <li>{@link LinkedList} is used for creating List and Queue collections.</li>
537      * <li>{@link HashSet} is used for creating Set collection.</li>
538      * </ul>
539      * If passed class parameter is an implementation of any collection type, then this method will create
540      * this implementation and returns it.
541      *
542      * @param map a map which values will be converted to a collection
543      * @param clazz collection type
544      * @return Collection of elements or null.
545      */
546     protected Collection<?> createCollectionFromMap(Map<?, ?> map, Class<?> clazz) throws SecurityException, NoSuchMethodException, IllegalArgumentException, InstantiationException, IllegalAccessException, InvocationTargetException {
547         Collection<?> collection = null;
548         Constructor<?> constructor = null;
549         if (clazz.isInterface()) {
550             // class is an interface, we need to decide which implementation of interface we will use
551             if (List.class.isAssignableFrom(clazz)) {
552                 constructor = defaultListImpl.getConstructor(Collection.class);
553             } else if (clazz.isAssignableFrom(Queue.class)) {
554                 constructor = defaultQueueImpl.getConstructor(Collection.class);
555             } else if (Set.class.isAssignableFrom(clazz)) {
556                 constructor = defaultSetImpl.getConstructor(Collection.class);
557             }
558         } else {
559             if (Collection.class.isAssignableFrom(clazz)) {
560                 constructor = clazz.getConstructor(Collection.class);
561             }
562         }
563         if (constructor != null) {
564             collection = (Collection<?>) constructor.newInstance(map.values());
565         }
566         return collection;
567     }
568 
569     private void handleSetPropertyException(TransformationState state, String propertyName, Object value, Object bean, Exception e) throws RepositoryException /* See MAGNOLIA-5890, Node2BeanException */ {
570         if (e instanceof InvocationTargetException) {
571             e = (Exception) e.getCause();
572         }
573         final String errorMessage = String.format("Can't set property [%s] to value [%s] in bean [%s] for node [%s] due to: [%s]", propertyName, value, bean.getClass().getName(), state.getCurrentNode().getPath(), e.toString());
574 
575         state.trackProblem(error(errorMessage).withException(e));
576     }
577 
578 }