Creating Your First Plugin

Level: Intermediate Module: Plugin Development 14 min read Lesson 21 of 47

Overview

  • What you’ll learn:
    • How to set up a new iDempiere plugin project in Eclipse with the correct directory structure
    • How to configure MANIFEST.MF, write an Activator class, and define Declarative Services components
    • How to build, deploy, and hot-reload your plugin, including the difference between plugin fragments and standalone plugins
  • Prerequisites: Lessons 1–20 (especially Lessons 18–19: OSGi Framework, Extension Points and Factories)
  • Estimated reading time: 25 minutes

Introduction

Everything you have learned about iDempiere’s architecture — OSGi bundles, Declarative Services, factory interfaces, and event handlers — comes together when you build your first plugin. A plugin is how you add custom functionality to iDempiere without modifying the core source code. It is how you ship your customizations, share them with others, and keep them maintainable across iDempiere upgrades.

This lesson is a hands-on walkthrough. We will create a complete plugin from an empty Eclipse workspace to a running extension inside iDempiere. Along the way, you will learn the conventions, configuration patterns, and deployment procedures that every iDempiere plugin developer needs to know.

Plugin Project Structure

Every iDempiere plugin follows a standard directory layout. Before we start building, let us look at the complete structure we are aiming for:

com.example.helloworld/
├── META-INF/
│   └── MANIFEST.MF              # OSGi bundle metadata
├── OSGI-INF/
│   └── EventHandler.xml          # Declarative Services component definitions
├── src/
│   └── com/
│       └── example/
│           └── helloworld/
│               ├── Activator.java        # Bundle lifecycle management
│               └── HelloEventHandler.java # Event handler implementation
├── build.properties              # Eclipse PDE build configuration
└── pom.xml                       # Maven build file (optional, for CI/CD)

This structure is an OSGi bundle project. The three critical directories are:

  • META-INF/ — Contains the MANIFEST.MF file that defines the bundle’s identity, version, and dependencies.
  • OSGI-INF/ — Contains Declarative Services component XML files that register your services with the OSGi framework.
  • src/ — Contains your Java source code organized in standard Java package structure.

Creating the Plugin Project in Eclipse

iDempiere development uses the Eclipse IDE with the Plugin Development Environment (PDE) tools. Here is how to create a new plugin project:

Step 1: Open Eclipse with iDempiere Workspace

Start Eclipse and open the workspace that contains the iDempiere source code. You should see the core iDempiere projects (org.adempiere.base, org.adempiere.ui.zk, etc.) in the Package Explorer. If you have not set up the iDempiere development environment yet, follow the setup instructions on the iDempiere wiki.

Step 2: Create a New Plug-in Project

  1. Go to File > New > Plug-in Project
  2. Enter the project name: com.example.helloworld
  3. Ensure the target platform is set to the iDempiere target platform (not the default Eclipse platform)
  4. Click Next
  5. Fill in the plug-in properties:
    • ID: com.example.helloworld
    • Version: 1.0.0.qualifier
    • Name: Hello World Plugin
    • Vendor: Example Corp
    • Execution Environment: JavaSE-17
    • Check Generate an activator
  6. Click Finish

Eclipse creates the project with a basic structure including MANIFEST.MF and an Activator class.

Step 3: Create the OSGI-INF Directory

Right-click the project, select New > Folder, and create a folder named OSGI-INF. This is where Declarative Services component XML files will live.

MANIFEST.MF Configuration

The MANIFEST.MF file is the most critical configuration in your plugin. Let us build one step by step:

Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: Hello World Plugin
Bundle-SymbolicName: com.example.helloworld;singleton:=true
Bundle-Version: 1.0.0.qualifier
Bundle-Activator: com.example.helloworld.Activator
Bundle-Vendor: Example Corp
Bundle-RequiredExecutionEnvironment: JavaSE-17
Automatic-Module-Name: com.example.helloworld
Bundle-ActivationPolicy: lazy
Import-Package: org.compiere.model;version="0.0.0",
 org.compiere.process;version="0.0.0",
 org.compiere.util;version="0.0.0",
 org.adempiere.base.event;version="0.0.0",
 org.adempiere.base.event.annotations;version="0.0.0",
 org.adempiere.exceptions;version="0.0.0",
 org.osgi.framework;version="1.9.0",
 org.osgi.service.component.annotations;version="1.3.0",
 org.osgi.service.event;version="1.4.0"
Export-Package: com.example.helloworld
Service-Component: OSGI-INF/*.xml

Let us review the important decisions in this configuration:

Dependencies: What to Import

The Import-Package header lists every external Java package your plugin uses. For a typical iDempiere plugin, you will need:

  • org.compiere.model — Access to PO (Persistent Object), MTable, and model classes
  • org.compiere.process — Access to SvrProcess, ProcessInfo for custom processes
  • org.compiere.util — Access to Env, CLogger, DB, and other utilities
  • org.adempiere.base.event — Access to event handler base classes and IEventTopics
  • org.osgi.framework — OSGi framework APIs for the Activator
  • org.osgi.service.event — OSGi Event Admin for event handling

Only import packages you actually use. Unnecessary imports can cause resolution failures if those packages are not available.

Exported Packages

The Export-Package header declares which of your packages are visible to other bundles. If your plugin is self-contained and no other plugins depend on it, you can omit this header entirely. If you provide an API that other plugins consume, export only the API packages — keep implementation packages private.

Service-Component Wildcard

The Service-Component: OSGI-INF/*.xml directive tells the OSGi framework to load all XML files in the OSGI-INF directory as Declarative Services component definitions. This is convenient because you can add new components by simply adding XML files without modifying MANIFEST.MF.

The Activator Class

The Activator is a class that implements org.osgi.framework.BundleActivator. It receives callbacks when the bundle starts and stops. For many plugins, the Activator performs initialization logic — registering services, setting up resources, or triggering 2Pack migrations.

package com.example.helloworld;

import java.util.logging.Level;
import org.compiere.util.CLogger;
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;

public class Activator implements BundleActivator {

    private static final CLogger logger =
        CLogger.getCLogger(Activator.class);

    @Override
    public void start(BundleContext context) throws Exception {
        logger.log(Level.INFO,
            "Hello World Plugin started! Bundle ID: "
            + context.getBundle().getBundleId());
    }

    @Override
    public void stop(BundleContext context) throws Exception {
        logger.log(Level.INFO,
            "Hello World Plugin stopped!");
    }
}

Using AdempiereActivator

If your plugin includes 2Pack migration files, extend org.adempiere.plugin.utils.AdempiereActivator instead of implementing BundleActivator directly. This base class automatically applies 2Pack files from your bundle’s migration/ directory during startup:

package com.example.helloworld;

import org.adempiere.plugin.utils.AdempiereActivator;
import org.osgi.framework.BundleContext;

public class Activator extends AdempiereActivator {

    @Override
    public void start(BundleContext context) throws Exception {
        super.start(context);  // Applies 2Pack migrations
        // Additional initialization here
    }

    @Override
    public void stop(BundleContext context) throws Exception {
        // Cleanup here
        super.stop(context);
    }
}

Declarative Services Component: component.xml

Rather than manually registering services in the Activator, we use Declarative Services to declare what our plugin provides. Let us create an event handler that logs a message whenever a new Business Partner is created.

The Event Handler Class

package com.example.helloworld;

import java.util.logging.Level;
import org.adempiere.base.event.AbstractEventHandler;
import org.adempiere.base.event.IEventTopics;
import org.compiere.model.PO;
import org.compiere.util.CLogger;
import org.osgi.service.event.Event;

public class HelloEventHandler extends AbstractEventHandler {

    private static final CLogger logger =
        CLogger.getCLogger(HelloEventHandler.class);

    @Override
    protected void initialize() {
        // Subscribe to events for C_BPartner table
        registerTableEvent(IEventTopics.PO_AFTER_NEW, "C_BPartner");
        logger.log(Level.INFO,
            "HelloEventHandler initialized - listening for "
            + "new Business Partners");
    }

    @Override
    protected void doHandleEvent(Event event) {
        String topic = event.getTopic();

        if (topic.equals(IEventTopics.PO_AFTER_NEW)) {
            PO po = getPO(event);
            String name = (String) po.get_Value("Name");
            logger.log(Level.INFO,
                "Hello World! A new Business Partner was created: "
                + name);
        }
    }
}

The Component XML

Create the file OSGI-INF/EventHandler.xml:

<?xml version="1.0" encoding="UTF-8"?>
<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0"
    name="com.example.helloworld.HelloEventHandler"
    immediate="true">
  <implementation
      class="com.example.helloworld.HelloEventHandler"/>
  <service>
    <provide interface="org.osgi.service.event.EventHandler"/>
  </service>
  <property name="event.topics" type="String">
    org/adempiere/base/event/*
  </property>
</scr:component>

This XML tells the DS runtime: “Create an instance of HelloEventHandler, register it as an EventHandler service, and subscribe it to all PO event topics.” The immediate="true" attribute ensures the component is activated as soon as the bundle starts.

The build.properties File

Eclipse PDE uses build.properties to know which files to include in the built plugin. Create or verify this file in the project root:

source.. = src/
output.. = bin/
bin.includes = META-INF/,\
               OSGI-INF/,\
               .

This tells the build system that source files are in src/, compiled classes go to bin/, and the final JAR should include META-INF/, OSGI-INF/, and the compiled classes.

Building the Plugin

There are several ways to build your plugin into a deployable JAR:

Export from Eclipse

  1. Right-click the project in the Package Explorer
  2. Select Export > Plug-in Development > Deployable plug-ins and fragments
  3. Select your plugin in the list
  4. Choose a destination directory (e.g., /tmp/plugins/)
  5. Click Finish

Eclipse will compile your code and create a JAR file named com.example.helloworld_1.0.0.qualifier.jar (with the qualifier replaced by a timestamp).

Build with Maven/Tycho

For automated builds, you can use Maven with the Tycho plugin, which understands OSGi bundle metadata. A minimal pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.example</groupId>
  <artifactId>com.example.helloworld</artifactId>
  <version>1.0.0-SNAPSHOT</version>
  <packaging>eclipse-plugin</packaging>

  <build>
    <plugins>
      <plugin>
        <groupId>org.eclipse.tycho</groupId>
        <artifactId>tycho-maven-plugin</artifactId>
        <version>4.0.4</version>
        <extensions>true</extensions>
      </plugin>
    </plugins>
  </build>
</project>

Deploying to iDempiere

Deployment is straightforward: place the built JAR file into the plugins/ directory of your iDempiere installation.

# Copy the plugin to the iDempiere plugins directory
cp com.example.helloworld_1.0.0.jar \
    $IDEMPIERE_HOME/plugins/

Hot Deployment

If iDempiere is already running, it will automatically detect the new JAR file, install the bundle, resolve its dependencies, and start it. You should see your Activator’s log message in the iDempiere server log within a few seconds.

To verify the bundle is running, connect to the OSGi console and check:

# Connect to the OSGi console
telnet localhost 11612

# List bundles matching your name
ss helloworld

# Expected output:
# 285 ACTIVE com.example.helloworld_1.0.0

Updating a Deployed Plugin

To update a plugin that is already deployed:

  1. Build the new version of your plugin JAR
  2. Replace the old JAR in the plugins/ directory with the new one
  3. The framework will detect the change, stop the old version, and start the new one

Alternatively, from the OSGi console:

# Update a specific bundle
update 285

# Or refresh all bundles to recalculate wiring
refresh

Running from Eclipse (Development Mode)

During development, you typically run iDempiere directly from Eclipse rather than deploying JARs. To include your plugin in the Eclipse launch configuration:

  1. Open Run > Run Configurations
  2. Select your iDempiere launch configuration (typically “iDempiere Server” or similar)
  3. Go to the Plug-ins tab
  4. Find com.example.helloworld in the workspace plugins list and check it
  5. Click Apply and then Run

With this setup, any code changes you make in Eclipse are immediately reflected when you restart the server (or in some cases, through hot code replacement during debugging).

Plugin Fragments vs Standalone Plugins

There are two types of OSGi bundles you can create for iDempiere: standalone plugins and plugin fragments. Understanding the difference is important.

Standalone Plugin (Bundle)

This is what we have been building. A standalone plugin is a self-contained bundle with its own classloader, activator, and lifecycle. It declares dependencies on other bundles via Import-Package or Require-Bundle, and the OSGi framework manages the wiring.

  • Advantages: Clean separation, independent lifecycle, can be installed/uninstalled/updated independently
  • Use when: Adding new functionality, new event handlers, new processes, new forms — most customizations

Plugin Fragment

A fragment is a special type of bundle that attaches to a host bundle and shares the host’s classloader. A fragment does not have its own Activator and cannot register its own services independently. Instead, its classes and resources are merged into the host bundle’s classpath.

# Fragment MANIFEST.MF
Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: My Fragment
Bundle-SymbolicName: com.example.myfragment
Bundle-Version: 1.0.0
Fragment-Host: org.adempiere.base;bundle-version="11.0.0"

The key difference is the Fragment-Host header, which specifies which bundle this fragment attaches to.

  • Advantages: Can access the host bundle’s internal (non-exported) packages, can provide resources (configuration files, translations) that the host bundle can find via its own classloader
  • Use when: You need to override or supplement resources in a core bundle (e.g., adding translations, modifying configuration files), or you need access to internal classes that are not exported
  • Caution: Fragments are more tightly coupled to their host bundle. Changes to the host bundle’s internals can break your fragment. Use standalone plugins whenever possible.

Practical Example: Complete Hello World Plugin

Let us bring everything together. Here is the complete source code for a minimal but functional plugin that logs a greeting whenever a new Business Partner is created in iDempiere.

Project Structure

com.example.helloworld/
├── META-INF/
│   └── MANIFEST.MF
├── OSGI-INF/
│   └── EventHandler.xml
├── src/
│   └── com/
│       └── example/
│           └── helloworld/
│               ├── Activator.java
│               └── HelloEventHandler.java
└── build.properties

META-INF/MANIFEST.MF

Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: Hello World Plugin
Bundle-SymbolicName: com.example.helloworld;singleton:=true
Bundle-Version: 1.0.0.qualifier
Bundle-Activator: com.example.helloworld.Activator
Bundle-Vendor: Example Corp
Bundle-RequiredExecutionEnvironment: JavaSE-17
Import-Package: org.compiere.model;version="0.0.0",
 org.compiere.util;version="0.0.0",
 org.adempiere.base.event;version="0.0.0",
 org.osgi.framework;version="1.9.0",
 org.osgi.service.event;version="1.4.0"
Service-Component: OSGI-INF/*.xml

src/com/example/helloworld/Activator.java

package com.example.helloworld;

import java.util.logging.Level;
import org.compiere.util.CLogger;
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;

public class Activator implements BundleActivator {

    private static final CLogger logger =
        CLogger.getCLogger(Activator.class);

    @Override
    public void start(BundleContext context) throws Exception {
        logger.log(Level.INFO,
            "=== Hello World Plugin STARTED (bundle "
            + context.getBundle().getBundleId() + ") ===");
    }

    @Override
    public void stop(BundleContext context) throws Exception {
        logger.log(Level.INFO,
            "=== Hello World Plugin STOPPED ===");
    }
}

src/com/example/helloworld/HelloEventHandler.java

package com.example.helloworld;

import java.util.logging.Level;
import org.adempiere.base.event.AbstractEventHandler;
import org.adempiere.base.event.IEventTopics;
import org.compiere.model.PO;
import org.compiere.util.CLogger;
import org.osgi.service.event.Event;

public class HelloEventHandler extends AbstractEventHandler {

    private static final CLogger logger =
        CLogger.getCLogger(HelloEventHandler.class);

    @Override
    protected void initialize() {
        registerTableEvent(IEventTopics.PO_AFTER_NEW, "C_BPartner");
        registerTableEvent(IEventTopics.PO_AFTER_CHANGE, "C_BPartner");
        logger.log(Level.INFO,
            "HelloEventHandler initialized");
    }

    @Override
    protected void doHandleEvent(Event event) {
        PO po = getPO(event);
        String name = po.get_ValueAsString("Name");
        String topic = event.getTopic();

        if (IEventTopics.PO_AFTER_NEW.equals(topic)) {
            logger.log(Level.INFO,
                "Hello! New Business Partner created: " + name);
        } else if (IEventTopics.PO_AFTER_CHANGE.equals(topic)) {
            logger.log(Level.INFO,
                "Hello! Business Partner updated: " + name);
        }
    }
}

OSGI-INF/EventHandler.xml

<?xml version="1.0" encoding="UTF-8"?>
<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0"
    name="com.example.helloworld.HelloEventHandler"
    immediate="true">
  <implementation
      class="com.example.helloworld.HelloEventHandler"/>
  <service>
    <provide interface="org.osgi.service.event.EventHandler"/>
  </service>
  <property name="event.topics" type="String">
    org/adempiere/base/event/*
  </property>
</scr:component>

build.properties

source.. = src/
output.. = bin/
bin.includes = META-INF/,\
               OSGI-INF/,\
               .

Testing the Plugin

After deploying (or running from Eclipse), test the plugin by:

  1. Opening iDempiere in your browser
  2. Navigating to the Business Partner window
  3. Creating a new Business Partner record
  4. Saving the record
  5. Checking the server log for your “Hello! New Business Partner created:” message

If you see the log message, your plugin is working correctly. If not, check the OSGi console with ss helloworld to verify the bundle is ACTIVE, and use scr:info com.example.helloworld.HelloEventHandler to verify the DS component is activated.

Key Takeaways

  • An iDempiere plugin is an OSGi bundle with three key components: MANIFEST.MF (identity and dependencies), OSGI-INF/*.xml (service declarations), and Java source code.
  • Use Eclipse PDE to create plugin projects, with the iDempiere target platform configured for dependency resolution.
  • The Activator class handles bundle lifecycle (start/stop). Extend AdempiereActivator if you need automatic 2Pack migration support.
  • Declarative Services component.xml files register your event handlers, factories, and other services without procedural code.
  • Deploy by copying the built JAR to the plugins/ directory. iDempiere supports hot deployment without restart.
  • Use standalone plugins for most customizations. Use plugin fragments only when you need access to a host bundle’s internal classes or resources.

What’s Next

Your Hello World plugin reacts to events by logging messages. In the next lesson, we will dive deep into the model event system, learning how to use event handlers to implement real business logic — validating data, auto-populating fields, preventing invalid saves, and responding to document state changes.

You Missed