Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
254 changes: 150 additions & 104 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
# ZenBPM Java Client

> Spring Boot starter and core REST/gRPC client to interact with the ZenBPM process engine. Includes auto-configuration, OpenAPI-generated REST client, optional gRPC job workers, logging and OpenTelemetry hooks.
> Spring Boot starter for ZenBPM, plus a core artifact containing the generated REST APIs and gRPC stubs. Spring auto-configuration, `@JobWorker`, logging, and OpenTelemetry hooks are provided by the starter; gRPC workers also require a `ManagedChannel` provider.

## Features

* Project uses JDK 8+ and Spring Boot 2.7+ to support legacy systems, but can be easily adjusted to the newest JDK and Spring versions (see pom.xml).
* Spring Boot auto-configuration (drop-in starter)
* REST client (`ApiClient` + typed APIs generated from OpenAPI)
* gRPC job workers via `@JobWorker` and ZenbpmJobWorkerManager
* REST client (`ApiClient` plus typed APIs generated from OpenAPI)
* gRPC job workers via `@JobWorker` and `ZenbpmJobWorkerManager`
* OpenTelemetry interceptors for REST and spans for gRPC
* Configurable HTTP/gRPC logging

## Build this project

This repo uses [mise ](https://mise.jdx.dev/) (`mise.toml`) to pin the local Java and Maven versions used by CI.
This repo uses [mise](https://mise.jdx.dev/) (`mise.toml`) to pin the local Java and Maven versions used by CI.

First-time setup:

Expand All @@ -33,152 +32,199 @@ This runs `mvn -B clean package` with pinned Temurin 17 and Maven, without requi
For release validation, set the Maven artifact version from a release tag before building:

```bash
RELEASE_TAG=v1.4.0 mise run set-release-version
RELEASE_TAG=v1.5.0 mise run set-release-version
mise run verify-release
```

The release workflow downloads backend OpenAPI/proto sources before running these Maven steps. Git tags keep the `v` prefix, while Maven artifact versions are published without it.

## Releasing

Releases are triggered by the ZenBPM release orchestrator with `workflow_dispatch` input `version` set to the backend release tag, for example `v1.4.0`.
Releases are triggered by the ZenBPM release orchestrator with `workflow_dispatch` input `version` set to the backend release tag, for example `v1.5.0`.

The workflow downloads `openapi/api.yaml` and `pkg/zenclient/proto/zenbpm.proto` from the matching `pbinitiative/zenbpm` tag, sets Maven artifact versions from that tag without the `v` prefix, commits the generated release inputs, tags this repository, creates a GitHub Release, and publishes artifacts to Maven Central under `org.pbinitiative.zenbpm`.

Required repository secrets: `APP_ID_ZENBPM_RELEASE`, `APP_PRIVATE_KEY_ZENBPM_RELEASE`, `MAVEN_CENTRAL_USERNAME`, `MAVEN_CENTRAL_TOKEN`, `MAVEN_GPG_PRIVATE_KEY`, and `MAVEN_GPG_PASSPHRASE`.

## Getting started

Add the starter to your application and the core client as needed.
The Java client uses Maven group and Java package prefix `org.pbinitiative.zenbpm`.

### Compatibility

The ZenBPM Java client has been tested on:

- Java 17 and Spring Boot 3
- Java 8 and Spring Boot 2.7

The examples below target Java 17 and Spring Boot 3 and intentionally use modern Java syntax.

### Dependencies

For a minimal Spring Boot application, use the standard Boot starter together with the ZenBPM starter and a gRPC channel provider:

Maven:
```xml
<dependency>
<groupId>org.pbinitiative.zenbpm</groupId>
<artifactId>zenbpm-spring-boot-starter</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.pbinitiative.zenbpm</groupId>
<artifactId>zenbpm-client-core</artifactId>
<version>${project.version}</version>
</dependency>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.16</version>
<relativePath/>
</parent>

<properties>
<java.version>17</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.pbinitiative.zenbpm</groupId>
<artifactId>zenbpm-spring-boot-starter</artifactId>
<version>1.5.0</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-netty-shaded</artifactId>
<version>1.78.0</version>
</dependency>
</dependencies>
```

Configure connection settings in application.yml
Do **not** directly declare `org.pbinitiative.zenbpm:zenbpm-client-core` in a Spring application: `zenbpm-spring-boot-starter` brings `zenbpm-client-core` transitively. Client 1.5.0 supplies the gRPC APIs and stubs but no `ManagedChannel` provider, so `grpc-netty-shaded` is required when using `@JobWorker`; it is not an optional worker transport.

values shown in `zenbpm` section are defaults.
A non-Spring application may instead declare `org.pbinitiative.zenbpm:zenbpm-client-core:1.5.0` to use the generated REST APIs or gRPC stubs, but it must construct and configure the clients itself. gRPC use still requires a channel provider. `@JobWorker` and its auto-configuration are provided by the Spring Boot starter.

### Configuration

This tested `application.yml` uses kebab-case Spring properties and environment overrides for the engine endpoints:

`logging` section configures logging for rest and grpc clients separately.
- `DEBUG` levels expose headers of calls and responses.
- `TRACE` level exposes full request and response bodies. <b>Never use this in production!</b>
```yaml
zenbpm:
restUrl: http://localhost:8080/v1
restLoggingEnabled: true
grpcHost: localhost
grpcPort: 9090
grpcPlaintext: true
grpcLoggingEnabled: true
jobWorkerEnabled: true

otel.sdk.disabled: true

logging:
level:
root: INFO
org.pbinitiative.zenbpm.rest: TRACE
org.pbinitiative.zenbpm.grpc: DEBUG

rest-url: ${ZENBPM_REST_URL:http://localhost:8080/v1}
rest-logging-enabled: false
grpc-host: ${ZENBPM_GRPC_HOST:localhost}
grpc-port: ${ZENBPM_GRPC_PORT:9090}
# Local development only. Use gRPC TLS and an HTTPS REST URL in other environments.
grpc-plaintext: true
grpc-logging-enabled: false
job-worker-enabled: true

otel:
sdk:
disabled: true
```

## Working examples
This sample explicitly disables client logging. Set both logging flags to `false` in production, especially when process variables may contain sensitive data. If REST logging is enabled, OkHttp `BASIC` logging at DEBUG emits request/response lines, not headers or bodies. At TRACE, REST `BODY` logging can expose full HTTP data, and gRPC logging can expose job variables and results.

### REST: deploy and start

Place the example classes in the same package as, or a child package of, your `@SpringBootApplication` class so Spring discovers them. Replace the example `package` declaration with your application's package when needed.

### 1) Use REST APIs
Inject the provided ZenbpmClientService to obtain the ApiClient, then create a typed API.
`ZenbpmClientService` supplies the configured `ApiClient`. Deploy a `java.io.File` with `ProcessDefinitionApi`, then start with a business key and variables in `CreateProcessInstanceRequest`:

```java
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;
import org.pbinitiative.zenbpm.rest.ZenbpmClientService;
import org.pbinitiative.zenbpm.client.ApiException;
package com.example.zenbpmverification;

import java.io.File;
import java.util.Map;

import org.pbinitiative.zenbpm.client.ApiClient;
import org.pbinitiative.zenbpm.client.ApiException;
import org.pbinitiative.zenbpm.client.api.ProcessDefinitionApi;
import org.pbinitiative.zenbpm.client.api.ProcessInstanceApi;
import org.pbinitiative.zenbpm.client.api.dto.CreateProcessInstanceRequest;

import java.util.HashMap;
import java.util.Map;
import org.pbinitiative.zenbpm.client.api.dto.ProcessInstance;
import org.pbinitiative.zenbpm.rest.ZenbpmClientService;
import org.springframework.stereotype.Service;

@Service
public class MyService {
@Autowired
private ZenbpmClientService zenbpm;
public class ZenbpmRestService {

private final ApiClient apiClient;
private final ProcessDefinitionApi processDefinitionApi;
private final ProcessInstanceApi processInstanceApi;

public ZenbpmRestService(ZenbpmClientService zenbpmClientService) {
this.apiClient = zenbpmClientService.getApiClient();
this.processDefinitionApi = new ProcessDefinitionApi(apiClient);
this.processInstanceApi = new ProcessInstanceApi(apiClient);
}

public long deploy(File bpmnFile) throws ApiException {
return processDefinitionApi.createProcessDefinition(bpmnFile).getProcessDefinitionKey();
}

public ProcessInstance start(
long processDefinitionKey,
String businessKey,
Map<String, Object> variables) throws ApiException {
CreateProcessInstanceRequest request = new CreateProcessInstanceRequest()
.processDefinitionKey(processDefinitionKey)
.businessKey(businessKey)
.variables(variables);

return processInstanceApi.createProcessInstance(request);
}

public ProcessInstance get(long processInstanceKey) throws ApiException {
return processInstanceApi.getProcessInstance(processInstanceKey);
}
}
```

public Long deployExampleProcess() throws ApiException {
ApiClient apiClient = zenbpm.getApiClient();
ProcessDefinitionApi defApi = new ProcessDefinitionApi(apiClient);
### gRPC worker

// Example: create a process definition from a BPMN string (adjust to your endpoint contract)
String bpmnXml = "<definitions ...>...</definitions>";
Long definitionKey = defApi.createProcessDefinition(bpmnXml).getProcessDefinitionKey();
return definitionKey;
}
The worker must be a discovered Spring bean. This example validates that `email` is a nonblank `String` and returns a mock completion:

public void startMyProcess() throws ApiException {
ApiClient apiClient = zenbpm.getApiClient();
ProcessInstanceApi piApi = new ProcessInstanceApi(apiClient);
```java
package com.example.zenbpmverification;

Map<String, Object> vars = new HashMap<>();
vars.put("orderId", 12345L);
import java.util.Map;

CreateProcessInstanceRequest req = new CreateProcessInstanceRequest()
.processDefinitionKey(123456L)
.variables(vars);
import org.pbinitiative.zenbpm.grpc.JobContext;
import org.pbinitiative.zenbpm.grpc.JobWorker;
import org.springframework.stereotype.Component;

@Component
public class EmailWorker {

piApi.createProcessInstance(req);
}
/** Returns a mock confirmation; this worker does not send an actual email. */
@JobWorker("send-email")
public Map<String, Object> sendEmail(JobContext context) {
Object emailValue = context.getVariables().get("email");
if (!(emailValue instanceof String email) || email.isBlank()) {
throw new IllegalArgumentException("Job variable 'email' must be a nonblank String");
}

return Map.of(
"emailSent", true,
"confirmation", "Mock email confirmation for " + email);
}
}
```

Notes:
- Available typed APIs include ProcessDefinitionApi, ProcessInstanceApi, JobApi, MessageApi, etc. Construct them with the provided ApiClient.
- Methods and DTOs come from the generated package `org.pbinitiative.zenbpm.client.api` and `org.pbinitiative.zenbpm.client.api.dto`.
An exception from the method fails the job. The returned map is serialized as job-completion output variables. The worker manager connects only when `zenbpm.job-worker-enabled` is true **and** at least one annotated worker has been discovered.

### 2) Register a gRPC job worker
Create a Spring bean with a method annotated by `@JobWorker`. Accepted method signatures:
- no parameters
- one parameter of type `org.pbinitiative.zenbpm.proto.Zenbpm.WaitingJob`
- one parameter of type `org.pbinitiative.zenbpm.grpc.JobContext`
- one parameter of type `Map<String, Object>`
### Tested flow

Return value can be any object and will be serialized as variables for job completion. Throwing an exception fails the job.
The BPMN service task's job type must match the annotation. Declare `xmlns:zenbpm="http://zenbpm.pbinitiative.org/1.0"` on the BPMN definitions and place the task definition and output mappings inside the service task's extension elements:

```java
import org.springframework.stereotype.Component;
import org.pbinitiative.zenbpm.grpc.JobWorker;
import org.pbinitiative.zenbpm.grpc.JobContext;
import java.util.Map;
import java.util.HashMap;

@Component
public class EmailWorker {
@JobWorker("send-email")
public Map<String, Object> handleJob(JobContext ctx) {
Map<String,Object> vars = ctx.getVariables();
String to = (String) vars.get("email");

// send email ...

Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("message", "Email to " + to + " mocked successfully");
return result;
}
}
```xml
<bpmn:serviceTask id="SendEmailTask" name="Send email">
<bpmn:extensionElements>
<zenbpm:taskDefinition type="send-email" retries="3"/>
<zenbpm:ioMapping>
<zenbpm:output source="=emailSent" target="emailSent"/>
<zenbpm:output source="=confirmation" target="confirmation"/>
</zenbpm:ioMapping>
</bpmn:extensionElements>
</bpmn:serviceTask>
```

The gRPC worker manager connects on application start if `zenbpm.jobWorkerEnabled` is true.
Deploy the BPMN `File` and start the instance through `ZenbpmRestService`, passing `Map.of("email", "customer@example.com")` and a business key. The `EmailWorker` then receives and completes the `send-email` job over gRPC; the mappings copy its `emailSent` and `confirmation` outputs into process variables.

---

Expand Down