View Javadoc
1   /**
2    * This file Copyright (c) 2012-2018 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.annotation.Annotation;
46  import java.lang.reflect.AnnotatedElement;
47  import java.lang.reflect.Method;
48  import java.lang.reflect.ParameterizedType;
49  import java.lang.reflect.Type;
50  import java.lang.reflect.WildcardType;
51  import java.util.ArrayList;
52  import java.util.Arrays;
53  import java.util.Collection;
54  import java.util.HashMap;
55  import java.util.List;
56  import java.util.Map;
57  
58  import org.apache.commons.beanutils.PropertyUtils;
59  import org.apache.commons.lang3.StringUtils;
60  import org.slf4j.Logger;
61  import org.slf4j.LoggerFactory;
62  
63  /**
64   * Basic type mapping implementation.
65   */
66  public class TypeMappingImpl implements TypeMapping {
67  
68      private static final Logger log = LoggerFactory.getLogger(TypeMappingImpl.class);
69  
70      private final Map<String, PropertyTypeDescriptor> propertyTypes = new HashMap<>();
71      private final Map<Class<?>, TypeDescriptor> types = new HashMap<>();
72  
73      @Override
74      public PropertyTypeDescriptor getPropertyTypeDescriptor(Class<?> beanClass, String propName) {
75          PropertyTypeDescriptor dscr;
76          String key = beanClass.getName() + "." + propName;
77  
78          dscr = propertyTypes.get(key);
79  
80          if (dscr != null) {
81              return dscr;
82          }
83  
84          dscr = new PropertyTypeDescriptor();
85          dscr.setName(propName);
86  
87          PropertyDescriptor[] descriptors = PropertyUtils.getPropertyDescriptors(beanClass);
88          for (PropertyDescriptor javaBeanDescriptor : descriptors) {
89              if (javaBeanDescriptor.getName().equals(propName)) {
90                  // According to Javadoc, may be null for indexed properties.
91                  // Since Java 8(?) this can also be null when getter and setter don't use the same type (and we're introspecting a subclass)
92                  // If getter and setter use different type (e.g setter takes a narrower type), and we're introspecting a subclass of the class where the property in question is declared, AND:
93                  // * if there is an adder method, everyone is happy.
94                  // * With Java 6: only the propertyType and readMethod are seen; node2bean won't be able to set the property without a writeMethod.
95                  // *** if there is an add method, then that's used instead of the writeMethod and all is well.
96                  // * With Java 8: only the readMethod and writeMethod are seen, but propertyType isn't, which is why we added the "if" below.
97                  // *** if there is an add method, then that's used instead of the writeMethod and all is well.
98  
99                  Class<?> propertyType = javaBeanDescriptor.getPropertyType();
100                 if (propertyType == null && javaBeanDescriptor.getReadMethod() != null) {
101                     log.warn("Property type for {}#{} was {}, using readMethod instead to get {}. Please check {} and make sure getters/setters for {} are consistent.", simpleName(beanClass), propName, javaBeanDescriptor.getPropertyType(), simpleName(javaBeanDescriptor.getReadMethod().getReturnType()), beanClass, propName);
102                     propertyType = javaBeanDescriptor.getReadMethod().getReturnType();
103                 }
104 
105                 if (propertyType == null) {
106                     log.warn("Can't determine property type for {}#{}. Skipping.", simpleName(beanClass), propName);
107                     break;
108                 }
109 
110                 final Method writeMethod = javaBeanDescriptor.getWriteMethod();
111                 final TypeDescriptor typeDescriptor = getTypeDescriptor(propertyType, writeMethod);
112 
113                 dscr.setType(typeDescriptor);
114                 dscr.setWriteMethod(writeMethod);
115 
116                 // find and set add method
117                 int numberOfParameters = dscr.isMap() ? 2 : 1;
118                 final Method addMethod = getAddMethod(beanClass, propName, numberOfParameters);
119                 dscr.setAddMethod(addMethod);
120 
121                 break;
122             }
123         }
124 
125         if (dscr.getType() != null) {
126             // we have discovered type for property
127             if (dscr.isMap() || dscr.isCollection()) {
128                 List<Class<?>> parameterTypes = new ArrayList<>(); // this will contain collection types (for map key/value type, for collection value type)
129                 if (dscr.getWriteMethod() != null) {
130                     parameterTypes = inferGenericTypes(dscr.getWriteMethod());
131                 }
132                 if (dscr.getAddMethod() != null && parameterTypes.size() == 0) {
133                     // here we know we don't have setter or setter doesn't have parameterized type
134                     // but we have add method so we take parameters from it
135                     parameterTypes = Arrays.asList(dscr.getAddMethod().getParameterTypes());
136                     // rather set it to null because when we are here we will use add method
137                     dscr.setWriteMethod(null);
138                 }
139                 if (parameterTypes.size() > 0) {
140                     // we resolved types
141                     if (dscr.isMap()) {
142                         dscr.setCollectionKeyType(getTypeDescriptor(parameterTypes.get(0)));
143                         dscr.setCollectionEntryType(getTypeDescriptor(parameterTypes.get(1)));
144                     } else {
145                         // collection
146                         dscr.setCollectionEntryType(getTypeDescriptor(parameterTypes.get(0)));
147                     }
148                 }
149             } else if (dscr.isArray()) {
150                 // for arrays we don't need to discover its parameter from set/add method
151                 // we just take it via Class#getComponentType() method
152                 dscr.setCollectionEntryType(getTypeDescriptor(dscr.getType().getType().getComponentType()));
153             }
154         }
155         propertyTypes.put(key, dscr);
156 
157         return dscr;
158     }
159 
160     private List<Class<?>> inferGenericTypes(Method method) {
161         List<Class<?>> inferredTypes = new ArrayList<>();
162         Type[] parameterTypes = method.getGenericParameterTypes();
163         for (Type parameterType : parameterTypes) {
164             if (parameterType instanceof ParameterizedType) {
165                 ParameterizedType type = (ParameterizedType) parameterType;
166                 for (Type t : type.getActualTypeArguments()) {
167                     if (t instanceof ParameterizedType) {
168                         // this the case when parameterized type looks like this: Collection<List<String>>
169                         // we care only for raw type List
170                         inferredTypes.add((Class<?>) ((ParameterizedType) t).getRawType());
171                     } else if (t instanceof WildcardType) {
172                         WildcardType wildcardType = (WildcardType) t;
173                         if (wildcardType.getUpperBounds().length > 0) {
174                             // upper bounds (? extends Number)
175                             inferredTypes.add((Class<?>) wildcardType.getUpperBounds()[0]);
176                         } else {
177                             // lower bounds (? super Number)
178                             inferredTypes.add((Class<?>) wildcardType.getLowerBounds()[0]);
179                         }
180                     } else {
181                         inferredTypes.add((Class<?>) t);
182                     }
183                 }
184             }
185         }
186         return inferredTypes;
187     }
188 
189     /**
190      * Resolves transformer from bean class or setter.
191      */
192     private Node2BeanTransformer resolveTransformer(Class<?> beanClass, Method writeMethod) throws Node2BeanException {
193         if (!beanClass.isArray() && !beanClass.isPrimitive()) { // don't bother looking for a transformer if the property is an array or a primitive type
194             Class<Node2BeanTransformer> transformerClass;
195             Node2BeanTransformer transformer = null;
196             if (writeMethod != null) {
197                 transformer = getTransformerFromAnnotation(writeMethod);
198                 if (transformer != null) {
199                     // MAGNOLIA-5865
200                     log.warn("You have a @{} annotation on {}. This is currently causing potential issues if you have other properties of the same type. " +
201                             "If possible, add the annotation on a type instead. See MAGNOLIA-5865 for details.", TransformedBy.class.getSimpleName(), writeMethod);
202                 }
203             }
204             if (transformer == null) {
205                 transformer = getTransformerFromAnnotation(beanClass);
206             }
207             if (transformer == null) {
208                 // if no @TransformedBy on class, look at the other annotations of that class to see if they are themselves annotated with that.
209                 final Annotation[] annotations = beanClass.getAnnotations();
210                 for (Annotation annotation : annotations) {
211                     transformer = getTransformerFromAnnotation(annotation.annotationType());
212                 }
213             }
214             if (transformer == null) {
215                 try {
216                     transformerClass = (Class<Node2BeanTransformer>) Class.forName(beanClass.getName() + "Transformer");
217                     // TODO inconsistency with getTransformerFromAnnotation which does "newInstance"
218                     transformer = Components.getComponent(transformerClass);
219                 } catch (ClassNotFoundException e) {
220                     log.debug("No transformer found for bean [{}]", beanClass);
221                 }
222             }
223 
224             return transformer;
225         }
226         return null;
227     }
228 
229     protected Node2BeanTransformer getTransformerFromAnnotation(AnnotatedElement writeMethod) {
230         final TransformedBy transformerAnnotation = writeMethod.getAnnotation(TransformedBy.class);
231         final Class<Node2BeanTransformer> transformerClass = transformerAnnotation == null ? null : (Class<Node2BeanTransformer>) transformerAnnotation.value();
232         return transformerClass == null ? null : Components.getComponentProvider().newInstance(transformerClass);
233     }
234 
235     /**
236      * Gets type descriptor from bean class.
237      */
238     private TypeDescriptor getTypeDescriptor(Class<?> beanClass, Method writeMethod) {
239         TypeDescriptor dscr = types.get(beanClass);
240         // eh, we know about this type, don't bother resolving any further.
241         if (dscr != null) {
242             return dscr;
243         }
244         dscr = new TypeDescriptor();
245         dscr.setType(beanClass);
246         dscr.setMap(Map.class.isAssignableFrom(beanClass));
247         dscr.setCollection(Collection.class.isAssignableFrom(beanClass));
248         dscr.setArray(beanClass.isArray());
249         try {
250             dscr.setTransformer(resolveTransformer(beanClass, writeMethod));
251         } catch (Node2BeanException e) {
252             log.error("Can't create transformer for bean [{}]", beanClass, e);
253         }
254 
255         types.put(beanClass, dscr);
256 
257         return dscr;
258     }
259 
260     @Override
261     public TypeDescriptor getTypeDescriptor(Class<?> beanClass) {
262         return getTypeDescriptor(beanClass, null);
263     }
264 
265     /**
266      * Get a adder method. Transforms name to singular.
267      *
268      * @deprecated since 5.0 - use setters
269      */
270     public Method getAddMethod(Class<?> type, String name, int numberOfParameters) {
271         name = StringUtils.capitalize(name);
272         Method method = getExactMethod(type, "add" + name, numberOfParameters);
273         if (method == null) {
274             method = getExactMethod(type, "add" + StringUtils.removeEnd(name, "s"), numberOfParameters);
275         }
276 
277         if (method == null) {
278             method = getExactMethod(type, "add" + StringUtils.removeEnd(name, "es"), numberOfParameters);
279         }
280 
281         if (method == null) {
282             method = getExactMethod(type, "add" + StringUtils.removeEnd(name, "ren"), numberOfParameters);
283         }
284 
285         if (method == null) {
286             method = getExactMethod(type, "add" + StringUtils.removeEnd(name, "ies") + "y", numberOfParameters);
287         }
288         return method;
289     }
290 
291     /**
292      * Find a method.
293      *
294      * @deprecated since 5.0 - use setters
295      */
296     protected Method getExactMethod(Class<?> type, String name, int numberOfParameters) {
297         Method[] methods = type.getMethods();
298         for (Method method : methods) {
299             if (method.getName().equals(name)) {
300                 // TODO - CAUTION: in case there's several methods with the same
301                 // name and the same numberOfParameters
302                 // this method might pick the "wrong" one. We should think about
303                 // adding a check and throw an exceptions
304                 // if there's more than one match!
305                 if (method.getParameterTypes().length == numberOfParameters) {
306                     return method;
307                 }
308             }
309         }
310         return null;
311     }
312 
313     private static String simpleName(Class<?> c) {
314         return c != null ? c.getSimpleName() : "<undetermined>";
315     }
316 
317 }