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