---
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");
```
