001: /*
002: * Copyright 2002-2007 the original author or authors.
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:
017: package org.springframework.beans;
018:
019: import java.beans.PropertyDescriptor;
020: import java.lang.reflect.Constructor;
021: import java.lang.reflect.InvocationTargetException;
022: import java.lang.reflect.Method;
023: import java.lang.reflect.Modifier;
024: import java.util.Arrays;
025: import java.util.List;
026:
027: import org.springframework.util.Assert;
028: import org.springframework.util.ClassUtils;
029: import org.springframework.util.ReflectionUtils;
030: import org.springframework.util.StringUtils;
031:
032: /**
033: * Static convenience methods for JavaBeans: for instantiating beans,
034: * checking bean property types, copying bean properties, etc.
035: *
036: * <p>Mainly for use within the framework, but to some degree also
037: * useful for application classes.
038: *
039: * @author Rod Johnson
040: * @author Juergen Hoeller
041: * @author Rob Harrop
042: */
043: public abstract class BeanUtils {
044:
045: /**
046: * Convenience method to instantiate a class using its no-arg constructor.
047: * As this method doesn't try to load classes by name, it should avoid
048: * class-loading issues.
049: * <p>Note that this method tries to set the constructor accessible
050: * if given a non-accessible (that is, non-public) constructor.
051: * @param clazz class to instantiate
052: * @return the new instance
053: * @throws BeanInstantiationException if the bean cannot be instantiated
054: */
055: public static Object instantiateClass(Class clazz)
056: throws BeanInstantiationException {
057: Assert.notNull(clazz, "Class must not be null");
058: if (clazz.isInterface()) {
059: throw new BeanInstantiationException(clazz,
060: "Specified class is an interface");
061: }
062: try {
063: return instantiateClass(clazz
064: .getDeclaredConstructor((Class[]) null), null);
065: } catch (NoSuchMethodException ex) {
066: throw new BeanInstantiationException(clazz,
067: "No default constructor found", ex);
068: }
069: }
070:
071: /**
072: * Convenience method to instantiate a class using the given constructor.
073: * As this method doesn't try to load classes by name, it should avoid
074: * class-loading issues.
075: * <p>Note that this method tries to set the constructor accessible
076: * if given a non-accessible (that is, non-public) constructor.
077: * @param ctor the constructor to instantiate
078: * @param args the constructor arguments to apply
079: * @return the new instance
080: * @throws BeanInstantiationException if the bean cannot be instantiated
081: */
082: public static Object instantiateClass(Constructor ctor,
083: Object[] args) throws BeanInstantiationException {
084: Assert.notNull(ctor, "Constructor must not be null");
085: try {
086: ReflectionUtils.makeAccessible(ctor);
087: return ctor.newInstance(args);
088: } catch (InstantiationException ex) {
089: throw new BeanInstantiationException(ctor
090: .getDeclaringClass(), "Is it an abstract class?",
091: ex);
092: } catch (IllegalAccessException ex) {
093: throw new BeanInstantiationException(
094: ctor.getDeclaringClass(),
095: "Has the class definition changed? Is the constructor accessible?",
096: ex);
097: } catch (IllegalArgumentException ex) {
098: throw new BeanInstantiationException(ctor
099: .getDeclaringClass(),
100: "Illegal arguments for constructor", ex);
101: } catch (InvocationTargetException ex) {
102: throw new BeanInstantiationException(ctor
103: .getDeclaringClass(),
104: "Constructor threw exception", ex
105: .getTargetException());
106: }
107: }
108:
109: /**
110: * Find a method with the given method name and the given parameter types,
111: * declared on the given class or one of its superclasses. Prefers public methods,
112: * but will return a protected, package access, or private method too.
113: * <p>Checks <code>Class.getMethod</code> first, falling back to
114: * <code>findDeclaredMethod</code>. This allows to find public methods
115: * without issues even in environments with restricted Java security settings.
116: * @param clazz the class to check
117: * @param methodName the name of the method to find
118: * @param paramTypes the parameter types of the method to find
119: * @return the Method object, or <code>null</code> if not found
120: * @see java.lang.Class#getMethod
121: * @see #findDeclaredMethod
122: */
123: public static Method findMethod(Class clazz, String methodName,
124: Class[] paramTypes) {
125: try {
126: return clazz.getMethod(methodName, paramTypes);
127: } catch (NoSuchMethodException ex) {
128: return findDeclaredMethod(clazz, methodName, paramTypes);
129: }
130: }
131:
132: /**
133: * Find a method with the given method name and the given parameter types,
134: * declared on the given class or one of its superclasses. Will return a public,
135: * protected, package access, or private method.
136: * <p>Checks <code>Class.getDeclaredMethod</code>, cascading upwards to all superclasses.
137: * @param clazz the class to check
138: * @param methodName the name of the method to find
139: * @param paramTypes the parameter types of the method to find
140: * @return the Method object, or <code>null</code> if not found
141: * @see java.lang.Class#getDeclaredMethod
142: */
143: public static Method findDeclaredMethod(Class clazz,
144: String methodName, Class[] paramTypes) {
145: try {
146: return clazz.getDeclaredMethod(methodName, paramTypes);
147: } catch (NoSuchMethodException ex) {
148: if (clazz.getSuperclass() != null) {
149: return findDeclaredMethod(clazz.getSuperclass(),
150: methodName, paramTypes);
151: }
152: return null;
153: }
154: }
155:
156: /**
157: * Find a method with the given method name and minimal parameters (best case: none),
158: * declared on the given class or one of its superclasses. Prefers public methods,
159: * but will return a protected, package access, or private method too.
160: * <p>Checks <code>Class.getMethods</code> first, falling back to
161: * <code>findDeclaredMethodWithMinimalParameters</code>. This allows to find public
162: * methods without issues even in environments with restricted Java security settings.
163: * @param clazz the class to check
164: * @param methodName the name of the method to find
165: * @return the Method object, or <code>null</code> if not found
166: * @throws IllegalArgumentException if methods of the given name were found but
167: * could not be resolved to a unique method with minimal parameters
168: * @see java.lang.Class#getMethods
169: * @see #findDeclaredMethodWithMinimalParameters
170: */
171: public static Method findMethodWithMinimalParameters(Class clazz,
172: String methodName) throws IllegalArgumentException {
173:
174: Method targetMethod = doFindMethodWithMinimalParameters(clazz
175: .getDeclaredMethods(), methodName);
176: if (targetMethod == null) {
177: return findDeclaredMethodWithMinimalParameters(clazz,
178: methodName);
179: }
180: return targetMethod;
181: }
182:
183: /**
184: * Find a method with the given method name and minimal parameters (best case: none),
185: * declared on the given class or one of its superclasses. Will return a public,
186: * protected, package access, or private method.
187: * <p>Checks <code>Class.getDeclaredMethods</code>, cascading upwards to all superclasses.
188: * @param clazz the class to check
189: * @param methodName the name of the method to find
190: * @return the Method object, or <code>null</code> if not found
191: * @throws IllegalArgumentException if methods of the given name were found but
192: * could not be resolved to a unique method with minimal parameters
193: * @see java.lang.Class#getDeclaredMethods
194: */
195: public static Method findDeclaredMethodWithMinimalParameters(
196: Class clazz, String methodName)
197: throws IllegalArgumentException {
198:
199: Method targetMethod = doFindMethodWithMinimalParameters(clazz
200: .getDeclaredMethods(), methodName);
201: if (targetMethod == null && clazz.getSuperclass() != null) {
202: return findDeclaredMethodWithMinimalParameters(clazz
203: .getSuperclass(), methodName);
204: }
205: return targetMethod;
206: }
207:
208: /**
209: * Find a method with the given method name and minimal parameters (best case: none)
210: * in the given list of methods.
211: * @param methods the methods to check
212: * @param methodName the name of the method to find
213: * @return the Method object, or <code>null</code> if not found
214: * @throws IllegalArgumentException if methods of the given name were found but
215: * could not be resolved to a unique method with minimal parameters
216: */
217: private static Method doFindMethodWithMinimalParameters(
218: Method[] methods, String methodName)
219: throws IllegalArgumentException {
220:
221: Method targetMethod = null;
222: int numMethodsFoundWithCurrentMinimumArgs = 0;
223: for (int i = 0; i < methods.length; i++) {
224: if (methods[i].getName().equals(methodName)) {
225: int numParams = methods[i].getParameterTypes().length;
226: if (targetMethod == null
227: || numParams < targetMethod.getParameterTypes().length) {
228: targetMethod = methods[i];
229: numMethodsFoundWithCurrentMinimumArgs = 1;
230: } else {
231: if (targetMethod.getParameterTypes().length == numParams) {
232: // Additional candidate with same length.
233: numMethodsFoundWithCurrentMinimumArgs++;
234: }
235: }
236: }
237: }
238: if (numMethodsFoundWithCurrentMinimumArgs > 1) {
239: throw new IllegalArgumentException(
240: "Cannot resolve method '"
241: + methodName
242: + "' to a unique method. Attempted to resolve to overloaded method with "
243: + "the least number of parameters, but there were "
244: + numMethodsFoundWithCurrentMinimumArgs
245: + " candidates.");
246: }
247: return targetMethod;
248: }
249:
250: /**
251: * Parse a method signature in the form <code>methodName[([arg_list])]</code>,
252: * where <code>arg_list</code> is an optional, comma-separated list of fully-qualified
253: * type names, and attempts to resolve that signature against the supplied <code>Class</code>.
254: * <p>When not supplying an argument list (<code>methodName</code>) the method whose name
255: * matches and has the least number of parameters will be returned. When supplying an
256: * argument type list, only the method whose name and argument types match will be returned.
257: * <p>Note then that <code>methodName</code> and <code>methodName()</code> are <strong>not</strong>
258: * resolved in the same way. The signature <code>methodName</code> means the method called
259: * <code>methodName</code> with the least number of arguments, whereas <code>methodName()</code>
260: * means the method called <code>methodName</code> with exactly 0 arguments.
261: * <p>If no method can be found, then <code>null</code> is returned.
262: * @param signature the method signature as String representation
263: * @param clazz the class to resolve the method signature against
264: * @return the resolved Method
265: * @see #findMethod
266: * @see #findMethodWithMinimalParameters
267: */
268: public static Method resolveSignature(String signature, Class clazz) {
269: Assert.hasText(signature, "'signature' must not be empty");
270: Assert.notNull(clazz, "Class must not be null");
271:
272: int firstParen = signature.indexOf("(");
273: int lastParen = signature.indexOf(")");
274:
275: if (firstParen > -1 && lastParen == -1) {
276: throw new IllegalArgumentException(
277: "Invalid method signature '" + signature
278: + "': expected closing ')' for args list");
279: } else if (lastParen > -1 && firstParen == -1) {
280: throw new IllegalArgumentException(
281: "Invalid method signature '" + signature
282: + "': expected opening '(' for args list");
283: } else if (firstParen == -1 && lastParen == -1) {
284: return findMethodWithMinimalParameters(clazz, signature);
285: } else {
286: String methodName = signature.substring(0, firstParen);
287: String[] parameterTypeNames = StringUtils
288: .commaDelimitedListToStringArray(signature
289: .substring(firstParen + 1, lastParen));
290: Class[] parameterTypes = new Class[parameterTypeNames.length];
291: for (int i = 0; i < parameterTypeNames.length; i++) {
292: String parameterTypeName = parameterTypeNames[i].trim();
293: try {
294: parameterTypes[i] = ClassUtils.forName(
295: parameterTypeName, clazz.getClassLoader());
296: } catch (Throwable ex) {
297: throw new IllegalArgumentException(
298: "Invalid method signature: unable to resolve type ["
299: + parameterTypeName
300: + "] for argument " + i
301: + ". Root cause: " + ex);
302: }
303: }
304: return findMethod(clazz, methodName, parameterTypes);
305: }
306: }
307:
308: /**
309: * Retrieve the JavaBeans <code>PropertyDescriptor</code>s of a given class.
310: * @param clazz the Class to retrieve the PropertyDescriptors for
311: * @return an array of <code>PropertyDescriptors</code> for the given class
312: * @throws BeansException if PropertyDescriptor look fails
313: */
314: public static PropertyDescriptor[] getPropertyDescriptors(
315: Class clazz) throws BeansException {
316: CachedIntrospectionResults cr = CachedIntrospectionResults
317: .forClass(clazz);
318: return cr.getBeanInfo().getPropertyDescriptors();
319: }
320:
321: /**
322: * Retrieve the JavaBeans <code>PropertyDescriptors</code> for the given property.
323: * @param clazz the Class to retrieve the PropertyDescriptor for
324: * @param propertyName the name of the property
325: * @return the corresponding PropertyDescriptor, or <code>null</code> if none
326: * @throws BeansException if PropertyDescriptor lookup fails
327: */
328: public static PropertyDescriptor getPropertyDescriptor(Class clazz,
329: String propertyName) throws BeansException {
330:
331: CachedIntrospectionResults cr = CachedIntrospectionResults
332: .forClass(clazz);
333: return cr.getPropertyDescriptor(propertyName);
334: }
335:
336: /**
337: * Find a JavaBeans <code>PropertyDescriptor</code> for the given method,
338: * with the method either being the read method or the write method for
339: * that bean property.
340: * @param method the method to find a corresponding PropertyDescriptor for
341: * @return the corresponding PropertyDescriptor, or <code>null</code> if none
342: * @throws BeansException if PropertyDescriptor lookup fails
343: */
344: public static PropertyDescriptor findPropertyForMethod(Method method)
345: throws BeansException {
346: Assert.notNull(method, "Method must not be null");
347: PropertyDescriptor[] pds = getPropertyDescriptors(method
348: .getDeclaringClass());
349: for (int i = 0; i < pds.length; i++) {
350: PropertyDescriptor pd = pds[i];
351: if (method.equals(pd.getReadMethod())
352: || method.equals(pd.getWriteMethod())) {
353: return pd;
354: }
355: }
356: return null;
357: }
358:
359: /**
360: * Determine the bean property type for the given property from the
361: * given classes/interfaces, if possible.
362: * @param propertyName the name of the bean property
363: * @param beanClasses the classes to check against
364: * @return the property type, or <code>Object.class</code> as fallback
365: */
366: public static Class findPropertyType(String propertyName,
367: Class[] beanClasses) {
368: if (beanClasses != null) {
369: for (int i = 0; i < beanClasses.length; i++) {
370: PropertyDescriptor pd = getPropertyDescriptor(
371: beanClasses[i], propertyName);
372: if (pd != null) {
373: return pd.getPropertyType();
374: }
375: }
376: }
377: return Object.class;
378: }
379:
380: /**
381: * Check if the given type represents a "simple" property:
382: * a primitive, a String, a Class, or a corresponding array.
383: * <p>Used to determine properties to check for a "simple" dependency-check.
384: * @param clazz the type to check
385: * @return whether the given type represent a "simple" property
386: * @see org.springframework.beans.factory.support.RootBeanDefinition#DEPENDENCY_CHECK_SIMPLE
387: * @see org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#checkDependencies
388: */
389: public static boolean isSimpleProperty(Class clazz) {
390: Assert.notNull(clazz, "Class must not be null");
391: return clazz.isPrimitive()
392: || ClassUtils.isPrimitiveArray(clazz)
393: || ClassUtils.isPrimitiveWrapper(clazz)
394: || ClassUtils.isPrimitiveWrapperArray(clazz)
395: || clazz.equals(String.class)
396: || clazz.equals(String[].class)
397: || clazz.equals(Class.class)
398: || clazz.equals(Class[].class);
399: }
400:
401: /**
402: * Determine if the given target type is assignable from the given value
403: * type, assuming setting by reflection. Considers primitive wrapper
404: * classes as assignable to the corresponding primitive types.
405: * @param targetType the target type
406: * @param valueType the value type that should be assigned to the target type
407: * @return if the target type is assignable from the value type
408: * @deprecated as of Spring 2.0, in favor of <code>ClassUtils.isAssignable</code>
409: * @see org.springframework.util.ClassUtils#isAssignable(Class, Class)
410: */
411: public static boolean isAssignable(Class targetType, Class valueType) {
412: return ClassUtils.isAssignable(targetType, valueType);
413: }
414:
415: /**
416: * Determine if the given type is assignable from the given value,
417: * assuming setting by reflection. Considers primitive wrapper classes
418: * as assignable to the corresponding primitive types.
419: * @param type the target type
420: * @param value the value that should be assigned to the type
421: * @return if the type is assignable from the value
422: * @deprecated as of Spring 2.0, in favor of <code>ClassUtils.isAssignableValue</code>
423: * @see org.springframework.util.ClassUtils#isAssignableValue(Class, Object)
424: */
425: public static boolean isAssignable(Class type, Object value) {
426: return ClassUtils.isAssignableValue(type, value);
427: }
428:
429: /**
430: * Copy the property values of the given source bean into the target bean.
431: * <p>Note: The source and target classes do not have to match or even be derived
432: * from each other, as long as the properties match. Any bean properties that the
433: * source bean exposes but the target bean does not will silently be ignored.
434: * <p>This is just a convenience method. For more complex transfer needs,
435: * consider using a full BeanWrapper.
436: * @param source the source bean
437: * @param target the target bean
438: * @throws BeansException if the copying failed
439: * @see BeanWrapper
440: */
441: public static void copyProperties(Object source, Object target)
442: throws BeansException {
443: copyProperties(source, target, null, null);
444: }
445:
446: /**
447: * Copy the property values of the given source bean into the given target bean,
448: * only setting properties defined in the given "editable" class (or interface).
449: * <p>Note: The source and target classes do not have to match or even be derived
450: * from each other, as long as the properties match. Any bean properties that the
451: * source bean exposes but the target bean does not will silently be ignored.
452: * <p>This is just a convenience method. For more complex transfer needs,
453: * consider using a full BeanWrapper.
454: * @param source the source bean
455: * @param target the target bean
456: * @param editable the class (or interface) to restrict property setting to
457: * @throws BeansException if the copying failed
458: * @see BeanWrapper
459: */
460: public static void copyProperties(Object source, Object target,
461: Class editable) throws BeansException {
462:
463: copyProperties(source, target, editable, null);
464: }
465:
466: /**
467: * Copy the property values of the given source bean into the given target bean,
468: * ignoring the given "ignoreProperties".
469: * <p>Note: The source and target classes do not have to match or even be derived
470: * from each other, as long as the properties match. Any bean properties that the
471: * source bean exposes but the target bean does not will silently be ignored.
472: * <p>This is just a convenience method. For more complex transfer needs,
473: * consider using a full BeanWrapper.
474: * @param source the source bean
475: * @param target the target bean
476: * @param ignoreProperties array of property names to ignore
477: * @throws BeansException if the copying failed
478: * @see BeanWrapper
479: */
480: public static void copyProperties(Object source, Object target,
481: String[] ignoreProperties) throws BeansException {
482:
483: copyProperties(source, target, null, ignoreProperties);
484: }
485:
486: /**
487: * Copy the property values of the given source bean into the given target bean.
488: * <p>Note: The source and target classes do not have to match or even be derived
489: * from each other, as long as the properties match. Any bean properties that the
490: * source bean exposes but the target bean does not will silently be ignored.
491: * @param source the source bean
492: * @param target the target bean
493: * @param editable the class (or interface) to restrict property setting to
494: * @param ignoreProperties array of property names to ignore
495: * @throws BeansException if the copying failed
496: * @see BeanWrapper
497: */
498: private static void copyProperties(Object source, Object target,
499: Class editable, String[] ignoreProperties)
500: throws BeansException {
501:
502: Assert.notNull(source, "Source must not be null");
503: Assert.notNull(target, "Target must not be null");
504:
505: Class actualEditable = target.getClass();
506: if (editable != null) {
507: if (!editable.isInstance(target)) {
508: throw new IllegalArgumentException("Target class ["
509: + target.getClass().getName()
510: + "] not assignable to Editable class ["
511: + editable.getName() + "]");
512: }
513: actualEditable = editable;
514: }
515: PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
516: List ignoreList = (ignoreProperties != null) ? Arrays
517: .asList(ignoreProperties) : null;
518:
519: for (int i = 0; i < targetPds.length; i++) {
520: PropertyDescriptor targetPd = targetPds[i];
521: if (targetPd.getWriteMethod() != null
522: && (ignoreProperties == null || (!ignoreList
523: .contains(targetPd.getName())))) {
524: PropertyDescriptor sourcePd = getPropertyDescriptor(
525: source.getClass(), targetPd.getName());
526: if (sourcePd != null
527: && sourcePd.getReadMethod() != null) {
528: try {
529: Method readMethod = sourcePd.getReadMethod();
530: if (!Modifier.isPublic(readMethod
531: .getDeclaringClass().getModifiers())) {
532: readMethod.setAccessible(true);
533: }
534: Object value = readMethod.invoke(source,
535: new Object[0]);
536: Method writeMethod = targetPd.getWriteMethod();
537: if (!Modifier.isPublic(writeMethod
538: .getDeclaringClass().getModifiers())) {
539: writeMethod.setAccessible(true);
540: }
541: writeMethod.invoke(target,
542: new Object[] { value });
543: } catch (Throwable ex) {
544: throw new FatalBeanException(
545: "Could not copy properties from source to target",
546: ex);
547: }
548: }
549: }
550: }
551: }
552:
553: }
|