java-actions · git:20260821.ff81a24 · 2026-08-21 · sha256 60b6fe255344de02
java-actions git:20260821.ff81a24A
Immutable. This exact content is served forever at /api/v1/blob/60b6fe255344de02.
---
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.
## 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");
```