/*
* fmgVen - A Convention over Configuration Java ORM Tool
* Copyright 2011 Fatih Mehmet Güler
*
* 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.
* under the License.
*/
package com.fmguler.ven.support;
import com.fmguler.ven.util.Convert;
import java.beans.PropertyDescriptor;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;
/**
* Convert domain objects to Liquibase changeset xml
* @author Fatih Mehmet Güler
*/
public class LiquibaseConverter {
private Set domainPackages;
private Map dbClasses;
private List unhandledForeignKeyConstraints;
private Set processedTables;
private String author;
private String schema = "public";
private int changeSetId = 0;
private static final int COLUMN_TYPE_VARCHAR_1000 = 0;
private static final int COLUMN_TYPE_TEXT = 1;
private static final int COLUMN_TYPE_DATE = 2;
private static final int COLUMN_TYPE_BOOLEAN = 3;
private static final int COLUMN_TYPE_INT = 4;
private static final int COLUMN_TYPE_DOUBLE = 5;
public LiquibaseConverter() {
domainPackages = new HashSet();
//the predefined database classes;
dbClasses = new HashMap();
dbClasses.put(Integer.class, new Integer(COLUMN_TYPE_INT));
dbClasses.put(String.class, new Integer(COLUMN_TYPE_VARCHAR_1000));
dbClasses.put(Date.class, new Integer(COLUMN_TYPE_DATE));
dbClasses.put(Double.class, new Integer(COLUMN_TYPE_DOUBLE));
dbClasses.put(Boolean.class, new Integer(COLUMN_TYPE_BOOLEAN));
//implementation specific
unhandledForeignKeyConstraints = new LinkedList();
processedTables = new HashSet();
}
/**
* Scans all classes accessible from the context class loader which belong to the given package and subpackages.
*
* @param packageName The base package
* @return The classes
* @throws ClassNotFoundException
* @throws IOException
*/
private static Class[] getClasses(String packageName) throws ClassNotFoundException, IOException {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
assert classLoader != null;
String path = packageName.replace('.', '/');
Enumeration resources = classLoader.getResources(path);
List dirs = new ArrayList();
while (resources.hasMoreElements()) {
URL resource = (URL)resources.nextElement();
dirs.add(new File(resource.getFile()));
}
ArrayList classes = new ArrayList();
Iterator it = dirs.iterator();
while (it.hasNext()) {
File directory = (File)it.next();
classes.addAll(findClasses(directory, packageName));
}
return (Class[])classes.toArray(new Class[classes.size()]);
}
/**
* Recursive method used to find all classes in a given directory and subdirs.
*
* @param directory The base directory
* @param packageName The package name for classes found inside the base directory
* @return The classes
* @throws ClassNotFoundException
*/
private static List findClasses(File directory, String packageName) throws ClassNotFoundException {
List classes = new ArrayList();
if (!directory.exists()) {
return classes;
}
File[] files = directory.listFiles();
for (int i = 0; i < files.length; i++) {
File file = files[i];
if (file.isDirectory()) {
assert file.getName().indexOf(".") != -1;
classes.addAll(findClasses(file, packageName + "." + file.getName()));
} else if (file.getName().endsWith(".class")) {
classes.add(Class.forName(packageName + '.' + file.getName().substring(0, file.getName().length() - 6)));
}
}
return classes;
}
/**
* Add the domain packages that will be considered persistent
* @param domainPackage the domain package
*/
public void addDomainPackage(String domainPackage) {
domainPackages.add(domainPackage);
}
/**
* Set the author field in the changesets
*/
public void setAuthor(String author) {
this.author = author;
}
/**
* Set the changeSetId start (default 0)
* @param changeSetId the id attribute in the changeset tag
*/
public void setChangeSetIdStart(int changeSetId) {
this.changeSetId = changeSetId;
}
/**
* Set the schema to be added to liquibase tags
*/
public void setSchema(String schema) {
this.schema = schema;
}
/**
* Convert the added domain packages to liquibase changeset xml
* @return changeset xml
*/
public String convert() {
StringBuffer liquibaseXml = new StringBuffer();
Iterator it = domainPackages.iterator();
while (it.hasNext()) {
String domainPackage = (String)it.next();
try {
Class[] domainClasses = getClasses(domainPackage);
for (int i = 0; i < domainClasses.length; i++) {
Class domainClass = domainClasses[i];
convertClass(liquibaseXml, domainClass);
}
} catch (ClassNotFoundException ex) {
ex.printStackTrace();
} catch (IOException ex) {
ex.printStackTrace();
}
}
return liquibaseXml.toString();
}
//convert a single class
private void convertClass(StringBuffer liquibaseXml, Class domainClass) {
String objectName = Convert.toSimpleName(domainClass.getName());
String tableName = Convert.toDB(objectName);
startTagChangeSet(liquibaseXml, author, "" + (changeSetId++));
startTagCreateTable(liquibaseXml, schema, tableName);
addIdColumn(liquibaseXml, tableName);
List foreignKeyConstraints = new LinkedList();
BeanWrapper wr = new BeanWrapperImpl(domainClass);
PropertyDescriptor[] pdArr = wr.getPropertyDescriptors();
for (int i = 0; i < pdArr.length; i++) {
Class fieldClass = pdArr[i].getPropertyType(); //field class
String fieldName = pdArr[i].getName(); //field name
String columnName = Convert.toDB(pdArr[i].getName()); //column name
if (fieldName.equals("id")) {
continue;
}
//direct database class (Integer, String, Date, etc)
if (dbClasses.keySet().contains(fieldClass)) {
addPrimitiveColumn(liquibaseXml, columnName, ((Integer)dbClasses.get(fieldClass)).intValue());
}
//many to one association (object property)
if (fieldClass.getPackage() != null && domainPackages.contains(fieldClass.getPackage().getName())) {
addPrimitiveColumn(liquibaseXml, columnName + "_id", COLUMN_TYPE_INT);
//handle foreign key
String referencedTableName = Convert.toDB(Convert.toSimpleName(fieldClass.getName()));
Map fkey = new HashMap();
fkey.put("baseTableName", tableName);
fkey.put("baseColumnNames", columnName + "_id");
fkey.put("referencedTableName", referencedTableName);
if (processedTables.contains(referencedTableName)) foreignKeyConstraints.add(fkey);
else unhandledForeignKeyConstraints.add(fkey);
}
}
endTagCreateTable(liquibaseXml);
endTagChangeSet(liquibaseXml);
//mark table as processed
processedTables.add(tableName);
//add fkeys waiting for this table
notifyUnhandledForeignKeyConstraints(liquibaseXml, tableName);
//add fkeys not waiting for this table
Iterator it = foreignKeyConstraints.iterator();
while (it.hasNext()) {
Map fkey = (Map)it.next();
addForeignKeyConstraint(liquibaseXml, fkey);
}
}
//private methods
//--------------------------------------------------------------------------
//start changeset tag
private void startTagChangeSet(StringBuffer buffer, String author, String id) {
buffer.append("\n");
}
//end changeset tag
private void endTagChangeSet(StringBuffer buffer) {
buffer.append("\n");
}
//start createtable tag
private void startTagCreateTable(StringBuffer buffer, String schema, String tableName) {
buffer.append("\t\n");
}
//end createtable tag
private void endTagCreateTable(StringBuffer buffer) {
buffer.append("\t\n");
}
//id column tag
private void addIdColumn(StringBuffer buffer, String tableName) {
buffer.append("\t\t\n");
buffer.append("\t\t\t\n");
buffer.append("\t\t\n");
}
//primitive column tag
private void addPrimitiveColumn(StringBuffer buffer, String columnName, int columnType) {
buffer.append("\t\t\n");
}
//foreign key constraint tag
private void addForeignKeyConstraint(StringBuffer buffer, Map fkey) {
startTagChangeSet(buffer, author, "" + (changeSetId++));
String baseTableName = (String)fkey.get("baseTableName");
String baseColumnNames = (String)fkey.get("baseColumnNames");
String referencedTableName = (String)fkey.get("referencedTableName");
String constraintName = baseTableName + "_" + baseColumnNames + "_fkey";
buffer.append("\t\n");
endTagChangeSet(buffer);
}
//handle fkeys waiting for a table
private void notifyUnhandledForeignKeyConstraints(StringBuffer buffer, String tableName) {
Iterator it = unhandledForeignKeyConstraints.iterator();
while (it.hasNext()) {
Map fkey = (Map)it.next();
if (fkey.get("referencedTableName").equals(tableName)) addForeignKeyConstraint(buffer, fkey);
}
}
private String getDataType(int columnType) {
//TODO: these are for postgresql, do it for all dbs, or make it generic.
switch (columnType) {
case COLUMN_TYPE_VARCHAR_1000:
return "VARCHAR(1000)";
case COLUMN_TYPE_TEXT:
return "TEXT(2147483647)";
case COLUMN_TYPE_DATE:
return "TIMESTAMP WITHOUT TIME ZONE"; //DATE? DATETIME? TIME?
case COLUMN_TYPE_BOOLEAN:
return "BOOLEAN";
case COLUMN_TYPE_INT:
return "int"; //BIGINT?
case COLUMN_TYPE_DOUBLE:
return "DOUBLE";
}
return "";
}
}