ADR-009: OpenAPI-Based REST Client for iDempiere

Status

Implemented (v1.27.0)

Implementation Summary

Component Status Location
OpenAPI Spec ✅ openapi/idempiere-openapi-v12.yml (contract, never modified)
OpenAPI Generator ✅ pom.xml - openapi-generator-maven-plugin v7.10.0
Generated APIs ✅ target/generated-sources/openapi/ - 19 APIs, 162 models
Cleanup Plugin ✅ pom.xml - maven-antrun-plugin deletes problematic files
Factory Bean ✅ GeneratedOpenApiFactory.java - CDI access to all 19 APIs

Architecture Decision: Stateless Bridge

The CLI is a stateless bridge that sends the configured token with each request - it does not manage authentication sessions.

This means:

Rationale: CLI tools should be stateless for:

Architecture Decision: Dual Client Approach

The hand-written IdempiereApiClient (~940 lines) remains the primary client for AD operations. Generated APIs are supplementary for endpoints not covered by the hand-written client.

┌─────────────────────────────────────────────────────────────────┐
│                         CLI Commands                             │
└─────────────────────────┬───────────────────────────────────────┘
                          │
        ┌─────────────────┴─────────────────┐
        ▼                                   ▼
┌───────────────────────┐         ┌───────────────────────┐
│   IdempiereApiClient  │         │ GeneratedOpenApiFactory│
│   (Hand-written)      │         │  (OpenAPI Generated)  │
├───────────────────────┤         ├───────────────────────┤
│ • AD Tables           │         │ • HealthApi           │
│ • AD Columns          │         │ • CachesApi           │
│ • AD Windows          │         │ • WorkflowsApi        │
│ • AD Processes        │         │ • NodesApi            │
│ • AD Menus            │         │ • ServerJobsApi       │
│ • AD References       │         │ • + 13 more APIs      │
└───────────────────────┘         └───────────────────────┘

Rationale: No facade layer needed. Both clients coexist independently:

Note: AuthenticationApi is available in GeneratedOpenApiFactory but intentionally not exposed as a CLI command (see Stateless Bridge decision above).

Generated APIs (19 of 21)

API Endpoints Usage
AuthenticationApi /auth/* Token management
BatchApi /batch Batch operations
CachesApi /caches/* Cache management
ChartsApi /charts/* Chart data
FilesApi /files/* File downloads
FormsApi /forms/* Form listing
HealthApi /health Server health
InfoWindowsApi /infos/* Info window queries
ModelsApi /models/* Generic CRUD
NodesApi /nodes/* Server nodes
ProcessApi /processes/* Process execution
ReferencesApi /reference/* Reference lookups
ServerJobsApi /servers/jobs/* Server jobs
StatusLinesApi /statusline/* Status lines
TasksApi /tasks/* Task execution
UploadsApi /uploads/* File uploads
ViewsApi /views/* REST views
WindowsApi /windows/* Window operations
WorkflowsApi /workflow/* Workflow activities

Excluded APIs (OpenAPI Schema Limitations)

API Issue Workaround
MenutreeApi Recursive oneOf with array type generates List<Object>.class (invalid Java) Use IdempiereApiClient
SchedulersApi allOf merges schemas with duplicate schedulerState field Use IdempiereApiClient

Note: The OpenAPI spec (idempiere-openapi-v12.yml) is the contract and must never be modified. These are generator limitations, not spec issues.

Context

The idempiere-cli currently uses a hand-written IdempiereClient class that manually constructs HTTP requests to the iDempiere REST API. This approach has several limitations:

  1. No formal API contract - Endpoints are hardcoded based on observed behavior
  2. Incomplete coverage - Only a subset of available endpoints are implemented
  3. No type safety - Request/response structures are manually defined
  4. Maintenance burden - API changes require manual updates to client code
  5. No validation - Requests aren't validated against the API schema

We now have access to the official iDempiere OpenAPI 3.0 specification (idempiere-openapi.yml) which defines:

Decision

We will generate a type-safe REST client from the OpenAPI specification using code generation, while maintaining backward compatibility with the existing IdempiereClient API.

OpenAPI Specification Analysis

API Base URL: {base_url}/api/v1 (default: http://localhost:8080/api/v1)

Authentication:

Endpoint Categories - Scoped Implementation:

In Scope (Phase 5):

Category Endpoints Current CLI Support Priority
Authentication /auth/* (9) Partial (token only) High
Models /models/* (10) Yes High
Process /processes/* (2) Partial High
References /reference/{id} (1) No High
Views /views/* (8) No High

Out of Scope (Future):

Category Endpoints Notes
Windows /windows/* (8) Window-based operations
InfoWindows /infos/* (5) Info window queries
Workflows /workflow/* (7) Workflow operations
Others ~30 endpoints Files, Batch, Charts, Caches, etc.

Key Endpoints for CLI

Models API (Primary for AD manipulation):

GET    /models                           # List all models (AD_Table)
GET    /models/{tableName}/yaml          # Get YAML schema for table
GET    /models/{tableName}               # Get records with OData query
POST   /models/{tableName}               # Create record
GET    /models/{tableName}/{id}          # Get record by ID/UUID
PUT    /models/{tableName}/{id}          # Update record
DELETE /models/{tableName}/{id}          # Delete record
GET    /models/{tableName}/{id}/print    # Print record

OData Query Parameters:

Process API:

GET  /processes                # List processes
POST /processes/{processSlug}  # Run process with parameters

Authentication API:

POST /auth/tokens              # Login (username/password)
PUT  /auth/tokens              # Set login parameters (role, org, warehouse)
POST /auth/refresh             # Refresh token
POST /auth/logout              # Logout
GET  /auth/jwk                 # Get JWK for token verification
GET  /auth/roles               # List available roles
GET  /auth/organizations       # List organizations for role
GET  /auth/warehouses          # List warehouses for org
GET  /auth/language            # Get client language

Implementation Approach

Phase 1: Code Generation Setup

  1. Add OpenAPI Generator Maven plugin
  2. Generate Java client interfaces from spec
  3. Configure Quarkus REST Client integration

Phase 2: Generated Client Integration

// Generated from OpenAPI spec
@RegisterRestClient(configKey = "idempiere-api")
@Path("/api/v1")
public interface IdempiereRestApi {

    @POST
    @Path("/auth/tokens")
    @Produces(MediaType.APPLICATION_JSON)
    @Consumes(MediaType.APPLICATION_JSON)
    AuthenticationResponse authenticate(AuthenticationRequest request);

    @GET
    @Path("/models/{tableName}")
    @Produces(MediaType.APPLICATION_JSON)
    ModelRecords getRecords(
        @PathParam("tableName") String tableName,
        @QueryParam("$filter") String filter,
        @QueryParam("$select") String select,
        @QueryParam("$orderby") String orderBy,
        @QueryParam("$top") Integer top,
        @QueryParam("$skip") Integer skip
    );

    @POST
    @Path("/models/{tableName}")
    @Produces(MediaType.APPLICATION_JSON)
    @Consumes(MediaType.APPLICATION_JSON)
    Object createRecord(
        @PathParam("tableName") String tableName,
        Object record
    );

    // ... additional endpoints
}

Phase 3: Facade Layer

Maintain IdempiereClient as a facade over the generated client for backward compatibility:

@ApplicationScoped
public class IdempiereClient {

    @Inject
    @RestClient
    IdempiereRestApi api;

    // Existing methods delegate to generated client
    public ADTable getTableByName(String tableName) {
        var response = api.getRecords("ad_table",
            "TableName eq '" + tableName + "'", null, null, 1, null);
        return mapToADTable(response.getRecords().get(0));
    }

    public ADTable createTable(ADTable table) {
        var response = api.createRecord("ad_table", table);
        return mapToADTable(response);
    }
}

Code Generation Configuration (Implemented)

OpenAPI Generator Maven Plugin (v7.10.0):

<plugin>
    <groupId>org.openapitools</groupId>
    <artifactId>openapi-generator-maven-plugin</artifactId>
    <version>7.10.0</version>
    <executions>
        <execution>
            <id>generate-idempiere-api</id>
            <goals>
                <goal>generate</goal>
            </goals>
            <configuration>
                <inputSpec>${project.basedir}/openapi/idempiere-openapi-current.yml</inputSpec>
                <generatorName>java</generatorName>
                <library>native</library>
                <output>${project.build.directory}/generated-sources/openapi</output>
                <apiPackage>org.idempiere.cli.api.generated</apiPackage>
                <modelPackage>org.idempiere.cli.api.generated.model</modelPackage>
                <invokerPackage>org.idempiere.cli.api.generated.invoker</invokerPackage>
                <skipValidateSpec>true</skipValidateSpec>
                <configOptions>
                    <sourceFolder>src/main/java</sourceFolder>
                    <dateLibrary>java8</dateLibrary>
                    <useJakartaEe>true</useJakartaEe>
                    <openApiNullable>false</openApiNullable>
                    <serializationLibrary>jackson</serializationLibrary>
                </configOptions>
            </configuration>
        </execution>
    </executions>
</plugin>

Cleanup Plugin (for problematic schemas):

<plugin>
    <artifactId>maven-antrun-plugin</artifactId>
    <version>3.1.0</version>
    <executions>
        <execution>
            <id>fix-openapi-generated</id>
            <phase>generate-sources</phase>
            <goals><goal>run</goal></goals>
            <configuration>
                <target>
                    <!-- Delete models with oneOf/allOf that generate invalid Java -->
                    <delete file=".../MenuTreeEntriesInner.java" failonerror="false"/>
                    <delete file=".../MenuTree.java" failonerror="false"/>
                    <delete file=".../ServersSchedulersIdGet200Response.java" failonerror="false"/>
                    <!-- Delete APIs that reference problematic models -->
                    <delete file=".../MenutreeApi.java" failonerror="false"/>
                    <delete file=".../SchedulersApi.java" failonerror="false"/>
                </target>
            </configuration>
        </execution>
    </executions>
</plugin>

Directory Structure

openapi/                                    # Versioned OpenAPI specs
├── README.md                               # Versioning documentation
├── idempiere-openapi-v12.yml               # iDempiere 12.x spec
├── idempiere-openapi-current.yml           # Symlink to current version
src/
├── main/java/org/idempiere/cli/
│   ├── api/
│   │   ├── IdempiereClient.java          # Facade (backward compat)
│   │   ├── IdempiereConfig.java          # Configuration
│   │   └── model/                        # CLI-specific models
│   │       ├── ADTable.java
│   │       ├── ADColumn.java
│   │       └── ...
target/generated-sources/openapi/         # Generated at build time
├── src/main/java/org/idempiere/cli/api/generated/
│   ├── api/                              # REST client interfaces
│   │   ├── AuthenticationApi.java
│   │   ├── ModelsApi.java
│   │   ├── ProcessApi.java
│   │   └── ... (21 API interfaces)
│   └── model/                            # Request/response DTOs
│       ├── AuthenticationRequest.java
│       ├── AuthenticationResponse.java
│       ├── ErrorResponse.java
│       └── ... (70+ model classes)

OpenAPI Specification Versioning

OpenAPI specifications are stored in openapi/ with versioned naming:

File iDempiere Version Status
idempiere-openapi-v12.yml 12.x Current
idempiere-openapi-v13.yml 13.x Planned

The symlink idempiere-openapi-current.yml points to the active specification.

Adding new versions:

  1. Copy new spec: openapi/idempiere-openapi-v{version}.yml
  2. Update symlink: ln -sf idempiere-openapi-v{version}.yml idempiere-openapi-current.yml
  3. Regenerate: mvn generate-sources

Configuration

# application.properties
quarkus.rest-client.idempiere-api.url=${IDEMPIERE_API_URL:http://localhost:8080/api/v1}
quarkus.rest-client.idempiere-api.scope=jakarta.inject.Singleton

# Connection settings
quarkus.rest-client.idempiere-api.connect-timeout=5000
quarkus.rest-client.idempiere-api.read-timeout=30000

Consequences

Positive

  1. Type Safety - Generated models match API schema exactly
  2. Complete Coverage - All 70+ endpoints available
  3. Auto-Updates - Regenerate when API changes
  4. Validation - Requests validated against schema
  5. Documentation - Generated Javadoc from OpenAPI descriptions
  6. Consistency - Same patterns across all endpoints

Negative

  1. Build Complexity - Code generation step in build
  2. Generated Code Size - ~50+ Java files generated
  3. Learning Curve - Team must understand generated patterns
  4. Customization - Generated code harder to customize

Neutral

  1. Migration Path - Existing IdempiereClient becomes facade
  2. Testing - Mock generated interfaces for unit tests

Implementation Plan

Phase 1: Setup (v1.27.0) ✅ COMPLETED

Phase 2: Integration (v1.27.0) ✅ COMPLETED

Phase 3: Command Integration (v1.27.0) ✅ COMPLETED

Command API Status
doctor --api HealthApi ✅ Check iDempiere server health
config test HealthApi ✅ Validate API connectivity with database check
cache list CachesApi ✅ List server caches
cache reset <name> CachesApi ✅ Reset specific cache
cache reset-all CachesApi ✅ Reset all caches
workflow list WorkflowsApi ✅ List pending workflow activities
workflow approve <id> WorkflowsApi ✅ Approve workflow activity
workflow reject <id> WorkflowsApi ✅ Reject workflow activity
workflow forward <id> WorkflowsApi ✅ Forward to another user
server nodes NodesApi ✅ List server cluster nodes
server jobs ServerJobsApi ✅ List server jobs
server health HealthApi ✅ Check server health status

Note: AuthenticationApi is intentionally NOT exposed as a command. See "Stateless Bridge" architecture decision.

Phase 4: Testing (Future)

Appendix: OpenAPI Endpoints Reference

Authentication (/auth/*)

Method Path Description
POST /auth/tokens Login with username/password
PUT /auth/tokens Set login parameters
POST /auth/refresh Refresh JWT token
POST /auth/logout Logout
GET /auth/jwk Get JSON Web Key Set
GET /auth/roles List available roles
GET /auth/organizations List organizations
GET /auth/warehouses List warehouses
GET /auth/language Get client language

Models (/models/*)

Method Path Description
GET /models List all models
GET /models/{table}/yaml Get YAML schema
GET /models/{table} Query records
POST /models/{table} Create record
GET /models/{table}/{id} Get by ID
PUT /models/{table}/{id} Update record
DELETE /models/{table}/{id} Delete record
GET /models/{table}/{id}/attachments List attachments
POST /models/{table}/{id}/attachments Upload attachment
GET /models/{table}/{id}/print Print record

Process (/processes/*)

Method Path Description
GET /processes List processes
POST /processes/{slug} Run process

Windows (/windows/*)

Method Path Description
GET /windows List windows
GET /windows/{slug} Query window records
POST /windows/{slug} Create via window
GET /windows/{slug}/{id} Get window record
PUT /windows/{slug}/{id} Update via window
GET /windows/{slug}/tabs List tabs
GET /windows/{slug}/tabs/{tab}/fields List fields

Other Notable Endpoints

Category Path Description
Health /health Health check
Batch /batch Batch operations
Files /files Download files
References /reference/{id} Get reference values
Menutree /menutree/{id} Get menu tree
Workflows /workflow/* Workflow operations
InfoWindows /infos/* Info window queries

References

Path: /docs/developers/architecture/idempiere-hub/009-openapi-rest-client