Classes in this File | Line Coverage | Branch Coverage | Complexity | ||||
BeanProcessor |
|
| 9.8;9.8 |
1 | /* | |
2 | * Licensed to the Apache Software Foundation (ASF) under one or more | |
3 | * contributor license agreements. See the NOTICE file distributed with | |
4 | * this work for additional information regarding copyright ownership. | |
5 | * The ASF licenses this file to You under the Apache License, Version 2.0 | |
6 | * (the "License"); you may not use this file except in compliance with | |
7 | * the License. You may obtain a copy of the License at | |
8 | * | |
9 | * http://www.apache.org/licenses/LICENSE-2.0 | |
10 | * | |
11 | * Unless required by applicable law or agreed to in writing, software | |
12 | * distributed under the License is distributed on an "AS IS" BASIS, | |
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
14 | * See the License for the specific language governing permissions and | |
15 | * limitations under the License. | |
16 | */ | |
17 | package org.apache.commons.dbutils; | |
18 | ||
19 | import java.beans.BeanInfo; | |
20 | import java.beans.IntrospectionException; | |
21 | import java.beans.Introspector; | |
22 | import java.beans.PropertyDescriptor; | |
23 | import java.lang.reflect.InvocationTargetException; | |
24 | import java.lang.reflect.Method; | |
25 | import java.sql.ResultSet; | |
26 | import java.sql.ResultSetMetaData; | |
27 | import java.sql.SQLException; | |
28 | import java.sql.Timestamp; | |
29 | import java.util.ArrayList; | |
30 | import java.util.Arrays; | |
31 | import java.util.HashMap; | |
32 | import java.util.List; | |
33 | import java.util.Map; | |
34 | ||
35 | /** | |
36 | * <p> | |
37 | * <code>BeanProcessor</code> matches column names to bean property names | |
38 | * and converts <code>ResultSet</code> columns into objects for those bean | |
39 | * properties. Subclasses should override the methods in the processing chain | |
40 | * to customize behavior. | |
41 | * </p> | |
42 | * | |
43 | * <p> | |
44 | * This class is thread-safe. | |
45 | * </p> | |
46 | * | |
47 | * @see BasicRowProcessor | |
48 | * | |
49 | * @since DbUtils 1.1 | |
50 | */ | |
51 | public class BeanProcessor { | |
52 | ||
53 | /** | |
54 | * Special array value used by <code>mapColumnsToProperties</code> that | |
55 | * indicates there is no bean property that matches a column from a | |
56 | * <code>ResultSet</code>. | |
57 | */ | |
58 | protected static final int PROPERTY_NOT_FOUND = -1; | |
59 | ||
60 | /** | |
61 | * Set a bean's primitive properties to these defaults when SQL NULL | |
62 | * is returned. These are the same as the defaults that ResultSet get* | |
63 | * methods return in the event of a NULL column. | |
64 | */ | |
65 | 0 | private static final Map primitiveDefaults = new HashMap(); |
66 | ||
67 | static { | |
68 | 0 | primitiveDefaults.put(Integer.TYPE, new Integer(0)); |
69 | 0 | primitiveDefaults.put(Short.TYPE, new Short((short) 0)); |
70 | 0 | primitiveDefaults.put(Byte.TYPE, new Byte((byte) 0)); |
71 | 0 | primitiveDefaults.put(Float.TYPE, new Float(0)); |
72 | 0 | primitiveDefaults.put(Double.TYPE, new Double(0)); |
73 | 0 | primitiveDefaults.put(Long.TYPE, new Long(0)); |
74 | 0 | primitiveDefaults.put(Boolean.TYPE, Boolean.FALSE); |
75 | 0 | primitiveDefaults.put(Character.TYPE, new Character('\u0000')); |
76 | 0 | } |
77 | ||
78 | /** | |
79 | * Constructor for BeanProcessor. | |
80 | */ | |
81 | public BeanProcessor() { | |
82 | 0 | super(); |
83 | 0 | } |
84 | ||
85 | /** | |
86 | * Convert a <code>ResultSet</code> row into a JavaBean. This | |
87 | * implementation uses reflection and <code>BeanInfo</code> classes to | |
88 | * match column names to bean property names. Properties are matched to | |
89 | * columns based on several factors: | |
90 | * <br/> | |
91 | * <ol> | |
92 | * <li> | |
93 | * The class has a writable property with the same name as a column. | |
94 | * The name comparison is case insensitive. | |
95 | * </li> | |
96 | * | |
97 | * <li> | |
98 | * The column type can be converted to the property's set method | |
99 | * parameter type with a ResultSet.get* method. If the conversion fails | |
100 | * (ie. the property was an int and the column was a Timestamp) an | |
101 | * SQLException is thrown. | |
102 | * </li> | |
103 | * </ol> | |
104 | * | |
105 | * <p> | |
106 | * Primitive bean properties are set to their defaults when SQL NULL is | |
107 | * returned from the <code>ResultSet</code>. Numeric fields are set to 0 | |
108 | * and booleans are set to false. Object bean properties are set to | |
109 | * <code>null</code> when SQL NULL is returned. This is the same behavior | |
110 | * as the <code>ResultSet</code> get* methods. | |
111 | * </p> | |
112 | * | |
113 | * @param rs ResultSet that supplies the bean data | |
114 | * @param type Class from which to create the bean instance | |
115 | * @throws SQLException if a database access error occurs | |
116 | * @return the newly created bean | |
117 | */ | |
118 | public Object toBean(ResultSet rs, Class type) throws SQLException { | |
119 | ||
120 | 0 | PropertyDescriptor[] props = this.propertyDescriptors(type); |
121 | ||
122 | 0 | ResultSetMetaData rsmd = rs.getMetaData(); |
123 | 0 | int[] columnToProperty = this.mapColumnsToProperties(rsmd, props); |
124 | ||
125 | 0 | return this.createBean(rs, type, props, columnToProperty); |
126 | } | |
127 | ||
128 | /** | |
129 | * Convert a <code>ResultSet</code> into a <code>List</code> of JavaBeans. | |
130 | * This implementation uses reflection and <code>BeanInfo</code> classes to | |
131 | * match column names to bean property names. Properties are matched to | |
132 | * columns based on several factors: | |
133 | * <br/> | |
134 | * <ol> | |
135 | * <li> | |
136 | * The class has a writable property with the same name as a column. | |
137 | * The name comparison is case insensitive. | |
138 | * </li> | |
139 | * | |
140 | * <li> | |
141 | * The column type can be converted to the property's set method | |
142 | * parameter type with a ResultSet.get* method. If the conversion fails | |
143 | * (ie. the property was an int and the column was a Timestamp) an | |
144 | * SQLException is thrown. | |
145 | * </li> | |
146 | * </ol> | |
147 | * | |
148 | * <p> | |
149 | * Primitive bean properties are set to their defaults when SQL NULL is | |
150 | * returned from the <code>ResultSet</code>. Numeric fields are set to 0 | |
151 | * and booleans are set to false. Object bean properties are set to | |
152 | * <code>null</code> when SQL NULL is returned. This is the same behavior | |
153 | * as the <code>ResultSet</code> get* methods. | |
154 | * </p> | |
155 | * | |
156 | * @param rs ResultSet that supplies the bean data | |
157 | * @param type Class from which to create the bean instance | |
158 | * @throws SQLException if a database access error occurs | |
159 | * @return the newly created List of beans | |
160 | */ | |
161 | public List toBeanList(ResultSet rs, Class type) throws SQLException { | |
162 | 0 | List results = new ArrayList(); |
163 | ||
164 | 0 | if (!rs.next()) { |
165 | 0 | return results; |
166 | } | |
167 | ||
168 | 0 | PropertyDescriptor[] props = this.propertyDescriptors(type); |
169 | 0 | ResultSetMetaData rsmd = rs.getMetaData(); |
170 | 0 | int[] columnToProperty = this.mapColumnsToProperties(rsmd, props); |
171 | ||
172 | do { | |
173 | 0 | results.add(this.createBean(rs, type, props, columnToProperty)); |
174 | 0 | } while (rs.next()); |
175 | ||
176 | 0 | return results; |
177 | } | |
178 | ||
179 | /** | |
180 | * Creates a new object and initializes its fields from the ResultSet. | |
181 | * | |
182 | * @param rs The result set. | |
183 | * @param type The bean type (the return type of the object). | |
184 | * @param props The property descriptors. | |
185 | * @param columnToProperty The column indices in the result set. | |
186 | * @return An initialized object. | |
187 | * @throws SQLException if a database error occurs. | |
188 | */ | |
189 | private Object createBean(ResultSet rs, Class type, | |
190 | PropertyDescriptor[] props, int[] columnToProperty) | |
191 | throws SQLException { | |
192 | ||
193 | 0 | Object bean = this.newInstance(type); |
194 | ||
195 | 0 | for (int i = 1; i < columnToProperty.length; i++) { |
196 | ||
197 | 0 | if (columnToProperty[i] == PROPERTY_NOT_FOUND) { |
198 | 0 | continue; |
199 | } | |
200 | ||
201 | 0 | PropertyDescriptor prop = props[columnToProperty[i]]; |
202 | 0 | Class propType = prop.getPropertyType(); |
203 | ||
204 | 0 | Object value = this.processColumn(rs, i, propType); |
205 | ||
206 | 0 | if (propType != null && value == null && propType.isPrimitive()) { |
207 | 0 | value = primitiveDefaults.get(propType); |
208 | } | |
209 | ||
210 | 0 | this.callSetter(bean, prop, value); |
211 | } | |
212 | ||
213 | 0 | return bean; |
214 | } | |
215 | ||
216 | /** | |
217 | * Calls the setter method on the target object for the given property. | |
218 | * If no setter method exists for the property, this method does nothing. | |
219 | * @param target The object to set the property on. | |
220 | * @param prop The property to set. | |
221 | * @param value The value to pass into the setter. | |
222 | * @throws SQLException if an error occurs setting the property. | |
223 | */ | |
224 | private void callSetter(Object target, PropertyDescriptor prop, Object value) | |
225 | throws SQLException { | |
226 | ||
227 | 0 | Method setter = prop.getWriteMethod(); |
228 | ||
229 | 0 | if (setter == null) { |
230 | 0 | return; |
231 | } | |
232 | ||
233 | 0 | Class[] params = setter.getParameterTypes(); |
234 | try { | |
235 | // convert types for some popular ones | |
236 | 0 | if (value != null) { |
237 | 0 | if (value instanceof java.util.Date) { |
238 | 0 | if (params[0].getName().equals("java.sql.Date")) { |
239 | 0 | value = new java.sql.Date(((java.util.Date) value).getTime()); |
240 | } else | |
241 | 0 | if (params[0].getName().equals("java.sql.Time")) { |
242 | 0 | value = new java.sql.Time(((java.util.Date) value).getTime()); |
243 | } else | |
244 | 0 | if (params[0].getName().equals("java.sql.Timestamp")) { |
245 | 0 | value = new java.sql.Timestamp(((java.util.Date) value).getTime()); |
246 | } | |
247 | } | |
248 | } | |
249 | ||
250 | // Don't call setter if the value object isn't the right type | |
251 | 0 | if (this.isCompatibleType(value, params[0])) { |
252 | 0 | setter.invoke(target, new Object[] { value }); |
253 | } else { | |
254 | 0 | throw new SQLException( |
255 | "Cannot set " + prop.getName() + ": incompatible types."); | |
256 | } | |
257 | ||
258 | 0 | } catch (IllegalArgumentException e) { |
259 | 0 | throw new SQLException( |
260 | "Cannot set " + prop.getName() + ": " + e.getMessage()); | |
261 | ||
262 | 0 | } catch (IllegalAccessException e) { |
263 | 0 | throw new SQLException( |
264 | "Cannot set " + prop.getName() + ": " + e.getMessage()); | |
265 | ||
266 | 0 | } catch (InvocationTargetException e) { |
267 | 0 | throw new SQLException( |
268 | "Cannot set " + prop.getName() + ": " + e.getMessage()); | |
269 | 0 | } |
270 | 0 | } |
271 | ||
272 | /** | |
273 | * ResultSet.getObject() returns an Integer object for an INT column. The | |
274 | * setter method for the property might take an Integer or a primitive int. | |
275 | * This method returns true if the value can be successfully passed into | |
276 | * the setter method. Remember, Method.invoke() handles the unwrapping | |
277 | * of Integer into an int. | |
278 | * | |
279 | * @param value The value to be passed into the setter method. | |
280 | * @param type The setter's parameter type. | |
281 | * @return boolean True if the value is compatible. | |
282 | */ | |
283 | private boolean isCompatibleType(Object value, Class type) { | |
284 | // Do object check first, then primitives | |
285 | 0 | if (value == null || type.isInstance(value)) { |
286 | 0 | return true; |
287 | ||
288 | 0 | } else if ( |
289 | 0 | type.equals(Integer.TYPE) && Integer.class.isInstance(value)) { |
290 | 0 | return true; |
291 | ||
292 | 0 | } else if (type.equals(Long.TYPE) && Long.class.isInstance(value)) { |
293 | 0 | return true; |
294 | ||
295 | 0 | } else if ( |
296 | type.equals(Double.TYPE) && Double.class.isInstance(value)) { | |
297 | 0 | return true; |
298 | ||
299 | 0 | } else if (type.equals(Float.TYPE) && Float.class.isInstance(value)) { |
300 | 0 | return true; |
301 | ||
302 | 0 | } else if (type.equals(Short.TYPE) && Short.class.isInstance(value)) { |
303 | 0 | return true; |
304 | ||
305 | 0 | } else if (type.equals(Byte.TYPE) && Byte.class.isInstance(value)) { |
306 | 0 | return true; |
307 | ||
308 | 0 | } else if ( |
309 | type.equals(Character.TYPE) && Character.class.isInstance(value)) { | |
310 | 0 | return true; |
311 | ||
312 | 0 | } else if ( |
313 | type.equals(Boolean.TYPE) && Boolean.class.isInstance(value)) { | |
314 | 0 | return true; |
315 | ||
316 | } else { | |
317 | 0 | return false; |
318 | } | |
319 | ||
320 | } | |
321 | ||
322 | /** | |
323 | * Factory method that returns a new instance of the given Class. This | |
324 | * is called at the start of the bean creation process and may be | |
325 | * overridden to provide custom behavior like returning a cached bean | |
326 | * instance. | |
327 | * | |
328 | * @param c The Class to create an object from. | |
329 | * @return A newly created object of the Class. | |
330 | * @throws SQLException if creation failed. | |
331 | */ | |
332 | protected Object newInstance(Class c) throws SQLException { | |
333 | try { | |
334 | 0 | return c.newInstance(); |
335 | ||
336 | 0 | } catch (InstantiationException e) { |
337 | 0 | throw new SQLException( |
338 | "Cannot create " + c.getName() + ": " + e.getMessage()); | |
339 | ||
340 | 0 | } catch (IllegalAccessException e) { |
341 | 0 | throw new SQLException( |
342 | "Cannot create " + c.getName() + ": " + e.getMessage()); | |
343 | } | |
344 | } | |
345 | ||
346 | /** | |
347 | * Returns a PropertyDescriptor[] for the given Class. | |
348 | * | |
349 | * @param c The Class to retrieve PropertyDescriptors for. | |
350 | * @return A PropertyDescriptor[] describing the Class. | |
351 | * @throws SQLException if introspection failed. | |
352 | */ | |
353 | private PropertyDescriptor[] propertyDescriptors(Class c) | |
354 | throws SQLException { | |
355 | // Introspector caches BeanInfo classes for better performance | |
356 | 0 | BeanInfo beanInfo = null; |
357 | try { | |
358 | 0 | beanInfo = Introspector.getBeanInfo(c); |
359 | ||
360 | 0 | } catch (IntrospectionException e) { |
361 | 0 | throw new SQLException( |
362 | "Bean introspection failed: " + e.getMessage()); | |
363 | 0 | } |
364 | ||
365 | 0 | return beanInfo.getPropertyDescriptors(); |
366 | } | |
367 | ||
368 | /** | |
369 | * The positions in the returned array represent column numbers. The | |
370 | * values stored at each position represent the index in the | |
371 | * <code>PropertyDescriptor[]</code> for the bean property that matches | |
372 | * the column name. If no bean property was found for a column, the | |
373 | * position is set to <code>PROPERTY_NOT_FOUND</code>. | |
374 | * | |
375 | * @param rsmd The <code>ResultSetMetaData</code> containing column | |
376 | * information. | |
377 | * | |
378 | * @param props The bean property descriptors. | |
379 | * | |
380 | * @throws SQLException if a database access error occurs | |
381 | * | |
382 | * @return An int[] with column index to property index mappings. The 0th | |
383 | * element is meaningless because JDBC column indexing starts at 1. | |
384 | */ | |
385 | protected int[] mapColumnsToProperties(ResultSetMetaData rsmd, | |
386 | PropertyDescriptor[] props) throws SQLException { | |
387 | ||
388 | 0 | int cols = rsmd.getColumnCount(); |
389 | 0 | int columnToProperty[] = new int[cols + 1]; |
390 | 0 | Arrays.fill(columnToProperty, PROPERTY_NOT_FOUND); |
391 | ||
392 | 0 | for (int col = 1; col <= cols; col++) { |
393 | 0 | String columnName = rsmd.getColumnName(col); |
394 | 0 | for (int i = 0; i < props.length; i++) { |
395 | ||
396 | 0 | if (columnName.equalsIgnoreCase(props[i].getName())) { |
397 | 0 | columnToProperty[col] = i; |
398 | 0 | break; |
399 | } | |
400 | } | |
401 | } | |
402 | ||
403 | 0 | return columnToProperty; |
404 | } | |
405 | ||
406 | /** | |
407 | * Convert a <code>ResultSet</code> column into an object. Simple | |
408 | * implementations could just call <code>rs.getObject(index)</code> while | |
409 | * more complex implementations could perform type manipulation to match | |
410 | * the column's type to the bean property type. | |
411 | * | |
412 | * <p> | |
413 | * This implementation calls the appropriate <code>ResultSet</code> getter | |
414 | * method for the given property type to perform the type conversion. If | |
415 | * the property type doesn't match one of the supported | |
416 | * <code>ResultSet</code> types, <code>getObject</code> is called. | |
417 | * </p> | |
418 | * | |
419 | * @param rs The <code>ResultSet</code> currently being processed. It is | |
420 | * positioned on a valid row before being passed into this method. | |
421 | * | |
422 | * @param index The current column index being processed. | |
423 | * | |
424 | * @param propType The bean property type that this column needs to be | |
425 | * converted into. | |
426 | * | |
427 | * @throws SQLException if a database access error occurs | |
428 | * | |
429 | * @return The object from the <code>ResultSet</code> at the given column | |
430 | * index after optional type processing or <code>null</code> if the column | |
431 | * value was SQL NULL. | |
432 | */ | |
433 | protected Object processColumn(ResultSet rs, int index, Class propType) | |
434 | throws SQLException { | |
435 | ||
436 | 0 | if ( !propType.isPrimitive() && rs.getObject(index) == null ) { |
437 | 0 | return null; |
438 | } | |
439 | ||
440 | 0 | if (propType.equals(String.class)) { |
441 | 0 | return rs.getString(index); |
442 | ||
443 | 0 | } else if ( |
444 | propType.equals(Integer.TYPE) || propType.equals(Integer.class)) { | |
445 | 0 | return new Integer(rs.getInt(index)); |
446 | ||
447 | 0 | } else if ( |
448 | propType.equals(Boolean.TYPE) || propType.equals(Boolean.class)) { | |
449 | 0 | return new Boolean(rs.getBoolean(index)); |
450 | ||
451 | 0 | } else if (propType.equals(Long.TYPE) || propType.equals(Long.class)) { |
452 | 0 | return new Long(rs.getLong(index)); |
453 | ||
454 | 0 | } else if ( |
455 | propType.equals(Double.TYPE) || propType.equals(Double.class)) { | |
456 | 0 | return new Double(rs.getDouble(index)); |
457 | ||
458 | 0 | } else if ( |
459 | propType.equals(Float.TYPE) || propType.equals(Float.class)) { | |
460 | 0 | return new Float(rs.getFloat(index)); |
461 | ||
462 | 0 | } else if ( |
463 | propType.equals(Short.TYPE) || propType.equals(Short.class)) { | |
464 | 0 | return new Short(rs.getShort(index)); |
465 | ||
466 | 0 | } else if (propType.equals(Byte.TYPE) || propType.equals(Byte.class)) { |
467 | 0 | return new Byte(rs.getByte(index)); |
468 | ||
469 | 0 | } else if (propType.equals(Timestamp.class)) { |
470 | 0 | return rs.getTimestamp(index); |
471 | ||
472 | } else { | |
473 | 0 | return rs.getObject(index); |
474 | } | |
475 | ||
476 | } | |
477 | ||
478 | } |