This document can be used as a guide to develop custom plugins.

Scope

  • How to start plugin development?

  • Tooling and APIs to build and deploy plugins

Where to Start?

We provide a sample hello world plugin which can be used as the starting point for developing a custom plugin.

In addition, we have separate docs for different types of plugins. A good starting point would be to assess what the plugin should do and then based on that, read the various docs that describe the different types of plugins offered in Kill Bill:

The next stage is to identify existing (similar) plugins which could be used as a starting point to write the code. At this point, this becomes a normal software development cycle, including writing unit tests for that plugin (independent of Kill Bill).

The rest of this document will explain how to setup and work with the hello-world-plugin.

Prerequisites

  • Ensure that you have gone through the What is Kill Bill document and are familiar with Kill Bill.

  • Ensure that you have gone through the Plugin Introduction document and are familiar with Kill Bill plugins.

  • Ensure that you have Maven 3.5.2 or higher (It can be downloaded from here).

  • Ensure that you have JDK 8 or higher (It can be downloaded from here).

  • Ensure that JAVA_HOME environment variable is set to the path of your JDK installation (For example, if JDK is installed at C:\Software\jdk1.8.0_102, you need to set JAVA_HOME to C:\Software\jdk1.8.0_102).

Getting Started

Let us now take a look at the steps involved in setting up the hello world plugin.

Setting up the Code in an IDE

The first step is to set up the plugin code in an IDE. For this, you need to do the following:

  1. Clone the hello-world-plugin repository from Github.

  2. Set up the code in the IDE of your choice. You can refer to our Development Document for detailed instructions on how to clone a repository and set up the code in Eclipse.

  3. This path where the repository is cloned will be referred to as PROJECT_ROOT from now on. For example, if you choose C:/MyProjects/killbill-hello-world-java-plugin, PROJECT_ROOT refers to this path.

  4. Once the code is set up in Eclipse, your Eclipse workspace should look like this:

    eclipse workspace with helloworldplugin

Build

The hello-world-plugin is a standard Maven project. So you can build it as follows:

  1. Open a command prompt/terminal window and navigate to the PROJECT_ROOT directory.

  2. Run the following command:

    mvn clean install -DskipTests
  3. Verify that a BUILD SUCCESS message is displayed on the console and that the plugin jar file is created as PROJECT_ROOT/target/<artifact_id>-<version>.jar.

Deployment

The hello-world-plugin can be deployed in an AWS, Docker or Tomcat Kill Bill installation as explained in the Getting Started guide. However, if you are using the plugin as a basis to develop your own plugin, it would be useful to deploy the plugin in a standalone Kill Bill installation. Let us take a look at how to do this.

  1. Ensure that the Kill Bill application is configured and running in a Jetty server as explained here.

  2. Ensure that you have kpm installed as per the instructions here.

  3. Open a command prompt/terminal window and run the following command to install the plugin (Replace PROJECT_ROOT with your actual project root, <artifact_id>-<version> with your JAR file name and path_to_install_plugin with the actual path where you would like to install the plugin):

    kpm install_java_plugin 'dev:hello' --from-source-file=PROJECT_ROOT/target/<artifact_id>-<version>.jar --destination=<path_to_install_plugin>
  4. Verify that the <artifact_id>-<version>.jar is copied at the path_to_install_plugin path.

  5. Open PROJECT_ROOT/profiles/killbill/src/main/resources/killbill-server.properties and specify the following property (Replace plugin_path with the actual path where the plugin is installed. Note that if this property is not specified, Kill Bill looks for the plugin at the /var/tmp/bundles/ path):

    org.killbill.osgi.bundle.install.dir=<plugin_path>
  6. Open a command prompt/terminal window and navigate to the PROJECT_ROOT directory. Start Kill Bill by running the following command (Replace PROJECT_ROOT with your actual project root):

     mvn -Dorg.killbill.server.properties=file:///PROJECT_ROOT/profiles/killbill/src/main/resources/killbill-server.properties -Dlogback.configurationFile=./profiles/killbill/src/main/resources/logback.xml jetty:run
  7. Verify that the following is displayed in the Kill Bill logs which confirms that the plugin is installed successfully:

      GET /               [*/*]     [*/*]    (/HelloWorldServlet.hello)
      GET /healthcheck    [*/*]     [*/*]    (/HelloWorldHealthcheckServlet.check)
    listening on:
      http://localhost:8080/plugins/hello-world-plugin/
  8. Open a browser and type http://localhost:8080/plugins/hello-world-plugin/. If the plugin is installed properly, the following should be displayed in the Kill Bill logs:

    2020-12-09T04:58:15,750+0000 lvl='INFO', log='HelloWorldServlet', th='http-nio-8080-exec-1', xff='', rId='b79decfb-e809-4c01-9064-cff18722a67c', tok='', aRId='', tRId='', Hello world

A Closer Look at Plugin Code

Let us now take a look at some of the important classes in the hello world plugin.

HelloWorldServlet

Plugins can export their own HTTP endpoints. The HelloWorldServet provides an example of this. It exports the plugins/hello-world-plugin/ endpoint. You can extend the HelloWorldServlet to add other endpoints as required.

A few pointers about the HelloWorldServlet.hello method (which provides the code for the plugins/hello-world-plugin/ endpoint):

    @GET
    public void hello(@Local @Named("killbill_tenant") final Optional<Tenant> tenant) {
        // Find me on http://127.0.0.1:8080/plugins/hello-world-plugin
        logger.info("Hello world");
        if(tenant != null && tenant.isPresent() ) {
        	Tenant t1 = tenant.get();
        	logger.info("tenant id:"+t1.getId());
        	login();
        }
        else {
        	logger.info("tenant is not available");
        }
    }
  • This method provides the code for the http://localhost:8080/plugins/hello-world-plugin endpoint.

  • It accepts a parameter corresponding to Tenant which is an Optional.

  • If the headers X-Killbill-ApiKey / X-Killbill-ApiSecret are set while accessing this endpoint as shown below, Kill Bill automatically injects a Tenant object into the servlet.

    curl -v -u admin:password -H "X-Killbill-ApiKey: bob" -H "X-Killbill-ApiSecret: lazar" "http://127.0.0.1:8080/plugins/hello-world-plugin"
  • The Tenant object can then be used to retrieve tenant information like tenantId as demonstrated in the code above.

  • If the headers X-Killbill-ApiKey / X-Killbill-ApiSecret are NOT set while accessing this endpoint as shown below, Kill Bill injects an empty Optional into the servlet.

    curl -v -u admin:password "http://127.0.0.1:8080/plugins/hello-world-plugin"

HelloWorldListener

The HelloWorldListener provides sample code for developing a notification plugin. It listens to Kill Bill events and takes actions. You can extend this class to handle other events as required. See the Notification Plugin Tutorial for further information.

HelloWorldPaymentPluginApi

The HelloWorldPaymentPluginApi class provides sample code for developing a payment plugin. It implements the PaymentPluginApi interface. You can extend this class as required to develop a payment plugin. See the Payment Plugin Tutorial for further information.

Other Classes

In addition to the classes listed above, some of the other classes in the hello-world-plugin are as follows:

Setting up a Breakpoint and Debugging

When you start developing your own plugin, it would be useful to be able to set up a break point and debug the plugin code. This section explains how you can achieve this.

  1. Create a new environment variable MAVEN_OPTS and set it to -Xdebug -Xnoagent -Djava.compiler=NONE -Xrunjdwp:transport=dt_socket,address=8000,server=y,suspend=n.

  2. Open Eclipse and do the following:

    1. Set up a break point in the HelloWorldServlet#L41.

    2. Click Run > Debug Configurations.

    3. Double click New Remote Java Application.

    4. Enter the name that you would like to give to this debug configuration in the Name field.

    5. Click Apply.

    6. Click Close.

  3. Restart the Kill Bill application as explained in the "Deployment" section above.

  4. Click Run > Debug Configurations and double click the the Debug configuration that you created above.

  5. This runs the application in debug mode. You can also set additional breakpoints as required.

Authentication Within Plugins

In order to invoke write API operations like AccountUserApi#createAccount, plugin code must authenticate against Kill Bill first. Otherwise, it will result in an org.apache.shiro.authz.UnauthenticatedException. This section explains how authentication can be done.

Authentication Steps

In order to authenticate against Kill Bill, the following needs to be done:

  1. Configure the plugin with custom credentials - Although plugins can use the admin/password credentials for authentication, it is typically not advisable to do so. This is to limit the scope of operations that plugins can execute. It is thus recommended to configure plugins with custom credentials. (See Per-tenant Plugin Configuration). So, you can configure the hello-world-plugin with custom credentials as follows:

    curl -v \
        -X POST \
        -u admin:password \
        -H "X-Killbill-ApiKey: bob" \
        -H "X-Killbill-ApiSecret: lazar" \
        -H "Content-Type: text/plain" \
        -H "Accept: application/json" \
        -H "X-Killbill-CreatedBy: demo" \
        -H "X-Killbill-Reason: demo" \
        -H "X-Killbill-Comment: demo" \
       -d 'org.killbill.billing.plugin.hello-world.credentials.username=hello-world-user
    org.killbill.billing.plugin.hello-world.credentials.password=hello-world-password' \
        "http://127.0.0.1:8080/1.0/kb/tenants/uploadPluginConfig/hello-world-plugin"
  2. Retrieve credentials in the code. For example, you can retrieve the credentials in the HelloWorldListener class as follows:

    Properties properties = helloWorldConfigurationHandler.getConfigurable(killbillEvent.getTenantId());
    final String username = properties.getProperty("org.killbill.billing.plugin.hello-world.credentials.username");
    final String password = properties.getProperty("org.killbill.billing.plugin.hello-world.credentials.password");
  3. Invoke SecurityApi - Use the credentials obtained above to login as follows:

    killbillAPI.getSecurityApi().login(login, password);
  4. Invoke the necessary write API method (The code below invokes the accountUserApi.createAccount method):

     osgiKillbillAPI.getAccountUserApi().createAccount(accountData, context);
  5. Invoke the logout method. This should typically be done within a finally clause:

    osgiKillbillAPI.getSecurityApi().logout();
  6. You can also perform authentication within the HelloWorldPaymentPluginApi as well as HelloWorldServlet. Within the servlet, you will need to write code similar to the following:

    private void login(final HttpServletRequest req) {
        String authHeader = req.getHeader("Authorization");
        if (authHeader == null) {
            return;
        }
    
        final String[] authHeaderChunks = authHeader.split(" ");
        if (authHeaderChunks.length < 2) {
            return;
        }
    
        try {
            final String credentials = new String(BaseEncoding.base64().decode(authHeaderChunks[1]), "UTF-8");
            int p = credentials.indexOf(":");
            if (p == -1) {
                return;
            }
    
            final String login = credentials.substring(0, p).trim();
            final String password = credentials.substring(p + 1).trim();
            killbillAPI.getSecurityApi().login(login, password);
        } catch (UnsupportedEncodingException ignored) {
        }
    }

Skipping Authentication

It is also possible to skip authentication in the plugin code. For this, the following needs to be done:

  1. Set the following property in the Kill Bill config file:

    org.killbill.security.skipAuthForPlugins=true
  2. Create a PluginCallContext class in your code similar to the email notification plugin PluginCallContext class.

  3. Create a PluginCallContext instance with CallOrigin.INTERNAL and UserType.ADMIN as follows:

    final PluginCallContext callContext = new PluginCallContext(UUID.randomUUID(),pluginName, CallOrigin.INTERNAL,UserType.ADMIN,reasonCode,comments, createdDate,updatedDate,accountId, tenantId);
  4. Use the above callContext while invoking the desired api method:

    accountUserApi.createAccount(accountData, callContext);

Additional Notes

  • We provide a Java plugin framework that can be used to implement some of the work that plugins need to do - although your plugin does not have to rely on this framework, it is often a good idea to leverage it to avoid boilerplate code.

  • Also, for internal reference, you might want to take a look at KillbillActivatorBase, which provides all the abstractions that plugins require (access to java APIs, database connections, system properties, logging, …​).

OSGi Configuration

As explained earlier, Kill Bill plugins are based on the OSGi standard. Let us now take a look at how this works and some additional OSGi configuration which may be required in some situations.

Note
Note: OSGi configuration is an advanced configuration option and may be required only in rare situations. So, feel free to skip this section.

Brief OSGi Overview

Let us first briefly understand how OSGi works. OSGi allows creating modular Java components (known as bundles) that run within an OSGi container. The OSGi container ensures that each bundle is isolated from other bundles. Thus, each bundle can use any external dependencies that it requires without having to worry about conflicts.

A bundle is nothing but a JAR file. However, its manifest.mf has some additional OSGi related headers.

Although each bundle is isolated from other bundles, sometimes bundles may need to communicate/share classes with other bundles. A bundle can export a package to make the corresponding classes available for use by other bundles. A bundle can also import a package to use the classes of another bundle.

For example if a bundle bundle1 requires a class p1.p2.A from bundle2, bundle2 needs to export the p1.p2 package and bundle1 needs to import this package. The packages imported by a bundle are specified as a Import-package header in the manifest.mf while packages exported by a bundle are specified as a Export-package header in the manifest.mf.

The OSGi container ensures that a given bundle’s package dependencies can be satisfied before the bundle runs. Thus, if the package dependencies cannot be satisfied, the bundle will not run.

Kill Bill OSGi Overview

Before we dive into the details, let us understand at a high-level how the import-export mechanism works in case of the core Kill Bill system and its plugins.

Packages Exported by Kill Bill

As explained earlier, the Kill Bill system bundle exports the packages which it desires to share with plugins. Refer to the value of the org.killbill.osgi.system.bundle.export.packages.api property in the Kill Bill Configuration Properties Table to see the complete list of packages exported by default.

Additionally, Kill Bill also offers the org.killbill.osgi.system.bundle.export.packages.extra property which can be used to specify additional packages to be exported by the system bundle and that could in turn be imported by a plugin. This property can be configured as explained in the Kill Bill configuration document.

Packages Imported by Plugins by Default

As explained earlier, Kill Bill plugins are packaged as OSGi bundles. The maven-bundle-plugin specified in the pom.xml is responsible for packaging a plugin as an OSGi bundle. Thus, the maven-bundle-plugin takes care of creating the jar with the correct OSGi headers (including adding the required packages to the Import-Package header). In addition, the killbill-oss-parent pom file (which is the parent of the plugin pom.xml file) also specifies the packages to be included in the Import-Package header.

Thus, when a plugin is built, the Import-Package header is automatically computed based on:

  • Packages computed by the maven-bundle-plugin.

  • Packages specified in the killbill-oss-parent pom file.

Importing Additional Packages in Plugins

Sometimes, a plugin may require to use some additional packages from Kill Bill (other than those automatically imported as specified above). In such cases, you will need to explicitly export the package from Kill Bill and import it in the plugin as explained below.

  1. All the packages exported by Kill Bill by default are specified as the value of the org.killbill.osgi.system.bundle.export.packages.api property in the Kill Bill Configuration Properties Table. Check whether the desired package is already present in this list.

  2. If Kill Bill does not already export the package, add the following property in the Kill Bill configuration file:

    org.killbill.osgi.system.bundle.export.packages.extra=<package1>,<package2>..<packagen>
  3. Open plugin pom.xml and specify the following in the properties section (Replace <package> with the fully qualified name of the package that you would like to export):

     <osgi.extra-import>
                <package1>;
                <package2>;
                ....
                <packagen>
    </osgi.extra-import>
  4. Build the plugin using Maven as specified above.

This causes the package to be added to the Import-Package header of the plugin jar. You can see an example of this in the Kill Bill Adyen Plugin pom file.

Exporting Additional Packages from a Plugin

A plugin can also export packages corresponding to the classes that it wants to share with other plugins. This mechanism is particularly useful since it allows plugins to share custom functionality with other plugins.

To export a package from a plugin, you need to follow the steps given below.

  1. Open plugin pom.xml. Specify the following in the properties section (Replace <package> with the fully qualified name of the package that you would like to export):

    <osgi.export>
      <package1>,
      <package2>
       ....
      <packagen>
    </osgi.export>
  2. Build the plugin using Maven as specified above.

This causes the specified packages to be added to the Export-Package header of the plugin jar. Other plugins can then use the classes in these packages by importing them as explained above.

In order to see an example of a plugin exporting packages, you can refer to the email notification plugin. It exports packages required for creating a custom invoice formatter. The custom invoice formatter plugin (which is a sample plugin) then imports these packages to customize the email invoice.

FAQ

This section lists some common errors that are encountered while developing plugins and how you can fix them.

Authentication Error

Sometimes, you may see the org.apache.shiro.authz.UnauthenticatedException: This subject is anonymous. This occurs when your plugin code invokes any of the read/write Kill Bill operations without authenticating against Kill Bill. So, you first need to invoke SecurityApi#login API. See "Authentication Steps" section.

Maven Build Error

Sometimes, when you run mvn clean install on the plugin code, you may see the following Maven error:

Failed to execute goal org.apache.maven.plugins:maven-enforcer-plugin:3.0.0-M3:enforce (default) on project killbill-plugin-momo: Some Enforcer rules have failed.

This generally occurs when your pom.xml contains dependencies that are not recommended. The maven build has lots of checks in place to make sure the right dependencies are pulled in, there are no duplicate dependencies, there are no obvious bugs, etc. If you’d like to ignore all these checks and still proceed with the build, you can run the following command:

mvn clean install -Dcheck.fail-enforcer=false -Dcheck.fail-dependency=false -Dcheck.fail-dependency-scope=false -Dcheck.fail-dependency-versions=false -Dcheck.fail-duplicate-finder=false -Dcheck.fail-enforcer=false -Dcheck.fail-spotbugs=false -Dcheck.ignore-rat=true

However, this is not recommended, we recommend that you fix the POM file and run the build with all the checks in place.

java.lang.NoClassDefFoundError or java.lang.ClassNotFoundException

Sometimes, when you develop a custom plugin, a java.lang.NoClassDefFoundError or a java.lang.ClassNotFoundException exception may occur on starting Kill Bill. This is most probably because the class in question is not present on the classpath.

For a plugin to work, any classes used by the plugin must be present on the classpath. So, the class needs to be present in the plugin jar itself or it must be imported from Kill Bill. Refer to the "Importing Additional Packages in Plugins" section above for further details.