diff --git a/ccm-core/src/main/java/org/libreccm/modules/CcmIntegrator.java b/ccm-core/src/main/java/org/libreccm/modules/CcmIntegrator.java
index 45946018b..6cd5490c8 100644
--- a/ccm-core/src/main/java/org/libreccm/modules/CcmIntegrator.java
+++ b/ccm-core/src/main/java/org/libreccm/modules/CcmIntegrator.java
@@ -40,20 +40,42 @@ import java.util.ServiceLoader;
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
+ * Flyway 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 Jens Pelzetter
*/
public class CcmIntegrator implements Integrator {
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 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
public void integrate(final Configuration configuration,
final SessionFactoryImplementor sessionFactory,
final SessionFactoryServiceRegistry registry) {
+ //Find all modules in the classpath
LOGGER.info("Retrieving modules...");
modules = ServiceLoader.load(CcmModule.class);
for (final CcmModule module : modules) {
@@ -65,25 +87,29 @@ public class CcmIntegrator implements Integrator {
Connection connection = null;
try {
+ //Create dependency tree for the modules
final DependencyTreeManager treeManager
- = new DependencyTreeManager();
+ = new DependencyTreeManager();
final List tree = treeManager.generateTree(modules);
final List orderedNodes = treeManager.orderModules(tree);
+ //Get DataSource and Connection from the sessionFactory of
+ //Hibernate.
final DataSource dataSource = (DataSource) sessionFactory.
- getProperties().get("javax.persistence.jtaDataSource");
+ getProperties().get("javax.persistence.jtaDataSource");
connection = dataSource.getConnection();
//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();
flyway.setDataSource(dataSource);
final StringBuffer buffer = new StringBuffer(
- "db/migrations/org/libreccm/base");
+ "db/migrations/org/libreccm/base");
appendDbLocation(buffer, connection);
flyway.setLocations(buffer.toString());
flyway.migrate();
+ //Migrate the modules
for (final TreeNode node : orderedNodes) {
migrateModule(node.getModule().getClass(), dataSource);
@@ -92,6 +118,7 @@ public class CcmIntegrator implements Integrator {
}
}
+ //Build Hibernate mappings for the entities.
configuration.buildMappings();
} catch (DependencyException | SQLException ex) {
@@ -103,75 +130,118 @@ public class CcmIntegrator implements Integrator {
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) {
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:
+ *
+ *
+ *
+ * | Database Product Name |
+ * Location |
+ *
+ *
+ * H2 |
+ * /h2 |
+ *
+ *
+ * PostgreSQL |
+ * /pgsql |
+ *
+ *
+ *
+ *
+ * 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,
final Connection connection)
- throws SQLException {
+ throws SQLException {
switch (connection.getMetaData().getDatabaseProductName()) {
case "H2":
buffer.append("/h2");
break;
-// 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()));
+ "Integration failed. Database \"%s\" is not supported yet.",
+ connection.getMetaData().
+ 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,
final Connection connection)
- throws SQLException {
+ throws SQLException {
final StringBuffer buffer = new StringBuffer(
- "classpath:/db/migrations/");
- //buffer.append(ModuleUtil.getModulePackageName(module));
+ "classpath:/db/migrations/");
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);
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,
final DataSource dataSource) throws SQLException {
+ //Get the JDBC connection from the DataSource
final Connection connection = dataSource.getConnection();
+ //Load the module info for the module
final ModuleInfo moduleInfo = new ModuleInfo();
moduleInfo.load(module);
+ //Create a Flyway instance for the the module.
final Flyway flyway = new Flyway();
flyway.setDataSource(dataSource);
+ //Set schema correctly for the different databases. Necessary because
+ //different RDBMS handle case different.
if ("H2".equals(connection.getMetaData().getDatabaseProductName())) {
flyway
- .setSchemas(getSchemaName(moduleInfo).toUpperCase(Locale.ROOT));
+ .setSchemas(getSchemaName(moduleInfo).toUpperCase(
+ Locale.ROOT));
} else {
flyway.setSchemas(getSchemaName(moduleInfo));
}
flyway.setLocations(getLocation(moduleInfo, connection));
+ //Get current migrations info
final MigrationInfo current = flyway.info().current();
boolean newModule;
if (current == null) {
@@ -184,20 +254,24 @@ public class CcmIntegrator implements Integrator {
newModule = false;
}
+ //Execute migrations. Flyway will check if there any migrations to apply.
flyway.migrate();
LOGGER.info("Migrated schema {} in database to version {}",
getSchemaName(moduleInfo),
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) {
try (Statement statement = connection.createStatement()) {
statement.execute(String.format(
- "INSERT INTO ccm_core.installed_modules "
- + "(module_id, module_class_name, status) "
- + "VALUES (%d, '%s', 'NEW')",
- module.getName().hashCode(),
- module.getName()));
+ "INSERT INTO ccm_core.installed_modules "
+ + "(module_id, module_class_name, status) "
+ + "VALUES (%d, '%s', 'NEW')",
+ module.getName().hashCode(),
+ module.getName()));
} catch (SQLException ex) {
throw new IntegrationException("Failed to integrate.", ex);
}
@@ -211,6 +285,13 @@ public class CcmIntegrator implements Integrator {
//Nothing
}
+ /**
+ * Called when the application is shutdown. Used to remove the database
+ * schema of uninstalled modules.
+ *
+ * @param sessionFactory
+ * @param registry
+ */
@Override
public void disintegrate(final SessionFactoryImplementor sessionFactory,
final SessionFactoryServiceRegistry registry) {
@@ -220,8 +301,9 @@ public class CcmIntegrator implements Integrator {
LOGGER.info("Removing schemas for modules scheduled for uninstall...");
try {
+ //Get JDBC connection
final DataSource dataSource = (DataSource) sessionFactory
- .getProperties().get("javax.persistence.jtaDataSource");
+ .getProperties().get("javax.persistence.jtaDataSource");
connection = dataSource.getConnection();
System.out.println("checking modules...");
LOGGER.info("Checking modules...");
@@ -231,18 +313,20 @@ public class CcmIntegrator implements Integrator {
moduleInfo.load(module);
try (Statement query = connection.createStatement();
+ //Check status of each module
ResultSet result = query.executeQuery(
- String.format("SELECT module_class_name, status "
- + "FROM ccm_core.installed_modules "
+ String.format("SELECT module_class_name, status "
+ + "FROM ccm_core.installed_modules "
+ "WHERE module_class_name = '%s'",
- module.getClass().getName()))) {
+ module.getClass().getName()))) {
System.out.printf("Checking status of module %s...%n",
module.getClass().getName());
+ //If there modules marked for uninstall remove the schema
+ //of the module from the database.
if (result.next() && ModuleStatus.UNINSTALL.toString()
- .equals(
- result.getString("status"))) {
+ .equals(result.getString("status"))) {
LOGGER.info("Removing schema for module %s...",
module.getClass().getName());
@@ -254,17 +338,18 @@ public class CcmIntegrator implements Integrator {
moduleInfo.getModuleName());
flyway.clean();
+ //Delete the module from the installed modules table.
try (final Statement statement = connection
- .createStatement()) {
+ .createStatement()) {
statement.addBatch(String.format(
- "DELETE FROM ccm_core.installed_modules "
- + "WHERE module_class_name = '%s'",
- module.getClass().getName()));
+ "DELETE FROM ccm_core.installed_modules "
+ + "WHERE module_class_name = '%s'",
+ module.getClass().getName()));
statement.executeBatch();
LOGGER.info("Done.");
} catch (SQLException ex) {
throw new IntegrationException(
- "Failed to desintegrate", ex);
+ "Failed to desintegrate", ex);
}
}