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
021 import java.beans.IntrospectionException;
022 import java.beans.PropertyDescriptor;
023 import java.lang.ref.Reference;
024 import java.lang.ref.SoftReference;
025 import java.lang.ref.WeakReference;
026 import java.lang.reflect.Method;
027 import java.lang.reflect.Modifier;
028
029
030 /**
031 * A MappedPropertyDescriptor describes one mapped property.
032 * Mapped properties are multivalued properties like indexed properties
033 * but that are accessed with a String key instead of an index.
034 * Such property values are typically stored in a Map collection.
035 * For this class to work properly, a mapped value must have
036 * getter and setter methods of the form
037 * <p><code>get<strong>Property</strong>(String key)<code> and
038 * <p><code>set<strong>Property</strong>(String key, Object value)<code>,
039 * <p>where <code><strong>Property</strong></code> must be replaced
040 * by the name of the property.
041 * @see java.beans.PropertyDescriptor
042 *
043 * @author Rey Francois
044 * @author Gregor Rayman
045 * @version $Revision: 806915 $ $Date: 2009-08-23 01:50:23 +0100 (Sun, 23 Aug 2009) $
046 */
047
048
049 public class MappedPropertyDescriptor extends PropertyDescriptor {
050 // ----------------------------------------------------- Instance Variables
051
052 /**
053 * The underlying data type of the property we are describing.
054 */
055 private Reference mappedPropertyTypeRef;
056
057 /**
058 * The reader method for this property (if any).
059 */
060 private MappedMethodReference mappedReadMethodRef;
061
062 /**
063 * The writer method for this property (if any).
064 */
065 private MappedMethodReference mappedWriteMethodRef;
066
067 /**
068 * The parameter types array for the reader method signature.
069 */
070 private static final Class[] STRING_CLASS_PARAMETER = new Class[]{String.class};
071
072 // ----------------------------------------------------------- Constructors
073
074 /**
075 * Constructs a MappedPropertyDescriptor for a property that follows
076 * the standard Java convention by having getFoo and setFoo
077 * accessor methods, with the addition of a String parameter (the key).
078 * Thus if the argument name is "fred", it will
079 * assume that the writer method is "setFred" and the reader method
080 * is "getFred". Note that the property name should start with a lower
081 * case character, which will be capitalized in the method names.
082 *
083 * @param propertyName The programmatic name of the property.
084 * @param beanClass The Class object for the target bean. For
085 * example sun.beans.OurButton.class.
086 *
087 * @exception IntrospectionException if an exception occurs during
088 * introspection.
089 */
090 public MappedPropertyDescriptor(String propertyName, Class beanClass)
091 throws IntrospectionException {
092
093 super(propertyName, null, null);
094
095 if (propertyName == null || propertyName.length() == 0) {
096 throw new IntrospectionException("bad property name: " +
097 propertyName + " on class: " + beanClass.getClass().getName());
098 }
099
100 setName(propertyName);
101 String base = capitalizePropertyName(propertyName);
102
103 // Look for mapped read method and matching write method
104 Method mappedReadMethod = null;
105 Method mappedWriteMethod = null;
106 try {
107 try {
108 mappedReadMethod = getMethod(beanClass, "get" + base,
109 STRING_CLASS_PARAMETER);
110 } catch (IntrospectionException e) {
111 mappedReadMethod = getMethod(beanClass, "is" + base,
112 STRING_CLASS_PARAMETER);
113 }
114 Class[] params = { String.class, mappedReadMethod.getReturnType() };
115 mappedWriteMethod = getMethod(beanClass, "set" + base, params);
116 } catch (IntrospectionException e) {
117 /* Swallow IntrospectionException
118 * TODO: Why?
119 */
120 }
121
122 // If there's no read method, then look for just a write method
123 if (mappedReadMethod == null) {
124 mappedWriteMethod = getMethod(beanClass, "set" + base, 2);
125 }
126
127 if ((mappedReadMethod == null) && (mappedWriteMethod == null)) {
128 throw new IntrospectionException("Property '" + propertyName +
129 "' not found on " +
130 beanClass.getName());
131 }
132 mappedReadMethodRef = new MappedMethodReference(mappedReadMethod);
133 mappedWriteMethodRef = new MappedMethodReference(mappedWriteMethod);
134
135 findMappedPropertyType();
136 }
137
138
139 /**
140 * This constructor takes the name of a mapped property, and method
141 * names for reading and writing the property.
142 *
143 * @param propertyName The programmatic name of the property.
144 * @param beanClass The Class object for the target bean. For
145 * example sun.beans.OurButton.class.
146 * @param mappedGetterName The name of the method used for
147 * reading one of the property values. May be null if the
148 * property is write-only.
149 * @param mappedSetterName The name of the method used for writing
150 * one of the property values. May be null if the property is
151 * read-only.
152 *
153 * @exception IntrospectionException if an exception occurs during
154 * introspection.
155 */
156 public MappedPropertyDescriptor(String propertyName, Class beanClass,
157 String mappedGetterName, String mappedSetterName)
158 throws IntrospectionException {
159
160 super(propertyName, null, null);
161
162 if (propertyName == null || propertyName.length() == 0) {
163 throw new IntrospectionException("bad property name: " +
164 propertyName);
165 }
166 setName(propertyName);
167
168 // search the mapped get and set methods
169 Method mappedReadMethod = null;
170 Method mappedWriteMethod = null;
171 mappedReadMethod =
172 getMethod(beanClass, mappedGetterName, STRING_CLASS_PARAMETER);
173
174 if (mappedReadMethod != null) {
175 Class[] params = { String.class, mappedReadMethod.getReturnType() };
176 mappedWriteMethod =
177 getMethod(beanClass, mappedSetterName, params);
178 } else {
179 mappedWriteMethod =
180 getMethod(beanClass, mappedSetterName, 2);
181 }
182 mappedReadMethodRef = new MappedMethodReference(mappedReadMethod);
183 mappedWriteMethodRef = new MappedMethodReference(mappedWriteMethod);
184
185 findMappedPropertyType();
186 }
187
188 /**
189 * This constructor takes the name of a mapped property, and Method
190 * objects for reading and writing the property.
191 *
192 * @param propertyName The programmatic name of the property.
193 * @param mappedGetter The method used for reading one of
194 * the property values. May be be null if the property
195 * is write-only.
196 * @param mappedSetter The method used for writing one the
197 * property values. May be null if the property is read-only.
198 *
199 * @exception IntrospectionException if an exception occurs during
200 * introspection.
201 */
202 public MappedPropertyDescriptor(String propertyName,
203 Method mappedGetter, Method mappedSetter)
204 throws IntrospectionException {
205
206 super(propertyName, mappedGetter, mappedSetter);
207
208 if (propertyName == null || propertyName.length() == 0) {
209 throw new IntrospectionException("bad property name: " +
210 propertyName);
211 }
212
213 setName(propertyName);
214 mappedReadMethodRef = new MappedMethodReference(mappedGetter);
215 mappedWriteMethodRef = new MappedMethodReference(mappedSetter);
216 findMappedPropertyType();
217 }
218
219 // -------------------------------------------------------- Public Methods
220
221 /**
222 * Gets the Class object for the property values.
223 *
224 * @return The Java type info for the property values. Note that
225 * the "Class" object may describe a built-in Java type such as "int".
226 * The result may be "null" if this is a mapped property that
227 * does not support non-keyed access.
228 * <p>
229 * This is the type that will be returned by the mappedReadMethod.
230 */
231 public Class getMappedPropertyType() {
232 return (Class)mappedPropertyTypeRef.get();
233 }
234
235 /**
236 * Gets the method that should be used to read one of the property value.
237 *
238 * @return The method that should be used to read the property value.
239 * May return null if the property can't be read.
240 */
241 public Method getMappedReadMethod() {
242 return mappedReadMethodRef.get();
243 }
244
245 /**
246 * Sets the method that should be used to read one of the property value.
247 *
248 * @param mappedGetter The mapped getter method.
249 * @throws IntrospectionException If an error occurs finding the
250 * mapped property
251 */
252 public void setMappedReadMethod(Method mappedGetter)
253 throws IntrospectionException {
254 mappedReadMethodRef = new MappedMethodReference(mappedGetter);
255 findMappedPropertyType();
256 }
257
258 /**
259 * Gets the method that should be used to write one of the property value.
260 *
261 * @return The method that should be used to write one of the property value.
262 * May return null if the property can't be written.
263 */
264 public Method getMappedWriteMethod() {
265 return mappedWriteMethodRef.get();
266 }
267
268 /**
269 * Sets the method that should be used to write the property value.
270 *
271 * @param mappedSetter The mapped setter method.
272 * @throws IntrospectionException If an error occurs finding the
273 * mapped property
274 */
275 public void setMappedWriteMethod(Method mappedSetter)
276 throws IntrospectionException {
277 mappedWriteMethodRef = new MappedMethodReference(mappedSetter);
278 findMappedPropertyType();
279 }
280
281 // ------------------------------------------------------- Private Methods
282
283 /**
284 * Introspect our bean class to identify the corresponding getter
285 * and setter methods.
286 */
287 private void findMappedPropertyType() throws IntrospectionException {
288 try {
289 Method mappedReadMethod = getMappedReadMethod();
290 Method mappedWriteMethod = getMappedWriteMethod();
291 Class mappedPropertyType = null;
292 if (mappedReadMethod != null) {
293 if (mappedReadMethod.getParameterTypes().length != 1) {
294 throw new IntrospectionException
295 ("bad mapped read method arg count");
296 }
297 mappedPropertyType = mappedReadMethod.getReturnType();
298 if (mappedPropertyType == Void.TYPE) {
299 throw new IntrospectionException
300 ("mapped read method " +
301 mappedReadMethod.getName() + " returns void");
302 }
303 }
304
305 if (mappedWriteMethod != null) {
306 Class[] params = mappedWriteMethod.getParameterTypes();
307 if (params.length != 2) {
308 throw new IntrospectionException
309 ("bad mapped write method arg count");
310 }
311 if (mappedPropertyType != null &&
312 mappedPropertyType != params[1]) {
313 throw new IntrospectionException
314 ("type mismatch between mapped read and write methods");
315 }
316 mappedPropertyType = params[1];
317 }
318 mappedPropertyTypeRef = new SoftReference(mappedPropertyType);
319 } catch (IntrospectionException ex) {
320 throw ex;
321 }
322 }
323
324
325 /**
326 * Return a capitalized version of the specified property name.
327 *
328 * @param s The property name
329 */
330 private static String capitalizePropertyName(String s) {
331 if (s.length() == 0) {
332 return s;
333 }
334
335 char[] chars = s.toCharArray();
336 chars[0] = Character.toUpperCase(chars[0]);
337 return new String(chars);
338 }
339
340 /**
341 * Find a method on a class with a specified number of parameters.
342 */
343 private static Method internalGetMethod(Class initial, String methodName,
344 int parameterCount) {
345 // For overridden methods we need to find the most derived version.
346 // So we start with the given class and walk up the superclass chain.
347 for (Class clazz = initial; clazz != null; clazz = clazz.getSuperclass()) {
348 Method[] methods = clazz.getDeclaredMethods();
349 for (int i = 0; i < methods.length; i++) {
350 Method method = methods[i];
351 if (method == null) {
352 continue;
353 }
354 // skip static methods.
355 int mods = method.getModifiers();
356 if (!Modifier.isPublic(mods) ||
357 Modifier.isStatic(mods)) {
358 continue;
359 }
360 if (method.getName().equals(methodName) &&
361 method.getParameterTypes().length == parameterCount) {
362 return method;
363 }
364 }
365 }
366
367 // Now check any inherited interfaces. This is necessary both when
368 // the argument class is itself an interface, and when the argument
369 // class is an abstract class.
370 Class[] interfaces = initial.getInterfaces();
371 for (int i = 0; i < interfaces.length; i++) {
372 Method method = internalGetMethod(interfaces[i], methodName, parameterCount);
373 if (method != null) {
374 return method;
375 }
376 }
377
378 return null;
379 }
380
381 /**
382 * Find a method on a class with a specified number of parameters.
383 */
384 private static Method getMethod(Class clazz, String methodName, int parameterCount)
385 throws IntrospectionException {
386 if (methodName == null) {
387 return null;
388 }
389
390 Method method = internalGetMethod(clazz, methodName, parameterCount);
391 if (method != null) {
392 return method;
393 }
394
395 // No Method found
396 throw new IntrospectionException("No method \"" + methodName +
397 "\" with " + parameterCount + " parameter(s)");
398 }
399
400 /**
401 * Find a method on a class with a specified parameter list.
402 */
403 private static Method getMethod(Class clazz, String methodName, Class[] parameterTypes)
404 throws IntrospectionException {
405 if (methodName == null) {
406 return null;
407 }
408
409 Method method = MethodUtils.getMatchingAccessibleMethod(clazz, methodName, parameterTypes);
410 if (method != null) {
411 return method;
412 }
413
414 int parameterCount = (parameterTypes == null) ? 0 : parameterTypes.length;
415
416 // No Method found
417 throw new IntrospectionException("No method \"" + methodName +
418 "\" with " + parameterCount + " parameter(s) of matching types.");
419 }
420
421 /**
422 * Holds a {@link Method} in a {@link SoftReference} so that it
423 * it doesn't prevent any ClassLoader being garbage collected, but
424 * tries to re-create the method if the method reference has been
425 * released.
426 *
427 * See http://issues.apache.org/jira/browse/BEANUTILS-291
428 */
429 private static class MappedMethodReference {
430 private String className;
431 private String methodName;
432 private Reference methodRef;
433 private Reference classRef;
434 private Reference writeParamTypeRef0;
435 private Reference writeParamTypeRef1;
436 private String[] writeParamClassNames;
437 MappedMethodReference(Method m) {
438 if (m != null) {
439 className = m.getDeclaringClass().getName();
440 methodName = m.getName();
441 methodRef = new SoftReference(m);
442 classRef = new WeakReference(m.getDeclaringClass());
443 Class[] types = m.getParameterTypes();
444 if (types.length == 2) {
445 writeParamTypeRef0 = new WeakReference(types[0]);
446 writeParamTypeRef1 = new WeakReference(types[1]);
447 writeParamClassNames = new String[2];
448 writeParamClassNames[0] = types[0].getName();
449 writeParamClassNames[1] = types[1].getName();
450 }
451 }
452 }
453 private Method get() {
454 if (methodRef == null) {
455 return null;
456 }
457 Method m = (Method)methodRef.get();
458 if (m == null) {
459 Class clazz = (Class)classRef.get();
460 if (clazz == null) {
461 clazz = reLoadClass();
462 if (clazz != null) {
463 classRef = new WeakReference(clazz);
464 }
465 }
466 if (clazz == null) {
467 throw new RuntimeException("Method " + methodName + " for " +
468 className + " could not be reconstructed - class reference has gone");
469 }
470 Class[] paramTypes = null;
471 if (writeParamClassNames != null) {
472 paramTypes = new Class[2];
473 paramTypes[0] = (Class)writeParamTypeRef0.get();
474 if (paramTypes[0] == null) {
475 paramTypes[0] = reLoadClass(writeParamClassNames[0]);
476 if (paramTypes[0] != null) {
477 writeParamTypeRef0 = new WeakReference(paramTypes[0]);
478 }
479 }
480 paramTypes[1] = (Class)writeParamTypeRef1.get();
481 if (paramTypes[1] == null) {
482 paramTypes[1] = reLoadClass(writeParamClassNames[1]);
483 if (paramTypes[1] != null) {
484 writeParamTypeRef1 = new WeakReference(paramTypes[1]);
485 }
486 }
487 } else {
488 paramTypes = STRING_CLASS_PARAMETER;
489 }
490 try {
491 m = clazz.getMethod(methodName, paramTypes);
492 // Un-comment following line for testing
493 // System.out.println("Recreated Method " + methodName + " for " + className);
494 } catch (NoSuchMethodException e) {
495 throw new RuntimeException("Method " + methodName + " for " +
496 className + " could not be reconstructed - method not found");
497 }
498 methodRef = new SoftReference(m);
499 }
500 return m;
501 }
502
503 /**
504 * Try to re-load the class
505 */
506 private Class reLoadClass() {
507 return reLoadClass(className);
508 }
509
510 /**
511 * Try to re-load the class
512 */
513 private Class reLoadClass(String name) {
514
515 ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
516
517 // Try the context class loader
518 if (classLoader != null) {
519 try {
520 return classLoader.loadClass(name);
521 } catch (ClassNotFoundException e) {
522 // ignore
523 }
524 }
525
526 // Try this class's class loader
527 classLoader = MappedPropertyDescriptor.class.getClassLoader();
528 try {
529 return classLoader.loadClass(name);
530 } catch (ClassNotFoundException e) {
531 return null;
532 }
533 }
534 }
535 }