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