CCM NG: JavaDoc for the CcmIntegrator

git-svn-id: https://svn.libreccm.org/ccm/ccm_ng@3614 8810af33-2d31-482b-a856-94f89814c4df
pull/2/head
jensp 2015-09-14 09:37:02 +00:00
parent f448802139
commit b754e4b04c
1 changed files with 130 additions and 45 deletions

View File

@ -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);
} }
} }