package org.zapodot.junit.db.plugin; import liquibase.Contexts; import liquibase.LabelExpression; import liquibase.Liquibase; import liquibase.database.core.H2Database; import liquibase.database.jvm.JdbcConnection; import liquibase.exception.LiquibaseException; import liquibase.resource.ClassLoaderResourceAccessor; import liquibase.resource.ResourceAccessor; import java.io.IOException; import java.sql.Connection; import java.sql.SQLException; import java.util.Arrays; import java.util.LinkedList; import java.util.List; /** * A custom InitializationPlugin for * * @author zapodot */ public class LiquibaseInitializer implements InitializationPlugin { private final Contexts contexts; private final LabelExpression labelExpression; private final String changeLog; private final ResourceAccessor resourceAccessor; private final Integer changesLimit; private final boolean addDbNameToContext; private final String defaultSchemaName; public static class Builder { private String databaseChangeLog; private ResourceAccessor resourceAccessor; private List<String> contexts = new LinkedList<>(); private List<String> labels = new LinkedList<>(); private Integer changesToApply; private boolean addNameToContext = false; private String defaultSchemaName; public Builder() { } /** * A reference to the changelog file. Can be in any format supported by liquibase (currently SQL,Yaml,XML or JSON) * * @param resource a resource that will be read from classpath using Class.getResource(String) * @return the same builder */ public Builder withChangelogResource(final String resource) { this.databaseChangeLog = resource; this.resourceAccessor = new ClassLoaderResourceAccessor(); return this; } /** * Limit the number of changes in the changelog to be applied * @param limit the number of changes * @return the same builder */ public Builder limitChanges(Integer limit) { this.changesToApply = limit; return this; } /** * Adds contexts to be used for Liquibase. As default no contexts are specified, which is interpreted be * Liquibase as meaning 'all contexts'. * To have the H2 database name added to the context list use {@link #addDatabaseNameToContext()}. * * @param contexts a variable length list of contexts * @return the same builder instance * @see <a href="http://www.liquibase.org/documentation/contexts.html">Liquibase Contexts documentation</a> */ public Builder withContexts(final String... contexts) { if (contexts != null) { this.contexts.addAll(Arrays.asList(contexts)); } return this; } /** * Avoids the current H2 database name to be added to the list of contexts. * * @return the same builder instance */ public Builder addDatabaseNameToContext() { this.addNameToContext = false; return this; } /** * If you need parameter support, use this method to add labels to * * @param labels a variable length array of labels to be added * @return the same builder instance */ public Builder withLabels(final String... labels) { if (labels != null) { this.labels.addAll(Arrays.asList(labels)); } return this; } /** * Overrides default H2 schema name (which is PUBLIC) with given one. Will handle schema * creation (if not exists) on startup. */ public Builder withDefaultSchemaName(String schemaName) { this.defaultSchemaName = schemaName; return this; } /** * Builds the LiquibaseInitializer based on the settings provided earlier by calls to the various builder methods * * @return a LiqubaseInitializer instance to be used with the EmbeddedDatabaseRule */ public LiquibaseInitializer build() { if (databaseChangeLog == null) { throw new IllegalArgumentException("You must provide a changelog file to the LiquibaseIntitializer Plugin builder"); } try { if(resourceAccessor.getResourcesAsStream(databaseChangeLog) == null) { throw new IllegalArgumentException(String.format("Can not load changelog from resource \"%s\". Does it exist?", databaseChangeLog)); } } catch (IOException e) { throw new IllegalArgumentException(String.format("An IO exception occurred while loading changelog from resource \"%s\"", databaseChangeLog), e); } return new LiquibaseInitializer(createContexts(), createLabels(), databaseChangeLog, resourceAccessor, changesToApply, addNameToContext, defaultSchemaName); } private LabelExpression createLabels() { return new LabelExpression(labels); } private Contexts createContexts() { return new Contexts(contexts); } } private LiquibaseInitializer(final Contexts contexts, final LabelExpression labelExpression, final String changeLog, final ResourceAccessor resourceAccessor, final Integer changesLimit, final boolean addDbNameToContext, final String defaultSchemaName) { this.contexts = contexts; this.labelExpression = labelExpression; this.changeLog = changeLog; this.resourceAccessor = resourceAccessor; this.changesLimit = changesLimit; this.addDbNameToContext = addDbNameToContext; this.defaultSchemaName = defaultSchemaName; } @Override public void connectionMade(final String name, final Connection connection) { if (defaultSchemaName != null) { try { connection.prepareStatement( String.format("CREATE SCHEMA IF NOT EXISTS %s", defaultSchemaName)) .execute(); } catch (SQLException e) { throw new RuntimeException(e); } } final Liquibase liquibase = createLiquibase(connection); if (addDbNameToContext) { contexts.add(name); } try { if (changesLimit == null) { liquibase.update(contexts, labelExpression); } else { liquibase.update(changesLimit.intValue(), contexts, labelExpression); } } catch (LiquibaseException e) { throw new RuntimeException("An exception occurred while applying Liquibase changesets", e); } } private Liquibase createLiquibase(final Connection connection) { try { JdbcConnection conn = new JdbcConnection(connection); H2Database h2Database = new H2Database(); h2Database.setConnection(conn); if (defaultSchemaName != null) { h2Database.setDefaultSchemaName(defaultSchemaName); } return new Liquibase(changeLog, resourceAccessor, h2Database); } catch (LiquibaseException e) { throw new RuntimeException("Could not initialize Liquibase", e); } } /** * Creates a builder for providing parameters for Liquibase to be run * * @return a new {@link Builder} */ public static Builder builder() { return new Builder(); } }