ff41556b9a
Client.Java-001: redactApiKey echoed the last 4 secret characters. It now keeps only the non-secret mxgw_<key-id>_ prefix plus ***; non-gateway-shaped tokens return <redacted>. Client.Java-002: a close() after a queue-overflow could wipe the enqueued overflow exception. Terminal transitions are now serialized through a single guarded terminate() — first terminal condition wins. Client.Java-003: openSession never read gateway_protocol_version. Both openSession paths now call ensureGatewayProtocolCompatible, rejecting a non-zero mismatch and accepting unset (0) for older gateways. Client.Java-004: register/addItem/addItem2 fell back to a return_value that silently yields 0 when unset. The fallback is now guarded by hasReturnValue() and throws on a protocol violation. Client.Java-005: close() in try-with-resources could mask the body exception when the CloseSession RPC failed. close() now catches and logs the close-time failure; closeRaw() still surfaces it for callers that want it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
241 lines
10 KiB
Markdown
241 lines
10 KiB
Markdown
# 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/
|
|
mxgateway-client/
|
|
mxgateway-cli/
|
|
```
|
|
|
|
`mxgateway-client` generates Java protobuf and gRPC sources from
|
|
`../../src/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.
|
|
|
|
`mxgateway-client` exposes `MxGatewayClientOptions`, `MxGatewayClient`,
|
|
`MxGatewaySession`, value/status helpers, typed gateway exceptions, raw
|
|
generated stubs, and generated protobuf messages for parity tests.
|
|
|
|
`mxgateway-cli` depends on `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 :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);
|
|
}
|
|
```
|
|
|
|
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.
|
|
|
|
`openSession` verifies the gateway's reported `gateway_protocol_version` against
|
|
the version this client was generated for and throws `MxGatewayException` on a
|
|
mismatch, so an incompatible client fails fast with a clear message instead of
|
|
issuing commands that fail downstream. A gateway that does not populate the
|
|
field is accepted unchanged.
|
|
|
|
`MxGatewaySession` implements `AutoCloseable`. The try-with-resources `close()`
|
|
performs a `CloseSession` network RPC but swallows (and logs) any failure of
|
|
that RPC so a close-time error never replaces the exception a try-with-resources
|
|
body is already propagating. Call `closeRaw()` explicitly when you need to
|
|
observe the close result or handle a close-time 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:
|
|
|
|
```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`, `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.
|
|
|
|
```powershell
|
|
gradle :mxgateway-cli:run --args="galaxy-test --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --json"
|
|
gradle :mxgateway-cli:run --args="galaxy-deploy-time --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --json"
|
|
gradle :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`.
|
|
|
|
```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 :mxgateway-cli:run --args="galaxy-watch --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --json"
|
|
gradle :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 :mxgateway-cli:run --args="version --json"
|
|
gradle :mxgateway-cli:run --args="open-session --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --client-session-name java-cli --json"
|
|
gradle :mxgateway-cli:run --args="register --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --session-id <id> --client-name java-cli --json"
|
|
gradle :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 :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 :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 :mxgateway-cli:run --args="stream-events --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --session-id <id> --limit 1 --json"
|
|
gradle :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:
|
|
|
|
```powershell
|
|
gradle :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 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`:
|
|
|
|
```powershell
|
|
gradle :mxgateway-client:jar :mxgateway-cli:installDist
|
|
```
|
|
|
|
The library jar is under `mxgateway-client/build/libs`. The installed CLI
|
|
distribution is under `mxgateway-cli/build/install/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 :mxgateway-cli:run --args="smoke --endpoint $env:MXGATEWAY_ENDPOINT --plaintext --api-key-env MXGATEWAY_API_KEY --item $env:MXGATEWAY_TEST_ITEM --json"
|
|
```
|
|
|
|
## 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)
|