View Javadoc
1   /**
2    *    Copyright 2009-2015 the original author or authors.
3    *
4    *    Licensed under the Apache License, Version 2.0 (the "License");
5    *    you may not use this file except in compliance with the License.
6    *    You may obtain a copy of the License at
7    *
8    *       http://www.apache.org/licenses/LICENSE-2.0
9    *
10   *    Unless required by applicable law or agreed to in writing, software
11   *    distributed under the License is distributed on an "AS IS" BASIS,
12   *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   *    See the License for the specific language governing permissions and
14   *    limitations under the License.
15   */
16  package org.apache.ibatis.reflection;
17  
18  import java.lang.reflect.Constructor;
19  import java.lang.reflect.Field;
20  import java.lang.reflect.Method;
21  import java.lang.reflect.Modifier;
22  import java.lang.reflect.ReflectPermission;
23  import java.util.ArrayList;
24  import java.util.Collection;
25  import java.util.HashMap;
26  import java.util.Iterator;
27  import java.util.List;
28  import java.util.Locale;
29  import java.util.Map;
30  
31  import org.apache.ibatis.reflection.invoker.GetFieldInvoker;
32  import org.apache.ibatis.reflection.invoker.Invoker;
33  import org.apache.ibatis.reflection.invoker.MethodInvoker;
34  import org.apache.ibatis.reflection.invoker.SetFieldInvoker;
35  import org.apache.ibatis.reflection.property.PropertyNamer;
36  
37  /*
38   * This class represents a cached set of class definition information that
39   * allows for easy mapping between property names and getter/setter methods.
40   */
41  /**
42   * @author Clinton Begin
43   */
44  public class Reflector {
45  
46    private static final String[] EMPTY_STRING_ARRAY = new String[0];
47  
48    private Class<?> type;
49    private String[] readablePropertyNames = EMPTY_STRING_ARRAY;
50    private String[] writeablePropertyNames = EMPTY_STRING_ARRAY;
51    private Map<String, Invoker> setMethods = new HashMap<String, Invoker>();
52    private Map<String, Invoker> getMethods = new HashMap<String, Invoker>();
53    private Map<String, Class<?>> setTypes = new HashMap<String, Class<?>>();
54    private Map<String, Class<?>> getTypes = new HashMap<String, Class<?>>();
55    private Constructor<?> defaultConstructor;
56  
57    private Map<String, String> caseInsensitivePropertyMap = new HashMap<String, String>();
58  
59    public Reflector(Class<?> clazz) {
60      type = clazz;
61      addDefaultConstructor(clazz);
62      addGetMethods(clazz);
63      addSetMethods(clazz);
64      addFields(clazz);
65      readablePropertyNames = getMethods.keySet().toArray(new String[getMethods.keySet().size()]);
66      writeablePropertyNames = setMethods.keySet().toArray(new String[setMethods.keySet().size()]);
67      for (String propName : readablePropertyNames) {
68        caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName);
69      }
70      for (String propName : writeablePropertyNames) {
71        caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName);
72      }
73    }
74  
75    private void addDefaultConstructor(Class<?> clazz) {
76      Constructor<?>[] consts = clazz.getDeclaredConstructors();
77      for (Constructor<?> constructor : consts) {
78        if (constructor.getParameterTypes().length == 0) {
79          if (canAccessPrivateMethods()) {
80            try {
81              constructor.setAccessible(true);
82            } catch (Exception e) {
83              // Ignored. This is only a final precaution, nothing we can do.
84            }
85          }
86          if (constructor.isAccessible()) {
87            this.defaultConstructor = constructor;
88          }
89        }
90      }
91    }
92  
93    private void addGetMethods(Class<?> cls) {
94      Map<String, List<Method>> conflictingGetters = new HashMap<String, List<Method>>();
95      Method[] methods = getClassMethods(cls);
96      for (Method method : methods) {
97        String name = method.getName();
98        if (name.startsWith("get") && name.length() > 3) {
99          if (method.getParameterTypes().length == 0) {
100           name = PropertyNamer.methodToProperty(name);
101           addMethodConflict(conflictingGetters, name, method);
102         }
103       } else if (name.startsWith("is") && name.length() > 2) {
104         if (method.getParameterTypes().length == 0) {
105           name = PropertyNamer.methodToProperty(name);
106           addMethodConflict(conflictingGetters, name, method);
107         }
108       }
109     }
110     resolveGetterConflicts(conflictingGetters);
111   }
112 
113   private void resolveGetterConflicts(Map<String, List<Method>> conflictingGetters) {
114     for (String propName : conflictingGetters.keySet()) {
115       List<Method> getters = conflictingGetters.get(propName);
116       Iterator<Method> iterator = getters.iterator();
117       Method firstMethod = iterator.next();
118       if (getters.size() == 1) {
119         addGetMethod(propName, firstMethod);
120       } else {
121         Method getter = firstMethod;
122         Class<?> getterType = firstMethod.getReturnType();
123         while (iterator.hasNext()) {
124           Method method = iterator.next();
125           Class<?> methodType = method.getReturnType();
126           if (methodType.equals(getterType)) {
127             throw new ReflectionException("Illegal overloaded getter method with ambiguous type for property "
128                 + propName + " in class " + firstMethod.getDeclaringClass()
129                 + ".  This breaks the JavaBeans " + "specification and can cause unpredicatble results.");
130           } else if (methodType.isAssignableFrom(getterType)) {
131             // OK getter type is descendant
132           } else if (getterType.isAssignableFrom(methodType)) {
133             getter = method;
134             getterType = methodType;
135           } else {
136             throw new ReflectionException("Illegal overloaded getter method with ambiguous type for property "
137                 + propName + " in class " + firstMethod.getDeclaringClass()
138                 + ".  This breaks the JavaBeans " + "specification and can cause unpredicatble results.");
139           }
140         }
141         addGetMethod(propName, getter);
142       }
143     }
144   }
145 
146   private void addGetMethod(String name, Method method) {
147     if (isValidPropertyName(name)) {
148       getMethods.put(name, new MethodInvoker(method));
149       getTypes.put(name, method.getReturnType());
150     }
151   }
152 
153   private void addSetMethods(Class<?> cls) {
154     Map<String, List<Method>> conflictingSetters = new HashMap<String, List<Method>>();
155     Method[] methods = getClassMethods(cls);
156     for (Method method : methods) {
157       String name = method.getName();
158       if (name.startsWith("set") && name.length() > 3) {
159         if (method.getParameterTypes().length == 1) {
160           name = PropertyNamer.methodToProperty(name);
161           addMethodConflict(conflictingSetters, name, method);
162         }
163       }
164     }
165     resolveSetterConflicts(conflictingSetters);
166   }
167 
168   private void addMethodConflict(Map<String, List<Method>> conflictingMethods, String name, Method method) {
169     List<Method> list = conflictingMethods.get(name);
170     if (list == null) {
171       list = new ArrayList<Method>();
172       conflictingMethods.put(name, list);
173     }
174     list.add(method);
175   }
176 
177   private void resolveSetterConflicts(Map<String, List<Method>> conflictingSetters) {
178     for (String propName : conflictingSetters.keySet()) {
179       List<Method> setters = conflictingSetters.get(propName);
180       Method firstMethod = setters.get(0);
181       if (setters.size() == 1) {
182         addSetMethod(propName, firstMethod);
183       } else {
184         Class<?> expectedType = getTypes.get(propName);
185         if (expectedType == null) {
186           throw new ReflectionException("Illegal overloaded setter method with ambiguous type for property "
187               + propName + " in class " + firstMethod.getDeclaringClass() + ".  This breaks the JavaBeans " +
188               "specification and can cause unpredicatble results.");
189         } else {
190           Iterator<Method> methods = setters.iterator();
191           Method setter = null;
192           while (methods.hasNext()) {
193             Method method = methods.next();
194             if (method.getParameterTypes().length == 1
195                 && expectedType.equals(method.getParameterTypes()[0])) {
196               setter = method;
197               break;
198             }
199           }
200           if (setter == null) {
201             throw new ReflectionException("Illegal overloaded setter method with ambiguous type for property "
202                 + propName + " in class " + firstMethod.getDeclaringClass() + ".  This breaks the JavaBeans " +
203                 "specification and can cause unpredicatble results.");
204           }
205           addSetMethod(propName, setter);
206         }
207       }
208     }
209   }
210 
211   private void addSetMethod(String name, Method method) {
212     if (isValidPropertyName(name)) {
213       setMethods.put(name, new MethodInvoker(method));
214       setTypes.put(name, method.getParameterTypes()[0]);
215     }
216   }
217 
218   private void addFields(Class<?> clazz) {
219     Field[] fields = clazz.getDeclaredFields();
220     for (Field field : fields) {
221       if (canAccessPrivateMethods()) {
222         try {
223           field.setAccessible(true);
224         } catch (Exception e) {
225           // Ignored. This is only a final precaution, nothing we can do.
226         }
227       }
228       if (field.isAccessible()) {
229         if (!setMethods.containsKey(field.getName())) {
230           // issue #379 - removed the check for final because JDK 1.5 allows
231           // modification of final fields through reflection (JSR-133). (JGB)
232           // pr #16 - final static can only be set by the classloader
233           int modifiers = field.getModifiers();
234           if (!(Modifier.isFinal(modifiers) && Modifier.isStatic(modifiers))) {
235             addSetField(field);
236           }
237         }
238         if (!getMethods.containsKey(field.getName())) {
239           addGetField(field);
240         }
241       }
242     }
243     if (clazz.getSuperclass() != null) {
244       addFields(clazz.getSuperclass());
245     }
246   }
247 
248   private void addSetField(Field field) {
249     if (isValidPropertyName(field.getName())) {
250       setMethods.put(field.getName(), new SetFieldInvoker(field));
251       setTypes.put(field.getName(), field.getType());
252     }
253   }
254 
255   private void addGetField(Field field) {
256     if (isValidPropertyName(field.getName())) {
257       getMethods.put(field.getName(), new GetFieldInvoker(field));
258       getTypes.put(field.getName(), field.getType());
259     }
260   }
261 
262   private boolean isValidPropertyName(String name) {
263     return !(name.startsWith("$") || "serialVersionUID".equals(name) || "class".equals(name));
264   }
265 
266   /*
267    * This method returns an array containing all methods
268    * declared in this class and any superclass.
269    * We use this method, instead of the simpler Class.getMethods(),
270    * because we want to look for private methods as well.
271    *
272    * @param cls The class
273    * @return An array containing all methods in this class
274    */
275   private Method[] getClassMethods(Class<?> cls) {
276     Map<String, Method> uniqueMethods = new HashMap<String, Method>();
277     Class<?> currentClass = cls;
278     while (currentClass != null) {
279       addUniqueMethods(uniqueMethods, currentClass.getDeclaredMethods());
280 
281       // we also need to look for interface methods -
282       // because the class may be abstract
283       Class<?>[] interfaces = currentClass.getInterfaces();
284       for (Class<?> anInterface : interfaces) {
285         addUniqueMethods(uniqueMethods, anInterface.getMethods());
286       }
287 
288       currentClass = currentClass.getSuperclass();
289     }
290 
291     Collection<Method> methods = uniqueMethods.values();
292 
293     return methods.toArray(new Method[methods.size()]);
294   }
295 
296   private void addUniqueMethods(Map<String, Method> uniqueMethods, Method[] methods) {
297     for (Method currentMethod : methods) {
298       if (!currentMethod.isBridge()) {
299         String signature = getSignature(currentMethod);
300         // check to see if the method is already known
301         // if it is known, then an extended class must have
302         // overridden a method
303         if (!uniqueMethods.containsKey(signature)) {
304           if (canAccessPrivateMethods()) {
305             try {
306               currentMethod.setAccessible(true);
307             } catch (Exception e) {
308               // Ignored. This is only a final precaution, nothing we can do.
309             }
310           }
311 
312           uniqueMethods.put(signature, currentMethod);
313         }
314       }
315     }
316   }
317 
318   private String getSignature(Method method) {
319     StringBuilder sb = new StringBuilder();
320     Class<?> returnType = method.getReturnType();
321     if (returnType != null) {
322       sb.append(returnType.getName()).append('#');
323     }
324     sb.append(method.getName());
325     Class<?>[] parameters = method.getParameterTypes();
326     for (int i = 0; i < parameters.length; i++) {
327       if (i == 0) {
328         sb.append(':');
329       } else {
330         sb.append(',');
331       }
332       sb.append(parameters[i].getName());
333     }
334     return sb.toString();
335   }
336 
337   private static boolean canAccessPrivateMethods() {
338     try {
339       SecurityManager securityManager = System.getSecurityManager();
340       if (null != securityManager) {
341         securityManager.checkPermission(new ReflectPermission("suppressAccessChecks"));
342       }
343     } catch (SecurityException e) {
344       return false;
345     }
346     return true;
347   }
348 
349   /*
350    * Gets the name of the class the instance provides information for
351    *
352    * @return The class name
353    */
354   public Class<?> getType() {
355     return type;
356   }
357 
358   public Constructor<?> getDefaultConstructor() {
359     if (defaultConstructor != null) {
360       return defaultConstructor;
361     } else {
362       throw new ReflectionException("There is no default constructor for " + type);
363     }
364   }
365 
366   public boolean hasDefaultConstructor() {
367     return defaultConstructor != null;
368   }
369 
370   public Invoker getSetInvoker(String propertyName) {
371     Invoker method = setMethods.get(propertyName);
372     if (method == null) {
373       throw new ReflectionException("There is no setter for property named '" + propertyName + "' in '" + type + "'");
374     }
375     return method;
376   }
377 
378   public Invoker getGetInvoker(String propertyName) {
379     Invoker method = getMethods.get(propertyName);
380     if (method == null) {
381       throw new ReflectionException("There is no getter for property named '" + propertyName + "' in '" + type + "'");
382     }
383     return method;
384   }
385 
386   /*
387    * Gets the type for a property setter
388    *
389    * @param propertyName - the name of the property
390    * @return The Class of the propery setter
391    */
392   public Class<?> getSetterType(String propertyName) {
393     Class<?> clazz = setTypes.get(propertyName);
394     if (clazz == null) {
395       throw new ReflectionException("There is no setter for property named '" + propertyName + "' in '" + type + "'");
396     }
397     return clazz;
398   }
399 
400   /*
401    * Gets the type for a property getter
402    *
403    * @param propertyName - the name of the property
404    * @return The Class of the propery getter
405    */
406   public Class<?> getGetterType(String propertyName) {
407     Class<?> clazz = getTypes.get(propertyName);
408     if (clazz == null) {
409       throw new ReflectionException("There is no getter for property named '" + propertyName + "' in '" + type + "'");
410     }
411     return clazz;
412   }
413 
414   /*
415    * Gets an array of the readable properties for an object
416    *
417    * @return The array
418    */
419   public String[] getGetablePropertyNames() {
420     return readablePropertyNames;
421   }
422 
423   /*
424    * Gets an array of the writeable properties for an object
425    *
426    * @return The array
427    */
428   public String[] getSetablePropertyNames() {
429     return writeablePropertyNames;
430   }
431 
432   /*
433    * Check to see if a class has a writeable property by name
434    *
435    * @param propertyName - the name of the property to check
436    * @return True if the object has a writeable property by the name
437    */
438   public boolean hasSetter(String propertyName) {
439     return setMethods.keySet().contains(propertyName);
440   }
441 
442   /*
443    * Check to see if a class has a readable property by name
444    *
445    * @param propertyName - the name of the property to check
446    * @return True if the object has a readable property by the name
447    */
448   public boolean hasGetter(String propertyName) {
449     return getMethods.keySet().contains(propertyName);
450   }
451 
452   public String findPropertyName(String name) {
453     return caseInsensitivePropertyMap.get(name.toUpperCase(Locale.ENGLISH));
454   }
455 }