Can We Get a Little Help Over Here?

Using the Liferay Deployment Helper to build an artifact for JEE admin console deployment...

Introduction

One of the benefits that you get from an enterprise-class JEE application server is a centralized administration console.

Rather than needing to manage nodes individually like you would with Tomcat, the JEE admin console can work on the whole cluster at one time.

But, with Liferay 7 CE and Liferay 7 DXP and the deployment of OSGi bundle jars, portlet/theme wars and Liferay lpkg files, the JEE admin console cannot be used to push your shiny new module or even a theme war file because it won't know to drop these files in the Liferay deploy folder.

Enter the Deployment Helper

So Liferay created this Maven and Gradle plugin called the Deployment Helper to give you a hand here.

Using the Deployment Helper, the last part of your build is the generation of a war file that contains the bundle jars and theme wars, but is a single war artifact.

This artifact can be deployed to all cluster nodes using the centralized admin console.

Adding the Deployment Helper to the Build

To add the Deployment Helper to your Gradle-based build: https://dev.liferay.com/en/develop/reference/-/knowledge_base/7-0/deployment-helper-gradle-plugin

To add the Deployment Helper to your Maven-based build: https://dev.liferay.com/en/develop/reference/-/knowledge_base/7-0/deployment-helper-plugin

While both pages offer the technical details, they are still awfully terse when it comes to usage.

Gradle Deployment Helper

Basically for Gradle you get a new task, the buildDeploymentHelper task. You can execute gradlew buildDeploymentHelper on the command line after including the plugin and you'll get a war file, but probably one that you'll want to configure.

The plugin is supposed to pull in all jar files for you, so that will cover all of your modules. You'll want to update the deploymentFiles to include your theme wars and any of the artifacts you might be pulling in from the legacy SDK.

In the example below, my Liferay Gradle Workspace has the following build.gradle file:

buildscript {
    dependencies {
        classpath group: "com.liferay", name: "com.liferay.gradle.plugins", version: "3.12.48"
        classpath group: "com.liferay", name: "com.liferay.gradle.plugins.deployment.helper", version: "1.0.3"
    }

    repositories {
        maven {
            
            url "https://repository-cdn.liferay.com/nexus/content/groups/public"
        }
    }
    
}

apply plugin: "com.liferay.deployment.helper"

buildDeploymentHelper {
  deploymentFiles = fileTree('modules'){include '**/build/libs/*.jar'} + 
    fileTree('themes'){include '**/build/libs/*.war'}
}

This will include all wars from the theme folder and all module jars from the modules folder. Since I'm being specific on the paths for files to include, any wars and jars that might happen to be polluting the directories will be avoided.

Maven Deployment Helper

The Maven Deployment Helper has a similar task, but of course you're going to use the pom to configure and you have a different command line.

The Maven equivalent of the Gradle config would be something along the lines of:

<build>
    <plugins>
    ...
        <plugin>
            <groupId>com.liferay</groupId>
            <artifactId>com.liferay.deployment.helper</artifactId>
            <version>1.0.4</version>
            <configuration>
              <deploymentFileNames>
                modules/my-mod-a/build/libs/my-mod-a-1.0.0.jar,
                modules/my-mod-b/build/libs/my-mod-b-1.0.0.jar,
                ...,
                themes/my-theme/build/libs/my-theme-1.0.0.war
              </deploymentFileNames>
            </configuration>
        </plugin>
    ...
    </plugins>
</build>

Unfortunately you can't do some cool wildcard magic here, you're going to have to list out the ones to include.

Deployment Helper War

So you've built a war now using the Deployment Helper, but what does it contain? Here's a sample from one of my projects:

Basically you get a single class, the com.liferay.deployment.helper.servlet.DeploymentHelperContextListener class.

You also get a web.xml for the war.

And finally, you get all of the files that you listed for the deployment helper task.

DeploymentHelperContextListener

You can find the source for DeploymentHelperContextListener here, but I'll give you a quick summary.

So we have two key methods, copy() and contextInitialized().

The copy() method does, well, the copying of data from the input stream (the artifact to be deployed) to the output stream (the target file in the Liferay deploy folder). Nothing fancy.

The contextInitialized() method is the implementation from the ContextListener interface and will be invoked when the application container has constructed the war's ServletContext.

If you scan the method, you can see how the parameters that are options to the plugins will eventually get to us via context parameters.

It then loops through the list of deployment filenames, and for each one in the list it will create the target file in the deploy directory (the Liferay deploy folder), and it will use the copy() method to copy the data out to the filesystem.

Lastly it will invoke the DeployManagerUtil.undeploy() on the current servlet context (itself) to attempt to remove the deployment helper permanently. Note that per the listed restrictions, this may not actually undeploy the Deployment Helper war.

web.xml

The web.xml file is pretty boring:

<?xml version="1.0"?>

<web-app xmlns="http://java.sun.com/xml/ns/javaee" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="3.0" 
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee 
      http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
  <context-param>
    <description>A comma delimited list of files in the WAR that should be deployed to the 
      deployment-path. The paths must be absolute paths within the WAR.</description>
    <param-name>deployment-files</param-name>
    <param-value>/WEB-INF/micro.maintainance.outdated.task-1.0.0.jar,
      /WEB-INF/fragment.com.liferay.portal.search.web-1.0.0.jar,
      ...
    </param-value>
  </context-param>
  <context-param>
    <description>The absolute path to the Liferay deploy folder on the target system.</description>
    <param-name>deployment-path</param-name>
    <param-value></param-value>
  </context-param>
  <listener>
    <listener-class>com.liferay.deployment.helper.servlet.DeploymentHelperContextListener</listener-class>
  </listener>
</web-app>

Each of the files that are supposed to be deployed, they are listed as a parameter for the context listener.

The rest of the war is just the files themselves.

An Important Limitation

So there is one important limitation you should be aware of...

ContextListeners are invoked every time the container is restarted or the war is (re)deployed.

If your deployment helper cannot undeploy itself, every time you restart the container, all of your artifacts in the Deployment Helper war are going to be processed again.

So, as part of your deployment process, you should verify and ensure that the Deployment Helper has been undeployed, whether it can remove itself or whether you must manually undeploy from the centralized console.

Conclusion

So now, using the Deployment Helper, you can create a war file that contains files to deploy to Liferay: the module jars, the portlet wars, the theme wars and yes, you could even deploy Liferay .lpkg files, .lar files and even your licence xml file (for DXP).

You can create the Deployment Helper war file directly out of your build tools. If you are using CI, you can use the war as one of your tracked artifacts.

Your operations folks will be happy in that they can return to using the centralized admin console to deploy a war to all of the nodes, they won't need to copy everything to the target server's deploy folders manually.

They may gripe a little about deploying the bundle followed by an undeploy when the war has started, but you just need to remind them of the pain that you're saving them from the older, manual development process.

Enjoy!