001: /*
002: * Copyright 2004 Clinton Begin
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License");
005: * you may not use this file except in compliance with the License.
006: * You may obtain a copy of the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS,
012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013: * See the License for the specific language governing permissions and
014: * limitations under the License.
015: */
016: package com.ibatis.common.beans;
017:
018: import com.ibatis.common.logging.Log;
019: import com.ibatis.common.logging.LogFactory;
020:
021: import java.lang.reflect.InvocationTargetException;
022: import java.lang.reflect.Method;
023: import java.lang.reflect.UndeclaredThrowableException;
024: import java.lang.reflect.ReflectPermission;
025: import java.math.BigDecimal;
026: import java.math.BigInteger;
027: import java.util.*;
028:
029: /**
030: * This class represents a cached set of class definition information that
031: * allows for easy mapping between property names and getter/setter methods.
032: */
033: public class ClassInfo {
034:
035: private static final Log log = LogFactory.getLog(ClassInfo.class);
036:
037: private static boolean cacheEnabled = true;
038: private static final String[] EMPTY_STRING_ARRAY = new String[0];
039: private static final Set SIMPLE_TYPE_SET = new HashSet();
040: private static final Map CLASS_INFO_MAP = Collections
041: .synchronizedMap(new HashMap());
042:
043: private String className;
044: private String[] readablePropertyNames = EMPTY_STRING_ARRAY;
045: private String[] writeablePropertyNames = EMPTY_STRING_ARRAY;
046: private HashMap setMethods = new HashMap();
047: private HashMap getMethods = new HashMap();
048: private HashMap setTypes = new HashMap();
049: private HashMap getTypes = new HashMap();
050:
051: static {
052: SIMPLE_TYPE_SET.add(String.class);
053: SIMPLE_TYPE_SET.add(Byte.class);
054: SIMPLE_TYPE_SET.add(Short.class);
055: SIMPLE_TYPE_SET.add(Character.class);
056: SIMPLE_TYPE_SET.add(Integer.class);
057: SIMPLE_TYPE_SET.add(Long.class);
058: SIMPLE_TYPE_SET.add(Float.class);
059: SIMPLE_TYPE_SET.add(Double.class);
060: SIMPLE_TYPE_SET.add(Boolean.class);
061: SIMPLE_TYPE_SET.add(Date.class);
062: SIMPLE_TYPE_SET.add(Class.class);
063: SIMPLE_TYPE_SET.add(BigInteger.class);
064: SIMPLE_TYPE_SET.add(BigDecimal.class);
065:
066: SIMPLE_TYPE_SET.add(Collection.class);
067: SIMPLE_TYPE_SET.add(Set.class);
068: SIMPLE_TYPE_SET.add(Map.class);
069: SIMPLE_TYPE_SET.add(List.class);
070: SIMPLE_TYPE_SET.add(HashMap.class);
071: SIMPLE_TYPE_SET.add(TreeMap.class);
072: SIMPLE_TYPE_SET.add(ArrayList.class);
073: SIMPLE_TYPE_SET.add(LinkedList.class);
074: SIMPLE_TYPE_SET.add(HashSet.class);
075: SIMPLE_TYPE_SET.add(TreeSet.class);
076: SIMPLE_TYPE_SET.add(Vector.class);
077: SIMPLE_TYPE_SET.add(Hashtable.class);
078: SIMPLE_TYPE_SET.add(Enumeration.class);
079: }
080:
081: private ClassInfo(Class clazz) {
082: className = clazz.getName();
083: addMethods(clazz);
084: readablePropertyNames = (String[]) getMethods.keySet().toArray(
085: new String[getMethods.keySet().size()]);
086: writeablePropertyNames = (String[]) setMethods.keySet()
087: .toArray(new String[setMethods.keySet().size()]);
088: }
089:
090: private void addMethods(Class cls) {
091: Method[] methods = getAllMethodsForClass(cls);
092: for (int i = 0; i < methods.length; i++) {
093: String name = methods[i].getName();
094: if (name.startsWith("set") && name.length() > 3) {
095: if (methods[i].getParameterTypes().length == 1) {
096: name = dropCase(name);
097: if (setMethods.containsKey(name)) {
098: // TODO(JGB) - this should probably be a RuntimeException at some point???
099: log
100: .error("Illegal overloaded setter method for property "
101: + name
102: + " in class "
103: + cls.getName()
104: + ". This breaks the JavaBeans specification and can cause unpredicatble results.");
105: }
106: setMethods.put(name, methods[i]);
107: setTypes.put(name,
108: methods[i].getParameterTypes()[0]);
109: }
110: } else if (name.startsWith("get") && name.length() > 3) {
111: if (methods[i].getParameterTypes().length == 0) {
112: name = dropCase(name);
113: getMethods.put(name, methods[i]);
114: getTypes.put(name, methods[i].getReturnType());
115: }
116: } else if (name.startsWith("is") && name.length() > 2) {
117: if (methods[i].getParameterTypes().length == 0) {
118: name = dropCase(name);
119: getMethods.put(name, methods[i]);
120: getTypes.put(name, methods[i].getReturnType());
121: }
122: }
123: name = null;
124: }
125: }
126:
127: private Method[] getAllMethodsForClass(Class cls) {
128: if (cls.isInterface()) {
129: // interfaces only have public methods - so the
130: // simple call is all we need (this will also get superinterface methods)
131: return cls.getMethods();
132: } else {
133: // need to get all the declared methods in this class
134: // and any super-class - then need to set access appropriatly
135: // for private methods
136: return getClassMethods(cls);
137: }
138: }
139:
140: /**
141: * This method returns an array containing all methods
142: * declared in this class and any superclass.
143: * We use this method, instead of the simpler Class.getMethods(),
144: * because we want to look for private methods as well.
145: *
146: * @param cls
147: * @return
148: */
149: private Method[] getClassMethods(Class cls) {
150: HashMap uniqueMethods = new HashMap();
151: Class currentClass = cls;
152: while (currentClass != null) {
153: addUniqueMethods(uniqueMethods, currentClass
154: .getDeclaredMethods());
155:
156: // we also need to look for interface methods -
157: // because the class may be abstract
158: Class[] interfaces = currentClass.getInterfaces();
159: for (int i = 0; i < interfaces.length; i++) {
160: addUniqueMethods(uniqueMethods, interfaces[i]
161: .getMethods());
162: }
163:
164: currentClass = currentClass.getSuperclass();
165: }
166:
167: Collection methods = uniqueMethods.values();
168:
169: return (Method[]) methods.toArray(new Method[methods.size()]);
170: }
171:
172: private void addUniqueMethods(HashMap uniqueMethods,
173: Method[] methods) {
174: for (int i = 0; i < methods.length; i++) {
175: Method currentMethod = methods[i];
176: String signature = getSignature(currentMethod);
177: // check to see if the method is already known
178: // if it is known, then an extended class must have
179: // overridden a method
180: if (!uniqueMethods.containsKey(signature)) {
181: if (canAccessPrivateMethods()) {
182: try {
183: currentMethod.setAccessible(true);
184: } catch (Exception e) {
185: // Ignored. This is only a final precaution, nothing we can do.
186: }
187: }
188:
189: uniqueMethods.put(signature, currentMethod);
190: }
191: }
192: }
193:
194: private String getSignature(Method method) {
195: StringBuffer sb = new StringBuffer();
196: sb.append(method.getName());
197: Class[] parameters = method.getParameterTypes();
198:
199: for (int i = 0; i < parameters.length; i++) {
200: if (i == 0) {
201: sb.append(':');
202: } else {
203: sb.append(',');
204: }
205: sb.append(parameters[i].getName());
206: }
207:
208: return sb.toString();
209: }
210:
211: private boolean canAccessPrivateMethods() {
212: try {
213: System.getSecurityManager().checkPermission(
214: new ReflectPermission("suppressAccessChecks"));
215: return true;
216: } catch (SecurityException e) {
217: return false;
218: } catch (NullPointerException e) {
219: return true;
220: }
221: }
222:
223: private static String dropCase(String name) {
224: if (name.startsWith("is")) {
225: name = name.substring(2);
226: } else if (name.startsWith("get") || name.startsWith("set")) {
227: name = name.substring(3);
228: } else {
229: throw new ProbeException("Error parsing property name '"
230: + name
231: + "'. Didn't start with 'is', 'get' or 'set'.");
232: }
233:
234: if (name.length() == 1
235: || (name.length() > 1 && !Character.isUpperCase(name
236: .charAt(1)))) {
237: name = name.substring(0, 1).toLowerCase(Locale.US)
238: + name.substring(1);
239: }
240:
241: return name;
242: }
243:
244: /**
245: * Gets the name of the class the instance provides information for
246: *
247: * @return The class name
248: */
249: public String getClassName() {
250: return className;
251: }
252:
253: /**
254: * Gets the setter for a property as a Method object
255: *
256: * @param propertyName - the property
257: * @return The Method
258: */
259: public Method getSetter(String propertyName) {
260: Method method = (Method) setMethods.get(propertyName);
261: if (method == null) {
262: throw new ProbeException(
263: "There is no WRITEABLE property named '"
264: + propertyName + "' in class '" + className
265: + "'");
266: }
267: return method;
268: }
269:
270: /**
271: * Gets the getter for a property as a Method object
272: *
273: * @param propertyName - the property
274: * @return The Method
275: */
276: public Method getGetter(String propertyName) {
277: Method method = (Method) getMethods.get(propertyName);
278: if (method == null) {
279: throw new ProbeException(
280: "There is no READABLE property named '"
281: + propertyName + "' in class '" + className
282: + "'");
283: }
284: return method;
285: }
286:
287: /**
288: * Gets the type for a property setter
289: *
290: * @param propertyName - the name of the property
291: * @return The Class of the propery setter
292: */
293: public Class getSetterType(String propertyName) {
294: Class clazz = (Class) setTypes.get(propertyName);
295: if (clazz == null) {
296: throw new ProbeException(
297: "There is no WRITEABLE property named '"
298: + propertyName + "' in class '" + className
299: + "'");
300: }
301: return clazz;
302: }
303:
304: /**
305: * Gets the type for a property getter
306: *
307: * @param propertyName - the name of the property
308: * @return The Class of the propery getter
309: */
310: public Class getGetterType(String propertyName) {
311: Class clazz = (Class) getTypes.get(propertyName);
312: if (clazz == null) {
313: throw new ProbeException(
314: "There is no READABLE property named '"
315: + propertyName + "' in class '" + className
316: + "'");
317: }
318: return clazz;
319: }
320:
321: /**
322: * Gets an array of the readable properties for an object
323: *
324: * @return The array
325: */
326: public String[] getReadablePropertyNames() {
327: return readablePropertyNames;
328: }
329:
330: /**
331: * Gets an array of the writeable properties for an object
332: *
333: * @return The array
334: */
335: public String[] getWriteablePropertyNames() {
336: return writeablePropertyNames;
337: }
338:
339: /**
340: * Check to see if a class has a writeable property by name
341: *
342: * @param propertyName - the name of the property to check
343: * @return True if the object has a writeable property by the name
344: */
345: public boolean hasWritableProperty(String propertyName) {
346: return setMethods.keySet().contains(propertyName);
347: }
348:
349: /**
350: * Check to see if a class has a readable property by name
351: *
352: * @param propertyName - the name of the property to check
353: * @return True if the object has a readable property by the name
354: */
355: public boolean hasReadableProperty(String propertyName) {
356: return getMethods.keySet().contains(propertyName);
357: }
358:
359: /**
360: * Tells us if the class passed in is a knwon common type
361: *
362: * @param clazz The class to check
363: * @return True if the class is known
364: */
365: public static boolean isKnownType(Class clazz) {
366: if (SIMPLE_TYPE_SET.contains(clazz)) {
367: return true;
368: } else if (Collection.class.isAssignableFrom(clazz)) {
369: return true;
370: } else if (Map.class.isAssignableFrom(clazz)) {
371: return true;
372: } else if (List.class.isAssignableFrom(clazz)) {
373: return true;
374: } else if (Set.class.isAssignableFrom(clazz)) {
375: return true;
376: } else if (Iterator.class.isAssignableFrom(clazz)) {
377: return true;
378: } else {
379: return false;
380: }
381: }
382:
383: /**
384: * Gets an instance of ClassInfo for the specified class.
385: *
386: * @param clazz The class for which to lookup the method cache.
387: * @return The method cache for the class
388: */
389: public static ClassInfo getInstance(Class clazz) {
390: if (cacheEnabled) {
391: synchronized (clazz) {
392: ClassInfo cache = (ClassInfo) CLASS_INFO_MAP.get(clazz);
393: if (cache == null) {
394: cache = new ClassInfo(clazz);
395: CLASS_INFO_MAP.put(clazz, cache);
396: }
397: return cache;
398: }
399: } else {
400: return new ClassInfo(clazz);
401: }
402: }
403:
404: public static void setCacheEnabled(boolean cacheEnabled) {
405: ClassInfo.cacheEnabled = cacheEnabled;
406: }
407:
408: /**
409: * Examines a Throwable object and gets it's root cause
410: *
411: * @param t - the exception to examine
412: * @return The root cause
413: */
414: public static Throwable unwrapThrowable(Throwable t) {
415: Throwable t2 = t;
416: while (true) {
417: if (t2 instanceof InvocationTargetException) {
418: t2 = ((InvocationTargetException) t)
419: .getTargetException();
420: } else if (t instanceof UndeclaredThrowableException) {
421: t2 = ((UndeclaredThrowableException) t)
422: .getUndeclaredThrowable();
423: } else {
424: return t2;
425: }
426: }
427: }
428:
429: }
|