1. /*
  2. * Copyright 2001-2004 The Apache Software Foundation.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. package org.apache.commons.beanutils;
  17. import java.beans.IntrospectionException;
  18. import java.beans.PropertyDescriptor;
  19. import java.lang.reflect.Method;
  20. import java.lang.reflect.Modifier;
  21. import java.security.AccessController;
  22. import java.security.PrivilegedAction;
  23. /**
  24. * A MappedPropertyDescriptor describes one mapped property.
  25. * Mapped properties are multivalued properties like indexed properties
  26. * but that are accessed with a String key instead of an index.
  27. * Such property values are typically stored in a Map collection.
  28. * For this class to work properly, a mapped value must have
  29. * getter and setter methods of the form
  30. * <p><code>get<strong>Property</strong>(String key)<code> and
  31. * <p><code>set<Property>(String key, Object value)<code>,
  32. * <p>where <code><strong>Property</strong></code> must be replaced
  33. * by the name of the property.
  34. * @see java.beans.PropertyDescriptor
  35. *
  36. * @author Rey François
  37. * @author Gregor Raıman
  38. * @version $Revision: 1.18.2.1 $ $Date: 2004/07/27 21:44:26 $
  39. */
  40. public class MappedPropertyDescriptor extends PropertyDescriptor {
  41. // ----------------------------------------------------- Instance Variables
  42. /**
  43. * The underlying data type of the property we are describing.
  44. */
  45. private Class mappedPropertyType;
  46. /**
  47. * The reader method for this property (if any).
  48. */
  49. private Method mappedReadMethod;
  50. /**
  51. * The writer method for this property (if any).
  52. */
  53. private Method mappedWriteMethod;
  54. /**
  55. * The parameter types array for the reader method signature.
  56. */
  57. private static final Class[] stringClassArray = new Class[]{String.class};
  58. // ----------------------------------------------------------- Constructors
  59. /**
  60. * Constructs a MappedPropertyDescriptor for a property that follows
  61. * the standard Java convention by having getFoo and setFoo
  62. * accessor methods, with the addition of a String parameter (the key).
  63. * Thus if the argument name is "fred", it will
  64. * assume that the writer method is "setFred" and the reader method
  65. * is "getFred". Note that the property name should start with a lower
  66. * case character, which will be capitalized in the method names.
  67. *
  68. * @param propertyName The programmatic name of the property.
  69. * @param beanClass The Class object for the target bean. For
  70. * example sun.beans.OurButton.class.
  71. *
  72. * @exception IntrospectionException if an exception occurs during
  73. * introspection.
  74. */
  75. public MappedPropertyDescriptor(String propertyName, Class beanClass)
  76. throws IntrospectionException {
  77. super(propertyName, null, null);
  78. if (propertyName == null || propertyName.length() == 0) {
  79. throw new IntrospectionException("bad property name: " +
  80. propertyName + " on class: " + beanClass.getClass().getName());
  81. }
  82. setName(propertyName);
  83. String base = capitalizePropertyName(propertyName);
  84. // Look for mapped read method and matching write method
  85. try {
  86. mappedReadMethod = findMethod(beanClass, "get" + base, 1,
  87. stringClassArray);
  88. Class params[] = { String.class, mappedReadMethod.getReturnType() };
  89. mappedWriteMethod = findMethod(beanClass, "set" + base, 2, params);
  90. } catch (IntrospectionException e) {
  91. ;
  92. }
  93. // If there's no read method, then look for just a write method
  94. if (mappedReadMethod == null) {
  95. mappedWriteMethod = findMethod(beanClass, "set" + base, 2);
  96. }
  97. if ((mappedReadMethod == null) && (mappedWriteMethod == null)) {
  98. throw new IntrospectionException("Property '" + propertyName +
  99. "' not found on " +
  100. beanClass.getName());
  101. }
  102. findMappedPropertyType();
  103. }
  104. /**
  105. * This constructor takes the name of a mapped property, and method
  106. * names for reading and writing the property.
  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. * @param mappedGetterName The name of the method used for
  112. * reading one of the property values. May be null if the
  113. * property is write-only.
  114. * @param mappedSetterName The name of the method used for writing
  115. * one of the property values. May be null if the property is
  116. * read-only.
  117. *
  118. * @exception IntrospectionException if an exception occurs during
  119. * introspection.
  120. */
  121. public MappedPropertyDescriptor(String propertyName, Class beanClass,
  122. String mappedGetterName, String mappedSetterName)
  123. throws IntrospectionException {
  124. super(propertyName, null, null);
  125. if (propertyName == null || propertyName.length() == 0) {
  126. throw new IntrospectionException("bad property name: " +
  127. propertyName);
  128. }
  129. setName(propertyName);
  130. // search the mapped get and set methods
  131. mappedReadMethod =
  132. findMethod(beanClass, mappedGetterName, 1, stringClassArray);
  133. if (mappedReadMethod != null) {
  134. Class params[] = { String.class, mappedReadMethod.getReturnType() };
  135. mappedWriteMethod =
  136. findMethod(beanClass, mappedSetterName, 2, params);
  137. } else {
  138. mappedWriteMethod =
  139. findMethod(beanClass, mappedSetterName, 2);
  140. }
  141. findMappedPropertyType();
  142. }
  143. /**
  144. * This constructor takes the name of a mapped property, and Method
  145. * objects for reading and writing the property.
  146. *
  147. * @param propertyName The programmatic name of the property.
  148. * @param mappedGetter The method used for reading one of
  149. * the property values. May be be null if the property
  150. * is write-only.
  151. * @param mappedSetter The method used for writing one the
  152. * property values. May be null if the property is read-only.
  153. *
  154. * @exception IntrospectionException if an exception occurs during
  155. * introspection.
  156. */
  157. public MappedPropertyDescriptor(String propertyName,
  158. Method mappedGetter, Method mappedSetter)
  159. throws IntrospectionException {
  160. super(propertyName, mappedGetter, mappedSetter);
  161. if (propertyName == null || propertyName.length() == 0) {
  162. throw new IntrospectionException("bad property name: " +
  163. propertyName);
  164. }
  165. setName(propertyName);
  166. mappedReadMethod = mappedGetter;
  167. mappedWriteMethod = mappedSetter;
  168. findMappedPropertyType();
  169. }
  170. // -------------------------------------------------------- Public Methods
  171. /**
  172. * Gets the Class object for the property values.
  173. *
  174. * @return The Java type info for the property values. Note that
  175. * the "Class" object may describe a built-in Java type such as "int".
  176. * The result may be "null" if this is a mapped property that
  177. * does not support non-keyed access.
  178. * <p>
  179. * This is the type that will be returned by the mappedReadMethod.
  180. */
  181. public Class getMappedPropertyType() {
  182. return mappedPropertyType;
  183. }
  184. /**
  185. * Gets the method that should be used to read one of the property value.
  186. *
  187. * @return The method that should be used to read the property value.
  188. * May return null if the property can't be read.
  189. */
  190. public Method getMappedReadMethod() {
  191. return mappedReadMethod;
  192. }
  193. /**
  194. * Sets the method that should be used to read one of the property value.
  195. *
  196. * @param mappedGetter The new getter method.
  197. */
  198. public void setMappedReadMethod(Method mappedGetter)
  199. throws IntrospectionException {
  200. mappedReadMethod = mappedGetter;
  201. findMappedPropertyType();
  202. }
  203. /**
  204. * Gets the method that should be used to write one of the property value.
  205. *
  206. * @return The method that should be used to write one of the property value.
  207. * May return null if the property can't be written.
  208. */
  209. public Method getMappedWriteMethod() {
  210. return mappedWriteMethod;
  211. }
  212. /**
  213. * Sets the method that should be used to write the property value.
  214. *
  215. * @param mappedSetter The new setter method.
  216. */
  217. public void setMappedWriteMethod(Method mappedSetter)
  218. throws IntrospectionException {
  219. mappedWriteMethod = mappedSetter;
  220. findMappedPropertyType();
  221. }
  222. // ------------------------------------------------------- Private Methods
  223. /**
  224. * Introspect our bean class to identify the corresponding getter
  225. * and setter methods.
  226. */
  227. private void findMappedPropertyType() throws IntrospectionException {
  228. try {
  229. mappedPropertyType = null;
  230. if (mappedReadMethod != null) {
  231. if (mappedReadMethod.getParameterTypes().length != 1) {
  232. throw new IntrospectionException
  233. ("bad mapped read method arg count");
  234. }
  235. mappedPropertyType = mappedReadMethod.getReturnType();
  236. if (mappedPropertyType == Void.TYPE) {
  237. throw new IntrospectionException
  238. ("mapped read method " +
  239. mappedReadMethod.getName() + " returns void");
  240. }
  241. }
  242. if (mappedWriteMethod != null) {
  243. Class params[] = mappedWriteMethod.getParameterTypes();
  244. if (params.length != 2) {
  245. throw new IntrospectionException
  246. ("bad mapped write method arg count");
  247. }
  248. if (mappedPropertyType != null &&
  249. mappedPropertyType != params[1]) {
  250. throw new IntrospectionException
  251. ("type mismatch between mapped read and write methods");
  252. }
  253. mappedPropertyType = params[1];
  254. }
  255. } catch (IntrospectionException ex) {
  256. throw ex;
  257. }
  258. }
  259. /**
  260. * Return a capitalized version of the specified property name.
  261. *
  262. * @param s The property name
  263. */
  264. private static String capitalizePropertyName(String s) {
  265. if (s.length() == 0) {
  266. return s;
  267. }
  268. char chars[] = s.toCharArray();
  269. chars[0] = Character.toUpperCase(chars[0]);
  270. return new String(chars);
  271. }
  272. //======================================================================
  273. // Package private support methods (copied from java.beans.Introspector).
  274. //======================================================================
  275. // Cache of Class.getDeclaredMethods:
  276. private static java.util.Hashtable
  277. declaredMethodCache = new java.util.Hashtable();
  278. /*
  279. * Internal method to return *public* methods within a class.
  280. */
  281. private static synchronized Method[] getPublicDeclaredMethods(Class clz) {
  282. // Looking up Class.getDeclaredMethods is relatively expensive,
  283. // so we cache the results.
  284. final Class fclz = clz;
  285. Method[] result = (Method[]) declaredMethodCache.get(fclz);
  286. if (result != null) {
  287. return result;
  288. }
  289. // We have to raise privilege for getDeclaredMethods
  290. result = (Method[])
  291. AccessController.doPrivileged(new PrivilegedAction() {
  292. public Object run() {
  293. try{
  294. return fclz.getDeclaredMethods();
  295. } catch (SecurityException ex) {
  296. // this means we're in a limited security environment
  297. // so let's try going through the public methods
  298. // and null those those that are not from the declaring
  299. // class
  300. Method[] methods = fclz.getMethods();
  301. for (int i = 0, size = methods.length; i < size; i++) {
  302. Method method = methods[i];
  303. if (!(fclz.equals(method.getDeclaringClass()))) {
  304. methods[i] = null;
  305. }
  306. }
  307. return methods;
  308. }
  309. }
  310. });
  311. // Null out any non-public methods.
  312. for (int i = 0; i < result.length; i++) {
  313. Method method = result[i];
  314. if (method != null) {
  315. int mods = method.getModifiers();
  316. if (!Modifier.isPublic(mods)) {
  317. result[i] = null;
  318. }
  319. }
  320. }
  321. // Add it to the cache.
  322. declaredMethodCache.put(clz, result);
  323. return result;
  324. }
  325. /**
  326. * Internal support for finding a target methodName on a given class.
  327. */
  328. private static Method internalFindMethod(Class start, String methodName,
  329. int argCount) {
  330. // For overridden methods we need to find the most derived version.
  331. // So we start with the given class and walk up the superclass chain.
  332. for (Class cl = start; cl != null; cl = cl.getSuperclass()) {
  333. Method methods[] = getPublicDeclaredMethods(cl);
  334. for (int i = 0; i < methods.length; i++) {
  335. Method method = methods[i];
  336. if (method == null) {
  337. continue;
  338. }
  339. // skip static methods.
  340. int mods = method.getModifiers();
  341. if (Modifier.isStatic(mods)) {
  342. continue;
  343. }
  344. if (method.getName().equals(methodName) &&
  345. method.getParameterTypes().length == argCount) {
  346. return method;
  347. }
  348. }
  349. }
  350. // Now check any inherited interfaces. This is necessary both when
  351. // the argument class is itself an interface, and when the argument
  352. // class is an abstract class.
  353. Class ifcs[] = start.getInterfaces();
  354. for (int i = 0; i < ifcs.length; i++) {
  355. Method m = internalFindMethod(ifcs[i], methodName, argCount);
  356. if (m != null) {
  357. return m;
  358. }
  359. }
  360. return null;
  361. }
  362. /**
  363. * Internal support for finding a target methodName with a given
  364. * parameter list on a given class.
  365. */
  366. private static Method internalFindMethod(Class start, String methodName,
  367. int argCount, Class args[]) {
  368. // For overriden methods we need to find the most derived version.
  369. // So we start with the given class and walk up the superclass chain.
  370. for (Class cl = start; cl != null; cl = cl.getSuperclass()) {
  371. Method methods[] = getPublicDeclaredMethods(cl);
  372. for (int i = 0; i < methods.length; i++) {
  373. Method method = methods[i];
  374. if (method == null) {
  375. continue;
  376. }
  377. // skip static methods.
  378. int mods = method.getModifiers();
  379. if (Modifier.isStatic(mods)) {
  380. continue;
  381. }
  382. // make sure method signature matches.
  383. Class params[] = method.getParameterTypes();
  384. if (method.getName().equals(methodName) &&
  385. params.length == argCount) {
  386. boolean different = false;
  387. if (argCount > 0) {
  388. for (int j = 0; j < argCount; j++) {
  389. if (params[j] != args[j]) {
  390. different = true;
  391. continue;
  392. }
  393. }
  394. if (different) {
  395. continue;
  396. }
  397. }
  398. return method;
  399. }
  400. }
  401. }
  402. // Now check any inherited interfaces. This is necessary both when
  403. // the argument class is itself an interface, and when the argument
  404. // class is an abstract class.
  405. Class ifcs[] = start.getInterfaces();
  406. for (int i = 0; i < ifcs.length; i++) {
  407. Method m = internalFindMethod(ifcs[i], methodName, argCount);
  408. if (m != null) {
  409. return m;
  410. }
  411. }
  412. return null;
  413. }
  414. /**
  415. * Find a target methodName on a given class.
  416. */
  417. static Method findMethod(Class cls, String methodName, int argCount)
  418. throws IntrospectionException {
  419. if (methodName == null) {
  420. return null;
  421. }
  422. Method m = internalFindMethod(cls, methodName, argCount);
  423. if (m != null) {
  424. return m;
  425. }
  426. // We failed to find a suitable method
  427. throw new IntrospectionException("No method \"" + methodName +
  428. "\" with " + argCount + " arg(s)");
  429. }
  430. /**
  431. * Find a target methodName with specific parameter list on a given class.
  432. */
  433. static Method findMethod(Class cls, String methodName, int argCount,
  434. Class args[]) throws IntrospectionException {
  435. if (methodName == null) {
  436. return null;
  437. }
  438. Method m = internalFindMethod(cls, methodName, argCount, args);
  439. if (m != null) {
  440. return m;
  441. }
  442. // We failed to find a suitable method
  443. throw new IntrospectionException("No method \"" + methodName +
  444. "\" with " + argCount + " arg(s) of matching types.");
  445. }
  446. /**
  447. * Return true if class a is either equivalent to class b, or
  448. * if class a is a subclass of class b, ie if a either "extends"
  449. * or "implements" b.
  450. * Note tht either or both "Class" objects may represent interfaces.
  451. */
  452. static boolean isSubclass(Class a, Class b) {
  453. // We rely on the fact that for any given java class or
  454. // primtitive type there is a unqiue Class object, so
  455. // we can use object equivalence in the comparisons.
  456. if (a == b) {
  457. return true;
  458. }
  459. if (a == null || b == null) {
  460. return false;
  461. }
  462. for (Class x = a; x != null; x = x.getSuperclass()) {
  463. if (x == b) {
  464. return true;
  465. }
  466. if (b.isInterface()) {
  467. Class interfaces[] = x.getInterfaces();
  468. for (int i = 0; i < interfaces.length; i++) {
  469. if (isSubclass(interfaces[i], b)) {
  470. return true;
  471. }
  472. }
  473. }
  474. }
  475. return false;
  476. }
  477. /**
  478. * Return true iff the given method throws the given exception.
  479. */
  480. private boolean throwsException(Method method, Class exception) {
  481. Class exs[] = method.getExceptionTypes();
  482. for (int i = 0; i < exs.length; i++) {
  483. if (exs[i] == exception) {
  484. return true;
  485. }
  486. }
  487. return false;
  488. }
  489. }