blob: 5fc8a5a25521fd289dd0835f206867c9e684ab69 [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.codegen;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Types;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;
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.DBTable;
import org.apache.empire.db.DBTableColumn;
import org.apache.empire.db.DBView;
import org.apache.empire.db.DBView.DBViewColumn;
import org.apache.empire.db.codegen.util.DBUtil;
import org.apache.empire.exceptions.ItemNotFoundException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class is used to create a in memory DBDatabase of a given SQLConnection
* and Configuration
*
* @author Benjamin Venditti
*/
public class CodeGenParser {
public static class InMemoryDatabase extends DBDatabase {
private static final long serialVersionUID = 1L;
@Override
public List<DBRelation> getRelations()
{
return relations;
}
}
public static class InMemoryView extends DBView {
private final static long serialVersionUID = 1L;
public InMemoryView(String name, DBDatabase db) {
super(name, db);
}
public DBViewColumn addCol(String columnName,DataType dataType)
{
return addColumn(columnName, dataType);
}
@Override
public DBCommandExpr createCommand() {
return null;
}
}
private static final Logger log = LoggerFactory.getLogger(CodeGenParser.class);
private DatabaseMetaData dbMeta;
private Connection con;
private CodeGenConfig config;
/**
* create a empty in memory Database and populates it
*/
public CodeGenParser(CodeGenConfig config) {
this.config = config;
}
/**
* returns the populated DBDatabase
*/
public DBDatabase loadDbModel() {
DBDatabase db = new InMemoryDatabase();
try {
con = openJDBCConnection(config);
populateDatabase(db);
}
catch (SQLException e)
{
throw new RuntimeException("Unable to read database metadata: " + e.getMessage(), e);
}
finally
{
DBUtil.close(con, log);
}
return db;
}
/**
* Opens and returns a JDBC-Connection.
* JDBC url, user and password for the connection are obained from the SampleConfig bean
* Please use the config.xml file to change connection params.
*/
protected Connection openJDBCConnection(CodeGenConfig config) throws SQLException{
log.info("Connecting to Database'" + config.getJdbcURL() + "' / User=" + config.getJdbcUser());
Connection conn = null;
try {
Class.forName(config.getJdbcClass()).newInstance();
}catch(Exception ex){
throw new SQLException("Could not load database driver: " + config.getJdbcClass());
}
conn = DriverManager.getConnection(config.getJdbcURL(), config.getJdbcUser(), config.getJdbcPwd());
log.info("Connected successfully");
return conn;
}
/**
* Returns whether to add the table or ignore it
* @param tableName
*/
protected boolean isPopulateTable(String tableName)
{
if (tableName.indexOf('$') >= 0)
return false;
return true;
}
/**
* Returns whether to add the column or ignore it
* @param columnName
*/
protected boolean isPopulateColumn(String columnName)
{
return true;
}
/**
* Queries the metadata of the database for tables and vies and populates the
* database with those
* @throws SQLException
*/
protected void populateDatabase(DBDatabase db) throws SQLException {
ResultSet tables = null;
ArrayList<String> populatedTables=new ArrayList<String>();
try{
this.dbMeta = con.getMetaData();
String[] tablePatterns = {null}; // Could be null, so start that way.
if(config.getDbTablePattern() != null)
tablePatterns = config.getDbTablePattern().split(","); // Support a comma separated list of table patterns (i.e. specify a list of table names in the config file).
int tableCount = 0; // Moved to be outside table pattern loop.
int viewCount = 0;
for(String pattern : tablePatterns){
// types
String[] types = config.isGenerateViews() ? new String[] { "TABLE", "VIEW" }
: new String[] { "TABLE" };
// Get table metadata
tables = dbMeta.getTables(
config.getDbCatalog(),
config.getDbSchema(),
pattern == null ? pattern: pattern.trim(),
types);
// Add all tables and views
while (tables.next()) {
String tableName = tables.getString("TABLE_NAME");
if (!isPopulateTable(tableName)) {
log.info("Ignoring table " + tableName);
continue;
}
// show
String tableType = tables.getString("TABLE_TYPE");
String tableSchema = tables.getString("TABLE_SCHEM");
String templ = StringUtils.isNotEmpty(tableSchema) ? "{0}: {1} ({2})" : "{0}: {1}";
log.info(MessageFormat.format(templ, tableType, tableName, tableSchema));
// Table or View
if(tableType.equalsIgnoreCase("VIEW")){
InMemoryView view = new InMemoryView(tableName, db);
populateView(view);
viewCount++;
} else {
DBTable table = new DBTable(tableName, db);
populateTable(table);
populatedTables.add(tableName);
tableCount++;
}
}
}
// Add all relations
gatherRelations(db, dbMeta, populatedTables);
if (tableCount==0 && viewCount==0) {
// getTables returned no result
String info = "catalog="+config.getDbCatalog();
info += "/ schema="+config.getDbSchema();
info += "/ pattern="+config.getDbTablePattern();
log.warn("DatabaseMetaData.getTables() returned no tables or views! Please check parameters: "+info);
log.info("Available catalogs: " + getCatalogs(dbMeta));
log.info("Available schemata: " + getSchemata(dbMeta));
}
} finally {
DBUtil.close(tables, log);
}
}
protected void gatherRelations(DBDatabase db, DatabaseMetaData dbMeta, ArrayList<String> tables) throws SQLException{
ResultSet relations = null;
String fkTableName, pkTableName, fkColName, pkColName, relName;
DBTableColumn fkCol, pkCol;
DBTable fkTable, pkTable;
DBColumn col;
// Add all Relations
for (String tableName :tables) {
// check for foreign-keys
relations = dbMeta.getImportedKeys(config.getDbCatalog(), config .getDbSchema(), tableName);
while (relations.next()) {
pkCol=fkCol=null;
fkTableName=relations.getString("FKTABLE_NAME");
pkTableName=relations.getString("PKTABLE_NAME");
fkColName=relations.getString("FKCOLUMN_NAME");
pkColName=relations.getString("PKCOLUMN_NAME");
// Detect relation name
relName=relations.getString("FK_NAME");
if (StringUtils.isEmpty(relName))
relName=fkTableName+"."+fkColName+"-"+pkTableName+"."+pkColName;
pkTable = db.getTable(pkTableName);
fkTable = db.getTable(fkTableName);
// check if both tables really exist in the model
if(pkTable==null || fkTable==null){
log.error("Unable to add the relation \""+relName+"\"! One of the tables could not be found.");
continue;
}
col=pkTable.getColumn(pkColName);
if(col instanceof DBTableColumn)
pkCol = (DBTableColumn) col;
col=fkTable.getColumn(fkColName);
if(col instanceof DBTableColumn)
fkCol = (DBTableColumn) col;
// check if both columns really exist in the model
if(fkCol==null || pkCol==null){
log.error("Unable to add the relation \""+relName+"\"! One of the columns could not be found.");
continue;
}
// add the relation
DBRelation.DBReference reference = fkCol.referenceOn(pkCol);
DBRelation.DBReference[] refs = null;
DBRelation r = db.getRelation(relName);
if (r!=null) {
DBRelation.DBReference[] refsOld = r.getReferences();
refs = new DBRelation.DBReference[refsOld.length+1];
int i=0;
for (; i<refsOld.length; i++)
refs[i]=refsOld[i];
refs[i]=reference;
// remove old relation
log.warn("Duplicate relation {}", r.getName());
db.getRelations().remove(r);
} else {
refs = new DBRelation.DBReference[] { reference };
}
// Add a new relation
db.addRelation(relName, refs);
log.info("Added relation (FK-PK): "+relName);
}
}
}
protected String getCatalogs(DatabaseMetaData dbMeta) throws SQLException {
String retVal = "";
ResultSet rs = dbMeta.getCatalogs();
while (rs.next()) {
retVal += rs.getString("TABLE_CAT") + ", ";
}
if(retVal.length()>2)
retVal=retVal.substring(0,retVal.length()-2);
return retVal;
}
protected String getSchemata(DatabaseMetaData dbMeta) throws SQLException {
String retVal = "";
ResultSet rs = dbMeta.getSchemas();
while (rs.next()) {
retVal += rs.getString("TABLE_SCHEM") + ", ";
}
if(retVal.length()>2)
retVal=retVal.substring(0,retVal.length()-2);
return retVal;
}
/**
* queries the metadata for columns of a specific table and populates the
* table with that information
* @throws SQLException
*/
protected void populateTable(DBTable t) throws SQLException {
List<String> pkCols = this.findPkColumns(t.getName());
String lockColName = config.getTimestampColumn();
DBColumn[] keys = new DBColumn[pkCols.size()];
ResultSet rs = null;
try {
rs = dbMeta.getColumns(config.getDbCatalog(), config.getDbSchema(), t.getName(), null);
int i=0;
while (rs.next()) {
DBTableColumn c = addColumn(t, rs, lockColName);
if (c==null)
continue;
// check if it is a KeyColumn
if (pkCols.contains(c.getName()))
keys[i++] = c;
}
// Check whether all key columns have been set
for (i=0; i<keys.length; i++)
if (keys[i]==null){
throw new ItemNotFoundException(pkCols.get(i));
}
if(keys.length > 0){
t.setPrimaryKey(keys);
}
} finally {
DBUtil.close(rs, log);
}
}
/**
* queries the metadata for columns of a specific table and populates the
* table with that information
* @throws SQLException
*/
protected void populateView(InMemoryView v) throws SQLException {
ResultSet rs = null;
try {
rs = dbMeta.getColumns(config.getDbCatalog(), config.getDbSchema(),
v.getName(), null);
while (rs.next()) {
addColumn(v, rs);
}
} finally {
DBUtil.close(rs, log);
}
}
/**
* Returns a list of column names that define the primarykey of the given
* table.
* @throws SQLException
*/
protected List<String> findPkColumns(String tableName) throws SQLException {
List<String> cols = new ArrayList<String>();
ResultSet rs = null;
try {
rs = dbMeta.getPrimaryKeys(config.getDbCatalog(), config
.getDbSchema(), tableName);
while (rs.next()) {
cols.add(rs.getString("COLUMN_NAME"));
}
} finally {
DBUtil.close(rs, log);
}
return cols;
}
/**
* Adds DBColumn object to the given DBTable. The DBColumn is created from
* the given ResultSet
*/
protected DBTableColumn addColumn(DBTable t, ResultSet rs, String lockColName)
throws SQLException {
String name = rs.getString("COLUMN_NAME");
if (!isPopulateColumn(name)) {
log.info("Ignoring column " + name);
return null;
}
DataType empireType = getEmpireDataType(rs.getInt("DATA_TYPE"));
double colSize = getColumnSize(empireType, rs.getInt("DATA_TYPE"), rs.getInt("COLUMN_SIZE"));
// Timestamp column
if (empireType==DataType.DATETIME && StringUtils.isNotEmpty(lockColName) && name.equalsIgnoreCase(lockColName))
{
empireType=DataType.TIMESTAMP;
colSize=0;
}
// Number column
if (empireType==DataType.DECIMAL || empireType==DataType.FLOAT)
{ // decimal digits
int decimalDig = rs.getInt("DECIMAL_DIGITS");
if (decimalDig>0)
{ // parse
try {
int intSize = rs.getInt("COLUMN_SIZE");
colSize = Double.parseDouble(String.valueOf(intSize)+'.'+decimalDig);
} catch(Exception e) {
log.error("Failed to parse decimal digits for column "+name);
}
}
// make integer?
if (colSize<1.0d)
{ // Turn into an integer
empireType=DataType.INTEGER;
}
}
// mandatory field?
boolean required = false;
String defaultValue = rs.getString("COLUMN_DEF");
if (rs.getString("IS_NULLABLE").equalsIgnoreCase("NO"))
required = true;
// The following is a hack for MySQL which currently gets sent a string "CURRENT_TIMESTAMP" from the Empire-db driver for MySQL.
// This will avoid the driver problem because CURRENT_TIMESTAMP in the db will just do the current datetime.
// Essentially, Empire-db needs the concept of default values of one type that get mapped to another.
// In this case, MySQL "CURRENT_TIMESTAMP" for Types.TIMESTAMP needs to emit from the Empire-db driver the null value and not "CURRENT_TIMESTAMP".
if(rs.getInt("DATA_TYPE") == Types.TIMESTAMP && defaultValue != null && defaultValue.equals("CURRENT_TIMESTAMP")){
required = false; // It is in fact not required even though MySQL schema is required because it has a default value. Generally, should Empire-db emit (required && defaultValue != null) to truly determine if a column is required?
defaultValue = null; // If null (and required per schema?) MySQL will apply internal default value.
}
// AUTOINC indicator is not in java.sql.Types but rather meta data from DatabaseMetaData.getColumns()
// getEmpireDataType() above is not enough to support AUTOINC as it will only return DataType.INTEGER
DataType originalType = empireType;
ResultSetMetaData metaData = rs.getMetaData();
int colCount = metaData.getColumnCount();
String colName;
for (int i = 1; i <= colCount; i++) {
colName = metaData.getColumnName(i);
// MySQL matches on IS_AUTOINCREMENT column.
// SQL Server matches on TYPE_NAME column with identity somewhere in the string value.
if ((colName.equalsIgnoreCase("IS_AUTOINCREMENT") && rs.getString(i).equalsIgnoreCase("YES")) ||
(colName.equals("TYPE_NAME") && rs.getString(i).matches(".*(?i:identity).*"))){
empireType = DataType.AUTOINC;
}
}
// Move from the return statement below so we can add
// some AUTOINC meta data to the column to be used by
// the ParserUtil and ultimately the template.
log.info("\tCOLUMN:\t" + name + " ("+empireType+")");
DBTableColumn col = t.addColumn(name, empireType, colSize, required, defaultValue);
// We still need to know the base data type for this AUTOINC
// because the Record g/setters need to know this, right?
// So, let's add it as meta data every time the column is AUTOINC
// and reference it in the template.
if(empireType.equals(DataType.AUTOINC))
col.setAttribute("AutoIncDataType", originalType);
return col;
}
protected double getColumnSize(DataType empireType, int dataType, int columnSize) {
return columnSize;
}
/**
* Adds DBColumn object to the given DBTable. The DBColumn is created from
* the given ResultSet
*/
private DBViewColumn addColumn(InMemoryView v, ResultSet rs)
throws SQLException {
String name = rs.getString("COLUMN_NAME");
DataType empireType = getEmpireDataType(rs.getInt("DATA_TYPE"));
log.info("\tCOLUMN:\t" + name + " ("+empireType+")");
return v.addCol(name, empireType);
}
/**
* converts a SQL DataType to a EmpireDataType
*/
private 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.TIMESTAMP:
case Types.TIME:
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.NCLOB:
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;
log.warn("SQL column type " + sqlType + " not supported.");
}
log.debug("Mapping date type " + String.valueOf(sqlType) + " to "
+ empireType);
return empireType;
}
}