CCM NG: JavaDoc for the CcmIntegrator
git-svn-id: https://svn.libreccm.org/ccm/ccm_ng@3614 8810af33-2d31-482b-a856-94f89814c4dfpull/2/head
parent
f448802139
commit
b754e4b04c
|
|
@ -40,20 +40,42 @@ import java.util.ServiceLoader;
|
||||||
import javax.sql.DataSource;
|
import javax.sql.DataSource;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Manages the database schema for new and updated modules.
|
||||||
|
*
|
||||||
|
* This implementation of Hibernate's {@code Integrator} interface which manages
|
||||||
|
* the database schema of LibreCCM. It uses the
|
||||||
|
* <a href="http://www.flywaydb.org">Flyway</a> framework to execute migrations
|
||||||
|
* on the database if necessary. To find the modules the Java service loader is
|
||||||
|
* used. Because the integrator is called in a very early phase of the
|
||||||
|
* application lifecycle we can't use CDI here.
|
||||||
*
|
*
|
||||||
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
|
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
|
||||||
*/
|
*/
|
||||||
public class CcmIntegrator implements Integrator {
|
public class CcmIntegrator implements Integrator {
|
||||||
|
|
||||||
private static final Logger LOGGER = LogManager.getLogger(
|
private static final Logger LOGGER = LogManager.getLogger(
|
||||||
CcmIntegrator.class);
|
CcmIntegrator.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service loader containing all modules. Initialised by the
|
||||||
|
* {@link #integrate(Configuration, SessionFactoryImplementor, SessionFactoryServiceRegistry)}
|
||||||
|
* method.
|
||||||
|
*/
|
||||||
private ServiceLoader<CcmModule> modules;
|
private ServiceLoader<CcmModule> modules;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks for new and updated modules when the persistence unit is started.
|
||||||
|
* If there are updates the necessary database migrations are executed.
|
||||||
|
*
|
||||||
|
* @param configuration
|
||||||
|
* @param sessionFactory
|
||||||
|
* @param registry
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void integrate(final Configuration configuration,
|
public void integrate(final Configuration configuration,
|
||||||
final SessionFactoryImplementor sessionFactory,
|
final SessionFactoryImplementor sessionFactory,
|
||||||
final SessionFactoryServiceRegistry registry) {
|
final SessionFactoryServiceRegistry registry) {
|
||||||
|
//Find all modules in the classpath
|
||||||
LOGGER.info("Retrieving modules...");
|
LOGGER.info("Retrieving modules...");
|
||||||
modules = ServiceLoader.load(CcmModule.class);
|
modules = ServiceLoader.load(CcmModule.class);
|
||||||
for (final CcmModule module : modules) {
|
for (final CcmModule module : modules) {
|
||||||
|
|
@ -65,25 +87,29 @@ public class CcmIntegrator implements Integrator {
|
||||||
|
|
||||||
Connection connection = null;
|
Connection connection = null;
|
||||||
try {
|
try {
|
||||||
|
//Create dependency tree for the modules
|
||||||
final DependencyTreeManager treeManager
|
final DependencyTreeManager treeManager
|
||||||
= new DependencyTreeManager();
|
= new DependencyTreeManager();
|
||||||
final List<TreeNode> tree = treeManager.generateTree(modules);
|
final List<TreeNode> tree = treeManager.generateTree(modules);
|
||||||
final List<TreeNode> orderedNodes = treeManager.orderModules(tree);
|
final List<TreeNode> orderedNodes = treeManager.orderModules(tree);
|
||||||
|
|
||||||
|
//Get DataSource and Connection from the sessionFactory of
|
||||||
|
//Hibernate.
|
||||||
final DataSource dataSource = (DataSource) sessionFactory.
|
final DataSource dataSource = (DataSource) sessionFactory.
|
||||||
getProperties().get("javax.persistence.jtaDataSource");
|
getProperties().get("javax.persistence.jtaDataSource");
|
||||||
connection = dataSource.getConnection();
|
connection = dataSource.getConnection();
|
||||||
|
|
||||||
//Migrate tables and sequences which don't belong to a module
|
//Migrate tables and sequences which don't belong to a module
|
||||||
//for instance hibernate_sequence
|
//for instance the hibernate_sequence
|
||||||
final Flyway flyway = new Flyway();
|
final Flyway flyway = new Flyway();
|
||||||
flyway.setDataSource(dataSource);
|
flyway.setDataSource(dataSource);
|
||||||
final StringBuffer buffer = new StringBuffer(
|
final StringBuffer buffer = new StringBuffer(
|
||||||
"db/migrations/org/libreccm/base");
|
"db/migrations/org/libreccm/base");
|
||||||
appendDbLocation(buffer, connection);
|
appendDbLocation(buffer, connection);
|
||||||
flyway.setLocations(buffer.toString());
|
flyway.setLocations(buffer.toString());
|
||||||
flyway.migrate();
|
flyway.migrate();
|
||||||
|
|
||||||
|
//Migrate the modules
|
||||||
for (final TreeNode node : orderedNodes) {
|
for (final TreeNode node : orderedNodes) {
|
||||||
migrateModule(node.getModule().getClass(), dataSource);
|
migrateModule(node.getModule().getClass(), dataSource);
|
||||||
|
|
||||||
|
|
@ -92,6 +118,7 @@ public class CcmIntegrator implements Integrator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Build Hibernate mappings for the entities.
|
||||||
configuration.buildMappings();
|
configuration.buildMappings();
|
||||||
|
|
||||||
} catch (DependencyException | SQLException ex) {
|
} catch (DependencyException | SQLException ex) {
|
||||||
|
|
@ -103,75 +130,118 @@ public class CcmIntegrator implements Integrator {
|
||||||
LOGGER.info("All modules integrated successfully.");
|
LOGGER.info("All modules integrated successfully.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Private helper method to get the database schema name of a module. The
|
||||||
|
* name is then name of the module in lower case with all hyphens replaced
|
||||||
|
* with underscores.
|
||||||
|
*
|
||||||
|
* @param moduleInfo The module info object for the module
|
||||||
|
* @return The database schema name of the module.
|
||||||
|
*/
|
||||||
private String getSchemaName(final ModuleInfo moduleInfo) {
|
private String getSchemaName(final ModuleInfo moduleInfo) {
|
||||||
return moduleInfo.getModuleName().toLowerCase().replace("-", "_");
|
return moduleInfo.getModuleName().toLowerCase().replace("-", "_");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Private helper method to append the name of the database in use to the
|
||||||
|
* location of the migrations. The value is determined using the return
|
||||||
|
* value of {@link Connection#getMetaData().getDatabaseProductName()} in
|
||||||
|
* lower case. The current supported values are:
|
||||||
|
*
|
||||||
|
* <table>
|
||||||
|
* <tr>
|
||||||
|
* <th>Database Product Name</th>
|
||||||
|
* <th>Location</th>
|
||||||
|
* </tr>
|
||||||
|
* <tr>
|
||||||
|
* <td><code>H2</code></td>
|
||||||
|
* <td><code>/h2</code></td>
|
||||||
|
* </tr>
|
||||||
|
* <tr>
|
||||||
|
* <td><code>PostgreSQL</code></td>
|
||||||
|
* <td><code>/pgsql</code></td>
|
||||||
|
* </tr>
|
||||||
|
* </table>
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* If the database is not supported an {@link IntegrationException} will be
|
||||||
|
* thrown.
|
||||||
|
*
|
||||||
|
* @param buffer Buffer for the location string.
|
||||||
|
* @param connection The JDBC connection object.
|
||||||
|
* @throws SQLException If an error occurs while accessing the database.
|
||||||
|
* @throws IntegrationException If the database is not supported yet.
|
||||||
|
*/
|
||||||
private void appendDbLocation(final StringBuffer buffer,
|
private void appendDbLocation(final StringBuffer buffer,
|
||||||
final Connection connection)
|
final Connection connection)
|
||||||
throws SQLException {
|
throws SQLException {
|
||||||
|
|
||||||
switch (connection.getMetaData().getDatabaseProductName()) {
|
switch (connection.getMetaData().getDatabaseProductName()) {
|
||||||
case "H2":
|
case "H2":
|
||||||
buffer.append("/h2");
|
buffer.append("/h2");
|
||||||
break;
|
break;
|
||||||
// case "MySQL":
|
|
||||||
// buffer.append("/mysql");
|
|
||||||
// break;
|
|
||||||
case "PostgreSQL":
|
case "PostgreSQL":
|
||||||
buffer.append("/pgsql");
|
buffer.append("/pgsql");
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new IntegrationException(String.format(
|
throw new IntegrationException(String.format(
|
||||||
"Integration failed. Database \"%s\" is not supported yet.",
|
"Integration failed. Database \"%s\" is not supported yet.",
|
||||||
connection.getMetaData().
|
connection.getMetaData().
|
||||||
getDatabaseProductName()));
|
getDatabaseProductName()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to determine the location of the migrations for a module.
|
||||||
|
*
|
||||||
|
* @param moduleInfo The module info object of the module.
|
||||||
|
* @param connection The database connection.
|
||||||
|
* @return The location of the database migrations for a specific module.
|
||||||
|
* @throws SQLException If an error on the JDBC site occurs.
|
||||||
|
*/
|
||||||
private String getLocation(final ModuleInfo moduleInfo,
|
private String getLocation(final ModuleInfo moduleInfo,
|
||||||
final Connection connection)
|
final Connection connection)
|
||||||
throws SQLException {
|
throws SQLException {
|
||||||
|
|
||||||
final StringBuffer buffer = new StringBuffer(
|
final StringBuffer buffer = new StringBuffer(
|
||||||
"classpath:/db/migrations/");
|
"classpath:/db/migrations/");
|
||||||
//buffer.append(ModuleUtil.getModulePackageName(module));
|
|
||||||
buffer.append(moduleInfo.getModuleDataPackage());
|
buffer.append(moduleInfo.getModuleDataPackage());
|
||||||
// switch (connection.getMetaData().getDatabaseProductName()) {
|
|
||||||
// case "MySQL":
|
|
||||||
// buffer.append("/mysql");
|
|
||||||
// break;
|
|
||||||
// case "PostgreSQL":
|
|
||||||
// buffer.append("/pgsql");
|
|
||||||
// break;
|
|
||||||
// default:
|
|
||||||
// throw new IntegrationException(String.format(
|
|
||||||
// "Integration failed. Database \"%s\" is not supported yet.",
|
|
||||||
// connection.getMetaData().
|
|
||||||
// getDatabaseProductName()));
|
|
||||||
// }
|
|
||||||
appendDbLocation(buffer, connection);
|
appendDbLocation(buffer, connection);
|
||||||
|
|
||||||
return buffer.toString();
|
return buffer.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method for executing the migrations for a module.
|
||||||
|
*
|
||||||
|
* @param module The module for which the migrations are executed.
|
||||||
|
* @param dataSource The JDBC data source for connecting to the database.
|
||||||
|
* @throws SQLException If an error occurs while applying the migrations.
|
||||||
|
*/
|
||||||
private void migrateModule(final Class<? extends CcmModule> module,
|
private void migrateModule(final Class<? extends CcmModule> module,
|
||||||
final DataSource dataSource) throws SQLException {
|
final DataSource dataSource) throws SQLException {
|
||||||
|
//Get the JDBC connection from the DataSource
|
||||||
final Connection connection = dataSource.getConnection();
|
final Connection connection = dataSource.getConnection();
|
||||||
|
|
||||||
|
//Load the module info for the module
|
||||||
final ModuleInfo moduleInfo = new ModuleInfo();
|
final ModuleInfo moduleInfo = new ModuleInfo();
|
||||||
moduleInfo.load(module);
|
moduleInfo.load(module);
|
||||||
|
|
||||||
|
//Create a Flyway instance for the the module.
|
||||||
final Flyway flyway = new Flyway();
|
final Flyway flyway = new Flyway();
|
||||||
flyway.setDataSource(dataSource);
|
flyway.setDataSource(dataSource);
|
||||||
|
//Set schema correctly for the different databases. Necessary because
|
||||||
|
//different RDBMS handle case different.
|
||||||
if ("H2".equals(connection.getMetaData().getDatabaseProductName())) {
|
if ("H2".equals(connection.getMetaData().getDatabaseProductName())) {
|
||||||
flyway
|
flyway
|
||||||
.setSchemas(getSchemaName(moduleInfo).toUpperCase(Locale.ROOT));
|
.setSchemas(getSchemaName(moduleInfo).toUpperCase(
|
||||||
|
Locale.ROOT));
|
||||||
} else {
|
} else {
|
||||||
flyway.setSchemas(getSchemaName(moduleInfo));
|
flyway.setSchemas(getSchemaName(moduleInfo));
|
||||||
}
|
}
|
||||||
flyway.setLocations(getLocation(moduleInfo, connection));
|
flyway.setLocations(getLocation(moduleInfo, connection));
|
||||||
|
|
||||||
|
//Get current migrations info
|
||||||
final MigrationInfo current = flyway.info().current();
|
final MigrationInfo current = flyway.info().current();
|
||||||
boolean newModule;
|
boolean newModule;
|
||||||
if (current == null) {
|
if (current == null) {
|
||||||
|
|
@ -184,20 +254,24 @@ public class CcmIntegrator implements Integrator {
|
||||||
newModule = false;
|
newModule = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Execute migrations. Flyway will check if there any migrations to apply.
|
||||||
flyway.migrate();
|
flyway.migrate();
|
||||||
|
|
||||||
LOGGER.info("Migrated schema {} in database to version {}",
|
LOGGER.info("Migrated schema {} in database to version {}",
|
||||||
getSchemaName(moduleInfo),
|
getSchemaName(moduleInfo),
|
||||||
flyway.info().current().getVersion());
|
flyway.info().current().getVersion());
|
||||||
|
|
||||||
|
//If a new module was installed register the module in the
|
||||||
|
//installed_modules table with the new status. The ModuleManager will
|
||||||
|
//call the install method of them module.
|
||||||
if (newModule) {
|
if (newModule) {
|
||||||
try (Statement statement = connection.createStatement()) {
|
try (Statement statement = connection.createStatement()) {
|
||||||
statement.execute(String.format(
|
statement.execute(String.format(
|
||||||
"INSERT INTO ccm_core.installed_modules "
|
"INSERT INTO ccm_core.installed_modules "
|
||||||
+ "(module_id, module_class_name, status) "
|
+ "(module_id, module_class_name, status) "
|
||||||
+ "VALUES (%d, '%s', 'NEW')",
|
+ "VALUES (%d, '%s', 'NEW')",
|
||||||
module.getName().hashCode(),
|
module.getName().hashCode(),
|
||||||
module.getName()));
|
module.getName()));
|
||||||
} catch (SQLException ex) {
|
} catch (SQLException ex) {
|
||||||
throw new IntegrationException("Failed to integrate.", ex);
|
throw new IntegrationException("Failed to integrate.", ex);
|
||||||
}
|
}
|
||||||
|
|
@ -211,6 +285,13 @@ public class CcmIntegrator implements Integrator {
|
||||||
//Nothing
|
//Nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the application is shutdown. Used to remove the database
|
||||||
|
* schema of uninstalled modules.
|
||||||
|
*
|
||||||
|
* @param sessionFactory
|
||||||
|
* @param registry
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void disintegrate(final SessionFactoryImplementor sessionFactory,
|
public void disintegrate(final SessionFactoryImplementor sessionFactory,
|
||||||
final SessionFactoryServiceRegistry registry) {
|
final SessionFactoryServiceRegistry registry) {
|
||||||
|
|
@ -220,8 +301,9 @@ public class CcmIntegrator implements Integrator {
|
||||||
LOGGER.info("Removing schemas for modules scheduled for uninstall...");
|
LOGGER.info("Removing schemas for modules scheduled for uninstall...");
|
||||||
try {
|
try {
|
||||||
|
|
||||||
|
//Get JDBC connection
|
||||||
final DataSource dataSource = (DataSource) sessionFactory
|
final DataSource dataSource = (DataSource) sessionFactory
|
||||||
.getProperties().get("javax.persistence.jtaDataSource");
|
.getProperties().get("javax.persistence.jtaDataSource");
|
||||||
connection = dataSource.getConnection();
|
connection = dataSource.getConnection();
|
||||||
System.out.println("checking modules...");
|
System.out.println("checking modules...");
|
||||||
LOGGER.info("Checking modules...");
|
LOGGER.info("Checking modules...");
|
||||||
|
|
@ -231,18 +313,20 @@ public class CcmIntegrator implements Integrator {
|
||||||
moduleInfo.load(module);
|
moduleInfo.load(module);
|
||||||
|
|
||||||
try (Statement query = connection.createStatement();
|
try (Statement query = connection.createStatement();
|
||||||
|
//Check status of each module
|
||||||
ResultSet result = query.executeQuery(
|
ResultSet result = query.executeQuery(
|
||||||
String.format("SELECT module_class_name, status "
|
String.format("SELECT module_class_name, status "
|
||||||
+ "FROM ccm_core.installed_modules "
|
+ "FROM ccm_core.installed_modules "
|
||||||
+ "WHERE module_class_name = '%s'",
|
+ "WHERE module_class_name = '%s'",
|
||||||
module.getClass().getName()))) {
|
module.getClass().getName()))) {
|
||||||
|
|
||||||
System.out.printf("Checking status of module %s...%n",
|
System.out.printf("Checking status of module %s...%n",
|
||||||
module.getClass().getName());
|
module.getClass().getName());
|
||||||
|
|
||||||
|
//If there modules marked for uninstall remove the schema
|
||||||
|
//of the module from the database.
|
||||||
if (result.next() && ModuleStatus.UNINSTALL.toString()
|
if (result.next() && ModuleStatus.UNINSTALL.toString()
|
||||||
.equals(
|
.equals(result.getString("status"))) {
|
||||||
result.getString("status"))) {
|
|
||||||
|
|
||||||
LOGGER.info("Removing schema for module %s...",
|
LOGGER.info("Removing schema for module %s...",
|
||||||
module.getClass().getName());
|
module.getClass().getName());
|
||||||
|
|
@ -254,17 +338,18 @@ public class CcmIntegrator implements Integrator {
|
||||||
moduleInfo.getModuleName());
|
moduleInfo.getModuleName());
|
||||||
flyway.clean();
|
flyway.clean();
|
||||||
|
|
||||||
|
//Delete the module from the installed modules table.
|
||||||
try (final Statement statement = connection
|
try (final Statement statement = connection
|
||||||
.createStatement()) {
|
.createStatement()) {
|
||||||
statement.addBatch(String.format(
|
statement.addBatch(String.format(
|
||||||
"DELETE FROM ccm_core.installed_modules "
|
"DELETE FROM ccm_core.installed_modules "
|
||||||
+ "WHERE module_class_name = '%s'",
|
+ "WHERE module_class_name = '%s'",
|
||||||
module.getClass().getName()));
|
module.getClass().getName()));
|
||||||
statement.executeBatch();
|
statement.executeBatch();
|
||||||
LOGGER.info("Done.");
|
LOGGER.info("Done.");
|
||||||
} catch (SQLException ex) {
|
} catch (SQLException ex) {
|
||||||
throw new IntegrationException(
|
throw new IntegrationException(
|
||||||
"Failed to desintegrate", ex);
|
"Failed to desintegrate", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue