package io.dropwizard.migrations; import com.google.common.annotations.VisibleForTesting; import io.dropwizard.Configuration; import io.dropwizard.db.DatabaseConfiguration; import liquibase.CatalogAndSchema; import liquibase.Liquibase; import liquibase.database.Database; 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.exception.DatabaseException; import liquibase.exception.UnexpectedLiquibaseException; import liquibase.snapshot.DatabaseSnapshot; import liquibase.snapshot.InvalidExampleException; import liquibase.snapshot.SnapshotControl; import liquibase.snapshot.SnapshotGeneratorFactory; import liquibase.structure.DatabaseObject; import liquibase.structure.core.Column; import liquibase.structure.core.Data; import liquibase.structure.core.ForeignKey; import liquibase.structure.core.Index; import liquibase.structure.core.PrimaryKey; import liquibase.structure.core.Sequence; import liquibase.structure.core.Table; import liquibase.structure.core.UniqueConstraint; import liquibase.structure.core.View; import net.sourceforge.argparse4j.impl.Arguments; import net.sourceforge.argparse4j.inf.ArgumentGroup; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; import javax.xml.parsers.ParserConfigurationException; import java.io.IOException; import java.io.PrintStream; import java.nio.charset.StandardCharsets; import java.util.HashSet; import java.util.Set; public class DbDumpCommand<T extends Configuration> extends AbstractLiquibaseCommand<T> { private PrintStream outputStream = System.out; @VisibleForTesting void setOutputStream(PrintStream outputStream) { this.outputStream = outputStream; } public DbDumpCommand(DatabaseConfiguration<T> strategy, Class<T> configurationClass, String migrationsFileName) { super("dump", "Generate a dump of the existing database state.", strategy, configurationClass, migrationsFileName); } @Override public void configure(Subparser subparser) { super.configure(subparser); subparser.addArgument("-o", "--output") .dest("output") .help("Write output to <file> instead of stdout"); final ArgumentGroup tables = subparser.addArgumentGroup("Tables"); tables.addArgument("--tables") .action(Arguments.storeTrue()) .dest("tables") .help("Check for added or removed tables (default)"); tables.addArgument("--ignore-tables") .action(Arguments.storeFalse()) .dest("tables") .help("Ignore tables"); final ArgumentGroup columns = subparser.addArgumentGroup("Columns"); columns.addArgument("--columns") .action(Arguments.storeTrue()) .dest("columns") .help("Check for added, removed, or modified columns (default)"); columns.addArgument("--ignore-columns") .action(Arguments.storeFalse()) .dest("columns") .help("Ignore columns"); final ArgumentGroup views = subparser.addArgumentGroup("Views"); views.addArgument("--views") .action(Arguments.storeTrue()) .dest("views") .help("Check for added, removed, or modified views (default)"); views.addArgument("--ignore-views") .action(Arguments.storeFalse()) .dest("views") .help("Ignore views"); final ArgumentGroup primaryKeys = subparser.addArgumentGroup("Primary Keys"); primaryKeys.addArgument("--primary-keys") .action(Arguments.storeTrue()) .dest("primary-keys") .help("Check for changed primary keys (default)"); primaryKeys.addArgument("--ignore-primary-keys") .action(Arguments.storeFalse()) .dest("primary-keys") .help("Ignore primary keys"); final ArgumentGroup uniqueConstraints = subparser.addArgumentGroup("Unique Constraints"); uniqueConstraints.addArgument("--unique-constraints") .action(Arguments.storeTrue()) .dest("unique-constraints") .help("Check for changed unique constraints (default)"); uniqueConstraints.addArgument("--ignore-unique-constraints") .action(Arguments.storeFalse()) .dest("unique-constraints") .help("Ignore unique constraints"); final ArgumentGroup indexes = subparser.addArgumentGroup("Indexes"); indexes.addArgument("--indexes") .action(Arguments.storeTrue()) .dest("indexes") .help("Check for changed indexes (default)"); indexes.addArgument("--ignore-indexes") .action(Arguments.storeFalse()) .dest("indexes") .help("Ignore indexes"); final ArgumentGroup foreignKeys = subparser.addArgumentGroup("Foreign Keys"); foreignKeys.addArgument("--foreign-keys") .action(Arguments.storeTrue()) .dest("foreign-keys") .help("Check for changed foreign keys (default)"); foreignKeys.addArgument("--ignore-foreign-keys") .action(Arguments.storeFalse()) .dest("foreign-keys") .help("Ignore foreign keys"); final ArgumentGroup sequences = subparser.addArgumentGroup("Sequences"); sequences.addArgument("--sequences") .action(Arguments.storeTrue()) .dest("sequences") .help("Check for changed sequences (default)"); sequences.addArgument("--ignore-sequences") .action(Arguments.storeFalse()) .dest("sequences") .help("Ignore sequences"); final ArgumentGroup data = subparser.addArgumentGroup("Data"); data.addArgument("--data") .action(Arguments.storeTrue()) .dest("data") .help("Check for changed data") .setDefault(Boolean.FALSE); data.addArgument("--ignore-data") .action(Arguments.storeFalse()) .dest("data") .help("Ignore data (default)") .setDefault(Boolean.FALSE); } @Override @SuppressWarnings("UseOfSystemOutOrSystemErr") public void run(Namespace namespace, Liquibase liquibase) throws Exception { final Set<Class<? extends DatabaseObject>> compareTypes = new HashSet<>(); if (isTrue(namespace.getBoolean("columns"))) { compareTypes.add(Column.class); } if (isTrue(namespace.getBoolean("data"))) { compareTypes.add(Data.class); } if (isTrue(namespace.getBoolean("foreign-keys"))) { compareTypes.add(ForeignKey.class); } if (isTrue(namespace.getBoolean("indexes"))) { compareTypes.add(Index.class); } if (isTrue(namespace.getBoolean("primary-keys"))) { compareTypes.add(PrimaryKey.class); } if (isTrue(namespace.getBoolean("sequences"))) { compareTypes.add(Sequence.class); } if (isTrue(namespace.getBoolean("tables"))) { compareTypes.add(Table.class); } if (isTrue(namespace.getBoolean("unique-constraints"))) { compareTypes.add(UniqueConstraint.class); } if (isTrue(namespace.getBoolean("views"))) { compareTypes.add(View.class); } final DiffToChangeLog diffToChangeLog = new DiffToChangeLog(new DiffOutputControl()); final Database database = liquibase.getDatabase(); final String filename = namespace.getString("output"); if (filename != null) { try (PrintStream file = new PrintStream(filename, StandardCharsets.UTF_8.name())) { generateChangeLog(database, database.getDefaultSchema(), diffToChangeLog, file, compareTypes); } } else { generateChangeLog(database, database.getDefaultSchema(), diffToChangeLog, outputStream, compareTypes); } } private void generateChangeLog(final Database database, final CatalogAndSchema catalogAndSchema, final DiffToChangeLog changeLogWriter, PrintStream outputStream, final Set<Class<? extends DatabaseObject>> compareTypes) throws DatabaseException, IOException, ParserConfigurationException { @SuppressWarnings({"unchecked", "rawtypes"}) final SnapshotControl snapshotControl = new SnapshotControl(database, compareTypes.toArray(new Class[compareTypes.size()])); final CompareControl compareControl = new CompareControl(new CompareControl.SchemaComparison[]{ new CompareControl.SchemaComparison(catalogAndSchema, catalogAndSchema)}, compareTypes); final CatalogAndSchema[] compareControlSchemas = compareControl .getSchemas(CompareControl.DatabaseRole.REFERENCE); try { final DatabaseSnapshot referenceSnapshot = SnapshotGeneratorFactory.getInstance() .createSnapshot(compareControlSchemas, database, snapshotControl); final DatabaseSnapshot comparisonSnapshot = SnapshotGeneratorFactory.getInstance() .createSnapshot(compareControlSchemas, null, snapshotControl); final DiffResult diffResult = DiffGeneratorFactory.getInstance() .compare(referenceSnapshot, comparisonSnapshot, compareControl); changeLogWriter.setDiffResult(diffResult); changeLogWriter.print(outputStream); } catch (InvalidExampleException e) { throw new UnexpectedLiquibaseException(e); } } private static boolean isTrue(Boolean nullableCondition) { return nullableCondition != null && nullableCondition; } }