Extension Points and Factories

Level: Intermediate Module: Architecture 16 min read Lesson 19 of 47

Overview

  • What you’ll learn:
    • The Factory pattern in iDempiere and how factories are resolved at runtime with priority ordering
    • How to implement IModelFactory, IColumnCallout, IProcessFactory, IFormFactory, and IModelValidatorFactory
    • The Event Handler pattern using IEventManager and IEventTopics, and how to register all extensions via component.xml
  • Prerequisites: Lessons 1–18 (especially Lesson 18: OSGi Framework in iDempiere)
  • Estimated reading time: 25 minutes

Introduction

The real power of iDempiere lies not in modifying its source code, but in extending it through well-defined interfaces. iDempiere provides a set of factory interfaces and an event handling system that allow plugins to inject custom behavior into virtually every part of the application — from how model objects are instantiated, to what happens when a user changes a field value, to how documents are processed.

These extension mechanisms follow the Factory design pattern: the iDempiere core defines an interface (the factory contract), and plugins provide implementations that the core discovers and invokes at runtime through the OSGi service registry. This lesson examines each factory interface in detail, walks through the event handler system, and shows you how to register everything using Declarative Services component.xml files.

The Factory Pattern in iDempiere

In classical object-oriented design, the Factory pattern abstracts object creation — instead of calling new SomeClass() directly, you ask a factory to create the object. This indirection allows the factory to return different implementations depending on context.

iDempiere takes this pattern a step further by using OSGi service-based factories. When the core needs to instantiate a model class, resolve a process, or create a form, it queries the OSGi service registry for all registered factory implementations, iterates through them in priority order, and uses the first one that returns a non-null result.

How Factory Resolution Works at Runtime

The resolution process follows this algorithm:

  1. The core code requests all registered implementations of a factory interface from the OSGi service registry.
  2. Implementations are sorted by the service.ranking property (higher values come first). If two services have the same ranking, the one with the lower service.id (registered earlier) takes precedence.
  3. The core iterates through the sorted list, calling the factory method on each implementation.
  4. The first factory that returns a non-null result wins — its result is used, and the remaining factories are skipped.
  5. If no factory returns a result, the core falls back to its default behavior.

This design means your plugin’s factory can override default behavior for specific cases while leaving everything else untouched. It also means multiple plugins can coexist, each handling different subsets of the requests.

IModelFactory: Custom Model Class Resolution

When iDempiere needs to create a Persistent Object (PO) for a database table — for example, when loading an order record — it uses IModelFactory to determine which Java class should represent that record.

The default model factory maps table names to classes following the convention: table C_Order maps to class org.compiere.model.MOrder (prefix “M” + table name without the entity type prefix). If you want to use a custom subclass for a specific table, you implement IModelFactory.

The IModelFactory Interface

public interface IModelFactory {

    /**
     * Get the model class for the given table name.
     * @param tableName the database table name (e.g., "C_Order")
     * @return the Class that should be used, or null to defer to the next factory
     */
    public Class<?> getClass(String tableName);

    /**
     * Create a new PO instance from a ResultSet.
     * @param tableName the database table name
     * @param rs the ResultSet positioned at the current row
     * @param trxName the transaction name
     * @return a PO instance, or null to defer to the next factory
     */
    public PO getPO(String tableName, ResultSet rs, String trxName);

    /**
     * Create a new PO instance for a specific record ID.
     * @param tableName the database table name
     * @param Record_ID the record's primary key value
     * @param trxName the transaction name
     * @return a PO instance, or null to defer to the next factory
     */
    public PO getPO(String tableName, int Record_ID, String trxName);
}

Implementing a Custom Model Factory

Suppose you have a custom subclass of MBPartner that adds domain-specific logic. Here is how you register it:

package com.example.myplugin.factory;

import java.sql.ResultSet;
import java.util.Properties;
import org.adempiere.base.IModelFactory;
import org.compiere.model.PO;
import org.compiere.util.Env;

public class MyModelFactory implements IModelFactory {

    @Override
    public Class<?> getClass(String tableName) {
        if ("C_BPartner".equals(tableName)) {
            return com.example.myplugin.model.MyBPartner.class;
        }
        return null; // Defer to the next factory for other tables
    }

    @Override
    public PO getPO(String tableName, ResultSet rs, String trxName) {
        if ("C_BPartner".equals(tableName)) {
            return new com.example.myplugin.model.MyBPartner(
                Env.getCtx(), rs, trxName);
        }
        return null;
    }

    @Override
    public PO getPO(String tableName, int Record_ID, String trxName) {
        if ("C_BPartner".equals(tableName)) {
            return new com.example.myplugin.model.MyBPartner(
                Env.getCtx(), Record_ID, trxName);
        }
        return null;
    }
}

The custom model class itself extends the standard model:

package com.example.myplugin.model;

import java.sql.ResultSet;
import java.util.Properties;
import org.compiere.model.MBPartner;

public class MyBPartner extends MBPartner {

    public MyBPartner(Properties ctx, int C_BPartner_ID, String trxName) {
        super(ctx, C_BPartner_ID, trxName);
    }

    public MyBPartner(Properties ctx, ResultSet rs, String trxName) {
        super(ctx, rs, trxName);
    }

    @Override
    protected boolean beforeSave(boolean newRecord) {
        // Custom validation: ensure business partners have a tax ID
        if (isCustomer() && (getTaxID() == null || getTaxID().isEmpty())) {
            log.saveError("Error", "Customer must have a Tax ID");
            return false;
        }
        return super.beforeSave(newRecord);
    }
}

Registering via component.xml

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

The service.ranking of 100 ensures this factory is consulted before the default factory (which has a lower ranking). When the table name matches C_BPartner, your custom class is used; for all other tables, your factory returns null and the default factory handles them.

IColumnCallout: Registering Callouts via Extension

Callouts are field-level event handlers that execute when a user changes a value in a form field. Traditionally in ADempiere, callouts were registered in the Application Dictionary (AD_Column.Callout field). iDempiere adds the IColumnCallout interface, which allows plugins to register callouts without modifying the Application Dictionary.

The IColumnCallout Interface

public interface IColumnCallout {

    /**
     * Called when the associated field value changes.
     * @param ctx the context properties
     * @param WindowNo the window number
     * @param mTab the current tab (provides access to all field values)
     * @param mField the field that triggered the callout
     * @param value the old value
     * @param oldValue the new value
     * @return an error message string, or "" if no error
     */
    public String start(Properties ctx, int WindowNo,
        GridTab mTab, GridField mField, Object value, Object oldValue);
}

Implementing and Registering a Callout

package com.example.myplugin.callout;

import java.util.Properties;
import org.adempiere.base.IColumnCallout;
import org.compiere.model.GridField;
import org.compiere.model.GridTab;

public class OrderLineQtyCallout implements IColumnCallout {

    @Override
    public String start(Properties ctx, int WindowNo,
            GridTab mTab, GridField mField, Object value, Object oldValue) {

        // When quantity changes on an order line, apply discount logic
        if (value == null) return "";

        java.math.BigDecimal qty = (java.math.BigDecimal) value;
        if (qty.compareTo(new java.math.BigDecimal("100")) >= 0) {
            // Auto-apply 10% discount for bulk orders
            java.math.BigDecimal discount = new java.math.BigDecimal("10");
            mTab.setValue("Discount", discount);
        }

        return ""; // No error
    }
}

The component.xml for a callout uses special properties to specify which table and column trigger it:

<?xml version="1.0" encoding="UTF-8"?>
<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0"
    name="com.example.myplugin.callout.OrderLineQtyCallout">
  <implementation class="com.example.myplugin.callout.OrderLineQtyCallout"/>
  <service>
    <provide interface="org.adempiere.base.IColumnCallout"/>
  </service>
  <property name="tableName" type="String" value="C_OrderLine"/>
  <property name="columnName" type="String" value="QtyOrdered"/>
  <property name="service.ranking" type="Integer" value="50"/>
</scr:component>

The tableName and columnName properties tell the framework to invoke this callout whenever the QtyOrdered field on the C_OrderLine table changes. You can also use a IColumnCalloutFactory for more dynamic control over which callouts apply to which fields.

IProcessFactory: Custom Process Resolution

Processes in iDempiere are server-side operations that perform batch work — generating invoices, posting accounting entries, running imports, producing reports. The IProcessFactory interface allows plugins to provide custom process implementations.

The IProcessFactory Interface

public interface IProcessFactory {

    /**
     * Create a process instance for the given class name.
     * @param className the fully qualified class name of the process
     * @return a ProcessCall instance, or null to defer to the next factory
     */
    public ProcessCall newProcessInstance(String className);
}

Implementation Example

package com.example.myplugin.factory;

import org.adempiere.base.IProcessFactory;
import org.compiere.process.ProcessCall;

public class MyProcessFactory implements IProcessFactory {

    @Override
    public ProcessCall newProcessInstance(String className) {
        if ("com.example.myplugin.process.GenerateReport"
                .equals(className)) {
            return new com.example.myplugin.process.GenerateReport();
        }
        return null;
    }
}
<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0"
    name="com.example.myplugin.factory.MyProcessFactory">
  <implementation class="com.example.myplugin.factory.MyProcessFactory"/>
  <service>
    <provide interface="org.adempiere.base.IProcessFactory"/>
  </service>
  <property name="service.ranking" type="Integer" value="100"/>
</scr:component>

IFormFactory: Custom Form Resolution

Forms are interactive user interfaces for specialized tasks that do not fit the standard window/tab/field paradigm. The IFormFactory interface lets plugins provide custom form implementations.

The IFormFactory Interface

public interface IFormFactory {

    /**
     * Create a form instance for the given class name.
     * @param formClassName the fully qualified class name of the form
     * @return an IFormController (for ZK) instance, or null to defer
     */
    public Object newFormInstance(String formClassName);
}

Registration Pattern

package com.example.myplugin.factory;

import org.adempiere.base.IFormFactory;

public class MyFormFactory implements IFormFactory {

    @Override
    public Object newFormInstance(String formClassName) {
        if ("com.example.myplugin.form.MyCustomForm"
                .equals(formClassName)) {
            return new com.example.myplugin.form.MyCustomForm();
        }
        return null;
    }
}

The component.xml registration follows the same pattern as the other factories.

IModelValidatorFactory

The IModelValidatorFactory provides a way to register ModelValidator instances — classes that receive callbacks when records are saved or documents are processed. While the newer Event Handler approach (covered next) is generally preferred for new development, model validators remain widely used in existing plugins.

public interface IModelValidatorFactory {

    /**
     * Create model validator instances for the given client.
     * @param client the MClient for which validators should be created
     * @return an array of ModelValidator instances, or null
     */
    public ModelValidator[] newModelValidatorInstances(MClient client);
}

The Event Handler Pattern

Event handlers are the modern, preferred approach for reacting to model events in iDempiere. They use the OSGi Event Admin service to subscribe to typed events that the core fires at specific points during record persistence and document processing.

IEventManager and IEventTopics

The IEventManager service is the central hub for event dispatch in iDempiere. It extends the standard OSGi Event Admin with iDempiere-specific convenience methods. The IEventTopics interface defines constants for all the event topics you can subscribe to:

public interface IEventTopics {
    // Persistence events
    public static final String PO_BEFORE_NEW =
        "org/adempiere/base/event/PO_BEFORE_NEW";
    public static final String PO_AFTER_NEW =
        "org/adempiere/base/event/PO_AFTER_NEW";
    public static final String PO_BEFORE_CHANGE =
        "org/adempiere/base/event/PO_BEFORE_CHANGE";
    public static final String PO_AFTER_CHANGE =
        "org/adempiere/base/event/PO_AFTER_CHANGE";
    public static final String PO_BEFORE_DELETE =
        "org/adempiere/base/event/PO_BEFORE_DELETE";
    public static final String PO_AFTER_DELETE =
        "org/adempiere/base/event/PO_AFTER_DELETE";

    // Document processing events
    public static final String DOC_BEFORE_PREPARE =
        "org/adempiere/base/event/DOC_BEFORE_PREPARE";
    public static final String DOC_BEFORE_COMPLETE =
        "org/adempiere/base/event/DOC_BEFORE_COMPLETE";
    public static final String DOC_AFTER_COMPLETE =
        "org/adempiere/base/event/DOC_AFTER_COMPLETE";
    public static final String DOC_BEFORE_REVERSECORRECT =
        "org/adempiere/base/event/DOC_BEFORE_REVERSECORRECT";
    // ... and more
}

Subscribing to Events

To handle events, your plugin implements org.osgi.service.event.EventHandler and registers as a DS component with the appropriate event topics:

package com.example.myplugin.event;

import org.adempiere.base.event.AbstractEventHandler;
import org.adempiere.base.event.IEventTopics;
import org.compiere.model.PO;
import org.osgi.service.event.Event;

public class BPartnerEventHandler extends AbstractEventHandler {

    @Override
    protected void initialize() {
        // Register for specific events on specific tables
        registerTableEvent(IEventTopics.PO_BEFORE_NEW, "C_BPartner");
        registerTableEvent(IEventTopics.PO_BEFORE_CHANGE, "C_BPartner");
    }

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

        if (topic.equals(IEventTopics.PO_BEFORE_NEW)
                || topic.equals(IEventTopics.PO_BEFORE_CHANGE)) {
            // Validate that customer business partners have a valid email
            boolean isCustomer = po.get_ValueAsBoolean("IsCustomer");
            String email = (String) po.get_Value("EMail");

            if (isCustomer && (email == null || !email.contains("@"))) {
                addErrorMessage(event,
                    "Customer must have a valid email address");
            }
        }
    }
}

The AbstractEventHandler base class provides convenience methods like registerTableEvent(), getPO(), and addErrorMessage() that simplify event handler implementation. The addErrorMessage() call prevents the record from being saved and displays the error to the user.

Component.xml for Event Handlers

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

Note that the event.topics property uses a wildcard (*) to subscribe to all PO events. The initialize() method in the handler further filters by specific tables and event types. The immediate="true" attribute ensures the handler is activated as soon as the bundle starts.

Priority Ordering Across Extensions

When multiple plugins register implementations of the same factory interface, priority matters. OSGi uses the service.ranking property to determine order:

  • Higher service.ranking values are consulted first
  • The iDempiere default factories typically have a ranking of 0 or are unranked
  • Set your plugin’s ranking to a positive value (e.g., 100) to be consulted before the defaults
  • If you want your factory to act as a fallback (consulted after defaults), use a negative ranking
<!-- High priority: consulted first -->
<property name="service.ranking" type="Integer" value="200"/>

<!-- Normal priority: consulted before defaults -->
<property name="service.ranking" type="Integer" value="100"/>

<!-- Low priority: fallback, consulted after defaults -->
<property name="service.ranking" type="Integer" value="-100"/>

Practical Example: Complete Factory Registration

Let us bring everything together with a complete example. Suppose you are building a plugin for a company that needs custom behavior for the C_BPartner (Business Partner) table. You need:

  1. A custom model class with additional business logic
  2. A callout that auto-fills the credit limit based on partner category
  3. An event handler that validates data before saving

Your OSGI-INF/component.xml would register all three:

<?xml version="1.0" encoding="UTF-8"?>
<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0"
    name="com.example.bpartner.factory.BPartnerModelFactory">
  <implementation
      class="com.example.bpartner.factory.BPartnerModelFactory"/>
  <service>
    <provide interface="org.adempiere.base.IModelFactory"/>
  </service>
  <property name="service.ranking" type="Integer" value="100"/>
</scr:component>

You would typically have separate component XML files for each service (referenced in MANIFEST.MF’s Service-Component header as a comma-separated list), or you can use a single file with multiple scr:component elements by wrapping them in a container format. The most common practice in iDempiere plugins is one XML file per component:

Service-Component: OSGI-INF/modelfactory.xml,
 OSGI-INF/callout.xml,
 OSGI-INF/eventhandler.xml

Key Takeaways

  • iDempiere uses the Factory pattern with OSGi service-based discovery to allow plugins to extend core behavior without modifying source code.
  • The main factory interfaces are IModelFactory (model class resolution), IColumnCallout (field-level callouts), IProcessFactory (process instantiation), IFormFactory (form instantiation), and IModelValidatorFactory (model validators).
  • Factories return null to defer to the next registered factory — this is how multiple plugins coexist, each handling specific cases.
  • The service.ranking property controls priority ordering. Higher values are consulted first.
  • Event handlers (using AbstractEventHandler and IEventTopics) are the modern, preferred approach for reacting to model events.
  • All extensions are registered declaratively via component.xml files referenced in MANIFEST.MF.

What’s Next

In the next lesson, we explore 2Pack — iDempiere’s built-in mechanism for packaging and distributing Application Dictionary changes. You will learn how to export customizations, import them into other environments, and integrate 2Pack into your plugin development workflow.

繁體中文

概述

  • 學習內容:iDempiere 中的工廠模式及工廠如何在執行時以優先順序解析;如何實作 IModelFactory、IColumnCallout、IProcessFactory、IFormFactory 和 IModelValidatorFactory;使用 IEventManager 和 IEventTopics 的 Event Handler 模式,以及如何透過 component.xml 註冊所有擴展。
  • 先修條件:第 1-18 課(尤其是第 18 課:iDempiere 中的 OSGi 框架)
  • 預估閱讀時間:25 分鐘

簡介

iDempiere 的真正威力不在於修改其原始碼,而在於透過明確定義的介面來擴展它。iDempiere 提供了一組工廠介面和事件處理系統,允許 Plugin 將自訂行為注入應用程式的幾乎每個部分。

這些擴展機制遵循工廠設計模式:iDempiere 核心定義一個介面(工廠契約),Plugin 提供核心在執行時透過 OSGi 服務登錄處發現並呼叫的實作。

iDempiere 中的工廠模式

iDempiere 使用基於 OSGi 服務的工廠。解析流程:

  1. 核心程式碼從 OSGi 服務登錄處請求工廠介面的所有已註冊實作。
  2. 實作按 service.ranking 屬性排序(較高值優先)。
  3. 核心遍歷排序後的列表,呼叫每個實作的工廠方法。
  4. 第一個返回非 null 結果的工廠獲勝。
  5. 如果沒有工廠返回結果,核心回退到其預設行為。

IModelFactory:自訂模型類別解析

當 iDempiere 需要為資料庫資料表建立持久物件(PO)時,它使用 IModelFactory 來決定應使用哪個 Java 類別。實作 IModelFactory 讓您的 Plugin 可以為特定資料表替換自己的模型類別。

透過 component.xml 註冊,使用 service.ranking 屬性確保您的工廠在預設工廠之前被諮詢。

IColumnCallout:透過擴展註冊 Callout

IColumnCallout 介面允許 Plugin 在不修改應用程式字典的情況下註冊 Callout。component.xml 使用 tableNamecolumnName 屬性指定觸發條件。您也可以使用 IColumnCalloutFactory 進行更動態的控制。

IProcessFactory:自訂流程解析

IProcessFactory 介面允許 Plugin 提供自訂流程實作。工廠根據完整限定類別名稱返回 ProcessCall 實例。

IFormFactory:自訂表單解析

IFormFactory 介面讓 Plugin 提供自訂表單實作,用於不適合標準視窗/頁籤/欄位模式的專用任務。

IModelValidatorFactory

IModelValidatorFactory 提供註冊 ModelValidator 實例的方式——在記錄儲存或文件處理時接收回呼的類別。

Event Handler 模式

Event Handler 是在 iDempiere 中回應模型事件的現代首選方法。它們使用 OSGi Event Admin 服務訂閱核心在記錄持久化和文件處理過程中觸發的事件。

IEventTopics 介面定義所有可訂閱的事件主題常數:

  • 持久化事件:PO_BEFORE_NEW、PO_AFTER_NEW、PO_BEFORE_CHANGE、PO_AFTER_CHANGE、PO_BEFORE_DELETE、PO_AFTER_DELETE
  • 文件處理事件:DOC_BEFORE_PREPARE、DOC_BEFORE_COMPLETE、DOC_AFTER_COMPLETE 等

AbstractEventHandler 基礎類別提供便利方法如 registerTableEvent()getPO()addErrorMessage()

擴展之間的優先順序排序

OSGi 使用 service.ranking 屬性決定順序:

  • 較高的 service.ranking 值優先被諮詢
  • iDempiere 預設工廠通常排名為 0 或未排名
  • 將您的 Plugin 排名設為正值(例如 100)以在預設之前被諮詢
  • 使用負排名作為回退

實際範例:完整的工廠註冊

在 MANIFEST.MF 的 Service-Component 標頭中列出多個元件 XML 檔案:

Service-Component: OSGI-INF/modelfactory.xml,
 OSGI-INF/callout.xml,
 OSGI-INF/eventhandler.xml

重點摘要

  • iDempiere 使用工廠模式搭配基於 OSGi 服務的發現,讓 Plugin 在不修改原始碼的情況下擴展核心行為。
  • 主要工廠介面:IModelFactory(模型類別解析)、IColumnCallout(欄位層級 Callout)、IProcessFactory(流程實例化)、IFormFactory(表單實例化)、IModelValidatorFactory(Model Validator)。
  • 工廠返回 null 以延遲到下一個已註冊的工廠——這是多個 Plugin 共存的方式。
  • service.ranking 屬性控制優先順序排序。較高值優先被諮詢。
  • Event Handler(使用 AbstractEventHandlerIEventTopics)是回應模型事件的現代首選方法。
  • 所有擴展都透過 MANIFEST.MF 中參照的 component.xml 檔案以宣告方式註冊。

下一步

在下一課中,我們將探索 2Pack——iDempiere 的內建機制,用於打包和分發應用程式字典變更。您將學習如何匯出自訂、匯入至其他環境,以及將 2Pack 整合到您的 Plugin 開發工作流程中。

日本語

概要

  • 学習内容:iDempiereにおけるファクトリーパターンとランタイムでの優先順位付きファクトリー解決方法。IModelFactory、IColumnCallout、IProcessFactory、IFormFactory、IModelValidatorFactoryの実装方法。IEventManagerとIEventTopicsを使用したイベントハンドラーパターン、component.xmlによる全拡張の登録方法。
  • 前提条件:レッスン1〜18(特にレッスン18:iDempiereにおけるOSGiフレームワーク)
  • 推定読了時間:25分

はじめに

iDempiereの真の力はソースコードの修正ではなく、明確に定義されたインターフェースを通じた拡張にあります。iDempiereはファクトリーインターフェースとイベント処理システムを提供し、プラグインがアプリケーションのほぼすべての部分にカスタム動作を注入できるようにします。

これらの拡張メカニズムはファクトリーデザインパターンに従います:コアがインターフェース(ファクトリーコントラクト)を定義し、プラグインがOSGiサービスレジストリを通じてランタイムで発見・呼び出される実装を提供します。

iDempiereにおけるファクトリーパターン

iDempiereはOSGiサービスベースのファクトリーを使用します。解決プロセス:

  1. コアコードがOSGiサービスレジストリからファクトリーインターフェースのすべての登録済み実装を要求。
  2. 実装はservice.rankingプロパティでソート(高い値が先)。
  3. コアがソートされたリストを反復し、各実装のファクトリーメソッドを呼び出す。
  4. 最初にnull以外の結果を返したファクトリーが勝つ。
  5. どのファクトリーも結果を返さない場合、コアはデフォルト動作にフォールバック。

IModelFactory:カスタムモデルクラス解決

iDempiereがデータベーステーブル用のPOを作成する必要がある場合、IModelFactoryを使用してどのJavaクラスを使用すべきかを決定します。IModelFactoryを実装することで、プラグインは特定のテーブルに独自のモデルクラスを置き換えることができます。

IColumnCallout:拡張によるコールアウト登録

IColumnCalloutインターフェースにより、プラグインはアプリケーション辞書を変更せずにコールアウトを登録できます。component.xmlでtableNamecolumnNameプロパティを使用してトリガー条件を指定します。

IProcessFactory:カスタムプロセス解決

IProcessFactoryインターフェースにより、プラグインはカスタムプロセス実装を提供できます。

IFormFactory:カスタムフォーム解決

IFormFactoryインターフェースにより、プラグインは標準のウィンドウ/タブ/フィールドパラダイムに収まらない専用タスクのカスタムフォーム実装を提供できます。

IModelValidatorFactory

IModelValidatorFactoryはModelValidatorインスタンスを登録する方法を提供します。

イベントハンドラーパターン

イベントハンドラーはiDempiereでモデルイベントに反応するための現代的な推奨アプローチです。OSGi Event Adminサービスを使用して、レコードの永続化やドキュメント処理中にコアが発火するイベントをサブスクライブします。

IEventTopicsインターフェースはサブスクライブ可能なすべてのイベントトピック定数を定義します:

  • 永続化イベント:PO_BEFORE_NEW、PO_AFTER_NEW、PO_BEFORE_CHANGE、PO_AFTER_CHANGE、PO_BEFORE_DELETE、PO_AFTER_DELETE
  • ドキュメント処理イベント:DOC_BEFORE_PREPARE、DOC_BEFORE_COMPLETE、DOC_AFTER_COMPLETEなど

AbstractEventHandler基底クラスがregisterTableEvent()getPO()addErrorMessage()などの便利メソッドを提供します。

拡張間の優先順位

OSGiはservice.rankingプロパティで順序を決定します:

  • 高いservice.ranking値が先に参照される
  • iDempiereのデフォルトファクトリーは通常ランキング0または未ランク
  • プラグインのランキングを正の値(例:100)に設定してデフォルトより先に参照させる
  • フォールバックには負のランキングを使用

実践例:完全なファクトリー登録

MANIFEST.MFのService-Componentヘッダーに複数のコンポーネントXMLファイルをリスト:

Service-Component: OSGI-INF/modelfactory.xml,
 OSGI-INF/callout.xml,
 OSGI-INF/eventhandler.xml

重要ポイント

  • iDempiereはファクトリーパターンとOSGiサービスベースの発見を使用し、プラグインがソースコードを修正せずにコア動作を拡張可能にする。
  • 主要なファクトリーインターフェース:IModelFactory(モデルクラス解決)、IColumnCallout(フィールドレベルコールアウト)、IProcessFactory(プロセスインスタンス化)、IFormFactory(フォームインスタンス化)、IModelValidatorFactory(モデルバリデータ)。
  • ファクトリーはnullを返して次の登録済みファクトリーに委譲——これが複数のプラグインが共存する仕組み。
  • service.rankingプロパティが優先順位を制御。高い値が先に参照。
  • イベントハンドラー(AbstractEventHandlerIEventTopics使用)がモデルイベントに反応する現代的な推奨アプローチ。
  • すべての拡張はMANIFEST.MFで参照されるcomponent.xmlファイルで宣言的に登録。

次のステップ

次のレッスンでは、2Pack——iDempiereの組み込みのアプリケーション辞書変更のパッケージングと配布メカニズムを探索します。カスタマイズのエクスポート、他の環境へのインポート、プラグイン開発ワークフローへの2Pack統合について学びます。

You Missed