package com.txtr.hibernatedelta.generator; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.net.URL; import java.sql.Connection; import java.util.Collection; import java.util.Collections; import java.util.List; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import com.google.common.base.Function; import com.google.common.base.Throwables; import com.google.common.collect.ImmutableList; import com.google.common.collect.Ordering; import com.txtr.hibernatedelta.DatabaseWithIndexes; import com.txtr.hibernatedelta.IndexIdFactory; import com.txtr.hibernatedelta.model.HibernateDatabase; import com.txtr.hibernatedelta.model.HibernateIndexName; import com.txtr.hibernatedelta.model.HibernateIndexNames; import liquibase.change.Change; import liquibase.changelog.ChangeSet; import liquibase.diff.DiffGeneratorFactory; import liquibase.diff.DiffResult; import liquibase.diff.compare.CompareControl; import liquibase.diff.output.DiffOutputControl; import liquibase.diff.output.changelog.DiffToChangeLog; import liquibase.snapshot.DatabaseSnapshot; import liquibase.sql.Sql; import liquibase.sqlgenerator.SqlGeneratorFactory; import liquibase.statement.SqlStatement; import liquibase.structure.DatabaseObject; import liquibase.structure.core.ForeignKey; import liquibase.structure.core.Index; import liquibase.structure.core.PrimaryKey; import liquibase.structure.core.UniqueConstraint; public class BackendSqlGenerator { public static final List<String> CREATE_SQL_FILES = ImmutableList.of("quartz.sql"); private static final JAXBContext MARSHALLER; private static final JAXBContext INDEX_MARSHALLER; static { try { MARSHALLER = JAXBContext.newInstance(HibernateDatabase.class); INDEX_MARSHALLER = JAXBContext.newInstance(HibernateIndexNames.class); } catch (JAXBException e) { throw Throwables.propagate(e); } } public static final String RENAME_INDEX = "ALTER INDEX %2$s RENAME TO %3$s;"; public static final String RENAME_CONSTRAINT = "ALTER TABLE %1s RENAME CONSTRAINT %2$s TO %3$s;"; public BackendSqlGenerator() throws IOException { } public static HibernateIndexNames getIndexNames(URL indexNames) { try { return (HibernateIndexNames) INDEX_MARSHALLER.createUnmarshaller().unmarshal(indexNames); } catch (JAXBException e) { throw Throwables.propagate(e); } } public String createUpdateAgainstCommittedSchema(URL schema, DatabaseWithIndexes newDatabase, URL indexNames) throws Exception { final DatabaseSnapshot reference = LiquibaseModelFactory.create(newDatabase.getDatabase()); final DatabaseSnapshot target = createFromCommittedSchema(schema, indexNames); Collections.sort(newDatabase.getIndexNames().getIndexNames(), Ordering.from(String.CASE_INSENSITIVE_ORDER).onResultOf(new Function<HibernateIndexName, String>() { @Override public String apply(HibernateIndexName input) { return input.getTableName() + "-" + StringUtils.join(input.getColumns(), ','); } })); final StringBuilder sb = new StringBuilder(); printChanges(reference, target, sb); return sb.toString(); } public void writeDataModelFiles(File mappings, File indexNames, DatabaseWithIndexes newDatabase) throws FileNotFoundException { try { getSchemaMarshaller().marshal(newDatabase.getDatabase(), new FileOutputStream(mappings)); getIndexMarshaller().marshal(newDatabase.getIndexNames(), new FileOutputStream(indexNames)); } catch (JAXBException e) { throw Throwables.propagate(e); } } public static Marshaller getIndexMarshaller() throws JAXBException { return getMarshaller(INDEX_MARSHALLER); } public static Marshaller getSchemaMarshaller() throws JAXBException { return getMarshaller(MARSHALLER); } private static Marshaller getMarshaller(JAXBContext context) throws JAXBException { Marshaller marshaller = context.createMarshaller(); marshaller.setProperty(javax.xml.bind.Marshaller.JAXB_FORMATTED_OUTPUT, true); marshaller.setProperty(javax.xml.bind.Marshaller.JAXB_ENCODING, "UTF-8"); return marshaller; } public String createNew(HibernateDatabase newDatabase) throws Exception { final DatabaseSnapshot reference = LiquibaseModelFactory.create(newDatabase); final DatabaseSnapshot target = LiquibaseModelFactory.createEmptySnapshot(); final StringBuilder sb = new StringBuilder(); printChanges(reference, target, sb); for (String file : CREATE_SQL_FILES) { @SuppressWarnings("unchecked") final List<String> lines = IOUtils.readLines(Thread.currentThread().getContextClassLoader().getResourceAsStream(file)); for (Object line : lines) { sb.append(line).append("\n"); } } return sb.toString(); } public String createUpdateAgainstExistingSchema(URL schema, String jdbcUrl, String userName, String password, URL indexNames) throws Exception { final DatabaseSnapshot reference = createFromCommittedSchema(schema, indexNames); final DatabaseSnapshot target = LiquibaseModelFactory.readSnapshotFromDatabase(jdbcUrl, userName, password); final StringBuilder sb = new StringBuilder(); printChanges(reference, target, sb); return sb.toString(); } public String createDropAgainstExistingSchema(String jdbcUrl, String userName, String password) throws Exception { return createDropAgainstExistingSchema(LiquibaseModelFactory.readSnapshotFromDatabase(jdbcUrl, userName, password)); } public String createDropAgainstExistingSchema(Connection connection) throws Exception { return createDropAgainstExistingSchema(LiquibaseModelFactory.readSnapshotFromDatabase(connection)); } private String createDropAgainstExistingSchema(DatabaseSnapshot target) throws Exception { final DatabaseSnapshot reference = LiquibaseModelFactory.createEmptySnapshot(); final StringBuilder sb = new StringBuilder(); printChanges(reference, target, sb); return sb.toString(); } private DatabaseSnapshot createFromCommittedSchema(URL schema, URL indexNames) throws Exception { HibernateDatabase hibernateDatabase = getDatabase(schema); IndexIdFactory.setIndexNames(hibernateDatabase, getIndexNames(indexNames)); return LiquibaseModelFactory.create(hibernateDatabase); } public static HibernateDatabase getDatabase(URL schema) { try { return (HibernateDatabase) MARSHALLER.createUnmarshaller().unmarshal(schema); } catch (JAXBException e) { throw Throwables.propagate(e); } } public void printChanges(DatabaseSnapshot reference, DatabaseSnapshot target, StringBuilder sb) throws Exception { printNonRenames(reference, target, sb); printRenameStatements(reference, target, sb); } public void printNonRenames(DatabaseSnapshot reference, DatabaseSnapshot target, StringBuilder sb) throws Exception { CompareControl compareControl = new CompareControl(reference.getSnapshotControl().getTypesToInclude()); DiffResult diffResult = DiffGeneratorFactory.getInstance().compare(reference, target, compareControl); DiffToChangeLog diffToChangeLog = new DiffToChangeLog(diffResult, new DiffOutputControl(false, false, false)); SqlGeneratorFactory generatorFactory = SqlGeneratorFactory.getInstance(); for (ChangeSet changeSet : diffToChangeLog.generateChangeSets()) { for (Change change : changeSet.getChanges()) { for (SqlStatement sqlStatement : change.generateStatements(LiquibaseModelFactory.DATABASE)) { for (Sql sql : generatorFactory.generateSql(sqlStatement, LiquibaseModelFactory.DATABASE)) { final String sqlString = sql.toSql(); if (sqlString.endsWith("DROP INDEX")){ sb.append(StringUtils.substringBefore(sqlString, " DROP INDEX")).append(";\n"); } else { sb.append(sqlString).append(";\n"); } } } } } } public void printRenameStatements(DatabaseSnapshot reference, DatabaseSnapshot target, StringBuilder sb) { printRenameStatements(sb, reference, target.get(Index.class), RENAME_INDEX, new Function<Index, String>() { @Override public String apply(Index input) { return input.getName(); } }, new Function<Index, String>() { @Override public String apply(Index input) { return input.getTable().getName(); } }); printRenameStatements(sb, reference, target.get(UniqueConstraint.class), RENAME_CONSTRAINT, new Function<UniqueConstraint, String>() { @Override public String apply(UniqueConstraint input) { return input.getName(); } }, new Function<UniqueConstraint, String>() { @Override public String apply(UniqueConstraint input) { return input.getTable().getName(); } }); printRenameStatements(sb, reference, target.get(ForeignKey.class), RENAME_CONSTRAINT, new Function<ForeignKey, String>() { @Override public String apply(ForeignKey input) { return input.getName(); } }, new Function<ForeignKey, String>() { @Override public String apply(ForeignKey input) { return input.getForeignKeyTable().getName(); } }); printRenameStatements(sb, reference, target.get(PrimaryKey.class), RENAME_CONSTRAINT, new Function<PrimaryKey, String>() { @Override public String apply(PrimaryKey input) { return input.getName(); } }, new Function<PrimaryKey, String>() { @Override public String apply(PrimaryKey input) { return input.getTable().getName(); } }); } private <T extends DatabaseObject> void printRenameStatements(StringBuilder sb, DatabaseSnapshot referenceSnapshot, Collection<T> targetList, String renameStatement, Function<T, String> getName, Function<T, String> getTableName) { for (T target : targetList) { T reference = referenceSnapshot.get(target); if (reference != null) { String referenceName = getName.apply(reference); String targetName = getName.apply(target); //noinspection ConstantConditions if (!referenceName.equalsIgnoreCase(targetName)) { sb.append(getRenameStatement(renameStatement, getTableName.apply(target), targetName, referenceName)).append("\n"); } } } } public static String getRenameStatement(String renameStatement, String tableName, String from, String to) { return String.format(renameStatement, tableName, from, to); } }