View Javadoc
1   /*
2    * Copyright 2004-2026 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    *    https://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 com.ibatis.common.beans;
17  
18  import java.lang.reflect.Constructor;
19  import java.lang.reflect.Field;
20  import java.lang.reflect.InvocationTargetException;
21  import java.lang.reflect.Method;
22  import java.lang.reflect.ReflectPermission;
23  import java.lang.reflect.UndeclaredThrowableException;
24  import java.math.BigDecimal;
25  import java.math.BigInteger;
26  import java.util.ArrayList;
27  import java.util.Collection;
28  import java.util.Date;
29  import java.util.Enumeration;
30  import java.util.HashMap;
31  import java.util.HashSet;
32  import java.util.Hashtable;
33  import java.util.Iterator;
34  import java.util.LinkedList;
35  import java.util.List;
36  import java.util.Locale;
37  import java.util.Map;
38  import java.util.Set;
39  import java.util.TreeMap;
40  import java.util.TreeSet;
41  import java.util.Vector;
42  import java.util.concurrent.ConcurrentHashMap;
43  
44  /**
45   * This class represents a cached set of class definition information that allows for easy mapping between property
46   * names and getter/setter methods.
47   */
48  public class ClassInfo {
49  
50    /** The cache enabled. */
51    private static boolean cacheEnabled = true;
52  
53    /** The Constant EMPTY_STRING_ARRAY. */
54    private static final String[] EMPTY_STRING_ARRAY = {};
55  
56    /** The Constant SIMPLE_TYPE_SET. */
57    private static final Set SIMPLE_TYPE_SET = new HashSet<>();
58  
59    /** The Constant CLASS_INFO_MAP. */
60    private static final Map<Class, ClassInfo> CLASS_INFO_MAP = new ConcurrentHashMap<>();
61  
62    /** The class name. */
63    private String className;
64  
65    /** The readable property names. */
66    private String[] readablePropertyNames = EMPTY_STRING_ARRAY;
67  
68    /** The writeable property names. */
69    private String[] writeablePropertyNames = EMPTY_STRING_ARRAY;
70  
71    /** The set methods. */
72    private HashMap setMethods = new HashMap<>();
73  
74    /** The get methods. */
75    private HashMap getMethods = new HashMap<>();
76  
77    /** The set types. */
78    private HashMap setTypes = new HashMap<>();
79  
80    /** The get types. */
81    private HashMap getTypes = new HashMap<>();
82  
83    /** The default constructor. */
84    private Constructor defaultConstructor;
85  
86    static {
87      SIMPLE_TYPE_SET.add(String.class);
88      SIMPLE_TYPE_SET.add(Byte.class);
89      SIMPLE_TYPE_SET.add(Short.class);
90      SIMPLE_TYPE_SET.add(Character.class);
91      SIMPLE_TYPE_SET.add(Integer.class);
92      SIMPLE_TYPE_SET.add(Long.class);
93      SIMPLE_TYPE_SET.add(Float.class);
94      SIMPLE_TYPE_SET.add(Double.class);
95      SIMPLE_TYPE_SET.add(Boolean.class);
96      SIMPLE_TYPE_SET.add(Date.class);
97      SIMPLE_TYPE_SET.add(Class.class);
98      SIMPLE_TYPE_SET.add(BigInteger.class);
99      SIMPLE_TYPE_SET.add(BigDecimal.class);
100 
101     SIMPLE_TYPE_SET.add(Collection.class);
102     SIMPLE_TYPE_SET.add(Set.class);
103     SIMPLE_TYPE_SET.add(Map.class);
104     SIMPLE_TYPE_SET.add(List.class);
105     SIMPLE_TYPE_SET.add(HashMap.class);
106     SIMPLE_TYPE_SET.add(TreeMap.class);
107     SIMPLE_TYPE_SET.add(ArrayList.class);
108     SIMPLE_TYPE_SET.add(LinkedList.class);
109     SIMPLE_TYPE_SET.add(HashSet.class);
110     SIMPLE_TYPE_SET.add(TreeSet.class);
111     SIMPLE_TYPE_SET.add(Vector.class);
112     SIMPLE_TYPE_SET.add(Hashtable.class);
113     SIMPLE_TYPE_SET.add(Enumeration.class);
114   }
115 
116   /**
117    * Instantiates a new class info.
118    *
119    * @param clazz
120    *          the clazz
121    */
122   private ClassInfo(Class clazz) {
123     className = clazz.getName();
124     addDefaultConstructor(clazz);
125     addGetMethods(clazz);
126     addSetMethods(clazz);
127     addFields(clazz);
128     readablePropertyNames = (String[]) getMethods.keySet().toArray(new String[getMethods.size()]);
129     writeablePropertyNames = (String[]) setMethods.keySet().toArray(new String[setMethods.size()]);
130   }
131 
132   /**
133    * Adds the default constructor.
134    *
135    * @param clazz
136    *          the clazz
137    */
138   private void addDefaultConstructor(Class clazz) {
139     Constructor[] consts = clazz.getDeclaredConstructors();
140     for (Constructor constructor : consts) {
141       if (constructor.getParameterTypes().length == 0) {
142         if (canAccessPrivateMethods()) {
143           try {
144             constructor.setAccessible(true);
145           } catch (Exception e) {
146             // Ignored. This is only a final precaution, nothing we can do.
147           }
148         }
149         if (constructor.isAccessible()) {
150           this.defaultConstructor = constructor;
151         }
152       }
153     }
154   }
155 
156   /**
157    * Adds the get methods.
158    *
159    * @param cls
160    *          the cls
161    */
162   private void addGetMethods(Class cls) {
163     Method[] methods = getClassMethods(cls);
164     for (Method method : methods) {
165       String name = method.getName();
166       if (name.startsWith("get") && name.length() > 3 || name.startsWith("is") && name.length() > 2) {
167         if (method.getParameterTypes().length == 0) {
168           name = dropCase(name);
169           addGetMethod(name, method);
170         }
171       }
172     }
173   }
174 
175   /**
176    * Adds the get method.
177    *
178    * @param name
179    *          the name
180    * @param method
181    *          the method
182    */
183   private void addGetMethod(String name, Method method) {
184     getMethods.put(name, new MethodInvoker(method));
185     getTypes.put(name, method.getReturnType());
186   }
187 
188   /**
189    * Adds the set methods.
190    *
191    * @param cls
192    *          the cls
193    */
194   private void addSetMethods(Class cls) {
195     Map conflictingSetters = new HashMap<>();
196     Method[] methods = getClassMethods(cls);
197     for (Method method : methods) {
198       String name = method.getName();
199       if (name.startsWith("set") && name.length() > 3 && method.getParameterTypes().length == 1) {
200         name = dropCase(name);
201         // /------------
202         addSetterConflict(conflictingSetters, name, method);
203         // addSetMethod(name, method);
204         // /------------
205       }
206     }
207     resolveSetterConflicts(conflictingSetters);
208   }
209 
210   /**
211    * Adds the setter conflict.
212    *
213    * @param conflictingSetters
214    *          the conflicting setters
215    * @param name
216    *          the name
217    * @param method
218    *          the method
219    */
220   private void addSetterConflict(Map conflictingSetters, String name, Method method) {
221     List list = (List) conflictingSetters.get(name);
222     if (list == null) {
223       list = new ArrayList<>();
224       conflictingSetters.put(name, list);
225     }
226     list.add(method);
227   }
228 
229   /**
230    * Resolve setter conflicts.
231    *
232    * @param conflictingSetters
233    *          the conflicting setters
234    */
235   private void resolveSetterConflicts(Map conflictingSetters) {
236     for (Iterator propNames = conflictingSetters.keySet().iterator(); propNames.hasNext();) {
237       String propName = (String) propNames.next();
238       List setters = (List) conflictingSetters.get(propName);
239       Method firstMethod = (Method) setters.get(0);
240       if (setters.size() == 1) {
241         addSetMethod(propName, firstMethod);
242       } else {
243         Class expectedType = (Class) getTypes.get(propName);
244         if (expectedType == null) {
245           throw new RuntimeException("Illegal overloaded setter method with ambiguous type for property " + propName
246               + " in class " + firstMethod.getDeclaringClass() + ".  This breaks the JavaBeans "
247               + "specification and can cause unpredicatble results.");
248         }
249         Iterator methods = setters.iterator();
250         Method setter = null;
251         while (methods.hasNext()) {
252           Method method = (Method) methods.next();
253           if (method.getParameterTypes().length == 1 && expectedType.equals(method.getParameterTypes()[0])) {
254             setter = method;
255             break;
256           }
257         }
258         if (setter == null) {
259           throw new RuntimeException("Illegal overloaded setter method with ambiguous type for property " + propName
260               + " in class " + firstMethod.getDeclaringClass() + ".  This breaks the JavaBeans "
261               + "specification and can cause unpredicatble results.");
262         }
263         addSetMethod(propName, setter);
264       }
265     }
266   }
267 
268   /**
269    * Adds the set method.
270    *
271    * @param name
272    *          the name
273    * @param method
274    *          the method
275    */
276   private void addSetMethod(String name, Method method) {
277     setMethods.put(name, new MethodInvoker(method));
278     setTypes.put(name, method.getParameterTypes()[0]);
279   }
280 
281   /**
282    * Adds the fields.
283    *
284    * @param clazz
285    *          the clazz
286    */
287   private void addFields(Class clazz) {
288     Field[] fields = clazz.getDeclaredFields();
289     for (Field field : fields) {
290       if (canAccessPrivateMethods()) {
291         try {
292           field.setAccessible(true);
293         } catch (Exception e) {
294           // Ignored. This is only a final precaution, nothing we can do.
295         }
296       }
297       if (field.isAccessible()) {
298         if (!setMethods.containsKey(field.getName())) {
299           addSetField(field);
300         }
301         if (!getMethods.containsKey(field.getName())) {
302           addGetField(field);
303         }
304       }
305     }
306     if (clazz.getSuperclass() != null) {
307       addFields(clazz.getSuperclass());
308     }
309   }
310 
311   /**
312    * Adds the set field.
313    *
314    * @param field
315    *          the field
316    */
317   private void addSetField(Field field) {
318     setMethods.put(field.getName(), new SetFieldInvoker(field));
319     setTypes.put(field.getName(), field.getType());
320   }
321 
322   /**
323    * Adds the get field.
324    *
325    * @param field
326    *          the field
327    */
328   private void addGetField(Field field) {
329     getMethods.put(field.getName(), new GetFieldInvoker(field));
330     getTypes.put(field.getName(), field.getType());
331   }
332 
333   /**
334    * This method returns an array containing all methods declared in this class and any superclass. We use this method,
335    * instead of the simpler Class.getMethods(), because we want to look for private methods as well.
336    *
337    * @param cls
338    *          The class
339    *
340    * @return An array containing all methods in this class
341    */
342   private Method[] getClassMethods(Class cls) {
343     HashMap uniqueMethods = new HashMap<>();
344     Class currentClass = cls;
345     while (currentClass != null) {
346       addUniqueMethods(uniqueMethods, currentClass.getDeclaredMethods());
347 
348       // we also need to look for interface methods -
349       // because the class may be abstract
350       Class[] interfaces = currentClass.getInterfaces();
351       for (Class element : interfaces) {
352         addUniqueMethods(uniqueMethods, element.getMethods());
353       }
354 
355       currentClass = currentClass.getSuperclass();
356     }
357 
358     Collection methods = uniqueMethods.values();
359 
360     return (Method[]) methods.toArray(new Method[methods.size()]);
361   }
362 
363   /**
364    * Adds the unique methods.
365    *
366    * @param uniqueMethods
367    *          the unique methods
368    * @param methods
369    *          the methods
370    */
371   private void addUniqueMethods(HashMap uniqueMethods, Method[] methods) {
372     for (Method currentMethod : methods) {
373       if (!currentMethod.isBridge()) {
374         String signature = getSignature(currentMethod);
375         // check to see if the method is already known
376         // if it is known, then an extended class must have
377         // overridden a method
378         if (!uniqueMethods.containsKey(signature)) {
379           if (canAccessPrivateMethods()) {
380             try {
381               currentMethod.setAccessible(true);
382             } catch (Exception e) {
383               // Ignored. This is only a final precaution, nothing we can do.
384             }
385           }
386 
387           uniqueMethods.put(signature, currentMethod);
388         }
389       }
390     }
391   }
392 
393   /**
394    * Gets the signature.
395    *
396    * @param method
397    *          the method
398    *
399    * @return the signature
400    */
401   private String getSignature(Method method) {
402     StringBuilder sb = new StringBuilder();
403     sb.append(method.getName());
404     Class[] parameters = method.getParameterTypes();
405 
406     for (int i = 0; i < parameters.length; i++) {
407       if (i == 0) {
408         sb.append(':');
409       } else {
410         sb.append(',');
411       }
412       sb.append(parameters[i].getName());
413     }
414 
415     return sb.toString();
416   }
417 
418   /**
419    * Drop case.
420    *
421    * @param name
422    *          the name
423    *
424    * @return the string
425    */
426   private static String dropCase(String name) {
427     if (name.startsWith("is")) {
428       name = name.substring(2);
429     } else if (name.startsWith("get") || name.startsWith("set")) {
430       name = name.substring(3);
431     } else {
432       throw new ProbeException("Error parsing property name '" + name + "'.  Didn't start with 'is', 'get' or 'set'.");
433     }
434 
435     if (name.length() == 1 || name.length() > 1 && !Character.isUpperCase(name.charAt(1))) {
436       name = name.substring(0, 1).toLowerCase(Locale.US) + name.substring(1);
437     }
438 
439     return name;
440   }
441 
442   /**
443    * Can access private methods.
444    *
445    * @return true, if successful
446    */
447   private static boolean canAccessPrivateMethods() {
448     try {
449       SecurityManager securityManager = System.getSecurityManager();
450       if (null != securityManager) {
451         securityManager.checkPermission(new ReflectPermission("suppressAccessChecks"));
452       }
453     } catch (SecurityException e) {
454       return false;
455     }
456     return true;
457   }
458 
459   /**
460    * Gets the name of the class the instance provides information for.
461    *
462    * @return The class name
463    */
464   public String getClassName() {
465     return className;
466   }
467 
468   /**
469    * Instantiate class.
470    *
471    * @return the object
472    */
473   public Object instantiateClass() {
474     if (defaultConstructor == null) {
475       throw new RuntimeException("Error instantiating class.  There is no default constructor for class " + className);
476     }
477     try {
478       return defaultConstructor.newInstance();
479     } catch (Exception e) {
480       throw new RuntimeException("Error instantiating class. Cause: " + e, e);
481     }
482   }
483 
484   /**
485    * Gets the setter for a property as a Method object.
486    *
487    * @param propertyName
488    *          - the property
489    *
490    * @return The Method
491    */
492   public Method getSetter(String propertyName) {
493     Invoker method = (Invoker) setMethods.get(propertyName);
494     if (method == null) {
495       throw new ProbeException(
496           "There is no WRITEABLE property named '" + propertyName + "' in class '" + className + "'");
497     }
498     if (!(method instanceof MethodInvoker)) {
499       throw new ProbeException(
500           "Can't get setter method because '" + propertyName + "' is a field in class '" + className + "'");
501     }
502     return ((MethodInvoker) method).getMethod();
503   }
504 
505   /**
506    * Gets the getter for a property as a Method object.
507    *
508    * @param propertyName
509    *          - the property
510    *
511    * @return The Method
512    */
513   public Method getGetter(String propertyName) {
514     Invoker method = (Invoker) getMethods.get(propertyName);
515     if (method == null) {
516       throw new ProbeException(
517           "There is no READABLE property named '" + propertyName + "' in class '" + className + "'");
518     }
519     if (!(method instanceof MethodInvoker)) {
520       throw new ProbeException(
521           "Can't get getter method because '" + propertyName + "' is a field in class '" + className + "'");
522     }
523     return ((MethodInvoker) method).getMethod();
524   }
525 
526   /**
527    * Gets the sets the invoker.
528    *
529    * @param propertyName
530    *          the property name
531    *
532    * @return the sets the invoker
533    */
534   public Invoker getSetInvoker(String propertyName) {
535     Invoker method = (Invoker) setMethods.get(propertyName);
536     if (method == null) {
537       throw new ProbeException(
538           "There is no WRITEABLE property named '" + propertyName + "' in class '" + className + "'");
539     }
540     return method;
541   }
542 
543   /**
544    * Gets the gets the invoker.
545    *
546    * @param propertyName
547    *          the property name
548    *
549    * @return the gets the invoker
550    */
551   public Invoker getGetInvoker(String propertyName) {
552     Invoker method = (Invoker) getMethods.get(propertyName);
553     if (method == null) {
554       throw new ProbeException(
555           "There is no READABLE property named '" + propertyName + "' in class '" + className + "'");
556     }
557     return method;
558   }
559 
560   /**
561    * Gets the type for a property setter.
562    *
563    * @param propertyName
564    *          - the name of the property
565    *
566    * @return The Class of the propery setter
567    */
568   public Class getSetterType(String propertyName) {
569     Class clazz = (Class) setTypes.get(propertyName);
570     if (clazz == null) {
571       throw new ProbeException(
572           "There is no WRITEABLE property named '" + propertyName + "' in class '" + className + "'");
573     }
574     return clazz;
575   }
576 
577   /**
578    * Gets the type for a property getter.
579    *
580    * @param propertyName
581    *          - the name of the property
582    *
583    * @return The Class of the propery getter
584    */
585   public Class getGetterType(String propertyName) {
586     Class clazz = (Class) getTypes.get(propertyName);
587     if (clazz == null) {
588       throw new ProbeException(
589           "There is no READABLE property named '" + propertyName + "' in class '" + className + "'");
590     }
591     return clazz;
592   }
593 
594   /**
595    * Gets an array of the readable properties for an object.
596    *
597    * @return The array
598    */
599   public String[] getReadablePropertyNames() {
600     return readablePropertyNames;
601   }
602 
603   /**
604    * Gets an array of the writeable properties for an object.
605    *
606    * @return The array
607    */
608   public String[] getWriteablePropertyNames() {
609     return writeablePropertyNames;
610   }
611 
612   /**
613    * Check to see if a class has a writeable property by name.
614    *
615    * @param propertyName
616    *          - the name of the property to check
617    *
618    * @return True if the object has a writeable property by the name
619    */
620   public boolean hasWritableProperty(String propertyName) {
621     return setMethods.containsKey(propertyName);
622   }
623 
624   /**
625    * Check to see if a class has a readable property by name.
626    *
627    * @param propertyName
628    *          - the name of the property to check
629    *
630    * @return True if the object has a readable property by the name
631    */
632   public boolean hasReadableProperty(String propertyName) {
633     return getMethods.containsKey(propertyName);
634   }
635 
636   /**
637    * Tells us if the class passed in is a knwon common type.
638    *
639    * @param clazz
640    *          The class to check
641    *
642    * @return True if the class is known
643    */
644   public static boolean isKnownType(Class clazz) {
645     return SIMPLE_TYPE_SET.contains(clazz) || Collection.class.isAssignableFrom(clazz)
646         || Map.class.isAssignableFrom(clazz) || List.class.isAssignableFrom(clazz) || Set.class.isAssignableFrom(clazz)
647         || Iterator.class.isAssignableFrom(clazz);
648   }
649 
650   /**
651    * Gets an instance of ClassInfo for the specified class.
652    *
653    * @param clazz
654    *          The class for which to lookup the method cache.
655    *
656    * @return The method cache for the class
657    */
658   public static ClassInfo getInstance(Class clazz) {
659     if (!cacheEnabled) {
660       return new ClassInfo(clazz);
661     }
662     ClassInfo cached = CLASS_INFO_MAP.get(clazz);
663     if (cached == null) {
664       cached = new ClassInfo(clazz);
665       CLASS_INFO_MAP.put(clazz, cached);
666     }
667     return cached;
668   }
669 
670   /**
671    * Sets the cache enabled.
672    *
673    * @param cacheEnabled
674    *          the new cache enabled
675    */
676   public static void setCacheEnabled(boolean cacheEnabled) {
677     ClassInfo.cacheEnabled = cacheEnabled;
678   }
679 
680   /**
681    * Examines a Throwable object and gets it's root cause.
682    *
683    * @param t
684    *          - the exception to examine
685    *
686    * @return The root cause
687    */
688   public static Throwable unwrapThrowable(Throwable t) {
689     Throwable t2 = t;
690     while (true) {
691       if (t2 instanceof InvocationTargetException) {
692         t2 = ((InvocationTargetException) t).getTargetException();
693       } else if (t2 instanceof UndeclaredThrowableException) {
694         t2 = ((UndeclaredThrowableException) t).getUndeclaredThrowable();
695       } else {
696         return t2;
697       }
698     }
699   }
700 
701 }