nz.co.gregs.dbvolution.generation.DBTableClassGenerator Maven / Gradle / Ivy
/*
* Copyright 2013 Gregory Graham.
*
* 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 nz.co.gregs.dbvolution.generation;
import nz.co.gregs.dbvolution.exceptions.UnknownJavaSQLTypeException;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.Charset;
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.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import nz.co.gregs.dbvolution.databases.DBDatabase;
import nz.co.gregs.dbvolution.databases.DBDatabaseCluster;
import nz.co.gregs.dbvolution.databases.DBStatement;
import nz.co.gregs.dbvolution.datatypes.*;
/**
* Automatically generates Java files to be used in your data model.
*
*
* DBvolution data model classes (DBRow subclasses) are designed to be easy to
* create and modify. However with a complex existing database it can be easier
* to use this class to generate the data model and then add the details.
*
*
Support DBvolution at
* Patreon
*
* @author Gregory Graham
*/
public class DBTableClassGenerator {
private static final String[] JAVA_RESERVED_WORDS_ARRAY = new String[]{"null", "true", "false", "abstract", "continue", "for", "new", "switch", "assert", "default", "goto", "package", "synchronized", "boolean", "do", "if", "private", "this", "break", "double", "implements", "", "protected", "throw", "byte", "else", "import", "public", "throws", "case", "enum", "instanceof", "return", "transient", "catch", "extends", "int", "short", "try", "char", "final", "interface", "static", "", "void", "class", "finally", "long", "strictfp", "volatile", "const", "float", "native", "super", "while"};
private static final List JAVA_RESERVED_WORDS = Arrays.asList(JAVA_RESERVED_WORDS_ARRAY);
/**
*
* Creates DBTableRow classes corresponding to all the tables and views
* accessible to the user specified in the database supplied.
*
*
* Classes are placed in the correct subdirectory of the base directory as
* defined by the package name supplied.
*
*
* Convenience method which calls {@code generateClasses(jdbcURL, username,
* password, packageName, 1L, baseDirectory,new PrimaryKeyRecognisor(),new
* ForeignKeyRecognisor());}
*
*
*
*
* 1 Database exceptions may be thrown
*
* @param database database
* @param packageName packageName
* @param baseDirectory baseDirectory
* @throws java.sql.SQLException java.sql.SQLException
* @throws java.io.FileNotFoundException java.io.FileNotFoundException
* @throws java.io.IOException java.io.IOException
*/
public static void generateClasses(DBDatabase database, String packageName, String baseDirectory) throws SQLException, FileNotFoundException, IOException {
generateClasses(database, packageName, baseDirectory, new Options());
}
/**
*
* Creates DBTableRow classes corresponding to all the tables and views
* accessible to the user specified in the database supplied.
*
*
* Classes are placed in the correct subdirectory of the base directory as
* defined by the package name supplied.
*
*
* Convenience method which calls {@code
* generateClasses(jdbcURL,username,password,packageName,baseDirectory,new
* PrimaryKeyRecognisor(),new ForeignKeyRecognisor());}
*
* @param database database
* @param versionNumber - the value to use for serialVersionUID
*
* 1 Database exceptions may be thrown
* @param packageName packageName
* @param baseDirectory baseDirectory
* @throws java.sql.SQLException java.sql.SQLException
* @throws java.io.FileNotFoundException java.io.FileNotFoundException
* @throws java.io.IOException java.io.IOException
*
*
*/
public static void generateClasses(DBDatabase database, String packageName, String baseDirectory, Long versionNumber) throws SQLException, FileNotFoundException, IOException {
Options opts = new Options();
opts.versionNumber = versionNumber;
generateClasses(database, packageName, baseDirectory, opts);
}
/**
*
* Creates DBTableRow classes corresponding to all the tables and views
* accessible to the user specified in the database supplied.
*
*
* Classes are placed in the correct subdirectory of the base directory as
* defined by the package name supplied.
*
*
* Primary keys and foreign keys are created based on the definitions within
* the database and the results from the PK and FK recognisors.
*
*
* Database exceptions may be thrown
*
* @param database database
* @param packageName packageName
* @param baseDirectory baseDirectory
* @param options
* @throws java.sql.SQLException java.sql.SQLException
* @throws java.io.FileNotFoundException java.io.FileNotFoundException
* @throws java.io.IOException java.io.IOException
*/
public static void generateClasses(DBDatabase database, String packageName, String baseDirectory, Options options) throws SQLException, FileNotFoundException, IOException {
String viewsPackage = packageName + ".views";
String viewsPath = viewsPackage.replaceAll("[.]", "/");
List generatedViews = DBTableClassGenerator.generateClassesOfViews(database, viewsPackage, options);
String tablesPackage = packageName + ".tables";
String tablesPath = tablesPackage.replaceAll("[.]", "/");
List generatedTables = DBTableClassGenerator.generateClassesOfTables(database, tablesPackage, options);
List allGeneratedClasses = new ArrayList<>();
allGeneratedClasses.addAll(generatedViews);
allGeneratedClasses.addAll(generatedTables);
generateAllJavaSource(allGeneratedClasses, options);
File dir = new File(baseDirectory + "/" + viewsPath);
if (dir.mkdirs() || dir.exists()) {
saveGeneratedClassesToDirectory(generatedViews, dir);
} else {
throw new RuntimeException("Unable to Make Directories, QUITTING!");
}
dir = new File(baseDirectory + "/" + tablesPath);
if (dir.mkdirs() || dir.exists()) {
saveGeneratedClassesToDirectory(generatedTables, dir);
} else {
throw new RuntimeException("Unable to Make Directories, QUITTING!");
}
}
/**
*
* Saves the supplied DBTableRow classes as java files in the supplied
* directory.
*
*
* No database interaction nor package name checking is performed.
*
*
* You probably want to use {@link #generateClasses(nz.co.gregs.dbvolution.databases.DBDatabase, java.lang.String, java.lang.String)
* }
*
*
*
* 1 Database exceptions may be thrown
*
*
*/
private static void saveGeneratedClassesToDirectory(List generatedClasses, File classDirectory) throws SQLException, FileNotFoundException, IOException {
{
File file;
FileOutputStream fileOutputStream;
for (DBTableClass clazz : generatedClasses) {
file = new File(classDirectory, clazz.getClassName() + ".java");
fileOutputStream = new FileOutputStream(file);
try {
fileOutputStream.write(clazz.getJavaSource().getBytes(Charset.forName("UTF8")));
fileOutputStream.close();
} finally {
fileOutputStream.close();
}
}
}
}
/**
* Generate the required Java classes for all the Tables on the database.
*
*
* Connects to the database using the DBDatabase instance supplied and
* generates class for the tables it can find.
*
*
* Classes will be in the package supplied, serialVersionUID will be set to
* the version number supplied and the supplied {@link PrimaryKeyRecognisor}
* and {@link ForeignKeyRecognisor} will be used.
*
*
* @param database database
* @param packageName packageName
* @param options
*
Support DBvolution at
* Patreon
* @return a List of DBTableClass instances representing the tables found on
* the database 1 Database exceptions may be thrown
* @throws java.sql.SQLException java.sql.SQLException
*/
public static List generateClassesOfTables(DBDatabase database, String packageName, Options options) throws SQLException {
return generateClassesOfObjectTypes(database, packageName, options, "TABLE");
}
/**
* Generate the required Java classes for all the Views on the database.
*
*
* Connects to the database using the DBDatabase instance supplied and
* generates class for the views it can find.
*
*
* Classes will be in the package supplied, serialVersionUID will be set to
* the version number supplied and the supplied {@link PrimaryKeyRecognisor}
* and {@link ForeignKeyRecognisor} will be used.
*
* @param database database
* @param packageName packageName
* @param options
*
Support DBvolution at
* Patreon
* @return a List of DBTableClass instances representing the views found on
* the database 1 Database exceptions may be thrown
* @throws java.sql.SQLException java.sql.SQLException
*/
public static List generateClassesOfViews(DBDatabase database, String packageName, Options options) throws SQLException {
return generateClassesOfObjectTypes(database, packageName, options, "VIEW");
}
/**
* Generate the required Java classes for all the Tables and Views on the
* database.
*
*
* Connects to the database using the DBDatabase instance supplied and
* generates class for the tables and views it can find.
*
*
* Classes will be in the package supplied, serialVersionUID will be set to
* the version number supplied and the supplied {@link PrimaryKeyRecognisor}
* and {@link ForeignKeyRecognisor} will be used.
*
*
Support DBvolution at
* Patreon
*
* @return a List of DBTableClass instances representing the tables and views
* found on the database 1 Database exceptions may be thrown
*/
private static List generateClassesOfObjectTypes(DBDatabase db, String packageName, Options options, String... dbObjectTypes) throws SQLException {
List dbTableClasses = new ArrayList<>();
DBDatabase database = db;
if (db instanceof DBDatabaseCluster) {
database =((DBDatabaseCluster)db).getReadyDatabase();
}
try (DBStatement dbStatement = database.getDBStatement()) {
Connection connection = dbStatement.getConnection();
String catalog = connection.getCatalog();
String schema = null;
try {
//Method method = connection.getClass().getMethod("getSchema");
//schema = (String) method.invoke(connection);
schema = connection.getSchema();
} catch (java.sql.SQLFeatureNotSupportedException nope) {
// SOMEONE DIDN'T WRITE THEIR DRIVER PROPERLY
} catch (java.lang.AbstractMethodError exp) {
// NOT USING Java 1.7+ apparently
} catch (IllegalArgumentException ex) {
// NOT USING Java 1.7+ apparently
} catch (SecurityException ex) {
// NOT USING Java 1.7+ apparently
}
DatabaseMetaData metaData = connection.getMetaData();
try (ResultSet tables = metaData.getTables(catalog, schema, null, dbObjectTypes)) {
while (tables.next()) {
final String tableName = tables.getString("TABLE_NAME");
if (schema == null) {
schema = tables.getString("TABLE_SCHEM");
}
if (tableName.matches(database.getDefinition().getSystemTableExclusionPattern())) {
final String className = toClassCase(tableName);
DBTableClass dbTableClass = new DBTableClass(tableName, schema, packageName, className);
ResultSet primaryKeysRS = metaData.getPrimaryKeys(catalog, schema, dbTableClass.getTableName());
List pkNames = new ArrayList<>();
try {
while (primaryKeysRS.next()) {
String pkColumnName = primaryKeysRS.getString("COLUMN_NAME");
pkNames.add(pkColumnName);
}
} finally {
primaryKeysRS.close();
}
ResultSet foreignKeysRS = metaData.getImportedKeys(catalog, schema, dbTableClass.getTableName());
Map fkNames = new HashMap<>();
try {
while (foreignKeysRS.next()) {
String pkTableName = foreignKeysRS.getString("PKTABLE_NAME");
String pkColumnName = foreignKeysRS.getString("PKCOLUMN_NAME");
String fkColumnName = foreignKeysRS.getString("FKCOLUMN_NAME");
fkNames.put(fkColumnName, new String[]{pkTableName, pkColumnName});
}
} finally {
foreignKeysRS.close();
}
try (ResultSet columns = metaData.getColumns(catalog, schema, dbTableClass.getTableName(), null)) {
while (columns.next()) {
DBTableField dbTableField = new DBTableField();
dbTableField.columnName = columns.getString("COLUMN_NAME");
dbTableField.fieldName = toFieldCase(dbTableField.columnName);
try {
dbTableField.referencedTable = columns.getString("SCOPE_TABLE");
} catch (SQLException exp) {
; // MSSQLServer throws an exception on this
}
dbTableField.precision = columns.getInt("COLUMN_SIZE");
dbTableField.comments = columns.getString("REMARKS");
String isAutoIncr = null;
try {
isAutoIncr = columns.getString("IS_AUTOINCREMENT");
} catch (SQLException sqlex) {
;// SQLite-JDBC throws an exception when retrieving IS_AUTOINCREMENT
}
dbTableField.isAutoIncrement = isAutoIncr != null && isAutoIncr.equals("YES");
try {
dbTableField.sqlDataTypeInt = columns.getInt("DATA_TYPE");
dbTableField.sqlDataTypeName = columns.getString("TYPE_NAME");
dbTableField.columnType = getQueryableDatatypeNameOfSQLType(database, dbTableField, options.trimCharColumns);
} catch (UnknownJavaSQLTypeException ex) {
dbTableField.columnType = DBUnknownDatatype.class;
dbTableField.javaSQLDatatype = ex.getUnknownJavaSQLType();
}
if (pkNames.contains(dbTableField.columnName) || options.pkRecog.isPrimaryKeyColumn(dbTableClass.getTableName(), dbTableField.columnName)) {
dbTableField.isPrimaryKey = true;
}
database.getDefinition().sanityCheckDBTableField(dbTableField);
String[] pkData = fkNames.get(dbTableField.columnName);
if (pkData != null && pkData.length == 2) {
dbTableField.isForeignKey = true;
dbTableField.referencesClass = toClassCase(pkData[0]);
dbTableField.referencesField = pkData[1];
} else if (options.fkRecog.isForeignKeyColumn(dbTableClass.getTableName(), dbTableField.columnName)) {
dbTableField.isForeignKey = true;
dbTableField.referencesField = options.fkRecog.getReferencedColumn(dbTableClass.getTableName(), dbTableField.columnName);
dbTableField.referencesClass = toClassCase(options.fkRecog.getReferencedTable(dbTableClass.getTableName(), dbTableField.columnName));
}
if (!dbTableClass.getFields().contains(dbTableField)) {
dbTableClass.getFields().add(dbTableField);
}
}
}
dbTableClasses.add(dbTableClass);
}
}
}
generateAllJavaSource(dbTableClasses, options);
}
return dbTableClasses;
}
static void generateAllJavaSource(List dbTableClasses, Options options) {
List dbTableClassNames = new ArrayList<>();
for (DBTableClass dbt : dbTableClasses) {
dbTableClassNames.add(dbt.getClassName());
}
for (DBTableClass dbt : dbTableClasses) {
for (DBTableField dbf : dbt.getFields()) {
if (dbf.isForeignKey) {
if (!dbTableClassNames.contains(dbf.referencesClass)) {
List matchingNames = new ArrayList<>();
for (String name : dbTableClassNames) {
if (name.toLowerCase().startsWith(dbf.referencesClass.toLowerCase())) {
matchingNames.add(name);
}
}
if (matchingNames.size() == 1) {
String properClassname = matchingNames.get(0);
dbf.referencesClass = properClassname;
}
}
}
}
dbt.generateJavaSource(options);
}
}
/**
*
* Returns a string of the appropriate QueryableDatatype for the specified
* SQLType
*
*
* Support DBvolution at
* Patreon
*
* @return a string of the appropriate QueryableDatatype for the specified
* SQLType
*/
private static Class extends Object> getQueryableDatatypeNameOfSQLType(DBDatabase database, DBTableField column, Boolean trimCharColumns) throws UnknownJavaSQLTypeException {
int columnType = column.sqlDataTypeInt;
int precision = column.precision;
String typeName = column.sqlDataTypeName;
Class extends Object> value;
switch (columnType) {
case Types.BIT:
if (precision == 1) {
value = DBBoolean.class;
} else {
value = DBLargeBinary.class;
}
break;
case Types.TINYINT:
case Types.INTEGER:
case Types.BIGINT:
case Types.BINARY:
case Types.BOOLEAN:
case Types.ROWID:
case Types.SMALLINT:
if (precision == 1) {
value = DBBoolean.class;
} else {
value = DBInteger.class;
}
break;
case Types.DECIMAL:
case Types.DOUBLE:
case Types.FLOAT:
case Types.NUMERIC:
case Types.REAL:
value = DBNumber.class;
break;
case Types.CHAR:
case Types.NCHAR:
if (trimCharColumns) {
value = DBStringTrimmed.class;
} else {
value = DBString.class;
}
break;
case Types.VARCHAR:
case Types.NVARCHAR:
case Types.LONGNVARCHAR:
case Types.LONGVARCHAR:
value = DBString.class;
break;
case Types.DATE:
case Types.TIME:
case Types.TIMESTAMP:
value = DBDate.class;
break;
case Types.OTHER:
Class extends QueryableDatatype>> customType = database.getDefinition().getQueryableDatatypeClassForSQLDatatype(typeName);
if (customType != null) {
value = customType;
break;
} else {
value = DBJavaObject.class;
break;
}
case Types.JAVA_OBJECT:
value = DBJavaObject.class;
break;
case Types.CLOB:
case Types.NCLOB:
value = DBLargeText.class;
break;
case Types.VARBINARY:
case Types.LONGVARBINARY:
case Types.BLOB:
case Types.ARRAY:
case Types.SQLXML:
value = DBLargeBinary.class;
break;
default:
throw new UnknownJavaSQLTypeException("Unknown Java SQL Type: " + columnType, columnType);
}
return value;
}
/**
*
* returns a good guess at the java CLASS version of a DB field name.
*
* I.e. changes "_" into an uppercase letter.
*
* @param s s
* Support DBvolution at
* Patreon
* @return camel case version of the String
*/
public static String toClassCase(String s) {
StringBuilder classCaseString = new StringBuilder("");
if (s == null) {
return null;
} else if (s.matches("[lLtT]+_[0-9]+(_[0-9]+)*")) {
classCaseString.append(s.toUpperCase());
} else {
String[] parts = s.split("[^a-zA-Z0-9]");//"[_$#]");
for (String part : parts) {
classCaseString.append(toProperCase(part));
}
}
return classCaseString.toString();
}
/**
*
* returns a good guess at the java field version of a DB field name.
*
* I.e. changes "_" into an uppercase letter.
*
*
* Support DBvolution at
* Patreon
*
* @return Camel Case version of S
*/
private static String toFieldCase(String s) {
String classClass = toClassCase(s);
String camelCaseString = classClass.substring(0, 1).toLowerCase() + classClass.substring(1);
camelCaseString = camelCaseString.replaceAll("[^a-zA-Z0-9_$]", "_");
if (JAVA_RESERVED_WORDS.contains(camelCaseString)) {
camelCaseString += "_";
}
return camelCaseString;
}
/**
*
* Capitalizes the first letter of the string
*
*
* Support DBvolution at
* Patreon
*
* @return Capitalizes the first letter of the string
*/
private static String toProperCase(String s) {
switch (s.length()) {
case 0:
return s;
case 1:
return s.toUpperCase();
default:
String firstChar = s.substring(0, 1);
String rest = s.substring(1).toLowerCase();
if (firstChar.matches("[^a-zA-Z]")) {
return "_" + firstChar + rest;
} else {
return firstChar.toUpperCase() + rest;
}
}
}
private DBTableClassGenerator() {
}
/*
* @param fkRecog fkRecog an object that can recognize foreign key columns by
* the column name and derive the related table
* @param versionNumber versionNumber
* @param pkRecog pkRecog an object that can recognize primary key columns by
* the column name
* @param trimCharColumns
*/
public static class Options {
Long versionNumber = 1l;
PrimaryKeyRecognisor pkRecog = new PrimaryKeyRecognisor();
ForeignKeyRecognisor fkRecog = new ForeignKeyRecognisor();
Boolean trimCharColumns = false;
Boolean includeForeignKeyColumnName = false;
public Options() {
}
public Options(Long versionNumber, PrimaryKeyRecognisor pkRecog, ForeignKeyRecognisor fkRecog, Boolean trimCharColumns) {
this.versionNumber = versionNumber;
this.pkRecog = pkRecog;
this.fkRecog = fkRecog;
this.trimCharColumns = trimCharColumns;
}
}
}