package com.exteso.lab.pf.config.reload.reloader; import com.exteso.lab.pf.config.reload.instrument.hibernate.JHipsterEntityManagerFactoryWrapper; import liquibase.Liquibase; import liquibase.database.Database; 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.LiquibaseException; import liquibase.ext.hibernate.database.HibernateSpringDatabase; 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.hibernate.cfg.Configuration; import org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl; import org.hibernate.jpa.boot.spi.Bootstrap; import org.hibernate.service.ServiceRegistry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.io.FileSystemResourceLoader; import org.springframework.orm.jpa.persistenceunit.DefaultPersistenceUnitManager; import org.springframework.orm.jpa.persistenceunit.SmartPersistenceUnitInfo; import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; import javax.persistence.spi.PersistenceUnitInfo; import javax.sql.DataSource; import java.io.*; import java.nio.file.*; 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 */ public class LiquibaseReloader { 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 ConfigurableApplicationContext applicationContext; private CompareControl compareControl; public LiquibaseReloader(ConfigurableApplicationContext applicationContext) { log.debug("Hot reloading JPA & Liquibase enabled"); this.applicationContext = applicationContext; initCompareControl(); } public void reloadEvent(List<Class> entities) { log.debug("Hot reloading JPA & Liquibase classes"); try { final String packagesToScan = "com.exteso.lab.pf.domain"; // Build source datasource DataSource dataSource = applicationContext.getBean(DataSource.class); final JdbcConnection jdbcConnection = new JdbcConnection(dataSource.getConnection()); Database sourceDatabase = getDatabaseSource(); sourceDatabase.setConnection(jdbcConnection); // Build hibernate datasource Database hibernateDatabase = new HibernateSpringDatabase() { @Override public Configuration buildConfigurationFromScanning(HibernateConnection connection) { String[] packagesToScan = connection.getPath().split(","); for (String packageName : packagesToScan) { log.info("Found package {}", packageName); } DefaultPersistenceUnitManager internalPersistenceUnitManager = new DefaultPersistenceUnitManager(); internalPersistenceUnitManager.setPackagesToScan(packagesToScan); String dialectName = connection.getProperties().getProperty("dialect", null); if (dialectName == null) { throw new IllegalArgumentException("A 'dialect' has to be specified."); } log.info("Found dialect {}", dialectName); internalPersistenceUnitManager.preparePersistenceUnitInfos(); PersistenceUnitInfo persistenceUnitInfo = internalPersistenceUnitManager.obtainDefaultPersistenceUnitInfo(); HibernateJpaVendorAdapter jpaVendorAdapter = new HibernateJpaVendorAdapter(); jpaVendorAdapter.setDatabasePlatform(dialectName); Map<String, Object> jpaPropertyMap = jpaVendorAdapter.getJpaPropertyMap(); jpaPropertyMap.put("hibernate.archive.autodetection", "false"); if (persistenceUnitInfo instanceof SmartPersistenceUnitInfo) { ((SmartPersistenceUnitInfo) persistenceUnitInfo).setPersistenceProviderPackageName(jpaVendorAdapter.getPersistenceProviderRootPackage()); } EntityManagerFactoryBuilderImpl builder = (EntityManagerFactoryBuilderImpl) Bootstrap.getEntityManagerFactoryBuilder(persistenceUnitInfo, jpaPropertyMap); ServiceRegistry serviceRegistry = builder.buildServiceRegistry(); return builder.buildHibernateConfiguration(serviceRegistry); } }; hibernateDatabase.setDefaultSchemaName(""); hibernateDatabase.setDefaultCatalogName(""); 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(), jdbcConnection); // Retrieve the difference DiffResult diffResult = liquibase.diff(hibernateDatabase, sourceDatabase, compareControl); // Build the changelogs if any changes DiffToChangeLog diffToChangeLog = new DiffToChangeLog(diffResult, new DiffOutputControl()); // 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(); 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(entities); } catch (Exception e) { log.error("Failed to generate the db-changelog.xml file", e); } } private void ignoreDatabaseChangeLogTable(DiffResult diffResult) throws Exception { Set<Table> unexpectedTables = diffResult .getUnexpectedObjects(Table.class); for (Table table : unexpectedTables) { if ("DATABASECHANGELOGLOCK".equalsIgnoreCase(table.getName()) || "DATABASECHANGELOG".equalsIgnoreCase(table.getName())) { diffResult.getUnexpectedObjects().remove(table); } } Set<Table> missingTables = diffResult .getMissingObjects(Table.class); for (Table table : missingTables) { if ("DATABASECHANGELOGLOCK".equalsIgnoreCase(table.getName()) || "DATABASECHANGELOG".equalsIgnoreCase(table.getName())) { diffResult.getMissingObjects().remove(table); } } Set<Column> 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<Column> 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<Index> 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<Index> 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<PrimaryKey> 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<PrimaryKey> 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<String> IGNORE_JHIPSTER = Arrays.asList("HIBERNATE_SEQUENCES", "T_AUTHORITY", "T_PERSISTENT_AUDIT_EVENT", "T_PERSISTENT_AUDIT_EVENT_DATA", "T_PERSISTENT_TOKEN", "T_USER", "T_USER_AUTHORITY"); Set<Table> unexpectedTables = diffResult .getUnexpectedObjects(Table.class); for (Table table : unexpectedTables) { if (IGNORE_JHIPSTER.contains(table.getName())) { diffResult.getUnexpectedObjects().remove(table); } } Set<Table> missingTables = diffResult .getMissingObjects(Table.class); for (Table table : missingTables) { if (IGNORE_JHIPSTER.contains(table.getName())) { diffResult.getMissingObjects().remove(table); } } Set<Column> unexpectedColumns = diffResult.getUnexpectedObjects(Column.class); for (Column column : unexpectedColumns) { if (IGNORE_JHIPSTER.contains(column.getRelation().getName())) { diffResult.getUnexpectedObjects().remove(column); } } Set<Column> missingColumns = diffResult.getMissingObjects(Column.class); for (Column column : missingColumns) { if (IGNORE_JHIPSTER.contains(column.getRelation().getName())) { diffResult.getMissingObjects().remove(column); } } Set<Index> unexpectedIndexes = diffResult.getUnexpectedObjects(Index.class); for (Index index : unexpectedIndexes) { if (IGNORE_JHIPSTER.contains(index.getTable().getName())) { diffResult.getUnexpectedObjects().remove(index); } } Set<Index> missingIndexes = diffResult.getMissingObjects(Index.class); for (Index index : missingIndexes) { if (IGNORE_JHIPSTER.contains(index.getTable().getName())) { diffResult.getMissingObjects().remove(index); } } Set<PrimaryKey> unexpectedPrimaryKeys = diffResult.getUnexpectedObjects(PrimaryKey.class); for (PrimaryKey primaryKey : unexpectedPrimaryKeys) { if (IGNORE_JHIPSTER.contains(primaryKey.getTable().getName())) { diffResult.getUnexpectedObjects().remove(primaryKey); } } Set<PrimaryKey> missingPrimaryKeys = diffResult.getMissingObjects(PrimaryKey.class); for (PrimaryKey primaryKey : missingPrimaryKeys) { if (IGNORE_JHIPSTER.contains(primaryKey.getTable().getName())) { diffResult.getMissingObjects().remove(primaryKey); } } Set<ForeignKey> unexpectedForeignKeys = diffResult.getUnexpectedObjects(ForeignKey.class); for (ForeignKey foreignKey : unexpectedForeignKeys) { if (IGNORE_JHIPSTER.contains(foreignKey.getForeignKeyTable().getName())) { diffResult.getUnexpectedObjects().remove(foreignKey); } } Set<ForeignKey> missingForeignKeys = diffResult.getMissingObjects(ForeignKey.class); for (ForeignKey foreignKey : missingForeignKeys) { if (IGNORE_JHIPSTER.contains(foreignKey.getForeignKeyTable().getName())) { diffResult.getMissingObjects().remove(foreignKey); } } } private String toChangeLog(DiffToChangeLog diffToChangeLog) throws Exception { ByteArrayOutputStream out = new ByteArrayOutputStream(); PrintStream printStream = new PrintStream(out, true, "UTF-8"); diffToChangeLog.setChangeSetAuthor("jhipster"); diffToChangeLog.print(printStream); printStream.close(); return out.toString("UTF-8"); } private void initCompareControl() { Set<Class<? extends DatabaseObject>> typesToInclude = new HashSet<>(); typesToInclude.add(Table.class); typesToInclude.add(Column.class); typesToInclude.add(PrimaryKey.class); typesToInclude.add(ForeignKey.class); typesToInclude.add(UniqueConstraint.class); typesToInclude.add(Sequence.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"); compareControl.addSuppressedField(ForeignKey.class, "deleteRule"); compareControl.addSuppressedField(ForeignKey.class, "updateRule"); compareControl.addSuppressedField(Index.class, "unique"); } /** * 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 getDatabaseSource() { return new liquibase.database.core.H2Database(); } /** * 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 = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + "<databaseChangeLog\n" + " xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n" + " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n" + " xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd\">\n\r"; String end = "</databaseChangeLog>"; IOUtils.write(begin, fileOutputStream); // Writer the changelogs StringBuffer sb = new StringBuffer(); for (File allChangelog : allChangelogs) { String fileName = allChangelog.getName(); sb.append("\t<include file=\"" + RELATIVE_CHANGELOG_FOLER).append(fileName).append("\" relativeToChangelogFile=\"false\"/>").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"); } } }