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