Developing Plug-in Model Validators
๐ฏ Goalโ
This tutorial shows you how to develop model validators in your own plug-in, enabling you to add custom validation and business logic that executes during model operations.
What is a Model Validator?โ
A model validator is a component that intercepts and validates model (record) operations in iDempiere. Model validators are similar to event handlers but use a different registration mechanism. They are commonly used to:
- Validate data before records are saved or deleted
- Enforce complex business rules across multiple fields
- Calculate and set field values based on business logic
- Prevent invalid operations by throwing exceptions
- Audit and log specific model changes
- Trigger side effects when records change
Model validators and event handlers serve similar purposes. The main difference is in how they're registered and managed:
- Model Validators use the
ModelValidatorinterface andIModelValidatorFactory - Event Handlers use the OSGi Event Admin framework and
IEventManager
For new development, consider using Event Handlers as they provide more flexibility and better OSGi integration.
โ Prerequisitesโ
Before starting, review:
You should already know how to create a plug-in, as this guide will not cover that in detail.
๐ฅ Video Tutorialโ
Watch this video walkthrough:
https://www.youtube.com/watch?v=Dwjl8p1Xguw
๐ Creating a Model Validatorโ
Step 1: Create the Model Validator Classโ
Create a new class that implements the ModelValidator interface and all its required methods.
Example: MyModelValidator.java
package com.mycompany.validator;
import java.util.logging.Level;
import org.compiere.model.ModelValidator;
import org.compiere.model.PO;
import org.compiere.model.MClient;
import org.compiere.model.MOrder;
import org.compiere.util.CLogger;
public class MyModelValidator implements ModelValidator {
/** Logger */
private static CLogger log = CLogger.getCLogger(MyModelValidator.class);
/** Client ID */
private int m_AD_Client_ID = -1;
/** User ID */
private int m_AD_User_ID = -1;
/** Organization ID */
private int m_AD_Org_ID = -1;
/** Role ID */
private int m_AD_Role_ID = -1;
@Override
public void initialize(ModelValidationEngine engine, MClient client) {
// Store client ID
if (client != null)
m_AD_Client_ID = client.getAD_Client_ID();
// Register for model changes on specific tables
engine.addModelChange(MOrder.Table_Name, this);
log.info("Model Validator initialized for client: " + m_AD_Client_ID);
}
@Override
public int getAD_Client_ID() {
return m_AD_Client_ID;
}
@Override
public String login(int AD_Org_ID, int AD_Role_ID, int AD_User_ID) {
// Store login information for later use
m_AD_Org_ID = AD_Org_ID;
m_AD_Role_ID = AD_Role_ID;
m_AD_User_ID = AD_User_ID;
log.info("Login - Org: " + AD_Org_ID + ", Role: " + AD_Role_ID +
", User: " + AD_User_ID);
return null; // Return null if login is successful
}
@Override
public String modelChange(PO po, int type) throws Exception {
// Log the model change
log.info("Model Change - Table: " + po.get_TableName() +
", Type: " + type + ", ID: " + po.get_ID());
// Handle specific validation based on type
if (po instanceof MOrder && type == TYPE_BEFORE_NEW) {
MOrder order = (MOrder) po;
// Example validation: Check if Business Partner is set
if (order.getC_BPartner_ID() <= 0) {
return "Business Partner is required";
}
// Example calculation: Set a custom field
// order.set_ValueOfColumn("CustomField", calculatedValue);
}
return null; // Return null if validation passes
}
@Override
public String docValidate(PO po, int timing) {
// Handle document-level validations
log.info("Document Validate - Table: " + po.get_TableName() +
", Timing: " + timing);
// You can add document-specific validation here
return null; // Return null if validation passes
}
}
Key Methods Explainedโ
initialize()- Called when the validator is loaded. Register for table events here.getAD_Client_ID()- Returns the client ID this validator serves.login()- Called when a user logs in. Store context information here.modelChange()- Called during model operations. Add validation logic here.docValidate()- Called during document workflow operations.
Validation Typesโ
The type parameter in modelChange() can be:
TYPE_BEFORE_NEW- Before a new record is createdTYPE_AFTER_NEW- After a new record is createdTYPE_BEFORE_CHANGE- Before a record is updatedTYPE_AFTER_CHANGE- After a record is updatedTYPE_BEFORE_DELETE- Before a record is deletedTYPE_AFTER_DELETE- After a record is deleted
- Return
nullif validation passes - Return an error message string to prevent the operation and show the message to the user
- Throw an exception to halt the operation with an error
๐ง Registering Your Model Validatorโ
Using a Model Validator Factoryโ
Create a factory class that implements IModelValidatorFactory to tell iDempiere how to find your validator.
Step 1: Create the Factory Classโ
Example: MyModelValidatorFactory.java
package com.mycompany.validator;
import org.adempiere.base.IModelValidatorFactory;
import org.compiere.model.ModelValidator;
public class MyModelValidatorFactory implements IModelValidatorFactory {
@Override
public ModelValidator newModelValidatorInstance(String className) {
if (className.equals("com.mycompany.validator.MyModelValidator")) {
return new MyModelValidator();
}
return null;
}
}
Step 2: Register as OSGi Componentโ
Option A: Using Annotations (Recommended)
Add the @Component annotation to your factory class:
import org.osgi.service.component.annotations.Component;
@Component(
service = IModelValidatorFactory.class,
immediate = true,
property = {"service.ranking:Integer=100"}
)
public class MyModelValidatorFactory implements IModelValidatorFactory {
// ... implementation
}
See Event Handling with Annotations for more details.
Option B: Manual Component Definition
- Create a directory named
OSGI-INFin your project root - Create a file
OSGI-INF/modelvalidatorfactory.xml:
<?xml version="1.0" encoding="UTF-8"?>
<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0"
name="com.mycompany.validator.modelvalidatorfactory">
<implementation class="com.mycompany.validator.MyModelValidatorFactory"/>
<property name="service.ranking" type="Integer" value="100"/>
<service>
<provide interface="org.adempiere.base.IModelValidatorFactory"/>
</service>
</scr:component>
- Update your
META-INF/MANIFEST.MF:
Service-Component: OSGI-INF/*.xml
- Ensure
build.propertiesincludes the OSGI-INF directory:
bin.includes = META-INF/,\
.,\
OSGI-INF/
The service.ranking property determines the priority for OSGi service lookup. Set it to a value like 100 to ensure your factory is loaded before the default iDempiere model validator factory.
โ๏ธ Configuring Your Model Validator in iDempiereโ
Creating a Model Validator Recordโ
- Log in as System Administrator
- Open System Admin > General Rules > Model Validator
- Create a new record:
- Name: Your validator name (e.g., "My Custom Validator")
- Entity Type: Your custom entity type
- Model Validation Class: Fully qualified class name
(e.g.,com.mycompany.validator.MyModelValidator) - Description: Brief description of what the validator does
The validator will be automatically activated when the system starts.
๐งช Testing Your Model Validatorโ
- Deploy your plug-in to iDempiere
- Ensure your plug-in is set to auto-start in the Run Configuration
- Start the iDempiere server
- Create or update a record that should trigger your validator (e.g., a Sales Order)
- Check the console logs for validation messages
- Verify that validation rules are enforced
Add logging statements in your validator methods to trace execution:
log.info("Validating order: " + order.getDocumentNo());
log.warning("Validation failed: Missing required field");
๐ Model Validator vs Event Handlerโ
Both approaches serve similar purposes but have different characteristics:
| Feature | Model Validator | Event Handler |
|---|---|---|
| Registration | Via IModelValidatorFactory | Via OSGi Event Admin |
| Interface | ModelValidator interface | EventHandler interface or AbstractEventHandler |
| Flexibility | Less flexible | More flexible with topics |
| Recommended | Legacy code | New development |
| Documentation | Older API | Modern OSGi approach |
For new plug-in development, consider using Event Handlers instead, as they provide better OSGi integration and more flexibility. Model validators are still supported but are considered the legacy approach.