001: /*
002: * The Apache Software License, Version 1.1
003: *
004: * Copyright (c) 2001-2003 The Apache Software Foundation. All rights
005: * reserved.
006: *
007: * Redistribution and use in source and binary forms, with or without
008: * modification, are permitted provided that the following conditions
009: * are met:
010: *
011: * 1. Redistributions of source code must retain the above copyright
012: * notice, this list of conditions and the following disclaimer.
013: *
014: * 2. Redistributions in binary form must reproduce the above copyright
015: * notice, this list of conditions and the following disclaimer in
016: * the documentation and/or other materials provided with the
017: * distribution.
018: *
019: * 3. The end-user documentation included with the redistribution,
020: * if any, must include the following acknowledgement:
021: * "This product includes software developed by the
022: * Apache Software Foundation (http://www.apache.org/)."
023: * Alternately, this acknowledgement may appear in the software itself,
024: * if and wherever such third-party acknowledgements normally appear.
025: *
026: * 4. The names "Apache", "The Jakarta Project", "Commons", and "Apache Software
027: * Foundation" must not be used to endorse or promote products derived
028: * from this software without prior written permission. For written
029: * permission, please contact apache@apache.org.
030: *
031: * 5. Products derived from this software may not be called "Apache",
032: * "Apache" nor may "Apache" appear in their names without prior
033: * written permission of the Apache Software Foundation.
034: *
035: * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
036: * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
037: * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
038: * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
039: * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
040: * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
041: * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
042: * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
043: * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
044: * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
045: * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
046: * SUCH DAMAGE.
047: * ====================================================================
048: *
049: * This software consists of voluntary contributions made by many
050: * individuals on behalf of the Apache Software Foundation. For more
051: * information on the Apache Software Foundation, please see
052: * <http://www.apache.org/>.
053: *
054: */
055:
056: package com.opensymphony.workflow.designer.beanutils;
057:
058: import java.beans.IntrospectionException;
059: import java.beans.PropertyDescriptor;
060: import java.lang.reflect.Method;
061: import java.lang.reflect.Modifier;
062: import java.security.AccessController;
063: import java.security.PrivilegedAction;
064:
065: /**
066: * A MappedPropertyDescriptor describes one mapped property.
067: * Mapped properties are multivalued properties like indexed properties
068: * but that are accessed with a String key instead of an index.
069: * Such property values are typically stored in a Map collection.
070: * For this class to work properly, a mapped value must have
071: * getter and setter methods of the form
072: * <p><code>get<strong>Property</strong>(String key)<code> and
073: * <p><code>set<Property>(String key, Object value)<code>,
074: * <p>where <code><strong>Property</strong></code> must be replaced
075: * by the name of the property.
076: *
077: * @author Rey Fran�ois
078: * @author Gregor Ra�man
079: * @see java.beans.PropertyDescriptor
080: */
081:
082: public class MappedPropertyDescriptor extends PropertyDescriptor {
083: // ----------------------------------------------------- Instance Variables
084:
085: /** The underlying data type of the property we are describing. */
086: private Class mappedPropertyType;
087:
088: /** The reader method for this property (if any). */
089: private Method mappedReadMethod;
090:
091: /** The writer method for this property (if any). */
092: private Method mappedWriteMethod;
093:
094: /** The parameter types array for the reader method signature. */
095: private static final Class[] stringClassArray = new Class[] { String.class };
096:
097: // ----------------------------------------------------------- Constructors
098:
099: /**
100: * Constructs a MappedPropertyDescriptor for a property that follows
101: * the standard Java convention by having getFoo and setFoo
102: * accessor methods, with the addition of a String parameter (the key).
103: * Thus if the argument name is "fred", it will
104: * assume that the writer method is "setFred" and the reader method
105: * is "getFred". Note that the property name should start with a lower
106: * case character, which will be capitalized in the method names.
107: *
108: * @param propertyName The programmatic name of the property.
109: * @param beanClass The Class object for the target bean. For
110: * example sun.beans.OurButton.class.
111: * @throws IntrospectionException if an exception occurs during
112: * introspection.
113: */
114: public MappedPropertyDescriptor(String propertyName, Class beanClass)
115: throws IntrospectionException {
116:
117: super (propertyName, null, null);
118:
119: if (propertyName == null || propertyName.length() == 0) {
120: throw new IntrospectionException("bad property name: "
121: + propertyName + " on class: "
122: + beanClass.getClass().getName());
123: }
124:
125: setName(propertyName);
126: String base = capitalizePropertyName(propertyName);
127:
128: // Look for mapped read method and matching write method
129: try {
130: mappedReadMethod = findMethod(beanClass, "get" + base, 1,
131: stringClassArray);
132: Class params[] = { String.class,
133: mappedReadMethod.getReturnType() };
134: mappedWriteMethod = findMethod(beanClass, "set" + base, 2,
135: params);
136: } catch (IntrospectionException e) {
137: ;
138: }
139:
140: // If there's no read method, then look for just a write method
141: if (mappedReadMethod == null) {
142: mappedWriteMethod = findMethod(beanClass, "set" + base, 2);
143: }
144:
145: if ((mappedReadMethod == null) && (mappedWriteMethod == null)) {
146: throw new IntrospectionException("Property '"
147: + propertyName + "' not found on "
148: + beanClass.getName());
149: }
150:
151: findMappedPropertyType();
152: }
153:
154: /**
155: * This constructor takes the name of a mapped property, and method
156: * names for reading and writing the property.
157: *
158: * @param propertyName The programmatic name of the property.
159: * @param beanClass The Class object for the target bean. For
160: * example sun.beans.OurButton.class.
161: * @param mappedGetterName The name of the method used for
162: * reading one of the property values. May be null if the
163: * property is write-only.
164: * @param mappedSetterName The name of the method used for writing
165: * one of the property values. May be null if the property is
166: * read-only.
167: * @throws IntrospectionException if an exception occurs during
168: * introspection.
169: */
170: public MappedPropertyDescriptor(String propertyName,
171: Class beanClass, String mappedGetterName,
172: String mappedSetterName) throws IntrospectionException {
173:
174: super (propertyName, null, null);
175:
176: if (propertyName == null || propertyName.length() == 0) {
177: throw new IntrospectionException("bad property name: "
178: + propertyName);
179: }
180: setName(propertyName);
181:
182: // search the mapped get and set methods
183: mappedReadMethod = findMethod(beanClass, mappedGetterName, 1,
184: stringClassArray);
185:
186: if (mappedReadMethod != null) {
187: Class params[] = { String.class,
188: mappedReadMethod.getReturnType() };
189: mappedWriteMethod = findMethod(beanClass, mappedSetterName,
190: 2, params);
191: } else {
192: mappedWriteMethod = findMethod(beanClass, mappedSetterName,
193: 2);
194: }
195:
196: findMappedPropertyType();
197: }
198:
199: /**
200: * This constructor takes the name of a mapped property, and Method
201: * objects for reading and writing the property.
202: *
203: * @param propertyName The programmatic name of the property.
204: * @param mappedGetter The method used for reading one of
205: * the property values. May be be null if the property
206: * is write-only.
207: * @param mappedSetter The method used for writing one the
208: * property values. May be null if the property is read-only.
209: * @throws IntrospectionException if an exception occurs during
210: * introspection.
211: */
212: public MappedPropertyDescriptor(String propertyName,
213: Method mappedGetter, Method mappedSetter)
214: throws IntrospectionException {
215:
216: super (propertyName, mappedGetter, mappedSetter);
217:
218: if (propertyName == null || propertyName.length() == 0) {
219: throw new IntrospectionException("bad property name: "
220: + propertyName);
221: }
222:
223: setName(propertyName);
224: mappedReadMethod = mappedGetter;
225: mappedWriteMethod = mappedSetter;
226: findMappedPropertyType();
227: }
228:
229: // -------------------------------------------------------- Public Methods
230:
231: /**
232: * Gets the Class object for the property values.
233: *
234: * @return The Java type info for the property values. Note that
235: * the "Class" object may describe a built-in Java type such as "int".
236: * The result may be "null" if this is a mapped property that
237: * does not support non-keyed access.
238: * <p/>
239: * This is the type that will be returned by the mappedReadMethod.
240: */
241: public Class getMappedPropertyType() {
242: return mappedPropertyType;
243: }
244:
245: /**
246: * Gets the method that should be used to read one of the property value.
247: *
248: * @return The method that should be used to read the property value.
249: * May return null if the property can't be read.
250: */
251: public Method getMappedReadMethod() {
252: return mappedReadMethod;
253: }
254:
255: /**
256: * Sets the method that should be used to read one of the property value.
257: *
258: * @param mappedGetter The new getter method.
259: */
260: public void setMappedReadMethod(Method mappedGetter)
261: throws IntrospectionException {
262: mappedReadMethod = mappedGetter;
263: findMappedPropertyType();
264: }
265:
266: /**
267: * Gets the method that should be used to write one of the property value.
268: *
269: * @return The method that should be used to write one of the property value.
270: * May return null if the property can't be written.
271: */
272: public Method getMappedWriteMethod() {
273: return mappedWriteMethod;
274: }
275:
276: /**
277: * Sets the method that should be used to write the property value.
278: *
279: * @param mappedSetter The new setter method.
280: */
281: public void setMappedWriteMethod(Method mappedSetter)
282: throws IntrospectionException {
283: mappedWriteMethod = mappedSetter;
284: findMappedPropertyType();
285: }
286:
287: // ------------------------------------------------------- Private Methods
288:
289: /**
290: * Introspect our bean class to identify the corresponding getter
291: * and setter methods.
292: */
293: private void findMappedPropertyType() throws IntrospectionException {
294: try {
295: mappedPropertyType = null;
296: if (mappedReadMethod != null) {
297: if (mappedReadMethod.getParameterTypes().length != 1) {
298: throw new IntrospectionException(
299: "bad mapped read method arg count");
300: }
301: mappedPropertyType = mappedReadMethod.getReturnType();
302: if (mappedPropertyType == Void.TYPE) {
303: throw new IntrospectionException(
304: "mapped read method "
305: + mappedReadMethod.getName()
306: + " returns void");
307: }
308: }
309:
310: if (mappedWriteMethod != null) {
311: Class params[] = mappedWriteMethod.getParameterTypes();
312: if (params.length != 2) {
313: throw new IntrospectionException(
314: "bad mapped write method arg count");
315: }
316: if (mappedPropertyType != null
317: && mappedPropertyType != params[1]) {
318: throw new IntrospectionException(
319: "type mismatch between mapped read and write methods");
320: }
321: mappedPropertyType = params[1];
322: }
323: } catch (IntrospectionException ex) {
324: throw ex;
325: }
326: }
327:
328: /**
329: * Return a capitalized version of the specified property name.
330: *
331: * @param s The property name
332: */
333: private static String capitalizePropertyName(String s) {
334: if (s.length() == 0) {
335: return s;
336: }
337:
338: char chars[] = s.toCharArray();
339: chars[0] = Character.toUpperCase(chars[0]);
340: return new String(chars);
341: }
342:
343: //======================================================================
344: // Package private support methods (copied from java.beans.Introspector).
345: //======================================================================
346:
347: // Cache of Class.getDeclaredMethods:
348: private static java.util.Hashtable declaredMethodCache = new java.util.Hashtable();
349:
350: /*
351: * Internal method to return *public* methods within a class.
352: */
353: private static synchronized Method[] getPublicDeclaredMethods(
354: Class clz) {
355: // Looking up Class.getDeclaredMethods is relatively expensive,
356: // so we cache the results.
357: final Class fclz = clz;
358: Method[] result = (Method[]) declaredMethodCache.get(fclz);
359: if (result != null) {
360: return result;
361: }
362:
363: // We have to raise privilege for getDeclaredMethods
364: result = (Method[]) AccessController
365: .doPrivileged(new PrivilegedAction() {
366: public Object run() {
367: return fclz.getDeclaredMethods();
368: }
369: });
370:
371: // Null out any non-public methods.
372: for (int i = 0; i < result.length; i++) {
373: Method method = result[i];
374: int mods = method.getModifiers();
375: if (!Modifier.isPublic(mods)) {
376: result[i] = null;
377: }
378: }
379:
380: // Add it to the cache.
381: declaredMethodCache.put(clz, result);
382: return result;
383: }
384:
385: /**
386: * Internal support for finding a target methodName on a given class.
387: */
388: private static Method internalFindMethod(Class start,
389: String methodName, int argCount) {
390: // For overridden methods we need to find the most derived version.
391: // So we start with the given class and walk up the superclass chain.
392: for (Class cl = start; cl != null; cl = cl.getSuperclass()) {
393: Method methods[] = getPublicDeclaredMethods(cl);
394: for (int i = 0; i < methods.length; i++) {
395: Method method = methods[i];
396: if (method == null) {
397: continue;
398: }
399: // skip static methods.
400: int mods = method.getModifiers();
401: if (Modifier.isStatic(mods)) {
402: continue;
403: }
404: if (method.getName().equals(methodName)
405: && method.getParameterTypes().length == argCount) {
406: return method;
407: }
408: }
409: }
410:
411: // Now check any inherited interfaces. This is necessary both when
412: // the argument class is itself an interface, and when the argument
413: // class is an abstract class.
414: Class ifcs[] = start.getInterfaces();
415: for (int i = 0; i < ifcs.length; i++) {
416: Method m = internalFindMethod(ifcs[i], methodName, argCount);
417: if (m != null) {
418: return m;
419: }
420: }
421:
422: return null;
423: }
424:
425: /**
426: * Internal support for finding a target methodName with a given
427: * parameter list on a given class.
428: */
429: private static Method internalFindMethod(Class start,
430: String methodName, int argCount, Class args[]) {
431: // For overriden methods we need to find the most derived version.
432: // So we start with the given class and walk up the superclass chain.
433: for (Class cl = start; cl != null; cl = cl.getSuperclass()) {
434: Method methods[] = getPublicDeclaredMethods(cl);
435: for (int i = 0; i < methods.length; i++) {
436: Method method = methods[i];
437: if (method == null) {
438: continue;
439: }
440: // skip static methods.
441: int mods = method.getModifiers();
442: if (Modifier.isStatic(mods)) {
443: continue;
444: }
445: // make sure method signature matches.
446: Class params[] = method.getParameterTypes();
447: if (method.getName().equals(methodName)
448: && params.length == argCount) {
449: boolean different = false;
450: if (argCount > 0) {
451: for (int j = 0; j < argCount; j++) {
452: if (params[j] != args[j]) {
453: different = true;
454: continue;
455: }
456: }
457: if (different) {
458: continue;
459: }
460: }
461: return method;
462: }
463: }
464: }
465:
466: // Now check any inherited interfaces. This is necessary both when
467: // the argument class is itself an interface, and when the argument
468: // class is an abstract class.
469: Class ifcs[] = start.getInterfaces();
470: for (int i = 0; i < ifcs.length; i++) {
471: Method m = internalFindMethod(ifcs[i], methodName, argCount);
472: if (m != null) {
473: return m;
474: }
475: }
476:
477: return null;
478: }
479:
480: /**
481: * Find a target methodName on a given class.
482: */
483: static Method findMethod(Class cls, String methodName, int argCount)
484: throws IntrospectionException {
485: if (methodName == null) {
486: return null;
487: }
488:
489: Method m = internalFindMethod(cls, methodName, argCount);
490: if (m != null) {
491: return m;
492: }
493:
494: // We failed to find a suitable method
495: throw new IntrospectionException("No method \"" + methodName
496: + "\" with " + argCount + " arg(s)");
497: }
498:
499: /**
500: * Find a target methodName with specific parameter list on a given class.
501: */
502: static Method findMethod(Class cls, String methodName,
503: int argCount, Class args[]) throws IntrospectionException {
504: if (methodName == null) {
505: return null;
506: }
507:
508: Method m = internalFindMethod(cls, methodName, argCount, args);
509: if (m != null) {
510: return m;
511: }
512:
513: // We failed to find a suitable method
514: throw new IntrospectionException("No method \"" + methodName
515: + "\" with " + argCount + " arg(s) of matching types.");
516: }
517:
518: /**
519: * Return true if class a is either equivalent to class b, or
520: * if class a is a subclass of class b, ie if a either "extends"
521: * or "implements" b.
522: * Note tht either or both "Class" objects may represent interfaces.
523: */
524: static boolean isSubclass(Class a, Class b) {
525: // We rely on the fact that for any given java class or
526: // primtitive type there is a unqiue Class object, so
527: // we can use object equivalence in the comparisons.
528: if (a == b) {
529: return true;
530: }
531:
532: if (a == null || b == null) {
533: return false;
534: }
535:
536: for (Class x = a; x != null; x = x.getSuperclass()) {
537: if (x == b) {
538: return true;
539: }
540:
541: if (b.isInterface()) {
542: Class interfaces[] = x.getInterfaces();
543: for (int i = 0; i < interfaces.length; i++) {
544: if (isSubclass(interfaces[i], b)) {
545: return true;
546: }
547: }
548: }
549: }
550:
551: return false;
552: }
553: }
|