Page tree
Skip to end of metadata
Go to start of metadata

This tutorial shows how to configure your own Magnolia bundle and how to include more modules in the bundle. As a best practice, let Magnolia configure your project, don't configure manually. See also Creating a custom bundle and WAR file with multiple configurations.


Prerequisites

  • Bundle created in Creating a custom bundle. We use the bundle to explain the concepts in this tutorial. Clone the project from our Git repository
  • Shell such as bash.
  • Git and basic Git commands to check out source code.
  • IDE such as IntelliJ IDEA or Eclipse to run a Magnolia project. See Creating a custom bundle on how to start a Magnolia project with IntelliJ IDEA.

Clone the project from Git

You have to checkout the project from Magnolia git server and checkout the branch  customize-configuration .

When you clone the project, you will checkout its "master" branch. On master is the version which we have used for Creating a custom bundle. If you checkout the branch customize-configuration, you will get the code which we are going to explain on this page. 

Open a shell, cd  to a directory where you keep source code and type the following command:

git clone http://git.magnolia-cms.com/git/documentation/example-project.git

When cloning is finished, switch the branch:

git checkout customize-configuration

Set magnolia.repositories.home

Assuming you are working with an IDE using derby as the underlying DB for JCR, you must set  magnolia.repositories.home . Per default the repositories is within the web app, this dierctory might get lost or overridden during the update process which we are going to execute during this example.

The customize-configuration branch comes with its own magnolia.properties, it resides in example-project/example-project-webapp/src/main/webapp/WEB-INF/config/.
Make sure that it points to a valid directory where Magnolia has write permissions. The file on the git repository comes with the following value:

/WEB-INF/config/default/magnolia.properties
magnolia.repositories.home=/tmp/example-project/repositories

Build the project

Build the project using Maven:

mvn clean install

Run the project

Start the project from your IDE and ensure that it is properly running. You should end up with a running minimal Magnolia project. See Creating a custom bundle > Run the project.

Use case

When developing a custom Magnolia project it often happens that you add a Magnolia module and then realize that its default configuration doesn't fit your requirements. In our case, we add the module fs-browser . The module comes with a configured property which must be changed.

You could change the property value manually in the Configuration app. The change would work immediately after reopening the app. There is no need to restart your Magnolia instance even. However, you should not change configuration manually for the following reasons:

  • Inconvenient. It is inconvenient to modify configuration in the tree once the project is in production. To ensure the availability of your site you would need to deploy a second public instance while you change the first. Always keep changes to production systems minimal.
  • Easy to forget. Changing configuration by hand is something you must actively remember to do. You need to write documentation for other developers and administrators so they remember to do the same.

We want to organize our project in a way that the configuration will be set appropriately during the installation.

Now imagine a second use case: You managed to customize configuration during the installation of your custom bundle. But now your system administrator wants you to change the configuration again in the already-deployed production system. You have to change the value of the property again. Avoid making this change manually. Look for way to update the configuration by code.

To change configuration by code we have two tools:

  • Installation tasks
  • Update tasks

Let's demonstrate this with the fs-browser-app module.

Adding fs-browser-app

The fs-browser-app (Git source) is small content app that uses the file system as its data source. Consider it an example of a module whose configuration you want to change.

Assuming that your module has a parent pom:

  1. Add the dependency including the version into the dependencyManagement section of  example-project/pom.xml

    example-project/pom.xml
    <dependencyManagement>
      <dependencies>
        <!-- more dependencies here... -->
        <dependency>
          <groupId>info.magnolia.documentation</groupId>
          <artifactId>fs-browser-app</artifactId>
          <version>1.0</version>
        </dependency>
        <!-- more dependencies here... -->
      </dependencies>
    </dependencyManagement>
    
    
  2. Add the dependency without the version into the dependencies section of  example-project/example-project-webapp/pom.xml

    example-project/example-project-webapp/pom.xml
    <dependencies>
      <!-- more dependencies here... -->
      <dependency>
        <groupId>info.magnolia.documentation</groupId>
        <artifactId>fs-browser-app</artifactId>
      </dependency>
      <!-- more dependencies here... -->
    </dependencies>
    
    

After having extended your pom files, restart Tomcat. The new module will be installed. You should see the freshly installed module in the Configuration app.

   

You see a new app launcher tile labeled "File Browser". You also can find new configuration in /modules/fs-browser-app in the Configuration app.

Have a look at the property /modules/fs-browser-app/apps/fs-browser/subApps/browser/contentConnector/rootFolder:

Node nameValue

 browser

 

 contentConnector

 

 rootFolder

/Users/cmeier/tmp/test/

The value for the rootFolder property is bootstrapped by the fs-browser-app. Instead of /Users/cmeier/tmp/test/ you want the root folder of the fs-browser-app map to /your-mounted-nas-drive/current-work/magnolia/.
That's the value which we want to change.

Customizing configuration by code

As we already mentioned above: Configuring your module manually is not recommended, instead we will:

  • Write code to change the configuration. Magnolia provides classes to add, edit, move and remove nodes and properties. The classes implement Task. You don't need to write much code yourself when using these readily available classes.
  • Execute code during installation of the module. When installing your custom bundle, pay attention to the order in which your modules is installed. You cannot manipulate data of the  config  workspace if the space is not yet available.
  • Execute code during update.

A Magnolia Module allows you to do all of the above. The module mechanism provides two things that are important for the task:

  • Declare dependencies. By declaring that our project module depends on fs-browser-app we ensure that our module is installed after  fs-browser-app , ensuring we can configure it.
  • Use a ModuleVersionHandler to automate all installation or update tasks our project requires such as changing the rootFolder property.

Let's convert our custom module into a Magnolia module.

Terminology: module

When we say “module” or “project module” we mean the files that comprise our project specific Magnolia module. It’s unfortunate that Magnolia and Maven use the term “module” for different things. When we mean Maven module we write “Maven module”.

In a Maven environment you typically create a Magnolia module as a separate Maven module. However, it's OK for project modules to be intertwined with the webapp Maven module. In our case the Maven module example-project-webapp becomes a real Magnolia module.

There is a Maven archetype for Magnolia modules. It creates the necessary infrastructure for more complex modules so in our case it would create more noise than necessary.

Convert your project module into a Magnolia module

Magnolia functional modules are a well-known part of the Magnolia ecosystem. Magnolia itself is built as a collection of base modules. But there’s also a lesser-known module type called “project module”. Their purpose is to install or configure your project.

Our custom project at the moment is nothing else then a parent pom plus the sub-module example-project-webapp.

The project module in our case is the example-project-webapp. Until now it is just a Maven module. Its src folder is empty or probably not existing. To change example-project-webapp into a Magnolia Module, we have to add a module descriptor at least. (And we also will create and register a module version handler class.)

Create a module descriptor

Create a new file example-project-webapp/src/main/resources/META-INF/magnolia/example-project.xml and add this content:

example-project/example-project-webapp/src/main/resources/META-INF/magnolia/example-project.xml
<!DOCTYPE module SYSTEM "module.dtd">
<module>
  <name>example-project</name>
  <displayName>${project.name}</displayName>
  <description>${project.description}</description>
  <versionHandler>com.example.myproject.setup.ExampleProjectModuleVersionHandler</versionHandler>
  <version>${project.version}</version>

  <dependencies>
    <dependency>
      <name>fs-browser-app</name>
      <version>1.0.1/*</version>
    </dependency>
  </dependencies>

</module>

In module descriptor we can rely on Maven’s variable substitution to fill in most values. We replicate the pom‘s dependencies except we use version ranges. Note two things:

  • By adding the <dependency> fs-browser-app, we ensured, that our custom module itself is installed after the modules we depend on.
  • Registration of the module version handler class in the <versionHandler>-section.

Create a version handler

The versionHandler element in the module descriptor refers to an not yet existing class. To create a version handler class: 

  1. Create a directory example-project/example-project-webapp/src/main/java, if its not already existing. (src/main/java is the typical folder for Java classes in a Maven project.)
  2. Tell your IDE that the new directory is a source folder. (IntelliJ IDEA, Eclipse)
  3. Add a new package com.example.myproject.setup
  4. Add a new class ExampleProjectModuleVersionHandler

You should now have a new class file example-project/example-project-webapp/src/main/java/com/example/myproject/setup/ExampleProjectModuleVersionHandler.java. The class should extend DefaultModuleVersionHandler.

com.example.myproject.setup.ExampleProjectModuleVersionHandler
package com.example.myproject.setup;

import info.magnolia.module.DefaultModuleVersionHandler;
/**
 * This class sets up our great example project.
 */
public class ExampleProjectModuleVersionHandler extends DefaultModuleVersionHandler {
}

The class won't do anything, yet. However you can use it to add tasks for both installing and updating your project.

Install Task

Add an install Task in the ExampleProjectModuleVersionHandler:

public class ExampleProjectModuleVersionHandler extends DefaultModuleVersionHandler {
    @Override
    protected List<Task> getExtraInstallTasks(InstallContext installContext) {
        ArrayList<Task> tasks = new ArrayList<Task>();
        SetPropertyTask setPropertyTask = new SetPropertyTask(
			"Change the rootFolder property of fs-browser-app contentConnector.", 
			RepositoryConstants.CONFIG, 
			"/modules/fs-browser-app/apps/fs-browser/subApps/browser/contentConnector", 
			"rootFolder", "/your-mounted-nas-drive/current-work/magnolia/");
        tasks.add(setPropertyTask);
        return tasks;
    }
}

The class extends DefaultModuleVersionHandler which provides a reasonable set of default tasks. Extending AbstractModuleVersionHandler is also an option but we don’t really need the other regular module specificities. If you know the difference between the two classes and are familiar with the version handling mechanism, we recommend you extend AbstractModuleVersionHandler in project modules.
In its default implementation #getExtraInstallTasks returns an empty list. We override the method and add an instance of SetPropertyTask to the install tasks list, which works fine for setting the value of a property.

Best practice

A good-citizen task should be safe and reliable. Think what the expected state of the system should be. Check existing data before updating it. Use CheckAndModifyPropertyValueTask,  CheckAndModifyPartOfPropertyValueTask or similar tasks or wrap your tasks into a ConditionalDelegateTask.

A good-citizen task names and describes itself properly so users know what is going to happen in the installation process. When all tasks follow this convention they appear uniform when displayed to the user. When instantiating a Task, use a constructor which allows to provide name and description.

Name of the task should be a simple sentence providing some context such as:

"Set new rootFolder property."

 Description should be a complete and meaningful sentence in third person, ending in a period, such as "Set a new rootFolder property."

Test the install Task

To test the extra install Task:

  1. Shut down the instance.
  2. Delete the repository to ensure your project will be re-installed completely on next start up:

    rm -rf example-project/example-project-webapp/target/example-project-webapp-1.0-SNAPSHOT/repositories/magnolia

    Or in the root of your project:

    mvn clean install
  3. Start the instance.

Verify that  /modules/fs-browser-app/apps/fs-browser/subApps/browser/contentConnector/rootFolder has the appropriate value as defined in the task.

Update Tasks

If a module is already deployed use an update task:

com.example.myproject.setup.ExampleProjectModuleVersionHandler
public ExampleProjectModuleVersionHandler() {
    register(DeltaBuilder.update("1.1", "Example project 1.1 has a bunch of cool new features.")
            .addTask(new CheckAndModifyPropertyValueTask(
            "Set new rootFolder property.", // task name
            "Setting new rootFolder property.", // task description
            RepositoryConstants.CONFIG, 
            "/modules/fs-browser-app/apps/fs-browser/subApps/browser/contentConnector", 
            "rootFolder", 
            "/your-mounted-nas-drive/current-work/magnolia/", 
            "/another-mounted-drive/another-directory/"))
    );
}

Make sure that ExampleProjectModuleVersionHandler contains the update task, it might be commented out.

In the update task we tell Magnolia: “For version 1.1 of this module, execute these tasks to be executed…” Magnolia knows which version of a module is currently installed by keeping track of the version property under each module’s node. And it knows what version of a module is being deployed by looking at the module descriptor. Since we use Maven variable substitution in our module descriptor we know that the version number Magnolia reads is the same as our Maven project’s version.

Think about how and when your module is deployed and installed. In this example, it is quite likely that you’ll want to also execute this task when installing the module. Register the same task in getExtraInstallTasks. Since tasks are stateless descriptors you can easily extract them into class fields, for example. If you look at Magnolia’s own module version handlers you won't find many install tasks because most of the installation work is done via bootstrap files which are extracted on installation by one of the default tasks provided by DefaultModuleVersionHandler.

When installing a module only install tasks are executed. When updating a module only the update tasks registered for versions between the current and the version we’re updating to are executed.

The logic above only applies to subclasses of AbstractModuleVersionHandler, and, if needed, you could completely modify this logic by re-implementing the getDeltas method of your ModuleVersionHandler.

Test the update Task

Before running the update (test) check the current version of the installed project in the Configuration app. It should be 1.0-SNAPSHOT.

 

If there is another value such as ${project.version} change it to  1.0-SNAPSHOT.

The update task will be executed only when the version of the module (with the version handler) rises up to 1.1. But when looking at the pom we realize, that the pom of the project (on the branch customize-configuration) has the same version like on master. Since the module never was released, 1.0-SNAPSHOT is correct. 
To test the update task, beside other things, we have to emulate that our custom module already reached version 1.1 (or 1.1-SNAPSHOT).

  1. Stop the server.
  2. Change the version from 1.0-SNAPSHOT to 1.1-SNAPSHOT in both example-project/pom.xml (line 12) and example-project/example-project-webapp/pom.xml (line 7).
  3. Make sure the changes get deployed and restart the server.
  4. Follow the update process in the browser.
  5. Check the changed property with the Configuration app in /modules/fs-browser-app/apps/fs-browser/subApps/browser/contentConnector@rootFolder.

In a real situation, you would raise the version during a release process. 

The future of your project

Managing updates and releases is a lot of work. Take advantage of Magnolia's update mechanism.

Use a stable version numbering scheme for your (custom) modules. A proper release process (Maven’s release plugin) helps for updates of connected and production systems. It is tedious to update your development environment but discipline helps. Remember to write update tasks for all needed changes and test them regularly. Write update tasks immediately. Use the info.magnolia.module.ModuleVersionHandlerTestCase to write unit tests for your module version handlers. And please submit your improvements.

This is also an exercise in dependency management. You’ll need to figure out when to update to Magnolia’s newest and latest release, and figure out what that means for your module. In most cases however, Magnolia itself will handle the update nicely. Everytime when there is a change in the configuration of a Magnolia module, the ModuleVersionHandler of the module will take care of updating your configuration.

Conclusion

We now have a project which not only has a stable and reproducible build, but which can also be installed and deployed right away by your peers. Hopefully you’ll be able to reuse ideas to better manage your project’s lifecycle by now. If not, feel free to comment on what doesn’t work for you, or what could be clarified or expanded upon.


Credits:

3 Comments

  1. Use the ModuleVersionHandlerTestCase to 

    I have "400 - Bad Request Could not find request in archive file" by linking https://javadoc.magnolia-cms.com/info.magnolia.module.ModuleVersionHandlerTestCase

    1. Hi Alex

      Thanks for the heads-up. I have to confess that i cannot find the javadoc file of this class atm. I investigate ..

      Meanwhile have a look at the source file .

      Cheers,
       Christoph 

    2. Hi Alex

      I have figured out that we currently don't build a jar for the source of test classes. So, the above link cannot work how it was added. I change the link to point it to the source code on git.
      Sorry for this inconvenience!