Extension Points and Factories
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:
- The core code requests all registered implementations of a factory interface from the OSGi service registry.
- Implementations are sorted by the
service.rankingproperty (higher values come first). If two services have the same ranking, the one with the lowerservice.id(registered earlier) takes precedence. - The core iterates through the sorted list, calling the factory method on each implementation.
- The first factory that returns a non-null result wins — its result is used, and the remaining factories are skipped.
- 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.rankingvalues 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:
- A custom model class with additional business logic
- A callout that auto-fills the credit limit based on partner category
- 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), andIModelValidatorFactory(model validators). - Factories return null to defer to the next registered factory — this is how multiple plugins coexist, each handling specific cases.
- The
service.rankingproperty controls priority ordering. Higher values are consulted first. - Event handlers (using
AbstractEventHandlerandIEventTopics) 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.