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 Method readMethod = javaBeanDescriptor.getReadMethod();
112                 final TypeDescriptor typeDescriptor = getTypeDescriptor(propertyType, writeMethod);
113 
114                 dscr.setType(typeDescriptor);
115                 dscr.setWriteMethod(writeMethod);
116                 dscr.setReadMethod(readMethod);
117 
118                 // find and set add method
119                 int numberOfParameters = dscr.isMap() ? 2 : 1;
120                 final Method addMethod = getAddMethod(beanClass, propName, numberOfParameters);
121                 dscr.setAddMethod(addMethod);
122 
123                 break;
124             }
125         }
126 
127         if (dscr.getType() != null) {
128             // we have discovered type for property
129             if (dscr.isMap() || dscr.isCollection()) {
130                 List<Class<?>> parameterTypes = new ArrayList<>(); // this will contain collection types (for map key/value type, for collection value type)
131                 if (dscr.getWriteMethod() != null) {
132                     parameterTypes = inferGenericTypes(dscr.getWriteMethod());
133                 }
134                 if (dscr.getAddMethod() != null && parameterTypes.size() == 0) {
135                     // here we know we don't have setter or setter doesn't have parameterized type
136                     // but we have add method so we take parameters from it
137                     parameterTypes = Arrays.asList(dscr.getAddMethod().getParameterTypes());
138                     // rather set it to null because when we are here we will use add method
139                     dscr.setWriteMethod(null);
140                 }
141                 if (parameterTypes.size() > 0) {
142                     // we resolved types
143                     if (dscr.isMap()) {
144                         dscr.setCollectionKeyType(getTypeDescriptor(parameterTypes.get(0)));
145                         dscr.setCollectionEntryType(getTypeDescriptor(parameterTypes.get(1)));
146                     } else {
147                         // collection
148                         dscr.setCollectionEntryType(getTypeDescriptor(parameterTypes.get(0)));
149                     }
150                 }
151             } else if (dscr.isArray()) {
152                 // for arrays we don't need to discover its parameter from set/add method
153                 // we just take it via Class#getComponentType() method
154                 dscr.setCollectionEntryType(getTypeDescriptor(dscr.getType().getType().getComponentType()));
155             }
156         }
157         propertyTypes.put(key, dscr);
158 
159         return dscr;
160     }
161 
162     private List<Class<?>> inferGenericTypes(Method method) {
163         List<Class<?>> inferredTypes = new ArrayList<>();
164         Type[] parameterTypes = method.getGenericParameterTypes();
165         for (Type parameterType : parameterTypes) {
166             if (parameterType instanceof ParameterizedType) {
167                 ParameterizedType type = (ParameterizedType) parameterType;
168                 for (Type t : type.getActualTypeArguments()) {
169                     if (t instanceof ParameterizedType) {
170                         // this the case when parameterized type looks like this: Collection<List<String>>
171                         // we care only for raw type List
172                         inferredTypes.add((Class<?>) ((ParameterizedType) t).getRawType());
173                     } else if (t instanceof WildcardType) {
174                         WildcardType wildcardType = (WildcardType) t;
175                         if (wildcardType.getUpperBounds().length > 0) {
176                             // upper bounds (? extends Number)
177                             inferredTypes.add((Class<?>) wildcardType.getUpperBounds()[0]);
178                         } else {
179                             // lower bounds (? super Number)
180                             inferredTypes.add((Class<?>) wildcardType.getLowerBounds()[0]);
181                         }
182                     } else {
183                         inferredTypes.add((Class<?>) t);
184                     }
185                 }
186             }
187         }
188         return inferredTypes;
189     }
190 
191     /**
192      * Resolves transformer from bean class or setter.
193      */
194     private Node2BeanTransformer resolveTransformer(Class<?> beanClass, Method writeMethod) throws Node2BeanException {
195         if (!beanClass.isArray() && !beanClass.isPrimitive()) { // don't bother looking for a transformer if the property is an array or a primitive type
196             Class<Node2BeanTransformer> transformerClass;
197             Node2BeanTransformer transformer = null;
198             if (writeMethod != null) {
199                 transformer = getTransformerFromAnnotation(writeMethod);
200                 if (transformer != null) {
201                     // MAGNOLIA-5865
202                     log.warn("You have a @{} annotation on {}. This is currently causing potential issues if you have other properties of the same type. " +
203                             "If possible, add the annotation on a type instead. See MAGNOLIA-5865 for details.", TransformedBy.class.getSimpleName(), writeMethod);
204                 }
205             }
206             if (transformer == null) {
207                 transformer = getTransformerFromAnnotation(beanClass);
208             }
209             if (transformer == null) {
210                 // if no @TransformedBy on class, look at the other annotations of that class to see if they are themselves annotated with that.
211                 final Annotation[] annotations = beanClass.getAnnotations();
212                 for (Annotation annotation : annotations) {
213                     transformer = getTransformerFromAnnotation(annotation.annotationType());
214                 }
215             }
216             if (transformer == null) {
217                 try {
218                     transformerClass = (Class<Node2BeanTransformer>) Class.forName(beanClass.getName() + "Transformer");
219                     // TODO inconsistency with getTransformerFromAnnotation which does "newInstance"
220                     transformer = Components.getComponent(transformerClass);
221                 } catch (ClassNotFoundException e) {
222                     log.debug("No transformer found for bean [{}]", beanClass);
223                 }
224             }
225 
226             return transformer;
227         }
228         return null;
229     }
230 
231     protected Node2BeanTransformer getTransformerFromAnnotation(AnnotatedElement writeMethod) {
232         final TransformedBy transformerAnnotation = writeMethod.getAnnotation(TransformedBy.class);
233         final Class<Node2BeanTransformer> transformerClass = transformerAnnotation == null ? null : (Class<Node2BeanTransformer>) transformerAnnotation.value();
234         return transformerClass == null ? null : Components.getComponentProvider().newInstance(transformerClass);
235     }
236 
237     /**
238      * Gets type descriptor from bean class.
239      */
240     private TypeDescriptor getTypeDescriptor(Class<?> beanClass, Method writeMethod) {
241         TypeDescriptor dscr = types.get(beanClass);
242         // eh, we know about this type, don't bother resolving any further.
243         if (dscr != null) {
244             return dscr;
245         }
246         dscr = new TypeDescriptor();
247         dscr.setType(beanClass);
248         dscr.setMap(Map.class.isAssignableFrom(beanClass));
249         dscr.setCollection(Collection.class.isAssignableFrom(beanClass));
250         dscr.setArray(beanClass.isArray());
251         try {
252             dscr.setTransformer(resolveTransformer(beanClass, writeMethod));
253         } catch (Node2BeanException e) {
254             log.error("Can't create transformer for bean [{}]", beanClass, e);
255         }
256 
257         types.put(beanClass, dscr);
258 
259         return dscr;
260     }
261 
262     @Override
263     public TypeDescriptor getTypeDescriptor(Class<?> beanClass) {
264         return getTypeDescriptor(beanClass, null);
265     }
266 
267     /**
268      * Get a adder method. Transforms name to singular.
269      *
270      * @deprecated since 5.0 - use setters
271      */
272     @Deprecated
273     public Method getAddMethod(Class<?> type, String name, int numberOfParameters) {
274         name = StringUtils.capitalize(name);
275         Method method = getExactMethod(type, "add" + name, numberOfParameters);
276         if (method == null) {
277             method = getExactMethod(type, "add" + StringUtils.removeEnd(name, "s"), numberOfParameters);
278         }
279 
280         if (method == null) {
281             method = getExactMethod(type, "add" + StringUtils.removeEnd(name, "es"), numberOfParameters);
282         }
283 
284         if (method == null) {
285             method = getExactMethod(type, "add" + StringUtils.removeEnd(name, "ren"), numberOfParameters);
286         }
287 
288         if (method == null) {
289             method = getExactMethod(type, "add" + StringUtils.removeEnd(name, "ies") + "y", numberOfParameters);
290         }
291         return method;
292     }
293 
294     /**
295      * Find a method.
296      *
297      * @deprecated since 5.0 - use setters
298      */
299     @Deprecated
300     protected Method getExactMethod(Class<?> type, String name, int numberOfParameters) {
301         Method[] methods = type.getMethods();
302         for (Method method : methods) {
303             if (method.getName().equals(name)) {
304                 // TODO - CAUTION: in case there's several methods with the same
305                 // name and the same numberOfParameters
306                 // this method might pick the "wrong" one. We should think about
307                 // adding a check and throw an exceptions
308                 // if there's more than one match!
309                 if (method.getParameterTypes().length == numberOfParameters) {
310                     return method;
311                 }
312             }
313         }
314         return null;
315     }
316 
317     private static String simpleName(Class<?> c) {
318         return c != null ? c.getSimpleName() : "<undetermined>";
319     }
320 
321 }