/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2004-2008, Open Source Geospatial Foundation (OSGeo) * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ /* * 26-may-2005 D. Adler Added constructor with returnFIDColumnsAsAttributes. * Added accessors for ColumnInfo * 12-jul-2006 D. Adler GEOT-728 Refactor FIDMapper classes */ package org.geotools.data.jdbc.fidmapper; import java.io.IOException; import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.sql.Types; import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import org.geotools.data.DataSourceException; import org.geotools.data.SchemaNotFoundException; import org.geotools.data.Transaction; import org.geotools.data.jdbc.JDBCUtils; import org.opengis.feature.simple.SimpleFeatureType; /** * Default FID mapper that works with default FID mappers. * * <p> * May also be used a base class for more specific and feature rich factories * </p> * * @author Andrea Aime * * @source $URL$ * @deprecated scheduled for removal in 2.7, use classes in org.geotools.jdbc */ public class DefaultFIDMapperFactory implements FIDMapperFactory { /** The logger for the filter module. */ protected static final Logger LOGGER = org.geotools.util.logging.Logging.getLogger( "org.geotools.data.jdbc"); protected boolean returningTypedFIDMapper = true; /** Set if table FID columns are to be returned as business attributes. */ protected boolean returnFIDColumnsAsAttributes = false; /** * Constructs a DefaultFIDMapperFactory which will not return FID columns * as business attributes. */ public DefaultFIDMapperFactory() { } /** * Constructs a DefaultFIDMapperFactory with user specification of whether * to return FID columns as business attributes. * * @param returnFIDColumnsAsAttributes true if FID columns should be * returned as business attributes. */ public DefaultFIDMapperFactory(boolean returnFIDColumnsAsAttributes) { this.returnFIDColumnsAsAttributes = returnFIDColumnsAsAttributes; } /** * Setter for the flag controlling wther a "typed" fid mapper is returned. * @param returningTypedFIDMapper */ public void setReturningTypedFIDMapper(boolean returningTypedFIDMapper) { this.returningTypedFIDMapper = returningTypedFIDMapper; } /** * Getter for the flog controll wether a "typed" fid mapper should be returned. */ public boolean isReturningTypedFIDMapper() { return returningTypedFIDMapper; } /** * Gets the appropriate FIDMapper for the specified table. * * @param catalog * @param schema * @param tableName * @param connection the active database connection to get table key * information * * @return the appropriate FIDMapper for the specified table. * * @throws IOException if any error occurs. */ public FIDMapper getMapper(String catalog, String schema, String tableName, Connection connection) throws IOException { ColumnInfo[] colInfos = getPkColumnInfo(catalog, schema, tableName, connection); FIDMapper mapper = null; if (colInfos.length == 0) { mapper = buildNoPKMapper(schema, tableName, connection); } else if (colInfos.length > 1) { mapper = buildMultiColumnFIDMapper(schema, tableName, connection, colInfos); } else { ColumnInfo ci = colInfos[0]; mapper = buildSingleColumnFidMapper(schema, tableName, connection, ci); } if (mapper == null) { mapper = buildLastResortFidMapper(schema, tableName, connection, colInfos); if (mapper == null) { String msg = "Cannot map primary key to a FID mapper, primary key columns are:\n" + getColumnInfoList(colInfos); LOGGER.log(Level.SEVERE, msg); throw new IOException(msg); } } if (returningTypedFIDMapper && (mapper != null)) { return new TypedFIDMapper(mapper, tableName); } else { return mapper; } } /** * Retuns a List of column infos, nice for logging the column infos * leveraging the complete toString() method provided by lists * * @param colInfos * */ protected List getColumnInfoList(ColumnInfo[] colInfos) { ArrayList list = new ArrayList(); for (int i = 0; i < colInfos.length; i++) { list.add(colInfos[i]); } return list; } /** * Builds a FidMapper when every other tentative of building one fails. * This method is used as a last resort fall back, use it if you can * provide a FIDMapper that works on every kind of table, but it's usually * suboptimal. The default behaviour is to return no FID mapper at all. * * @param schema * @param tableName * @param connection * @param colInfos * */ protected FIDMapper buildLastResortFidMapper(String schema, String tableName, Connection connection, ColumnInfo[] colInfos) { return null; } /** * Builds a FID mapper based on a single column primary key. Default * version tries the auto-increment way, then a mapping on an {@link * MaxIncFIDMapper} type for numeric columns, and a plain {@link * BasicFIDMapper} of text based columns. * * @param schema * @param tableName * @param connection an open database connection. * @param ci the column information for the FID column. * * @return the appropriate FIDMapper. */ protected FIDMapper buildSingleColumnFidMapper(String schema, String tableName, Connection connection, ColumnInfo ci) { if (ci.autoIncrement) { return new AutoIncrementFIDMapper(schema, tableName, ci.colName, ci.dataType); } else if (isIntegralType(ci.dataType)) { return new MaxIncFIDMapper(schema, tableName, ci.colName, ci.dataType, this.returnFIDColumnsAsAttributes); } else { return new BasicFIDMapper(ci.colName, ci.size, this.returnFIDColumnsAsAttributes); } } /** * DOCUMENT ME! * * @param schema * @param tableName * @param connection * */ protected FIDMapper buildNoPKMapper(String schema, String tableName, Connection connection) { FIDMapper mapper; mapper = new NullFIDMapper(); return mapper; } /** * Builds a FID mapper for multi column public columns * * @param schema * @param tableName * @param connection * @param colInfos * */ protected FIDMapper buildMultiColumnFIDMapper(String schema, String tableName, Connection connection, ColumnInfo[] colInfos) { String[] colNames = new String[colInfos.length]; int[] colTypes = new int[colInfos.length]; int[] colSizes = new int[colInfos.length]; int[] colDecimalDigits = new int[colInfos.length]; boolean[] autoIncrement = new boolean[colInfos.length]; for (int i = 0; i < colInfos.length; i++) { ColumnInfo ci = colInfos[i]; colNames[i] = ci.colName; colTypes[i] = ci.dataType; colSizes[i] = ci.size; colDecimalDigits[i] = ci.decimalDigits; autoIncrement[i] = ci.autoIncrement; } return new MultiColumnFIDMapper(schema, tableName, colNames, colTypes, colSizes, colDecimalDigits, autoIncrement); } protected ColumnInfo[] getPkColumnInfo(String catalog, String schema, String typeName, Connection conn) throws SchemaNotFoundException, DataSourceException { ResultSet tableInfo = null; ResultSet pkInfo = null; boolean pkMetadataFound = false; try { DatabaseMetaData dbMetaData = conn.getMetaData(); Map pkMap = new LinkedHashMap(); pkInfo = dbMetaData.getPrimaryKeys(catalog, schema, typeName); pkMetadataFound = true; while (pkInfo.next()) { ColumnInfo ci = new ColumnInfo(); ci.colName = pkInfo.getString("COLUMN_NAME"); ci.keySeq = pkInfo.getInt("KEY_SEQ"); pkMap.put(ci.colName, ci); } tableInfo = dbMetaData.getColumns(catalog, schema, typeName, "%"); boolean tableInfoFound = false; while (tableInfo.next()) { tableInfoFound = true; String columnName = tableInfo.getString("COLUMN_NAME"); ColumnInfo ci = (ColumnInfo) pkMap.get(columnName); if (ci != null) { ci.dataType = tableInfo.getInt("DATA_TYPE"); ci.typeName = tableInfo.getString("TYPE_NAME"); ci.size = tableInfo.getInt("COLUMN_SIZE"); ci.decimalDigits = tableInfo.getInt("DECIMAL_DIGITS"); ci.autoIncrement = isAutoIncrement(catalog, schema, typeName, conn, tableInfo, columnName, ci.dataType); } } if (!tableInfoFound) { throw new SchemaNotFoundException(typeName); } Collection columnInfos = pkMap.values(); return (ColumnInfo[]) columnInfos.toArray(new ColumnInfo[columnInfos .size()]); } catch (SQLException sqlException) { JDBCUtils.close(conn, Transaction.AUTO_COMMIT, sqlException); conn = null; // prevent finally block from reclosing if (pkMetadataFound) { throw new DataSourceException( "SQL Error building FeatureType for " + typeName + " " + sqlException.getMessage(), sqlException); } else { throw new SchemaNotFoundException(typeName, sqlException); } } finally { JDBCUtils.close(tableInfo); } } /** * Returns true if the specified column is auto-increment. This method is * left protected so that specific datastore implementations can put their * own logic, should the default one be ineffective or have bad * performance. NOTE: the postgis subclass will call this with the * columnname and table name pre-double-quoted! Other DB may have to do * the same - please check your DB's documentation. * * @param catalog * @param schema * @param tableName * @param conn * @param tableInfo * @param columnName * @param dataType * * * @throws SQLException */ protected boolean isAutoIncrement(String catalog, String schema, String tableName, Connection conn, ResultSet tableInfo, String columnName, int dataType) throws SQLException { // if it's not an integer type it can't be an auto increment type if (!isIntegralType(dataType)) { return false; } // ok, it's an integer. To know if it's an auto-increment let's have a look at resultset metadata // and try to force it to get just a single row for exploring the metadata boolean autoIncrement = false; Statement statement = null; ResultSet rs = null; try { statement = conn.createStatement(); statement.setFetchSize(1); String query = "SELECT " + columnName + " FROM "; if (schema != null) { query = query + schema + "."; //the schema will default to public if not specified } query = query + tableName + " WHERE 0=1"; //DJB: the "where 0=1" will optimize if you have a lot of dead tuples rs = statement.executeQuery(query); // if the WHERE 0=1 give any data store problems, just remove it // and put a comment here as to why it caused problems. java.sql.ResultSetMetaData rsInfo = rs.getMetaData(); autoIncrement = rsInfo.isAutoIncrement(1); } finally { JDBCUtils.close(statement); JDBCUtils.close(rs); } return autoIncrement; } /** * Returns true if the dataType for the column can serveget as a primary key. * Note that this now returns true for a DECIMAL type, because oracle * Numbers are returned in jdbc as DECIMAL. This may cause errors in very * rare cases somewhere down the line, but only if users do something * incredibly silly like defining a primary key with a double. * * @param dataType DOCUMENT ME! * * @return DOCUMENT ME! */ protected boolean isIntegralType(int dataType) { return (dataType == Types.BIGINT) || (dataType == Types.INTEGER) || (dataType == Types.NUMERIC) || (dataType == Types.SMALLINT) || (dataType == Types.TINYINT) || (dataType == Types.DECIMAL); } protected boolean isTextType(int dataType) { return (dataType == Types.VARCHAR) || (dataType == Types.CHAR) || (dataType == Types.CLOB); } /** * @see org.geotools.data.jdbc.fidmapper.FIDMapperFactory#getMapper(org.geotools.feature.FeatureType) */ public FIDMapper getMapper(SimpleFeatureType featureType) { return new BasicFIDMapper("ID", 255, false); } /** * Simple class used as a struct to hold column informations used in this * factory * * @author Andrea Aime */ protected class ColumnInfo implements Comparable { public String colName; public int dataType; public String typeName; public int size; public int decimalDigits; public boolean autoIncrement; public int keySeq; /** * @see java.lang.Comparable#compareTo(java.lang.Object) */ public int compareTo(Object o) { return keySeq - ((ColumnInfo) o).keySeq; } public String toString() { return "ColumnInfo, name(" + colName + "), type(" + dataType + ") size(" + size + ") decimalDigits(" + decimalDigits + ") autoIncrement(" + autoIncrement + ")"; } /** * DOCUMENT ME! * * @return Returns the autoIncrement. */ public boolean isAutoIncrement() { return autoIncrement; } /** * DOCUMENT ME! * * @return Returns the colName. */ public String getColName() { return colName; } /** * DOCUMENT ME! * * @return Returns the dataType. */ public int getDataType() { return dataType; } public String getTypeName() { return typeName; } /** * DOCUMENT ME! * * @return Returns the decimalDigits. */ public int getDecimalDigits() { return decimalDigits; } /** * DOCUMENT ME! * * @return Returns the size. */ public int getSize() { return size; } } }