001: /*
002: * Copyright 2002-2007 the original author or authors.
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License");
005: * you may not use this file except in compliance with the License.
006: * You may obtain a copy of the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS,
012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013: * See the License for the specific language governing permissions and
014: * limitations under the License.
015: */
016:
017: package org.springframework.jdbc.core;
018:
019: import org.apache.commons.logging.Log;
020: import org.apache.commons.logging.LogFactory;
021: import org.springframework.beans.BeanWrapper;
022: import org.springframework.beans.BeanWrapperImpl;
023: import org.springframework.beans.NotWritablePropertyException;
024: import org.springframework.dao.DataAccessResourceFailureException;
025: import org.springframework.dao.DataRetrievalFailureException;
026: import org.springframework.dao.InvalidDataAccessApiUsageException;
027: import org.springframework.jdbc.support.JdbcUtils;
028: import org.springframework.util.StringUtils;
029:
030: import java.lang.reflect.Constructor;
031: import java.lang.reflect.Field;
032: import java.lang.reflect.InvocationTargetException;
033: import java.math.BigDecimal;
034: import java.sql.ResultSet;
035: import java.sql.ResultSetMetaData;
036: import java.sql.SQLException;
037: import java.util.HashMap;
038: import java.util.Map;
039:
040: /**
041: * Abstract base class for BeanPropertyRowMapper implementations. Provides initialization of mapped/persistent fields
042: * metadata and the actual mapping between bean properties and SQL table columns.
043: *
044: * @author trisberg
045: * @since 2.5
046: */
047: public abstract class AbstractBeanPropertyRowMapper {
048:
049: /** Logger available to subclasses */
050: protected final Log logger = LogFactory.getLog(getClass());
051:
052: /** The class we are mapping to */
053: protected Class mappedClass;
054:
055: /** The default or no-arg constructor for the mapped class */
056: private Constructor defaultConstruct;
057:
058: /** Map of the fields we provide mapping for */
059: private Map mappedFields;
060:
061: /**
062: * Set the class that each row should be mapped to.
063: * @param mappedClass the mapped class
064: */
065: protected synchronized void doSetMappedClass(Class mappedClass) {
066: if (this .mappedClass == null) {
067: initialize(mappedClass);
068: } else {
069: if (!this .mappedClass.equals(mappedClass)) {
070: throw new InvalidDataAccessApiUsageException(
071: "The mapped class can not be reassigned to map to "
072: + mappedClass
073: + " since it is already providing mapping for "
074: + this .mappedClass);
075: }
076: }
077: }
078:
079: /**
080: * Get the class that we are mapping to.
081: * @return the mapped class
082: */
083: public Class getMappedClass() {
084: return mappedClass;
085: }
086:
087: protected Object doMapRow(ResultSet rs, int rowNumber)
088: throws SQLException {
089: if (getMappedClass() == null)
090: throw new InvalidDataAccessApiUsageException(
091: "Target class was not specified - it is mandatory");
092: Object result;
093: try {
094: result = this .defaultConstruct.newInstance((Object[]) null);
095: } catch (IllegalAccessException e) {
096: throw new DataAccessResourceFailureException(
097: "Failed to load class "
098: + this .mappedClass.getName(), e);
099: } catch (InvocationTargetException e) {
100: throw new DataAccessResourceFailureException(
101: "Failed to load class "
102: + this .mappedClass.getName(), e);
103: } catch (InstantiationException e) {
104: throw new DataAccessResourceFailureException(
105: "Failed to load class "
106: + this .mappedClass.getName(), e);
107: }
108: ResultSetMetaData rsmd = rs.getMetaData();
109: int columns = rsmd.getColumnCount();
110: for (int i = 1; i <= columns; i++) {
111: String column = JdbcUtils.lookupColumnName(rsmd, i)
112: .toLowerCase();
113: PersistentField fieldMeta = (PersistentField) this .mappedFields
114: .get(column);
115: if (fieldMeta != null) {
116: BeanWrapper bw = new BeanWrapperImpl(mappedClass);
117: bw.setWrappedInstance(result);
118: fieldMeta.setSqlType(rsmd.getColumnType(i));
119: Object value = null;
120: Class fieldType = fieldMeta.getJavaType();
121: if (fieldType.equals(String.class)) {
122: value = rs.getString(column);
123: } else if (fieldType.equals(byte.class)
124: || fieldType.equals(Byte.class)) {
125: value = new Byte(rs.getByte(column));
126: } else if (fieldType.equals(short.class)
127: || fieldType.equals(Short.class)) {
128: value = new Short(rs.getShort(column));
129: } else if (fieldType.equals(int.class)
130: || fieldType.equals(Integer.class)) {
131: value = new Integer(rs.getInt(column));
132: } else if (fieldType.equals(long.class)
133: || fieldType.equals(Long.class)) {
134: value = new Long(rs.getLong(column));
135: } else if (fieldType.equals(float.class)
136: || fieldType.equals(Float.class)) {
137: value = new Float(rs.getFloat(column));
138: } else if (fieldType.equals(double.class)
139: || fieldType.equals(Double.class)) {
140: value = new Double(rs.getDouble(column));
141: } else if (fieldType.equals(BigDecimal.class)) {
142: value = rs.getBigDecimal(column);
143: } else if (fieldType.equals(boolean.class)
144: || fieldType.equals(Boolean.class)) {
145: value = (rs.getBoolean(column)) ? Boolean.TRUE
146: : Boolean.FALSE;
147: } else if (fieldType.equals(java.util.Date.class)
148: || fieldType.equals(java.sql.Timestamp.class)
149: || fieldType.equals(java.sql.Time.class)
150: || fieldType.equals(Number.class)) {
151: value = JdbcUtils.getResultSetValue(rs, rs
152: .findColumn(column));
153: }
154: if (value != null) {
155: if (bw.isWritableProperty(fieldMeta.getFieldName())) {
156: try {
157: if (logger.isDebugEnabled()
158: && rowNumber == 0) {
159: logger
160: .debug("Mapping column named \""
161: + column
162: + "\""
163: + " containing values of SQL type "
164: + fieldMeta
165: .getSqlType()
166: + " to property \""
167: + fieldMeta
168: .getFieldName()
169: + "\""
170: + " of type "
171: + fieldMeta
172: .getJavaType());
173: }
174: bw.setPropertyValue(fieldMeta
175: .getFieldName(), value);
176: } catch (NotWritablePropertyException ex) {
177: throw new DataRetrievalFailureException(
178: "Unable to map column " + column
179: + " to property "
180: + fieldMeta.getFieldName(),
181: ex);
182: }
183: } else {
184: if (rowNumber == 0) {
185: logger
186: .warn("Unable to access the setter for "
187: + fieldMeta.getFieldName()
188: + ". Check that "
189: + "set"
190: + StringUtils
191: .capitalize(fieldMeta
192: .getFieldName())
193: + " is declared and has public access.");
194: }
195: }
196: }
197: }
198: }
199: return result;
200: }
201:
202: /**
203: * Initialize the mapping metadata
204: * @param mappedClass
205: */
206: protected void initialize(Class mappedClass) {
207: this .mappedClass = mappedClass;
208: try {
209: this .defaultConstruct = mappedClass
210: .getConstructor((Class[]) null);
211: } catch (NoSuchMethodException ex) {
212: throw new DataAccessResourceFailureException(
213: new StringBuffer()
214: .append(
215: "Failed to access default or no-arg constructor of ")
216: .append(mappedClass.getName()).toString(),
217: ex);
218: }
219: this .mappedFields = new HashMap();
220: Class metaDataClass = mappedClass;
221: while (metaDataClass != null) {
222: Field[] f = metaDataClass.getDeclaredFields();
223: for (int i = 0; i < f.length; i++) {
224: PersistentField pf = new PersistentField();
225: pf.setFieldName(f[i].getName());
226: pf.setJavaType(f[i].getType());
227: this .mappedFields.put(f[i].getName().toLowerCase(), pf);
228: String underscoredName = underscoreName(f[i].getName());
229: if (!f[i].getName().toLowerCase().equals(
230: underscoredName)) {
231: this .mappedFields.put(underscoredName, pf);
232: }
233: }
234: // try any superclass
235: metaDataClass = metaDataClass.getSuperclass();
236: }
237: }
238:
239: /**
240: * Convert a name in camelCase to an underscored name in lower case. Any upper case letters are
241: * converted to lower case with a preceding underscore.
242: * @param name The string containing original name
243: * @return The name converted
244: */
245: public static String underscoreName(String name) {
246: StringBuffer result = new StringBuffer();
247: if (name != null && name.length() > 0) {
248: result.append(name.substring(0, 1).toLowerCase());
249: for (int i = 1; i < name.length(); i++) {
250: String s = name.substring(i, i + 1);
251: if (s.equals(s.toUpperCase())) {
252: result.append("_");
253: result.append(s.toLowerCase());
254: } else {
255: result.append(s);
256: }
257: }
258: }
259: return result.toString();
260: }
261:
262: /**
263: * A PersistentField represents the info we are interested in knowing about
264: * the fields and their relationship to the columns of the database table.
265: */
266: protected class PersistentField {
267:
268: private String fieldName;
269:
270: private Class javaType;
271:
272: private int sqlType;
273:
274: public String getFieldName() {
275: return fieldName;
276: }
277:
278: public void setFieldName(String fieldName) {
279: this .fieldName = fieldName;
280: }
281:
282: public Class getJavaType() {
283: return javaType;
284: }
285:
286: public void setJavaType(Class javaType) {
287: this .javaType = javaType;
288: }
289:
290: public int getSqlType() {
291: return sqlType;
292: }
293:
294: public void setSqlType(int sqlType) {
295: this.sqlType = sqlType;
296: }
297: }
298: }
|