001: /*
002: * GeoTools - OpenSource mapping toolkit
003: * http://geotools.org
004: * (C) 2004-2006, GeoTools Project Managment Committee (PMC)
005: *
006: * This library is free software; you can redistribute it and/or
007: * modify it under the terms of the GNU Lesser General Public
008: * License as published by the Free Software Foundation;
009: * version 2.1 of the License.
010: *
011: * This library is distributed in the hope that it will be useful,
012: * but WITHOUT ANY WARRANTY; without even the implied warranty of
013: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
014: * Lesser General Public License for more details.
015: *
016: * TODO: 26-may-2005 D. Adler Added constructor with returnFIDColumnsAsAttributes. Added accessors for ColumnInfo
017: */
018: /*
019: * 26-may-2005 D. Adler Added constructor with returnFIDColumnsAsAttributes.
020: * Added accessors for ColumnInfo
021: * 12-jul-2006 D. Adler GEOT-728 Refactor FIDMapper classes
022: */
023: package org.geotools.data.jdbc.fidmapper;
024:
025: import org.geotools.data.DataSourceException;
026: import org.geotools.data.SchemaNotFoundException;
027: import org.geotools.data.Transaction;
028: import org.geotools.data.jdbc.JDBCUtils;
029: import org.geotools.feature.FeatureType;
030: import java.io.IOException;
031: import java.sql.Connection;
032: import java.sql.DatabaseMetaData;
033: import java.sql.ResultSet;
034: import java.sql.SQLException;
035: import java.sql.Statement;
036: import java.sql.Types;
037: import java.util.ArrayList;
038: import java.util.Collection;
039: import java.util.HashMap;
040: import java.util.LinkedHashMap;
041: import java.util.List;
042: import java.util.Map;
043: import java.util.logging.Level;
044: import java.util.logging.Logger;
045:
046: /**
047: * Default FID mapper that works with default FID mappers.
048: *
049: * <p>
050: * May also be used a base class for more specific and feature rich factories
051: * </p>
052: *
053: * @author Andrea Aime
054: * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/jdbc/src/main/java/org/geotools/data/jdbc/fidmapper/DefaultFIDMapperFactory.java $
055: */
056: public class DefaultFIDMapperFactory implements FIDMapperFactory {
057: /** The logger for the filter module. */
058: protected static final Logger LOGGER = org.geotools.util.logging.Logging
059: .getLogger("org.geotools.data.jdbc");
060: protected boolean returningTypedFIDMapper = true;
061:
062: /** Set if table FID columns are to be returned as business attributes. */
063: protected boolean returnFIDColumnsAsAttributes = false;
064:
065: /**
066: * Constructs a DefaultFIDMapperFactory which will not return FID columns
067: * as business attributes.
068: */
069: public DefaultFIDMapperFactory() {
070: }
071:
072: /**
073: * Constructs a DefaultFIDMapperFactory with user specification of whether
074: * to return FID columns as business attributes.
075: *
076: * @param returnFIDColumnsAsAttributes true if FID columns should be
077: * returned as business attributes.
078: */
079: public DefaultFIDMapperFactory(boolean returnFIDColumnsAsAttributes) {
080: this .returnFIDColumnsAsAttributes = returnFIDColumnsAsAttributes;
081: }
082:
083: /**
084: * Setter for the flag controlling wther a "typed" fid mapper is returned.
085: * @param returningTypedFIDMapper
086: */
087: public void setReturningTypedFIDMapper(
088: boolean returningTypedFIDMapper) {
089: this .returningTypedFIDMapper = returningTypedFIDMapper;
090: }
091:
092: /**
093: * Getter for the flog controll wether a "typed" fid mapper should be returned.
094: */
095: public boolean isReturningTypedFIDMapper() {
096: return returningTypedFIDMapper;
097: }
098:
099: /**
100: * Gets the appropriate FIDMapper for the specified table.
101: *
102: * @param catalog
103: * @param schema
104: * @param tableName
105: * @param connection the active database connection to get table key
106: * information
107: *
108: * @return the appropriate FIDMapper for the specified table.
109: *
110: * @throws IOException if any error occurs.
111: */
112: public FIDMapper getMapper(String catalog, String schema,
113: String tableName, Connection connection) throws IOException {
114: ColumnInfo[] colInfos = getPkColumnInfo(catalog, schema,
115: tableName, connection);
116: FIDMapper mapper = null;
117:
118: if (colInfos.length == 0) {
119: mapper = buildNoPKMapper(schema, tableName, connection);
120: } else if (colInfos.length > 1) {
121: mapper = buildMultiColumnFIDMapper(schema, tableName,
122: connection, colInfos);
123: } else {
124: ColumnInfo ci = colInfos[0];
125:
126: mapper = buildSingleColumnFidMapper(schema, tableName,
127: connection, ci);
128: }
129:
130: if (mapper == null) {
131: mapper = buildLastResortFidMapper(schema, tableName,
132: connection, colInfos);
133:
134: if (mapper == null) {
135: String msg = "Cannot map primary key to a FID mapper, primary key columns are:\n"
136: + getColumnInfoList(colInfos);
137: LOGGER.log(Level.SEVERE, msg);
138: throw new IOException(msg);
139: }
140: }
141:
142: if (returningTypedFIDMapper && (mapper != null)) {
143: return new TypedFIDMapper(mapper, tableName);
144: } else {
145: return mapper;
146: }
147: }
148:
149: /**
150: * Retuns a List of column infos, nice for logging the column infos
151: * leveraging the complete toString() method provided by lists
152: *
153: * @param colInfos
154: *
155: */
156: protected List getColumnInfoList(ColumnInfo[] colInfos) {
157: ArrayList list = new ArrayList();
158:
159: for (int i = 0; i < colInfos.length; i++) {
160: list.add(colInfos[i]);
161: }
162:
163: return list;
164: }
165:
166: /**
167: * Builds a FidMapper when every other tentative of building one fails.
168: * This method is used as a last resort fall back, use it if you can
169: * provide a FIDMapper that works on every kind of table, but it's usually
170: * suboptimal. The default behaviour is to return no FID mapper at all.
171: *
172: * @param schema
173: * @param tableName
174: * @param connection
175: * @param colInfos
176: *
177: */
178: protected FIDMapper buildLastResortFidMapper(String schema,
179: String tableName, Connection connection,
180: ColumnInfo[] colInfos) {
181: return null;
182: }
183:
184: /**
185: * Builds a FID mapper based on a single column primary key. Default
186: * version tries the auto-increment way, then a mapping on an {@link
187: * MaxIncFIDMapper} type for numeric columns, and a plain {@link
188: * BasicFIDMapper} of text based columns.
189: *
190: * @param schema
191: * @param tableName
192: * @param connection an open database connection.
193: * @param ci the column information for the FID column.
194: *
195: * @return the appropriate FIDMapper.
196: */
197: protected FIDMapper buildSingleColumnFidMapper(String schema,
198: String tableName, Connection connection, ColumnInfo ci) {
199: if (ci.autoIncrement) {
200: return new AutoIncrementFIDMapper(schema, tableName,
201: ci.colName, ci.dataType);
202: } else if (isIntegralType(ci.dataType)) {
203: return new MaxIncFIDMapper(schema, tableName, ci.colName,
204: ci.dataType, this .returnFIDColumnsAsAttributes);
205: } else {
206: return new BasicFIDMapper(ci.colName, ci.size,
207: this .returnFIDColumnsAsAttributes);
208: }
209: }
210:
211: /**
212: * DOCUMENT ME!
213: *
214: * @param schema
215: * @param tableName
216: * @param connection
217: *
218: */
219: protected FIDMapper buildNoPKMapper(String schema,
220: String tableName, Connection connection) {
221: FIDMapper mapper;
222: mapper = new NullFIDMapper();
223:
224: return mapper;
225: }
226:
227: /**
228: * Builds a FID mapper for multi column public columns
229: *
230: * @param schema
231: * @param tableName
232: * @param connection
233: * @param colInfos
234: *
235: */
236: protected FIDMapper buildMultiColumnFIDMapper(String schema,
237: String tableName, Connection connection,
238: ColumnInfo[] colInfos) {
239: String[] colNames = new String[colInfos.length];
240: int[] colTypes = new int[colInfos.length];
241: int[] colSizes = new int[colInfos.length];
242: int[] colDecimalDigits = new int[colInfos.length];
243: boolean[] autoIncrement = new boolean[colInfos.length];
244:
245: for (int i = 0; i < colInfos.length; i++) {
246: ColumnInfo ci = colInfos[i];
247: colNames[i] = ci.colName;
248: colTypes[i] = ci.dataType;
249: colSizes[i] = ci.size;
250: colDecimalDigits[i] = ci.decimalDigits;
251: autoIncrement[i] = ci.autoIncrement;
252: }
253:
254: return new MultiColumnFIDMapper(schema, tableName, colNames,
255: colTypes, colSizes, colDecimalDigits, autoIncrement);
256: }
257:
258: protected ColumnInfo[] getPkColumnInfo(String catalog,
259: String schema, String typeName, Connection conn)
260: throws SchemaNotFoundException, DataSourceException {
261: ResultSet tableInfo = null;
262: ResultSet pkInfo = null;
263: boolean pkMetadataFound = false;
264:
265: try {
266: DatabaseMetaData dbMetaData = conn.getMetaData();
267:
268: Map pkMap = new LinkedHashMap();
269: pkInfo = dbMetaData.getPrimaryKeys(catalog, schema,
270: typeName);
271: pkMetadataFound = true;
272:
273: while (pkInfo.next()) {
274: ColumnInfo ci = new ColumnInfo();
275: ci.colName = pkInfo.getString("COLUMN_NAME");
276: ci.keySeq = pkInfo.getInt("KEY_SEQ");
277: pkMap.put(ci.colName, ci);
278: }
279:
280: tableInfo = dbMetaData.getColumns(catalog, schema,
281: typeName, "%");
282:
283: boolean tableInfoFound = false;
284:
285: while (tableInfo.next()) {
286: tableInfoFound = true;
287:
288: String columnName = tableInfo.getString("COLUMN_NAME");
289: ColumnInfo ci = (ColumnInfo) pkMap.get(columnName);
290:
291: if (ci != null) {
292: ci.dataType = tableInfo.getInt("DATA_TYPE");
293: ci.size = tableInfo.getInt("COLUMN_SIZE");
294: ci.decimalDigits = tableInfo
295: .getInt("DECIMAL_DIGITS");
296: ci.autoIncrement = isAutoIncrement(catalog, schema,
297: typeName, conn, tableInfo, columnName,
298: ci.dataType);
299: }
300: }
301:
302: if (!tableInfoFound) {
303: throw new SchemaNotFoundException(typeName);
304: }
305:
306: Collection columnInfos = pkMap.values();
307:
308: return (ColumnInfo[]) columnInfos
309: .toArray(new ColumnInfo[columnInfos.size()]);
310: } catch (SQLException sqlException) {
311: JDBCUtils
312: .close(conn, Transaction.AUTO_COMMIT, sqlException);
313: conn = null; // prevent finally block from reclosing
314:
315: if (pkMetadataFound) {
316: throw new DataSourceException(
317: "SQL Error building FeatureType for "
318: + typeName + " "
319: + sqlException.getMessage(),
320: sqlException);
321: } else {
322: throw new SchemaNotFoundException(typeName,
323: sqlException);
324: }
325: } finally {
326: JDBCUtils.close(tableInfo);
327: }
328: }
329:
330: /**
331: * Returns true if the specified column is auto-increment. This method is
332: * left protected so that specific datastore implementations can put their
333: * own logic, should the default one be ineffective or have bad
334: * performance. NOTE: the postgis subclass will call this with the
335: * columnname and table name pre-double-quoted! Other DB may have to do
336: * the same - please check your DB's documentation.
337: *
338: * @param catalog
339: * @param schema
340: * @param tableName
341: * @param conn
342: * @param tableInfo
343: * @param columnName
344: * @param dataType
345: *
346: *
347: * @throws SQLException
348: */
349: protected boolean isAutoIncrement(String catalog, String schema,
350: String tableName, Connection conn, ResultSet tableInfo,
351: String columnName, int dataType) throws SQLException {
352: // if it's not an integer type it can't be an auto increment type
353: if (!isIntegralType(dataType)) {
354: return false;
355: }
356:
357: // ok, it's an integer. To know if it's an auto-increment let's have a look at resultset metadata
358: // and try to force it to get just a single row for exploring the metadata
359: boolean autoIncrement = false;
360: Statement statement = null;
361: ResultSet rs = null;
362:
363: try {
364: statement = conn.createStatement();
365: statement.setFetchSize(1);
366: String query = "SELECT " + columnName + " FROM ";
367: if (schema != null) {
368: query = query + schema + "."; //the schema will default to public if not specified
369: }
370: query = query + tableName + " WHERE 0=1"; //DJB: the "where 0=1" will optimize if you have a lot of dead tuples
371: rs = statement.executeQuery(query);
372:
373: // if the WHERE 0=1 give any data store problems, just remove it
374: // and put a comment here as to why it caused problems.
375: java.sql.ResultSetMetaData rsInfo = rs.getMetaData();
376: autoIncrement = rsInfo.isAutoIncrement(1);
377: } finally {
378: JDBCUtils.close(statement);
379: JDBCUtils.close(rs);
380: }
381:
382: return autoIncrement;
383: }
384:
385: /**
386: * Returns true if the dataType for the column can serve as a primary key.
387: * Note that this now returns true for a DECIMAL type, because oracle
388: * Numbers are returned in jdbc as DECIMAL. This may cause errors in very
389: * rare cases somewhere down the line, but only if users do something
390: * incredibly silly like defining a primary key with a double.
391: *
392: * @param dataType DOCUMENT ME!
393: *
394: * @return DOCUMENT ME!
395: */
396: protected boolean isIntegralType(int dataType) {
397: return (dataType == Types.BIGINT)
398: || (dataType == Types.INTEGER)
399: || (dataType == Types.NUMERIC)
400: || (dataType == Types.SMALLINT)
401: || (dataType == Types.TINYINT)
402: || (dataType == Types.DECIMAL);
403: }
404:
405: protected boolean isTextType(int dataType) {
406: return (dataType == Types.VARCHAR) || (dataType == Types.CHAR)
407: || (dataType == Types.CLOB);
408: }
409:
410: /**
411: * @see org.geotools.data.jdbc.fidmapper.FIDMapperFactory#getMapper(org.geotools.feature.FeatureType)
412: */
413: public FIDMapper getMapper(FeatureType featureType) {
414: return new BasicFIDMapper("ID", 255, false);
415: }
416:
417: /**
418: * Simple class used as a struct to hold column informations used in this
419: * factory
420: *
421: * @author Andrea Aime
422: */
423: protected class ColumnInfo implements Comparable {
424: public String colName;
425: public int dataType;
426: public int size;
427: public int decimalDigits;
428: public boolean autoIncrement;
429: public int keySeq;
430:
431: /**
432: * @see java.lang.Comparable#compareTo(java.lang.Object)
433: */
434: public int compareTo(Object o) {
435: return keySeq - ((ColumnInfo) o).keySeq;
436: }
437:
438: public String toString() {
439: return "ColumnInfo, name(" + colName + "), type("
440: + dataType + ") size(" + size + ") decimalDigits("
441: + decimalDigits + ") autoIncrement("
442: + autoIncrement + ")";
443: }
444:
445: /**
446: * DOCUMENT ME!
447: *
448: * @return Returns the autoIncrement.
449: */
450: public boolean isAutoIncrement() {
451: return autoIncrement;
452: }
453:
454: /**
455: * DOCUMENT ME!
456: *
457: * @return Returns the colName.
458: */
459: public String getColName() {
460: return colName;
461: }
462:
463: /**
464: * DOCUMENT ME!
465: *
466: * @return Returns the dataType.
467: */
468: public int getDataType() {
469: return dataType;
470: }
471:
472: /**
473: * DOCUMENT ME!
474: *
475: * @return Returns the decimalDigits.
476: */
477: public int getDecimalDigits() {
478: return decimalDigits;
479: }
480:
481: /**
482: * DOCUMENT ME!
483: *
484: * @return Returns the size.
485: */
486: public int getSize() {
487: return size;
488: }
489: }
490: }
|