/* * Copyright 2005 The Apache Software Foundation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.ibatis.ibator.internal.db; import java.sql.DatabaseMetaData; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.StringTokenizer; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.ibatis.ibator.api.FullyQualifiedTable; import org.apache.ibatis.ibator.api.IntrospectedColumn; import org.apache.ibatis.ibator.api.IntrospectedTable; import org.apache.ibatis.ibator.api.JavaTypeResolver; import org.apache.ibatis.ibator.api.dom.java.FullyQualifiedJavaType; import org.apache.ibatis.ibator.config.ColumnOverride; import org.apache.ibatis.ibator.config.GeneratedKey; import org.apache.ibatis.ibator.config.IbatorContext; import org.apache.ibatis.ibator.config.PropertyRegistry; import org.apache.ibatis.ibator.config.TableConfiguration; import org.apache.ibatis.ibator.internal.IbatorObjectFactory; import org.apache.ibatis.ibator.internal.util.JavaBeansUtil; import org.apache.ibatis.ibator.internal.util.StringUtility; import org.apache.ibatis.ibator.internal.util.messages.Messages; import org.apache.ibatis.ibator.logging.Log; import org.apache.ibatis.ibator.logging.LogFactory; /** * * @author Jeff Butler */ public class DatabaseIntrospector { private DatabaseMetaData databaseMetaData; private JavaTypeResolver javaTypeResolver; private List warnings; private IbatorContext ibatorContext; private Log logger; public DatabaseIntrospector(IbatorContext ibatorContext, DatabaseMetaData databaseMetaData, JavaTypeResolver javaTypeResolver, List warnings) { super(); this.ibatorContext = ibatorContext; this.databaseMetaData = databaseMetaData; this.javaTypeResolver = javaTypeResolver; this.warnings = warnings; logger = LogFactory.getLog(getClass()); } private void calculatePrimaryKey(FullyQualifiedTable table, IntrospectedTable introspectedTable) { ResultSet rs = null; try { rs = databaseMetaData.getPrimaryKeys(table.getIntrospectedCatalog(), table.getIntrospectedSchema(), table.getIntrospectedTableName()); } catch (SQLException e) { closeResultSet(rs); warnings.add(Messages.getString("Warning.15")); //$NON-NLS-1$ return; } try { while (rs.next()) { String columnName = rs.getString("COLUMN_NAME"); //$NON-NLS-1$ introspectedTable.addPrimaryKeyColumn(columnName); } } catch (SQLException e) { // ignore the primary key if there's any error } finally { closeResultSet(rs); } } private void closeResultSet(ResultSet rs) { if (rs != null) { try { rs.close(); } catch (SQLException e) { // ignore ; } } } private void reportIntrospectionWarnings( IntrospectedTable introspectedTable, TableConfiguration tableConfiguration, FullyQualifiedTable table) { // make sure that every column listed in column overrides // actually exists in the table for (ColumnOverride columnOverride : tableConfiguration.getColumnOverrides()) { if (introspectedTable.getColumn(columnOverride.getColumnName()) == null) { warnings.add(Messages.getString("Warning.3", //$NON-NLS-1$ columnOverride.getColumnName(), table.toString())); } } // make sure that every column listed in ignored columns // actually exists in the table for (String string : tableConfiguration.getIgnoredColumnsInError()) { warnings.add(Messages.getString("Warning.4", //$NON-NLS-1$ string, table.toString())); } GeneratedKey generatedKey = tableConfiguration.getGeneratedKey(); if (generatedKey != null && introspectedTable.getColumn(generatedKey.getColumn()) == null) { if (generatedKey.isIdentity()) { warnings.add(Messages.getString("Warning.5", //$NON-NLS-1$ generatedKey.getColumn(), table.toString())); } else { warnings.add(Messages.getString("Warning.6", //$NON-NLS-1$ generatedKey.getColumn(), table.toString())); } } } /** * Returns a List that matches the specified table * configuration. * * @param tc * @return a list of introspected tables * @throws SQLException */ public List introspectTables(TableConfiguration tc) throws SQLException { // get the raw columns from the DB Map> columns = getColumns(tc); if (columns.isEmpty()) { warnings.add(Messages.getString("Warning.19", tc.getCatalog(), //$NON-NLS-1$ tc.getSchema(), tc.getTableName())); return null; } removeIgnoredColumns(tc, columns); calculateExtraColumnInformation(tc, columns); applyColumnOverrides(tc, columns); calculateIdentityColumns(tc, columns); List introspectedTables = calculateIntrospectedTables(tc, columns); // now introspectedTables has all the columns from all the // tables in the configuration. Do some validation... Iterator iter = introspectedTables.iterator(); while (iter.hasNext()) { IntrospectedTable introspectedTable = iter.next(); if (!introspectedTable.hasAnyColumns()) { // add warning that the table has no columns, remove from the list String warning = Messages.getString("Warning.1", introspectedTable.getFullyQualifiedTable().toString()); //$NON-NLS-1$ warnings.add(warning); iter.remove(); } else if (!introspectedTable.hasPrimaryKeyColumns() && !introspectedTable.hasBaseColumns()) { // add warning that the table has only BLOB columns, remove from the list String warning = Messages.getString("Warning.18", introspectedTable.getFullyQualifiedTable().toString()); //$NON-NLS-1$ warnings.add(warning); iter.remove(); } else { // now make sure that all columns called out in the configuration // actually exist reportIntrospectionWarnings(introspectedTable, tc, introspectedTable.getFullyQualifiedTable()); } } return introspectedTables; } /** * @param tc * @param columns */ private void removeIgnoredColumns(TableConfiguration tc, Map> columns) { for (Map.Entry> entry : columns.entrySet()) { Iterator tableColumns = (entry.getValue()).iterator(); while (tableColumns.hasNext()) { IntrospectedColumn introspectedColumn = tableColumns.next(); if (tc.isColumnIgnored(introspectedColumn.getActualColumnName())) { tableColumns.remove(); if (logger.isDebugEnabled()) { logger.debug(Messages.getString("Tracing.3", //$NON-NLS-1$ introspectedColumn.getActualColumnName(), entry.getKey().toString())); } } } } } private void calculateExtraColumnInformation(TableConfiguration tc, Map> columns) { Pattern pattern = null; String replaceString = null; if (tc.getColumnRenamingRule() != null) { pattern = Pattern.compile(tc.getColumnRenamingRule().getSearchString()); replaceString = tc.getColumnRenamingRule().getReplaceString(); replaceString = replaceString == null ? "" : replaceString; //$NON-NLS-1$ } for (Map.Entry> entry : columns.entrySet()) { for (IntrospectedColumn introspectedColumn : entry.getValue()) { String calculatedColumnName; if (pattern == null) { calculatedColumnName = introspectedColumn.getActualColumnName(); } else { Matcher matcher = pattern.matcher(introspectedColumn.getActualColumnName()); calculatedColumnName = matcher.replaceAll(replaceString); } if (StringUtility.isTrue(tc.getProperty(PropertyRegistry.TABLE_USE_ACTUAL_COLUMN_NAMES))) { introspectedColumn.setJavaProperty(JavaBeansUtil.getValidPropertyName(calculatedColumnName)); } else { introspectedColumn.setJavaProperty(JavaBeansUtil.getCamelCaseString(calculatedColumnName, false)); } FullyQualifiedJavaType fullyQualifiedJavaType = javaTypeResolver.calculateJavaType(introspectedColumn); if (fullyQualifiedJavaType != null) { introspectedColumn.setFullyQualifiedJavaType(fullyQualifiedJavaType); introspectedColumn.setJdbcTypeName(javaTypeResolver.calculateJdbcTypeName(introspectedColumn)); } else { // type cannot be resolved. Check for ignored or overridden boolean warn = true; if (tc.isColumnIgnored(introspectedColumn.getActualColumnName())) { warn = false; } ColumnOverride co = tc.getColumnOverride(introspectedColumn.getActualColumnName()); if (co != null) { if (StringUtility.stringHasValue(co.getJavaType()) && StringUtility.stringHasValue(co.getJavaType())) { warn = false; } } // if the type is not supported, then we'll report a warning if (warn) { introspectedColumn.setFullyQualifiedJavaType(FullyQualifiedJavaType.getObjectInstance()); introspectedColumn.setJdbcTypeName("OTHER"); //$NON-NLS-1$ String warning = Messages.getString("Warning.14", //$NON-NLS-1$ entry.getKey().toString(), introspectedColumn.getActualColumnName()); warnings.add(warning); } } if (ibatorContext.autoDelimitKeywords()) { if (SqlReservedWords.containsWord(introspectedColumn.getActualColumnName())) { introspectedColumn.setColumnNameDelimited(true); } } if (tc.isAllColumnDelimitingEnabled()) { introspectedColumn.setColumnNameDelimited(true); } } } } private void calculateIdentityColumns(TableConfiguration tc, Map> columns) { for (Map.Entry> entry : columns.entrySet()) { for (IntrospectedColumn introspectedColumn : entry.getValue()) { if (tc.getGeneratedKey() != null && tc.getGeneratedKey().isIdentity()) { if (introspectedColumn.isColumnNameDelimited()) { if (introspectedColumn.getActualColumnName().equals(tc.getGeneratedKey().getColumn())) { introspectedColumn.setIdentity(true); } } else { if (introspectedColumn.getActualColumnName().equalsIgnoreCase(tc.getGeneratedKey().getColumn())) { introspectedColumn.setIdentity(true); } } } else { introspectedColumn.setIdentity(false); } } } } private void applyColumnOverrides(TableConfiguration tc, Map> columns) { for (Map.Entry> entry : columns.entrySet()) { for (IntrospectedColumn introspectedColumn : entry.getValue()) { ColumnOverride columnOverride = tc.getColumnOverride(introspectedColumn .getActualColumnName()); if (columnOverride != null) { if (logger.isDebugEnabled()) { logger.debug(Messages.getString("Tracing.4", //$NON-NLS-1$ introspectedColumn.getActualColumnName(), entry.getKey().toString())); } if (StringUtility.stringHasValue(columnOverride.getJavaProperty())) { introspectedColumn.setJavaProperty(columnOverride.getJavaProperty()); } if (StringUtility.stringHasValue(columnOverride.getJavaType())) { introspectedColumn. setFullyQualifiedJavaType( new FullyQualifiedJavaType(columnOverride .getJavaType())); } if (StringUtility.stringHasValue(columnOverride.getJdbcType())) { introspectedColumn.setJdbcTypeName( columnOverride.getJdbcType()); } if (StringUtility.stringHasValue(columnOverride.getTypeHandler())) { introspectedColumn.setTypeHandler(columnOverride.getTypeHandler()); } if (columnOverride.isColumnNameDelimited()) { introspectedColumn.setColumnNameDelimited(true); } introspectedColumn.setProperties(columnOverride.getProperties()); } } } } /** * This method returns a Map> of columns * returned from the database introspection. * * @param tc * @return introspected columns * @throws SQLException */ private Map> getColumns(TableConfiguration tc) throws SQLException { String localCatalog; String localSchema; String localTableName; boolean delimitIdentifiers = tc.isDelimitIdentifiers() || StringUtility.stringContainsSpace(tc.getCatalog()) || StringUtility.stringContainsSpace(tc.getSchema()) || StringUtility.stringContainsSpace(tc.getTableName()); if (delimitIdentifiers) { localCatalog = tc.getCatalog(); localSchema = tc.getSchema(); localTableName = tc.getTableName(); } else if (databaseMetaData.storesLowerCaseIdentifiers()) { localCatalog = tc.getCatalog() == null ? null : tc.getCatalog() .toLowerCase(); localSchema = tc.getSchema() == null ? null : tc.getSchema() .toLowerCase(); localTableName = tc.getTableName() == null ? null : tc .getTableName().toLowerCase(); } else if (databaseMetaData.storesUpperCaseIdentifiers()) { localCatalog = tc.getCatalog() == null ? null : tc.getCatalog() .toUpperCase(); localSchema = tc.getSchema() == null ? null : tc.getSchema() .toUpperCase(); localTableName = tc.getTableName() == null ? null : tc .getTableName().toUpperCase(); } else { localCatalog = tc.getCatalog(); localSchema = tc.getSchema(); localTableName = tc.getTableName(); } if (tc.isWildcardEscapingEnabled()) { String escapeString = databaseMetaData.getSearchStringEscape(); StringBuilder sb = new StringBuilder(); StringTokenizer st; if (localSchema != null) { st = new StringTokenizer(localSchema, "_%", true); //$NON-NLS-1$ while (st.hasMoreTokens()) { String token = st.nextToken(); if (token.equals("_") //$NON-NLS-1$ || token.equals("%")) { //$NON-NLS-1$ sb.append(escapeString); } sb.append(token); } localSchema = sb.toString(); } sb.setLength(0); st = new StringTokenizer(localTableName, "_%", true); //$NON-NLS-1$ while (st.hasMoreTokens()) { String token = st.nextToken(); if (token.equals("_") //$NON-NLS-1$ || token.equals("%")) { //$NON-NLS-1$ sb.append(escapeString); } sb.append(token); } localTableName = sb.toString(); } Map> answer = new HashMap>(); if (logger.isDebugEnabled()) { String fullTableName = StringUtility.composeFullyQualifiedTableName(localCatalog, localSchema, localTableName, '.'); logger.debug(Messages.getString("Tracing.1", fullTableName)); //$NON-NLS-1$ } ResultSet rs = databaseMetaData.getColumns(localCatalog, localSchema, localTableName, null); while (rs.next()) { IntrospectedColumn introspectedColumn = IbatorObjectFactory.createIntrospectedColumn(ibatorContext); introspectedColumn.setTableAlias(tc.getAlias()); introspectedColumn.setJdbcType(rs.getInt("DATA_TYPE")); //$NON-NLS-1$ introspectedColumn.setLength(rs.getInt("COLUMN_SIZE")); //$NON-NLS-1$ introspectedColumn.setActualColumnName(rs.getString("COLUMN_NAME")); //$NON-NLS-1$ introspectedColumn.setNullable(rs.getInt("NULLABLE") == DatabaseMetaData.columnNullable); //$NON-NLS-1$ introspectedColumn.setScale(rs.getInt("DECIMAL_DIGITS")); //$NON-NLS-1$ introspectedColumn.setRemarks(rs.getString("REMARKS")); //$NON-NLS-1$ ActualTableName atn = new ActualTableName(rs.getString("TABLE_CAT"), //$NON-NLS-1$ rs.getString("TABLE_SCHEM"), //$NON-NLS-1$ rs.getString("TABLE_NAME")); //$NON-NLS-1$ List columns = answer.get(atn); if (columns == null) { columns = new ArrayList(); answer.put(atn, columns); } columns.add(introspectedColumn); if (logger.isDebugEnabled()) { logger.debug(Messages.getString("Tracing.2", //$NON-NLS-1$ introspectedColumn.getActualColumnName(), Integer.toString(introspectedColumn.getJdbcType()), atn.toString())); } } closeResultSet(rs); if (answer.size() > 1 && !StringUtility.stringContainsSQLWildcard(localSchema) && !StringUtility.stringContainsSQLWildcard(localTableName)) { // issue a warning if there is more than one table and // no wildcards were used ActualTableName inputAtn = new ActualTableName(tc.getCatalog(), tc.getSchema(), tc.getTableName()); StringBuilder sb = new StringBuilder(); boolean comma = false; for (ActualTableName atn : answer.keySet()) { if (comma) { sb.append(','); } else { comma = true; } sb.append(atn.toString()); } warnings.add(Messages.getString("Warning.25", //$NON-NLS-1$ inputAtn.toString(), sb.toString())); } return answer; } private List calculateIntrospectedTables(TableConfiguration tc, Map> columns) { boolean delimitIdentifiers = tc.isDelimitIdentifiers() || StringUtility.stringContainsSpace(tc.getCatalog()) || StringUtility.stringContainsSpace(tc.getSchema()) || StringUtility.stringContainsSpace(tc.getTableName()); List answer = new ArrayList(); for (Map.Entry> entry : columns.entrySet()) { ActualTableName atn = entry.getKey(); // we only use the returned catalog and schema if something was actually // specified on the table configuration. If something was returned // from the DB for these fields, but nothing was specified on the table // configuration, then some sort of DB default is being returned // and we don't want that in our SQL FullyQualifiedTable table = new FullyQualifiedTable( StringUtility.stringHasValue(tc.getCatalog()) ? atn.getCatalog() : null, StringUtility.stringHasValue(tc.getSchema()) ? atn.getSchema() : null, atn.getTableName(), tc.getDomainObjectName(), tc.getAlias(), StringUtility.isTrue(tc.getProperty(PropertyRegistry.TABLE_IGNORE_QUALIFIERS_AT_RUNTIME)), tc.getProperty(PropertyRegistry.TABLE_RUNTIME_CATALOG), tc.getProperty(PropertyRegistry.TABLE_RUNTIME_SCHEMA), tc.getProperty(PropertyRegistry.TABLE_RUNTIME_TABLE_NAME), delimitIdentifiers, ibatorContext); IntrospectedTable introspectedTable = IbatorObjectFactory.createIntrospectedTable(tc, table, ibatorContext); for (IntrospectedColumn introspectedColumn : entry.getValue()) { introspectedTable.addColumn(introspectedColumn); } calculatePrimaryKey(table, introspectedTable); answer.add(introspectedTable); } return answer; } }