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