java-actions · diff
git:20260821.ff81a24 to git:20260822.b0cab65
12 added, 680 removed. Audit A to A.
---
name: java-actions
description: "Create and call custom Java actions — extending Mendix with server-side Java, and invoking those actions from microflows in MDL. Use when logic needs a Java library, an algorithm microflows cannot express, or an integration only available in Java."
---
# Mendix Java Actions Skill
This skill provides comprehensive guidance for creating and using custom Java actions in Mendix projects.
+ ## Reference files
+
+ `SKILL.md` covers declaring a Java action in MDL and calling it from a microflow —
+ the parts that touch the model. The Java side is next door:
+
+ - [`reference/writing-java.md`](reference/writing-java.md) — creating the action
+ in Studio Pro, writing the `executeAction` body, the Core API surface you have,
+ and the recurring patterns (HTTP calls, file handling, batch work).
+ - [`reference/best-practices.md`](reference/best-practices.md) — error handling,
+ transactions and rollback, logging, threading, security, and performance. Read
+ it before shipping a Java action that touches data or an external system.
+
## When to Use This Skill
Use this skill when:
- You need to extend Mendix with custom Java logic
- Building integrations with external Java libraries
- Implementing complex algorithms not feasible in microflows
- Calling Java actions from MDL microflows
- Debugging Java action calls
## Overview
Java actions allow you to extend Mendix with custom Java code. The workflow is:
1. **Define** the Java action in Studio Pro (parameters, return type)
2. **Implement** the Java code in Eclipse/IDE
3. **Call** the Java action from microflows using MDL
- ## Part 1: Creating Java Actions in Studio Pro
-
- ### Step 1: Create the Java Action
-
- In Studio Pro:
- 1. Right-click module in Project Explorer → **Add other** → **Java action**
- 2. Name using convention: `JA_ActionName` (e.g., `JA_CalculateTax`, `JA_SendEmail`)
- 3. Define parameters and return type
-
- ### Step 2: Define Parameters
-
- | Parameter Type | Mendix Type | Java Type |
- |----------------|-------------|-----------|
- | String | String | `java.lang.String` |
- | Integer | Integer/Long | `java.lang.Long` |
- | Decimal | Decimal | `java.math.BigDecimal` |
- | Boolean | Boolean | `java.lang.Boolean` |
- | DateTime | Date and time | `java.util.Date` |
- | Object | Entity | `IMendixObject` |
- | List | List of Entity | `java.util.List<IMendixObject>` |
- | StringTemplate(Sql) | SQL template | `com.mendix.core.objectmanagement.member.MendixObjectReference` |
- | StringTemplate(Text) | Text template | `com.mendix.core.objectmanagement.member.MendixObjectReference` |
-
- **Note:** `stringtemplate(sql)` and `stringtemplate(text)` are specialized types for parameterized SQL/OQL queries and text templates respectively.
-
- ### Step 3: Export for Eclipse
-
- 1. Menu → **App** → **Deploy for Eclipse**
- 2. Open project in Eclipse
- 3. Find Java action in `javasource/<modulename>/actions/`
-
- ## Part 2: Writing Java Action Code
-
- ### Basic Structure
-
- ```java
- package mymodule.actions;
-
- import com.mendix.systemwideinterfaces.core.IContext;
- import com.mendix.webui.CustomJavaAction;
- import com.mendix.core.Core;
- import com.mendix.systemwideinterfaces.core.IMendixObject;
-
- public class JA_CalculateTax extends CustomJavaAction<java.math.BigDecimal>
- {
- private java.math.BigDecimal amount;
- private java.math.BigDecimal taxRate;
-
- public JA_CalculateTax(IContext context, java.math.BigDecimal amount, java.math.BigDecimal taxRate)
- {
- super(context);
- this.amount = amount;
- this.taxRate = taxRate;
- }
-
- @java.lang.Override
- public java.math.BigDecimal executeAction() throws Exception
- {
- // begin user CODE
- if (this.amount == null || this.taxRate == null) {
- return java.math.BigDecimal.ZERO;
- }
-
- return this.amount.multiply(this.taxRate);
- // end user CODE
- }
- }
- ```
-
- **CRITICAL**: Only code between `// begin user CODE` and `// end user CODE` is preserved. Everything else is regenerated by Studio Pro.
-
- ### Working with Mendix Objects
-
- ```java
- @java.lang.Override
- public IMendixObject executeAction() throws Exception
- {
- // begin user CODE
- IContext context = getContext();
-
- // create a new object
- IMendixObject order = Core.instantiate(context, "Sales.Order");
- order.setValue(context, "OrderNumber", "ORD-" + System.currentTimeMillis());
- order.setValue(context, "OrderDate", new java.util.Date());
- order.setValue(context, "status", "Draft");
- order.setValue(context, "TotalAmount", java.math.BigDecimal.ZERO);
-
- // commit to database
- Core.commit(context, order);
-
- return order;
- // end user CODE
- }
- ```
-
- ### Core API Reference
-
- | Method | Description |
- |--------|-------------|
- | `Core.instantiate(context, "Module.Entity")` | Create new object |
- | `Core.commit(context, object)` | Save to database |
- | `Core.commitWithoutEvents(context, object)` | Save without triggering events |
- | `Core.delete(context, object)` | Delete object |
- | `Core.rollback(context, object)` | Discard uncommitted changes |
- | `Core.retrieveId(context, id)` | Retrieve by GUID |
- | `Core.createXPathQuery(xpath).execute(context)` | Query with XPath |
- | `Core.microflowCall(name).execute(context)` | Call microflow |
-
- ### Reading and Writing Attributes
-
- ```java
- // Reading values
- string name = (string) order.getValue(context, "Name");
- java.math.BigDecimal amount = (java.math.BigDecimal) order.getValue(context, "Amount");
- boolean isActive = (boolean) order.getValue(context, "IsActive");
- java.util.Date orderDate = (java.util.Date) order.getValue(context, "OrderDate");
-
- // Writing values
- order.setValue(context, "Name", "New Order");
- order.setValue(context, "Amount", new java.math.BigDecimal("100.00"));
- order.setValue(context, "IsActive", true);
- order.setValue(context, "ProcessedDate", new java.util.Date());
- ```
-
- ### Working with Associations
-
- ```java
- // set association (reference)
- IMendixObject customer = Core.createXPathQuery("//Sales.Customer[CustomerCode = 'CUST001']")
- .execute(context).get(0);
- order.setValue(context, "Sales.Order_Customer", customer.getId());
-
- // get associated object
- IMendixIdentifier customerId = order.getValue(context, "Sales.Order_Customer");
- if (customerId != null) {
- IMendixObject relatedCustomer = Core.retrieveId(context, customerId);
- string customerName = (string) relatedCustomer.getValue(context, "Name");
- }
- ```
-
- ### Working with Lists
-
- ```java
- // retrieve list
- list<IMendixObject> orders = Core.createXPathQuery("//Sales.Order[status = 'Pending']")
- .execute(context);
-
- // Process list
- java.math.BigDecimal total = java.math.BigDecimal.ZERO;
- for (IMendixObject order : orders) {
- java.math.BigDecimal amount = (java.math.BigDecimal) order.getValue(context, "Amount");
- if (amount != null) {
- total = total.add(amount);
- }
- }
-
- // create list to return
- list<IMendixObject> results = new java.util.ArrayList<>();
- results.add(order1);
- results.add(order2);
- return results;
- ```
-
- ### Error Handling
-
- ```java
- @java.lang.Override
- public boolean executeAction() throws Exception
- {
- // begin user CODE
- IContext context = getContext();
-
- try {
- // business logic here
- IMendixObject order = Core.instantiate(context, "Sales.Order");
- order.setValue(context, "OrderNumber", generateOrderNumber());
- Core.commit(context, order);
- return true;
-
- } catch (Exception e) {
- // log error
- Core.getLogger("MyModule").error("Failed to create order: " + e.getMessage(), e);
-
- // Optionally throw to show error to user
- throw new com.mendix.systemwideinterfaces.MendixRuntimeException(
- "Could not create order: " + e.getMessage());
- }
- // end user CODE
- }
- ```
-
- ### Logging
-
- ```java
- import com.mendix.logging.ILogNode;
-
- // get logger
- ILogNode logger = Core.getLogger("MyModule.MyAction");
-
- // log at different levels
- logger.trace("Detailed trace message");
- logger.debug("debug information");
- logger.info("Processing started for order: " + orderNumber);
- logger.warn("Unusual condition detected");
- logger.error("error processing order", exception);
- logger.critical("critical system failure");
- ```
-
## Part 2.5: Creating Java Actions in MDL
MDL supports defining Java actions with inline Java code using `create java action`.
### Basic Syntax
```mdl
create java action Module.ActionName(param1: type, param2: type) returns ReturnType
as $$
// java code here
return result;
$$;
```
**`AS $$ ... $$` is mandatory.** The body cannot be omitted even for placeholder or stub actions. Omitting it causes a parse error: `no viable alternative at input '...'`. Use a minimal body if the real implementation is not yet written:
```mdl
create java action Module.Stub() returns boolean
as $$
return false;
$$;
```
### Type Parameters (Generics)
Type parameters let Java actions accept any entity type dynamically. Use `entity <pEntity>` in a parameter type to declare the type parameter inline. That parameter becomes the **entity type selector** (receives the entity type name, e.g., `'Module.Entity'`). Bare `pEntity` parameters become **parameterized entity** params (receive entity instances, e.g., `$Variable`).
```mdl
-- ENTITY <pEntity> declares the type parameter; bare pEntity references it
create java action Module.Validate(
EntityType: entity <pEntity> not null,
InputObject: pEntity not null
) returns boolean
as $$
return InputObject != null;
$$;
```
Multiple type parameters use separate `entity <...>` declarations:
```mdl
create java action Module.Transform(
SourceType: entity <pSource> not null,
TargetType: entity <pTarget> not null,
source: pSource not null,
Target: pTarget not null
) returns boolean
as $$
return true;
$$;
```
Type parameter names can be mixed with regular parameter types:
```mdl
create java action Module.CopyAttributes(
EntityType: entity <pEntity> not null,
source: pEntity not null,
Target: pEntity not null,
AttributeNames: string not null
) returns boolean
as $$
return true;
$$;
```
When **calling** these actions from microflows, the entity type selector receives the entity type name as a string literal, while instance params receive variables:
```mdl
$Result = call java action Module.CopyAttributes(
EntityType = 'Module.ProcessResult',
source = $source,
Target = $Target,
AttributeNames = 'Name,Status'
);
```
### EXPOSED AS (Toolbox Visibility)
The `exposed as 'caption' in 'Category'` clause makes the Java action appear as a toolbox item in Studio Pro's microflow editor:
```mdl
create java action Module.FormatCurrency(
Amount: decimal not null,
CurrencyCode: string not null
) returns string
exposed as 'Format Currency' in 'Formatting'
as $$
return String.format("%.2f %s", Amount, CurrencyCode);
$$;
```
Type parameters and EXPOSED AS can be combined:
```mdl
create java action Module.DeepClone(
EntityType: entity <pEntity> not null,
Original: pEntity not null
) returns boolean
exposed as 'Deep Clone Object' in 'Object Utils'
as $$
return true;
$$;
```
### Supported Parameter Types
| MDL Type | Description |
|----------|-------------|
| `string` | Text value |
| `integer` | Whole number |
| `long` | Large whole number |
| `decimal` | Decimal number |
| `boolean` | True/false |
| `datetime` | Date and time |
| `Module.Entity` | Entity reference |
| `list of Module.Entity` | List of entities |
| `stringtemplate(sql)` | SQL/OQL query template with parameters |
| `stringtemplate(text)` | Text template with parameters |
| `entity <pEntity>` | Type parameter declaration (entity type selector) |
| `enum Module.EnumName` | Enumeration type |
| `enumeration(Module.EnumName)` | Enumeration type (alternative syntax) |
| `pEntity` (type param ref) | Type parameter reference (entity instance) |
### Examples
#### Simple Action (No Parameters)
```mdl
/** Returns the current timestamp. */
create java action MyModule.GetCurrentTimestamp() returns datetime
as $$
return new java.util.Date();
$$;
```
#### Action with Primitive Parameters
```mdl
/** Calculates tax amount. */
create java action Finance.CalculateTax(Amount: decimal, TaxRate: decimal) returns decimal
as $$
if (Amount == null || TaxRate == null) {
return java.math.BigDecimal.ZERO;
}
return Amount.multiply(TaxRate).divide(java.math.BigDecimal.valueOf(100), 2, java.math.RoundingMode.HALF_UP);
$$;
```
#### Action with StringTemplate (SQL/OQL)
```mdl
/** Executes an OQL statement with parameterized query. */
create java action Database.ExecuteOQLStatement(OqlStatement: stringtemplate(sql) not null) returns boolean
as $$
// execute the parameterized OQL statement
// The stringtemplate handles parameter substitution safely
return true;
$$;
```
#### Action with NOT NULL Parameter
```mdl
/** Validates an email address - email is required. */
create java action Validation.ValidateEmail(EmailAddress: string not null) returns boolean
as $$
string emailRegex = "^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$";
return EmailAddress.matches(emailRegex);
$$;
```
#### Action with Type Parameter (Generic)
```mdl
/** Validates any entity - checks that required fields are filled. */
create java action Validation.ValidateEntity(
EntityType: entity <pEntity> not null,
InputObject: pEntity not null
) returns boolean
as $$
return InputObject.getMembers().values().stream()
.allMatch(m -> !m.isRequired() || m.getValue(getContext()) != null);
$$;
```
#### Action with Type Parameter + EXPOSED AS
```mdl
/** Deep clones any entity (toolbox-visible). */
create java action Utils.DeepClone(
EntityType: entity <pEntity> not null,
Original: pEntity not null
) returns boolean
exposed as 'Deep Clone Object' in 'Object Utils'
as $$
return true;
$$;
```
## Part 3: Calling Java Actions from MDL
### Basic Syntax
```mdl
-- Call Java action (no return value)
call java action Module.JA_ActionName(
ParamName1 = value1,
ParamName2 = value2
);
-- Call Java action with return value
$Result = call java action Module.JA_ActionName(
ParamName1 = value1,
ParamName2 = value2
);
```
### Avoiding Duplicate Variables (CE0111)
`$Var = call java action ...` **creates a new variable**. Do NOT `declare` a variable with the same name first:
```mdl
-- WRONG: DECLARE + CALL both create $Success → CE0111
declare $success boolean = false;
$success = call java action Module.DoWork();
-- CORRECT: Use a separate name when you need a default
declare $success boolean = false;
$WorkResult = call java action Module.DoWork();
set $success = $WorkResult;
-- CORRECT: Simple pass-through (no default needed)
$success = call java action Module.DoWork();
return $success;
```
When calling Java actions in **multiple branches**, use unique result variable names:
```mdl
declare $success boolean = false;
if $Priority = 'HIGH' then
$UrgentResult = call java action Module.SendUrgent(Msg = $Email);
set $success = $UrgentResult;
else
$NormalResult = call java action Module.SendNormal(Msg = $Email);
set $success = $NormalResult;
end if;
```
### Expression Escaping in String Arguments
Single quotes within string literal arguments must be doubled (`''`):
```mdl
-- OQL with embedded quotes — use '' to escape
$count = call java action Module.ExecuteOQL(
Statement = 'SELECT * FROM Module.Entity WHERE Status = ''Active'''
);
```
### Complete Examples
#### Example 1: Simple Calculation
```mdl
/**
* Calculate tax using custom Java action
*/
create microflow Tax.ACT_CalculateOrderTax($order: Tax.Order)
returns decimal as $taxAmount
begin
declare $subtotal decimal = $order/Subtotal;
declare $taxRate decimal = 0.21;
-- Call Java action for complex calculation
$taxAmount = call java action Tax.JA_CalculateTax(
Amount = $subtotal,
TaxRate = $taxRate
);
change $order (TaxAmount = $taxAmount);
commit $order;
return $taxAmount;
end;
```
#### Example 2: External API Integration
```mdl
/**
* Send notification via external service using Java action
*/
create microflow Notifications.ACT_SendOrderConfirmation($order: Sales.Order)
returns boolean as $success
begin
declare $customerEmail string = $order/Sales.Order_Customer/Email;
declare $orderNumber string = $order/OrderNumber;
-- Call Java action that integrates with external email service
$success = call java action Notifications.JA_SendEmail(
ToAddress = $customerEmail,
Subject = 'Order Confirmation: ' + $orderNumber,
body = 'Your order has been confirmed.',
TemplateName = 'OrderConfirmation'
);
if $success then
change $order (NotificationSent = true);
commit $order;
else
log warning node 'Notifications' 'Failed to send email for order: ' + $orderNumber;
end if;
return $success;
end;
```
#### Example 3: OQL Bulk Operations (Mendix 11.6+)
```mdl
/**
* Bulk update using OQL via Java action
*/
create microflow Finance.ACT_ArchiveOldTransactions()
returns integer as $rowsAffected
begin
-- Use built-in OQL execution Java action
$rowsAffected = call java action CustomActivities.ExecuteOQLStatement(
OqlStatement = 'UPDATE Finance.Transaction SET Status = ''ARCHIVED'' WHERE TransactionDate < ''2024-01-01'' AND Status = ''COMPLETED'''
);
log info node 'Finance' 'Archived ' + toString($rowsAffected) + ' transactions';
return $rowsAffected;
end;
```
#### Example 4: OQL with Parameters
```mdl
/**
* Parameterized OQL update via Java action
*/
create microflow Finance.ACT_UpdateTransactionStatus(
$oldStatus: string,
$newStatus: string,
$cutoffDate: datetime
)
returns integer as $rowsUpdated
begin
$rowsUpdated = call java action CustomActivities.ExecuteOQLStatementPars(
OqlStatement = 'UPDATE Finance.Transaction SET Status = {1} WHERE Status = {2} AND TransactionDate < {3}' with (
{1} = $newStatus,
{2} = $oldStatus,
{3} = $cutoffDate as datetime
)
);
return $rowsUpdated;
end;
```
#### Example 5: Returning Objects
```mdl
/**
* Create complex object structure using Java action
*/
create microflow Import.ACT_ParseCSVFile($fileDocument: System.FileDocument)
returns list of Import.ImportRecord as $records
begin
-- Java action parses CSV and returns list of objects
$records = call java action Import.JA_ParseCSV(
FileDocument = $fileDocument,
HasHeader = true,
Delimiter = ','
);
if $records = empty then
log warning node 'Import' 'No records parsed from file';
else
log info node 'Import' 'Parsed ' + toString(length($records)) + ' records';
end if;
return $records;
end;
- ```
-
- ## Part 4: Common Java Action Patterns
-
- ### Pattern 1: Validation Helper
-
- **Java Action Definition:**
- - Name: `JA_ValidateEmail`
- - Parameter: `EmailAddress` (String)
- - Return: Boolean
-
- ```java
- @java.lang.Override
- public java.lang.Boolean executeAction() throws Exception
- {
- // begin user CODE
- if (this.EmailAddress == null || this.EmailAddress.trim().isEmpty()) {
- return false;
- }
-
- string emailRegex = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$";
- return this.EmailAddress.matches(emailRegex);
- // end user CODE
- }
- ```
-
- **MDL Usage:**
- ```mdl
- create microflow Customer.VAL_CustomerEmail($customer: Customer.Customer)
- returns boolean as $isValid
- begin
- $isValid = call java action Customer.JA_ValidateEmail(
- EmailAddress = $customer/Email
- );
-
- if not($isValid) then
- validation feedback $customer/Email
- message 'Please enter a valid email address';
- end if;
-
- return $isValid;
- end;
- ```
-
- ### Pattern 2: External API Call
-
- **Java Action Definition:**
- - Name: `JA_FetchExchangeRate`
- - Parameters: `FromCurrency` (String), `ToCurrency` (String)
- - Return: Decimal
-
- ```java
- @java.lang.Override
- public java.math.BigDecimal executeAction() throws Exception
- {
- // begin user CODE
- IContext context = getContext();
-
- try {
- // build api url
- string url = "https://api.exchangerate.host/convert?from="
- + this.FromCurrency + "&to=" + this.ToCurrency;
-
- // Make HTTP request (using your preferred HTTP client)
- java.net.HttpURLConnection conn =
- (java.net.HttpURLConnection) new java.net.URL(url).openConnection();
- conn.setRequestMethod("get");
-
- // Parse response
- java.io.BufferedReader reader = new java.io.BufferedReader(
- new java.io.InputStreamReader(conn.getInputStream()));
- StringBuilder response = new StringBuilder();
- string line;
- while ((line = reader.readLine()) != null) {
- response.append(line);
- }
- reader.close();
-
- // Parse json and extract rate (simplified)
- // in production, use a proper json library
- string json = response.toString();
- int rateIndex = json.indexOf("\"result\":");
- if (rateIndex > 0) {
- string rateStr = json.substring(rateIndex + 9, json.indexOf(",", rateIndex));
- return new java.math.BigDecimal(rateStr.trim());
- }
-
- return java.math.BigDecimal.ONE;
-
- } catch (Exception e) {
- Core.getLogger("ExchangeRate").error("Failed to fetch rate", e);
- throw new com.mendix.systemwideinterfaces.MendixRuntimeException(
- "Could not fetch exchange rate: " + e.getMessage());
- }
- // end user CODE
- }
- ```
-
- **MDL Usage:**
- ```mdl
- create microflow Finance.ACT_ConvertCurrency(
- $amount: decimal,
- $fromCurrency: string,
- $toCurrency: string
- )
- returns decimal as $convertedAmount
- begin
- declare $rate decimal;
-
- $rate = call java action Finance.JA_FetchExchangeRate(
- FromCurrency = $fromCurrency,
- ToCurrency = $toCurrency
- );
-
- set $convertedAmount = $amount * $rate;
- return $convertedAmount;
- end;
- ```
-
- ### Pattern 3: File Processing
-
- **Java Action Definition:**
- - Name: `JA_GeneratePDF`
- - Parameters: `Order` (Sales.Order entity), `TemplateName` (String)
- - Return: System.FileDocument
-
- ```java
- @java.lang.Override
- public IMendixObject executeAction() throws Exception
- {
- // begin user CODE
- IContext context = getContext();
-
- // get order data
- string orderNumber = (string) this.Order.getValue(context, "OrderNumber");
- java.math.BigDecimal total = (java.math.BigDecimal) this.Order.getValue(context, "TotalAmount");
-
- // generate PDF content (using iText or similar library)
- byte[] pdfContent = generatePdfBytes(orderNumber, total);
-
- // create FileDocument
- IMendixObject fileDoc = Core.instantiate(context, "System.FileDocument");
- fileDoc.setValue(context, "Name", "Order_" + orderNumber + ".pdf");
- Core.storeFileDocumentContent(context, fileDoc,
- new java.io.ByteArrayInputStream(pdfContent));
-
- Core.commit(context, fileDoc);
-
- return fileDoc;
- // end user CODE
- }
- ```
-
- **MDL Usage:**
- ```mdl
- create microflow Sales.ACT_GenerateOrderPDF($order: Sales.Order)
- returns System.FileDocument as $pdfFile
- begin
- $pdfFile = call java action Sales.JA_GeneratePDF(
- Order = $order,
- TemplateName = 'OrderConfirmation'
- );
-
- log info node 'Sales' 'Generated PDF for order: ' + $order/OrderNumber;
-
- return $pdfFile;
- end;
- ```
-
- ## Part 5: Best Practices
-
- ### Naming Conventions
-
- | Element | Convention | Example |
- |---------|------------|---------|
- | Java Action | `JA_` prefix + PascalCase | `JA_CalculateTax`, `JA_SendEmail` |
- | Module | Business domain name | `Finance`, `Integration`, `Utils` |
- | Parameters | PascalCase, descriptive | `OrderAmount`, `CustomerEmail` |
-
- ### Code Organization (Recommended)
-
- **Keep Java action code minimal** - only handle parameter extraction and delegation. Put the actual implementation in separate classes under `<modulename>.impl`.
-
- **Why?**
- - Code between `begin user CODE` and `end user CODE` is preserved, but it's limited space
- - Implementation classes are fully under your control (not regenerated)
- - Easier to unit test implementation logic separately
- - Better code organization and reusability
-
- **Package Structure:**
- ```
- javasource/
- ├── mymodule/
- │ ├── actions/
- │ │ └── JA_ProcessOrder.java # Generated action (minimal code)
- │ └── impl/
- │ ├── processorder/
- │ │ ├── OrderProcessor.java # Main implementation
- │ │ ├── OrderValidator.java # validation logic
- │ │ └── OrderNotifier.java # notification logic
- │ └── shared/
- │ └── EmailService.java # Shared utilities
- ```
-
- **Example - Java Action (Thin Wrapper):**
- ```java
- // in javasource/mymodule/actions/JA_ProcessOrder.java
- @java.lang.Override
- public java.lang.Boolean executeAction() throws Exception
- {
- // begin user CODE
- // Delegate to implementation class - keep this minimal!
- return new mymodule.impl.processorder.OrderProcessor(getContext())
- .process(this.Order, this.SendNotification);
- // end user CODE
- }
- ```
-
- **Example - Implementation Class (Testable Design):**
-
- The key to testability is separating **pure business logic** from **Mendix API calls**. Use interfaces for data access so you can mock them in tests.
-
- ```java
- // in javasource/mymodule/impl/processorder/OrderProcessor.java
- package mymodule.impl.processorder;
-
- import java.math.BigDecimal;
- import java.util.Date;
-
- /**
- * Pure business logic - NO Mendix dependencies!
- * Can be tested with plain JUnit without running Mendix.
- */
- public class OrderProcessor {
-
- public ProcessResult process(OrderData order, boolean sendNotification) {
- // Validate - pure java logic
- if (order.getOrderNumber() == null || order.getOrderNumber().isEmpty()) {
- return ProcessResult.failure("Order number is required");
- }
- if (order.getTotalAmount() == null || order.getTotalAmount().compareTo(BigDecimal.ZERO) <= 0) {
- return ProcessResult.failure("Order amount must be positive");
- }
-
- // Calculate - pure java logic
- BigDecimal tax = calculateTax(order.getTotalAmount(), order.getTaxRate());
- BigDecimal finalAmount = order.getTotalAmount().add(tax);
-
- // return result (actual persistence happens in adapter)
- return ProcessResult.success(finalAmount, tax, new date());
- }
-
- public BigDecimal calculateTax(BigDecimal amount, BigDecimal rate) {
- if (amount == null || rate == null) {
- return BigDecimal.ZERO;
- }
- return amount.multiply(rate).divide(BigDecimal.valueOf(100), 2, java.math.RoundingMode.HALF_UP);
- }
- }
- ```
-
- ```java
- // in javasource/mymodule/impl/processorder/OrderData.java
- package mymodule.impl.processorder;
-
- import java.math.BigDecimal;
-
- /**
- * Plain Java data object - no Mendix dependencies.
- */
- public class OrderData {
- private string orderNumber;
- private BigDecimal totalAmount;
- private BigDecimal taxRate;
-
- // Constructor, getters, setters...
- public OrderData(string orderNumber, BigDecimal totalAmount, BigDecimal taxRate) {
- this.orderNumber = orderNumber;
- this.totalAmount = totalAmount;
- this.taxRate = taxRate;
- }
-
- public string getOrderNumber() { return orderNumber; }
- public BigDecimal getTotalAmount() { return totalAmount; }
- public BigDecimal getTaxRate() { return taxRate; }
- }
- ```
-
- ```java
- // in javasource/mymodule/impl/processorder/MendixOrderAdapter.java
- package mymodule.impl.processorder;
-
- import com.mendix.systemwideinterfaces.core.IContext;
- import com.mendix.systemwideinterfaces.core.IMendixObject;
- import com.mendix.core.Core;
- import java.math.BigDecimal;
-
- /**
- * Adapter: converts between Mendix objects and pure Java objects.
- * This is the ONLY class that touches Mendix APIs.
- */
- public class MendixOrderAdapter {
- private final IContext context;
-
- public MendixOrderAdapter(IContext context) {
- this.context = context;
- }
-
- public OrderData toOrderData(IMendixObject mendixOrder) {
- return new OrderData(
- (string) mendixOrder.getValue(context, "OrderNumber"),
- (BigDecimal) mendixOrder.getValue(context, "TotalAmount"),
- (BigDecimal) mendixOrder.getValue(context, "TaxRate")
- );
- }
-
- public void applyResult(IMendixObject mendixOrder, ProcessResult result) throws Exception {
- mendixOrder.setValue(context, "status", "Processed");
- mendixOrder.setValue(context, "FinalAmount", result.getFinalAmount());
- mendixOrder.setValue(context, "TaxAmount", result.getTaxAmount());
- mendixOrder.setValue(context, "ProcessedDate", result.getProcessedDate());
- Core.commit(context, mendixOrder);
- }
- }
- ```
-
- **Example - Java Action (Wiring Only):**
- ```java
- // in javasource/mymodule/actions/JA_ProcessOrder.java
- @java.lang.Override
- public java.lang.Boolean executeAction() throws Exception
- {
- // begin user CODE
- // Wire up adapter and processor
- MendixOrderAdapter adapter = new MendixOrderAdapter(getContext());
- OrderProcessor processor = new OrderProcessor();
-
- // Convert Mendix object to plain java object
- OrderData orderData = adapter.toOrderData(this.Order);
-
- // Process (pure java - no Mendix dependencies)
- ProcessResult result = processor.process(orderData, this.SendNotification);
-
- if (result.isSuccess()) {
- // apply result back to Mendix object
- adapter.applyResult(this.Order, result);
- return true;
- } else {
- Core.getLogger("MyModule").warn("Order processing failed: " + result.getMessage());
- return false;
- }
- // end user CODE
- }
- ```
-
- **Example - Unit Test (No Mendix Runtime Required):**
- ```java
- // in javasource/mymodule/impl/processorder/OrderProcessorTest.java
- package mymodule.impl.processorder;
-
- import org.junit.Test;
- import static org.junit.Assert.*;
- import java.math.BigDecimal;
-
- public class OrderProcessorTest {
-
- @Test
- public void testProcessValidOrder() {
- // Arrange - plain java objects, no mocking needed!
- OrderProcessor processor = new OrderProcessor();
- OrderData order = new OrderData("ORD-001", new BigDecimal("100.00"), new BigDecimal("21"));
-
- // Act
- ProcessResult result = processor.process(order, false);
-
- // Assert
- assertTrue(result.isSuccess());
- assertEquals(new BigDecimal("21.00"), result.getTaxAmount());
- assertEquals(new BigDecimal("121.00"), result.getFinalAmount());
- }
-
- @Test
- public void testProcessInvalidOrder_MissingOrderNumber() {
- OrderProcessor processor = new OrderProcessor();
- OrderData order = new OrderData(null, new BigDecimal("100.00"), new BigDecimal("21"));
-
- ProcessResult result = processor.process(order, false);
-
- assertFalse(result.isSuccess());
- assertEquals("Order number is required", result.getMessage());
- }
-
- @Test
- public void testCalculateTax() {
- OrderProcessor processor = new OrderProcessor();
-
- BigDecimal tax = processor.calculateTax(new BigDecimal("200.00"), new BigDecimal("10"));
-
- assertEquals(new BigDecimal("20.00"), tax);
- }
- }
- ```
-
- **Run tests with Maven or standalone:**
- ```bash
- # from javasource directory
- javac -cp .:junit-4.13.jar mymodule/impl/processorder/*.java
- java -cp .:junit-4.13.jar org.junit.runner.JUnitCore mymodule.impl.processorder.OrderProcessorTest
- ```
-
- **Benefits:**
- - **Testable without Mendix** - Run JUnit tests locally or in CI without Mendix runtime
- - **Fast feedback** - Unit tests run in milliseconds, not minutes
- - **Clear separation** - Business logic is pure Java; Mendix integration is isolated in adapters
- - **Reusable** - `OrderProcessor` can be used in other contexts (batch jobs, REST APIs)
- - **Maintainable** - Changes to business logic don't require Mendix knowledge
-
- ### Error Handling Best Practices
-
- 1. **Always wrap in try-catch**:
- ```java
- try {
- // business logic
- } catch (Exception e) {
- Core.getLogger("MyModule").error("operation failed", e);
- throw new MendixRuntimeException("user-friendly message: " + e.getMessage());
- }
- ```
-
- 2. **Validate inputs early**:
- ```java
- if (this.requiredParam == null) {
- throw new IllegalArgumentException("RequiredParam is required");
- }
- ```
-
- 3. **Use appropriate log levels**:
- - `trace`: Detailed debugging
- - `debug`: Development information
- - `info`: Normal operations
- - `warn`: Potential issues
- - `error`: Recoverable errors
- - `critical`: System failures
-
- ### Performance Considerations
-
- 1. **Batch operations** when possible:
- ```java
- // Instead of committing one by one
- list<IMendixObject> toCommit = new ArrayList<>();
- for (IMendixObject obj : objects) {
- obj.setValue(context, "status", "Processed");
- toCommit.add(obj);
- }
- Core.commit(context, toCommit); // single batch commit
- ```
-
- 2. **Use pagination** for large datasets:
- ```java
- int offset = 0;
- int batchSize = 1000;
- list<IMendixObject> batch;
- do {
- batch = Core.createXPathQuery(xpath).setAmount(batchSize).setOffset(offset).execute(context);
- // Process batch
- offset += batchSize;
- } while (batch.size() == batchSize);
- ```
-
- 3. **Cache expensive lookups**:
- ```java
- private static map<string, object> cache = new ConcurrentHashMap<>();
```
## Validation Checklist
Before deploying Java actions, verify:
- [ ] Java action has `JA_` prefix naming convention
- [ ] All parameters are defined with correct types
- [ ] Return type matches what you return in Java
- [ ] Code is only between `begin user CODE` / `end user CODE` markers
- [ ] Proper null checks for all parameters
- [ ] Exception handling with logging
- [ ] No hardcoded credentials or sensitive data
- [ ] Entity and attribute names match model exactly
- [ ] Unit tests cover main scenarios
## Common Errors
| Error | Cause | Fix |
|-------|-------|-----|
| `ClassNotFoundException` | Missing library | Add JAR to `userlib/` folder |
| `NullPointerException` | Null parameter | Add null checks |
| `Could not find entity` | Wrong entity name | Use exact qualified name |
| `attribute not found` | Wrong attribute name | Check model for exact name |
| `ClassCastException` | Wrong type cast | Check parameter types |
| `no viable alternative at input '...'` (parse error) | `AS $$ ... $$` body is missing — it is **mandatory** even for void/stub actions | Add `as $$ return null; $$;` (or appropriate stub) |
## Related Documentation
- [Mendix Java Actions Reference](https://docs.mendix.com/refguide/java-actions/)
- [Build Microflow Actions with Java](https://docs.mendix.com/howto/extensibility/howto-connector-kit/)
- [Java Programming in Mendix](https://docs.mendix.com/refguide/java-programming/)
- [Write Microflows Skill](../write-microflows/SKILL.md)
- [Validation Microflows Skill](../validation-microflows/SKILL.md)
## Quick Reference
### Java Action Definition Syntax
```mdl
-- Basic Java action
create java action Module.Name(Param: type not null) returns boolean
as $$
return true;
$$;
-- With type parameters (generics)
-- ENTITY <pEntity> = entity type selector, bare pEntity = entity instances
create java action Module.Name(
EntityType: entity <pEntity> not null,
Obj: pEntity not null
) returns boolean
as $$
return Obj != null;
$$;
-- With EXPOSED AS (toolbox visibility)
create java action Module.Name(Amount: decimal) returns string
exposed as 'Format Amount' in 'Formatting'
as $$
return Amount.toString();
$$;
-- Combined type parameters + EXPOSED AS
create java action Module.Name(
EntityType: entity <pEntity> not null,
Obj: pEntity not null
) returns boolean
exposed as 'Validate Object' in 'Validation'
as $$
return Obj != null;
$$;
```
### Java Action Call Syntax
```mdl
-- Without return value
call java action Module.JA_ActionName(Param1 = value1, Param2 = value2);
-- With return value
$Result = call java action Module.JA_ActionName(Param1 = value1);
-- With OQL parameters (Mendix 11.6+)
$Rows = call java action Module.JA_ExecuteOQL(
Statement = 'UPDATE Module.Entity SET Attr = {1} WHERE Id = {2}' with (
{1} = $value1,
{2} = $value2 as integer
)
);
```
### Core API Quick Reference
```java
// context
IContext context = getContext();
// create
IMendixObject obj = Core.instantiate(context, "Module.Entity");
// read
object value = obj.getValue(context, "attributename");
// update
obj.setValue(context, "attributename", newValue);
// Save
Core.commit(context, obj);
// delete
Core.delete(context, obj);
// query
list<IMendixObject> results = Core.createXPathQuery("//Module.Entity[attr = 'value']").execute(context);
// log
Core.getLogger("ModuleName").info("message");
```