aiM18 Developer Center aiM18 Developer Center
DOC Home
  • Platform

    • Overview
    • Frontend Framework
    • Backend Framework
    • EBI Development
    • JSF Components
    • BPM Extension Interface
    • Telescope Extension Interface
    • Mobile App Setup
  • ERP

    • ERP EJB Interfaces
    • ERP XML Config Files
    • ERP Decorators (Frontend Interfaces)
    • ERP Group ERP Extension
  • Tutorial

    • Setup Development Environment
    • Sample App
  • Platform

    • Authentication
    • Common JSON Objects
    • Core Services
    • EBI Services
  • Enterprise Resource Planning
  • Human Capital Management
  • Business Process Management
  • Schedule Management
  • Document Management
  • Tutorial

    • Interacting with aiM18 via webservices
Tutorial
GitHub (opens new window)
DOC Home
  • Platform

    • Overview
    • Frontend Framework
    • Backend Framework
    • EBI Development
    • JSF Components
    • BPM Extension Interface
    • Telescope Extension Interface
    • Mobile App Setup
  • ERP

    • ERP EJB Interfaces
    • ERP XML Config Files
    • ERP Decorators (Frontend Interfaces)
    • ERP Group ERP Extension
  • Tutorial

    • Setup Development Environment
    • Sample App
  • Platform

    • Authentication
    • Common JSON Objects
    • Core Services
    • EBI Services
  • Enterprise Resource Planning
  • Human Capital Management
  • Business Process Management
  • Schedule Management
  • Document Management
  • Tutorial

    • Interacting with aiM18 via webservices
Tutorial
GitHub (opens new window)
  • platform

    • Overview
    • Frontend Framework
    • Backend Framework
    • EBI Development
    • JSF Components
    • BPM Extension Interface
    • Telescope Extension Interface
    • Mobile App Setup
  • erp

    • ERP EJB Interfaces
    • ERP XML Config Files
    • ERP Decorators (Frontend Interfaces)
    • ERP Group ERP Extension
      • Overview
        • Deployment Configuration
        • Related Modules
        • Sync Methods
      • Configuration Parameters
        • module.xml params
        • navmenu.xml params
      • Common Prerequisite: Cross-Server ID Alignment
      • Method 1: Record Sync (FM Modules)
        • 1. Declare whether the module joins the sync
        • 2. Adjust the synchronized field scope
        • 3. Customization: grouperpSyncHandler
        • 4. Customization: DataSwapHandler / SearchCodeHandler
      • Method 2: Object Sync (Configuration Modules)
        • 1. Review your ObjectHandler
        • 2. Customization: grouperpSyncHandler
      • Method 3: Event Sync (Event)
        • 1. Write the Event class
        • 2. Write the view Listener
        • 3. Register the menu
      • Debugging and Troubleshooting
        • Integration test steps
        • Debug mode
        • Common problems
  • tutorial

    • Setup Development Environment
    • Sample App
  • App
  • erp
Multiable
2026-08-20
目录

ERP Group ERP Extension

Version: 1.0 | Release Date: 20/08/2026

# Overview

Group ERP (app name ce01_grouperp) synchronizes configuration across several independently deployed aiM18 systems: one master server owns the creation, update and deletion of master-file / configuration modules, and pushes them over REST + OAuth2 to N sub-servers. On a sub-server those modules become read-only — the menu is locked and offers a "Go to master server" shortcut instead.

Item Description
Direction One-way: master server → sub-server
Deployment Independent systems with independent databases

# Deployment Configuration

The role of a server is decided by its WildFly deployment configuration:

Configuration Description
caw.grouperp.master This machine is the master server
caw.grouperp.slave This machine is a sub-server
caw.grouperp.gerpKey Identity code of this machine, unique per server. A request whose key does not match the local one is rejected

In code, read them through GrouperpUtil.isMasterServer() / isSlaveServer() / getGerpKey().


# Related Modules

Menu Module / Table Description
[Group ERP Server Setup] grouperpServer Registers the address, server code and integration account of every server in the group and verifies each connection; also assigns the business entities each server is responsible for
[Group ERP Module Setup] grouperpSetting Lists every module and menu that joins the sync and marks whether each one uses Excel / Object / Event; lets you configure a special setting per module
[Group ERP Sync Error Log] grouperpaudittrail Failed synchronizations, including the error returned by the far side and the payload actually sent; entries can be selected and retried
[Group ERP Sync Trail Log] grouperpauditdata Successful synchronizations; entries can be selected and re-sent to a chosen sub-server

# Sync Methods

Method Applies to Underlying mechanism What the 3PD App writes
Record Sync FM modules (standard master-file / configuration modules) [Data Export] on the master → [Data Import] on the sub-server Usually just a declaration in module.xml; a Handler only when something special is needed
Object Sync Configuration modules that implement DataObjectHandler [Create Objects] on the master → [Install Objects] on the sub-server Review and adapt your own ObjectHandler
Event Sync View button actions, and configuration that is not a module Reflective call of the class and method you specify An Event class + a view Listener


# Configuration Parameters

# module.xml params

key Value Applies to Description
grouperpSyncAddFm true Record / Object Make this module join the sync
grouperpSyncSkipFm true Record / Object Exclude this module from the sync
grouperpSyncLenientTable Table names separated by ; Record Pull a table the framework would not import into the sync scope
grouperpSyncLenientField table.column separated by ; Record Pull a column the framework would not import into the sync scope
grouperpSyncHandler Fully qualified class name Record / Object Custom sync handler class for this module
grouperpSettingXhtml Path to an xhtml All Special-setting dialog for this module in [Group ERP Module Setup]

# navmenu.xml params

key Value Applies to Description
grouperpSyncAddFm true Event Make this menu join the event sync
grouperpSyncSkipFm true Event Exclude this menu from the event sync


# Common Prerequisite: Cross-Server ID Alignment

The master server and a sub-server are two independent databases, so the auto-increment id of the same record differs on both sides. All three sync methods rest on the same premise: the code is used to look up the local id on the far side.

A module that joins the sync must therefore satisfy:

  • the main table has a code column, and that code points to the same business entity on both sides — it must not be produced by a local running number, must not contain a local id, and must not be rewritten locally after a sync;
  • every master file referenced by a foreign key must already exist on the sub-server under the same code, otherwise the sync reports cannot find lookup code.

When a module has no natural code (a one-to-one setting table, for example), build a stable one. Common practice is code = the module name (singleton table), or code = the code of the owning business entity (one row per BE).

Besides the code lookup, the system automatically adds a hidden column gerpSyncSId to the main table of every module that joins the sync. It holds the id the record has on the master server and is used to locate records on delete and to build the "Go to master server" link. A 3PD App neither declares nor writes this column.



# Method 1: Record Sync (FM Modules)

Applies to FM modules — the standard master-file / configuration modules. All FM modules are synchronized by default, with no code to write.

The mechanism is equivalent to a [Data Export] on the master server followed by a [Data Import] on the sub-server: the master exports the whole record to xlsx (plus an optional 7z attachment package) and pushes it to the sub-server, which saves it through the standard import flow. The Checkers of the module itself run as usual.


# 1. Declare whether the module joins the sync

A declaration in the module.xml of your own app is enough:

    <!-- make this module join the group sync -->
    <module name="myBaseData" mess="my3pd.myBaseData" mainTable="mybasedata" fmShare="N">
        <table name="mybasedata" key="code" initRow="1"/>
        <param key="grouperpSyncAddFm" value="true"/>
    </module>

    <!-- keep this module out of the group sync -->
    <module name="myLocalConf" mess="my3pd.myLocalConf" mainTable="mylocalconf">
        <table name="mylocalconf" key="code" initRow="1"/>
        <param key="grouperpSyncSkipFm" value="true"/>
    </module>

# 2. Adjust the synchronized field scope

Because the transport is [Data Export] / [Data Import], the fields that can be synchronized are exactly the fields those two functions allow. A table or column marked dataImport="false" is not synchronized.

If the 3PD App has fields that must stay identical to the master server but that the framework does not allow to be imported, pull them in with a white list:

    <module name="myBaseData" extend="true">
        <param key="grouperpSyncLenientTable" value="mybasedataext"/>
        <param key="grouperpSyncLenientField" value="mybasedataext.adminFlag;mybasedataext.apiKey"/>
    </module>
param Syntax Description
grouperpSyncLenientTable table;table Pulls a whole table into the sync scope
grouperpSyncLenientField table.column;table.column Column level, no wildcard support

Security note: white-listed fields travel to every sub-server over HTTP inside the sync payload. Never white-list a plain-text key or password.


# 3. Customization: grouperpSyncHandler

When the default export / import behaviour is not enough — an extra lookup code has to travel, the field set has to be trimmed according to a setting, or a foreign key has to be re-mapped before saving — give the module a custom handler class:

    <module name="myBaseData" extend="true">
        <param key="grouperpSyncHandler" value="com.my3pd.erp.handler.MySyncHandler"/>
    </module>

At start-up the class is instantiated and cached per module. During a sync the system uses reflection to look for a method with exactly the same name and signature and calls it. Implement only what you need; no interface has to be implemented. The Excel-related hook points are:

Signature Side Purpose
void handlerExportExcel(DataExportConfig config, SqlEntity entity) Master Add or remove synchronized fields, or put values into getParamMap()
void handlerImportExcel(DataImportConfig config, WorkbookMapping mapping, Long beId, String moduleName) Sub The counterpart of the above: trim imported fields, read the parameters back
void handlerImportExcelEntity(DataImportConfig config, SqlEntity entity, Long beId) Sub Final adjustment of the entity before it is saved
void handlerSyncDto(SeSaveParam param, GrouperpSyncDto syncDto) Master Add custom information after the payload is assembled (shared by Excel / Object)
void updateSyncDto(GrouperpSyncDto syncDto) Sub Translate a code into the local id before saving (shared by Excel / Object)

Example: the main table of module myBaseData has a foreign key myTypeId. The id differs on both sides, so the code has to travel instead.

package com.my3pd.erp.handler;

public class MySyncHandler {

	/** master server: put the code of the foreign key into the payload */
	public void handlerExportExcel(DataExportConfig config, SqlEntity entity) {
		long myTypeId = entity.getMainData().getLong(1, "myTypeId");
		StLookupDto dto = StLookupLib.getDto("myType", myTypeId);
		if (dto != null) {
			config.getParamMap().put("myTypeCode", dto.getCode());
		}
	}

	/** sub-server: translate the code into the local id */
	public void handlerImportExcelEntity(DataImportConfig config, SqlEntity entity, Long beId) {
		String code = ConvertLib.toString(config.getParamMap().get("myTypeCode"));
		if (!code.isEmpty()) {
			entity.getMainData().setValue(1, "myTypeId", GrouperpLookupLib.getIdByCode("myType", code));
		}
	}
}

Type pitfall: paramMap is a Map<String, Object>, and its values change type once they have been through JSON (a Java object becomes a JSONObject, a number may become a string). Always write the put and the get as a pair, read values through ConvertLib, and never cast directly.


# 4. Customization: DataSwapHandler / SearchCodeHandler

grouperpSyncHandler only affects a single module. The following handlers work across modules:

	// register in a serverBoot (com.multiable.core.share.handler.EntityUtil)
	EntityUtil.addDataSwap(new MyDataSwapHandler());     // implements DataSwapHandler
	EntityUtil.addSearchCode(new MySearchCodeHandler()); // implements SearchCodeHandler
Interface Purpose
DataSwapHandler Adds or removes virtual columns during export / import to swap between an id and a business key
SearchCodeHandler Takes over the "code to local id" lookup


# Method 2: Object Sync (Configuration Modules)

Applies to configuration modules that implement DataObjectHandler. Such modules are synchronized by default.

The mechanism is equivalent to a [Create Objects] on the master server followed by an [Install Objects] on the sub-server. The key difference from Method 1 is on the landing side: instead of [Data Import], the system calls the 3PD App's own ObjectHandler.install(...).


# 1. Review your ObjectHandler

Review the 3PD App's own ObjectHandler — a group sync has to follow a path that re-maps foreign keys by code:

DataObject param Description
grouperpSyncFlag true means this installation comes from a group sync, not from a manual installation by a user
grouperpSyncCheck true means this is the dry-run stage of the sync and must not cause side effects (writing to the database, sending a notification or an email)
	// branch inside ObjectHandler.install(...)
	boolean syncFlag  = ConvertLib.toBoolean(ErpDataObjectLib.getParamValue(dsDto, "grouperpSyncFlag"));
	boolean syncCheck = ConvertLib.toBoolean(ErpDataObjectLib.getParamValue(dsDto, "grouperpSyncCheck"));

	// manual installation keeps the original logic; a group sync re-maps foreign keys by code
	boolean status = !syncFlag ? genFkTableRecord(entity) : grouperpSyncSave(entity, syncCheck);

A typical grouperpSyncSave looks up the local id by code in the local cache table, replaces the foreign keys on the entity one by one, and skips every write when syncCheck is true.


# 2. Customization: grouperpSyncHandler

The same grouperpSyncHandler param as in Method 1; only the hook points differ:

Signature Side Purpose
void handlerExportObject(Long beId, String moduleName, DataObjectBaseDto dto) Master Append parameters to the data object
void handlerImportObject(Long beId, String moduleName, DataObjectBaseDto dto, List<DsInstallDto> insList, Object detail) Sub Re-map before installation, or keep the local value of the sub-server
void handlerSyncDto(SeSaveParam param, GrouperpSyncDto syncDto) Master Add custom information after the payload is assembled (shared by Excel / Object)
void updateSyncDto(GrouperpSyncDto syncDto) Sub Translate a code into the local id before saving (shared by Excel / Object)


# Method 3: Event Sync (Event)

Applies to actions triggered by a view button, and to configuration that is not a standard module (data stored in your own tables, or an API that has to be called on the sub-server).

An event sync does not carry data tables; it carries an instruction: the master server tells the sub-server which class to instantiate, which method to call and what payload to pass, and the sub-server performs a reflective call. A 3PD App writes two things: an Event class and a view Listener.


# 1. Write the Event class

One class covers both sides: the static method starts the sync on the master server, the instance method lands it on the sub-server.

package com.my3pd.erp.share.event;

import java.util.List;

import com.multiable.core.share.lib.ConvertLib;
import com.multiable.core.share.message.CheckMsg;
import com.multiable.core.share.message.CheckMsgLib;
import com.multiable.erp.grouperp.share.entity.GrouperpEvent;
import com.multiable.erp.grouperp.share.lib.GrouperpEJBLib;
import com.multiable.logging.CawLog;

public class MySettingEvent {

	/** master server: start the event */
	public static List<CheckMsg> syncMySetting(String settingValue) {
		GrouperpEvent event = new GrouperpEvent();
		event.setEjbClass(MySettingEvent.class.getName());   // fully qualified handler class
		event.setEjbMethod("handlerMySetting");              // handler method name
		event.setData(settingValue);                         // payload
		event.setMenuCode("mySetting");                      // shown in the sync log
		event.setActionMess("core.save");                    // messCode of the action
		return GrouperpEJBLib.getCommonEJB().syncEvent(event);
	}

	/** sub-server: handle the event */
	public CheckMsg handlerMySetting(Object data) {
		CheckMsg msg = null;
		try {
			MySettingLib.save(ConvertLib.toString(data));
		} catch (Exception e) {
			CawLog.logException(e);
			msg = CheckMsgLib.createErrorMsg(e.toString());
		}
		return msg;
	}
}

GrouperpEvent fields

Name Type Description Required
ejbClass String Fully qualified name of the class to instantiate on the sub-server Y
ejbMethod String Name of the method to call Y
data Object Payload, serialized to JSON before it is sent Y
moduleName String Module name, used in the sync log and the alert text N
menuCode String Menu code, used in the sync log and the alert text N
actionMess String messCode of the action, shown in the Operation column of the sync log N
dto StLookupDto Points at a specific record (id + code) so the sync log can locate it N
user UserDto Operator, taken from the current user on construction; the sub-server maps it to a local uid by user code Auto

The handler method is invoked by reflection, so its shape is fixed:

Requirement Description
Parameter Exactly Object — not JSONObject data and not String data
Return value Must be CheckMsg (any other type causes no error, but the failure cannot be reported back to the operator)
Location The handler class must be on the sub-server classpath, that is in p-share or p-ejb — never in p-jsf

# 2. Write the view Listener

The Listener does two things: it locks the view read-only on a sub-server, and it starts the event when the button is pressed on the master server.

To add sync behaviour to an existing FRD view, register the listener through cawweb.xml (see Backend Framework and ERP Decorators).

public class MySettingListener extends ViewBean {

	MySettingBean sourceBean = null;

	@Override
	public void initialized() {
		super.initialized();
		sourceBean = (MySettingBean) GrouperpWebUtil.getCurrentBeanInstance("mySetting");

		// lock the view read-only on a sub-server
		if (GrouperpUtil.isSlaveServer() && !UserCc.isSuper()) {
			WebUtil.setDisabled(true, "save");
		}
	}

	@Override
	public void actionPerformed(ViewActionEvent vae) {
		super.actionPerformed(vae);
		if ("save".equals(vae.getActionCommand())) {
			if (!GrouperpUtil.isMasterServer()) {   // only the master server starts a sync
				return;
			}
			GrouperpWebUtil.postMessage(MySettingEvent.syncMySetting(sourceBean.getValue()));
		}
	}
}

# 3. Register the menu

The menu has to be registered before it appears in [Group ERP Module Setup] and gets locked on a sub-server:

    <menu code="mySetting" mess="my3pd.mySetting">
        <param key="grouperpSyncAddFm" value="true"/>
    </menu>


# Debugging and Troubleshooting

# Integration test steps

A group sync can only be tested across two systems; a single machine is not enough.

  1. Prepare two independently deployed aiM18 systems (each with its own database) and deploy the same version of the 3PD App on both.
  2. Set caw.grouperp.master=true on one and caw.grouperp.slave=true on the other, and give each a different caw.grouperp.gerpKey.
  3. Register both servers in [Group ERP Server Setup] on the master and press Verify on each row until it passes.
  4. Open [Group ERP Module Setup] and confirm that the modules and menus of the 3PD App appear in the list and are marked with the expected method (Excel / Object / Event). If one is missing, it does not take part in the sync.
  5. Save a record on the master server and check the result on the sub-server.
  6. On failure look at [Group ERP Sync Error Log], on success at [Group ERP Sync Trail Log]. The eventData column of both tables holds the payload that was actually sent.

# Debug mode

With the framework debug mode enabled, both the master and the sub-server print the complete sync payload, and the temporary files are kept. You can open them directly under <jboss>/excel/ and <jboss>/cawobj/ on the master and <jboss>/sync/ on the sub-server and check the content field by field. This is the most direct way to confirm whether a Handler took effect and whether a given field really travelled.


# Common problems

Symptom Check first
Nothing is synchronized and there is no log at all The caw.grouperp.master / slave configuration
One module is never synchronized Whether grouperpSyncAddFm is declared, and whether the param key is spelled correctly
A few fields are never synchronized Those columns are dataImport="false" and have to be declared through grouperpSyncLenientField
A custom Handler is never called 1. The signature does not match (in particular beId must be the boxed Long); 2. the module itself does not take part in the sync
A foreign key points at the wrong record on the sub-server The ObjectHandler.install of an object sync does not branch on grouperpSyncFlag and used the master server id directly
The sub-server log contains a Java stack trace The 3PD App version differs between master and sub-server, or the handler parameter / return type does not follow the contract
Unmatched server code is reported The gerpKey carried by the request does not match caw.grouperp.gerpKey on the sub-server
Last Updated: 2026/08/21, 02:55:29
ERP Decorators (Frontend Interfaces)
Setup Development Environment

← ERP Decorators (Frontend Interfaces) Setup Development Environment→

Theme by Vdoing | Copyright © 1990-2026 Multiable | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式