# 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

```text
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:

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

## Client Usage

Create a client with explicit transport and auth options:

```java
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);
}
```

The gateway can auto-generate its own self-signed certificate (it has no PKI), so
the client is **lenient by default**: a TLS connection (`plaintext(false)`) with
no `caCertificatePath` accepts whatever certificate the gateway presents (via
grpc-netty-shaded's `InsecureTrustManagerFactory`). To verify instead, set
`caCertificatePath` to pin a CA, or set `requireCertificateValidation(true)` to
verify against the JVM trust store without pinning. Use `serverNameOverride` /
`--server-name-override` when the dialed host differs from the certificate SAN.
See
[Gateway Configuration](../../docs/GatewayConfiguration.md#automatic-self-signed-certificate).

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. It is a **single-consumer** surface: drive
`hasNext()`/`next()` (or `nextItem()`) from one thread only.

### Reconnect-replay gap signal

When you resume a stream with `streamEventsAfter(afterWorkerSequence)` and the
requested cursor predates the oldest event the gateway still retains, the
gateway emits a single **replay-gap sentinel** at the head of the stream: an
`MxEvent` with its `replay_gap` field set, `family` unspecified, and the body
oneof unset. It means "you missed events — discard cached state and
re-snapshot": the events in the open interval `(requested_after_sequence,
oldest_available_sequence)` were evicted and cannot be replayed. The gateway
never synthesizes this signal from anything else, and the client never swallows
it.

Use `nextItem()` to branch on it as a distinct typed item; `next()` still
returns the sentinel as a plain `MxEvent` (test `event.hasReplayGap()`). After a
gap, re-snapshot, then resume without another gap by requesting
`oldestAvailableSequence - 1` as the next `afterWorkerSequence`:

```java
try (MxEventStream events = session.streamEventsAfter(lastSeenSequence)) {
    while (events.hasNext()) {
        MxEventStreamItem item = events.nextItem();
        if (item.isReplayGap()) {
            long resumeFrom = item.replayGap().getOldestAvailableSequence() - 1;
            // discard cached per-item state, re-snapshot, then resume from resumeFrom
            continue;
        }
        MxEvent event = item.event();
        // normal event handling
    }
}
```

For alarms, `MxGatewayClient` exposes `queryActiveAlarms` (one-shot snapshot),
`streamAlarms` (returns an `MxGatewayAlarmFeedSubscription` whose iterator
yields alarm-feed messages from the gateway's central monitor), and
`acknowledgeAlarm` (ack by full alarm reference with an optional comment and
ack target). Close the subscription to cancel the underlying gRPC stream.

`ActiveAlarmSnapshot.getFromTruncatedSnapshot()` reports that the record came
from a provider fetch which hit the per-fetch cap: the snapshot set may omit
active alarms, and the gateway suspended its absence-implies-cleared inference
for that poll. Treat the set as possibly incomplete rather than reconciling
deletions from it. It is set-level degraded status, not a comment on the
record's own fidelity, and is distinct from `getDegraded()` (the subtag
fallback provider).

`streamAlarms` also carries that completeness verdict at feed level, as a
message whose `getPayloadCase()` is `SNAPSHOT_STATUS` and whose
`getSnapshotStatus().getTruncated()` is true while the monitor's cached set
derives from a truncated fetch. One arrives at stream open (after the
`PROVIDER_STATUS` frame, before the cached `ACTIVE_ALARM` frames) so a late
joiner learns the current verdict, then one on every verdict change — including
the clearing frame sent when the gateway's alarm monitor restarts and drops a
truncated verdict. Track it if you need set completeness on a live feed without
polling `queryActiveAlarms`.

## Write Semantics And Common Pitfalls

These are MXAccess parity behaviors that surprise new callers. The gateway
forwards them unchanged — it does not paper over them.

### Typed single-item command helpers

`MxGatewaySession` exposes typed helpers for the parity-critical single-item
commands, so you do not need to build raw `MxCommand` messages:

- `adviseSupervisory(serverHandle, itemHandle)` (and `adviseSupervisoryRaw`)
- `writeSecured(serverHandle, itemHandle, currentUserId, verifierUserId, value)`
  and `writeSecured2(..., timestampValue)` (plus `*Raw` variants)
- `authenticateUser(serverHandle, verifyUser, verifyUserPassword)` → user id
- `archestrAUserToId(serverHandle, userIdGuid)` → user id
- `addBufferedItem(serverHandle, itemDefinition, itemContext)` → item handle
- `setBufferedUpdateInterval(serverHandle, updateIntervalMs)`
- `suspend(serverHandle, itemHandle)` / `activate(serverHandle, itemHandle)` →
  the reply's `MxStatusProxy`

All of them run the same MXAccess reply validation as the bulk helpers (protocol
status plus HRESULT/`MxStatusProxy` check) via the shared `invoke` path, so an
MXAccess COM-side failure surfaces as `MxAccessException`. That validation
follows COM semantics: only a **negative** HRESULT is a failure, so positive
success codes such as `S_FALSE` (1) pass. `MxStatuses.succeeded` judges each
entry by its category — an entry fails when its category is not
`MX_STATUS_CATEGORY_OK`, and the raw `success` member is a diagnostic that never
decides the verdict. A `null` entry is success.

**Secret redaction.** Credentials passed to `authenticateUser` (and the
credential-sensitive values passed to `writeSecured`/`writeSecured2`) travel
only in the request. They never appear in logs, exception messages, or
`toString()`: gateway status text is scrubbed through `MxGatewaySecrets`, and
MXAccess failures carry only the reply (never the request). Do not log the
credentials yourself.

### Attributing a write to a user without `authenticateUser`

MXAccess only stamps a plain `write`/`write2` with a Galaxy user id when the
item carries an active *supervisory* advise. If you are **not** using the
verified/secured path (`authenticateUser` → `writeSecured`/`writeSecured2`) but
still need the write attributed to a user id, first advise the item supervisory
and then pass that user id on the write. Without the supervisory advise the
`userId` on a plain write is ignored.

```java
session.adviseSupervisory(serverHandle, itemHandle);
session.write(serverHandle, itemHandle, value, userId);
```

**MXAccess parity:** `writeSecured` failing before a prior `authenticateUser` +
`adviseSupervisory`, or before a value-bearing body, is correct behavior — the
native failure is surfaced, not papered over.

The CLI exposes `advise-supervisory`, `write-secured`, and `authenticate-user`
(credential via `--password` or the variable named by `--password-env`, default
`MXGATEWAY_VERIFY_PASSWORD`, never echoed), and `write` / `write2` take
`--user-id`. The credential is required: a missing or empty resolved value is a
picocli usage error naming the option and the variable, so the CLI fails before
connecting instead of authenticating with an empty password.
`MXGATEWAY_VERIFY_PASSWORD` is the canonical variable across all five client
CLIs — see [Cross-Language Smoke Matrix](../../docs/CrossLanguageSmokeMatrix.md).

### Array writes replace the whole array

A write to an array attribute **replaces the entire array**; it is not an
element-wise patch. To change a subset of elements, send the full array with
the unchanged elements included. For example, to change 2 elements of a
20-element array, build the `MxValue` from all 20 values (the 18 unchanged plus
the 2 new ones). Sending only the 2 changed values overwrites the attribute
with a 2-element array.

When only a few indices need changing and the rest should be reset to the
element type's default, use `writeArrayElements` instead of building the full
array manually:

```java
session.writeArrayElements(
        serverHandle, itemHandle,
        MxDataType.MX_DATA_TYPE_INTEGER,
        20, // totalLength
        Map.of(
                2, MxValues.int32Value(42),
                7, MxValues.int32Value(99)),
        userId);
```

The gateway expands the sparse descriptor into a full `totalLength`-element
array before forwarding to the worker. Indices not listed in the map are
written as the element type's default — this is a **reset**, not a preserve;
current values at those positions are discarded. `totalLength` is required and
must match the declared length of the array attribute. Bare-name array items
(`Area001.Pump001.Speed`) are auto-normalized to the `[]` form across the whole
add family — `AddItem`, `AddItem2`, `AddItemBulk`, and `AddBufferedItem` — so the
array attribute accepts the write.

## 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:

```java
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-connection`,
`galaxy-last-deploy`, `galaxy-discover`, `galaxy-browse`, and `galaxy-watch`.
The short names `galaxy-test` and `galaxy-deploy-time` remain as deprecated
aliases for `galaxy-test-connection` and `galaxy-last-deploy` so existing
scripts keep working. They take the same `--endpoint`, `--api-key-env`,
`--plaintext`, `--ca-file`, `--server-name-override`, `--timeout`, and `--json`
options as the gateway commands.

```powershell
gradle :zb-mom-ww-mxgateway-cli:run --args="galaxy-test-connection --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --json"
gradle :zb-mom-ww-mxgateway-cli:run --args="galaxy-last-deploy --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"
```

`galaxy-browse` walks the hierarchy via `BrowseChildren`. Without `--parent` it
returns the root nodes and eagerly expands `--depth` further levels; with
`--parent <gobject-id>` it returns exactly one level of children for that
parent. The filter flags (`--category-ids`, `--template-contains`,
`--tag-name-glob`, `--alarm-bearing-only`, `--historized-only`,
`--include-attributes`) match `galaxy-discover`. The `--json` node shape is the
cross-client browse surface: the flattened object fields plus a
`hasChildrenHint` flag and a nested `children` array.

```powershell
gradle :zb-mom-ww-mxgateway-cli:run --args="galaxy-browse --depth 1 --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --json"
```

### Browsing lazily

For UI trees or OPC UA bridges, use `browseChildrenRaw` to walk one level at a
time instead of loading the full hierarchy with `discoverHierarchy`. Pass a
default request for root objects; subsequent calls set `parentGobjectId`,
`parentTagName`, or `parentContainedPath`. Filter fields match
`DiscoverHierarchy`. Each response pairs `getChildrenList()` with
`getChildHasChildrenList()` so you know which nodes to expand. See
[Galaxy Repository](../../docs/GalaxyRepository.md#browsechildren) for full
request and filter semantics. For most callers the high-level
`browse()`/`LazyBrowseNode` walker below is the preferred surface;
`browseChildrenRaw` exposes the single underlying RPC when you need direct
control of paging.

```java
BrowseChildrenReply reply = galaxy.browseChildrenRaw(
        BrowseChildrenRequest.newBuilder().build());

List<GalaxyObject> children = reply.getChildrenList();
List<Boolean> hasChildren = reply.getChildHasChildrenList();
for (int i = 0; i < children.size(); i++) {
    System.out.printf("%s expand=%b%n", children.get(i).getTagName(), hasChildren.get(i));
}
```

#### High-level walker

For UI trees, the client provides a `LazyBrowseNode` walker that handles
sibling pagination and the `child_has_children` hint for you:

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

try (GalaxyRepositoryClient galaxy = GalaxyRepositoryClient.connect(options)) {
    List<LazyBrowseNode> roots = galaxy.browse();
    for (LazyBrowseNode root : roots) {
        if (root.hasChildrenHint()) {
            root.expand();
        }
        for (LazyBrowseNode child : root.getChildren()) {
            String kind = child.hasChildrenHint() ? "has children" : "leaf";
            System.out.println(child.getObject().getTagName() + " (" + kind + ")");
        }
    }
}
```

`expand` is idempotent — calling it twice fires only one RPC,
and is safe under concurrent callers. To refresh after a Galaxy redeploy, call
`browse` again from the root.

### 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`.

```java
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:

```java
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`:

```powershell
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:

```powershell
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="ping --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --session-id <id> --message hello --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="advise-supervisory --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="authenticate-user --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --session-id <id> --server-handle 1 --verify-user operator --password-env MXGATEWAY_VERIFY_PASSWORD --json"
gradle :zb-mom-ww-mxgateway-cli:run --args="write-secured --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --session-id <id> --server-handle 1 --item-handle 1 --type int32 --value 123 --current-user-id 100 --verifier-user-id 100 --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="stream-alarms --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --filter-prefix Galaxy --limit 1 --json"
gradle :zb-mom-ww-mxgateway-cli:run --args="acknowledge-alarm --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --reference \"\\Galaxy\Area001.Pump001.PumpFault\" --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`, `--require-certificate-validation`, `--timeout`, and
`--json` on gateway commands. JSON output redacts API keys. TLS is lenient by
default (the certificate is not verified unless you pin a CA with `--ca-file`);
pass `--require-certificate-validation` to verify the server certificate against
the JVM trust store without pinning.

Use TLS options for a secured gateway:

```powershell
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`:

```powershell
gradle test
```

The build uses the Java 17 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`:

```powershell
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:

```powershell
$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"
```

## Installing from the Gitea Maven repository

The client publishes to the internal Gitea Maven repository at
`https://gitea.dohertylan.com/api/packages/dohertj2/maven`.

In your consumer project's `build.gradle`:

````groovy
repositories {
    maven {
        url 'https://gitea.dohertylan.com/api/packages/dohertj2/maven'
        credentials {
            username = System.getenv('GITEA_USERNAME')
            password = System.getenv('GITEA_TOKEN')
        }
    }
}

dependencies {
    implementation 'com.zb.mom.ww.mxgateway:zb-mom-ww-mxgateway-client:0.2.1'
}
````

To publish a new version from this repo:

````bash
export GITEA_USERNAME=dohertj2
export GITEA_TOKEN=<your-gitea-token>
gradle :zb-mom-ww-mxgateway-client:publish
````

## Related Documentation

- [Client Packaging](../../docs/ClientPackaging.md)
- [Client Proto Generation](../../docs/ClientProtoGeneration.md)
- [Java Client Detailed Design](./JavaClientDesign.md)
- [Java Style Guide](../../docs/style-guides/JavaStyleGuide.md)
