View Javadoc

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