001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017:
018: package org.apache.commons.beanutils;
019:
020: import java.beans.IntrospectionException;
021: import java.beans.PropertyDescriptor;
022: import java.lang.reflect.Method;
023: import java.lang.reflect.Modifier;
024:
025: /**
026: * A MappedPropertyDescriptor describes one mapped property.
027: * Mapped properties are multivalued properties like indexed properties
028: * but that are accessed with a String key instead of an index.
029: * Such property values are typically stored in a Map collection.
030: * For this class to work properly, a mapped value must have
031: * getter and setter methods of the form
032: * <p><code>get<strong>Property</strong>(String key)<code> and
033: * <p><code>set<strong>Property</strong>(String key, Object value)<code>,
034: * <p>where <code><strong>Property</strong></code> must be replaced
035: * by the name of the property.
036: * @see java.beans.PropertyDescriptor
037: *
038: * @author Rey Francois
039: * @author Gregor Rayman
040: * @version $Revision: 556229 $ $Date: 2007-07-14 07:11:19 +0100 (Sat, 14 Jul 2007) $
041: */
042:
043: public class MappedPropertyDescriptor extends PropertyDescriptor {
044: // ----------------------------------------------------- Instance Variables
045:
046: /**
047: * The underlying data type of the property we are describing.
048: */
049: private Class mappedPropertyType;
050:
051: /**
052: * The reader method for this property (if any).
053: */
054: private Method mappedReadMethod;
055:
056: /**
057: * The writer method for this property (if any).
058: */
059: private Method mappedWriteMethod;
060:
061: /**
062: * The parameter types array for the reader method signature.
063: */
064: private static final Class[] STRING_CLASS_PARAMETER = new Class[] { String.class };
065:
066: // ----------------------------------------------------------- Constructors
067:
068: /**
069: * Constructs a MappedPropertyDescriptor for a property that follows
070: * the standard Java convention by having getFoo and setFoo
071: * accessor methods, with the addition of a String parameter (the key).
072: * Thus if the argument name is "fred", it will
073: * assume that the writer method is "setFred" and the reader method
074: * is "getFred". Note that the property name should start with a lower
075: * case character, which will be capitalized in the method names.
076: *
077: * @param propertyName The programmatic name of the property.
078: * @param beanClass The Class object for the target bean. For
079: * example sun.beans.OurButton.class.
080: *
081: * @exception IntrospectionException if an exception occurs during
082: * introspection.
083: */
084: public MappedPropertyDescriptor(String propertyName, Class beanClass)
085: throws IntrospectionException {
086:
087: super (propertyName, null, null);
088:
089: if (propertyName == null || propertyName.length() == 0) {
090: throw new IntrospectionException("bad property name: "
091: + propertyName + " on class: "
092: + beanClass.getClass().getName());
093: }
094:
095: setName(propertyName);
096: String base = capitalizePropertyName(propertyName);
097:
098: // Look for mapped read method and matching write method
099: try {
100: try {
101: mappedReadMethod = getMethod(beanClass, "get" + base,
102: STRING_CLASS_PARAMETER);
103: } catch (IntrospectionException e) {
104: mappedReadMethod = getMethod(beanClass, "is" + base,
105: STRING_CLASS_PARAMETER);
106: }
107: Class[] params = { String.class,
108: mappedReadMethod.getReturnType() };
109: mappedWriteMethod = getMethod(beanClass, "set" + base,
110: params);
111: } catch (IntrospectionException e) {
112: /* Swallow IntrospectionException
113: * TODO: Why?
114: */
115: }
116:
117: // If there's no read method, then look for just a write method
118: if (mappedReadMethod == null) {
119: mappedWriteMethod = getMethod(beanClass, "set" + base, 2);
120: }
121:
122: if ((mappedReadMethod == null) && (mappedWriteMethod == null)) {
123: throw new IntrospectionException("Property '"
124: + propertyName + "' not found on "
125: + beanClass.getName());
126: }
127:
128: findMappedPropertyType();
129: }
130:
131: /**
132: * This constructor takes the name of a mapped property, and method
133: * names for reading and writing the property.
134: *
135: * @param propertyName The programmatic name of the property.
136: * @param beanClass The Class object for the target bean. For
137: * example sun.beans.OurButton.class.
138: * @param mappedGetterName The name of the method used for
139: * reading one of the property values. May be null if the
140: * property is write-only.
141: * @param mappedSetterName The name of the method used for writing
142: * one of the property values. May be null if the property is
143: * read-only.
144: *
145: * @exception IntrospectionException if an exception occurs during
146: * introspection.
147: */
148: public MappedPropertyDescriptor(String propertyName,
149: Class beanClass, String mappedGetterName,
150: String mappedSetterName) throws IntrospectionException {
151:
152: super (propertyName, null, null);
153:
154: if (propertyName == null || propertyName.length() == 0) {
155: throw new IntrospectionException("bad property name: "
156: + propertyName);
157: }
158: setName(propertyName);
159:
160: // search the mapped get and set methods
161: mappedReadMethod = getMethod(beanClass, mappedGetterName,
162: STRING_CLASS_PARAMETER);
163:
164: if (mappedReadMethod != null) {
165: Class[] params = { String.class,
166: mappedReadMethod.getReturnType() };
167: mappedWriteMethod = getMethod(beanClass, mappedSetterName,
168: params);
169: } else {
170: mappedWriteMethod = getMethod(beanClass, mappedSetterName,
171: 2);
172: }
173:
174: findMappedPropertyType();
175: }
176:
177: /**
178: * This constructor takes the name of a mapped property, and Method
179: * objects for reading and writing the property.
180: *
181: * @param propertyName The programmatic name of the property.
182: * @param mappedGetter The method used for reading one of
183: * the property values. May be be null if the property
184: * is write-only.
185: * @param mappedSetter The method used for writing one the
186: * property values. May be null if the property is read-only.
187: *
188: * @exception IntrospectionException if an exception occurs during
189: * introspection.
190: */
191: public MappedPropertyDescriptor(String propertyName,
192: Method mappedGetter, Method mappedSetter)
193: throws IntrospectionException {
194:
195: super (propertyName, mappedGetter, mappedSetter);
196:
197: if (propertyName == null || propertyName.length() == 0) {
198: throw new IntrospectionException("bad property name: "
199: + propertyName);
200: }
201:
202: setName(propertyName);
203: mappedReadMethod = mappedGetter;
204: mappedWriteMethod = mappedSetter;
205: findMappedPropertyType();
206: }
207:
208: // -------------------------------------------------------- Public Methods
209:
210: /**
211: * Gets the Class object for the property values.
212: *
213: * @return The Java type info for the property values. Note that
214: * the "Class" object may describe a built-in Java type such as "int".
215: * The result may be "null" if this is a mapped property that
216: * does not support non-keyed access.
217: * <p>
218: * This is the type that will be returned by the mappedReadMethod.
219: */
220: public Class getMappedPropertyType() {
221: return mappedPropertyType;
222: }
223:
224: /**
225: * Gets the method that should be used to read one of the property value.
226: *
227: * @return The method that should be used to read the property value.
228: * May return null if the property can't be read.
229: */
230: public Method getMappedReadMethod() {
231: return mappedReadMethod;
232: }
233:
234: /**
235: * Sets the method that should be used to read one of the property value.
236: *
237: * @param mappedGetter The mapped getter method.
238: * @throws IntrospectionException If an error occurs finding the
239: * mapped property
240: */
241: public void setMappedReadMethod(Method mappedGetter)
242: throws IntrospectionException {
243: mappedReadMethod = mappedGetter;
244: findMappedPropertyType();
245: }
246:
247: /**
248: * Gets the method that should be used to write one of the property value.
249: *
250: * @return The method that should be used to write one of the property value.
251: * May return null if the property can't be written.
252: */
253: public Method getMappedWriteMethod() {
254: return mappedWriteMethod;
255: }
256:
257: /**
258: * Sets the method that should be used to write the property value.
259: *
260: * @param mappedSetter The mapped setter method.
261: * @throws IntrospectionException If an error occurs finding the
262: * mapped property
263: */
264: public void setMappedWriteMethod(Method mappedSetter)
265: throws IntrospectionException {
266: mappedWriteMethod = mappedSetter;
267: findMappedPropertyType();
268: }
269:
270: // ------------------------------------------------------- Private Methods
271:
272: /**
273: * Introspect our bean class to identify the corresponding getter
274: * and setter methods.
275: */
276: private void findMappedPropertyType() throws IntrospectionException {
277: try {
278: mappedPropertyType = null;
279: if (mappedReadMethod != null) {
280: if (mappedReadMethod.getParameterTypes().length != 1) {
281: throw new IntrospectionException(
282: "bad mapped read method arg count");
283: }
284: mappedPropertyType = mappedReadMethod.getReturnType();
285: if (mappedPropertyType == Void.TYPE) {
286: throw new IntrospectionException(
287: "mapped read method "
288: + mappedReadMethod.getName()
289: + " returns void");
290: }
291: }
292:
293: if (mappedWriteMethod != null) {
294: Class[] params = mappedWriteMethod.getParameterTypes();
295: if (params.length != 2) {
296: throw new IntrospectionException(
297: "bad mapped write method arg count");
298: }
299: if (mappedPropertyType != null
300: && mappedPropertyType != params[1]) {
301: throw new IntrospectionException(
302: "type mismatch between mapped read and write methods");
303: }
304: mappedPropertyType = params[1];
305: }
306: } catch (IntrospectionException ex) {
307: throw ex;
308: }
309: }
310:
311: /**
312: * Return a capitalized version of the specified property name.
313: *
314: * @param s The property name
315: */
316: private static String capitalizePropertyName(String s) {
317: if (s.length() == 0) {
318: return s;
319: }
320:
321: char[] chars = s.toCharArray();
322: chars[0] = Character.toUpperCase(chars[0]);
323: return new String(chars);
324: }
325:
326: /**
327: * Find a method on a class with a specified number of parameters.
328: */
329: private static Method internalGetMethod(Class initial,
330: String methodName, int parameterCount) {
331: // For overridden methods we need to find the most derived version.
332: // So we start with the given class and walk up the superclass chain.
333: for (Class clazz = initial; clazz != null; clazz = clazz
334: .getSuperclass()) {
335: Method[] methods = clazz.getDeclaredMethods();
336: for (int i = 0; i < methods.length; i++) {
337: Method method = methods[i];
338: if (method == null) {
339: continue;
340: }
341: // skip static methods.
342: int mods = method.getModifiers();
343: if (!Modifier.isPublic(mods) || Modifier.isStatic(mods)) {
344: continue;
345: }
346: if (method.getName().equals(methodName)
347: && method.getParameterTypes().length == parameterCount) {
348: return method;
349: }
350: }
351: }
352:
353: // Now check any inherited interfaces. This is necessary both when
354: // the argument class is itself an interface, and when the argument
355: // class is an abstract class.
356: Class[] interfaces = initial.getInterfaces();
357: for (int i = 0; i < interfaces.length; i++) {
358: Method method = internalGetMethod(interfaces[i],
359: methodName, parameterCount);
360: if (method != null) {
361: return method;
362: }
363: }
364:
365: return null;
366: }
367:
368: /**
369: * Find a method on a class with a specified number of parameters.
370: */
371: private static Method getMethod(Class clazz, String methodName,
372: int parameterCount) throws IntrospectionException {
373: if (methodName == null) {
374: return null;
375: }
376:
377: Method method = internalGetMethod(clazz, methodName,
378: parameterCount);
379: if (method != null) {
380: return method;
381: }
382:
383: // No Method found
384: throw new IntrospectionException("No method \"" + methodName
385: + "\" with " + parameterCount + " parameter(s)");
386: }
387:
388: /**
389: * Find a method on a class with a specified parameter list.
390: */
391: private static Method getMethod(Class clazz, String methodName,
392: Class[] parameterTypes) throws IntrospectionException {
393: if (methodName == null) {
394: return null;
395: }
396:
397: Method method = MethodUtils.getMatchingAccessibleMethod(clazz,
398: methodName, parameterTypes);
399: if (method != null) {
400: return method;
401: }
402:
403: int parameterCount = (parameterTypes == null) ? 0
404: : parameterTypes.length;
405:
406: // No Method found
407: throw new IntrospectionException("No method \"" + methodName
408: + "\" with " + parameterCount
409: + " parameter(s) of matching types.");
410: }
411:
412: }
|