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:
- No
auth login/logoutcommands - Users configure tokens viaconfig --token TOKEN - Long-lived API tokens - Created in iDempiere UI (User Profile > API Tokens), not short-lived JWTs
- Token passed via header - All requests include
Authorization: Bearer {token} - No session state - Each command execution is independent
Rationale: CLI tools should be stateless for:
- Automation and scripting (CI/CD pipelines, cron jobs)
- Parallel execution without session conflicts
- Simple configuration (one token, no refresh logic)
- Security (long-lived tokens can be revoked in iDempiere)
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:
IdempiereApiClient- Optimized for Application Dictionary operationsGeneratedOpenApiFactory- Type-safe access to remaining 19 API categories
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:
- No formal API contract - Endpoints are hardcoded based on observed behavior
- Incomplete coverage - Only a subset of available endpoints are implemented
- No type safety - Request/response structures are manually defined
- Maintenance burden - API changes require manual updates to client code
- 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:
- 70+ REST endpoints across 18 categories
- Complete request/response schemas
- Authentication flows (JWT bearer tokens)
- OData-style query parameters ($filter, $select, $expand, $top, $skip)
- Examples for common operations
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:
- Bearer token (JWT) via
Authorization: Bearer {token} - Token obtained from
POST /auth/tokenswith username/password - Refresh via
POST /auth/refresh - Role/Org/Warehouse selection via
PUT /auth/tokens
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:
$filter- Query filter (e.g.,Name eq 'Test',ID in (1,2,3))$select- Select specific columns$expand- Expand related entities$orderby- Sort order$top/$skip- Pagination$valrule- Apply validation rule$context- Context variables for validationshowsql- Debug SQL generationlabel/showlabel- Label filtering
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
- Add OpenAPI Generator Maven plugin
- Generate Java client interfaces from spec
- 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:
- Copy new spec:
openapi/idempiere-openapi-v{version}.yml - Update symlink:
ln -sf idempiere-openapi-v{version}.yml idempiere-openapi-current.yml - 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
- Type Safety - Generated models match API schema exactly
- Complete Coverage - All 70+ endpoints available
- Auto-Updates - Regenerate when API changes
- Validation - Requests validated against schema
- Documentation - Generated Javadoc from OpenAPI descriptions
- Consistency - Same patterns across all endpoints
Negative
- Build Complexity - Code generation step in build
- Generated Code Size - ~50+ Java files generated
- Learning Curve - Team must understand generated patterns
- Customization - Generated code harder to customize
Neutral
- Migration Path - Existing
IdempiereClientbecomes facade - Testing - Mock generated interfaces for unit tests
Implementation Plan
Phase 1: Setup (v1.27.0) ✅ COMPLETED
- [x] Add OpenAPI Generator Maven plugin (v7.10.0)
- [x] Configure generation with
nativelibrary (Java HttpClient) - [x] Generate API classes (19 APIs, 162 models)
- [x] Add maven-antrun-plugin to clean problematic generated files
- [x] Add build-helper-maven-plugin to include generated sources
- [x] Create
GeneratedOpenApiFactory.javaCDI bean
Phase 2: Integration (v1.27.0) ✅ COMPLETED
- [x] Keep existing
IdempiereApiClientas primary (no facade) - [x] Create
GeneratedOpenApiFactoryfor supplementary API access - [x] Configure ApiClient with Bearer token auth
- [x] Project compiles with generated code
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)
- [ ] Add integration tests for generated APIs
- [ ] Mock HealthApi for DoctorCommand tests
- [ ] Test CachesApi operations
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 |