1. /*
  2. * $Header: /home/cvs/jakarta-commons/dbutils/src/java/org/apache/commons/dbutils/BasicRowProcessor.java,v 1.5 2003/11/11 00:53:19 dgraham Exp $
  3. * $Revision: 1.5 $
  4. * $Date: 2003/11/11 00:53:19 $
  5. *
  6. * ====================================================================
  7. *
  8. * The Apache Software License, Version 1.1
  9. *
  10. * Copyright (c) 2002-2003 The Apache Software Foundation. All rights
  11. * reserved.
  12. *
  13. * Redistribution and use in source and binary forms, with or without
  14. * modification, are permitted provided that the following conditions
  15. * are met:
  16. *
  17. * 1. Redistributions of source code must retain the above copyright
  18. * notice, this list of conditions and the following disclaimer.
  19. *
  20. * 2. Redistributions in binary form must reproduce the above copyright
  21. * notice, this list of conditions and the following disclaimer in
  22. * the documentation and/or other materials provided with the
  23. * distribution.
  24. *
  25. * 3. The end-user documentation included with the redistribution, if
  26. * any, must include the following acknowledgement:
  27. * "This product includes software developed by the
  28. * Apache Software Foundation (http://www.apache.org/)."
  29. * Alternately, this acknowledgement may appear in the software itself,
  30. * if and wherever such third-party acknowledgements normally appear.
  31. *
  32. * 4. The names "The Jakarta Project", "Commons", and "Apache Software
  33. * Foundation" must not be used to endorse or promote products derived
  34. * from this software without prior written permission. For written
  35. * permission, please contact apache@apache.org.
  36. *
  37. * 5. Products derived from this software may not be called "Apache"
  38. * nor may "Apache" appear in their names without prior written
  39. * permission of the Apache Software Foundation.
  40. *
  41. * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
  42. * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
  43. * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  44. * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
  45. * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  46. * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  47. * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
  48. * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  49. * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
  50. * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
  51. * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
  52. * SUCH DAMAGE.
  53. * ====================================================================
  54. *
  55. * This software consists of voluntary contributions made by many
  56. * individuals on behalf of the Apache Software Foundation. For more
  57. * information on the Apache Software Foundation, please see
  58. * <http://www.apache.org/>.
  59. *
  60. */
  61. package org.apache.commons.dbutils;
  62. import java.beans.BeanInfo;
  63. import java.beans.IntrospectionException;
  64. import java.beans.Introspector;
  65. import java.beans.PropertyDescriptor;
  66. import java.lang.reflect.InvocationTargetException;
  67. import java.lang.reflect.Method;
  68. import java.sql.ResultSet;
  69. import java.sql.ResultSetMetaData;
  70. import java.sql.SQLException;
  71. import java.util.ArrayList;
  72. import java.util.HashMap;
  73. import java.util.Iterator;
  74. import java.util.List;
  75. import java.util.Map;
  76. /**
  77. * Basic implementation of the <code>RowProcessor</code> interface.
  78. * This class is a thread-safe Singleton.
  79. *
  80. * @see RowProcessor
  81. *
  82. * @author Henri Yandell
  83. * @author Juozas Baliuka
  84. * @author David Graham
  85. * @author Yoav Shapira
  86. */
  87. public class BasicRowProcessor implements RowProcessor {
  88. /**
  89. * Set a bean's primitive properties to these defaults when SQL NULL
  90. * is returned. These are the same as the defaults that ResultSet get*
  91. * methods return in the event of a NULL column.
  92. */
  93. private static final Map primitiveDefaults = new HashMap();
  94. static {
  95. primitiveDefaults.put(Integer.TYPE, new Integer(0));
  96. primitiveDefaults.put(Short.TYPE, new Short((short) 0));
  97. primitiveDefaults.put(Byte.TYPE, new Byte((byte) 0));
  98. primitiveDefaults.put(Float.TYPE, new Float(0));
  99. primitiveDefaults.put(Double.TYPE, new Double(0));
  100. primitiveDefaults.put(Long.TYPE, new Long(0));
  101. primitiveDefaults.put(Boolean.TYPE, Boolean.FALSE);
  102. primitiveDefaults.put(Character.TYPE, new Character('\u0000'));
  103. }
  104. /**
  105. * Special array index that indicates there is no bean property that
  106. * matches a column from a ResultSet.
  107. */
  108. private static final int PROPERTY_NOT_FOUND = -1;
  109. /**
  110. * The Singleton instance of this class.
  111. */
  112. private static final BasicRowProcessor instance = new BasicRowProcessor();
  113. /**
  114. * Returns the Singleton instance of this class.
  115. *
  116. * @return The single instance of this class.
  117. */
  118. public static BasicRowProcessor instance() {
  119. return instance;
  120. }
  121. /**
  122. * Protected constructor for BasicRowProcessor subclasses only.
  123. */
  124. protected BasicRowProcessor() {
  125. super();
  126. }
  127. /**
  128. * Convert a <code>ResultSet</code> row into an <code>Object[]</code>.
  129. * This implementation copies column values into the array in the same
  130. * order they're returned from the <code>ResultSet</code>. Array elements
  131. * will be set to <code>null</code> if the column was SQL NULL.
  132. *
  133. * @see org.apache.commons.dbutils.RowProcessor#toArray(java.sql.ResultSet)
  134. */
  135. public Object[] toArray(ResultSet rs) throws SQLException {
  136. ResultSetMetaData meta = rs.getMetaData();
  137. int cols = meta.getColumnCount();
  138. Object[] result = new Object[cols];
  139. for (int i = 0; i < cols; i++) {
  140. result[i] = rs.getObject(i + 1);
  141. }
  142. return result;
  143. }
  144. /**
  145. * Convert a <code>ResultSet</code> row into a JavaBean. This
  146. * implementation uses reflection and <code>BeanInfo</code> classes to
  147. * match column names to bean property names. Properties are matched to
  148. * columns based on several factors:
  149. * <br/>
  150. * <ol>
  151. * <li>
  152. * The class has a writable property with the same name as a column.
  153. * The name comparison is case insensitive.
  154. * </li>
  155. *
  156. * <li>
  157. * The property's set method parameter type matches the column
  158. * type. If the data types do not match, the setter will not be called.
  159. * </li>
  160. * </ol>
  161. *
  162. * <p>
  163. * Primitive bean properties are set to their defaults when SQL NULL is
  164. * returned from the <code>ResultSet</code>. Numeric fields are set to 0
  165. * and booleans are set to false. Object bean properties are set to
  166. * <code>null</code> when SQL NULL is returned. This is the same behavior
  167. * as the <code>ResultSet</code> get* methods.
  168. * </p>
  169. *
  170. * @see org.apache.commons.dbutils.RowProcessor#toBean(java.sql.ResultSet, java.lang.Class)
  171. */
  172. public Object toBean(ResultSet rs, Class type) throws SQLException {
  173. PropertyDescriptor[] props = this.propertyDescriptors(type);
  174. ResultSetMetaData rsmd = rs.getMetaData();
  175. int[] columnToProperty = this.mapColumnsToProperties(rsmd, props);
  176. int cols = rsmd.getColumnCount();
  177. return this.createBean(rs, type, props, columnToProperty, cols);
  178. }
  179. /**
  180. * Convert a <code>ResultSet</code> into a <code>List</code> of JavaBeans.
  181. * This implementation uses reflection and <code>BeanInfo</code> classes to
  182. * match column names to bean property names. Properties are matched to
  183. * columns based on several factors:
  184. * <br/>
  185. * <ol>
  186. * <li>
  187. * The class has a writable property with the same name as a column.
  188. * The name comparison is case insensitive.
  189. * </li>
  190. *
  191. * <li>
  192. * The property's set method parameter type matches the column
  193. * type. If the data types do not match, the setter will not be called.
  194. * </li>
  195. * </ol>
  196. *
  197. * <p>
  198. * Primitive bean properties are set to their defaults when SQL NULL is
  199. * returned from the <code>ResultSet</code>. Numeric fields are set to 0
  200. * and booleans are set to false. Object bean properties are set to
  201. * <code>null</code> when SQL NULL is returned. This is the same behavior
  202. * as the <code>ResultSet</code> get* methods.
  203. * </p>
  204. *
  205. * @see org.apache.commons.dbutils.RowProcessor#toBeanList(java.sql.ResultSet, java.lang.Class)
  206. */
  207. public List toBeanList(ResultSet rs, Class type) throws SQLException {
  208. List results = new ArrayList();
  209. if (!rs.next()) {
  210. return results;
  211. }
  212. PropertyDescriptor[] props = this.propertyDescriptors(type);
  213. ResultSetMetaData rsmd = rs.getMetaData();
  214. int[] columnToProperty = this.mapColumnsToProperties(rsmd, props);
  215. int cols = rsmd.getColumnCount();
  216. do {
  217. results.add(this.createBean(rs, type, props, columnToProperty, cols));
  218. } while (rs.next());
  219. return results;
  220. }
  221. /**
  222. * Creates a new object and initializes its fields from the ResultSet.
  223. *
  224. * @param rs The result set
  225. * @param type The bean type (the return type of the object)
  226. * @param props The property descriptors
  227. * @param columnToProperty The column indices in the result set
  228. * @param cols The number of columns
  229. * @return An initialized object.
  230. * @throws SQLException If a database error occurs
  231. */
  232. private Object createBean(
  233. ResultSet rs,
  234. Class type,
  235. PropertyDescriptor[] props,
  236. int[] columnToProperty,
  237. int cols)
  238. throws SQLException {
  239. Object bean = this.newInstance(type);
  240. for (int i = 1; i <= cols; i++) {
  241. if (columnToProperty[i] == PROPERTY_NOT_FOUND) {
  242. continue;
  243. }
  244. Object value = rs.getObject(i);
  245. PropertyDescriptor prop = props[columnToProperty[i]];
  246. Class propType = prop.getPropertyType();
  247. if (propType != null && value == null && propType.isPrimitive()) {
  248. value = primitiveDefaults.get(propType);
  249. }
  250. this.callSetter(bean, prop, value);
  251. }
  252. return bean;
  253. }
  254. /**
  255. * The positions in the returned array represent column numbers. The values
  256. * stored at each position represent the index in the PropertyDescriptor[]
  257. * for the bean property that matches the column name. If no bean property
  258. * was found for a column, the position is set to PROPERTY_NOT_FOUND.
  259. *
  260. * @param rsmd The result set meta data containing column information
  261. * @param props The bean property descriptors
  262. * @return An int[] with column index to property index mappings. The 0th
  263. * element is meaningless as column indexing starts at 1.
  264. *
  265. * @throws SQLException If a database error occurs
  266. */
  267. private int[] mapColumnsToProperties(
  268. ResultSetMetaData rsmd,
  269. PropertyDescriptor[] props)
  270. throws SQLException {
  271. int cols = rsmd.getColumnCount();
  272. int columnToProperty[] = new int[cols + 1];
  273. for (int col = 1; col <= cols; col++) {
  274. String columnName = rsmd.getColumnName(col);
  275. for (int i = 0; i < props.length; i++) {
  276. if (columnName.equalsIgnoreCase(props[i].getName())) {
  277. columnToProperty[col] = i;
  278. break;
  279. } else {
  280. columnToProperty[col] = PROPERTY_NOT_FOUND;
  281. }
  282. }
  283. }
  284. return columnToProperty;
  285. }
  286. /**
  287. * Convert a <code>ResultSet</code> row into a <code>Map</code>. This
  288. * implementation returns a <code>Map</code> with case insensitive column
  289. * names as keys. Calls to <code>map.get("COL")</code> and
  290. * <code>map.get("col")</code> return the same value.
  291. * @see org.apache.commons.dbutils.RowProcessor#toMap(java.sql.ResultSet)
  292. */
  293. public Map toMap(ResultSet rs) throws SQLException {
  294. Map result = new CaseInsensitiveHashMap();
  295. ResultSetMetaData rsmd = rs.getMetaData();
  296. int cols = rsmd.getColumnCount();
  297. for (int i = 1; i <= cols; i++) {
  298. result.put(rsmd.getColumnName(i), rs.getObject(i));
  299. }
  300. return result;
  301. }
  302. /**
  303. * Calls the setter method on the target object for the given property.
  304. * If no setter method exists for the property, this method does nothing.
  305. * @param target The object to set the property on.
  306. * @param prop The property to set.
  307. * @param value The value to pass into the setter.
  308. * @throws SQLException if an error occurs setting the property.
  309. */
  310. private void callSetter(
  311. Object target,
  312. PropertyDescriptor prop,
  313. Object value)
  314. throws SQLException {
  315. Method setter = prop.getWriteMethod();
  316. if (setter == null) {
  317. return;
  318. }
  319. Class[] params = setter.getParameterTypes();
  320. try {
  321. // Don't call setter if the value object isn't the right type
  322. if (this.isCompatibleType(value, params[0])) {
  323. setter.invoke(target, new Object[] { value });
  324. }
  325. } catch (IllegalArgumentException e) {
  326. throw new SQLException(
  327. "Cannot set " + prop.getName() + ": " + e.getMessage());
  328. } catch (IllegalAccessException e) {
  329. throw new SQLException(
  330. "Cannot set " + prop.getName() + ": " + e.getMessage());
  331. } catch (InvocationTargetException e) {
  332. throw new SQLException(
  333. "Cannot set " + prop.getName() + ": " + e.getMessage());
  334. }
  335. }
  336. /**
  337. * ResultSet.getObject() returns an Integer object for an INT column. The
  338. * setter method for the property might take an Integer or a primitive int.
  339. * This method returns true if the value can be successfully passed into
  340. * the setter method. Remember, Method.invoke() handles the unwrapping
  341. * of Integer into an int.
  342. *
  343. * @param value The value to be passed into the setter method.
  344. * @param type The setter's parameter type.
  345. * @return boolean True if the value is compatible.
  346. */
  347. private boolean isCompatibleType(Object value, Class type) {
  348. // Do object check first, then primitives
  349. if (value == null || type.isInstance(value)) {
  350. return true;
  351. } else if (
  352. type.equals(Integer.TYPE) && Integer.class.isInstance(value)) {
  353. return true;
  354. } else if (type.equals(Long.TYPE) && Long.class.isInstance(value)) {
  355. return true;
  356. } else if (
  357. type.equals(Double.TYPE) && Double.class.isInstance(value)) {
  358. return true;
  359. } else if (type.equals(Float.TYPE) && Float.class.isInstance(value)) {
  360. return true;
  361. } else if (type.equals(Short.TYPE) && Short.class.isInstance(value)) {
  362. return true;
  363. } else if (type.equals(Byte.TYPE) && Byte.class.isInstance(value)) {
  364. return true;
  365. } else if (
  366. type.equals(Character.TYPE) && Character.class.isInstance(value)) {
  367. return true;
  368. } else if (
  369. type.equals(Boolean.TYPE) && Boolean.class.isInstance(value)) {
  370. return true;
  371. } else {
  372. return false;
  373. }
  374. }
  375. /**
  376. * Returns a new instance of the given Class.
  377. *
  378. * @param c The Class to create an object from.
  379. * @return A newly created object of the Class.
  380. * @throws SQLException if creation failed.
  381. */
  382. private Object newInstance(Class c) throws SQLException {
  383. try {
  384. return c.newInstance();
  385. } catch (InstantiationException e) {
  386. throw new SQLException(
  387. "Cannot create " + c.getName() + ": " + e.getMessage());
  388. } catch (IllegalAccessException e) {
  389. throw new SQLException(
  390. "Cannot create " + c.getName() + ": " + e.getMessage());
  391. }
  392. }
  393. /**
  394. * Returns a PropertyDescriptor[] for the given Class.
  395. *
  396. * @param c The Class to retrieve PropertyDescriptors for.
  397. * @return A PropertyDescriptor[] describing the Class.
  398. * @throws SQLException if introspection failed.
  399. */
  400. private PropertyDescriptor[] propertyDescriptors(Class c)
  401. throws SQLException {
  402. // Introspector caches BeanInfo classes for better performance
  403. BeanInfo beanInfo = null;
  404. try {
  405. beanInfo = Introspector.getBeanInfo(c);
  406. } catch (IntrospectionException e) {
  407. throw new SQLException(
  408. "Bean introspection failed: " + e.getMessage());
  409. }
  410. return beanInfo.getPropertyDescriptors();
  411. }
  412. /**
  413. * A Map that converts all keys to lowercase Strings for case insensitive
  414. * lookups. This is needed for the toMap() implementation because
  415. * databases don't consistenly handle the casing of column names.
  416. */
  417. private static class CaseInsensitiveHashMap extends HashMap {
  418. /**
  419. * @see java.util.Map#containsKey(java.lang.Object)
  420. */
  421. public boolean containsKey(Object key) {
  422. return super.containsKey(key.toString().toLowerCase());
  423. }
  424. /**
  425. * @see java.util.Map#get(java.lang.Object)
  426. */
  427. public Object get(Object key) {
  428. return super.get(key.toString().toLowerCase());
  429. }
  430. /**
  431. * @see java.util.Map#put(java.lang.Object, java.lang.Object)
  432. */
  433. public Object put(Object key, Object value) {
  434. return super.put(key.toString().toLowerCase(), value);
  435. }
  436. /**
  437. * @see java.util.Map#putAll(java.util.Map)
  438. */
  439. public void putAll(Map m) {
  440. Iterator iter = m.keySet().iterator();
  441. while (iter.hasNext()) {
  442. Object key = iter.next();
  443. Object value = m.get(key);
  444. this.put(key, value);
  445. }
  446. }
  447. /**
  448. * @see java.util.Map#remove(java.lang.ObjecT)
  449. */
  450. public Object remove(Object key) {
  451. return super.remove(key.toString().toLowerCase());
  452. }
  453. }
  454. }