blob: 15a8f22cf0d0d65a170f6ec2f8260425ff7487e5 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.empire.db.validation;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.empire.commons.StringUtils;
import org.apache.empire.data.DataType;
import org.apache.empire.db.DBColumn;
import org.apache.empire.db.DBCommandExpr;
import org.apache.empire.db.DBDatabase;
import org.apache.empire.db.DBRelation;
import org.apache.empire.db.DBRelation.DBReference;
import org.apache.empire.db.DBRowSet;
import org.apache.empire.db.DBTable;
import org.apache.empire.db.DBTableColumn;
import org.apache.empire.db.DBView;
import org.apache.empire.exceptions.InternalException;
import org.apache.empire.exceptions.NotSupportedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class DBModelParser
{
protected static final Logger log = LoggerFactory.getLogger(DBModelParser.class);
/**
* The remote Database
* This will be populated by the ModelChecker
*/
private static class RemoteDatabase extends DBDatabase
{
/*
* Will be dynamically populated
*/
}
private static class RemoteView extends DBView
{
public RemoteView(String name, DBDatabase db)
{
super(name, db);
}
public DBColumn addColumn(String columnName, DataType dataType, double size, boolean dummy)
{
return super.addColumn(columnName, dataType, size);
}
@Override
public DBCommandExpr createCommand()
{
throw new NotSupportedException(this, "createCommand");
}
}
protected final String catalog;
protected final String schema;
protected final String remoteName;
protected DBDatabase remoteDb = null; /* will be recreated on every call to checkModel */
protected final Map<String, DBRowSet> tableMap = new HashMap<String, DBRowSet>();
private String standardIdentityColumnName = null;
private String standardTimestampColumnName = null;
/**
* Creates a new Model Checker
* @param catalog
* @param schema
*/
public DBModelParser(String catalog, String schema)
{
this.catalog = catalog;
this.schema = schema;
// set origin
StringBuilder b = new StringBuilder();
if (StringUtils.isNotEmpty(catalog))
b.append(catalog);
if (StringUtils.isNotEmpty(schema))
{ if (b.length()>0)
b.append(".");
b.append(schema);
}
if (b.length()==0)
b.append("[Unknown]");
this.remoteName = b.toString();
}
public String getCatalog()
{
return catalog;
}
public String getSchema()
{
return schema;
}
public void setStandardIdentityColumnName(String standardIdentityColumnName)
{
this.standardIdentityColumnName = standardIdentityColumnName;
}
public void setStandardTimestampColumnName(String standardTimestampColumnName)
{
this.standardTimestampColumnName = standardTimestampColumnName;
}
/**
* Returns the RemoteDatabase
* Only available after parseModel() is called
* @return the remote Database
*/
public DBDatabase getDatabase()
{
return remoteDb;
}
/**
* This method is used to parse the populate the remote database
* @param conn the connection for retrieving the remote database metadata
*/
public void parseModel(Connection conn)
{
try
{ // create remote db instance
remoteDb = createRemoteDatabase();
// populate
DatabaseMetaData dbMeta = conn.getMetaData();
populateRemoteDatabase(dbMeta);
}
catch (SQLException e)
{
log.error("checkModel failed for {}", remoteName);
throw new InternalException(e);
}
finally
{ // cleanup
tableMap.clear();
}
}
protected void populateRemoteDatabase(DatabaseMetaData dbMeta)
throws SQLException
{
// collect tables & views
int count = collectTablesAndViews(dbMeta, null);
log.info("{} tables and views added for schema \"{}\"", count, remoteName);
// Collect all columns
count = collectColumns(dbMeta);
log.info("{} columns added for schema \"{}\"", count, remoteName);
// Collect PKs
count = collectPrimaryKeys(dbMeta);
log.info("{} primary keys added for schema \"{}\"", count, remoteName);
// Collect FKs
count = collectForeignKeys(dbMeta);
log.info("{} foreign keys added for schema \"{}\"", count, remoteName);
}
/**
* Checks if the tableName belongs to a system or hidden table
* @param tableName the table name
* @param tableMeta the table meta
* @return true if the table is hidden or false otherwise
*/
protected boolean isSystemTable(String tableName, ResultSet tableMeta)
{ // system tables containing a '$' symbol (required for Oracle!)
return (tableName.indexOf('$') >= 0);
}
/**
* collects table and view information from database meta data
* @param dbMeta the database meta data
* @param tablePattern the table pattern
* @throws SQLException
*/
protected int collectTablesAndViews(DatabaseMetaData dbMeta, String tablePattern)
throws SQLException
{
tableMap.clear();
ResultSet dbTables = dbMeta.getTables(catalog, schema, tablePattern, new String[] { "TABLE", "VIEW" });
try {
// ResultSet dbTables = dbMeta.getTables("PATOOL", "DBO", null, new String[] { "TABLE", "VIEW" });
int count = 0;
while (dbTables.next())
{
String tableName = dbTables.getString("TABLE_NAME");
String tableType = dbTables.getString("TABLE_TYPE");
if (isSystemTable(tableName, dbTables))
{ // ignore system table
DBModelParser.log.info("Ignoring system table " + tableName);
continue;
}
if ("VIEW".equalsIgnoreCase(tableType))
addView(tableName);
else
addTable(tableName);
count++;
}
return count;
} finally {
dbTables.close();
}
}
/**
* collects column information from database meta data for each table
*/
protected int collectColumns(DatabaseMetaData dbMeta)
throws SQLException
{
int count = 0;
for (DBRowSet t : getTables())
{
ResultSet dbColumns = dbMeta.getColumns(catalog, schema, t.getName(), null);
try {
while (dbColumns.next())
{ // add the column
addColumn(t, dbColumns);
count++;
}
} finally {
dbColumns.close();
}
}
return count;
}
/**
* collects column information from database meta data for whole schema
*/
protected int collectColumns(DatabaseMetaData dbMeta, String tablePattern)
throws SQLException
{
ResultSet dbColumns = dbMeta.getColumns(catalog, schema, tablePattern, null);
try {
int count = 0;
while (dbColumns.next())
{
String tableName = dbColumns.getString("TABLE_NAME");
DBRowSet t = getTable(tableName);
if (t == null)
{ log.error("Table not found: {}", tableName);
continue;
}
addColumn(t, dbColumns);
count++;
}
return count;
} finally {
dbColumns.close();
}
}
/**
* collects primary key information from database meta data
* @param dbMeta the database meta data
* @throws SQLException
*/
protected int collectPrimaryKeys(DatabaseMetaData dbMeta)
throws SQLException
{
int count = 0;
for (DBRowSet rs : getTables())
{
if (!(rs instanceof DBTable))
continue; // not a table
// read pk
DBTable t = (DBTable)rs;
List<String> pkCols = new ArrayList<String>();
ResultSet primaryKeys = dbMeta.getPrimaryKeys(catalog, schema, t.getName());
try {
while (primaryKeys.next())
{
pkCols.add(primaryKeys.getString("COLUMN_NAME"));
}
if (pkCols.size() > 0)
{
DBColumn[] keys = new DBColumn[pkCols.size()];
for (int i = 0; i < keys.length; i++)
{
keys[i] = t.getColumn(pkCols.get(i).toUpperCase());
}
t.setPrimaryKey(keys);
count++;
}
} finally {
primaryKeys.close();
}
}
return count;
}
/**
* collects foreign key information from database meta data
* @param dbMeta the database meta data
* @throws SQLException
*/
protected int collectForeignKeys(DatabaseMetaData dbMeta)
throws SQLException
{
int count = 0;
for (DBRowSet t : getTables())
{
if (t instanceof DBTable)
count += collectForeignKeys(dbMeta, t.getName());
}
return count;
}
/**
* collects foreign key information from database meta data
* @param dbMeta the database meta data
* @param tablePattern the table pattern
* @throws SQLException
*/
protected int collectForeignKeys(DatabaseMetaData dbMeta, String tablePattern)
throws SQLException
{
ResultSet foreignKeys = dbMeta.getImportedKeys(catalog, schema, tablePattern);
try {
int count = 0;
while (foreignKeys.next())
{
String fkTable = foreignKeys.getString("FKTABLE_NAME");
String fkColumn = foreignKeys.getString("FKCOLUMN_NAME");
String pkTable = foreignKeys.getString("PKTABLE_NAME");
String pkColumn = foreignKeys.getString("PKCOLUMN_NAME");
String fkName = foreignKeys.getString("FK_NAME");
DBTableColumn c1 = (DBTableColumn) getTable(fkTable).getColumn(fkColumn.toUpperCase());
DBTableColumn c2 = (DBTableColumn) getTable(pkTable).getColumn(pkColumn.toUpperCase());
DBRelation relation = this.remoteDb.getRelation(fkName);
if (relation == null)
{
addRelation(fkName, c1.referenceOn(c2));
count++;
}
else
{ // get existing references
DBReference[] refs = relation.getReferences();
// remove old
this.remoteDb.removeRelation(relation);
DBReference[] newRefs = new DBReference[refs.length + 1];
// copy existing
DBReference newRef = new DBReference(c1, c2);
for (int i = 0; i < refs.length; i++)
{
newRefs[i] = refs[i];
}
newRefs[newRefs.length - 1] = newRef;
addRelation(fkName, newRefs);
}
}
return count;
} finally {
foreignKeys.close();
}
}
/*
* internal methods
*/
protected final Collection<DBRowSet> getTables()
{
return this.tableMap.values();
}
protected final DBRowSet getTable(String tableName)
{
return this.tableMap.get(tableName.toUpperCase());
}
protected DBDatabase createRemoteDatabase()
{
return new RemoteDatabase();
}
protected void addTable(String tableName)
{
this.tableMap.put(tableName.toUpperCase(), new DBTable(tableName, this.remoteDb));
}
protected void addView(String viewName)
{
this.tableMap.put(viewName.toUpperCase(), new RemoteView(viewName, this.remoteDb));
}
protected void addRelation(String relName, DBReference... references)
{
this.remoteDb.addRelation(relName, references);
}
protected DBColumn addColumn(DBRowSet t, ResultSet rs)
throws SQLException
{
String name = rs.getString("COLUMN_NAME");
DataType empireType = getEmpireDataType(rs.getInt("DATA_TYPE"));
// get Size
double colSize = getColumnSize(empireType, rs);
// mandatory field?
boolean required = isColumnRequired(rs);
Object defaultValue = getColumnDefault(rs);
// Now add the column to table / view
DBColumn col;
if (t instanceof DBTable)
{ // check Identity and Timestamp
boolean timestampColumn = false;
if (empireType==DataType.INTEGER && isIdentityColumn(rs))
empireType= DataType.AUTOINC;
if (empireType.isDate() && (timestampColumn=isTimestampColumn(rs)))
empireType= DataType.TIMESTAMP;
// Add Column
col = ((DBTable)t).addColumn(name, empireType, colSize, required, defaultValue);
// Set Timestamp
if (empireType==DataType.AUTOINC)
((DBTable) t).setPrimaryKey(col);
if (timestampColumn)
t.setTimestampColumn(col);
// info
log.debug("Added table column {}.{} of type {}", t.getName(), name, empireType);
}
else if (t instanceof DBView)
{
col = ((RemoteView)t).addColumn(name, empireType, colSize, false);
log.debug("Added view column {}.{} of type {}", t.getName(), name, empireType);
}
else
{ // Unknown type
log.error("Unknown Object Type {}", t.getClass().getName());
col = null;
}
// done
return col;
}
protected double getColumnSize(DataType empireType, ResultSet rs)
throws SQLException
{
double colSize = rs.getInt("COLUMN_SIZE");
if (empireType == DataType.DECIMAL || empireType == DataType.FLOAT)
{ // decimal digits
int decimalDig = rs.getInt("DECIMAL_DIGITS");
if (decimalDig > 0)
{ try
{ // concat and parse
int intSize = rs.getInt("COLUMN_SIZE");
colSize = Double.parseDouble(String.valueOf(intSize) + '.' + decimalDig);
}
catch (Exception e)
{
String name = rs.getString("COLUMN_NAME");
DBModelParser.log.error("Failed to parse decimal digits for column " + name);
}
}
// make integer?
if (colSize < 1.0d)
{ // Turn into an integer
empireType = DataType.INTEGER;
}
}
else if (empireType.isDate())
{
colSize = 0.0;
}
else if (empireType == DataType.INTEGER || empireType == DataType.CLOB || empireType == DataType.BLOB)
{
colSize = 0.0;
}
return colSize;
}
protected boolean isColumnRequired(ResultSet rs)
throws SQLException
{
return rs.getString("IS_NULLABLE").equalsIgnoreCase("NO");
}
protected Object getColumnDefault(ResultSet rs)
throws SQLException
{
return rs.getString("COLUMN_DEF");
}
protected boolean isIdentityColumn(ResultSet rs)
{ try {
return (standardIdentityColumnName!=null &&
standardIdentityColumnName.equalsIgnoreCase(rs.getString("COLUMN_NAME")));
} catch(SQLException e) {
return false;
}
}
protected boolean isTimestampColumn(ResultSet rs)
{ try {
return (standardTimestampColumnName!=null &&
standardTimestampColumnName.equalsIgnoreCase(rs.getString("COLUMN_NAME")));
} catch(SQLException e) {
return false;
}
}
protected DataType getEmpireDataType(int sqlType)
{
DataType empireType = DataType.UNKNOWN;
switch (sqlType)
{
case Types.INTEGER:
case Types.SMALLINT:
case Types.TINYINT:
case Types.BIGINT:
empireType = DataType.INTEGER;
break;
case Types.VARCHAR:
case Types.NVARCHAR:
empireType = DataType.VARCHAR;
break;
case Types.DATE:
empireType = DataType.DATE;
break;
case Types.TIME:
empireType = DataType.TIME;
break;
case Types.TIMESTAMP:
empireType = DataType.DATETIME;
break;
case Types.CHAR:
case Types.NCHAR:
empireType = DataType.CHAR;
break;
case Types.DOUBLE:
case Types.FLOAT:
case Types.REAL:
empireType = DataType.FLOAT;
break;
case Types.DECIMAL:
case Types.NUMERIC:
empireType = DataType.DECIMAL;
break;
case Types.BIT:
case Types.BOOLEAN:
empireType = DataType.BOOL;
break;
case Types.CLOB:
case Types.LONGVARCHAR:
case Types.LONGNVARCHAR:
empireType = DataType.CLOB;
break;
case Types.BINARY:
case Types.VARBINARY:
case Types.LONGVARBINARY:
case Types.BLOB:
empireType = DataType.BLOB;
break;
default:
empireType = DataType.UNKNOWN;
DBModelParser.log.warn("SQL column type " + sqlType + " not supported.");
}
DBModelParser.log.debug("Mapping date type " + String.valueOf(sqlType) + " to " + empireType);
return empireType;
}
}