To main content

Guide: How to develop an Open Liberty Feature

Published by Benjamin Marwell on

Get me straight to the code!

The cowsay-liberty-feature-showcase is a minimal feature for Open Liberty. Configuration and everything from below is used to show how to set up a Liberty feature.

Description:
This feature will implement the famous "cowsay" on startup like so:

server.xml

A minimal cowsay server.xml setup
<?xml version="1.0" encoding="UTF-8"?>
<server description="cowsay">
  <featureManager>
    <feature>usr:cowsay-1.0</feature>
  </featureManager>

  <cowsay message="Hello, Liberty" />
</server>

Example output

Launching defaultServer (WebSphere Application Server 22.0.0.5/wlp-1.0.64.cl220520220425-0301) on OpenJDK 64-Bit Server VM, version 1.8.0_332-b09 (en_US)
[AUDIT   ] CWWKE0001I: The server defaultServer has been launched.
[AUDIT   ] CWWKE0100I: This product is licensed for development, and limited production use. The full license terms can be viewed here: https://public.dhe.ibm.com/ibmdl/export/pub/software/websphere/wasdev/license/base_ilan/ilan/22.0.0.5/lafiles/en.html
[INFO    ] CWWKE0002I: The kernel started after 0.544 seconds
[INFO    ] CWWKF0007I: Feature update started.
[ERROR   ] COWSA0001I:  ________________
[ERROR   ] COWSA0001I: < Hello, Liberty >
[ERROR   ] COWSA0001I:  ----------------
[ERROR   ] COWSA0001I:         \   ^__^
[ERROR   ] COWSA0001I:          \  (oo)\_______
[ERROR   ] COWSA0001I:             (__)\       )\/\
[ERROR   ] COWSA0001I:                 ||----w |
[ERROR   ] COWSA0001I:                 ||     ||
[AUDIT   ] CWWKF0012I: The server installed the following features: [usr:cowsay-1.0].
[INFO    ] CWWKF0008I: Feature update completed in 0.075 seconds.
[AUDIT   ] CWWKF0011I: The defaultServer server is ready to run a smarter planet. The defaultServer server started in 0.620 seconds.

Maven module layout

First of all, we need a maven reactor project with some modules. The minimum setup is:

pom.xml
|- bundle
|  ` pom.xml
`- feature
   ` pom.xml

In other words: We need to define three modules:

root

The reactor root project.

bundle

The jar file containing the OSGi module and the Java code.

feature

The feature description and .esa packaging.

We could combine them into one project in theory, but I find one-module-per-use-case far more helpful when writing tests. E.g. the bundle module could easily be tested within an integration test module.

Minimum files in the bundle project

Since this is a bundle project, you will at least set up the maven reactor project as a bundle type.

Setting up a type=bundle maven project

In your bundle/pom.xml, set the type to »bundle« using <type>bundle</type>. For this type to be recognized, setup the appropriate plugin as well:

Defining a bundle project
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.felix</groupId>
        <artifactId>maven-bundle-plugin</artifactId>
        <extensions>true</extensions>
      </plugin>
    </plugins>
  </build>

Explanation:

extensions

Setting this to true will enable maven to build a bundle type project (i.e. recognize the <type>bundle</type> directive).

Activator class

You will need some Java code which is going to be executed at SOME point during the Open Liberty runtime. The entry point is a class usually called Activator (or MyFeatureActivator or similar) implementing both org.osgi.framework.BundleActivator and org.osgi.service.cm.ManagedService. The BundleActivator will introduce start and stop methods, while the ManagedService will provide an update mechanism (i.e. changed configuration during the runtime).

I will skip the description of the methods emitStartMessage() and

Implementing the start() method

The start method will be executed on activation, obviously. The only thing you need is a registration call:

  @Override
  public void start(BundleContext context) throws Exception {
    configRef = context.registerService(ManagedService.class, this, this.getConfigDefaults());
  }

  protected Dictionary<String, ?> getConfigDefaults() {
    Hashtable<String, Object> configDefaults = new Hashtable<>();
    configDefaults.put(org.osgi.framework.Constants.SERVICE_PID, "cowsay");

    return configDefaults;
  }

The 2nd method is almost copied from the docs. It sets the service PID, which is the configuration tag name in your server.xml.

Implementing the stop() method

As we do not implement a "stop message" in this showcase feature, we just need to unregister the service.

  @Override
  public void stop(BundleContext context) throws Exception {
    this.configRef.unregister();
  }

Implementing the updated() method

Now this is the method which is called when the configuration gets updated. Caveat: It seems to get called AFTER the start method, but I have seen overlapping calls of those methods as well.

I implemented it in a way that there is one cowsay call after reading in the variables — if set. Since there is no further check, any configuration update will also trigger a "moo".

 @Override
  public void updated(Dictionary<String, ?> properties) throws ConfigurationException {
    if (properties == null) {
      return;
    }

    Object cowfile = properties.get("cowfile");
    if (cowfile instanceof String) {
      this.cowfile = (String) cowfile;
    }

    Object message = properties.get("message");
    if (message instanceof String) {
      this.message = (String) message;
    }

    emitStartMessage();
  }

Bundle description

We will need to add a manifest description OSGi can read. We can either write it into a file, but I find XML in this case much more useful. IDEs will usually help you with completion, and you can more easily use maven properties here (without configuring filtered resources).

The updated bundle/pom.xml
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.felix</groupId>
        <artifactId>maven-bundle-plugin</artifactId>
        <extensions>true</extensions>
        <configuration>
          <instructions>
            <Import-Package>
              org.osgi.framework,
              org.osgi.service.cm
            </Import-Package>
            <Embed-Dependency>*;scope=compile|runtime</Embed-Dependency>
            <Embed-Transitive>true</Embed-Transitive>
            <Export-Package>io.github.bmarwell.liberty.cowsay</Export-Package>
            <Private-Package>io.github.bmarwell.liberty.cowsay.*</Private-Package>
            <Bundle-Activator>io.github.bmarwell.liberty.cowsay.Activator</Bundle-Activator>
            <!--
              <Bundle-Version> is assumed to be "${pom.version}",
              but is normalized to the OSGi version format of "MAJOR.MINOR.MICRO.QUALIFIER".
              For example "2.1-SNAPSHOT" would become "2.1.0.SNAPSHOT".
              https://felix.apache.org/documentation/subprojects/apache-felix-maven-bundle-plugin-bnd.html
            -->
            <Bundle-RequiredExecutionEnvironment>JavaSE-1.8</Bundle-RequiredExecutionEnvironment>
          </instructions>
        </configuration>
      </plugin>
    </plugins>
  </build>
Bundle lazy loading

Do NOT activate lazy loading. This will result in (Open) Liberty not executing your feature AT ALL!

Explanation of what we added:

Import-Package

Here, all used imports from dependencies need to be declared on a package level. The bundle-plugin will create this list for you.

HOWEVER.
If any of your dependencies is not an OSGi bundle, this will fail. In this case, just add the packages you are using like in the example above.

Embed-Dependency

We will need to bring all dependencies in our bundle, because we cannot do that in our .esa file easily. This will results in all runtime-dependencies to be included in our final .jar file.

Embed-Transitive

In addition to this, we will need to include all transitive packages. Otherwise Open Liberty would not be able to find them.

Export-Package

You can set this (optionally), but this is also auto-generated from all packages not containing .impl or .internal in their package name. In this case it is so simple, I will just keep it.

Private-Package

By default, everything containing .internal will go here. This means, those files are being hidden from Open Liberty.

Bundle-Activator

This is one of the most important option: You will need to define the point-of-entry. Usually, the class is in the topmost package of your package structure and called either MyModuleActivator or just Activator (see below).

Bundle-RequiredExecutionEnvironment

Can be set to JavaSE-1.8, but you will need Java 1.8 anyway to run Open Liberty. But you can set it to something like JavaSE-11 to make this plugin incompatible with Java 8 runtimes.

Setting up the Open Liberty feature definition

Now that we have the bundle, we need to add some code to the feature definition. Luckily, you won’t need much of configuration either.

Creating an .esa output file

Change the type to .esa by using <type>esa</type>. Then, add the esa-maven-plugin and enable its extensions like so:

<build>
    <plugins>
      <plugin>
        <groupId>org.apache.aries</groupId>
        <artifactId>esa-maven-plugin</artifactId>
        <extensions>true</extensions>
      </plugin>
    </plugins>
  </build>

This would almost run, but we need to add some more information for Open Liberty to pick it up.

Prettier version numbers

Now you might notice, liberty feature version numbers and maven version numbers don’t match and mix well. For this reason, I decided to create a new property project.version.libertyfeature which contains just the major and minor version of the maven project.

To achieve this, do not define this property on your project. If you do, the following will not work:

Defining a feature version property in your root-pom.xml
  <build>
    <plugins>
      <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>build-helper-maven-plugin</artifactId>
        <executions>
          <execution>
            <id>set-liberty-version</id>
            <inherited>true</inherited>
            <goals>
              <goal>regex-property</goal>
            </goals>
            <configuration>
              <name>project.version.libertyfeature</name>
              <value>${project.version}</value>
              <regex>^([0-9]+)\.([0-9]+)\.([0-9]+)(-SNAPSHOT)?$</regex>
              <replacement>$1.$2</replacement>
              <failIfNoMatch>true</failIfNoMatch>
            </configuration>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>

Now, this will execute on every reactor module and extract the version number using regex, re-assemble it using the replacement and store the result into project.version.libertyfeature.

I will use the property $\{project.version.libertyfeature} in the next paragraph.

Writing the .esa manifest

Liberty needs specific information in the .esa manifest file. The full documentation is on IBMs website. Let’s go through this with simple words.

IBM-Feature-Version

2.
Must be present and always be 2.

Subsystem-Type

Must be present and set to osgi.subsystem.feature.

Subsystem-SymbolicName

The name of the feature and some configuration about how Liberty can use it. The default visiblilty must be set to public, and you can define whether multiple versions of your feature can be loaded at the same time (i.e. singleton: true, default is false) or specify whether your feature is superseeded by another feature with a different name.
Example value:
cowsay-$\{project.version.libertyfeature}; visibility:=public.

These are optional values:

Subsystem-Name

A nice name for your feature.

Subsystem-Description

A longer description for your feature.

Subsystem-Vendor

The creator. You can use a company name or just your name (or omit it as it is optional).

Subsystem-Localization

You will only need this, if you have translations for your server.xml configuration or internal strings. I recommend keeping everything in English. It always annoys me that Liberty spits out German messages whenever I run a German app.
Value: set it to the resource path of your localizations within the bundle, e.g. OSGI-INF/l10n/loc.

IBM-ShortName

What you will actually use as a feature name in your server.xml. Usually the same as the SymbolicName. But you can change it here.

IBM-AppliesTo

Most commonly used for defining a minimum Liberty version like so:
<IBM-AppliesTo>com.ibm.websphere.appserver;productVersion=20.0.0.0+</IBM-AppliesTo>

Installing the feature

Building the project

As always, just execute mvn verify. You will get a bundle file (./bundle/target/cowsay-bundle-1.0.0-SNAPSHOT.jar), a feature definition (./feature/target/cowsay-1.0/OSGI-INF/SUBSYSTEM.MF) and an esa file (./feature/target/cowsay-1.0.esa).

Using featureManager on IBM WebSphere Liberty Profile

The tool featureManager is only available on IBM WebSphere Liberty Profile, but not on Open Liberty. This is one of the few differences between those two.

To install the built esa file, use this command:

# Step 0 (optional): uninstall any previouss version.
$HOME/.local/apps/wlp-22.0.0.5/bin/featureManager uninstall --verbose cowsay-1.0
# Step 1: Install the feature from a file.
$HOME/.local/apps/wlp-22.0.0.5/bin/featureManager install --verbose --to=usr $PWD/feature/target/cowsay-1.0.esa

Using featureManager on Open Liberty

While the featureManager utility is not available on Open Liberty, you can use any WLP installation to install the feature to OL. Just set the USER dir via an environment variable:

export WLP_USER_DIR="$HOME/.local/apps/ol-22.0.0.5/usr"
# Step 0 (optional): uninstall any previouss version.
$HOME/.local/apps/wlp-22.0.0.5/bin/featureManager uninstall --verbose cowsay-1.0
# Step 1: Install the feature from a file.
$HOME/.local/apps/wlp-22.0.0.5/bin/featureManager install --verbose --to=usr $PWD/feature/target/cowsay-1.0.esa

As I have the variable WLP_USER_DIR set to $HOME/.local/share/wlp-usr in my $HOME/.profile file, I do not need to set any variable and will see the shared servers on any version of (Open)Liberty.

Installing the feature manually

If you do not want IBM WebSphere Liberty Profile to hit your harddisk for whatever reason, you can install the feature manually.

Assume you are in either your $WLP/usr or $WLP_USER_DIR dir. The resulting layout should look like this:

bmarwell@wells ~/.local/share/wlp-usr $ tree extension -lFh
[   6]  extension/
└── [ 116]  lib/
    ├── [  18]  features/
    │   └── [ 545]  cowsay.mf
    └── [1.5M]  io.github.bmarwell.liberty.cowsay.bundle_1.0.0.jar

2 directories, 2 files
  1. copy the bundle file ./bundle/target/cowsay-bundle-1.0.0-SNAPSHOT.jar to $WLP_USER_DIR/extensions/lib/${groupid}.bundle_${version}.jar.

  2. copy the feature descriptor ./feature/target/cowsay-1.0/OSGI-INF/SUBSYSTEM.MF to $WLP_USER_DIR/extensions/lib/features/${featurename}.mf.

     ___________________________________
    < That's it! Thank you for reading! >
     -----------------------------------
            \   ^__^
             \  (oo)\_______
                (__)\       )\/\
                    ||----w |
                    ||     ||
 

Where to go from here

This article only covered the basics. There are many things on my mind which need to be explained in further blog posts.

Be sure to follow my Open Liberty tag on this blog!

Using and relying on existing features

You can use existing features, like JNDI for browsing registered datasources or servlet and JSPs for creating simple endpoints. It gets even more complicated with version ranges, so I will dedicate another article to this topic.

Adding integration tests

While you can probably easily add unit tests to cover your code logic, integration tests are a different thing. You’d want to see if your plugin actually can be installed and started on Liberty, right?

As of writing, the liberty-maven-plugin sadly does not support maven property resolution nor maven reactor feature resolution yet. I have opened an issue on GitHub for the reactor esa module as well as for the maven property resolution.

As a workaround, manually copying works. You can see this in action in the actual code repo: https://github.com/bmarwell/cowsay-liberty-feature.

Manually writing .esa and bundle manifest files

This is what I had to do on my other non-public project. Some statements, like Subsystem-Content are auto-generated and cannot be overriden. The only way around this is to enable resource-filtering and configure the plugins to use the filtered manifest file instead.

Extending the idea of this feature

Yes, of course I created a "real" repository! Head over to https://github.com/bmarwell/cowsay-liberty-feature. :-)

… and many more ideas.