Page History
...
With the same philosophy as the prior concept, rationale and goals are not listed here but the design and implementation will be mentioned to align with Magnolia 5.6. Also over long time usage, customers having more and more demand on start / stop / install / uninstall a module, the needs has been increased while the technology has been updated. This allow us to easier achieve this goal in comparison to previous versions.
An overview of implementation looks like below:
Most of the design and implementation focused in ModuleManager where Magnolia control its modules and components. Let's start with an easiest function uninstalling a module design.
...
Start / stop module
With traditional Mangolia Magnolia modules, we didn't really put our attentions to Module lifecycle management which means we didn't really implemented module 'start' and 'stop' function. However it is fully supported and in some cases, customers might want to implement them to support them persist of their critical information. Currently the start and stop just call corresponding functions of the modules.
...
Now let's go the difficult part - change module manager to support installing a new module on the fly.
Installing a module
Design
In order to support external module installation, we need to refactor the whole Magnolia module installation, bootstrap, start and components loading process. This is so risky that we need to carefully test the solution before actually apply it. One of the most important requirement is backward compatibility when chaning this kind of thing. Just a note that previously we do the module installation in Magnolia startup process in bunch, which means the startup would take care of all modules installation at a time, now we broke it down into pieces to control, manage and also support installing external modules on the fly. This require us to have a mechanism to support loading external jars, persist those jars classpath and things like that for later start up. Also dependency checking has been by pass as an end user must control his / her external module dependency and install them sequentially.
Install external module would require
Load module package which should be implemented in JAR file (loadExternalJars)
Using loaded Jars, load module definition (loadExternalDefinitions)
Finally we need to install and start loaded modules.
To implement this, we need to break out and refactor module manager in below detail. Example file for reference: ModuleManagerImplExt.java
External Jar file loader support
This function get into consideration when we support dynamic loading of new jars file, instead of putting them within WEB-INF/lib so that any Application server such as Tomcat can load it automatically, we have to support dynamic jar file loading where-ever the file is located. Eventhough hacking directly to URLClassLoader is not recommended and will be deprecated soon, but this's the shortest path as of Magnolia 5.6 release.
TO BE IMPROVED:
- consider whether not providing restart actions instead of start/stop. As stopping some module might mean that system will not work correctly and module "downtime" should be minimized.
- and last but not least consider that on restart (or stop) you should restart (or stop) all modules that have declared dependency on given module in a cascade.
Installing a module
Design
In order to support external module installation, we need to refactor the whole Magnolia module installation, bootstrap, start and components loading process. This is so risky that we need to carefully test the solution before actually apply it. One of the most important requirement is backward compatibility when chaning this kind of thing. Just a note that previously we do the module installation in Magnolia startup process in bunch, which means the startup would take care of all modules installation at a time, now we broke it down into pieces to control, manage and also support installing external modules on the fly. This require us to have a mechanism to support loading external jars, persist those jars classpath and things like that for later start up. Also dependency checking has been by pass as an end user must control his / her external module dependency and install them sequentially.
Install external module would require
Load module package which should be implemented in JAR file (loadExternalJars)
Using loaded Jars, load module definition (loadExternalDefinitions)
Finally we need to install and start loaded modules.
To implement this, we need to break out and refactor module manager in below detail. Example file for reference: ModuleManagerImplExt.java
External Jar file loader support
This function get into consideration when we support dynamic loading of new jars file, instead of putting them within WEB-INF/lib so that any Application server such as Tomcat can load it automatically, we have to support dynamic jar file loading where-ever the file is located. Eventhough hacking directly to URLClassLoader is not recommended and will be deprecated soon, but this's the shortest path as of Magnolia 5.6 release.
Code Block | ||
---|---|---|
| ||
public void loadJar(File jar2load) { | ||
Code Block | ||
| ||
public void loadJar(File jar2load) { try (JarFile jarFile = new JarFile(jar2load)) { Enumeration<JarEntry> e = jarFile.entries(); try (JarFile jarFile = URL url = jar2load.toURI().toURL();new JarFile(jar2load)) { Enumeration<JarEntry> e = jarFile.entries(); URL url = jar2load.toURI().toURL(); URLClassLoader cl = (URLClassLoader) this.getClass().getClassLoader(); Method method = URLClassLoader.class.getDeclaredMethod("addURL", new Class[]{URL.class}); method.setAccessible(true); // method.invoke(cl, new Object[]{url}); while (e.hasMoreElements()) { JarEntry je = e.nextElement(); if (je.isDirectory() || !je.getName().endsWith(".class")) { continue; } // -6 because of .class String className = je.getName().substring(0, je.getName().length() - 6); className = className.replace('/', '.'); Class<?> c = cl.loadClass(className); log.debug("Class loaded: {}", c.getName()); } } catch (Exception e) { log.error("Jar file failed to load."); log.error(e.getMessage(), e); } } |
...
Just use org.apache.maven.model.io.xpp3.MavenXpp3Reader to read your org.apache.maven.model.Model then from this model just like light module reader, we just need to read conventioned properties in.
Referenced implementation:
MavenModuleDefinitionReader.java
A note on load repository on the fly
First we will need RepositoryDefinition from existing repository mapping
.model.Model then from this model just like light module reader, we just need to read conventioned properties in.
Referenced implementation:
MavenModuleDefinitionReader.java
A note on load repository on the fly
First we will need RepositoryDefinition from existing repository mapping
Code Block | ||||
---|---|---|---|---|
| ||||
private info.magnolia.repository.definition.RepositoryDefinition getRepositoryMapping(String repositoryOrLogicalWorkspace) {
if (!repositoryManager.hasRepository(repositoryOrLogicalWorkspace)) {
WorkspaceMappingDefinition mapping = repositoryManager.getWorkspaceMapping(repositoryOrLogicalWorkspace);
repositoryOrLogicalWorkspace = mapping != null ? mapping.getRepositoryName() : repositoryOrLogicalWorkspace;
}
return repositoryManager.getRepositoryDefinition(repositoryOrLogicalWorkspace);
} |
If it is null (doesn't exist before) then we'll create a new one.
Create a new repository definition on the fly
Code Block | ||||
---|---|---|---|---|
| ||||
final info.magnolia.repository.definition.RepositoryDefinition defaultRepositoryMapping = getRepositoryMapping(DEFAULT_REPOSITORY_NAME);
final Map<String, String> defaultParameters = defaultRepositoryMapping.getParameters();
rm = new info.magnolia.repository.definition.RepositoryDefinition();
rm.setName(repositoryName);
rm.setProvider(defaultRepositoryMapping.getProvider());
rm.setLoadOnStartup(true);
final Map<String, String> parameters = new HashMap<>();
parameters.putAll(defaultParameters);
// override changed parameters
final String bindName = repositoryName + StringUtils.replace(defaultParameters.get("bindName"), "magnolia", "");
final String repositoryHome = StringUtils.substringBeforeLast(defaultParameters.get("configFile"), "/") + "/" + repositoryName;
parameters.put("repositoryHome", repositoryHome);
parameters.put("bindName", bindName);
parameters.put("customNodeTypes", nodeTypeFile);
rm.setParameters(parameters | ||||
Code Block | ||||
| ||||
private info.magnolia.repository.definition.RepositoryDefinition getRepositoryMapping(String repositoryOrLogicalWorkspace) { if (!repositoryManager.hasRepository(repositoryOrLogicalWorkspace)) { WorkspaceMappingDefinition mapping = repositoryManager.getWorkspaceMapping(repositoryOrLogicalWorkspace); try { repositoryOrLogicalWorkspace = mapping != null ? mapping.getRepositoryName() : repositoryOrLogicalWorkspace; repositoryManager.loadRepository(rm); } catch (Exception e) }{ return repositoryManager.getRepositoryDefinition(repositoryOrLogicalWorkspace); log.error(e.getMessage(), e); } |
If it is null (doesn't exist before) then we'll create a new one.
Create a new repository definition on the fly
Then register nodetype
Code Block | ||||
---|---|---|---|---|
| ||||
void registerNodeTypeFile(String repositoryName, String nodeTypeFile) {
| ||||
Code Block | ||||
| ||||
final info.magnolia.repository.definition.RepositoryDefinition defaultRepositoryMapping Provider provider = getRepositoryMapping(DEFAULT_REPOSITORY_NAMErepositoryManager.getRepositoryProvider(repositoryName); final Map<String, String> defaultParameters = defaultRepositoryMapping.getParameters();try { rm = new info.magnolia.repository.definition.RepositoryDefinition(); rmprovider.setNameregisterNodeTypes(repositoryNamenodeTypeFile); rm.setProvider(defaultRepositoryMapping.getProvider()); } catch rm.setLoadOnStartup(true);(RepositoryException e) { final Map<String, String> parameters = new HashMap<>(); parameters.putAll(defaultParameterslog.error(e.getMessage(), e); // override changed parameters final String bindName} = repositoryName + StringUtils.replace(defaultParameters.get("bindName"), "magnolia", ""); final String repositoryHome = StringUtils.substringBeforeLast(defaultParameters.get("configFile"), "/") + "/" + repositoryName; parameters.put("repositoryHome", repositoryHome); parameters.put("bindName", bindName); parameters.put("customNodeTypes", nodeTypeFile); rm.setParameters(parameters); try { repositoryManager.loadRepository(rm); } catch (Exception e) { } |
Load workspace
As simple as calling repositoryManager.loadWorkspace(repositoryName, workspace);
Module installation notes
Current status
Previously Magnolia crashed when failed to install any module. This has a reason and could not be easily fixed. However by refactoring module installation and startup now we can handle this. A note on module installation and startup is that if we failed on installing a root module which has few dependent modules, all dependent modules will be affected. In this context when we support end user to install their custom modules, this should not heavily affect Magnolia platform provided ones.
Implementation
Just prevent exception propagation by catch it here and by it pass to continue running:
Code Block | ||||
---|---|---|---|---|
| ||||
protected void installOrUpdateModule(ModuleAndDeltas moduleAndDeltas, InstallContextImpl ctx) { final ModuleDefinition moduleDef = moduleAndDeltas.getModule(); final List<Delta> deltas = moduleAndDeltas.getDeltas(); ctx.setCurrentModule(moduleDef); log.error(e.getMessage(), e); } |
Then register nodetype
Code Block | ||||
---|---|---|---|---|
| ||||
void registerNodeTypeFile(String repositoryName, String nodeTypeFile) { Provider provider = repositoryManager.getRepositoryProvider(repositoryName); debug("Install/update for {} is starting: {}", moduleDef, moduleAndDeltas); // viet fix continue running after fail install try { provider.registerNodeTypes(nodeTypeFileapplyDeltas(moduleDef, deltas, ctx); } catch (RepositoryExceptionException e) { log.error("Install module {} failed {}.", moduleDef.getName(), e.getMessage(), e); ; } log.debug("Install/update for {} has finished", moduleDef, moduleAndDeltas); } |
Load workspace
Verify the result
As a result you'll find this, which not block Magnolia from continue running (I've tried with Contacts module like below):
As simple as calling repositoryManager.loadWorkspace(repositoryName, workspace);