Creating Your First Plugin

Level: Intermediate Module: Plugin Development 27 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.

繁體中文翻譯

概覽

  • 您將學到:
    • 如何在 Eclipse 中使用正確的目錄結構設定新的 iDempiere 外掛專案
    • 如何設定 MANIFEST.MF、撰寫 Activator 類別,以及定義 Declarative Services 元件
    • 如何建置、部署和熱載入您的外掛,包括外掛片段(plugin fragment)與獨立外掛(standalone plugin)的差異
  • 先決條件:第 1–20 課(特別是第 18–19 課:OSGi 框架、擴充點與工廠)
  • 預估閱讀時間:25 分鐘

簡介

您所學到的關於 iDempiere 架構的一切 — OSGi bundle、Declarative Services、工廠介面和事件處理器 — 都在您建立第一個外掛時匯聚在一起。外掛是您在不修改核心原始碼的情況下為 iDempiere 添加自訂功能的方式。它是您發佈客製化、與他人分享,並在 iDempiere 升級過程中保持可維護性的方法。

本課是一個實作演練。我們將從一個空的 Eclipse 工作區開始,建立一個完整的外掛,直到在 iDempiere 中執行的擴充功能。在這個過程中,您將學習每個 iDempiere 外掛開發者都需要知道的慣例、設定模式和部署程序。

外掛專案結構

每個 iDempiere 外掛都遵循標準的目錄配置。在我們開始建置之前,讓我們看看我們的目標完整結構:

com.example.helloworld/
├── META-INF/
│   └── MANIFEST.MF              # OSGi bundle 中繼資料
├── OSGI-INF/
│   └── EventHandler.xml          # Declarative Services 元件定義
├── src/
│   └── com/
│       └── example/
│           └── helloworld/
│               ├── Activator.java        # Bundle 生命週期管理
│               └── HelloEventHandler.java # 事件處理器實作
├── build.properties              # Eclipse PDE 建置設定
└── pom.xml                       # Maven 建置檔案(選用,用於 CI/CD)

這個結構是一個 OSGi bundle 專案。三個關鍵目錄是:

  • META-INF/ — 包含定義 bundle 身份、版本和相依性的 MANIFEST.MF 檔案。
  • OSGI-INF/ — 包含向 OSGi 框架註冊服務的 Declarative Services 元件 XML 檔案。
  • src/ — 包含以標準 Java 套件結構組織的 Java 原始碼。

在 Eclipse 中建立外掛專案

iDempiere 開發使用 Eclipse IDE 搭配 Plugin Development Environment (PDE) 工具。以下是建立新外掛專案的方法:

步驟 1:使用 iDempiere 工作區開啟 Eclipse

啟動 Eclipse 並開啟包含 iDempiere 原始碼的工作區。您應該在 Package Explorer 中看到核心 iDempiere 專案(org.adempiere.base、org.adempiere.ui.zk 等)。如果您尚未設定 iDempiere 開發環境,請按照 iDempiere wiki 上的設定說明操作。

步驟 2:建立新的 Plug-in 專案

  1. 前往 File > New > Plug-in Project
  2. 輸入專案名稱:com.example.helloworld
  3. 確保目標平台設定為 iDempiere 目標平台(而非預設的 Eclipse 平台)
  4. 點擊 Next
  5. 填寫外掛屬性:
    • ID: com.example.helloworld
    • Version: 1.0.0.qualifier
    • Name: Hello World Plugin
    • Vendor: Example Corp
    • Execution Environment: JavaSE-17
    • 勾選 Generate an activator
  6. 點擊 Finish

Eclipse 會建立包含基本結構的專案,包括 MANIFEST.MF 和一個 Activator 類別。

步驟 3:建立 OSGI-INF 目錄

右鍵點擊專案,選擇 New > Folder,建立名為 OSGI-INF 的資料夾。這是 Declarative Services 元件 XML 檔案存放的位置。

MANIFEST.MF 設定

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
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

讓我們檢視這個設定中的重要決策:

相依性:匯入什麼

Import-Package 標頭列出您的外掛使用的每個外部 Java 套件。對於典型的 iDempiere 外掛,您將需要:

  • org.compiere.model — 存取 PO(Persistent Object)、MTable 和模型類別
  • org.compiere.process — 存取 SvrProcess、ProcessInfo 以建立自訂流程
  • org.compiere.util — 存取 Env、CLogger、DB 和其他工具程式
  • org.adempiere.base.event — 存取事件處理器基礎類別和 IEventTopics
  • org.osgi.framework — Activator 的 OSGi 框架 API
  • org.osgi.service.event — 事件處理的 OSGi Event Admin

只匯入您實際使用的套件。不必要的匯入可能在這些套件不可用時導致解析失敗。

匯出套件

Export-Package 標頭宣告您的哪些套件對其他 bundle 可見。如果您的外掛是自包含的,沒有其他外掛依賴它,您可以完全省略此標頭。如果您提供其他外掛使用的 API,只匯出 API 套件 — 將實作套件保持為私有。

Service-Component 萬用字元

Service-Component: OSGI-INF/*.xml 指令告訴 OSGi 框架將 OSGI-INF 目錄中的所有 XML 檔案載入為 Declarative Services 元件定義。這很方便,因為您可以透過簡單地添加 XML 檔案來新增元件,而無需修改 MANIFEST.MF。

Activator 類別

Activator 是實作 org.osgi.framework.BundleActivator 的類別。它在 bundle 啟動和停止時接收回呼。對於許多外掛,Activator 執行初始化邏輯 — 註冊服務、設定資源或觸發 2Pack 遷移。

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!");
    }
}

使用 AdempiereActivator

如果您的外掛包含 2Pack 遷移檔案,請擴展 org.adempiere.plugin.utils.AdempiereActivator,而非直接實作 BundleActivator。這個基礎類別會在啟動期間自動套用 bundle 的 migration/ 目錄中的 2Pack 檔案:

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);  // 套用 2Pack 遷移
        // 額外的初始化在此
    }

    @Override
    public void stop(BundleContext context) throws Exception {
        // 清理在此
        super.stop(context);
    }
}

Declarative Services 元件:component.xml

我們不在 Activator 中手動註冊服務,而是使用 Declarative Services 來宣告我們的外掛提供什麼。讓我們建立一個事件處理器,每當建立新的業務夥伴時記錄訊息。

事件處理器類別

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");
        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);
        }
    }
}

元件 XML

建立檔案 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>

這個 XML 告訴 DS 執行環境:「建立 HelloEventHandler 的實例,將其註冊為 EventHandler 服務,並訂閱所有 PO 事件主題。」immediate="true" 屬性確保元件在 bundle 啟動後立即啟動。

build.properties 檔案

Eclipse PDE 使用 build.properties 來了解建置的外掛中要包含哪些檔案。在專案根目錄建立或驗證此檔案:

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

這告訴建置系統原始檔案在 src/ 中,編譯後的類別放到 bin/,最終的 JAR 應包含 META-INF/OSGI-INF/ 和編譯後的類別。

建置外掛

有幾種方式可以將您的外掛建置為可部署的 JAR:

從 Eclipse 匯出

  1. 在 Package Explorer 中右鍵點擊專案
  2. 選擇 Export > Plug-in Development > Deployable plug-ins and fragments
  3. 在清單中選擇您的外掛
  4. 選擇目標目錄(例如 /tmp/plugins/
  5. 點擊 Finish

Eclipse 將編譯您的程式碼並建立名為 com.example.helloworld_1.0.0.qualifier.jar 的 JAR 檔案(qualifier 會被時間戳記取代)。

使用 Maven/Tycho 建置

對於自動化建置,您可以使用帶有 Tycho 外掛的 Maven,它能理解 OSGi bundle 的中繼資料。最小的 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>

部署到 iDempiere

部署很簡單:將建置好的 JAR 檔案放到 iDempiere 安裝目錄的 plugins/ 目錄中。

# 將外掛複製到 iDempiere plugins 目錄
cp com.example.helloworld_1.0.0.jar \
    $IDEMPIERE_HOME/plugins/

熱部署

如果 iDempiere 正在執行,它會自動偵測到新的 JAR 檔案,安裝 bundle,解析其相依性,並啟動它。您應該在幾秒鐘內在 iDempiere 伺服器日誌中看到 Activator 的日誌訊息。

要驗證 bundle 是否在執行,連接到 OSGi 主控台並檢查:

# 連接到 OSGi 主控台
telnet localhost 11612

# 列出符合名稱的 bundle
ss helloworld

# 預期輸出:
# 285 ACTIVE com.example.helloworld_1.0.0

更新已部署的外掛

要更新已部署的外掛:

  1. 建置新版本的外掛 JAR
  2. 用新的 JAR 取代 plugins/ 目錄中的舊 JAR
  3. 框架會偵測到變更,停止舊版本,並啟動新版本

或者,從 OSGi 主控台:

# 更新特定 bundle
update 285

# 或重新整理所有 bundle 以重新計算接線
refresh

從 Eclipse 執行(開發模式)

在開發期間,您通常直接從 Eclipse 執行 iDempiere,而非部署 JAR。要在 Eclipse 啟動設定中包含您的外掛:

  1. 開啟 Run > Run Configurations
  2. 選擇您的 iDempiere 啟動設定(通常是 “iDempiere Server” 或類似名稱)
  3. 前往 Plug-ins 頁籤
  4. 在工作區外掛清單中找到 com.example.helloworld 並勾選
  5. 點擊 Apply 然後 Run

透過此設定,您在 Eclipse 中所做的任何程式碼變更都會在重新啟動伺服器時立即反映(或在某些情況下,透過除錯時的熱碼替換)。

外掛片段 vs 獨立外掛

您可以為 iDempiere 建立兩種類型的 OSGi bundle:獨立外掛外掛片段。了解兩者的差異很重要。

獨立外掛(Bundle)

這就是我們一直在建置的。獨立外掛是一個自包含的 bundle,擁有自己的類別載入器、啟動器和生命週期。它透過 Import-Package 或 Require-Bundle 宣告對其他 bundle 的相依性,OSGi 框架管理接線。

  • 優點:清晰的分離、獨立的生命週期、可獨立安裝/解除安裝/更新
  • 使用時機:添加新功能、新事件處理器、新流程、新表單 — 大多數客製化

外掛片段

片段是一種特殊類型的 bundle,附加到宿主 bundle並共享宿主的類別載入器。片段沒有自己的 Activator,無法獨立註冊服務。相反地,它的類別和資源被合併到宿主 bundle 的類別路徑中。

# 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"

關鍵差異是 Fragment-Host 標頭,它指定此片段附加到哪個 bundle。

  • 優點:可以存取宿主 bundle 的內部(未匯出的)套件,可以提供宿主 bundle 透過自身類別載入器能找到的資源(設定檔、翻譯)
  • 使用時機:當您需要覆蓋或補充核心 bundle 中的資源(例如添加翻譯、修改設定檔),或需要存取未匯出的內部類別
  • 注意:片段與宿主 bundle 更緊密耦合。宿主 bundle 內部的變更可能會破壞您的片段。盡可能使用獨立外掛。

實作範例:完整的 Hello World 外掛

讓我們將所有內容整合在一起。以下是一個最小但功能完整的外掛的完整原始碼,每當在 iDempiere 中建立新的業務夥伴時,它會記錄一條問候訊息。

專案結構

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/,\
               .

測試外掛

部署後(或從 Eclipse 執行),透過以下步驟測試外掛:

  1. 在瀏覽器中開啟 iDempiere
  2. 導覽至業務夥伴視窗
  3. 建立一個新的業務夥伴記錄
  4. 儲存記錄
  5. 在伺服器日誌中檢查 “Hello! New Business Partner created:” 訊息

如果您看到日誌訊息,您的外掛正常運作。如果沒有,使用 ss helloworld 在 OSGi 主控台中檢查 bundle 是否為 ACTIVE,並使用 scr:info com.example.helloworld.HelloEventHandler 驗證 DS 元件是否已啟動。

重點摘要

  • iDempiere 外掛是一個 OSGi bundle,包含三個關鍵元件:MANIFEST.MF(身份和相依性)、OSGI-INF/*.xml(服務宣告)和 Java 原始碼。
  • 使用 Eclipse PDE 建立外掛專案,並設定 iDempiere 目標平台以進行相依性解析。
  • Activator 類別處理 bundle 生命週期(啟動/停止)。如果您需要自動 2Pack 遷移支援,請擴展 AdempiereActivator
  • Declarative Services component.xml 檔案無需程式碼即可註冊您的事件處理器、工廠和其他服務。
  • 將建置好的 JAR 複製到 plugins/ 目錄即可部署。iDempiere 支援無需重新啟動的熱部署。
  • 大多數客製化使用獨立外掛。只有在需要存取宿主 bundle 的內部類別或資源時才使用外掛片段。

下一步

您的 Hello World 外掛透過記錄訊息來回應事件。在下一課中,我們將深入探討模型事件系統,學習如何使用事件處理器來實作真實的業務邏輯 — 驗證資料、自動填充欄位、防止無效儲存,以及回應文件狀態變更。

日本語翻訳

概要

  • 学習内容:
    • Eclipse で正しいディレクトリ構造を使用して新しい iDempiere プラグインプロジェクトをセットアップする方法
    • MANIFEST.MF の設定、Activator クラスの作成、Declarative Services コンポーネントの定義方法
    • プラグインのビルド、デプロイ、ホットリロードの方法(プラグインフラグメントとスタンドアロンプラグインの違いを含む)
  • 前提条件:第1〜20課(特に第18〜19課:OSGi フレームワーク、拡張ポイントとファクトリ)
  • 推定読了時間:25分

はじめに

iDempiere のアーキテクチャについて学んだすべて — OSGi バンドル、Declarative Services、ファクトリインターフェース、イベントハンドラ — は、初めてのプラグインを構築する際にすべてが結集します。プラグインは、コアソースコードを変更せずに iDempiere にカスタム機能を追加する方法です。カスタマイズを配布し、他の人と共有し、iDempiere のアップグレードを通じて保守性を維持する手段です。

この課は実践的なウォークスルーです。空の Eclipse ワークスペースから、iDempiere 内で実行される拡張機能まで、完全なプラグインを作成します。その過程で、すべての iDempiere プラグイン開発者が知っておくべき規約、設定パターン、デプロイ手順を学びます。

プラグインプロジェクト構造

すべての iDempiere プラグインは標準的なディレクトリレイアウトに従います。構築を開始する前に、目標とする完全な構造を見てみましょう:

com.example.helloworld/
├── META-INF/
│   └── MANIFEST.MF              # OSGi バンドルメタデータ
├── OSGI-INF/
│   └── EventHandler.xml          # Declarative Services コンポーネント定義
├── src/
│   └── com/
│       └── example/
│           └── helloworld/
│               ├── Activator.java        # バンドルライフサイクル管理
│               └── HelloEventHandler.java # イベントハンドラ実装
├── build.properties              # Eclipse PDE ビルド設定
└── pom.xml                       # Maven ビルドファイル(オプション、CI/CD用)

この構造は OSGi バンドルプロジェクトです。3つの重要なディレクトリは:

  • META-INF/ — バンドルのアイデンティティ、バージョン、依存関係を定義する MANIFEST.MF ファイルを含みます。
  • OSGI-INF/ — OSGi フレームワークにサービスを登録する Declarative Services コンポーネント XML ファイルを含みます。
  • src/ — 標準的な Java パッケージ構造で整理された Java ソースコードを含みます。

Eclipse でプラグインプロジェクトを作成する

iDempiere の開発では、Eclipse IDE と Plugin Development Environment (PDE) ツールを使用します。新しいプラグインプロジェクトの作成方法は以下の通りです:

ステップ 1:iDempiere ワークスペースで Eclipse を開く

Eclipse を起動し、iDempiere のソースコードを含むワークスペースを開きます。Package Explorer にコアの iDempiere プロジェクト(org.adempiere.base、org.adempiere.ui.zk など)が表示されるはずです。まだ iDempiere の開発環境をセットアップしていない場合は、iDempiere wiki のセットアップ手順に従ってください。

ステップ 2:新しい Plug-in プロジェクトを作成する

  1. File > New > Plug-in Project に移動します
  2. プロジェクト名を入力します:com.example.helloworld
  3. ターゲットプラットフォームが iDempiere ターゲットプラットフォームに設定されていることを確認します(デフォルトの Eclipse プラットフォームではなく)
  4. Next をクリックします
  5. プラグインのプロパティを入力します:
    • ID: com.example.helloworld
    • Version: 1.0.0.qualifier
    • Name: Hello World Plugin
    • Vendor: Example Corp
    • Execution Environment: JavaSE-17
    • Generate an activator にチェックを入れます
  6. Finish をクリックします

Eclipse が MANIFEST.MF と Activator クラスを含む基本構造のプロジェクトを作成します。

ステップ 3:OSGI-INF ディレクトリを作成する

プロジェクトを右クリックし、New > Folder を選択して、OSGI-INF という名前のフォルダを作成します。ここに Declarative Services コンポーネント XML ファイルが配置されます。

MANIFEST.MF の設定

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
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

この設定における重要な決定事項を確認しましょう:

依存関係:何をインポートするか

Import-Package ヘッダーは、プラグインが使用するすべての外部 Java パッケージをリストします。典型的な iDempiere プラグインでは、以下が必要です:

  • org.compiere.model — PO(Persistent Object)、MTable、モデルクラスへのアクセス
  • org.compiere.process — カスタムプロセス用の SvrProcess、ProcessInfo へのアクセス
  • org.compiere.util — Env、CLogger、DB、その他のユーティリティへのアクセス
  • org.adempiere.base.event — イベントハンドラ基底クラスと IEventTopics へのアクセス
  • org.osgi.framework — Activator 用の OSGi フレームワーク API
  • org.osgi.service.event — イベント処理用の OSGi Event Admin

実際に使用するパッケージのみをインポートしてください。不要なインポートは、それらのパッケージが利用できない場合に解決エラーを引き起こす可能性があります。

エクスポートパッケージ

Export-Package ヘッダーは、他のバンドルから見えるパッケージを宣言します。プラグインが自己完結型で、他のプラグインがそれに依存しない場合、このヘッダーを完全に省略できます。他のプラグインが使用する API を提供する場合は、API パッケージのみをエクスポートし、実装パッケージはプライベートに保ちます。

Service-Component ワイルドカード

Service-Component: OSGI-INF/*.xml ディレクティブは、OSGi フレームワークに OSGI-INF ディレクトリ内のすべての XML ファイルを Declarative Services コンポーネント定義として読み込むよう指示します。これは便利で、MANIFEST.MF を変更せずに XML ファイルを追加するだけで新しいコンポーネントを追加できます。

Activator クラス

Activator は org.osgi.framework.BundleActivator を実装するクラスです。バンドルの開始時と停止時にコールバックを受け取ります。多くのプラグインでは、Activator が初期化ロジック(サービスの登録、リソースのセットアップ、2Pack マイグレーションのトリガー)を実行します。

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!");
    }
}

AdempiereActivator の使用

プラグインに 2Pack マイグレーションファイルが含まれる場合は、BundleActivator を直接実装する代わりに org.adempiere.plugin.utils.AdempiereActivator を拡張します。この基底クラスは、起動時にバンドルの migration/ ディレクトリから 2Pack ファイルを自動的に適用します:

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);  // 2Pack マイグレーションを適用
        // 追加の初期化をここに記述
    }

    @Override
    public void stop(BundleContext context) throws Exception {
        // クリーンアップをここに記述
        super.stop(context);
    }
}

Declarative Services コンポーネント:component.xml

Activator でサービスを手動で登録する代わりに、Declarative Services を使用してプラグインが提供するものを宣言します。新しいビジネスパートナーが作成されるたびにメッセージをログに記録するイベントハンドラを作成しましょう。

イベントハンドラクラス

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");
        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);
        }
    }
}

コンポーネント XML

ファイル 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>

この XML は DS ランタイムに次のことを伝えます:「HelloEventHandler のインスタンスを作成し、EventHandler サービスとして登録し、すべての PO イベントトピックを購読する。」immediate="true" 属性は、バンドルの開始と同時にコンポーネントがアクティベートされることを保証します。

build.properties ファイル

Eclipse PDE は build.properties を使用して、ビルドされたプラグインに含めるファイルを把握します。プロジェクトルートでこのファイルを作成または確認します:

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

これはビルドシステムに、ソースファイルは src/ にあり、コンパイルされたクラスは bin/ に出力され、最終的な JAR には META-INF/OSGI-INF/、およびコンパイルされたクラスを含めることを伝えます。

プラグインのビルド

プラグインをデプロイ可能な JAR にビルドする方法はいくつかあります:

Eclipse からエクスポート

  1. Package Explorer でプロジェクトを右クリックします
  2. Export > Plug-in Development > Deployable plug-ins and fragments を選択します
  3. リストからプラグインを選択します
  4. 出力先ディレクトリを選択します(例:/tmp/plugins/
  5. Finish をクリックします

Eclipse がコードをコンパイルし、com.example.helloworld_1.0.0.qualifier.jar という名前の JAR ファイルを作成します(qualifier はタイムスタンプに置き換えられます)。

Maven/Tycho でビルド

自動ビルドには、OSGi バンドルのメタデータを理解する Tycho プラグインを使用した Maven を利用できます。最小限の 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>

iDempiere へのデプロイ

デプロイは簡単です:ビルドした JAR ファイルを iDempiere インストールディレクトリの plugins/ ディレクトリに配置します。

# プラグインを iDempiere の plugins ディレクトリにコピー
cp com.example.helloworld_1.0.0.jar \
    $IDEMPIERE_HOME/plugins/

ホットデプロイ

iDempiere が既に実行中の場合、新しい JAR ファイルを自動的に検出し、バンドルをインストールし、依存関係を解決して起動します。数秒以内に iDempiere サーバーログに Activator のログメッセージが表示されるはずです。

バンドルが実行中であることを確認するには、OSGi コンソールに接続して確認します:

# OSGi コンソールに接続
telnet localhost 11612

# 名前に一致するバンドルをリスト表示
ss helloworld

# 期待される出力:
# 285 ACTIVE com.example.helloworld_1.0.0

デプロイ済みプラグインの更新

既にデプロイされたプラグインを更新するには:

  1. プラグイン JAR の新しいバージョンをビルドします
  2. plugins/ ディレクトリの古い JAR を新しいものに置き換えます
  3. フレームワークが変更を検出し、古いバージョンを停止して新しいバージョンを起動します

あるいは、OSGi コンソールから:

# 特定のバンドルを更新
update 285

# またはすべてのバンドルをリフレッシュしてワイヤリングを再計算
refresh

Eclipse からの実行(開発モード)

開発中は、JAR をデプロイするのではなく、通常 Eclipse から直接 iDempiere を実行します。Eclipse の起動構成にプラグインを含めるには:

  1. Run > Run Configurations を開きます
  2. iDempiere の起動構成を選択します(通常「iDempiere Server」など)
  3. Plug-ins タブに移動します
  4. ワークスペースプラグインリストで com.example.helloworld を見つけてチェックします
  5. Apply をクリックしてから Run をクリックします

このセットアップにより、Eclipse で行ったコード変更がサーバー再起動時に即座に反映されます(場合によっては、デバッグ中のホットコード置換を通じて反映されます)。

プラグインフラグメント vs スタンドアロンプラグイン

iDempiere 用に作成できる OSGi バンドルには2種類あります:スタンドアロンプラグインプラグインフラグメントです。その違いを理解することが重要です。

スタンドアロンプラグイン(バンドル)

これが今まで構築してきたものです。スタンドアロンプラグインは、独自のクラスローダー、アクティベータ、ライフサイクルを持つ自己完結型のバンドルです。Import-Package または Require-Bundle を通じて他のバンドルへの依存関係を宣言し、OSGi フレームワークがワイヤリングを管理します。

  • 利点:クリーンな分離、独立したライフサイクル、独立してインストール/アンインストール/更新可能
  • 使用する場面:新しい機能、新しいイベントハンドラ、新しいプロセス、新しいフォームの追加 — ほとんどのカスタマイズ

プラグインフラグメント

フラグメントは、ホストバンドルにアタッチされ、ホストのクラスローダーを共有する特殊なタイプのバンドルです。フラグメントは独自の Activator を持たず、独立してサービスを登録できません。代わりに、そのクラスとリソースがホストバンドルのクラスパスにマージされます。

# 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"

主な違いは Fragment-Host ヘッダーで、このフラグメントがアタッチするバンドルを指定します。

  • 利点:ホストバンドルの内部(エクスポートされていない)パッケージにアクセスでき、ホストバンドルが自身のクラスローダーで見つけられるリソース(設定ファイル、翻訳)を提供できます
  • 使用する場面:コアバンドルのリソースをオーバーライドまたは補完する必要がある場合(翻訳の追加、設定ファイルの変更など)、またはエクスポートされていない内部クラスへのアクセスが必要な場合
  • 注意:フラグメントはホストバンドルとより密に結合しています。ホストバンドルの内部変更によりフラグメントが壊れる可能性があります。可能な限りスタンドアロンプラグインを使用してください。

実践例:完全な Hello World プラグイン

すべてをまとめましょう。以下は、iDempiere で新しいビジネスパートナーが作成されるたびに挨拶メッセージをログに記録する、最小限ながら機能的なプラグインの完全なソースコードです。

プロジェクト構造

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/,\
               .

プラグインのテスト

デプロイ後(または Eclipse から実行後)、以下の手順でプラグインをテストします:

  1. ブラウザで iDempiere を開きます
  2. ビジネスパートナーウィンドウに移動します
  3. 新しいビジネスパートナーレコードを作成します
  4. レコードを保存します
  5. サーバーログで「Hello! New Business Partner created:」メッセージを確認します

ログメッセージが表示されれば、プラグインは正常に動作しています。表示されない場合は、OSGi コンソールで ss helloworld を使用してバンドルが ACTIVE であることを確認し、scr:info com.example.helloworld.HelloEventHandler を使用して DS コンポーネントがアクティベートされていることを確認してください。

重要なポイント

  • iDempiere プラグインは、3つの主要コンポーネントを持つ OSGi バンドルです:MANIFEST.MF(アイデンティティと依存関係)、OSGI-INF/*.xml(サービス宣言)、Java ソースコード。
  • Eclipse PDE を使用してプラグインプロジェクトを作成し、依存関係の解決のために iDempiere ターゲットプラットフォームを設定します。
  • Activator クラスはバンドルのライフサイクル(開始/停止)を処理します。自動 2Pack マイグレーションサポートが必要な場合は AdempiereActivator を拡張します。
  • Declarative Services の component.xml ファイルは、手続き的なコードなしでイベントハンドラ、ファクトリ、その他のサービスを登録します。
  • ビルドした JAR を plugins/ ディレクトリにコピーしてデプロイします。iDempiere は再起動なしのホットデプロイをサポートしています。
  • ほとんどのカスタマイズにはスタンドアロンプラグインを使用します。ホストバンドルの内部クラスやリソースへのアクセスが必要な場合のみプラグインフラグメントを使用してください。

次のステップ

Hello World プラグインはメッセージをログに記録することでイベントに反応します。次の課では、モデルイベントシステムを深く掘り下げ、イベントハンドラを使用して実際のビジネスロジックを実装する方法を学びます — データの検証、フィールドの自動入力、無効な保存の防止、ドキュメント状態変更への応答などです。

You Missed