Model Events and Event Handlers
Overview
- What you’ll learn:
- The complete event system architecture in iDempiere, including all IEventTopics constants for persistence and document events
- How to implement event handlers using AbstractEventHandler with table-specific filtering, data access, and error handling
- How to compare old and new values, control event priority and ordering, and debug event handler issues in development
- Prerequisites: Lessons 1–21 (especially Lesson 21: Creating Your First Plugin)
- Estimated reading time: 24 minutes
Introduction
In the previous lesson, you created a plugin with a simple event handler that logged a message when a Business Partner was created. In real-world development, event handlers are the primary tool for implementing custom business logic in iDempiere. They enforce validation rules, auto-calculate field values, synchronize data between related records, trigger notifications, and integrate with external systems.
This lesson is a comprehensive guide to the model event system. We will examine every event topic, explore the full API available within event handlers, learn techniques for comparing old and new values, and build practical examples that demonstrate real business scenarios. By the end, you will be able to implement production-quality event-driven logic in your iDempiere plugins.
The Event System Architecture
iDempiere’s event system is built on top of the OSGi Event Admin service. When the core system performs an operation on a Persistent Object (PO) — such as saving, deleting, or processing a document — it fires events at specific lifecycle points. Plugins subscribe to these events and receive callbacks with full access to the affected data.
The architecture has three main components:
- Event Producer — The iDempiere core (specifically, the PO base class and the Document Engine) fires events at defined lifecycle points.
- Event Bus — The
IEventManagerservice (wrapping OSGi Event Admin) routes events to registered handlers based on topic matching. - Event Consumers — Your plugin’s event handlers, registered as OSGi services, receive and process the events.
Events are synchronous — the event producer waits for all handlers to complete before proceeding. This is important because it means your handler can prevent a save (by adding an error) or modify data (by changing PO field values) and the changes will take effect before the operation completes.
IEventTopics: The Complete Event Reference
The org.adempiere.base.event.IEventTopics interface defines constants for all available event topics. These fall into two categories: persistence events (fired during record save/delete) and document events (fired during document processing).
Persistence Events
These events fire when a record is saved to or deleted from the database:
| Constant | Topic String | When Fired |
|---|---|---|
PO_BEFORE_NEW |
org/adempiere/base/event/PO_BEFORE_NEW |
Before a new record is inserted into the database. The record has been validated but not yet committed. |
PO_AFTER_NEW |
org/adempiere/base/event/PO_AFTER_NEW |
After a new record has been successfully inserted. The database transaction has not yet been committed (you are still within the same transaction). |
PO_BEFORE_CHANGE |
org/adempiere/base/event/PO_BEFORE_CHANGE |
Before an existing record is updated. The PO contains the new values; original values are accessible via get_ValueOld(). |
PO_AFTER_CHANGE |
org/adempiere/base/event/PO_AFTER_CHANGE |
After an existing record has been successfully updated. |
PO_BEFORE_DELETE |
org/adempiere/base/event/PO_BEFORE_DELETE |
Before a record is deleted. You can prevent deletion by adding an error. |
PO_AFTER_DELETE |
org/adempiere/base/event/PO_AFTER_DELETE |
After a record has been deleted (but the transaction has not yet committed). |
Document Processing Events
These events fire during the document processing lifecycle (Prepare, Complete, Void, Close, Reverse, etc.):
| Constant | When Fired |
|---|---|
DOC_BEFORE_PREPARE |
Before a document enters the Prepare phase (validation before completion) |
DOC_AFTER_PREPARE |
After the Prepare phase completes successfully |
DOC_BEFORE_COMPLETE |
Before a document is completed (final processing) |
DOC_AFTER_COMPLETE |
After a document has been completed successfully |
DOC_BEFORE_VOID |
Before a document is voided |
DOC_AFTER_VOID |
After a document has been voided |
DOC_BEFORE_CLOSE |
Before a document is closed |
DOC_AFTER_CLOSE |
After a document has been closed |
DOC_BEFORE_REVERSECORRECT |
Before a document is reverse-corrected |
DOC_AFTER_REVERSECORRECT |
After a document has been reverse-corrected |
DOC_BEFORE_REVERSEACCRUAL |
Before a document is reverse-accrualed |
DOC_AFTER_REVERSEACCRUAL |
After a document has been reverse-accrualed |
DOC_BEFORE_REACTIVATE |
Before a document is reactivated from completed status |
DOC_AFTER_REACTIVATE |
After a document has been reactivated |
Choosing Between BEFORE and AFTER Events
The choice between BEFORE and AFTER events depends on what you need to do:
- Use BEFORE events when: You need to validate data and potentially prevent the operation (by adding an error), or you need to modify field values before they are saved to the database.
- Use AFTER events when: You need to perform follow-up actions that depend on the operation having succeeded (creating related records, sending notifications, updating counters), or you need access to the generated primary key ID for new records.
Implementing an Event Handler
The recommended base class for event handlers in iDempiere is org.adempiere.base.event.AbstractEventHandler. It provides convenience methods and handles the registration boilerplate.
The AbstractEventHandler API
public abstract class AbstractEventHandler implements EventHandler {
// Override this to register for specific events
protected abstract void initialize();
// Override this to handle events
protected abstract void doHandleEvent(Event event);
// Register for events on a specific table
protected void registerTableEvent(String topic, String tableName);
// Register for events on all tables
protected void registerEvent(String topic);
// Get the PO (Persistent Object) from the event
protected PO getPO(Event event);
// Add an error message that prevents the operation
protected void addErrorMessage(Event event, String errorMessage);
// Set an event property
protected void setEventProperty(Event event, String key, Object value);
}
Table-Specific Event Filtering
The registerTableEvent() method filters events so your handler only receives events for the specified table. This is both a performance optimization and a safety measure — you do not want your Business Partner handler accidentally processing Invoice events.
@Override
protected void initialize() {
// Only handle events for these specific tables
registerTableEvent(IEventTopics.PO_BEFORE_NEW, "C_Order");
registerTableEvent(IEventTopics.PO_BEFORE_CHANGE, "C_Order");
registerTableEvent(IEventTopics.PO_BEFORE_NEW, "C_OrderLine");
registerTableEvent(IEventTopics.PO_BEFORE_CHANGE, "C_OrderLine");
// For document events, register on the document table
registerTableEvent(IEventTopics.DOC_BEFORE_COMPLETE, "C_Order");
}
You can also use registerEvent() without a table name to receive events for all tables, but this is rarely appropriate in production code.
Accessing PO Data in Handlers
The getPO(event) method returns the Persistent Object being saved, deleted, or processed. The PO class provides a rich API for accessing and modifying record data.
Reading Field Values
PO po = getPO(event);
// Get a value by column name (returns Object)
Object value = po.get_Value("Name");
// Get a typed value
String name = po.get_ValueAsString("Name");
int bPartnerId = po.get_ValueAsInt("C_BPartner_ID");
boolean isActive = po.get_ValueAsBoolean("IsActive");
java.math.BigDecimal amount = (java.math.BigDecimal)
po.get_Value("GrandTotal");
java.sql.Timestamp date = (java.sql.Timestamp)
po.get_Value("DateOrdered");
// Get the table ID and record ID
int tableId = po.get_Table_ID();
int recordId = po.get_ID();
// Get the table name
String tableName = po.get_TableName();
Modifying Field Values (BEFORE Events Only)
In BEFORE events, you can modify field values before they are persisted:
// Set a field value (only effective in BEFORE events)
po.set_ValueOfColumn("Description",
"Auto-generated on " + new java.sql.Timestamp(
System.currentTimeMillis()));
// For typed model classes, you can cast and use setters
if (po instanceof org.compiere.model.MOrder) {
org.compiere.model.MOrder order = (org.compiere.model.MOrder) po;
order.setDescription("Auto-generated");
}
Checking If This Is a New Record
// Check if the record is being created (vs updated)
boolean isNew = po.is_new();
// Alternatively, check the event topic
if (event.getTopic().equals(IEventTopics.PO_BEFORE_NEW)) {
// This is a new record
}
Preventing Save with addError()
One of the most powerful capabilities of BEFORE event handlers is the ability to prevent the save operation. Calling addErrorMessage() stops the save and displays the error to the user.
@Override
protected void doHandleEvent(Event event) {
PO po = getPO(event);
String topic = event.getTopic();
if (IEventTopics.PO_BEFORE_NEW.equals(topic)
|| IEventTopics.PO_BEFORE_CHANGE.equals(topic)) {
// Validate: Sales Orders must have a PO Reference
if ("C_Order".equals(po.get_TableName())) {
boolean isSOTrx = po.get_ValueAsBoolean("IsSOTrx");
String poReference = po.get_ValueAsString("POReference");
if (isSOTrx &&
(poReference == null || poReference.trim().isEmpty())) {
addErrorMessage(event,
"Sales Orders require a PO Reference number. "
+ "Please enter the customer's purchase order number.");
return; // Stop processing
}
}
}
}
When addErrorMessage() is called, the save operation is aborted, the database transaction is rolled back, and the user sees the error message in the UI. This is the standard way to implement server-side validation in iDempiere plugins.
Comparing Old vs New Values
In BEFORE_CHANGE and AFTER_CHANGE events, you often need to know whether a specific field was modified and what its previous value was. The PO class provides several methods for this:
@Override
protected void doHandleEvent(Event event) {
PO po = getPO(event);
if (IEventTopics.PO_BEFORE_CHANGE.equals(event.getTopic())) {
// Check if a specific column was changed
if (po.is_ValueChanged("CreditLimit")) {
// Get the old value
java.math.BigDecimal oldLimit = (java.math.BigDecimal)
po.get_ValueOld("CreditLimit");
// Get the new value
java.math.BigDecimal newLimit = (java.math.BigDecimal)
po.get_Value("CreditLimit");
logger.info("Credit limit changed from "
+ oldLimit + " to " + newLimit
+ " for BPartner " + po.get_ValueAsString("Name"));
// Validate: credit limit cannot be increased by more
// than 50% without manager approval
if (oldLimit != null && oldLimit.signum() > 0) {
java.math.BigDecimal maxAllowed = oldLimit.multiply(
new java.math.BigDecimal("1.5"));
if (newLimit.compareTo(maxAllowed) > 0) {
addErrorMessage(event,
"Credit limit increase exceeds 50%. "
+ "Maximum allowed: " + maxAllowed
+ ". Please request manager approval.");
}
}
}
// Check if any column in a set was changed
int[] changedColumns = po.get_IDsOldValues();
// This returns column indices of all changed columns
}
}
The is_ValueChanged() Method
The is_ValueChanged(String columnName) method returns true if the specified column’s value has been modified in the current save operation. This is essential for:
- Avoiding unnecessary processing when unrelated fields change
- Triggering logic only when specific business-critical fields are modified
- Building audit trails that capture only actual changes
Event Handler Priority and Ordering
When multiple event handlers subscribe to the same event topic and table, the order in which they execute matters. iDempiere uses the OSGi service ranking mechanism to determine order.
<!-- In component.xml, set the service ranking -->
<property name="service.ranking" type="Integer" value="100"/>
Handlers with higher ranking values execute first. If no ranking is specified, the default is 0. Negative rankings execute after handlers with the default ranking.
Use cases for priority ordering:
- High priority (200+): Validation handlers that should reject invalid data before other handlers process it
- Normal priority (0-100): Business logic handlers that auto-set values or create related records
- Low priority (negative): Audit/logging handlers that should run last, after all other handlers have finished their work
Practical Example: Auto-Set Field Values on Save
Let us build a complete, practical event handler that demonstrates several techniques. This handler auto-populates fields on Sales Order lines based on business rules:
package com.example.orderplugin.event;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.logging.Level;
import org.adempiere.base.event.AbstractEventHandler;
import org.adempiere.base.event.IEventTopics;
import org.compiere.model.MBPartner;
import org.compiere.model.PO;
import org.compiere.util.CLogger;
import org.compiere.util.DB;
import org.compiere.util.Env;
import org.osgi.service.event.Event;
public class OrderLineEventHandler extends AbstractEventHandler {
private static final CLogger logger =
CLogger.getCLogger(OrderLineEventHandler.class);
@Override
protected void initialize() {
registerTableEvent(IEventTopics.PO_BEFORE_NEW, "C_OrderLine");
registerTableEvent(IEventTopics.PO_BEFORE_CHANGE, "C_OrderLine");
}
@Override
protected void doHandleEvent(Event event) {
PO po = getPO(event);
try {
applyCustomPricing(po, event);
validateMinimumQuantity(po, event);
} catch (Exception e) {
logger.log(Level.SEVERE,
"Error in OrderLineEventHandler", e);
addErrorMessage(event,
"An error occurred processing the order line: "
+ e.getMessage());
}
}
private void applyCustomPricing(PO po, Event event) {
// Only apply when quantity or product changes
if (!po.is_new()
&& !po.is_ValueChanged("QtyOrdered")
&& !po.is_ValueChanged("M_Product_ID")) {
return;
}
BigDecimal qty = (BigDecimal) po.get_Value("QtyOrdered");
if (qty == null || qty.signum() <= 0) return;
// Look up the business partner's discount group
int orderId = po.get_ValueAsInt("C_Order_ID");
String sql = "SELECT bp.C_BP_Group_ID "
+ "FROM C_Order o "
+ "JOIN C_BPartner bp ON o.C_BPartner_ID = bp.C_BPartner_ID "
+ "WHERE o.C_Order_ID = ?";
int bpGroupId = DB.getSQLValue(po.get_TrxName(), sql, orderId);
// Apply tiered discount based on quantity
BigDecimal discount = BigDecimal.ZERO;
if (qty.compareTo(new BigDecimal("100")) >= 0) {
discount = new BigDecimal("15"); // 15% for 100+
} else if (qty.compareTo(new BigDecimal("50")) >= 0) {
discount = new BigDecimal("10"); // 10% for 50-99
} else if (qty.compareTo(new BigDecimal("25")) >= 0) {
discount = new BigDecimal("5"); // 5% for 25-49
}
if (discount.signum() > 0) {
po.set_ValueOfColumn("Discount", discount);
logger.info("Applied " + discount
+ "% volume discount for qty " + qty);
}
}
private void validateMinimumQuantity(PO po, Event event) {
BigDecimal qty = (BigDecimal) po.get_Value("QtyOrdered");
if (qty != null && qty.signum() > 0
&& qty.compareTo(BigDecimal.ONE) < 0) {
addErrorMessage(event,
"Minimum order quantity is 1. "
+ "Please enter a valid quantity.");
}
}
}
Component XML for This Handler
<?xml version="1.0" encoding="UTF-8"?>
<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0"
name="com.example.orderplugin.event.OrderLineEventHandler"
immediate="true">
<implementation
class="com.example.orderplugin.event.OrderLineEventHandler"/>
<service>
<provide interface="org.osgi.service.event.EventHandler"/>
</service>
<property name="event.topics" type="String">
org/adempiere/base/event/*
</property>
<property name="service.ranking" type="Integer" value="50"/>
</scr:component>
Handling Document Events
Document events follow the same pattern as persistence events, but they fire during document processing (Complete, Void, Close, etc.) rather than during record save/delete:
package com.example.orderplugin.event;
import org.adempiere.base.event.AbstractEventHandler;
import org.adempiere.base.event.IEventTopics;
import org.compiere.model.MOrder;
import org.compiere.model.PO;
import org.osgi.service.event.Event;
public class OrderDocEventHandler extends AbstractEventHandler {
@Override
protected void initialize() {
registerTableEvent(IEventTopics.DOC_BEFORE_COMPLETE, "C_Order");
registerTableEvent(IEventTopics.DOC_AFTER_COMPLETE, "C_Order");
}
@Override
protected void doHandleEvent(Event event) {
PO po = getPO(event);
String topic = event.getTopic();
if (IEventTopics.DOC_BEFORE_COMPLETE.equals(topic)) {
// Validate before allowing completion
MOrder order = (MOrder) po;
if (order.isSOTrx() && order.getLines().length == 0) {
addErrorMessage(event,
"Cannot complete a Sales Order with no lines.");
return;
}
// Check credit limit
if (order.isSOTrx()) {
MBPartner bp = new MBPartner(
order.getCtx(), order.getC_BPartner_ID(),
order.get_TrxName());
if (bp.getCreditLimit().signum() > 0) {
java.math.BigDecimal openBalance =
bp.getTotalOpenBalance();
java.math.BigDecimal orderTotal =
order.getGrandTotal();
if (openBalance.add(orderTotal).compareTo(
bp.getCreditLimit()) > 0) {
addErrorMessage(event,
"Order exceeds customer credit limit. "
+ "Open balance: " + openBalance
+ ", Order total: " + orderTotal
+ ", Credit limit: "
+ bp.getCreditLimit());
}
}
}
}
if (IEventTopics.DOC_AFTER_COMPLETE.equals(topic)) {
// Post-completion actions
MOrder order = (MOrder) po;
// Send notification, update external system, etc.
}
}
}
Debugging Event Handlers
When event handlers do not behave as expected, use these debugging techniques:
Verify the Component Is Active
Connect to the OSGi console and check your component’s status:
# List all DS components
scr:list
# Show details of a specific component
scr:info com.example.orderplugin.event.OrderLineEventHandler
# Expected output should show:
# State: ACTIVE
# Service: {org.osgi.service.event.EventHandler}={...}
# References:
# - Satisfied
Add Diagnostic Logging
Add logging at the entry point of your handler to confirm events are being received:
@Override
protected void doHandleEvent(Event event) {
PO po = getPO(event);
logger.info("Event received: topic=" + event.getTopic()
+ " table=" + po.get_TableName()
+ " record=" + po.get_ID());
// ... rest of handler
}
Check Event Topic Matching
A common mistake is registering for the wrong event topic. Double-check that:
- The topic string in your component.xml matches what you expect (remember the
org/adempiere/base/event/prefix with forward slashes, not dots) - Your
initialize()method registers for the correct combination of topics and table names - You are listening for the right event type (PO_BEFORE_NEW vs PO_BEFORE_CHANGE vs DOC_BEFORE_COMPLETE)
Exception Handling
Always wrap your handler logic in a try-catch block. An unhandled exception in an event handler can cause unpredictable behavior — the save might succeed without your validation, or it might fail with a confusing error message.
@Override
protected void doHandleEvent(Event event) {
try {
// Your handler logic here
} catch (Exception e) {
logger.log(Level.SEVERE,
"Unexpected error in event handler", e);
addErrorMessage(event,
"Internal error: " + e.getMessage());
}
}
Breakpoint Debugging in Eclipse
When running iDempiere from Eclipse in debug mode, you can set breakpoints in your event handler code just like any other Java code. The debugger will pause execution when the breakpoint is hit, allowing you to inspect the PO values, step through your logic, and verify your assumptions.
Best Practices
- Keep handlers focused. Each handler should handle one table or one business concern. Do not create a single “catch-all” handler for all tables.
- Check is_ValueChanged(). Before performing expensive operations (database queries, external API calls), verify that the relevant fields actually changed.
- Handle null values. PO field values can be null. Always check for null before performing operations on returned values.
- Use the same transaction. When creating or modifying related records in an AFTER event, use the same transaction name (
po.get_TrxName()) to ensure atomicity. - Avoid infinite loops. If your BEFORE_CHANGE handler calls
po.set_ValueOfColumn(), be aware that this could trigger another BEFORE_CHANGE event if the PO is saved again. Use flags or checkis_ValueChanged()to break the loop. - Log judiciously. Use
Level.INFOfor significant business events andLevel.FINEfor diagnostic details. Excessive INFO logging in high-frequency handlers will flood your server log.
Key Takeaways
- iDempiere’s event system fires synchronous events at defined lifecycle points during record persistence (PO_BEFORE_NEW, PO_AFTER_CHANGE, etc.) and document processing (DOC_BEFORE_COMPLETE, etc.).
- Use
AbstractEventHandleras your base class — it providesregisterTableEvent(),getPO(), andaddErrorMessage()convenience methods. - BEFORE events allow you to validate data, prevent saves (with
addErrorMessage()), and modify field values. AFTER events are for follow-up actions that depend on the save having succeeded. - Use
is_ValueChanged()andget_ValueOld()to detect and respond to specific field changes, avoiding unnecessary processing. - Control execution order with
service.rankingin component.xml — higher values execute first. - Always wrap handler logic in try-catch blocks and verify component activation in the OSGi console when debugging.
What’s Next
Event handlers react to changes in existing data. In the next lesson, you will learn to create custom processes that perform batch operations and custom forms that provide specialized user interfaces — the two other primary extension points for iDempiere plugin developers.
繁體中文翻譯
概覽
- 您將學到:
- iDempiere 中完整的事件系統架構,包括所有用於持久化和文件事件的 IEventTopics 常數
- 如何使用 AbstractEventHandler 實作事件處理器,包括表格特定過濾、資料存取和錯誤處理
- 如何比較新舊值、控制事件優先順序和排序,以及在開發中除錯事件處理器問題
- 先決條件:第 1–21 課(特別是第 21 課:建立您的第一個外掛)
- 預估閱讀時間:24 分鐘
簡介
在上一課中,您建立了一個帶有簡單事件處理器的外掛,當建立業務夥伴時記錄訊息。在實際開發中,事件處理器是在 iDempiere 中實作自訂業務邏輯的主要工具。它們強制執行驗證規則、自動計算欄位值、同步相關記錄之間的資料、觸發通知,以及與外部系統整合。
本課是模型事件系統的完整指南。我們將檢視每個事件主題,探索事件處理器中可用的完整 API,學習比較新舊值的技術,並建立示範真實業務場景的實際範例。在課程結束時,您將能夠在 iDempiere 外掛中實作生產品質的事件驅動邏輯。
事件系統架構
iDempiere 的事件系統建立在 OSGi Event Admin 服務之上。當核心系統對持久物件(PO)執行操作時 — 例如儲存、刪除或處理文件 — 它會在特定的生命週期點觸發事件。外掛訂閱這些事件並接收回呼,可以完全存取受影響的資料。
該架構有三個主要元件:
- 事件產生者 — iDempiere 核心(具體來說是 PO 基礎類別和 Document Engine)在定義的生命週期點觸發事件。
- 事件匯流排 —
IEventManager服務(封裝 OSGi Event Admin)根據主題匹配將事件路由到已註冊的處理器。 - 事件消費者 — 您的外掛事件處理器,作為 OSGi 服務註冊,接收和處理事件。
事件是同步的 — 事件產生者會等待所有處理器完成後才繼續。這很重要,因為這意味著您的處理器可以防止儲存(透過添加錯誤)或修改資料(透過變更 PO 欄位值),且變更會在操作完成前生效。
IEventTopics:完整事件參考
org.adempiere.base.event.IEventTopics 介面定義了所有可用事件主題的常數。這些分為兩類:持久化事件(在記錄儲存/刪除時觸發)和文件事件(在文件處理期間觸發)。
持久化事件
這些事件在記錄儲存到資料庫或從資料庫刪除時觸發:
| 常數 | 主題字串 | 觸發時機 |
|---|---|---|
PO_BEFORE_NEW |
org/adempiere/base/event/PO_BEFORE_NEW |
在新記錄插入資料庫之前。記錄已驗證但尚未提交。 |
PO_AFTER_NEW |
org/adempiere/base/event/PO_AFTER_NEW |
在新記錄成功插入之後。資料庫交易尚未提交(您仍在同一交易中)。 |
PO_BEFORE_CHANGE |
org/adempiere/base/event/PO_BEFORE_CHANGE |
在現有記錄更新之前。PO 包含新值;原始值可透過 get_ValueOld() 存取。 |
PO_AFTER_CHANGE |
org/adempiere/base/event/PO_AFTER_CHANGE |
在現有記錄成功更新之後。 |
PO_BEFORE_DELETE |
org/adempiere/base/event/PO_BEFORE_DELETE |
在記錄刪除之前。您可以透過添加錯誤來防止刪除。 |
PO_AFTER_DELETE |
org/adempiere/base/event/PO_AFTER_DELETE |
在記錄刪除之後(但交易尚未提交)。 |
文件處理事件
這些事件在文件處理生命週期期間觸發(準備、完成、作廢、關閉、沖銷等):
| 常數 | 觸發時機 |
|---|---|
DOC_BEFORE_PREPARE |
在文件進入準備階段之前(完成前的驗證) |
DOC_AFTER_PREPARE |
在準備階段成功完成之後 |
DOC_BEFORE_COMPLETE |
在文件完成之前(最終處理) |
DOC_AFTER_COMPLETE |
在文件成功完成之後 |
DOC_BEFORE_VOID |
在文件作廢之前 |
DOC_AFTER_VOID |
在文件作廢之後 |
DOC_BEFORE_CLOSE |
在文件關閉之前 |
DOC_AFTER_CLOSE |
在文件關閉之後 |
DOC_BEFORE_REVERSECORRECT |
在文件沖銷更正之前 |
DOC_AFTER_REVERSECORRECT |
在文件沖銷更正之後 |
DOC_BEFORE_REVERSEACCRUAL |
在文件沖銷應計之前 |
DOC_AFTER_REVERSEACCRUAL |
在文件沖銷應計之後 |
DOC_BEFORE_REACTIVATE |
在文件從完成狀態重新啟動之前 |
DOC_AFTER_REACTIVATE |
在文件重新啟動之後 |
選擇 BEFORE 或 AFTER 事件
BEFORE 和 AFTER 事件的選擇取決於您需要做什麼:
- 使用 BEFORE 事件:當您需要驗證資料並可能阻止操作(透過添加錯誤),或者需要在值儲存到資料庫之前修改欄位值。
- 使用 AFTER 事件:當您需要執行依賴於操作已成功的後續動作(建立相關記錄、傳送通知、更新計數器),或者需要存取新記錄的已產生主鍵 ID。
實作事件處理器
iDempiere 中事件處理器的建議基礎類別是 org.adempiere.base.event.AbstractEventHandler。它提供便利方法並處理註冊樣板程式碼。
AbstractEventHandler API
public abstract class AbstractEventHandler implements EventHandler {
// 覆寫此方法以註冊特定事件
protected abstract void initialize();
// 覆寫此方法以處理事件
protected abstract void doHandleEvent(Event event);
// 註冊特定表格的事件
protected void registerTableEvent(String topic, String tableName);
// 註冊所有表格的事件
protected void registerEvent(String topic);
// 從事件取得 PO(持久物件)
protected PO getPO(Event event);
// 添加阻止操作的錯誤訊息
protected void addErrorMessage(Event event, String errorMessage);
// 設定事件屬性
protected void setEventProperty(Event event, String key, Object value);
}
表格特定事件過濾
registerTableEvent() 方法過濾事件,使您的處理器只接收指定表格的事件。這既是效能最佳化也是安全措施 — 您不希望業務夥伴處理器意外處理發票事件。
@Override
protected void initialize() {
// 只處理這些特定表格的事件
registerTableEvent(IEventTopics.PO_BEFORE_NEW, "C_Order");
registerTableEvent(IEventTopics.PO_BEFORE_CHANGE, "C_Order");
registerTableEvent(IEventTopics.PO_BEFORE_NEW, "C_OrderLine");
registerTableEvent(IEventTopics.PO_BEFORE_CHANGE, "C_OrderLine");
// 對於文件事件,在文件表格上註冊
registerTableEvent(IEventTopics.DOC_BEFORE_COMPLETE, "C_Order");
}
您也可以使用不帶表格名稱的 registerEvent() 來接收所有表格的事件,但這在生產程式碼中很少適用。
在處理器中存取 PO 資料
getPO(event) 方法返回正在儲存、刪除或處理的持久物件。PO 類別提供豐富的 API 來存取和修改記錄資料。
讀取欄位值
PO po = getPO(event);
// 依欄位名稱取得值(返回 Object)
Object value = po.get_Value("Name");
// 取得型別化的值
String name = po.get_ValueAsString("Name");
int bPartnerId = po.get_ValueAsInt("C_BPartner_ID");
boolean isActive = po.get_ValueAsBoolean("IsActive");
java.math.BigDecimal amount = (java.math.BigDecimal)
po.get_Value("GrandTotal");
java.sql.Timestamp date = (java.sql.Timestamp)
po.get_Value("DateOrdered");
// 取得表格 ID 和記錄 ID
int tableId = po.get_Table_ID();
int recordId = po.get_ID();
// 取得表格名稱
String tableName = po.get_TableName();
修改欄位值(僅限 BEFORE 事件)
在 BEFORE 事件中,您可以在值持久化之前修改欄位值:
// 設定欄位值(僅在 BEFORE 事件中有效)
po.set_ValueOfColumn("Description",
"Auto-generated on " + new java.sql.Timestamp(
System.currentTimeMillis()));
// 對於型別化的模型類別,您可以轉型並使用 setter
if (po instanceof org.compiere.model.MOrder) {
org.compiere.model.MOrder order = (org.compiere.model.MOrder) po;
order.setDescription("Auto-generated");
}
檢查是否為新記錄
// 檢查記錄是否正在建立(vs 更新)
boolean isNew = po.is_new();
// 或者,檢查事件主題
if (event.getTopic().equals(IEventTopics.PO_BEFORE_NEW)) {
// 這是一個新記錄
}
使用 addError() 防止儲存
BEFORE 事件處理器最強大的功能之一是能夠防止儲存操作。呼叫 addErrorMessage() 會停止儲存並向使用者顯示錯誤。
@Override
protected void doHandleEvent(Event event) {
PO po = getPO(event);
String topic = event.getTopic();
if (IEventTopics.PO_BEFORE_NEW.equals(topic)
|| IEventTopics.PO_BEFORE_CHANGE.equals(topic)) {
// 驗證:銷售訂單必須有採購訂單參考
if ("C_Order".equals(po.get_TableName())) {
boolean isSOTrx = po.get_ValueAsBoolean("IsSOTrx");
String poReference = po.get_ValueAsString("POReference");
if (isSOTrx &&
(poReference == null || poReference.trim().isEmpty())) {
addErrorMessage(event,
"銷售訂單需要採購訂單參考號碼。"
+ "請輸入客戶的採購訂單號碼。");
return;
}
}
}
}
當呼叫 addErrorMessage() 時,儲存操作會被中止,資料庫交易會被回滾,使用者會在 UI 中看到錯誤訊息。這是在 iDempiere 外掛中實作伺服器端驗證的標準方式。
比較新舊值
在 BEFORE_CHANGE 和 AFTER_CHANGE 事件中,您經常需要知道特定欄位是否被修改以及其先前的值。PO 類別為此提供了幾個方法:
@Override
protected void doHandleEvent(Event event) {
PO po = getPO(event);
if (IEventTopics.PO_BEFORE_CHANGE.equals(event.getTopic())) {
// 檢查特定欄位是否被變更
if (po.is_ValueChanged("CreditLimit")) {
// 取得舊值
java.math.BigDecimal oldLimit = (java.math.BigDecimal)
po.get_ValueOld("CreditLimit");
// 取得新值
java.math.BigDecimal newLimit = (java.math.BigDecimal)
po.get_Value("CreditLimit");
logger.info("信用額度從 "
+ oldLimit + " 變更為 " + newLimit
+ ",業務夥伴 " + po.get_ValueAsString("Name"));
// 驗證:信用額度增加不能超過 50%
if (oldLimit != null && oldLimit.signum() > 0) {
java.math.BigDecimal maxAllowed = oldLimit.multiply(
new java.math.BigDecimal("1.5"));
if (newLimit.compareTo(maxAllowed) > 0) {
addErrorMessage(event,
"信用額度增加超過 50%。"
+ "最大允許值:" + maxAllowed
+ "。請申請主管核准。");
}
}
}
}
}
is_ValueChanged() 方法
is_ValueChanged(String columnName) 方法在指定欄位的值在當前儲存操作中被修改時返回 true。這對以下情況至關重要:
- 當不相關的欄位變更時避免不必要的處理
- 僅在特定的業務關鍵欄位被修改時觸發邏輯
- 建立只擷取實際變更的稽核軌跡
事件處理器優先順序和排序
當多個事件處理器訂閱相同的事件主題和表格時,它們的執行順序很重要。iDempiere 使用 OSGi 服務排名機制來決定順序。
<!-- 在 component.xml 中設定服務排名 -->
<property name="service.ranking" type="Integer" value="100"/>
排名值較高的處理器先執行。如果未指定排名,預設值為 0。負排名在預設排名的處理器之後執行。
優先順序排序的使用案例:
- 高優先順序(200+):驗證處理器,應在其他處理器處理之前拒絕無效資料
- 一般優先順序(0-100):自動設定值或建立相關記錄的業務邏輯處理器
- 低優先順序(負值):稽核/日誌記錄處理器,應在所有其他處理器完成工作後最後執行
實作範例:儲存時自動設定欄位值
讓我們建立一個完整的實用事件處理器,示範幾種技術。此處理器根據業務規則自動填充銷售訂單行的欄位:
package com.example.orderplugin.event;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.logging.Level;
import org.adempiere.base.event.AbstractEventHandler;
import org.adempiere.base.event.IEventTopics;
import org.compiere.model.MBPartner;
import org.compiere.model.PO;
import org.compiere.util.CLogger;
import org.compiere.util.DB;
import org.compiere.util.Env;
import org.osgi.service.event.Event;
public class OrderLineEventHandler extends AbstractEventHandler {
private static final CLogger logger =
CLogger.getCLogger(OrderLineEventHandler.class);
@Override
protected void initialize() {
registerTableEvent(IEventTopics.PO_BEFORE_NEW, "C_OrderLine");
registerTableEvent(IEventTopics.PO_BEFORE_CHANGE, "C_OrderLine");
}
@Override
protected void doHandleEvent(Event event) {
PO po = getPO(event);
try {
applyCustomPricing(po, event);
validateMinimumQuantity(po, event);
} catch (Exception e) {
logger.log(Level.SEVERE,
"Error in OrderLineEventHandler", e);
addErrorMessage(event,
"處理訂單行時發生錯誤:"
+ e.getMessage());
}
}
// ... 方法實作與英文版相同
}
處理文件事件
文件事件遵循與持久化事件相同的模式,但它們在文件處理期間(完成、作廢、關閉等)而非記錄儲存/刪除期間觸發。
除錯事件處理器
當事件處理器的行為不如預期時,使用這些除錯技術:
驗證元件是否啟動
連接到 OSGi 主控台並檢查您的元件狀態:
# 列出所有 DS 元件
scr:list
# 顯示特定元件的詳細資訊
scr:info com.example.orderplugin.event.OrderLineEventHandler
添加診斷日誌
在處理器的進入點添加日誌以確認事件正在被接收。
檢查事件主題匹配
常見的錯誤是註冊了錯誤的事件主題。請仔細檢查:
- component.xml 中的主題字串是否符合預期(請記住
org/adempiere/base/event/前綴使用正斜線而非點) initialize()方法是否註冊了正確的主題和表格名稱組合- 您是否在監聽正確的事件類型(PO_BEFORE_NEW vs PO_BEFORE_CHANGE vs DOC_BEFORE_COMPLETE)
例外處理
始終用 try-catch 區塊包裝處理器邏輯。事件處理器中未處理的例外可能導致不可預測的行為。
最佳實務
- 保持處理器專注。每個處理器應處理一個表格或一個業務關注點。不要建立一個「萬能」處理器來處理所有表格。
- 檢查 is_ValueChanged()。在執行昂貴的操作(資料庫查詢、外部 API 呼叫)之前,驗證相關欄位是否確實變更。
- 處理 null 值。PO 欄位值可以是 null。在對返回的值執行操作之前,始終檢查 null。
- 使用相同的交易。在 AFTER 事件中建立或修改相關記錄時,使用相同的交易名稱(
po.get_TrxName())以確保原子性。 - 避免無限迴圈。如果您的 BEFORE_CHANGE 處理器呼叫
po.set_ValueOfColumn(),請注意如果再次儲存 PO,這可能觸發另一個 BEFORE_CHANGE 事件。使用旗標或檢查is_ValueChanged()來中斷迴圈。 - 審慎記錄日誌。對重要的業務事件使用
Level.INFO,對診斷細節使用Level.FINE。在高頻處理器中過多的 INFO 日誌會淹沒伺服器日誌。
重點摘要
- iDempiere 的事件系統在記錄持久化(PO_BEFORE_NEW、PO_AFTER_CHANGE 等)和文件處理(DOC_BEFORE_COMPLETE 等)的定義生命週期點觸發同步事件。
- 使用
AbstractEventHandler作為基礎類別 — 它提供registerTableEvent()、getPO()和addErrorMessage()便利方法。 - BEFORE 事件允許您驗證資料、防止儲存(使用
addErrorMessage())和修改欄位值。AFTER 事件用於依賴儲存已成功的後續動作。 - 使用
is_ValueChanged()和get_ValueOld()來偵測和回應特定欄位變更,避免不必要的處理。 - 在 component.xml 中使用
service.ranking控制執行順序 — 較高的值先執行。 - 始終用 try-catch 區塊包裝處理器邏輯,並在除錯時在 OSGi 主控台中驗證元件啟動。
下一步
事件處理器對現有資料的變更做出反應。在下一課中,您將學習建立執行批次操作的自訂流程和提供專門使用者介面的自訂表單 — 這是 iDempiere 外掛開發者的另外兩個主要擴充點。
日本語翻訳
概要
- 学習内容:
- iDempiere の完全なイベントシステムアーキテクチャ(永続化イベントとドキュメントイベントのすべての IEventTopics 定数を含む)
- AbstractEventHandler を使用したイベントハンドラの実装方法(テーブル固有のフィルタリング、データアクセス、エラー処理を含む)
- 新旧値の比較方法、イベント優先度と実行順序の制御、開発時のイベントハンドラのデバッグ方法
- 前提条件:第1〜21課(特に第21課:初めてのプラグイン作成)
- 推定読了時間:24分
はじめに
前の課では、ビジネスパートナーが作成されたときにメッセージをログに記録する簡単なイベントハンドラを持つプラグインを作成しました。実際の開発では、イベントハンドラは iDempiere でカスタムビジネスロジックを実装するための主要なツールです。検証ルールの適用、フィールド値の自動計算、関連レコード間のデータ同期、通知のトリガー、外部システムとの統合に使用されます。
この課はモデルイベントシステムの包括的なガイドです。すべてのイベントトピックを調べ、イベントハンドラ内で利用可能な完全な API を探り、新旧値を比較するテクニックを学び、実際のビジネスシナリオを示す実践的な例を構築します。この課の終わりまでに、iDempiere プラグインで本番品質のイベント駆動ロジックを実装できるようになります。
イベントシステムアーキテクチャ
iDempiere のイベントシステムは、OSGi Event Admin サービスの上に構築されています。コアシステムが Persistent Object(PO)に対して操作を実行するとき — 保存、削除、ドキュメントの処理など — 特定のライフサイクルポイントでイベントを発火します。プラグインはこれらのイベントを購読し、影響を受けるデータへの完全なアクセスを持つコールバックを受け取ります。
アーキテクチャには3つの主要コンポーネントがあります:
- イベントプロデューサー — iDempiere コア(具体的には PO 基底クラスと Document Engine)が定義されたライフサイクルポイントでイベントを発火します。
- イベントバス —
IEventManagerサービス(OSGi Event Admin をラップ)がトピックマッチングに基づいて登録済みハンドラにイベントをルーティングします。 - イベントコンシューマー — OSGi サービスとして登録されたプラグインのイベントハンドラがイベントを受信して処理します。
イベントは同期的です — イベントプロデューサーはすべてのハンドラが完了するまで待機します。これは重要で、ハンドラが保存を防止(エラーの追加による)したり、データを変更(PO のフィールド値の変更による)でき、操作が完了する前に変更が反映されることを意味します。
IEventTopics:完全なイベントリファレンス
org.adempiere.base.event.IEventTopics インターフェースは、利用可能なすべてのイベントトピックの定数を定義します。これらは2つのカテゴリに分かれます:永続化イベント(レコードの保存/削除時に発火)とドキュメントイベント(ドキュメント処理中に発火)。
永続化イベント
これらのイベントは、レコードがデータベースに保存または削除される時に発火します:
| 定数 | トピック文字列 | 発火タイミング |
|---|---|---|
PO_BEFORE_NEW |
org/adempiere/base/event/PO_BEFORE_NEW |
新しいレコードがデータベースに挿入される前。レコードは検証済みだがまだコミットされていない。 |
PO_AFTER_NEW |
org/adempiere/base/event/PO_AFTER_NEW |
新しいレコードが正常に挿入された後。データベーストランザクションはまだコミットされていない(同じトランザクション内)。 |
PO_BEFORE_CHANGE |
org/adempiere/base/event/PO_BEFORE_CHANGE |
既存のレコードが更新される前。PO には新しい値が含まれ、元の値は get_ValueOld() でアクセス可能。 |
PO_AFTER_CHANGE |
org/adempiere/base/event/PO_AFTER_CHANGE |
既存のレコードが正常に更新された後。 |
PO_BEFORE_DELETE |
org/adempiere/base/event/PO_BEFORE_DELETE |
レコードが削除される前。エラーを追加して削除を防止できる。 |
PO_AFTER_DELETE |
org/adempiere/base/event/PO_AFTER_DELETE |
レコードが削除された後(ただしトランザクションはまだコミットされていない)。 |
ドキュメント処理イベント
これらのイベントはドキュメント処理ライフサイクル中に発火します(準備、完了、無効化、クローズ、逆仕訳など):
| 定数 | 発火タイミング |
|---|---|
DOC_BEFORE_PREPARE |
ドキュメントが準備フェーズに入る前(完了前の検証) |
DOC_AFTER_PREPARE |
準備フェーズが正常に完了した後 |
DOC_BEFORE_COMPLETE |
ドキュメントが完了する前(最終処理) |
DOC_AFTER_COMPLETE |
ドキュメントが正常に完了した後 |
DOC_BEFORE_VOID |
ドキュメントが無効化される前 |
DOC_AFTER_VOID |
ドキュメントが無効化された後 |
DOC_BEFORE_CLOSE |
ドキュメントがクローズされる前 |
DOC_AFTER_CLOSE |
ドキュメントがクローズされた後 |
DOC_BEFORE_REVERSECORRECT |
ドキュメントの逆仕訳修正の前 |
DOC_AFTER_REVERSECORRECT |
ドキュメントの逆仕訳修正の後 |
DOC_BEFORE_REVERSEACCRUAL |
ドキュメントの逆仕訳見越の前 |
DOC_AFTER_REVERSEACCRUAL |
ドキュメントの逆仕訳見越の後 |
DOC_BEFORE_REACTIVATE |
ドキュメントが完了状態から再有効化される前 |
DOC_AFTER_REACTIVATE |
ドキュメントが再有効化された後 |
BEFORE イベントと AFTER イベントの選択
BEFORE イベントと AFTER イベントの選択は、何をする必要があるかによって決まります:
- BEFORE イベントを使用する場合:データを検証して操作を防止する必要がある場合(エラーの追加による)、またはデータベースに保存される前にフィールド値を変更する必要がある場合。
- AFTER イベントを使用する場合:操作が成功したことに依存するフォローアップアクションを実行する必要がある場合(関連レコードの作成、通知の送信、カウンターの更新)、または新しいレコードの生成された主キー ID にアクセスする必要がある場合。
イベントハンドラの実装
iDempiere でイベントハンドラに推奨される基底クラスは org.adempiere.base.event.AbstractEventHandler です。便利なメソッドを提供し、登録のボイラープレートを処理します。
AbstractEventHandler API
public abstract class AbstractEventHandler implements EventHandler {
// 特定のイベントに登録するためにオーバーライド
protected abstract void initialize();
// イベントを処理するためにオーバーライド
protected abstract void doHandleEvent(Event event);
// 特定のテーブルのイベントに登録
protected void registerTableEvent(String topic, String tableName);
// すべてのテーブルのイベントに登録
protected void registerEvent(String topic);
// イベントから PO(Persistent Object)を取得
protected PO getPO(Event event);
// 操作を防止するエラーメッセージを追加
protected void addErrorMessage(Event event, String errorMessage);
// イベントプロパティを設定
protected void setEventProperty(Event event, String key, Object value);
}
テーブル固有のイベントフィルタリング
registerTableEvent() メソッドはイベントをフィルタリングし、ハンドラが指定されたテーブルのイベントのみを受信するようにします。これはパフォーマンスの最適化とセーフティの両方です。
ハンドラ内の PO データへのアクセス
getPO(event) メソッドは、保存、削除、または処理されている Persistent Object を返します。PO クラスはレコードデータのアクセスと変更のための豊富な API を提供します。
addError() による保存の防止
BEFORE イベントハンドラの最も強力な機能の1つは、保存操作を防止する能力です。addErrorMessage() を呼び出すと保存が停止し、ユーザーにエラーが表示されます。
新旧値の比較
BEFORE_CHANGE および AFTER_CHANGE イベントでは、特定のフィールドが変更されたかどうか、以前の値が何であったかを知る必要があることがよくあります。PO クラスはこのためのいくつかのメソッドを提供します。
is_ValueChanged() メソッド
is_ValueChanged(String columnName) メソッドは、指定されたカラムの値が現在の保存操作で変更された場合に true を返します。これは以下の場合に不可欠です:
- 無関係なフィールドが変更された場合の不要な処理を回避
- 特定のビジネスクリティカルなフィールドが変更された場合にのみロジックをトリガー
- 実際の変更のみをキャプチャする監査証跡の構築
イベントハンドラの優先度と順序
複数のイベントハンドラが同じイベントトピックとテーブルを購読している場合、実行順序が重要です。iDempiere は OSGi サービスランキングメカニズムを使用して順序を決定します。
<!-- component.xml でサービスランキングを設定 -->
<property name="service.ranking" type="Integer" value="100"/>
ランキング値が高いハンドラが先に実行されます。ランキングが指定されていない場合、デフォルトは 0 です。
ベストプラクティス
- ハンドラを集中させる。各ハンドラは1つのテーブルまたは1つのビジネス上の関心事を処理すべきです。
- is_ValueChanged() を確認する。高コストな操作を実行する前に、関連するフィールドが実際に変更されたことを確認します。
- null 値を処理する。PO フィールド値は null になる可能性があります。返された値に対して操作を実行する前に、常に null をチェックしてください。
- 同じトランザクションを使用する。AFTER イベントで関連レコードを作成または変更する場合、原子性を確保するために同じトランザクション名(
po.get_TrxName())を使用します。 - 無限ループを回避する。BEFORE_CHANGE ハンドラが
po.set_ValueOfColumn()を呼び出す場合、PO が再度保存されると別の BEFORE_CHANGE イベントがトリガーされる可能性があります。フラグを使用するかis_ValueChanged()をチェックしてループを中断してください。 - ログは慎重に。重要なビジネスイベントには
Level.INFOを、診断の詳細にはLevel.FINEを使用してください。
重要なポイント
- iDempiere のイベントシステムは、レコード永続化(PO_BEFORE_NEW、PO_AFTER_CHANGE など)およびドキュメント処理(DOC_BEFORE_COMPLETE など)の定義されたライフサイクルポイントで同期イベントを発火します。
AbstractEventHandlerを基底クラスとして使用してください —registerTableEvent()、getPO()、addErrorMessage()の便利なメソッドを提供します。- BEFORE イベントではデータの検証、保存の防止(
addErrorMessage()を使用)、フィールド値の変更が可能です。AFTER イベントは保存が成功したことに依存するフォローアップアクション用です。 is_ValueChanged()とget_ValueOld()を使用して特定のフィールド変更を検出し応答し、不要な処理を回避します。- component.xml の
service.rankingで実行順序を制御します — 高い値が先に実行されます。 - 常にハンドラロジックを try-catch ブロックでラップし、デバッグ時は OSGi コンソールでコンポーネントのアクティベーションを確認してください。
次のステップ
イベントハンドラは既存データの変更に反応します。次の課では、バッチ操作を実行するカスタムプロセスと、特殊なユーザーインターフェースを提供するカスタムフォームの作成方法を学びます — iDempiere プラグイン開発者にとっての残りの2つの主要な拡張ポイントです。