OSGi Framework in iDempiere

Level: Intermediate Module: Architecture 15 min read Lesson 18 of 47

Overview

  • What you’ll learn:
    • The OSGi specification and how Eclipse Equinox serves as the OSGi container in iDempiere
    • Bundle lifecycle management, MANIFEST.MF configuration, and Declarative Services component model
    • How to work with extension points, service references, bundle dependencies, hot deployment, and debug common OSGi resolution failures
  • Prerequisites: Lessons 1–17 (especially Lesson 2: iDempiere Architecture Overview)
  • Estimated reading time: 22 minutes

Introduction

iDempiere’s modular architecture is built on top of the OSGi (Open Services Gateway initiative) framework. This is not a superficial design choice — OSGi permeates every aspect of how iDempiere is structured, deployed, and extended. When you install a plugin, register a model validator, or add a custom process, you are using OSGi whether you realize it or not.

In Lesson 2, we introduced OSGi at a high level. This lesson takes a deep dive into the framework itself. We will examine how bundles work internally, trace a bundle through its entire lifecycle, dissect the MANIFEST.MF metadata file, explore the Declarative Services component model that iDempiere uses for dependency injection, and learn how to diagnose the OSGi issues that inevitably arise during plugin development. By the end, you will have a thorough understanding of the plumbing that makes iDempiere’s extensibility possible.

The OSGi Specification

OSGi is a Java modularity specification originally developed for embedded systems and home gateways in the late 1990s. It has since evolved into the standard modularity layer for enterprise Java applications. The specification defines:

  • A module system — Java code is organized into bundles, each with explicit declarations of what it provides and what it requires.
  • A lifecycle layer — Bundles can be installed, started, stopped, updated, and uninstalled at runtime without restarting the JVM.
  • A service layer — Bundles can publish services (Java objects) into a service registry and look up services published by other bundles.

The key insight of OSGi is that Java’s default classloading model (a flat classpath) is inadequate for large, modular applications. Without OSGi, all JAR files share a single classpath and any class can see any other class. This leads to version conflicts, hidden dependencies, and fragile applications that break when a single library is updated. OSGi solves this by giving each bundle its own classloader with strictly controlled visibility.

Eclipse Equinox: iDempiere’s OSGi Container

Several OSGi implementations exist: Apache Felix, Knopflerfish, and Eclipse Equinox. iDempiere uses Eclipse Equinox, the reference implementation of the OSGi specification and the same runtime that powers the Eclipse IDE.

Equinox provides:

  • A compliant OSGi R7+ framework implementation
  • The osgi.console — a command-line console for inspecting and managing bundles at runtime
  • Integration with Eclipse PDE (Plugin Development Environment) for development tooling
  • The p2 provisioning system for managing bundle installation and updates

When iDempiere starts, the Equinox framework initializes first, then loads and resolves all configured bundles, and finally starts them in dependency order. The entire iDempiere application — including the web server, business logic, and UI — runs as a collection of OSGi bundles inside this container.

Bundles: The Building Blocks

A bundle is the fundamental unit of modularity in OSGi. Physically, a bundle is a JAR file with special metadata in its META-INF/MANIFEST.MF file. Logically, a bundle is an independently deployable module with well-defined boundaries.

iDempiere’s core consists of many bundles. Some of the most important ones are:

  • org.adempiere.base — Core model classes, business logic, and utility functions
  • org.adempiere.ui.zk — The ZK-based web user interface layer
  • org.adempiere.server — Server processes, schedulers, and background tasks
  • org.adempiere.report.jasper — JasperReports integration
  • org.adempiere.plugin.utils — Utility classes for plugin development
  • org.compiere.db.postgresql — PostgreSQL database driver and dialect

Each of these bundles exports specific Java packages that other bundles (including your plugins) can import and use.

The Bundle Lifecycle

Every OSGi bundle goes through a well-defined lifecycle with the following states:

INSTALLED → RESOLVED → STARTING → ACTIVE → STOPPING → UNINSTALLED

Understanding these states is critical for troubleshooting:

  1. INSTALLED — The bundle’s JAR file has been placed into the framework, but its dependencies have not yet been verified. The framework knows the bundle exists but has not checked whether all required packages are available.
  2. RESOLVED — The framework has verified that all of the bundle’s declared dependencies (Import-Package, Require-Bundle) can be satisfied by other installed bundles. The bundle’s classloader has been created and wired to the appropriate provider bundles. The bundle is ready to start but is not yet active.
  3. STARTING — The bundle’s BundleActivator.start() method (if one is declared) is currently executing. This is a transient state.
  4. ACTIVE — The bundle is fully operational. Its activator has completed, its services are registered, and its code is available to other bundles.
  5. STOPPING — The bundle’s BundleActivator.stop() method is executing. Resources are being released and services are being unregistered. This is also a transient state.
  6. UNINSTALLED — The bundle has been removed from the framework. Its classloader is discarded, and its packages are no longer available.

The most common problem you will encounter is a bundle stuck in the INSTALLED state when it should be ACTIVE. This means the framework could not resolve one or more of its dependencies. We will cover debugging this in the final section of this lesson.

MANIFEST.MF: The Bundle Descriptor

The META-INF/MANIFEST.MF file is the heart of every OSGi bundle. It declares the bundle’s identity, version, dependencies, and capabilities. Here is a typical MANIFEST.MF for an iDempiere plugin:

Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: My Custom Plugin
Bundle-SymbolicName: com.example.myplugin;singleton:=true
Bundle-Version: 1.0.0.qualifier
Bundle-Activator: com.example.myplugin.Activator
Bundle-Vendor: Example Corp
Bundle-RequiredExecutionEnvironment: JavaSE-17
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.osgi.framework;version="1.9.0",
 org.osgi.service.event;version="1.4.0"
Export-Package: com.example.myplugin.api
Service-Component: OSGI-INF/component.xml

Let us break down each critical header:

Bundle-SymbolicName

This is the unique identifier for the bundle within the OSGi framework. By convention, it follows the reverse domain name pattern of the bundle’s base Java package. The singleton:=true directive means only one version of this bundle can be active at a time — this is required for bundles that contribute to extension points.

Bundle-Version

The version follows the format major.minor.micro.qualifier. OSGi uses semantic versioning to resolve dependencies. The qualifier part is a string typically set to a timestamp or “qualifier” (replaced during the build). Version ranges in dependency declarations allow the framework to handle version compatibility automatically.

Bundle-Activator

Specifies a class that implements org.osgi.framework.BundleActivator. This class receives callbacks when the bundle starts and stops. Not every bundle needs an activator — Declarative Services (covered below) often eliminate the need for one.

Import-Package

Declares the Java packages that this bundle requires from other bundles. The OSGi framework will not resolve (and therefore will not start) a bundle unless every imported package is available from some other active bundle. The version attribute specifies the minimum acceptable version of the package.

Import-Package: org.compiere.model;version="[11.0.0,12.0.0)",
 org.compiere.util;version="0.0.0"

Version ranges use interval notation: [11.0.0,12.0.0) means version 11.0.0 (inclusive) up to but not including 12.0.0.

Export-Package

Declares the Java packages from this bundle that are visible to other bundles. Any package not listed in Export-Package is private to the bundle and invisible to other bundles — even if it exists in the JAR file. This is how OSGi enforces encapsulation.

Service-Component

Points to the XML files that define Declarative Services components (covered in the next section). Multiple component files can be listed, separated by commas.

Require-Bundle vs Import-Package

You may also see a Require-Bundle header, which declares a dependency on an entire bundle rather than on specific packages:

Require-Bundle: org.adempiere.base;bundle-version="11.0.0"

While this is simpler to write, the OSGi community generally recommends Import-Package because it creates a looser coupling — your bundle depends on a capability (a package) rather than a specific provider (a bundle). However, in the iDempiere ecosystem, Require-Bundle is common because the core bundles are the only providers of their packages.

Declarative Services (DS)

Declarative Services is an OSGi component model that simplifies the creation and consumption of services. Instead of writing procedural code in an Activator to register and look up services, you declare your components and their dependencies in XML, and the DS runtime handles the wiring automatically.

iDempiere relies heavily on Declarative Services for its extension mechanisms — model factories, callout factories, event handlers, and more are all registered as DS components.

Component XML

A DS component is defined in an XML file, typically placed in the OSGI-INF/ directory. Here is a representative example:

<?xml version="1.0" encoding="UTF-8"?>
<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0"
    name="com.example.myplugin.MyEventHandler"
    immediate="true">
  <implementation class="com.example.myplugin.MyEventHandler"/>
  <service>
    <provide interface="org.osgi.service.event.EventHandler"/>
  </service>
  <property name="event.topics" type="String"
      value="org/adempiere/base/event/PO_BEFORE_NEW"/>
  <property name="component.name" type="String"
      value="com.example.myplugin.MyEventHandler"/>
  <reference name="eventManager"
      interface="org.adempiere.base.event.IEventManager"
      bind="bindEventManager"
      unbind="unbindEventManager"
      cardinality="1..1"
      policy="dynamic"/>
</scr:component>

Key elements of a component definition:

  • implementation class — The Java class that implements the component.
  • service / provide interface — The service interface(s) this component implements and registers in the OSGi service registry.
  • property — Configuration properties for the component. Event handlers use properties to declare which event topics they subscribe to.
  • reference — A dependency on another OSGi service. The DS runtime will inject the referenced service automatically. cardinality="1..1" means exactly one instance is required. policy="dynamic" means the reference can be updated without restarting the component.
  • immediate=”true” — The component is activated as soon as its dependencies are satisfied, rather than waiting until the service is first requested (lazy activation).

Service References and Binding

When your component declares a reference, the DS runtime calls the specified bind method with the service instance when the service becomes available, and the unbind method when it goes away. Here is how the corresponding Java code looks:

public class MyEventHandler implements EventHandler {

    private IEventManager eventManager;

    // Called by DS runtime when IEventManager service is available
    protected void bindEventManager(IEventManager em) {
        this.eventManager = em;
    }

    // Called by DS runtime when IEventManager service is removed
    protected void unbindEventManager(IEventManager em) {
        this.eventManager = null;
    }

    @Override
    public void handleEvent(Event event) {
        // Handle the event
    }
}

The bind/unbind methods must be protected or public (not private) for the DS runtime to invoke them.

Annotations vs XML

Modern OSGi (DS 1.3+) supports annotations as an alternative to XML component definitions:

import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;

@Component(
    immediate = true,
    property = {
        "event.topics=org/adempiere/base/event/PO_BEFORE_NEW"
    }
)
public class MyEventHandler implements EventHandler {

    @Reference
    private IEventManager eventManager;

    @Override
    public void handleEvent(Event event) {
        // Handle the event
    }
}

While annotations are more concise, many iDempiere plugins and examples still use XML component definitions. Both approaches are valid. The annotations are processed at build time and generate the XML automatically.

Extension Points vs Declarative Services

iDempiere’s history spans two different OSGi extension mechanisms, and you will encounter both:

Eclipse Extension Points

Extension points come from the Eclipse runtime (a layer on top of OSGi). They use a plugin.xml file where you declare extensions to named extension points defined by other bundles. iDempiere originally used extension points for its factory registrations. Some older documentation and plugins still reference this mechanism.

<?xml version="1.0" encoding="UTF-8"?>
<plugin>
  <extension point="org.adempiere.base.IModelFactory">
    <factory class="com.example.myplugin.MyModelFactory"/>
  </extension>
</plugin>

Declarative Services (Preferred)

The iDempiere project has been migrating toward pure OSGi Declarative Services, which are more standard, more flexible, and do not require Eclipse-specific runtime dependencies. The preferred approach for new plugins is to register factories and handlers as DS components.

<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0"
    name="com.example.myplugin.MyModelFactory">
  <implementation class="com.example.myplugin.MyModelFactory"/>
  <service>
    <provide interface="org.adempiere.base.IModelFactory"/>
  </service>
  <property name="service.ranking" type="Integer" value="100"/>
</scr:component>

The service.ranking property determines priority when multiple implementations of the same service exist. Higher values take precedence.

Bundle Dependencies and Versioning

Managing dependencies correctly is essential for plugin stability. Here are the key principles:

Dependency Resolution

When the OSGi framework resolves a bundle, it matches each Import-Package entry against the Export-Package entries of all installed bundles. If a matching package with a compatible version is found, the importing bundle’s classloader is wired to the exporting bundle’s classloader for that package.

Version Ranges

Using precise version ranges protects your plugin from incompatible changes:

# Exact version (fragile — avoids any updates):
Import-Package: org.compiere.model;version="11.0.0"

# Minimum version (accepts any version 11.0.0 or higher):
Import-Package: org.compiere.model;version="11.0.0"

# Version range (recommended — accepts 11.x but not 12.x):
Import-Package: org.compiere.model;version="[11.0.0,12.0.0)"

Optional Dependencies

Some imports can be marked as optional if your plugin can function without them:

Import-Package: org.compiere.model;version="0.0.0",
 com.example.optional.feature;version="0.0.0";resolution:=optional

If the optional package is not available, the bundle will still resolve. However, your code must handle the case where classes from that package are not loadable (typically using try-catch around ClassNotFoundException).

Hot Deployment

One of the most powerful features of OSGi is the ability to deploy and update bundles at runtime without restarting the application. In iDempiere, this works as follows:

  1. Drop the JAR — Place your plugin’s JAR file into the plugins/ directory of your iDempiere installation.
  2. Automatic detection — iDempiere’s file installer monitors the plugins/ directory and automatically installs new bundles.
  3. Resolution and activation — The framework resolves the new bundle’s dependencies and, if successful, starts it. Your services become available immediately.

To update a plugin, simply replace the JAR file in the plugins/ directory with the new version. The framework will stop the old version, install the new one, and start it.

You can also manage bundles through the OSGi console. Connect to the console (typically via telnet on port 11612) and use these commands:

# List all bundles and their states
ss

# Install a bundle
install file:///path/to/plugin.jar

# Start a bundle (use the bundle ID from the 'ss' output)
start 285

# Stop a bundle
stop 285

# Update a bundle (reloads from the same location)
update 285

# Uninstall a bundle
uninstall 285

# Refresh bundles (recalculates wiring after changes)
refresh

# Show details of a specific bundle
bundle 285

Debugging OSGi Issues

OSGi problems can be mystifying if you do not know where to look. Here are the most common issues and how to solve them.

Bundle Stuck in INSTALLED State

This is the most frequent problem. It means the bundle cannot resolve — one or more of its dependencies are not satisfied. To diagnose:

# In the OSGi console, use the 'diag' command:
diag 285

# Output will show exactly which packages cannot be resolved:
# com.example.myplugin [285]
#   Unresolved requirement: Import-Package: org.compiere.model; version="12.0.0"
#     -> No matching export found

The fix is usually one of:

  • Correct the version range in your Import-Package to match what is actually exported
  • Install the missing dependency bundle
  • Remove an import for a package you do not actually use

ClassNotFoundException at Runtime

If your bundle is ACTIVE but you get a ClassNotFoundException, the problem is usually that you are trying to use a class from a package you did not import. Add the missing package to your Import-Package header.

Service Not Found

If your DS component is not activating or a service reference is not being injected:

# List all registered services
services

# List DS components and their states
scr:list

# Show details of a specific component
scr:info com.example.myplugin.MyEventHandler

Common causes:

  • The Service-Component header in MANIFEST.MF does not point to the correct XML file
  • The component XML has a typo in the interface name or implementation class
  • A required service reference has cardinality 1..1 but the service it depends on is not registered
  • The implementation class does not have a no-argument constructor

Stale Bundle Cache

Sometimes the framework caches bundle data that becomes stale. If you are experiencing unexplainable behavior after updating a plugin, try clearing the cache:

# Stop iDempiere, clear the OSGi configuration area, then restart:
# The configuration area is typically in the 'configuration/' directory
# Delete the 'org.eclipse.osgi/' subdirectory to force a clean resolve

Key Takeaways

  • OSGi is the modularity framework that enables iDempiere’s plugin architecture. Eclipse Equinox is the specific OSGi implementation used.
  • Bundles are the fundamental unit of deployment, with a well-defined lifecycle: INSTALLED, RESOLVED, STARTING, ACTIVE, STOPPING, UNINSTALLED.
  • The MANIFEST.MF file declares a bundle’s identity, dependencies (Import-Package), and exports (Export-Package). Getting this right is essential.
  • Declarative Services (DS) provides a declarative, XML-based (or annotation-based) way to register and consume services — this is the preferred extension mechanism in modern iDempiere.
  • Hot deployment allows plugins to be installed, updated, and removed without restarting iDempiere.
  • When bundles fail to resolve, the diag command in the OSGi console is your best diagnostic tool.

What’s Next

Now that you understand the OSGi framework, the next lesson explores iDempiere’s extension point and factory pattern in detail. You will learn about IModelFactory, IColumnCallout, IProcessFactory, and the Event Handler pattern — the specific mechanisms by which plugins integrate with iDempiere’s core functionality.

You Missed