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.jcr.node2bean.Node2BeanException;
37  import info.magnolia.jcr.node2bean.Node2BeanTransformer;
38  import info.magnolia.jcr.node2bean.PropertyTypeDescriptor;
39  import info.magnolia.jcr.node2bean.TransformedBy;
40  import info.magnolia.jcr.node2bean.TypeDescriptor;
41  import info.magnolia.jcr.node2bean.TypeMapping;
42  import info.magnolia.objectfactory.Components;
43  
44  import java.beans.PropertyDescriptor;
45  import java.lang.reflect.Method;
46  import java.lang.reflect.ParameterizedType;
47  import java.lang.reflect.Type;
48  import java.lang.reflect.WildcardType;
49  import java.util.ArrayList;
50  import java.util.Arrays;
51  import java.util.Collection;
52  import java.util.HashMap;
53  import java.util.List;
54  import java.util.Map;
55  
56  import org.apache.commons.beanutils.PropertyUtils;
57  import org.apache.commons.lang.StringUtils;
58  import org.slf4j.Logger;
59  import org.slf4j.LoggerFactory;
60  
61  /**
62   * Basic type mapping implementation.
63   */
64  public class TypeMappingImpl implements TypeMapping {
65  
66      private static Logger log = LoggerFactory.getLogger(TypeMappingImpl.class);
67  
68      private final Map<String, PropertyTypeDescriptor> propertyTypes = new HashMap<String, PropertyTypeDescriptor>();
69      private final Map<Class<?>, TypeDescriptor> types = new HashMap<Class<?>, TypeDescriptor>();
70  
71      @Override
72      public PropertyTypeDescriptor getPropertyTypeDescriptor(Class<?> beanClass, String propName) {
73          PropertyTypeDescriptor dscr = null;
74          String key = beanClass.getName() + "." + propName;
75  
76          dscr = propertyTypes.get(key);
77  
78          if (dscr != null) {
79              return dscr;
80          }
81  
82          dscr = new PropertyTypeDescriptor();
83          dscr.setName(propName);
84  
85          PropertyDescriptor[] descriptors = PropertyUtils.getPropertyDescriptors(beanClass);
86          Method writeMethod = null;
87          for (PropertyDescriptor descriptor : descriptors) {
88              if (descriptor.getName().equals(propName)) {
89                  // may be null for indexed properties
90                  Class<?> propertyType = descriptor.getPropertyType();
91                  writeMethod = descriptor.getWriteMethod();
92                  if (propertyType != null) {
93                      dscr.setType(getTypeDescriptor(propertyType, writeMethod));
94                  }
95                  // set write method
96                  dscr.setWriteMethod(writeMethod);
97                  // set add method
98                  int numberOfParameters = dscr.isMap() ? 2 : 1;
99                  dscr.setAddMethod(getAddMethod(beanClass, propName, numberOfParameters));
100 
101                 break;
102             }
103         }
104 
105         if (dscr.getType() != null) {
106             // we have discovered type for property
107             if (dscr.isMap() || dscr.isCollection()) {
108                 List<Class<?>> parameterTypes = new ArrayList<Class<?>>(); // this will contain collection types (for map key/value type, for collection value type)
109                 if (dscr.getWriteMethod() != null) {
110                     parameterTypes = inferGenericTypes(dscr.getWriteMethod());
111                 }
112                 if (dscr.getAddMethod() != null && parameterTypes.size() == 0) {
113                     // here we know we don't have setter or setter doesn't have parameterized type
114                     // but we have add method so we take parameters from it
115                     parameterTypes = Arrays.asList(dscr.getAddMethod().getParameterTypes());
116                     // rather set it to null because when we are here we will use add method
117                     dscr.setWriteMethod(null);
118                 }
119                 if (parameterTypes.size() > 0) {
120                     // we resolved types
121                     if (dscr.isMap()) {
122                         dscr.setCollectionKeyType(getTypeDescriptor(parameterTypes.get(0)));
123                         dscr.setCollectionEntryType(getTypeDescriptor(parameterTypes.get(1)));
124                     } else {
125                         // collection
126                         dscr.setCollectionEntryType(getTypeDescriptor(parameterTypes.get(0)));
127                     }
128                 }
129             } else if (dscr.isArray()) {
130                 // for arrays we don't need to discover its parameter from set/add method
131                 // we just take it via Class#getComponentType() method
132                 dscr.setCollectionEntryType(getTypeDescriptor(dscr.getType().getType().getComponentType()));
133             }
134         }
135         propertyTypes.put(key, dscr);
136 
137         return dscr;
138     }
139 
140     private List<Class<?>> inferGenericTypes(Method method) {
141         List<Class<?>> inferredTypes = new ArrayList<Class<?>>();
142         Type[] parameterTypes = method.getGenericParameterTypes();
143         for (Type parameterType : parameterTypes) {
144             if (parameterType instanceof ParameterizedType) {
145                 ParameterizedType type = (ParameterizedType) parameterType;
146                 for (Type t : type.getActualTypeArguments()) {
147                     if (t instanceof ParameterizedType) {
148                         // this the case when parameterized type looks like this: Collection<List<String>>
149                         // we care only for raw type List
150                         inferredTypes.add((Class<?>) ((ParameterizedType) t).getRawType());
151                     } else if (t instanceof WildcardType) {
152                         WildcardType wildcardType = (WildcardType) t;
153                         if (wildcardType.getUpperBounds().length > 0) {
154                             // upper bounds (? extends Number)
155                             inferredTypes.add((Class<?>) wildcardType.getUpperBounds()[0]);
156                         } else {
157                             // lower bounds (? super Number)
158                             inferredTypes.add((Class<?>) wildcardType.getLowerBounds()[0]);
159                         }
160                     } else {
161                         inferredTypes.add((Class<?>) t);
162                     }
163                 }
164             }
165         }
166         return inferredTypes;
167     }
168 
169     /**
170      * Resolves transformer from bean class or setter.
171      */
172     private Node2BeanTransformer resolveTransformer(Class<?> beanClass, Method writeMethod) throws Node2BeanException {
173         if (!beanClass.isArray() && !beanClass.isPrimitive()) { // don't bother looking for a transformer if the property is an array or a primitive type
174             Class<Node2BeanTransformer> transformerClass = null;
175             Node2BeanTransformer transformer = null;
176             if (writeMethod != null) {
177                 TransformedBy transformerAnnotation = writeMethod.getAnnotation(TransformedBy.class);
178                 transformerClass = transformerAnnotation == null ? null : (Class<Node2BeanTransformer>) transformerAnnotation.value();
179                 transformer = transformerClass == null ? null : Components.getComponentProvider().newInstance(transformerClass);
180             }
181             if (transformer == null) {
182                 try {
183                     transformerClass = (Class<Node2BeanTransformer>) Class.forName(beanClass.getName() + "Transformer");
184                     transformer = Components.getComponent(transformerClass);
185                 } catch (ClassNotFoundException e) {
186                     log.debug("No transformer found for bean [{}]", beanClass);
187                 }
188             }
189             return transformer;
190         }
191         return null;
192     }
193 
194     /**
195      * Gets type descriptor from bean class.
196      */
197     private TypeDescriptor getTypeDescriptor(Class<?> beanClass, Method method) {
198         TypeDescriptor dscr = types.get(beanClass);
199         // eh, we know about this type, don't bother resolving any further.
200         if(dscr != null){
201             return dscr;
202         }
203         dscr = new TypeDescriptor();
204         dscr.setType(beanClass);
205         dscr.setMap(Map.class.isAssignableFrom(beanClass));
206         dscr.setCollection(Collection.class.isAssignableFrom(beanClass));
207         dscr.setArray(beanClass.isArray());
208         try {
209             dscr.setTransformer(resolveTransformer(beanClass, method));
210         } catch (Node2BeanException e) {
211             log.error("Can't create transformer for bean [" + beanClass + "]", e);
212         }
213 
214         types.put(beanClass, dscr);
215 
216         return dscr;
217     }
218 
219     @Override
220     public TypeDescriptor getTypeDescriptor(Class<?> beanClass) {
221         return getTypeDescriptor(beanClass, null);
222     }
223 
224     /**
225      * Get a adder method. Transforms name to singular.
226      * @deprecated since 5.0 - use setters
227      */
228     public Method getAddMethod(Class<?> type, String name, int numberOfParameters) {
229         name = StringUtils.capitalize(name);
230         Method method = getExactMethod(type, "add" + name, numberOfParameters);
231         if (method == null) {
232             method = getExactMethod(type, "add" + StringUtils.removeEnd(name, "s"), numberOfParameters);
233         }
234 
235         if (method == null) {
236             method = getExactMethod(type, "add" + StringUtils.removeEnd(name, "es"), numberOfParameters);
237         }
238 
239         if (method == null) {
240             method = getExactMethod(type, "add" + StringUtils.removeEnd(name, "ren"), numberOfParameters);
241         }
242 
243         if (method == null) {
244             method = getExactMethod(type, "add" + StringUtils.removeEnd(name, "ies") + "y", numberOfParameters);
245         }
246         return method;
247     }
248 
249     /**
250      * Find a method.
251      *
252      * @deprecated since 5.0 - use setters
253      */
254     protected Method getExactMethod(Class<?> type, String name, int numberOfParameters) {
255         Method[] methods = type.getMethods();
256         for (int i = 0; i < methods.length; i++) {
257             Method method = methods[i];
258             if (method.getName().equals(name)) {
259                 // TODO - CAUTION: in case there's several methods with the same
260                 // name and the same numberOfParameters
261                 // this method might pick the "wrong" one. We should think about
262                 // adding a check and throw an exceptions
263                 // if there's more than one match!
264                 if (method.getParameterTypes().length == numberOfParameters) {
265                     return method;
266                 }
267             }
268         }
269         return null;
270     }
271 
272 }