001: // Copyright (c) 2003-2007, Jodd Team (jodd.sf.net). All Rights Reserved.
002:
003: package jodd.db.orm.mapper;
004:
005: import jodd.bean.BeanUtil;
006: import jodd.db.orm.DbOrm;
007: import jodd.db.orm.DbOrmException;
008: import jodd.db.orm.DbEntityDescriptor;
009: import jodd.util.Wildcard;
010: import jodd.util.ReflectUtil;
011:
012: import java.sql.ResultSet;
013: import java.sql.ResultSetMetaData;
014: import java.sql.SQLException;
015: import java.util.Map;
016: import java.util.Set;
017: import java.util.HashSet;
018:
019: /**
020: * Maps all columns of database result set (RS) row to objects.
021: * It does it in two steps: preparation (reading table and column names)
022: * and parsing (parsing one result set row to resulting objects).
023: *
024: * <p>
025: * <b>Preparation</b><br>
026: * Default mapper reads RS column and table names from RS meta-data and external maps, if provided.
027: * Since column name is always availiable in RS meta-data, it may be used to hold table name information.
028: * Column names may contain table code separator ({@link jodd.db.orm.DbOrm#getColumnAliasSeparator()} that
029: * divides column name to table reference and column name. Here, table reference may be either table name or
030: * table alias. When it is table alias, external alias-to-name map must be provided.
031: * Hence, this defines the table name, and there is no need to read it from RS meta-data.
032: *
033: * <p>
034: * When column name doesn't contain a separator, it may be either an actual column name, or a column code.
035: * For column codes, both table and colum name is lookuped from external map. If column name is an actual column name,
036: * table information is read from the RS meta data. Unfortunately, some DBs (such Oracle) doesn't implements
037: * this simple JDBC feature. Therefore, it must be expected that column table name is not availiable.
038: *
039: * <p>
040: * Table name is also not availiable for columns which are not directly table columns:
041: * e.g. some calculations, counts etc.
042: *
043: * <p>
044: * <b>Parsing</b><br>
045: * Parser takes types array and tries to populate their instances in best possible way. It assumes that provided
046: * types list matches selected columns. That is very important, and yet very easy and natural to follow.
047: * So, parser will try to inject columns value into the one result instance. Now, there are two types of instances:
048: * simple types (numbers and strings) and entities (pojo objects). Simple types are always mapped to
049: * one and only one column. Entities will be mapped to all possible columns that can be matched starting from
050: * current column. So, simple types are not column-hungry, entity types are column-hungry:)
051: *
052: * <p>
053: * A column can be injected in one entities property only once. If one column is already mapped to current result,
054: * RS mapper will assume that current result is finished with mapping and will proceed to the next one.
055: * Similarly, if property name is not found for a column, RS mapper will proceed to the next result.
056: * Therefore, entity types are column precize and hungry;) - all listed columns must be mapped somewhere.
057: *
058: * <p>
059: * Results that are not used during parsing will be set to <code>null</code>.
060: */
061: public class DefaultResultSetMapper implements ResultSetMapper {
062:
063: protected DbOrm dbOrm;
064: protected ResultSet rs;
065:
066: protected int totalColumns; // total number of columns
067: protected String[] columnNames; // list of all column names
068: protected String[] tableNames; // list of table names for each column, table name may be null
069:
070: private Set<String> resultColums; // internal columns per entity cache
071:
072: // ---------------------------------------------------------------- ctor
073:
074: public DefaultResultSetMapper(ResultSet rs) {
075: this (rs, null, DbOrm.getInstance());
076: }
077:
078: public DefaultResultSetMapper(ResultSet rs, DbOrm orm) {
079: this (rs, null, orm);
080: }
081:
082: public DefaultResultSetMapper(ResultSet rs,
083: Map<String, String[]> columnAliases) {
084: this (rs, columnAliases, DbOrm.getInstance());
085: }
086:
087: /**
088: * Reads RS meta-data for column and table names.
089: */
090: public DefaultResultSetMapper(ResultSet rs,
091: Map<String, String[]> columnAliases, DbOrm orm) {
092: this .dbOrm = orm;
093: this .rs = rs;
094: this .resultColums = new HashSet<String>();
095: try {
096: ResultSetMetaData rsMetaData = rs.getMetaData();
097: if (rsMetaData == null) {
098: throw new DbOrmException(
099: "JDBC driver doesn't provide meta-data.");
100: }
101: totalColumns = rsMetaData.getColumnCount();
102: columnNames = new String[totalColumns];
103: tableNames = new String[totalColumns];
104:
105: for (int i = 0; i < totalColumns; i++) {
106: String columnName = rsMetaData.getColumnName(i + 1);
107: String tableName = null;
108:
109: // resolve column and table name
110: int sepNdx = columnName.indexOf(dbOrm
111: .getColumnAliasSeparator());
112: if (sepNdx != -1) {
113: // column alias exist, result set is ignored and columnAliases contains table data.
114: tableName = columnName.substring(0, sepNdx);
115: if (columnAliases != null) {
116: String[] tableData = columnAliases
117: .get(tableName);
118: if (tableData != null) {
119: tableName = tableData[0];
120: }
121: }
122: columnName = columnName.substring(sepNdx + 1);
123: } else {
124: // column alias doesn't exist, table name is readed from columnAliases and result set (if availiable).
125: if (columnAliases != null) {
126: String[] columnData = columnAliases
127: .get(columnName.toLowerCase());
128: if (columnData != null) {
129: tableName = columnData[0];
130: columnName = columnData[1];
131: }
132: }
133: if (tableName == null) {
134: try {
135: tableName = rsMetaData.getTableName(i + 1);
136: } catch (SQLException sex) {
137: // ignore
138: }
139: if ((tableName != null)
140: && (tableName.length() == 0)) {
141: tableName = null;
142: }
143: }
144: }
145:
146: columnName = columnName.trim();
147: if (columnName.length() == 0) {
148: columnName = null;
149: }
150: columnNames[i] = columnName;
151: if (tableName != null) {
152: tableName = tableName.trim();
153: }
154: tableNames[i] = tableName;
155: }
156: } catch (SQLException sex) {
157: throw new DbOrmException(
158: "Unable to read ResultSet meta-data.", sex);
159: }
160: }
161:
162: // ---------------------------------------------------------------- delegates
163:
164: /**
165: * Moves the cursor down one row from its current position.
166: */
167: public boolean next() {
168: try {
169: return rs.next();
170: } catch (SQLException sex) {
171: throw new DbOrmException(
172: "Unable to move ResultSet cursor to next position.",
173: sex);
174: }
175: }
176:
177: /**
178: * Releases this ResultSet object's database and JDBC resources immediately instead of
179: * waiting for this to happen when it is automatically closed.
180: */
181: public void close() {
182: try {
183: rs.close();
184: } catch (SQLException sex) {
185: // ignore
186: }
187: }
188:
189: /**
190: * Return JDBC result set.
191: */
192: public ResultSet getResultSet() {
193: return rs;
194: }
195:
196: // ---------------------------------------------------------------- parse objects
197:
198: /**
199: * Creates new instances of a types.
200: */
201: protected Object newInstance(Class types) {
202: try {
203: return types.newInstance();
204: } catch (Exception ex) {
205: throw new DbOrmException(
206: "Unable to create new entity instance for type '"
207: + types + "'.", ex);
208: }
209: }
210:
211: protected Class[] cachedUsedTypes;
212: protected String[] cachedTypesTableNames;
213:
214: /**
215: * Creates table names for all specified types.
216: * Since this is usually done once per result set, these names are cached.
217: * Type name will be <code>null</code> for simple names, i.e. for all those
218: * types that returns <code>null</code> when used by {@link DbOrm#lookup(Class)}.
219: */
220: protected String[] createTypesTableNames(Class[] types) {
221: if (types != cachedUsedTypes) {
222: cachedTypesTableNames = new String[types.length];
223: for (int i = 0; i < types.length; i++) {
224: if (types[i] == null) {
225: cachedTypesTableNames[i] = null;
226: continue;
227: }
228: DbEntityDescriptor ded = dbOrm.lookup(types[i]);
229: if (ded != null) {
230: cachedTypesTableNames[i] = ded.getTableName();
231: }
232: }
233: cachedUsedTypes = types;
234: }
235: return cachedTypesTableNames;
236: }
237:
238: protected int cachedColumnNdx;
239: protected Object cachedColumnValue;
240:
241: /**
242: * Reads column value from result set. Since this method may be called more then once for
243: * the same column, it caches column value.
244: */
245: protected Object readColumnValue(int colNdx) {
246: if (colNdx != cachedColumnNdx) {
247: try {
248: cachedColumnValue = rs.getObject(colNdx + 1);
249: } catch (SQLException sex) {
250: throw new DbOrmException(
251: "Unable to read value for column #"
252: + (colNdx + 1) + '.');
253: }
254: cachedColumnNdx = colNdx;
255: }
256: return cachedColumnValue;
257: }
258:
259: public Object[] parseObjects(Class... types) {
260: int totalTypes = types.length;
261: Object[] result = new Object[totalTypes];
262: boolean[] resultUsage = new boolean[totalTypes];
263: String[] typesTableNames = createTypesTableNames(types);
264:
265: int currentResult = 0;
266: cachedColumnNdx = -1;
267: int colNdx = 0;
268: while (colNdx < totalColumns) {
269:
270: // no more types for mapping?
271: if (currentResult >= totalTypes) {
272: break;
273: }
274:
275: // skip columns that doesn't map
276: Class currentType = types[currentResult];
277: if (currentType == null) {
278: colNdx++;
279: currentResult++;
280: resultColums.clear();
281: continue;
282: }
283:
284: Object value = readColumnValue(colNdx);
285: String columnName = columnNames[colNdx];
286: String tableName = tableNames[colNdx];
287: String resultTableName = typesTableNames[currentResult];
288:
289: if (resultTableName == null) {
290: // match: simple type
291: result[currentResult] = ReflectUtil.castType(value,
292: currentType);
293: resultUsage[currentResult] = true;
294: colNdx++;
295: currentResult++;
296: resultColums.clear();
297: continue;
298: }
299: //if ((tableName == null) || (resultTableName.equals(tableName) == true)) {
300: if ((tableName == null)
301: || (Wildcard.equalsOrMatch(tableName,
302: resultTableName) == true)) {
303: if (resultColums.contains(columnName) == false) {
304: String propertyName = dbOrm.lookup(currentType)
305: .getPropertyName(columnName);
306: if (propertyName != null) {
307: if (result[currentResult] == null) {
308: result[currentResult] = newInstance(currentType);
309: }
310: /*
311: boolean success =
312: value != null ?
313: BeanUtil.setDeclaredPropertySilent(result[currentResult], propertyName, value)
314: :
315: BeanUtil.hasDeclaredProperty(result[currentResult], propertyName);
316: */
317: if (BeanUtil.hasDeclaredProperty(
318: result[currentResult], propertyName) == true) {
319: // match: entity
320: if (value != null) {
321: BeanUtil.setDeclaredProperty(
322: result[currentResult],
323: propertyName, value);
324: resultUsage[currentResult] = true;
325: }
326: colNdx++;
327: resultColums.add(columnName);
328: continue;
329: }
330: }
331: }
332: }
333: // got to next type, i.e. result
334: currentResult++;
335: resultColums.clear();
336: }
337:
338: resultColums.clear();
339: for (int i = 0; i < resultUsage.length; i++) {
340: if (resultUsage[i] == false) {
341: result[i] = null;
342: }
343: }
344: return result;
345: }
346:
347: public Object parseOneObject(Class... types) {
348: return parseObjects(types)[0];
349: }
350:
351: }
|