View Javadoc

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