Files
mxaccessgw/clients/java
Joseph Doherty 71d2c39f01 e2e: port batch subcommand to all five client CLIs
scripts/run-client-e2e-tests.ps1 expects each language CLI to expose a
`batch` subcommand that reads command lines from stdin, runs each
through the normal subcommand dispatch, writes the JSON result, then
a sentinel line `__MXGW_BATCH_EOR__`. The implementation lived on a
divergent branch (commit 6126099) that was never merged into main —
this commit ports the same protocol to HEAD's renamed CLIs so the
existing matrix script runs end-to-end.

The protocol:
  - one line of stdin = one full CLI invocation
  - successful output → stdout, then __MXGW_BATCH_EOR__
  - failure → {"error":"...","type":"error"} JSON on stdout, then
    __MXGW_BATCH_EOR__ (errors do NOT exit the loop)
  - empty line or EOF terminates the loop

Per-CLI additions:

  .NET: RunBatchAsync + per-line StringWriter capture, JSON error
    envelope when forceJsonErrors is true. Two new tests in
    MxGatewayClientCliTests covering the success and error paths.

  Go:   runBatch with bufio.Scanner, runs each line through the
    existing runWithIO switch with a buffered stdout writer. One new
    test pinning the EOR sentinel.

  Rust: new `Batch` variant on the clap Command enum, run_batch
    re-parses each line via Cli::try_parse_from. Two new tests in the
    inline mod tests block.

  Python: new `batch` click command in commands.py that uses
    CliRunner to dispatch each line; synthesises {"error",..."type"}
    JSON from click error messages when the captured output isn't
    already JSON-shaped. Three new tests in test_cli.py.

  Java: BatchCommand inner @Command with BufferedReader stdin loop,
    fresh commandLine() per dispatch with captured stdout/stderr
    PrintWriters; non-zero exit codes and uncaught exceptions both
    surface as JSON-error blocks. Two new tests.

Also fixes scripts/run-client-e2e-tests.ps1 line 705: the Python
invocation was still passing the old module name `mxgateway_cli` to
`python -m`; the client SDK rename in 397d3c5 moved it to
`zb_mom_ww_mxgateway_cli`. Without the fix the Python leg fails
with "No module named mxgateway_cli" before reaching open-session.

Verification: full matrix at the redeployed gateway (localhost:5120,
running ZB.MOM.WW.MxGateway.Server.exe / ZB.MOM.WW.MxGateway.Worker.exe)
with -SkipBulk -SkipReadWriteBulk -SkipParity -SkipAuth (those phases
exercise bulk read/write CLI subcommands that also live on the
divergent branch — porting those is a follow-up). All five clients
report `closed=true, addedItems=120, eventCount=5` and overall
`success=true`. Per-language unit tests pass:
  - dotnet: 59/59
  - go:     all packages clean
  - rust:   cargo test --workspace clean
  - python: 42/42
  - java:   gradle build SUCCESSFUL

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 04:08:15 -04:00
..
2026-05-24 03:21:06 -04:00

Java Client

The Java client workspace contains the MXAccess Gateway client library, generated protobuf/gRPC bindings, a Picocli test CLI project, and JUnit tests.

Layout

clients/java/
  settings.gradle
  build.gradle
  src/main/generated/
  zb-mom-ww-mxgateway-client/
  zb-mom-ww-mxgateway-cli/

zb-mom-ww-mxgateway-client generates Java protobuf and gRPC sources from ../../src/ZB.MOM.WW.MxGateway.Contracts/Protos. The Gradle protobuf plugin writes those generated sources under src/main/generated, which matches the client proto manifest in ../proto/proto-inputs.json. Do not edit generated files by hand.

zb-mom-ww-mxgateway-client exposes MxGatewayClientOptions, MxGatewayClient, MxGatewaySession, value/status helpers, typed gateway exceptions, raw generated stubs, and generated protobuf messages for parity tests.

zb-mom-ww-mxgateway-cli depends on zb-mom-ww-mxgateway-client and provides the mxgw-java application entry point. The CLI supports version, session, command, event streaming, write, and smoke-test commands with deterministic JSON output.

Regenerating Protobuf Bindings

Run generation from clients/java after the shared .proto files or Java output path changes:

gradle :zb-mom-ww-mxgateway-client:generateProto

Client Usage

Create a client with explicit transport and auth options:

MxGatewayClientOptions options = MxGatewayClientOptions.builder()
        .endpoint("localhost:5000")
        .apiKey(System.getenv("MXGATEWAY_API_KEY"))
        .plaintext(true)
        .build();

try (MxGatewayClient client = MxGatewayClient.connect(options);
        MxGatewaySession session = client.openSession("java-client")) {
    int serverHandle = session.register("java-client");
    int itemHandle = session.addItem(serverHandle, "TestObject.TestInt");
    session.advise(serverHandle, itemHandle);
    session.write(serverHandle, itemHandle, MxValues.int32Value(123), 0);
}

Use rawBlockingStub, rawFutureStub, rawAsyncStub, openSessionRaw, closeSessionRaw, invoke, and raw session helper methods when tests need the underlying protobuf messages. MxGatewayCommandException and MxAccessException preserve the raw MxCommandReply when the gateway returns a data-bearing MXAccess failure.

MxEventStream implements Iterator<MxEvent> and AutoCloseable. Closing it cancels the underlying gRPC stream. Canceling or timing out a Java client call only stops the client from waiting; it does not abort an in-flight MXAccess COM call on the worker STA.

Galaxy Repository Browse

The Galaxy Repository service is a separate metadata-only gRPC service exposed by the gateway. It lets clients enumerate the deployed Galaxy object hierarchy and the dynamic attributes on each object so they know which tag references to subscribe to via the MXAccess Gateway service. It uses the same API-key auth as the gateway and requires the metadata:read scope.

GalaxyRepositoryClient mirrors the MxGatewayClient pattern (caller-managed or owned channel, MxGatewayClientOptions, blocking + async variants). Three RPCs are exposed:

MxGatewayClientOptions options = MxGatewayClientOptions.builder()
        .endpoint("localhost:5000")
        .apiKey(System.getenv("MXGATEWAY_API_KEY"))
        .plaintext(true)
        .build();

try (GalaxyRepositoryClient galaxy = GalaxyRepositoryClient.connect(options)) {
    boolean ok = galaxy.testConnection();
    Optional<Instant> lastDeploy = galaxy.getLastDeployTime();
    List<GalaxyObject> hierarchy = galaxy.discoverHierarchy();
}

getLastDeployTime returns Optional.empty() when the server reports present=false. discoverHierarchy returns the generated GalaxyObject proto messages directly so callers can read all fields (including the nested GalaxyAttribute list) without an extra DTO layer.

The CLI exposes matching subcommands: galaxy-test, galaxy-deploy-time, galaxy-discover, and galaxy-watch. They take the same --endpoint, --api-key-env, --plaintext, --ca-file, --server-name-override, --timeout, and --json options as the gateway commands.

gradle :zb-mom-ww-mxgateway-cli:run --args="galaxy-test --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --json"
gradle :zb-mom-ww-mxgateway-cli:run --args="galaxy-deploy-time --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --json"
gradle :zb-mom-ww-mxgateway-cli:run --args="galaxy-discover --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --json"

Watching deploy events

GalaxyRepository.WatchDeployEvents is a server-streaming RPC: the gateway sends a bootstrap DeployEvent immediately on subscribe and then one event each time it observes a new galaxy.time_of_last_deploy. The sequence field is monotonic per server start; gaps mean the per-subscriber buffer dropped older events because the consumer was too slow.

The client exposes both an iterator-style adaptor over the async stub and an observer-callback variant. Both honour the channel-level streamTimeout.

try (GalaxyRepositoryClient galaxy = GalaxyRepositoryClient.connect(options);
        DeployEventStream events = galaxy.watchDeployEvents(/* lastSeenDeployTime */ null)) {
    while (events.hasNext()) {
        DeployEvent event = events.next();
        // event.getSequence(), event.getObservedAt(),
        // event.getTimeOfLastDeploy() / getTimeOfLastDeployPresent(),
        // event.getObjectCount(), event.getAttributeCount()
    }
}

Pass an Instant for lastSeenDeployTime to suppress the bootstrap event when the cached deploy time matches what the caller already has. DeployEventStream implements Iterator<DeployEvent> and AutoCloseable; closing it cancels the underlying gRPC call.

For callback delivery (e.g. when the consumer wants to drive a queue or reactive pipeline), use the async variant:

DeployEventSubscription subscription = galaxy.watchDeployEventsAsync(
        lastSeen,
        new StreamObserver<>() {
            @Override public void onNext(DeployEvent value) { /* ... */ }
            @Override public void onError(Throwable t) { /* ... */ }
            @Override public void onCompleted() { /* ... */ }
        });
// later:
subscription.cancel(); // or subscription.close()

The matching CLI subcommand streams events until cancelled (Ctrl+C) and prints one line per event in text mode or one JSON object per event with --json:

gradle :zb-mom-ww-mxgateway-cli:run --args="galaxy-watch --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --json"
gradle :zb-mom-ww-mxgateway-cli:run --args="galaxy-watch --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --last-seen-deploy-time 2026-04-28T18:30:00Z --limit 5"

CLI Usage

Run the CLI through Gradle:

gradle :zb-mom-ww-mxgateway-cli:run --args="version --json"
gradle :zb-mom-ww-mxgateway-cli:run --args="open-session --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --client-session-name java-cli --json"
gradle :zb-mom-ww-mxgateway-cli:run --args="register --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --session-id <id> --client-name java-cli --json"
gradle :zb-mom-ww-mxgateway-cli:run --args="add-item --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --session-id <id> --server-handle 1 --item TestObject.TestInt --json"
gradle :zb-mom-ww-mxgateway-cli:run --args="advise --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --session-id <id> --server-handle 1 --item-handle 1 --json"
gradle :zb-mom-ww-mxgateway-cli:run --args="write --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --session-id <id> --server-handle 1 --item-handle 1 --type int32 --value 123 --json"
gradle :zb-mom-ww-mxgateway-cli:run --args="stream-events --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --session-id <id> --limit 1 --json"
gradle :zb-mom-ww-mxgateway-cli:run --args="smoke --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --item TestObject.TestInt --json"

The CLI accepts --api-key, --api-key-env, --plaintext, --ca-file, --server-name-override, --timeout, and --json on gateway commands. JSON output redacts API keys.

Use TLS options for a secured gateway:

gradle :zb-mom-ww-mxgateway-cli:run --args="smoke --endpoint mxgateway.example.local:5001 --ca-file C:\certs\mxgateway-ca.pem --server-name-override mxgateway.example.local --api-key-env MXGATEWAY_API_KEY --item TestObject.TestInt --json"

Build And Test

Run the Java checks from clients/java:

gradle test

The build uses the Java 21 Gradle toolchain, compiles generated protobuf/gRPC code, and runs JUnit 5 tests for the client wrapper, shared behavior fixtures, in-process gRPC behavior, stream cancellation, and CLI parser/output behavior.

Packaging

Create local library and CLI artifacts from clients/java:

gradle :zb-mom-ww-mxgateway-client:jar :zb-mom-ww-mxgateway-cli:installDist

The library jar is under zb-mom-ww-mxgateway-client/build/libs. The installed CLI distribution is under zb-mom-ww-mxgateway-cli/build/install/zb-mom-ww-mxgateway-cli.

Integration Checks

Run live checks only when a gateway and MXAccess-backed worker are available:

$env:MXGATEWAY_INTEGRATION = '1'
$env:MXGATEWAY_ENDPOINT = 'localhost:5000'
$env:MXGATEWAY_API_KEY = '<gateway-api-key>'
$env:MXGATEWAY_TEST_ITEM = 'TestObject.TestInt'
gradle :zb-mom-ww-mxgateway-cli:run --args="smoke --endpoint $env:MXGATEWAY_ENDPOINT --plaintext --api-key-env MXGATEWAY_API_KEY --item $env:MXGATEWAY_TEST_ITEM --json"