View Javadoc

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