All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.github.jhipster.loaded.reloader.LiquibaseReloader Maven / Gradle / Ivy

package io.github.jhipster.loaded.reloader;

import io.github.jhipster.loaded.hibernate.JHipsterEntityManagerFactoryWrapper;
import io.github.jhipster.loaded.patch.liquibase.JhipsterHibernateSpringDatabase;
import io.github.jhipster.loaded.reloader.liquibase.CustomXMLChangeLogSerializer;
import io.github.jhipster.loaded.reloader.type.EntityReloaderType;
import io.github.jhipster.loaded.reloader.type.ReloaderType;
import liquibase.Liquibase;
import liquibase.database.Database;
import liquibase.database.ObjectQuotingStrategy;
import liquibase.database.jvm.JdbcConnection;
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.LiquibaseException;
import liquibase.ext.hibernate.database.connection.HibernateConnection;
import liquibase.integration.spring.SpringLiquibase;
import liquibase.resource.ClassLoaderResourceAccessor;
import liquibase.structure.DatabaseObject;
import liquibase.structure.core.*;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.filefilter.SuffixFileFilter;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.FileSystemResourceLoader;
import org.springframework.stereotype.Component;

import javax.sql.DataSource;
import java.io.*;
import java.nio.file.FileSystems;
import java.util.*;

/**
 * Compare the Hibernate Entity JPA and the current database.
 * If changes have been done, a new db-changelog-[SEQUENCE].xml file will be generated and the database will be updated
 */
@Component
@Order(90)
@ConditionalOnClass(Liquibase.class)
public class LiquibaseReloader implements Reloader {

    private final Logger log = LoggerFactory.getLogger(LiquibaseReloader.class);

    public static final String MASTER_FILE = "src/main/resources/config/liquibase/master.xml";
    public static final String CHANGELOG_FOLER = "src/main/resources/config/liquibase/changelog/";
    public static final String RELATIVE_CHANGELOG_FOLER = "classpath:config/liquibase/changelog/";

    private Collection entitiesToReload = new LinkedHashSet<>();

    private ConfigurableApplicationContext applicationContext;

    private CompareControl compareControl;

    @Override
    public void init(ConfigurableApplicationContext applicationContext) {
        log.debug("Hot reloading JPA & Liquibase enabled");
        this.applicationContext = applicationContext;
        initCompareControl();
    }

    @Override
    public boolean supports(Class reloaderType) {
        return reloaderType.equals(EntityReloaderType.class);
    }

    @Override
    public void prepare() {}

    @Override
    public boolean hasBeansToReload() {
        return false;
    }

    @Override
    public void addBeansToReload(Collection classes, Class reloaderType) {
        entitiesToReload.addAll(classes);
    }

    @Override
    public void reload() {
        log.debug("Hot reloading JPA & Liquibase classes");
        Database hibernateDatabase = null;
        Database sourceDatabase = null;

        try {
            final String packagesToScan = applicationContext.getEnvironment().getProperty("hotReload.package.domain");

            // Build source datasource
            DataSource dataSource = applicationContext.getBean(DataSource.class);
            sourceDatabase = getSourceDatabase(dataSource);
            sourceDatabase.setObjectQuotingStrategy(ObjectQuotingStrategy.QUOTE_ALL_OBJECTS);

            // Build hibernate datasource - used as a reference
            hibernateDatabase = new JhipsterHibernateSpringDatabase(sourceDatabase.getDefaultCatalogName(), sourceDatabase.getLiquibaseSchemaName());
            hibernateDatabase.setObjectQuotingStrategy(ObjectQuotingStrategy.QUOTE_ALL_OBJECTS);
            hibernateDatabase.setConnection(new JdbcConnection(
                    new HibernateConnection("hibernate:spring:" + packagesToScan + "?dialect=" + applicationContext.getEnvironment().getProperty("spring.jpa.database-platform"))));

            // Use liquibase to do a difference of schema between hibernate and database
            Liquibase liquibase = new Liquibase(null, new ClassLoaderResourceAccessor(), sourceDatabase);

            // Retrieve the difference
            DiffResult diffResult = liquibase.diff(hibernateDatabase, sourceDatabase, compareControl);

            // Build the changelogs if any changes
            DiffToChangeLog diffToChangeLog = new DiffToChangeLog(diffResult, new DiffOutputControl(false, false, true));

            // Ignore the database changeLog table
            ignoreDatabaseChangeLogTable(diffResult);
            ignoreDatabaseJHipsterTables(diffResult);

            // If no changes do nothing
            if (diffToChangeLog.generateChangeSets().size() == 0) {
                log.debug("JHipster reload - No database change");
                return;
            }

            // Write the db-changelog-[SEQUENCE].xml file
            String changeLogString = toChangeLog(diffToChangeLog);
            String changeLogName = "db-changelog-" + calculateNextSequence() + ".xml";
            final File changelogFile = FileSystems.getDefault().getPath(CHANGELOG_FOLER + changeLogName).toFile();
            final FileOutputStream out = new FileOutputStream(changelogFile);
            IOUtils.write(changeLogString, out);
            IOUtils.closeQuietly(out);
            log.debug("JHipster reload - the db-changelog file '{}' has been generated", changelogFile.getAbsolutePath());

            // Re-write the master.xml files
            rewriteMasterFiles();

            // Execute the new changelog on the database
            SpringLiquibase springLiquibase = new SpringLiquibase() {
                @Override
                protected void performUpdate(Liquibase liquibase) throws LiquibaseException {
                    // Override to be able to add
                    liquibase.setChangeLogParameter("logicalFilePath", "none");
                    super.performUpdate(liquibase);
                }
            };
            springLiquibase.setResourceLoader(new FileSystemResourceLoader());
            springLiquibase.setDataSource(dataSource);
            springLiquibase.setChangeLog("file:" + changelogFile.getAbsolutePath());
            springLiquibase.setContexts("development");
            try {
                springLiquibase.afterPropertiesSet();
                log.debug("JHipster reload - Successful database update");
            } catch (LiquibaseException e) {
                log.error("Failed to reload the database", e);
            }

            // Ask to reload the EntityManager
            JHipsterEntityManagerFactoryWrapper.reload(entitiesToReload);
            entitiesToReload.clear();
        } catch (Exception e) {
            log.error("Failed to generate the db-changelog.xml file", e);
        } finally {
            // close the database
            if (sourceDatabase != null) {
                try {
                    sourceDatabase.close();
                } catch (DatabaseException e) {
                    log.error("Failed to close the source database", e);
                }
            }

            if (hibernateDatabase != null) {
                try {
                    hibernateDatabase.close();
                } catch (DatabaseException e) {
                    log.error("Failed to close the reference database", e);
                }
            }
        }
    }

    private void ignoreDatabaseChangeLogTable(DiffResult diffResult)
            throws Exception {
				
        Set unexpectedTables = diffResult
                .getUnexpectedObjects(Table.class);
		
        for (Table table : unexpectedTables) {
            if ("DATABASECHANGELOGLOCK".equalsIgnoreCase(table.getName())
                    || "DATABASECHANGELOG".equalsIgnoreCase(table.getName())) {
						
                diffResult.getUnexpectedObjects().remove(table);
			}
        }
        Set
missingTables = diffResult .getMissingObjects(Table.class); for (Table table : missingTables) { if ("DATABASECHANGELOGLOCK".equalsIgnoreCase(table.getName()) || "DATABASECHANGELOG".equalsIgnoreCase(table.getName())) { diffResult.getMissingObjects().remove(table); } } Set unexpectedColumns = diffResult.getUnexpectedObjects(Column.class); for (Column column : unexpectedColumns) { if ("DATABASECHANGELOGLOCK".equalsIgnoreCase(column.getRelation().getName()) || "DATABASECHANGELOG".equalsIgnoreCase(column.getRelation().getName())) { diffResult.getUnexpectedObjects().remove(column); } } Set missingColumns = diffResult.getMissingObjects(Column.class); for (Column column : missingColumns) { if ("DATABASECHANGELOGLOCK".equalsIgnoreCase(column.getRelation().getName()) || "DATABASECHANGELOG".equalsIgnoreCase(column.getRelation().getName())) { diffResult.getMissingObjects().remove(column); } } Set unexpectedIndexes = diffResult.getUnexpectedObjects(Index.class); for (Index index : unexpectedIndexes) { if ("DATABASECHANGELOGLOCK".equalsIgnoreCase(index.getTable().getName()) || "DATABASECHANGELOG".equalsIgnoreCase(index.getTable().getName())) { diffResult.getUnexpectedObjects().remove(index); } } Set missingIndexes = diffResult.getMissingObjects(Index.class); for (Index index : missingIndexes) { if ("DATABASECHANGELOGLOCK".equalsIgnoreCase(index.getTable().getName()) || "DATABASECHANGELOG".equalsIgnoreCase(index.getTable().getName())) { diffResult.getMissingObjects().remove(index); } } Set unexpectedPrimaryKeys = diffResult.getUnexpectedObjects(PrimaryKey.class); for (PrimaryKey primaryKey : unexpectedPrimaryKeys) { if ("DATABASECHANGELOGLOCK".equalsIgnoreCase(primaryKey.getTable().getName()) || "DATABASECHANGELOG".equalsIgnoreCase(primaryKey.getTable().getName())) { diffResult.getUnexpectedObjects().remove(primaryKey); } } Set missingPrimaryKeys = diffResult.getMissingObjects(PrimaryKey.class); for (PrimaryKey primaryKey : missingPrimaryKeys) { if ("DATABASECHANGELOGLOCK".equalsIgnoreCase(primaryKey.getTable().getName()) || "DATABASECHANGELOG".equalsIgnoreCase(primaryKey.getTable().getName())) { diffResult.getMissingObjects().remove(primaryKey); } } } private void ignoreDatabaseJHipsterTables(DiffResult diffResult) throws Exception { List jhipsterTables = new ArrayList<>(); jhipsterTables.add("HIBERNATE_SEQUENCES"); final String excludeTables = applicationContext.getEnvironment().getProperty("hotReload.liquibase.excludeTables", ""); if (StringUtils.isNotEmpty(excludeTables)) { jhipsterTables.addAll(Arrays.asList(excludeTables.toUpperCase().split(","))); } Set
unexpectedTables = diffResult .getUnexpectedObjects(Table.class); for (Table table : unexpectedTables) { if (jhipsterTables.contains(table.getName().toUpperCase().toUpperCase())) { diffResult.getUnexpectedObjects().remove(table); } } Set
missingTables = diffResult .getMissingObjects(Table.class); for (Table table : missingTables) { if (jhipsterTables.contains(table.getName().toUpperCase())) { diffResult.getMissingObjects().remove(table); } } Set unexpectedColumns = diffResult.getUnexpectedObjects(Column.class); for (Column column : unexpectedColumns) { if (jhipsterTables.contains(column.getRelation().getName().toUpperCase())) { diffResult.getUnexpectedObjects().remove(column); } } Set missingColumns = diffResult.getMissingObjects(Column.class); for (Column column : missingColumns) { if (jhipsterTables.contains(column.getRelation().getName().toUpperCase())) { diffResult.getMissingObjects().remove(column); } } Set unexpectedIndexes = diffResult.getUnexpectedObjects(Index.class); for (Index index : unexpectedIndexes) { if (jhipsterTables.contains(index.getTable().getName().toUpperCase())) { diffResult.getUnexpectedObjects().remove(index); } } Set missingIndexes = diffResult.getMissingObjects(Index.class); for (Index index : missingIndexes) { if (jhipsterTables.contains(index.getTable().getName().toUpperCase())) { diffResult.getMissingObjects().remove(index); } } Set unexpectedPrimaryKeys = diffResult.getUnexpectedObjects(PrimaryKey.class); for (PrimaryKey primaryKey : unexpectedPrimaryKeys) { if (jhipsterTables.contains(primaryKey.getTable().getName().toUpperCase())) { diffResult.getUnexpectedObjects().remove(primaryKey); } } Set missingPrimaryKeys = diffResult.getMissingObjects(PrimaryKey.class); for (PrimaryKey primaryKey : missingPrimaryKeys) { if (jhipsterTables.contains(primaryKey.getTable().getName().toUpperCase())) { diffResult.getMissingObjects().remove(primaryKey); } } } private String toChangeLog(DiffToChangeLog diffToChangeLog) throws Exception { ByteArrayOutputStream out = new ByteArrayOutputStream(); PrintStream printStream = new PrintStream(out, true, "UTF-8"); diffToChangeLog.setChangeSetAuthor("jhipster"); CustomXMLChangeLogSerializer customXMLChangeLogSerializer = new CustomXMLChangeLogSerializer(); diffToChangeLog.print(printStream, customXMLChangeLogSerializer); printStream.close(); return out.toString("UTF-8"); } protected void initCompareControl() { Set> typesToInclude = new HashSet<>(); typesToInclude.add(Table.class); typesToInclude.add(Column.class); compareControl = new CompareControl(typesToInclude); compareControl.addSuppressedField(Table.class, "remarks"); compareControl.addSuppressedField(Column.class, "remarks"); compareControl.addSuppressedField(Column.class, "certainDataType"); compareControl.addSuppressedField(Column.class, "autoIncrementInformation"); } /** * Calculate the next sequence used to generate the db-changelog file. * * The sequence is formatted as follow: * leftpad with 0 + number * @return the next sequence */ private String calculateNextSequence() { final File changeLogFolder = FileSystems.getDefault().getPath(CHANGELOG_FOLER).toFile(); final File[] allChangelogs = changeLogFolder.listFiles((FileFilter) new SuffixFileFilter(".xml")); Integer sequence = 0; for (File changelog : allChangelogs) { String fileName = FilenameUtils.getBaseName(changelog.getName()); String currentSequence = StringUtils.substringAfterLast(fileName, "-"); int cpt = Integer.parseInt(currentSequence); if (cpt > sequence) { sequence = cpt; } } sequence++; return StringUtils.leftPad(sequence.toString(), 3, "0"); } /** * @return the source database */ private Database getSourceDatabase(DataSource dataSource) { String currentDatabase = applicationContext.getEnvironment().getProperty("spring.jpa.database"); String liquibaseDatabase; switch (currentDatabase) { case "MYSQL": liquibaseDatabase = "liquibase.database.core.MySQLDatabase"; break; case "POSTGRESQL": liquibaseDatabase = "liquibase.database.core.PostgresDatabase"; break; case "H2": liquibaseDatabase = "liquibase.database.core.H2Database"; break; default: throw new IllegalStateException("The database named '" + currentDatabase + "' is not supported"); } try { Database database = (Database) Class.forName(liquibaseDatabase).newInstance(); database.setConnection(new JdbcConnection(dataSource.getConnection())); return database; } catch (Exception e) { throw new IllegalStateException("Failed to instanciate the liquibase database: " + liquibaseDatabase, e); } } /** * The master.xml file will be rewritten to include the new changelogs */ private void rewriteMasterFiles() { try { File masterFile = FileSystems.getDefault().getPath(MASTER_FILE).toFile(); FileOutputStream fileOutputStream = new FileOutputStream(masterFile); final File changeLogFolder = FileSystems.getDefault().getPath(CHANGELOG_FOLER).toFile(); final File[] allChangelogs = changeLogFolder.listFiles((FileFilter) new SuffixFileFilter(".xml")); String begin = "\n" + "\n\r"; String end = ""; IOUtils.write(begin, fileOutputStream); // Writer the changelogs StringBuilder sb = new StringBuilder(); for (File allChangelog : allChangelogs) { String fileName = allChangelog.getName(); sb.append("\t").append("\r\n"); } IOUtils.write(sb.toString(), fileOutputStream); IOUtils.write(end, fileOutputStream); IOUtils.closeQuietly(fileOutputStream); log.debug("The file '{}' has been updated", MASTER_FILE); } catch (Exception e) { log.error("Failed to write the master.xml file. This file must be updated manually"); } } }