CCM NG: JavaDoc for the module system

git-svn-id: https://svn.libreccm.org/ccm/ccm_ng@3615 8810af33-2d31-482b-a856-94f89814c4df
pull/2/head
jensp 2015-09-14 12:41:18 +00:00
parent b754e4b04c
commit ae8943a75c
17 changed files with 381 additions and 202 deletions

View File

@ -42,13 +42,32 @@ public interface CcmModule {
/** /**
* An implementation of this method is called after the module is installed. * An implementation of this method is called after the module is installed.
* *
* @param event * This method can be used to create initial data.
*
* @param event @see InstallEvent
*/ */
void install(InstallEvent event); void install(InstallEvent event);
/**
* Called each time the application is restarted.
*
* @param event @see InitEvent
*/
void init(InitEvent event); void init(InitEvent event);
/**
* Called each time the application is shutdown.
*
* @param event @see ShutdownEvent
*/
void shutdown(ShutdownEvent event); void shutdown(ShutdownEvent event);
/**
* Called if the module is uninstalled. The implementation of this
* method should remove all data created by the {@link #install(InstallEvent)}
* method.
*
* @param event @see UnInstallEvent
*/
void uninstall(UnInstallEvent event); void uninstall(UnInstallEvent event);
} }

View File

@ -25,9 +25,13 @@ import javax.servlet.annotation.WebListener;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.libreccm.modules.ModuleManager;
/** /**
* An implementation of the {@link ServletContextListener} interface for
* initialising the modules.
*
* The real work is done by the {@link ModuleManager}. This class only delegates
* to the {@code ModuleManager}.
* *
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a> * @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/ */

View File

@ -19,6 +19,8 @@
package org.libreccm.modules; package org.libreccm.modules;
/** /**
* Thrown by the {@link DependencyTreeManager} if something is wrong with the
* dependency tree.
* *
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a> * @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/ */
@ -32,7 +34,7 @@ public class DependencyException extends Exception {
*/ */
public DependencyException() { public DependencyException() {
super(); super();
//Nothin more //Nothing more
} }
/** /**

View File

@ -37,6 +37,8 @@ import org.apache.maven.artifact.versioning.ComparableVersion;
* method. The list returned by {@link #orderModules(java.util.List)} contains * method. The list returned by {@link #orderModules(java.util.List)} contains
* all modules in order. * all modules in order.
* *
* This class is <strong>not</strong> part of the public API.
*
* More information about topological sorting: * More information about topological sorting:
* <a href="https://en.wikipedia.org/wiki/Topological_sorting">https://en.wikipedia.org/wiki/Topological_sorting</a> * <a href="https://en.wikipedia.org/wiki/Topological_sorting">https://en.wikipedia.org/wiki/Topological_sorting</a>
* *
@ -47,21 +49,34 @@ final class DependencyTreeManager {
private static final Logger LOGGER = LogManager.getLogger( private static final Logger LOGGER = LogManager.getLogger(
DependencyTreeManager.class); DependencyTreeManager.class);
/**
* Initialises the tree with the provided list of modules.
*
* @param modules The module for which a dependency tree is generated.
* @return An ordered list of tree nodes.
*
* @throws DependencyException If something is wrong with the dependency
* tree. For example if a module on which another module depends is missing
* or if a cycle is detected in the dependency tree.
*/
public List<TreeNode> generateTree(final Iterable<CcmModule> modules) public List<TreeNode> generateTree(final Iterable<CcmModule> modules)
throws DependencyException { throws DependencyException {
LOGGER.info("Starting to generate dependency tree..."); LOGGER.info("Starting to generate dependency tree...");
//Create the tree nodes. A HashMap is used to avoid duplicates and
//the lookup the nodes based on their name.
final Map<String, TreeNode> nodes = new HashMap<>(); final Map<String, TreeNode> nodes = new HashMap<>();
for (final CcmModule module : modules) { for (final CcmModule module : modules) {
createTreeNode(module, nodes); createTreeNode(module, nodes);
} }
//Add the dependency relations to the nodes
for (final CcmModule module : modules) { for (final CcmModule module : modules) {
addDependencyRelations(module, nodes); addDependencyRelations(module, nodes);
} }
//Generate the node list
final List<TreeNode> nodeList = new ArrayList<>(); final List<TreeNode> nodeList = new ArrayList<>();
for (final Map.Entry<String, TreeNode> entry : nodes.entrySet()) { for (final Map.Entry<String, TreeNode> entry : nodes.entrySet()) {
nodeList.add(entry.getValue()); nodeList.add(entry.getValue());
@ -72,15 +87,29 @@ final class DependencyTreeManager {
return nodeList; return nodeList;
} }
/**
* Generates an ordered list of the tree nodes which can be used to
* initialise the modules in correct order.
*
* In this method the topological sorting happens.
*
* @param dependencyTree The list of tree nodes of the dependency tree.
* @return A ordered list of the tree nodes.
* @throws DependencyException If something is wrong with the dependency
* tree.
*/
public List<TreeNode> orderModules(final List<TreeNode> dependencyTree) public List<TreeNode> orderModules(final List<TreeNode> dependencyTree)
throws DependencyException { throws DependencyException {
LOGGER.info("Creating an ordered list from the dependency tree..."); LOGGER.info("Creating an ordered list from the dependency tree...");
//List for the ordered and resolved nodes/modules.
final List<TreeNode> orderedModules = new ArrayList<>(); final List<TreeNode> orderedModules = new ArrayList<>();
final List<TreeNode> resolvedModules = new ArrayList<>(); final List<TreeNode> resolvedModules = new ArrayList<>();
LOGGER.info("Looking for modules which do not depend on any other " LOGGER.info("Looking for modules which do not depend on any other "
+ "modules..."); + "modules...");
//Find all modules which do not depend on any other module. These
//modules are used as starting point for the sorting.
for (final TreeNode node : dependencyTree) { for (final TreeNode node : dependencyTree) {
if (node.getDependsOn().isEmpty()) { if (node.getDependsOn().isEmpty()) {
LOGGER.info( LOGGER.info(
@ -93,21 +122,28 @@ final class DependencyTreeManager {
LOGGER.info("Ordering remaining nodes..."); LOGGER.info("Ordering remaining nodes...");
while (!resolvedModules.isEmpty()) { while (!resolvedModules.isEmpty()) {
//Remove the first node from the resolved modules list
final TreeNode current = resolvedModules.remove(0); final TreeNode current = resolvedModules.remove(0);
LOGGER.info("\tProcessing node for module \"{}\"...", LOGGER.info("\tProcessing node for module \"{}\"...",
current.getModuleInfo().getModuleName()); current.getModuleInfo().getModuleName());
//Add the node to the ordered modules list.
orderedModules.add(current); orderedModules.add(current);
//Remove the edges to the current node.
for (final TreeNode dependent : current.getDependentModules()) { for (final TreeNode dependent : current.getDependentModules()) {
dependent.removeDependsOn(current); dependent.removeDependsOn(current);
//If the dependent node has node more dependsOn relations put
//the node into the resolved modules list.
if (dependent.getDependsOn().isEmpty()) { if (dependent.getDependsOn().isEmpty()) {
resolvedModules.add(dependent); resolvedModules.add(dependent);
} }
} }
} }
//Check if all nodes have been ordered. If not the tree has at least on
//on cycle and can't be processed.
if (orderedModules.size() == dependencyTree.size()) { if (orderedModules.size() == dependencyTree.size()) {
LOGGER.info("Dependency graph proceessed successfully. " LOGGER.info("Dependency graph proceessed successfully. "
+ "Modules in order:"); + "Modules in order:");
@ -123,6 +159,8 @@ final class DependencyTreeManager {
} }
} }
//Helper method for creating a tree node for a module and putting the
//node into the map of tree nodes.
private void createTreeNode(final CcmModule module, private void createTreeNode(final CcmModule module,
final Map<String, TreeNode> nodes) { final Map<String, TreeNode> nodes) {
final TreeNode node = new TreeNode(module); final TreeNode node = new TreeNode(module);
@ -132,17 +170,29 @@ final class DependencyTreeManager {
nodes.put(node.getModuleInfo().getModuleName(), node); nodes.put(node.getModuleInfo().getModuleName(), node);
} }
/**
* Helper method for adding the dependency relations for a module to the
* nodes.
*
* @param module The module.
* @param nodes The map of nodes.
*
* @throws DependencyException If something goes wrong.
*/
private void addDependencyRelations(final CcmModule module, private void addDependencyRelations(final CcmModule module,
final Map<String, TreeNode> nodes) final Map<String, TreeNode> nodes)
throws DependencyException { throws DependencyException {
//Load the module info for the current module
final ModuleInfo moduleInfo = new ModuleInfo(); final ModuleInfo moduleInfo = new ModuleInfo();
moduleInfo.load(module); moduleInfo.load(module);
LOGGER.info("Adding dependency relations for module \"{}\"...", LOGGER.info("Adding dependency relations for module \"{}\"...",
moduleInfo.getModuleName()); moduleInfo.getModuleName());
//Get the name of the module from the module info.
final String moduleName = moduleInfo.getModuleName(); final String moduleName = moduleInfo.getModuleName();
//Check if the nodes map has an entry for the module.
if (!nodes.containsKey(moduleName)) { if (!nodes.containsKey(moduleName)) {
LOGGER.fatal("Modules nodes map does contain an entry for \"{}\". " LOGGER.fatal("Modules nodes map does contain an entry for \"{}\". "
+ "That should not happen.", + "That should not happen.",
@ -153,62 +203,33 @@ final class DependencyTreeManager {
moduleName)); moduleName));
} }
//Get the node from the map
final TreeNode node = nodes.get(moduleName); final TreeNode node = nodes.get(moduleName);
LOGGER.info("Processing required modules for module \"{}\"...", LOGGER.info("Processing required modules for module \"{}\"...",
node.getModuleInfo().getModuleName()); node.getModuleInfo().getModuleName());
//Process the modules required by the current module and add the
//dependency relations.
for (final RequiredModule requiredModule : node.getModuleInfo(). for (final RequiredModule requiredModule : node.getModuleInfo().
getRequiredModules()) { getRequiredModules()) {
// final ModuleInfo requiredModuleInfo = new ModuleInfo();
// requiredModuleInfo.load(requiredModule.module());
//
// LOGGER.info("\tModule \"{}\" requires module \"{}\".",
// node.getModuleInfo().getModuleName(),
// requiredModuleInfo.getModuleName());
//
// if (!nodes.containsKey(requiredModuleInfo.getModuleName())) {
//
// LOGGER.fatal("Required module \"{}\" no found.",
// requiredModuleInfo.getModuleName());
//
// throw new DependencyException(String.format(
// "Module \"%s\" depends on module \"%s\" but the dependency "
// + "tree does contain an entry for module \"%s\".",
// moduleInfo.getModuleName(),
// requiredModuleInfo.getModuleName(),
// requiredModuleInfo.getModuleName()));
// }
//
// final TreeNode dependencyNode = nodes.get(requiredModuleInfo.
// getModuleName());
//
// //Check version
// if (!validateVersion(dependencyNode.getModuleInfo().
// getModuleVersion(),
// requiredModule.minVersion(),
// requiredModule.maxVersion())) {
// throw new DependencyException(String.format(
// "The required module is avialable but in the correct "
// + "version. "
// + "Available version: \"%s\"; "
// + "minimal required version: \"%s\"; "
// + "maximum required version: \"%s\"",
// dependencyNode.getModuleInfo().getModuleVersion(),
// requiredModule.minVersion(),
// requiredModule.maxVersion()));
// }
//
// node.addDependsOn(dependencyNode);
// dependencyNode.addDependentModule(node);
addDependencyRelation(nodes, node, requiredModule); addDependencyRelation(nodes, node, requiredModule);
} }
} }
/**
* Helper method for adding a single dependency relation.
*
* @param nodes The map of tree nodes.
* @param node The node to which the dependency relations are added.
* @param requiredModule The module required by the current module/node.
* @throws DependencyException
*/
private void addDependencyRelation(final Map<String, TreeNode> nodes, private void addDependencyRelation(final Map<String, TreeNode> nodes,
final TreeNode node, final TreeNode node,
final RequiredModule requiredModule) final RequiredModule requiredModule)
throws DependencyException { throws DependencyException {
//Get the module info for the required module
final ModuleInfo requiredInfo = new ModuleInfo(); final ModuleInfo requiredInfo = new ModuleInfo();
requiredInfo.load(requiredModule.module()); requiredInfo.load(requiredModule.module());
@ -216,6 +237,7 @@ final class DependencyTreeManager {
node.getModuleInfo().getModuleName(), node.getModuleInfo().getModuleName(),
requiredInfo.getModuleName()); requiredInfo.getModuleName());
//Check if the nodes list has an entry for the required module.
if (!nodes.containsKey(requiredInfo.getModuleName())) { if (!nodes.containsKey(requiredInfo.getModuleName())) {
LOGGER.fatal("Required module \"{}\" no found.", LOGGER.fatal("Required module \"{}\" no found.",
requiredInfo.getModuleName()); requiredInfo.getModuleName());
@ -227,6 +249,7 @@ final class DependencyTreeManager {
requiredInfo.getModuleName())); requiredInfo.getModuleName()));
} }
//Validate the version of the required module.
final TreeNode dependencyNode = nodes.get(requiredInfo. final TreeNode dependencyNode = nodes.get(requiredInfo.
getModuleName()); getModuleName());
if (!validateVersion(dependencyNode.getModuleInfo(). if (!validateVersion(dependencyNode.getModuleInfo().
@ -244,6 +267,7 @@ final class DependencyTreeManager {
requiredModule.maxVersion())); requiredModule.maxVersion()));
} }
//Create the dependencies relations.
node.addDependsOn(dependencyNode); node.addDependsOn(dependencyNode);
dependencyNode.addDependentModule(node); dependencyNode.addDependentModule(node);
} }

View File

@ -19,6 +19,10 @@
package org.libreccm.modules; package org.libreccm.modules;
/** /**
* Event object for the module initialisation.
*
* @see ModuleEvent
*
* *
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a> * @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/ */

View File

@ -19,6 +19,9 @@
package org.libreccm.modules; package org.libreccm.modules;
/** /**
* Event object for the module initialisation.
*
* @see ModuleEvent
* *
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a> * @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/ */

View File

@ -30,6 +30,8 @@ import javax.persistence.Table;
import static org.libreccm.core.CoreConstants.DB_SCHEMA; import static org.libreccm.core.CoreConstants.DB_SCHEMA;
/** /**
* A JPA entity bean for for installed modules table for use in the LibreCCM
* administration UI.
* *
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a> * @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/ */
@ -49,9 +51,15 @@ public class InstalledModule implements Serializable {
@Column(name = "MODULE_ID") @Column(name = "MODULE_ID")
private int moduleId; private int moduleId;
/**
* The fully qualified name of the module class.
*/
@Column(name = "MODULE_CLASS_NAME", length = 2048, unique = true) @Column(name = "MODULE_CLASS_NAME", length = 2048, unique = true)
private String moduleClassName; private String moduleClassName;
/**
* The status of the module.
*/
@Column(name = "STATUS") @Column(name = "STATUS")
@Enumerated(EnumType.STRING) @Enumerated(EnumType.STRING)
private ModuleStatus status; private ModuleStatus status;

View File

@ -19,6 +19,7 @@
package org.libreccm.modules; package org.libreccm.modules;
/** /**
* Thrown if an error occurs in the {@link CcmIntegrator}.
* *
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a> * @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/ */

View File

@ -26,7 +26,7 @@ import static java.lang.annotation.ElementType.*;
import java.lang.annotation.Target; import java.lang.annotation.Target;
/** /**
* Annotation for describing some meta of a module. * Annotation for describing some meta data of a module.
* *
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a> * @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/ */

View File

@ -21,6 +21,8 @@ package org.libreccm.modules;
import javax.persistence.EntityManager; import javax.persistence.EntityManager;
/** /**
* Base class for the module lifecycle events. Provides access to the JPA
* {@code EntityManager}.
* *
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a> * @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/ */

View File

@ -32,21 +32,47 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
/** /**
* A representation of the module metadata combining the data from the modules
* info file and the annotations on the module class.
* *
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a> * @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/ */
public class ModuleInfo { public class ModuleInfo {
/**
* Constant for the artifactId property in the module info file.
*/
private static final String ARTIFACT_ID = "artifactId"; private static final String ARTIFACT_ID = "artifactId";
/**
* Constant for the groupId property in the module info file.
*/
private static final String GROUP_ID = "groupId"; private static final String GROUP_ID = "groupId";
/**
* Constant for the version property in the module info file.
*/
private static final String VERSION = "version"; private static final String VERSION = "version";
private static final Logger LOGGER = LogManager.getLogger(ModuleInfo.class); private static final Logger LOGGER = LogManager.getLogger(ModuleInfo.class);
/**
* The name of the module (artifact id).
*/
private transient String moduleName; private transient String moduleName;
/**
* The data package of the module (group id).
*/
private transient String moduleDataPackage; private transient String moduleDataPackage;
/**
* The entities provided by the module.
*/
private transient Class<?>[] moduleEntities; private transient Class<?>[] moduleEntities;
/**
* The version of the module.
*/
private transient String moduleVersion; private transient String moduleVersion;
/**
* The modules required by the described module.
*/
private transient RequiredModule[] requiredModules; private transient RequiredModule[] requiredModules;
public ModuleInfo() { public ModuleInfo() {
@ -61,36 +87,47 @@ public class ModuleInfo {
return moduleDataPackage; return moduleDataPackage;
} }
public String getDbSchemaName() { // /**
return moduleName.toLowerCase(Locale.ROOT).replace("-", "_"); // * Gets the database schema name for the module. That is the
} // * {@link moduleName} with all hyphens replaced by underscore (hyphens can
// * be nasty in SQL).
public String getDbScriptsLocation(final Connection connection) // *
throws SQLException { // * @return The name of the database schema of the module.
final StringBuffer buffer // */
= new StringBuffer("classpath:/db/migrations/"); // public String getDbSchemaName() {
buffer.append(moduleDataPackage); // return moduleName.toLowerCase(Locale.ROOT).replace("-", "_");
switch (connection.getMetaData().getDatabaseProductName()) { // }
case "H2": { // /**
buffer.append("/h2"); // *
break; // * @param connection
} // * @return
case "MySQL": // * @throws SQLException
buffer.append("/mysql"); // */
break; // public String getDbScriptsLocation(final Connection connection)
case "PostgreSQL": // throws SQLException {
buffer.append("/postgresql"); // final StringBuffer buffer
break; // = new StringBuffer("classpath:/db/migrations/");
default: // buffer.append(moduleDataPackage);
throw new IntegrationException(String.format( // switch (connection.getMetaData().getDatabaseProductName()) {
"Integration failed. Database \"%s\" is not supported yet.", // case "H2": {
connection.getMetaData(). // buffer.append("/h2");
getDatabaseProductName())); // break;
} // }
// case "MySQL":
return buffer.toString(); // buffer.append("/mysql");
} // break;
// case "PostgreSQL":
// buffer.append("/postgresql");
// break;
// default:
// throw new IntegrationException(String.format(
// "Integration failed. Database \"%s\" is not supported yet.",
// connection.getMetaData().
// getDatabaseProductName()));
// }
//
// return buffer.toString();
// }
public String getModuleVersion() { public String getModuleVersion() {
return moduleVersion; return moduleVersion;
} }
@ -107,85 +144,29 @@ public class ModuleInfo {
load(module.getClass()); load(module.getClass());
} }
/**
* Loads the module info data.
*
* @param moduleClass The module class for which the module data is loaded.
*/
public void load(final Class<? extends CcmModule> moduleClass) { public void load(final Class<? extends CcmModule> moduleClass) {
LOGGER.info("Reading module info for {}...", moduleClass.getName()); LOGGER.info("Reading module info for {}...", moduleClass.getName());
final Module annotation = moduleClass.getAnnotation(Module.class); final Module annotation = moduleClass.getAnnotation(Module.class);
final Properties properties = loadModuleInfoFile(moduleClass); final Properties properties = loadModuleInfoFile(moduleClass);
// final Properties properties = new Properties();
//
// final String path = String.format("/module-info/%s.properties",
// moduleClass.getName());
// LOGGER.info("Trying to retrieve module info for module {} from {}...",
// moduleClass.getName(),
// path);
// final InputStream stream = moduleClass.getResourceAsStream(path);
// if (stream == null) {
// LOGGER.warn("No module info for {} found at {}",
// moduleClass.getName(),
// path);
// } else {
// try {
// properties.load(stream);
// } catch (IOException ex) {
// LOGGER.error("Failed to read module info for {} at {}.",
// moduleClass.getName(),
// path);
// LOGGER.error("Cause: ", ex);
// }
// }
LOGGER.info("Reading module name..."); LOGGER.info("Reading module name...");
// if (annotation.name() != null && !annotation.name().isEmpty()) {
// moduleName = annotation.name();
// } else if (properties.getProperty(ARTIFACT_ID) != null
// && !properties.getProperty(ARTIFACT_ID).isEmpty()) {
// moduleName = properties.getProperty(ARTIFACT_ID);
// } else {
// LOGGER.warn(
// "The module was not specificied by the module annotation "
// + "or by the module info file. Creating name from "
// + "simple name of the module class.");
// moduleName = moduleClass.getSimpleName().toLowerCase();
// }
LOGGER.info("Reading module name..."); LOGGER.info("Reading module name...");
moduleName = readModuleName(moduleClass, annotation, properties); moduleName = readModuleName(moduleClass, annotation, properties);
LOGGER.info("Module name is \"{}\".", moduleName); LOGGER.info("Module name is \"{}\".", moduleName);
LOGGER.info("Reading module package name..."); LOGGER.info("Reading module package name...");
// if (annotation.packageName() != null
// && !annotation.packageName().isEmpty()) {
// moduleDataPackage = annotation.packageName();
// } else if (properties.getProperty(GROUP_ID) != null
// && !properties.getProperty(GROUP_ID).isEmpty()) {
// moduleDataPackage = String.format("%s/%s",
// properties.getProperty(GROUP_ID),
// properties.
// getProperty(ARTIFACT_ID).replace(
// "-", "_"));
// } else {
// LOGGER.warn("The module data package was specified by the module "
// + "annotation nore was an group id found in the module info"
// + "file. Creating data package name from the name of the "
// + "module class.");
// moduleDataPackage = moduleClass.getName().toLowerCase();
// }
moduleDataPackage = readModulePackageName(moduleClass, moduleDataPackage = readModulePackageName(moduleClass,
annotation, annotation,
properties); properties);
LOGGER.info("Module data package is \"{}\".", moduleDataPackage); LOGGER.info("Module data package is \"{}\".", moduleDataPackage);
LOGGER.info("Reading module version..."); LOGGER.info("Reading module version...");
// if (annotation.version() != null && !annotation.version().isEmpty()) {
// moduleVersion = annotation.version();
// } else if (properties.getProperty(VERSION) != null
// && !properties.getProperty(VERSION).isEmpty()) {
// moduleVersion = properties.getProperty(VERSION);
// } else {
// LOGGER.warn("Module version is not specified by the module "
// + "annotation or in the module info file. Module version is "
// + "undefinied. This can lead to all sorts of strange errors!");
// }
moduleVersion = readModuleVersion(annotation, properties); moduleVersion = readModuleVersion(annotation, properties);
LOGGER.info("Module version is \"{}.\"", moduleVersion); LOGGER.info("Module version is \"{}.\"", moduleVersion);
@ -193,8 +174,16 @@ public class ModuleInfo {
moduleEntities = annotation.entities(); moduleEntities = annotation.entities();
} }
/**
* Load the module info properties file.
*
* @param moduleClass The class for which the module info properties file is
* loaded.
*
* @return The properties from the module info properties file.
*/
private Properties loadModuleInfoFile( private Properties loadModuleInfoFile(
final Class<? extends CcmModule> moduleClass) { final Class<? extends CcmModule> moduleClass) {
final Properties moduleInfo = new Properties(); final Properties moduleInfo = new Properties();
final String path = String.format("/module-info/%s.properties", final String path = String.format("/module-info/%s.properties",
@ -220,19 +209,30 @@ public class ModuleInfo {
return moduleInfo; return moduleInfo;
} }
/**
* Reads the module name. If the module annotation of the module class has a
* value for {@code name} that value is used. Otherwise the name from the
* module info file is used. If this name is also empty the simple name of
* the module class is used.
*
* @param moduleClass The module class.
* @param annotation The module annotation of the module class.
* @param moduleInfo The module info properties.
* @return The name of the module.
*/
private String readModuleName(final Class<? extends CcmModule> moduleClass, private String readModuleName(final Class<? extends CcmModule> moduleClass,
final Module annotation, final Module annotation,
final Properties moduleInfo) { final Properties moduleInfo) {
@SuppressWarnings( @SuppressWarnings(
"PMD.LongVariable") "PMD.LongVariable")
final boolean annotationHasModuleName = annotation.name() != null final boolean annotationHasModuleName = annotation.name() != null
&& !annotation.name() && !annotation.name()
.isEmpty(); .isEmpty();
@SuppressWarnings("PMD.LongVariable") @SuppressWarnings("PMD.LongVariable")
final boolean moduleInfoHasModuleName = moduleInfo.getProperty( final boolean moduleInfoHasModuleName = moduleInfo.getProperty(
ARTIFACT_ID) ARTIFACT_ID)
!= null && !moduleInfo != null && !moduleInfo
.getProperty(ARTIFACT_ID).isEmpty(); .getProperty(ARTIFACT_ID).isEmpty();
if (annotationHasModuleName) { if (annotationHasModuleName) {
return annotation.name(); return annotation.name();
@ -240,29 +240,40 @@ public class ModuleInfo {
return moduleInfo.getProperty(ARTIFACT_ID); return moduleInfo.getProperty(ARTIFACT_ID);
} else { } else {
LOGGER.warn( LOGGER.warn(
"The module was not specificied by the module annotation " "The module was not specificied by the module annotation "
+ "or by the module info file. Creating name from " + "or by the module info file. Creating name from "
+ "simple name of the module class."); + "simple name of the module class.");
return moduleClass.getSimpleName().toLowerCase(); return moduleClass.getSimpleName().toLowerCase();
} }
} }
/**
* Reads the module name. If the module annotation of the module class has a
* value for {@code packageName} that value is used. Otherwise the groupId
* from the module info file is used. If this name is also empty the package
* name of the module class is used.
*
* @param moduleClass The module class.
* @param annotation The module annotation of the module class.
* @param moduleInfo The module info properties.
* @return The name of the module.
*/
private String readModulePackageName( private String readModulePackageName(
final Class<? extends CcmModule> moduleClass, final Class<? extends CcmModule> moduleClass,
final Module annotation, final Module annotation,
final Properties moduleInfo) { final Properties moduleInfo) {
@SuppressWarnings( @SuppressWarnings(
"PMD.LongVariable") "PMD.LongVariable")
final boolean annotationHasPackageName = annotation.packageName() final boolean annotationHasPackageName = annotation.packageName()
!= null != null
&& !annotation && !annotation
.packageName() .packageName()
.isEmpty(); .isEmpty();
@SuppressWarnings("PMD.LongVariable") @SuppressWarnings("PMD.LongVariable")
final boolean moduleInfoHasPackageName = moduleInfo final boolean moduleInfoHasPackageName = moduleInfo
.getProperty(GROUP_ID) != null && !moduleInfo.getProperty( .getProperty(GROUP_ID) != null && !moduleInfo.getProperty(
GROUP_ID).isEmpty(); GROUP_ID).isEmpty();
if (annotationHasPackageName) { if (annotationHasPackageName) {
return annotation.packageName(); return annotation.packageName();
} else if (moduleInfoHasPackageName) { } else if (moduleInfoHasPackageName) {
@ -273,27 +284,39 @@ public class ModuleInfo {
"_")); "_"));
} else { } else {
LOGGER.warn("The module data package was specified by the module " LOGGER.warn("The module data package was specified by the module "
+ "annotation nore was an group id found in the module info" + "annotation nore was an group id found in the module info"
+ "file. Creating data package name from the name of the " + "file. Creating data package name from the name of the "
+ "module class."); + "module class.");
return moduleClass.getName().toLowerCase(); return moduleClass.getName().toLowerCase();
} }
} }
/**
* Reads the module version. If the module annotation on the module class
* specifies a value for the version that value is used. Otherwise the value
* from the module info properties is used. If the properties do not specify
* a version the version is undefined. That should be avoided because it can
* lead to strange errors.
*
* @param annotation The module annotation of the module.
* @param moduleInfo The module info properties.
* @return The version of the module or {@code null} if the version is
* unspecified.
*/
private String readModuleVersion( private String readModuleVersion(
final Module annotation, final Module annotation,
final Properties moduleInfo) { final Properties moduleInfo) {
@SuppressWarnings("PMD.LongVariable") @SuppressWarnings("PMD.LongVariable")
final boolean annotationHasVersion = annotation.version() != null final boolean annotationHasVersion = annotation.version() != null
&& !annotation.version() && !annotation.version()
.isEmpty(); .isEmpty();
@SuppressWarnings("PMD.LongVariable") @SuppressWarnings("PMD.LongVariable")
final boolean moduleInfoHasVersion = moduleInfo.getProperty(VERSION) final boolean moduleInfoHasVersion = moduleInfo.getProperty(VERSION)
!= null != null
&& !moduleInfo.getProperty( && !moduleInfo.getProperty(
VERSION) VERSION)
.isEmpty(); .isEmpty();
if (annotationHasVersion) { if (annotationHasVersion) {
return annotation.version(); return annotation.version();
@ -301,7 +324,7 @@ public class ModuleInfo {
return moduleInfo.getProperty(VERSION); return moduleInfo.getProperty(VERSION);
} else { } else {
LOGGER.warn("Module version is not specified by the module " LOGGER.warn("Module version is not specified by the module "
+ "annotation or in the module info file. Module version is " + "annotation or in the module info file. Module version is "
+ "undefinied. This can lead to all sorts of strange errors!"); + "undefinied. This can lead to all sorts of strange errors!");
return ""; return "";
} }

View File

@ -32,6 +32,10 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
/** /**
* The {@code ModuleManager} manages the lifecycle of all modules.
*
* Please note: While this class is public it is not part of the public API and
* should not be called by other classes.
* *
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a> * @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/ */
@ -39,33 +43,45 @@ import org.apache.logging.log4j.Logger;
public class ModuleManager { public class ModuleManager {
private static final Logger LOGGER = LogManager.getLogger( private static final Logger LOGGER = LogManager.getLogger(
ModuleManager.class ModuleManager.class
); );
/**
* {@code EntityManager} for interacting with the database.
*/
@PersistenceContext(name = "LibreCCM") @PersistenceContext(name = "LibreCCM")
private EntityManager entityManager; private EntityManager entityManager;
/**
* Dependency tree nodes.
*/
private List<TreeNode> moduleNodes; private List<TreeNode> moduleNodes;
/**
* Method for initialising the dependency tree (is put into
* {@link #moduleNodes}. The method is annotated with {@link PostConstruct}
* which causes the CDI container to call this method after an instance of
* this class is generated.
*/
@PostConstruct @PostConstruct
public void initDependencyTree() { public void initDependencyTree() {
//Find all modules using the service loader.
LOGGER.info("Finding modules"); LOGGER.info("Finding modules");
final ServiceLoader<CcmModule> modules = ServiceLoader.load( final ServiceLoader<CcmModule> modules = ServiceLoader.load(
CcmModule.class); CcmModule.class);
LOGGER.info("Creating dependency tree these modules:"); LOGGER.info("Creating dependency tree these modules:");
for (final CcmModule module : modules) { for (final CcmModule module : modules) {
final ModuleInfo moduleInfo = new ModuleInfo(); final ModuleInfo moduleInfo = new ModuleInfo();
moduleInfo.load(module); moduleInfo.load(module);
LOGGER.info("\t{} {}", LOGGER.info("\t{} {}",
// ModuleUtil.getModuleName(module),
// ModuleUtil.getVersion(module));
moduleInfo.getModuleName(), moduleInfo.getModuleName(),
moduleInfo.getModuleVersion()); moduleInfo.getModuleVersion());
} }
//Create the dependency tree
final DependencyTreeManager treeManager = new DependencyTreeManager(); final DependencyTreeManager treeManager = new DependencyTreeManager();
try { try {
final List<TreeNode> tree = treeManager.generateTree(modules); final List<TreeNode> tree = treeManager.generateTree(modules);
moduleNodes = treeManager.orderModules(tree); moduleNodes = treeManager.orderModules(tree);
@ -75,23 +91,33 @@ public class ModuleManager {
} }
/**
* Initialises all modules.
*/
@Transactional(Transactional.TxType.REQUIRED) @Transactional(Transactional.TxType.REQUIRED)
public void initModules() { public void initModules() {
LOGGER.info("Initalising modules..."); LOGGER.info("Initalising modules...");
//Initialise all modules in the correct order
for (final TreeNode node : moduleNodes) { for (final TreeNode node : moduleNodes) {
//Create an install event instance.
final InstallEvent installEvent = new InstallEvent(); final InstallEvent installEvent = new InstallEvent();
installEvent.setEntityManager(entityManager); installEvent.setEntityManager(entityManager);
//Check if the module is a new module. If it is a new module
//call the install method of the module and set the module status
//to installed after the install method has run sucessfully.
final InstalledModule installedModule = entityManager.find( final InstalledModule installedModule = entityManager.find(
InstalledModule.class, InstalledModule.class,
node.getModule().getClass().getName().hashCode()); node.getModule().getClass().getName().hashCode());
if (installedModule != null if (installedModule != null
&& installedModule.getStatus() == ModuleStatus.NEW) { && installedModule.getStatus() == ModuleStatus.NEW) {
node.getModule().install(installEvent); node.getModule().install(installEvent);
installedModule.setStatus(ModuleStatus.INSTALLED); installedModule.setStatus(ModuleStatus.INSTALLED);
entityManager.merge(installedModule); entityManager.merge(installedModule);
} }
//Create an init event instance and call the init method.
final InitEvent initEvent = new InitEvent(); final InitEvent initEvent = new InitEvent();
initEvent.setEntityManager(entityManager); initEvent.setEntityManager(entityManager);
node.getModule().init(initEvent); node.getModule().init(initEvent);
@ -100,8 +126,8 @@ public class ModuleManager {
node.getModule().getClass().getName()); node.getModule().getClass().getName());
final Properties moduleInfo = getModuleInfo(node.getModule()); final Properties moduleInfo = getModuleInfo(node.getModule());
LOGGER LOGGER
.info("Module group id: {}", moduleInfo.getProperty( .info("Module group id: {}", moduleInfo.getProperty(
"groupId")); "groupId"));
LOGGER.info("Module artifact id: {}", moduleInfo.getProperty( LOGGER.info("Module artifact id: {}", moduleInfo.getProperty(
"artifactId")); "artifactId"));
LOGGER.info("Module version: {}", moduleInfo.getProperty("version")); LOGGER.info("Module version: {}", moduleInfo.getProperty("version"));
@ -111,16 +137,21 @@ public class ModuleManager {
} }
} }
/**
* Helper method for retrieving the module info for a module.
*
* @param module
* @return
*/
private Properties getModuleInfo(final CcmModule module) { private Properties getModuleInfo(final CcmModule module) {
final Properties moduleInfo = new Properties(); final Properties moduleInfo = new Properties();
// try {
final String moduleInfoPath = String.format( final String moduleInfoPath = String.format(
"/module-info/%s.properties", "/module-info/%s.properties",
module.getClass().getName()); module.getClass().getName());
LOGGER.info("Path for module info: {}", moduleInfoPath); LOGGER.info("Path for module info: {}", moduleInfoPath);
try (final InputStream stream = module.getClass().getResourceAsStream( try (final InputStream stream = module.getClass().getResourceAsStream(
moduleInfoPath)) { moduleInfoPath)) {
if (stream == null) { if (stream == null) {
LOGGER.warn("No module info found."); LOGGER.warn("No module info found.");
} else { } else {
@ -134,11 +165,17 @@ public class ModuleManager {
return moduleInfo; return moduleInfo;
} }
/**
* Called to shutdown all modules.
*
*/
@Transactional(Transactional.TxType.REQUIRED) @Transactional(Transactional.TxType.REQUIRED)
public void shutdownModules() { public void shutdownModules() {
LOGGER.info("Shutting down modules..."); LOGGER.info("Shutting down modules...");
System.out.println("Shutting down modules..."); System.out.println("Shutting down modules...");
for (final TreeNode node : moduleNodes) { for (final TreeNode node : moduleNodes) {
//Create a shutdown event instance and call the shutdown method of
//the module.
final ShutdownEvent shutdownEvent = new ShutdownEvent(); final ShutdownEvent shutdownEvent = new ShutdownEvent();
shutdownEvent.setEntityManager(entityManager); shutdownEvent.setEntityManager(entityManager);
node.getModule().shutdown(shutdownEvent); node.getModule().shutdown(shutdownEvent);
@ -147,12 +184,13 @@ public class ModuleManager {
System.out.println("Modules shut down."); System.out.println("Modules shut down.");
System.out.println("Checking for modules to uninstall..."); System.out.println("Checking for modules to uninstall...");
//Check for modules to uninstall
for (final TreeNode node : moduleNodes) { for (final TreeNode node : moduleNodes) {
System.out.printf("Checking status of module %s%n", System.out.printf("Checking status of module %s%n",
node.getModule().getClass().getName()); node.getModule().getClass().getName());
final InstalledModule installedModule = entityManager.find( final InstalledModule installedModule = entityManager.find(
InstalledModule.class, node. InstalledModule.class, node.
getModule().getClass().getName().hashCode()); getModule().getClass().getName().hashCode());
LOGGER.info("Status of module {} ({}): {}", LOGGER.info("Status of module {} ({}): {}",
node.getModuleInfo().getModuleName(), node.getModuleInfo().getModuleName(),
node.getModule().getClass().getName(), node.getModule().getClass().getName(),
@ -165,20 +203,27 @@ public class ModuleManager {
node.getModule().getClass().getName()); node.getModule().getClass().getName());
if (ModuleStatus.UNINSTALL.equals(installedModule.getStatus())) { if (ModuleStatus.UNINSTALL.equals(installedModule.getStatus())) {
//If the current module is marked for uninstall check if there
//modules which depend on the module. If there are now module
//depending on the module create an uninstall event instance
//and call the uninstall method of the module.
System.out.printf("Module %s is scheduled for uninstall...%n", System.out.printf("Module %s is scheduled for uninstall...%n",
node.getModuleInfo().getModuleName()); node.getModuleInfo().getModuleName());
if (node.getDependentModules().isEmpty()) { if (node.getDependentModules().isEmpty()) {
System.out. System.out.
printf("Calling uninstall method of module %s...%n", printf("Calling uninstall method of module %s...%n",
node.getModuleInfo().getModuleName()); node.getModuleInfo().getModuleName());
final UnInstallEvent unInstallEvent = new UnInstallEvent(); final UnInstallEvent unInstallEvent = new UnInstallEvent();
unInstallEvent.setEntityManager(entityManager); unInstallEvent.setEntityManager(entityManager);
node.getModule().uninstall(null); node.getModule().uninstall(null);
} else { } else {
//If there are module depending on the module marked for
//uninstall reset the status of the module to installed.
//Normally this should never happen but just in case...
System.out.printf("There are other modules depending on " System.out.printf("There are other modules depending on "
+ "module %s. Module can't be " + "module %s. Module can't be "
+ "uninstalled. Depending modules:%n", + "uninstalled. Depending modules:%n",
node.getModuleInfo().getModuleName()); node.getModuleInfo().getModuleName());
for (final TreeNode dependent : node.getDependentModules()) { for (final TreeNode dependent : node.getDependentModules()) {
System.out.printf("\t%s%n", System.out.printf("\t%s%n",
@ -190,8 +235,8 @@ public class ModuleManager {
} }
} else { } else {
System.out.printf( System.out.printf(
"Module %s is *not* scheduled for uninstall.%n", "Module %s is *not* scheduled for uninstall.%n",
node.getModuleInfo().getModuleName()); node.getModuleInfo().getModuleName());
} }
} }
} }

View File

@ -19,16 +19,20 @@
package org.libreccm.modules; package org.libreccm.modules;
/** /**
* Thrown by the {@link ModuleManager} if something goes wrong.
* *
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a> * @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/ */
public class ModuleManagerException extends RuntimeException { public class ModuleManagerException extends RuntimeException {
private static final long serialVersionUID = 1426939919890655697L;
/** /**
* Creates a new instance of <code>ModuleManagerException</code> without * Creates a new instance of <code>ModuleManagerException</code> without
* detail message. * detail message.
*/ */
public ModuleManagerException() { public ModuleManagerException() {
super();
//Nothing //Nothing
} }

View File

@ -19,15 +19,31 @@
package org.libreccm.modules; package org.libreccm.modules;
/** /**
* Annotation for describing a dependency relation between a module and another
* module.
* *
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a> * @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/ */
public @interface RequiredModule { public @interface RequiredModule {
/**
* The module class required by the module.
*
* @return
*/
Class<? extends CcmModule> module(); Class<? extends CcmModule> module();
/**
* The minimal version required by the module.
* @return
*/
String minVersion() default ""; String minVersion() default "";
/**
* The maximum version required by the module.
*
* @return
*/
String maxVersion() default ""; String maxVersion() default "";
} }

View File

@ -19,7 +19,9 @@
package org.libreccm.modules; package org.libreccm.modules;
/** /**
* Event object for the module shutdown.
* *
* @see ModuleEvent
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a> * @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/ */
public class ShutdownEvent extends ModuleEvent { public class ShutdownEvent extends ModuleEvent {

View File

@ -24,7 +24,6 @@ import java.util.List;
import java.util.Objects; import java.util.Objects;
/** /**
*
* Represents a node in the dependency tree. This class is <strong>not</strong> * Represents a node in the dependency tree. This class is <strong>not</strong>
* part of the public API. * part of the public API.
* *
@ -32,11 +31,26 @@ import java.util.Objects;
*/ */
final class TreeNode { final class TreeNode {
/**
* The module class of the module represented by this node.
*/
private CcmModule module; private CcmModule module;
/**
* The module info for the module.
*/
private ModuleInfo moduleInfo; private ModuleInfo moduleInfo;
/**
* The modules depending on the module represented by this tree node.
*/
private List<TreeNode> dependentModules; private List<TreeNode> dependentModules;
/**
* The modules the module represented by this tree node depends on.
*/
private List<TreeNode> dependsOn; private List<TreeNode> dependsOn;
/**
* Creates a new empty tree node.
*/
public TreeNode() { public TreeNode() {
super(); super();
@ -44,6 +58,12 @@ final class TreeNode {
dependsOn = new ArrayList<>(); dependsOn = new ArrayList<>();
} }
/**
* Creates a tree node for the provided module. Them module info for the
* module is loaded automatically.
*
* @param module
*/
public TreeNode(final CcmModule module) { public TreeNode(final CcmModule module) {
this(); this();

View File

@ -19,7 +19,9 @@
package org.libreccm.modules; package org.libreccm.modules;
/** /**
* Event object for the module deinstallation.
* *
* @see ModuleEvent
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a> * @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/ */
public class UnInstallEvent extends ModuleEvent { public class UnInstallEvent extends ModuleEvent {