001: // $Id: TableMapping.java 15 2007-09-01 03:09:40Z jcamaia $
002:
003: package net.sf.persist;
004:
005: import java.lang.reflect.Method;
006: import java.sql.DatabaseMetaData;
007: import java.sql.ResultSet;
008: import java.sql.SQLException;
009: import java.util.ArrayList;
010: import java.util.Collection;
011: import java.util.HashSet;
012: import java.util.LinkedHashMap;
013: import java.util.List;
014: import java.util.Locale;
015: import java.util.Map;
016: import java.util.Set;
017:
018: /**
019: * Holds mapping data from a given class and a table
020: */
021: public final class TableMapping extends Mapping {
022:
023: private final Class objectClass;
024:
025: private final net.sf.persist.annotations.Table tableAnnotation;
026: private final String tableName;
027:
028: private final String[] fields; // list of fields which have getters and setters
029: private final Map<String, net.sf.persist.annotations.Column> annotationsMap; // maps field names to annotations
030: private final Map<String, Method> gettersMap; // maps field names to getters
031: private final Map<String, Method> settersMap; // maps field names to setters
032:
033: private final boolean supportsGetGeneratedKeys;
034: private final boolean supportsBatchUpdates;
035:
036: private final Map<String, String> columnsMap = new LinkedHashMap(); // maps table columns to property names
037: private final String[] columns;
038: private final String[] primaryKeys;
039: private final String[] notPrimaryKeys;
040: private final String[] autoGeneratedColumns;
041: private final String[] notAutoGeneratedColumns;
042:
043: private final String selectSql;
044: private final String selectAllSql;
045: private final String insertSql;
046: private final String updateSql;
047: private final String deleteSql;
048:
049: public TableMapping(final DatabaseMetaData metaData,
050: final Class objectClass, final NameGuesser nameGuesser)
051: throws SQLException {
052:
053: ResultSet resultSet = null;
054:
055: // object class
056: this .objectClass = objectClass;
057:
058: // database support for auto increment keys
059: supportsGetGeneratedKeys = metaData.supportsGetGeneratedKeys();
060:
061: // database support for batch updates
062: supportsBatchUpdates = metaData.supportsBatchUpdates();
063:
064: // database name
065: final String databaseProductName = metaData
066: .getDatabaseProductName();
067:
068: // table annotation
069: tableAnnotation = (net.sf.persist.annotations.Table) objectClass
070: .getAnnotation(net.sf.persist.annotations.Table.class);
071:
072: // schema pattern
073: String schemaPattern = null;
074: if (databaseProductName.equalsIgnoreCase("Oracle")) {
075: schemaPattern = "%"; // oracle expects a pattern such as "%" to work
076: }
077:
078: // table name and annotation
079: tableName = getTableName(metaData, schemaPattern, objectClass,
080: nameGuesser);
081:
082: // all column names and types (from db)
083:
084: final List<String> columnsList = new ArrayList();
085: resultSet = metaData.getColumns(null, schemaPattern, tableName,
086: "%");
087: while (resultSet.next()) {
088: final String columnName = resultSet.getString(4);
089: columnsList.add(columnName);
090: }
091: columns = toArray(columnsList);
092:
093: // all primary keys (from db)
094:
095: final List<String> primaryKeysList = new ArrayList();
096: resultSet = metaData.getPrimaryKeys(null, schemaPattern,
097: tableName);
098: while (resultSet.next()) {
099: final String columnName = resultSet.getString(4);
100: primaryKeysList.add(columnName);
101: }
102: primaryKeys = toArray(primaryKeysList);
103:
104: // not primary keys
105:
106: final List<String> notPrimaryKeysList = new ArrayList();
107: for (String columnName : columns) {
108: if (!primaryKeysList.contains(columnName)) {
109: notPrimaryKeysList.add(columnName);
110: }
111: }
112: notPrimaryKeys = toArray(notPrimaryKeysList);
113:
114: // map field names to annotations, getters and setters
115:
116: final Map[] fieldsMaps = getFieldsMaps(objectClass);
117: annotationsMap = fieldsMaps[0];
118: gettersMap = fieldsMaps[1];
119: settersMap = fieldsMaps[2];
120: fields = toArray(gettersMap.keySet());
121:
122: // map column names to field names; create list of auto-increment columns
123: // columnsMap use keys in lower case
124:
125: // the actual autoGeneratedColumns list should have columns in the database order
126: final Set<String> autoGeneratedColumnsTemp = new HashSet();
127: for (String fieldName : fields) {
128: final String columnName = getColumnName(objectClass,
129: nameGuesser, annotationsMap, columnsList,
130: tableName, fieldName);
131: columnsMap.put(columnName.toLowerCase(Locale.ENGLISH),
132: fieldName);
133: final net.sf.persist.annotations.Column annotation = annotationsMap
134: .get(fieldName);
135: if (annotation != null && annotation.autoGenerated()) {
136: autoGeneratedColumnsTemp.add(columnName);
137: }
138: }
139:
140: // auto-increment and not-auto-increment columns, in the database order
141:
142: final List<String> notAutoGeneratedColumnsList = new ArrayList();
143: final List<String> autoGeneratedColumnsList = new ArrayList();
144: for (String columnName : columns) {
145: if (autoGeneratedColumnsTemp.contains(columnName)) {
146: autoGeneratedColumnsList.add(columnName);
147: } else {
148: notAutoGeneratedColumnsList.add(columnName);
149: }
150: }
151: notAutoGeneratedColumns = toArray(notAutoGeneratedColumnsList);
152: autoGeneratedColumns = toArray(autoGeneratedColumnsList);
153:
154: // assemble sql blocks to be used by crud sql statements
155:
156: final String allColumns = join(columns, "", ",");
157: final String noAutoColumns = join(notAutoGeneratedColumns, "",
158: ",");
159: final String allPlaceholders = multiply("?", columns.length,
160: ",");
161: final String noAutoPlaceholders = multiply("?",
162: notAutoGeneratedColumns.length, ",");
163: final String where = join(primaryKeys, "=?", ",");
164: final String updateSet = join(notPrimaryKeys, "=?", ",");
165:
166: // assemble crud sql statements
167:
168: selectSql = "select " + allColumns + " from " + tableName
169: + " where " + where;
170: selectAllSql = "select " + allColumns + " from " + tableName;
171:
172: if (autoGeneratedColumns.length == 0) {
173: insertSql = "insert into " + tableName + "(" + allColumns
174: + ")values(" + allPlaceholders + ")";
175: } else {
176: insertSql = "insert into " + tableName + "("
177: + noAutoColumns + ")values(" + noAutoPlaceholders
178: + ")";
179: }
180:
181: updateSql = "update " + tableName + " set " + updateSet
182: + " where " + where;
183: deleteSql = "delete from " + tableName + " where " + where;
184:
185: }
186:
187: // ---------- getters and setters ----------
188:
189: public boolean supportsGetGeneratedKeys() {
190: return supportsGetGeneratedKeys;
191: }
192:
193: public boolean supportsBatchUpdates() {
194: return supportsBatchUpdates;
195: }
196:
197: public Class getObjectClass() {
198: return objectClass;
199: }
200:
201: public String getTableName() {
202: return tableName;
203: }
204:
205: public net.sf.persist.annotations.Table getTableAnnotation() {
206: return tableAnnotation;
207: }
208:
209: public String[] getColumns() {
210: return columns;
211: }
212:
213: public Map<String, String> getColumnsMap() {
214: return columnsMap;
215: }
216:
217: public String[] getPrimaryKeys() {
218: return primaryKeys;
219: }
220:
221: public String[] getNotPrimaryKeys() {
222: return notPrimaryKeys;
223: }
224:
225: public String[] getAutoGeneratedColumns() {
226: return autoGeneratedColumns;
227: }
228:
229: public String[] getNotAutoGeneratedColumns() {
230: return notAutoGeneratedColumns;
231: }
232:
233: public String[] getFields() {
234: return fields;
235: }
236:
237: public Map<String, net.sf.persist.annotations.Column> getAnnotationsMap() {
238: return annotationsMap;
239: }
240:
241: public Map<String, Method> getGettersMap() {
242: return gettersMap;
243: }
244:
245: public Map<String, Method> getSettersMap() {
246: return settersMap;
247: }
248:
249: public Method getGetterForColumn(final String columnName) {
250: final String fieldName = columnsMap.get(columnName
251: .toLowerCase(Locale.ENGLISH));
252: return gettersMap.get(fieldName);
253: }
254:
255: public Method getSetterForColumn(final String columnName) {
256: final String fieldName = columnsMap.get(columnName
257: .toLowerCase(Locale.ENGLISH));
258: return settersMap.get(fieldName);
259: }
260:
261: public String getSelectSql() {
262: return selectSql;
263: }
264:
265: public String getSelectAllSql() {
266: return selectAllSql;
267: }
268:
269: public String getInsertSql() {
270: return insertSql;
271: }
272:
273: public String getUpdateSql() {
274: return updateSql;
275: }
276:
277: public String getDeleteSql() {
278: return deleteSql;
279: }
280:
281: // ---------- utility methods ----------
282:
283: private static String getTableName(final DatabaseMetaData metaData,
284: final String schema, final Class objectClass,
285: final NameGuesser nameGuesser) throws SQLException {
286:
287: String name = null;
288:
289: final net.sf.persist.annotations.Table tableAnnotation = (net.sf.persist.annotations.Table) objectClass
290: .getAnnotation(net.sf.persist.annotations.Table.class);
291:
292: if (tableAnnotation != null
293: && !tableAnnotation.name().equals("")) {
294: // if there's a Table annotation, use it
295: name = checkTableName(metaData, schema, tableAnnotation
296: .name());
297:
298: // test if the specified table name actually exists
299: if (name == null) {
300: throw new PersistException("Class ["
301: + objectClass.getName() + "] specifies table ["
302: + tableAnnotation.name()
303: + "] that does not exist in the database");
304: }
305: } else {
306: // if no annotation, try guessed table names
307: final String className = objectClass.getSimpleName()
308: .substring(0, 1).toLowerCase()
309: + objectClass.getSimpleName().substring(1);
310: final Set<String> guessedNames = nameGuesser
311: .guessColumn(className);
312: for (String guessedTableName : guessedNames) {
313: name = checkTableName(metaData, schema,
314: guessedTableName);
315: if (name != null) {
316: break;
317: }
318: }
319: if (name == null) {
320: throw new PersistException(
321: "Class ["
322: + objectClass.getName()
323: + "] does not specify a table name through a Table annotation and no guessed table names "
324: + guessedNames
325: + " exist in the database");
326: }
327: }
328:
329: return name;
330: }
331:
332: /**
333: * Check if the given name corresponds to a table in the database and
334: * returns the corresponding name with the capitalization returned by the
335: * database metadata
336: */
337: private static String checkTableName(
338: final DatabaseMetaData metaData, final String schema,
339: final String tableName) throws SQLException {
340:
341: ResultSet resultSet;
342: String ret = null;
343:
344: // try name in upper case -- should work in most databases
345: resultSet = metaData.getTables(null, schema, tableName
346: .toUpperCase(Locale.ENGLISH), null);
347: if (resultSet.next()) {
348: ret = tableName.toUpperCase(Locale.ENGLISH);
349: }
350: resultSet.close();
351:
352: if (ret == null) {
353: // try name in lower case
354: resultSet = metaData.getTables(null, schema, tableName
355: .toLowerCase(Locale.ENGLISH), null);
356: if (resultSet.next()) {
357: ret = tableName.toLowerCase(Locale.ENGLISH);
358: }
359: resultSet.close();
360: }
361:
362: if (ret == null) {
363: // last resort: compare with all table names in the schema
364: // (may be very expensive in databases such as oracle)
365: // this may end up being used in databases that allow case sensitive names (such as postgresql)
366: resultSet = metaData.getTables(null, schema, "%", null);
367: while (resultSet.next()) {
368: final String dbTableName = resultSet.getString(3);
369: if (tableName.equalsIgnoreCase(dbTableName)) {
370: ret = dbTableName;
371: break;
372: }
373: }
374: resultSet.close();
375: }
376:
377: return ret;
378: }
379:
380: private static String getColumnName(
381: final Class objectClass,
382: final NameGuesser nameGuesser,
383: final Map<String, net.sf.persist.annotations.Column> annotationsMap,
384: final List<String> columnsList, final String tableName,
385: final String fieldName) throws SQLException {
386:
387: String columnName = null;
388:
389: final net.sf.persist.annotations.Column annotation = annotationsMap
390: .get(fieldName);
391: if (annotation != null && !annotation.name().equals("")) {
392: // if there's an annotation, use it
393: columnName = getIgnoreCase(columnsList, annotation.name());
394:
395: // check if the specified column actually exists in the table
396: if (columnName == null) {
397: throw new PersistException("Field [" + fieldName
398: + "] from class [" + objectClass.getName()
399: + "] specifies column [" + annotation.name()
400: + "] on table ["
401: + tableName.toLowerCase(Locale.ENGLISH)
402: + "] that does not exist in the database");
403: }
404: } else {
405: // if no annotation, try guessed column names
406: final Set<String> guessedNames = nameGuesser
407: .guessColumn(fieldName);
408: for (String guessedColumnName : guessedNames) {
409: columnName = getIgnoreCase(columnsList,
410: guessedColumnName);
411: if (columnName != null) {
412: break;
413: }
414: }
415: if (columnName == null) {
416: throw new PersistException(
417: "Field ["
418: + fieldName
419: + "] from class ["
420: + objectClass.getName()
421: + "] does not specify a column name through a Column annotation and no guessed column names "
422: + guessedNames
423: + " exist in the database. If this field is not supposed to be associated "
424: + "with the database, please annotate it with @NoColumn");
425: }
426: }
427:
428: return columnName;
429: }
430:
431: // ---------- helpers ----------
432:
433: /**
434: * Returns the first entry from the provided collection that matches the
435: * provided string ignoring case during the comparison.
436: */
437: private static String getIgnoreCase(
438: final Collection<String> collection, final String str) {
439: String ret = null;
440: for (String s : collection) {
441: if (s.equalsIgnoreCase(str)) {
442: ret = s;
443: break;
444: }
445: }
446: return ret;
447: }
448:
449: private static String[] toArray(final List<String> list) {
450: String[] array = new String[list.size()];
451: for (int i = 0; i < list.size(); i++) {
452: array[i] = list.get(i);
453: }
454: return array;
455: }
456:
457: private static String[] toArray(final Set<String> set) {
458: final String[] array = new String[set.size()];
459: int i = 0;
460: for (String s : set) {
461: array[i] = s;
462: i++;
463: }
464: return array;
465: }
466:
467: private static String join(final String[] list,
468: final String suffix, final String separator) {
469: final StringBuffer buf = new StringBuffer();
470: for (String obj : list) {
471: buf.append(obj.toString()).append(suffix).append(separator);
472: }
473: if (buf.length() > 0 && separator.length() > 0) {
474: buf.delete(buf.length() - separator.length(), buf.length());
475: }
476: return buf.toString();
477: }
478:
479: private static String multiply(final String str, final int times,
480: final String separator) {
481: final StringBuffer buf = new StringBuffer();
482: for (int i = 0; i < times; i++) {
483: buf.append(str).append(separator);
484: }
485: if (separator.length() > 0) {
486: buf.delete(buf.length() - separator.length(), buf.length());
487: }
488: return buf.toString();
489: }
490:
491: }
|