Add idiomatic documentation to Go, Java, Python, and Rust clients

This commit is contained in:
Joseph Doherty
2026-04-30 12:04:46 -04:00
parent eed1e88a37
commit 8d3352f2c6
44 changed files with 1631 additions and 102 deletions
@@ -38,6 +38,10 @@ import picocli.CommandLine.Model.CommandSpec;
import picocli.CommandLine.Option;
import picocli.CommandLine.Spec;
/**
* Picocli entry point for the {@code mxgw-java} test CLI used by the
* cross-language smoke matrix.
*/
@Command(
name = "mxgw-java",
mixinStandardHelpOptions = true,
@@ -48,6 +52,9 @@ public final class MxGatewayCli implements Callable<Integer> {
@Spec
private CommandSpec spec;
/**
* Builds a CLI bound to the default gRPC client factory.
*/
public MxGatewayCli() {
this(new GrpcMxGatewayCliClientFactory());
}
@@ -56,11 +63,25 @@ public final class MxGatewayCli implements Callable<Integer> {
this.clientFactory = clientFactory;
}
/**
* Process entry point.
*
* @param args command-line arguments
*/
public static void main(String[] args) {
int exitCode = commandLine(new GrpcMxGatewayCliClientFactory()).execute(args);
System.exit(exitCode);
}
/**
* Test-friendly entry point that runs the CLI against the supplied
* {@link PrintWriter} pair instead of the system streams.
*
* @param out writer that receives standard output
* @param err writer that receives standard error
* @param args command-line arguments
* @return the picocli exit code
*/
public static int execute(PrintWriter out, PrintWriter err, String... args) {
return execute(new GrpcMxGatewayCliClientFactory(), out, err, args);
}
@@ -280,6 +301,9 @@ public final class MxGatewayCli implements Callable<Integer> {
return values;
}
/**
* Picocli subcommand that prints the client and protocol version numbers.
*/
@Command(name = "version", description = "Prints the Java client version.")
public static final class VersionCommand implements Callable<Integer> {
@Spec
@@ -45,6 +45,11 @@ public final class DeployEventSubscription implements AutoCloseable {
};
}
/**
* Cancels the underlying gRPC call. Safe to invoke before the call has
* started; cancellation is recorded and applied as soon as the stream
* attaches.
*/
public void cancel() {
cancelled.set(true);
ClientCallStreamObserver<WatchDeployEventsRequest> stream = requestStream.get();
@@ -52,8 +52,12 @@ public final class GalaxyRepositoryClient implements AutoCloseable {
}
/**
* Construct a client over a caller-managed {@link Channel}. The caller owns
* Constructs a client over a caller-managed {@link Channel}. The caller owns
* channel lifecycle; {@link #close()} is a no-op for this constructor.
*
* @param channel the gRPC channel to use for outbound calls
* @param options the client options carrying the API key and timeouts
* @throws NullPointerException if {@code options} is {@code null}
*/
public GalaxyRepositoryClient(Channel channel, MxGatewayClientOptions options) {
this.ownedChannel = null;
@@ -64,25 +68,49 @@ public final class GalaxyRepositoryClient implements AutoCloseable {
asyncStub = GalaxyRepositoryGrpc.newStub(intercepted);
}
/** Build a new client and own its channel; close shuts the channel down. */
/**
* Builds a new client and owns its channel; {@link #close()} shuts the
* channel down.
*
* @param options the client options carrying the endpoint and credentials
* @return a connected client
*/
public static GalaxyRepositoryClient connect(MxGatewayClientOptions options) {
return new GalaxyRepositoryClient(createChannel(options), options);
}
/**
* Returns the underlying blocking stub with the per-call deadline applied.
*
* @return the blocking stub
*/
public GalaxyRepositoryGrpc.GalaxyRepositoryBlockingStub rawBlockingStub() {
return withDeadline(blockingStub);
}
/**
* Returns the underlying future stub with the per-call deadline applied.
*
* @return the future stub
*/
public GalaxyRepositoryGrpc.GalaxyRepositoryFutureStub rawFutureStub() {
return withDeadline(futureStub);
}
/**
* Returns the underlying async stub. Stream deadlines are applied per call.
*
* @return the async stub
*/
public GalaxyRepositoryGrpc.GalaxyRepositoryStub rawAsyncStub() {
return asyncStub;
}
/**
* Invoke the {@code TestConnection} RPC and return the {@code ok} flag.
* Invokes the {@code TestConnection} RPC and returns the {@code ok} flag.
*
* @return {@code true} when the gateway reached the Galaxy Repository database
* @throws MxGatewayException on transport or protocol failure
*/
public boolean testConnection() {
try {
@@ -96,14 +124,23 @@ public final class GalaxyRepositoryClient implements AutoCloseable {
}
}
/**
* Invokes {@code TestConnection} asynchronously.
*
* @return a future completed with the {@code ok} flag, or completed
* exceptionally with {@link MxGatewayException} on failure
*/
public CompletableFuture<Boolean> testConnectionAsync() {
return toCompletable(rawFutureStub().testConnection(TestConnectionRequest.getDefaultInstance()))
.thenApply(TestConnectionReply::getOk);
}
/**
* Invoke the {@code GetLastDeployTime} RPC. Returns {@link Optional#empty()}
* when the server reports {@code present=false}.
* Invokes the {@code GetLastDeployTime} RPC.
*
* @return the time of the last deploy, or {@link Optional#empty()} when the
* server reports {@code present=false}
* @throws MxGatewayException on transport or protocol failure
*/
public Optional<Instant> getLastDeployTime() {
try {
@@ -118,15 +155,25 @@ public final class GalaxyRepositoryClient implements AutoCloseable {
}
}
/**
* Invokes {@code GetLastDeployTime} asynchronously.
*
* @return a future completed with the time of the last deploy, or
* {@link Optional#empty()} when the server reports {@code present=false};
* completed exceptionally with {@link MxGatewayException} on failure
*/
public CompletableFuture<Optional<Instant>> getLastDeployTimeAsync() {
return toCompletable(rawFutureStub().getLastDeployTime(GetLastDeployTimeRequest.getDefaultInstance()))
.thenApply(GalaxyRepositoryClient::mapDeployTime);
}
/**
* Invoke the {@code DiscoverHierarchy} RPC and return the generated
* Invokes the {@code DiscoverHierarchy} RPC and returns the generated
* {@link GalaxyObject} messages directly. Callers can read every field of
* the proto message without an extra DTO layer.
*
* @return the Galaxy object hierarchy
* @throws MxGatewayException on transport or protocol failure
*/
public List<GalaxyObject> discoverHierarchy() {
try {
@@ -141,18 +188,25 @@ public final class GalaxyRepositoryClient implements AutoCloseable {
}
}
/**
* Invokes {@code DiscoverHierarchy} asynchronously.
*
* @return a future completed with the Galaxy object hierarchy, or completed
* exceptionally with {@link MxGatewayException} on failure
*/
public CompletableFuture<List<GalaxyObject>> discoverHierarchyAsync() {
return toCompletable(rawFutureStub().discoverHierarchy(DiscoverHierarchyRequest.getDefaultInstance()))
.thenApply(DiscoverHierarchyReply::getObjectsList);
}
/**
* Subscribe to {@code WatchDeployEvents} via the async stub and consume
* Subscribes to {@code WatchDeployEvents} via the async stub and consumes
* results through a blocking iterator. Closing the returned stream cancels
* the underlying gRPC call.
*
* @param lastSeenDeployTime optional. When non-null, the bootstrap event is
* suppressed if the cached deploy time matches.
* @param lastSeenDeployTime optional. When non-{@code null}, the bootstrap
* event is suppressed if the cached deploy time matches.
* @return an iterator-style stream of deploy events
*/
public DeployEventStream watchDeployEvents(Instant lastSeenDeployTime) {
DeployEventStream stream = new DeployEventStream(16);
@@ -163,15 +217,23 @@ public final class GalaxyRepositoryClient implements AutoCloseable {
/**
* Iterator-style alias for {@link #watchDeployEvents(Instant)} matching the
* task-spec signature.
*
* @param lastSeenDeployTime optional cached deploy time for bootstrap suppression
* @return an iterator over deploy events
*/
public Iterator<DeployEvent> watchDeployEventsIterator(Instant lastSeenDeployTime) {
return watchDeployEvents(lastSeenDeployTime);
}
/**
* Subscribe to {@code WatchDeployEvents} via the async stub, dispatching
* Subscribes to {@code WatchDeployEvents} via the async stub, dispatching
* each event to {@code observer}. The returned subscription is cancellable
* and {@link AutoCloseable}.
*
* @param lastSeenDeployTime optional cached deploy time for bootstrap suppression
* @param observer caller-supplied observer that receives events and completion
* @return a cancellable subscription handle
* @throws NullPointerException if {@code observer} is {@code null}
*/
public DeployEventSubscription watchDeployEventsAsync(
Instant lastSeenDeployTime, StreamObserver<DeployEvent> observer) {
@@ -207,6 +269,13 @@ public final class GalaxyRepositoryClient implements AutoCloseable {
}
}
/**
* Shuts the owned channel down and waits up to the configured connect
* timeout for termination, forcibly shutting it down on timeout. No-op
* for clients that do not own their channel.
*
* @throws InterruptedException if the calling thread is interrupted while waiting
*/
public void closeAndAwaitTermination() throws InterruptedException {
if (ownedChannel != null) {
ownedChannel.shutdown();
@@ -3,11 +3,29 @@ package com.dohertylan.mxgateway.client;
import mxaccess_gateway.v1.MxaccessGateway.MxCommandReply;
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatus;
/**
* Thrown when the worker reports an MXAccess COM-side failure. Distinguishes
* MXAccess errors (non-zero {@code HResult} or unsuccessful {@code MxStatusProxy})
* from other gateway protocol failures.
*/
public final class MxAccessException extends MxGatewayCommandException {
/**
* Creates a new MXAccess exception with an explicit protocol status.
*
* @param operation human-readable name of the failing operation
* @param protocolStatus protocol status reported by the gateway
* @param reply raw command reply containing the MXAccess failure detail
*/
public MxAccessException(String operation, ProtocolStatus protocolStatus, MxCommandReply reply) {
super(operation, protocolStatus, reply);
}
/**
* Creates a new MXAccess exception derived from a command reply.
*
* @param operation human-readable name of the failing operation
* @param reply raw command reply; the protocol status is taken from this reply when present
*/
public MxAccessException(String operation, MxCommandReply reply) {
super(operation, reply == null ? null : reply.getProtocolStatus(), reply);
}
@@ -12,6 +12,16 @@ import java.util.concurrent.BlockingQueue;
import mxaccess_gateway.v1.MxaccessGateway.MxEvent;
import mxaccess_gateway.v1.MxaccessGateway.StreamEventsRequest;
/**
* Iterator-style adaptor over the gateway {@code StreamEvents} server-streaming
* RPC.
*
* <p>Events arrive on a background gRPC thread and are buffered in a bounded
* blocking queue; the iterator drains them on the calling thread. Closing the
* stream cancels the underlying gRPC call. If the queue overflows the call is
* cancelled and a follow-up call to {@link #next()} throws
* {@link MxGatewayException}.
*/
public final class MxEventStream implements Iterator<MxEvent>, AutoCloseable {
private static final Object END = new Object();
@@ -8,12 +8,22 @@ import io.grpc.ForwardingClientCall;
import io.grpc.Metadata;
import io.grpc.MethodDescriptor;
/**
* gRPC client interceptor that attaches the {@code authorization: Bearer ...}
* header carrying the gateway API key. A blank or {@code null} key disables
* the interceptor so unauthenticated calls pass through unchanged.
*/
public final class MxGatewayAuthInterceptor implements ClientInterceptor {
static final Metadata.Key<String> AUTHORIZATION_HEADER =
Metadata.Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER);
private final String apiKey;
/**
* Creates a new interceptor using the supplied API key.
*
* @param apiKey gateway API key; {@code null} or blank disables the interceptor
*/
public MxGatewayAuthInterceptor(String apiKey) {
this.apiKey = apiKey == null ? "" : apiKey;
}
@@ -1,6 +1,16 @@
package com.dohertylan.mxgateway.client;
/**
* Thrown when the gateway rejects a call because the supplied API key is
* missing, malformed, or unrecognised (gRPC {@code UNAUTHENTICATED}).
*/
public final class MxGatewayAuthenticationException extends MxGatewayException {
/**
* Creates a new authentication exception.
*
* @param message human-readable description of the failure
* @param cause underlying gRPC error reported by the transport
*/
public MxGatewayAuthenticationException(String message, Throwable cause) {
super(message, cause);
}
@@ -1,6 +1,16 @@
package com.dohertylan.mxgateway.client;
/**
* Thrown when the gateway accepts an API key but rejects a call because the
* key lacks the required scope (gRPC {@code PERMISSION_DENIED}).
*/
public final class MxGatewayAuthorizationException extends MxGatewayException {
/**
* Creates a new authorization exception.
*
* @param message human-readable description of the failure
* @param cause underlying gRPC error reported by the transport
*/
public MxGatewayAuthorizationException(String message, Throwable cause) {
super(message, cause);
}
@@ -25,6 +25,15 @@ import mxaccess_gateway.v1.MxaccessGateway.OpenSessionRequest;
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatusCode;
import mxaccess_gateway.v1.MxaccessGateway.StreamEventsRequest;
/**
* Idiomatic Java wrapper around the generated {@code MxAccessGateway} gRPC
* stubs.
*
* <p>Owns or borrows a {@link ManagedChannel}, attaches a
* {@link MxGatewayAuthInterceptor} carrying the configured API key, and
* exposes blocking, future, and async stub variants. Translates protocol
* status failures into typed {@link MxGatewayException} subclasses.
*/
public final class MxGatewayClient implements AutoCloseable {
private final ManagedChannel ownedChannel;
private final MxGatewayClientOptions options;
@@ -41,6 +50,14 @@ public final class MxGatewayClient implements AutoCloseable {
asyncStub = MxAccessGatewayGrpc.newStub(intercepted);
}
/**
* Constructs a client over a caller-managed {@link Channel}. The caller
* owns channel lifecycle; {@link #close()} is a no-op for this constructor.
*
* @param channel the gRPC channel to use for outbound calls
* @param options the client options carrying the API key and timeouts
* @throws NullPointerException if {@code options} is {@code null}
*/
public MxGatewayClient(Channel channel, MxGatewayClientOptions options) {
this.ownedChannel = null;
this.options = Objects.requireNonNull(options, "options");
@@ -50,27 +67,64 @@ public final class MxGatewayClient implements AutoCloseable {
asyncStub = MxAccessGatewayGrpc.newStub(intercepted);
}
/**
* Builds a new client and owns its channel; {@link #close()} shuts the
* channel down.
*
* @param options the client options carrying the endpoint and credentials
* @return a connected client
*/
public static MxGatewayClient connect(MxGatewayClientOptions options) {
return new MxGatewayClient(createChannel(options), options);
}
/**
* Returns the underlying blocking stub with the per-call deadline applied.
*
* @return the blocking stub
*/
public MxAccessGatewayGrpc.MxAccessGatewayBlockingStub rawBlockingStub() {
return withDeadline(blockingStub);
}
/**
* Returns the underlying future stub with the per-call deadline applied.
*
* @return the future stub
*/
public MxAccessGatewayGrpc.MxAccessGatewayFutureStub rawFutureStub() {
return withDeadline(futureStub);
}
/**
* Returns the underlying async stub. Stream deadlines are applied per call.
*
* @return the async stub
*/
public MxAccessGatewayGrpc.MxAccessGatewayStub rawAsyncStub() {
return asyncStub;
}
/**
* Opens a gateway session and returns a typed handle for further commands.
*
* @param request the {@code OpenSessionRequest} to send
* @return a session bound to the resulting {@code OpenSessionReply}
* @throws MxGatewayException on transport or protocol failure
*/
public MxGatewaySession openSession(OpenSessionRequest request) {
OpenSessionReply reply = openSessionRaw(request);
return new MxGatewaySession(this, reply);
}
/**
* Opens a gateway session using the configured call timeout for the
* worker command timeout and a caller-supplied client session name.
*
* @param clientSessionName the human-readable session name reported by the gateway
* @return a session bound to the resulting {@code OpenSessionReply}
* @throws MxGatewayException on transport or protocol failure
*/
public MxGatewaySession openSession(String clientSessionName) {
return openSession(OpenSessionRequest.newBuilder()
.setClientSessionName(clientSessionName)
@@ -81,6 +135,13 @@ public final class MxGatewayClient implements AutoCloseable {
.build());
}
/**
* Invokes {@code OpenSession} and returns the raw reply.
*
* @param request the {@code OpenSessionRequest} to send
* @return the raw {@code OpenSessionReply}
* @throws MxGatewayException on transport or protocol failure
*/
public OpenSessionReply openSessionRaw(OpenSessionRequest request) {
try {
OpenSessionReply reply = rawBlockingStub().openSession(request);
@@ -94,6 +155,13 @@ public final class MxGatewayClient implements AutoCloseable {
}
}
/**
* Invokes {@code OpenSession} asynchronously.
*
* @param request the {@code OpenSessionRequest} to send
* @return a future completed with the raw reply, or completed exceptionally
* with {@link MxGatewayException} on failure
*/
public CompletableFuture<OpenSessionReply> openSessionAsync(OpenSessionRequest request) {
CompletableFuture<OpenSessionReply> future = toCompletable(rawFutureStub().openSession(request));
return future.thenApply(reply -> {
@@ -102,6 +170,15 @@ public final class MxGatewayClient implements AutoCloseable {
});
}
/**
* Invokes the {@code Invoke} unary RPC and validates both the protocol
* status and any MXAccess-side failure carried in the reply.
*
* @param request the {@code MxCommandRequest} to send
* @return the raw command reply
* @throws MxGatewayException on transport or protocol failure
* @throws MxAccessException when the worker reports an MXAccess COM-side failure
*/
public MxCommandReply invoke(MxCommandRequest request) {
try {
MxCommandReply reply = rawBlockingStub().invoke(request);
@@ -116,6 +193,14 @@ public final class MxGatewayClient implements AutoCloseable {
}
}
/**
* Invokes the {@code Invoke} RPC asynchronously.
*
* @param request the {@code MxCommandRequest} to send
* @return a future completed with the raw reply, or completed exceptionally
* with {@link MxGatewayException} (including {@link MxAccessException})
* on failure
*/
public CompletableFuture<MxCommandReply> invokeAsync(MxCommandRequest request) {
CompletableFuture<MxCommandReply> future = toCompletable(rawFutureStub().invoke(request));
return future.thenApply(reply -> {
@@ -125,6 +210,13 @@ public final class MxGatewayClient implements AutoCloseable {
});
}
/**
* Invokes the {@code CloseSession} unary RPC.
*
* @param request the {@code CloseSessionRequest} to send
* @return the raw reply
* @throws MxGatewayException on transport or protocol failure
*/
public CloseSessionReply closeSessionRaw(CloseSessionRequest request) {
try {
CloseSessionReply reply = rawBlockingStub().closeSession(request);
@@ -138,12 +230,28 @@ public final class MxGatewayClient implements AutoCloseable {
}
}
/**
* Subscribes to the {@code StreamEvents} server-streaming RPC and exposes
* results as a blocking iterator. Closing the returned stream cancels the
* underlying gRPC call.
*
* @param request the {@code StreamEventsRequest} carrying the session id and resume cursor
* @return an iterator-style stream of events
*/
public MxEventStream streamEvents(StreamEventsRequest request) {
MxEventStream stream = new MxEventStream(16);
withStreamDeadline(rawAsyncStub()).streamEvents(request, stream.observer());
return stream;
}
/**
* Subscribes to {@code StreamEvents} and dispatches each event to the
* supplied observer. The returned subscription is cancellable.
*
* @param request the {@code StreamEventsRequest} to send
* @param observer caller-supplied observer that receives events and completion
* @return a cancellable subscription handle
*/
public MxGatewayEventSubscription streamEventsAsync(
StreamEventsRequest request, StreamObserver<MxEvent> observer) {
MxGatewayEventSubscription subscription = new MxGatewayEventSubscription();
@@ -158,6 +266,13 @@ public final class MxGatewayClient implements AutoCloseable {
}
}
/**
* Shuts the owned channel down and waits up to the configured connect
* timeout for termination, forcibly shutting it down on timeout. No-op
* for clients that do not own their channel.
*
* @throws InterruptedException if the calling thread is interrupted while waiting
*/
public void closeAndAwaitTermination() throws InterruptedException {
if (ownedChannel != null) {
ownedChannel.shutdown();
@@ -4,6 +4,13 @@ import java.nio.file.Path;
import java.time.Duration;
import java.util.Objects;
/**
* Immutable configuration for {@link MxGatewayClient} and
* {@link GalaxyRepositoryClient}.
*
* <p>Captures the gateway endpoint, API key, transport security selection and
* call/stream timeouts. Instances are constructed via {@link #builder()}.
*/
public final class MxGatewayClientOptions {
private static final Duration DEFAULT_CONNECT_TIMEOUT = Duration.ofSeconds(10);
private static final Duration DEFAULT_CALL_TIMEOUT = Duration.ofSeconds(30);
@@ -28,42 +35,93 @@ public final class MxGatewayClientOptions {
streamTimeout = builder.streamTimeout;
}
/**
* Returns a fresh builder with default timeouts and no endpoint set.
*
* @return a new {@link Builder}
*/
public static Builder builder() {
return new Builder();
}
/**
* Returns the configured gRPC target endpoint.
*
* @return the endpoint string in {@code host:port} or DNS-target form
*/
public String endpoint() {
return endpoint;
}
/**
* Returns the configured API key, or an empty string if none was supplied.
*
* @return the raw API key
*/
public String apiKey() {
return apiKey;
}
/**
* Returns the API key with the body redacted, safe to write to logs.
*
* @return the redacted form produced by {@link MxGatewaySecrets#redactApiKey(String)}
*/
public String redactedApiKey() {
return MxGatewaySecrets.redactApiKey(apiKey);
}
/**
* Returns whether the client is configured to use plaintext transport.
*
* @return {@code true} for plaintext, {@code false} for TLS
*/
public boolean plaintext() {
return plaintext;
}
/**
* Returns the configured CA certificate file used to verify the gateway,
* or {@code null} when the platform trust store is used.
*
* @return the CA certificate path, or {@code null}
*/
public Path caCertificatePath() {
return caCertificatePath;
}
/**
* Returns the TLS server-name override, or an empty string when none was supplied.
*
* @return the server-name override
*/
public String serverNameOverride() {
return serverNameOverride;
}
/**
* Returns the channel connect timeout.
*
* @return the connect timeout duration
*/
public Duration connectTimeout() {
return connectTimeout;
}
/**
* Returns the per-call deadline applied to unary RPCs.
*
* @return the call timeout duration
*/
public Duration callTimeout() {
return callTimeout;
}
/**
* Returns the deadline applied to server-streaming RPCs, or {@code null} when none is set.
*
* @return the stream timeout duration, or {@code null}
*/
public Duration streamTimeout() {
return streamTimeout;
}
@@ -100,6 +158,9 @@ public final class MxGatewayClientOptions {
return value;
}
/**
* Mutable builder for {@link MxGatewayClientOptions}.
*/
public static final class Builder {
private String endpoint;
private String apiKey;
@@ -113,46 +174,103 @@ public final class MxGatewayClientOptions {
private Builder() {
}
/**
* Sets the gRPC target endpoint.
*
* @param value endpoint in {@code host:port} or DNS-target form; required
* @return this builder
*/
public Builder endpoint(String value) {
endpoint = value;
return this;
}
/**
* Sets the API key sent in the {@code authorization} header.
*
* @param value the API key, or {@code null}/blank to disable authentication
* @return this builder
*/
public Builder apiKey(String value) {
apiKey = value;
return this;
}
/**
* Selects plaintext transport instead of TLS.
*
* @param value {@code true} for plaintext, {@code false} for TLS
* @return this builder
*/
public Builder plaintext(boolean value) {
plaintext = value;
return this;
}
/**
* Sets the CA certificate used to verify the gateway server.
*
* @param value path to a PEM-encoded CA certificate, or {@code null} to use the platform trust store
* @return this builder
*/
public Builder caCertificatePath(Path value) {
caCertificatePath = value;
return this;
}
/**
* Overrides the TLS server name used during the handshake.
*
* @param value the override host name, or empty/{@code null} for none
* @return this builder
*/
public Builder serverNameOverride(String value) {
serverNameOverride = value;
return this;
}
/**
* Sets the channel connect timeout.
*
* @param value the connect timeout, must be non-{@code null}
* @return this builder
* @throws NullPointerException if {@code value} is {@code null}
*/
public Builder connectTimeout(Duration value) {
connectTimeout = Objects.requireNonNull(value, "connectTimeout");
return this;
}
/**
* Sets the per-call deadline applied to unary RPCs.
*
* @param value the call timeout, must be non-{@code null}
* @return this builder
* @throws NullPointerException if {@code value} is {@code null}
*/
public Builder callTimeout(Duration value) {
callTimeout = Objects.requireNonNull(value, "callTimeout");
return this;
}
/**
* Sets the deadline applied to server-streaming RPCs.
*
* @param value the stream timeout, must be non-{@code null}
* @return this builder
* @throws NullPointerException if {@code value} is {@code null}
*/
public Builder streamTimeout(Duration value) {
streamTimeout = Objects.requireNonNull(value, "streamTimeout");
return this;
}
/**
* Builds an immutable {@link MxGatewayClientOptions} from the current state.
*
* @return a new options instance
* @throws IllegalArgumentException if {@code endpoint} was not set or is blank
*/
public MxGatewayClientOptions build() {
return new MxGatewayClientOptions(this);
}
@@ -1,5 +1,11 @@
package com.dohertylan.mxgateway.client;
/**
* Reports the client and protocol version numbers compiled into this build.
*
* <p>Used by smoke-test tooling and the CLI to confirm that a gateway and
* worker speak the same protocol version as the client.
*/
public final class MxGatewayClientVersion {
private static final int GATEWAY_PROTOCOL_VERSION = 1;
private static final int WORKER_PROTOCOL_VERSION = 1;
@@ -8,14 +14,29 @@ public final class MxGatewayClientVersion {
private MxGatewayClientVersion() {
}
/**
* Returns the human-readable client release version.
*
* @return the client version string
*/
public static String clientVersion() {
return CLIENT_VERSION;
}
/**
* Returns the gRPC gateway protocol version this client targets.
*
* @return the gateway protocol version
*/
public static int gatewayProtocolVersion() {
return GATEWAY_PROTOCOL_VERSION;
}
/**
* Returns the worker IPC protocol version this client targets.
*
* @return the worker protocol version
*/
public static int workerProtocolVersion() {
return WORKER_PROTOCOL_VERSION;
}
@@ -3,20 +3,42 @@ package com.dohertylan.mxgateway.client;
import mxaccess_gateway.v1.MxaccessGateway.MxCommandReply;
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatus;
/**
* Thrown when the gateway accepts an MXAccess command but the command itself
* fails at the protocol layer. Carries the original {@code MxCommandReply} and
* {@code ProtocolStatus} so callers can inspect the failure detail.
*/
public class MxGatewayCommandException extends MxGatewayException {
private final ProtocolStatus protocolStatus;
private final MxCommandReply reply;
/**
* Creates a new command exception.
*
* @param operation human-readable name of the failing operation
* @param protocolStatus protocol status returned by the gateway
* @param reply raw command reply, or {@code null} when the call failed before a reply was produced
*/
public MxGatewayCommandException(String operation, ProtocolStatus protocolStatus, MxCommandReply reply) {
super(MxGatewayErrors.protocolStatusMessage(operation, protocolStatus));
this.protocolStatus = protocolStatus;
this.reply = reply;
}
/**
* Returns the gateway protocol status that triggered this exception.
*
* @return the protocol status, or {@code null} if none was supplied
*/
public ProtocolStatus protocolStatus() {
return protocolStatus;
}
/**
* Returns the raw command reply associated with the failure.
*
* @return the command reply, or {@code null} if no reply was available
*/
public MxCommandReply reply() {
return reply;
}
@@ -8,6 +8,14 @@ import java.util.concurrent.atomic.AtomicBoolean;
import mxaccess_gateway.v1.MxaccessGateway.MxEvent;
import mxaccess_gateway.v1.MxaccessGateway.StreamEventsRequest;
/**
* Cancellable handle returned by the async {@code streamEvents} variant.
*
* <p>Wraps a caller-supplied {@link StreamObserver} and exposes a
* {@link #cancel()} entry point that aborts the underlying gRPC call. The
* subscription also implements {@link AutoCloseable} so it can participate in
* try-with-resources blocks.
*/
public final class MxGatewayEventSubscription implements AutoCloseable {
private final AtomicReference<ClientCallStreamObserver<StreamEventsRequest>> requestStream = new AtomicReference<>();
private final AtomicBoolean cancelled = new AtomicBoolean();
@@ -39,6 +47,11 @@ public final class MxGatewayEventSubscription implements AutoCloseable {
};
}
/**
* Cancels the underlying gRPC call. Safe to invoke before the call has
* started; cancellation is recorded and applied as soon as the stream
* attaches.
*/
public void cancel() {
cancelled.set(true);
ClientCallStreamObserver<StreamEventsRequest> stream = requestStream.get();
@@ -1,10 +1,28 @@
package com.dohertylan.mxgateway.client;
/**
* Base unchecked exception thrown by the MXAccess Gateway Java client.
*
* <p>All gateway-specific failures derive from this type so callers can catch a
* single supertype regardless of whether the cause was a transport error,
* protocol-level failure, or MXAccess-side problem.
*/
public class MxGatewayException extends RuntimeException {
/**
* Creates a new exception with the supplied message.
*
* @param message human-readable description of the failure
*/
public MxGatewayException(String message) {
super(message);
}
/**
* Creates a new exception with the supplied message and underlying cause.
*
* @param message human-readable description of the failure
* @param cause underlying error that triggered the failure
*/
public MxGatewayException(String message, Throwable cause) {
super(message, cause);
}
@@ -1,9 +1,24 @@
package com.dohertylan.mxgateway.client;
/**
* Helpers for redacting secrets such as gateway API keys from log output.
*
* <p>API keys must never reach logs in plaintext. The methods on this class
* produce shortened, masked forms safe for diagnostic messages.
*/
public final class MxGatewaySecrets {
private MxGatewaySecrets() {
}
/**
* Redacts the body of an API key, leaving only short prefix and suffix
* windows so it remains comparable in logs.
*
* @param apiKey the API key to redact, may be {@code null} or empty
* @return an empty string for {@code null}/empty input, {@code "<redacted>"}
* for keys eight characters or shorter, or a masked form preserving
* the leading and trailing four characters
*/
public static String redactApiKey(String apiKey) {
if (apiKey == null || apiKey.isEmpty()) {
return "";
@@ -17,6 +32,15 @@ public final class MxGatewaySecrets {
+ apiKey.substring(apiKey.length() - 4);
}
/**
* Replaces gateway-style credential tokens (the {@code mxgw_} prefix and
* any {@code Bearer} marker) inside a free-form string with a redaction
* placeholder.
*
* @param value the string to scrub, may be {@code null}
* @return an empty string for {@code null}, the original value when blank,
* or the value with credential tokens replaced by {@code "<redacted>"}
*/
public static String redactCredentials(String value) {
if (value == null || value.isBlank()) {
return value == null ? "" : value;
@@ -30,6 +30,14 @@ import mxaccess_gateway.v1.MxaccessGateway.UnregisterCommand;
import mxaccess_gateway.v1.MxaccessGateway.Write2Command;
import mxaccess_gateway.v1.MxaccessGateway.WriteCommand;
/**
* Typed handle for a single MXAccess gateway session.
*
* <p>Wraps an {@link OpenSessionReply} together with the {@link MxGatewayClient}
* that opened it and exposes the MXAccess command surface (Register, AddItem,
* Advise, bulk subscribe variants, Write, event streaming, and close). Each
* command request carries a freshly generated client correlation id.
*/
public final class MxGatewaySession implements AutoCloseable {
private static final SecureRandom RANDOM = new SecureRandom();
@@ -42,19 +50,45 @@ public final class MxGatewaySession implements AutoCloseable {
this.openReply = Objects.requireNonNull(openReply, "openReply");
}
/**
* Builds a session handle for an existing gateway session id without
* issuing an {@code OpenSession} call. Useful for CLI tools that operate
* against a session opened in a separate invocation.
*
* @param client the gateway client used for further commands
* @param sessionId the existing gateway session id
* @return a session handle bound to the supplied id
*/
public static MxGatewaySession forSessionId(MxGatewayClient client, String sessionId) {
return new MxGatewaySession(
client, OpenSessionReply.newBuilder().setSessionId(sessionId).build());
}
/**
* Returns the gateway-assigned session id.
*
* @return the session id
*/
public String sessionId() {
return openReply.getSessionId();
}
/**
* Returns the original {@link OpenSessionReply} that this session was opened with.
*
* @return the open-session reply
*/
public OpenSessionReply openReply() {
return openReply;
}
/**
* Sends a {@code CloseSession} RPC and caches the reply so subsequent calls
* are idempotent.
*
* @return the raw close-session reply
* @throws MxGatewayException on transport or protocol failure
*/
public synchronized CloseSessionReply closeRaw() {
if (closeReply == null) {
closeReply = client.closeSessionRaw(CloseSessionRequest.newBuilder()
@@ -70,6 +104,13 @@ public final class MxGatewaySession implements AutoCloseable {
closeRaw();
}
/**
* Invokes MXAccess {@code Register} and returns the server handle.
*
* @param clientName the MXAccess client name to register
* @return the {@code ServerHandle} returned by MXAccess
* @throws MxGatewayException on transport or protocol failure
*/
public int register(String clientName) {
MxCommandReply reply = registerRaw(clientName);
if (reply.hasRegister()) {
@@ -78,6 +119,13 @@ public final class MxGatewaySession implements AutoCloseable {
return reply.getReturnValue().getInt32Value();
}
/**
* Invokes MXAccess {@code Register} and returns the raw reply.
*
* @param clientName the MXAccess client name to register
* @return the raw command reply
* @throws MxGatewayException on transport or protocol failure
*/
public MxCommandReply registerRaw(String clientName) {
return invokeCommand(MxCommand.newBuilder()
.setKind(MxCommandKind.MX_COMMAND_KIND_REGISTER)
@@ -85,6 +133,12 @@ public final class MxGatewaySession implements AutoCloseable {
.build());
}
/**
* Invokes MXAccess {@code Unregister}.
*
* @param serverHandle the {@code ServerHandle} returned by {@link #register(String)}
* @throws MxGatewayException on transport or protocol failure
*/
public void unregister(int serverHandle) {
invokeCommand(MxCommand.newBuilder()
.setKind(MxCommandKind.MX_COMMAND_KIND_UNREGISTER)
@@ -92,6 +146,14 @@ public final class MxGatewaySession implements AutoCloseable {
.build());
}
/**
* Invokes MXAccess {@code AddItem} and returns the new item handle.
*
* @param serverHandle the {@code ServerHandle} owning the item
* @param itemDefinition the MXAccess item definition (tag reference)
* @return the {@code ItemHandle} assigned by MXAccess
* @throws MxGatewayException on transport or protocol failure
*/
public int addItem(int serverHandle, String itemDefinition) {
MxCommandReply reply = addItemRaw(serverHandle, itemDefinition);
if (reply.hasAddItem()) {
@@ -100,6 +162,14 @@ public final class MxGatewaySession implements AutoCloseable {
return reply.getReturnValue().getInt32Value();
}
/**
* Invokes MXAccess {@code AddItem} and returns the raw reply.
*
* @param serverHandle the {@code ServerHandle} owning the item
* @param itemDefinition the MXAccess item definition
* @return the raw command reply
* @throws MxGatewayException on transport or protocol failure
*/
public MxCommandReply addItemRaw(int serverHandle, String itemDefinition) {
return invokeCommand(MxCommand.newBuilder()
.setKind(MxCommandKind.MX_COMMAND_KIND_ADD_ITEM)
@@ -109,6 +179,15 @@ public final class MxGatewaySession implements AutoCloseable {
.build());
}
/**
* Invokes MXAccess {@code AddItem2} and returns the new item handle.
*
* @param serverHandle the {@code ServerHandle} owning the item
* @param itemDefinition the MXAccess item definition
* @param itemContext the MXAccess item context (e.g. galaxy/object scope)
* @return the {@code ItemHandle} assigned by MXAccess
* @throws MxGatewayException on transport or protocol failure
*/
public int addItem2(int serverHandle, String itemDefinition, String itemContext) {
MxCommandReply reply = addItem2Raw(serverHandle, itemDefinition, itemContext);
if (reply.hasAddItem2()) {
@@ -117,6 +196,15 @@ public final class MxGatewaySession implements AutoCloseable {
return reply.getReturnValue().getInt32Value();
}
/**
* Invokes MXAccess {@code AddItem2} and returns the raw reply.
*
* @param serverHandle the {@code ServerHandle} owning the item
* @param itemDefinition the MXAccess item definition
* @param itemContext the MXAccess item context
* @return the raw command reply
* @throws MxGatewayException on transport or protocol failure
*/
public MxCommandReply addItem2Raw(int serverHandle, String itemDefinition, String itemContext) {
return invokeCommand(MxCommand.newBuilder()
.setKind(MxCommandKind.MX_COMMAND_KIND_ADD_ITEM2)
@@ -127,10 +215,25 @@ public final class MxGatewaySession implements AutoCloseable {
.build());
}
/**
* Invokes MXAccess {@code RemoveItem}.
*
* @param serverHandle the {@code ServerHandle} owning the item
* @param itemHandle the {@code ItemHandle} to remove
* @throws MxGatewayException on transport or protocol failure
*/
public void removeItem(int serverHandle, int itemHandle) {
removeItemRaw(serverHandle, itemHandle);
}
/**
* Invokes MXAccess {@code RemoveItem} and returns the raw reply.
*
* @param serverHandle the {@code ServerHandle} owning the item
* @param itemHandle the {@code ItemHandle} to remove
* @return the raw command reply
* @throws MxGatewayException on transport or protocol failure
*/
public MxCommandReply removeItemRaw(int serverHandle, int itemHandle) {
return invokeCommand(MxCommand.newBuilder()
.setKind(MxCommandKind.MX_COMMAND_KIND_REMOVE_ITEM)
@@ -140,10 +243,25 @@ public final class MxGatewaySession implements AutoCloseable {
.build());
}
/**
* Invokes MXAccess {@code Advise} so the item starts emitting data-change events.
*
* @param serverHandle the {@code ServerHandle} owning the item
* @param itemHandle the {@code ItemHandle} to advise
* @throws MxGatewayException on transport or protocol failure
*/
public void advise(int serverHandle, int itemHandle) {
adviseRaw(serverHandle, itemHandle);
}
/**
* Invokes MXAccess {@code Advise} and returns the raw reply.
*
* @param serverHandle the {@code ServerHandle} owning the item
* @param itemHandle the {@code ItemHandle} to advise
* @return the raw command reply
* @throws MxGatewayException on transport or protocol failure
*/
public MxCommandReply adviseRaw(int serverHandle, int itemHandle) {
return invokeCommand(MxCommand.newBuilder()
.setKind(MxCommandKind.MX_COMMAND_KIND_ADVISE)
@@ -153,10 +271,25 @@ public final class MxGatewaySession implements AutoCloseable {
.build());
}
/**
* Invokes MXAccess {@code UnAdvise} so the item stops emitting data-change events.
*
* @param serverHandle the {@code ServerHandle} owning the item
* @param itemHandle the {@code ItemHandle} to un-advise
* @throws MxGatewayException on transport or protocol failure
*/
public void unAdvise(int serverHandle, int itemHandle) {
unAdviseRaw(serverHandle, itemHandle);
}
/**
* Invokes MXAccess {@code UnAdvise} and returns the raw reply.
*
* @param serverHandle the {@code ServerHandle} owning the item
* @param itemHandle the {@code ItemHandle} to un-advise
* @return the raw command reply
* @throws MxGatewayException on transport or protocol failure
*/
public MxCommandReply unAdviseRaw(int serverHandle, int itemHandle) {
return invokeCommand(MxCommand.newBuilder()
.setKind(MxCommandKind.MX_COMMAND_KIND_UN_ADVISE)
@@ -166,6 +299,15 @@ public final class MxGatewaySession implements AutoCloseable {
.build());
}
/**
* Invokes the bulk {@code AddItem} variant.
*
* @param serverHandle the {@code ServerHandle} owning the items
* @param tagAddresses the MXAccess tag addresses to add
* @return a per-tag {@link SubscribeResult} list
* @throws MxGatewayException on transport or protocol failure
* @throws NullPointerException if {@code tagAddresses} is {@code null}
*/
public List<SubscribeResult> addItemBulk(int serverHandle, List<String> tagAddresses) {
Objects.requireNonNull(tagAddresses, "tagAddresses");
MxCommandReply reply = invokeCommand(MxCommand.newBuilder()
@@ -177,6 +319,15 @@ public final class MxGatewaySession implements AutoCloseable {
return reply.getAddItemBulk().getResultsList();
}
/**
* Invokes the bulk {@code Advise} variant.
*
* @param serverHandle the {@code ServerHandle} owning the items
* @param itemHandles the {@code ItemHandle} list to advise
* @return a per-item {@link SubscribeResult} list
* @throws MxGatewayException on transport or protocol failure
* @throws NullPointerException if {@code itemHandles} is {@code null}
*/
public List<SubscribeResult> adviseItemBulk(int serverHandle, List<Integer> itemHandles) {
Objects.requireNonNull(itemHandles, "itemHandles");
MxCommandReply reply = invokeCommand(MxCommand.newBuilder()
@@ -188,6 +339,15 @@ public final class MxGatewaySession implements AutoCloseable {
return reply.getAdviseItemBulk().getResultsList();
}
/**
* Invokes the bulk {@code RemoveItem} variant.
*
* @param serverHandle the {@code ServerHandle} owning the items
* @param itemHandles the {@code ItemHandle} list to remove
* @return a per-item {@link SubscribeResult} list
* @throws MxGatewayException on transport or protocol failure
* @throws NullPointerException if {@code itemHandles} is {@code null}
*/
public List<SubscribeResult> removeItemBulk(int serverHandle, List<Integer> itemHandles) {
Objects.requireNonNull(itemHandles, "itemHandles");
MxCommandReply reply = invokeCommand(MxCommand.newBuilder()
@@ -199,6 +359,15 @@ public final class MxGatewaySession implements AutoCloseable {
return reply.getRemoveItemBulk().getResultsList();
}
/**
* Invokes the bulk {@code UnAdvise} variant.
*
* @param serverHandle the {@code ServerHandle} owning the items
* @param itemHandles the {@code ItemHandle} list to un-advise
* @return a per-item {@link SubscribeResult} list
* @throws MxGatewayException on transport or protocol failure
* @throws NullPointerException if {@code itemHandles} is {@code null}
*/
public List<SubscribeResult> unAdviseItemBulk(int serverHandle, List<Integer> itemHandles) {
Objects.requireNonNull(itemHandles, "itemHandles");
MxCommandReply reply = invokeCommand(MxCommand.newBuilder()
@@ -210,6 +379,16 @@ public final class MxGatewaySession implements AutoCloseable {
return reply.getUnAdviseItemBulk().getResultsList();
}
/**
* Invokes the gateway {@code SubscribeBulk} convenience that combines
* AddItem and Advise for the supplied tag addresses.
*
* @param serverHandle the {@code ServerHandle} owning the items
* @param tagAddresses the MXAccess tag addresses to subscribe
* @return a per-tag {@link SubscribeResult} list
* @throws MxGatewayException on transport or protocol failure
* @throws NullPointerException if {@code tagAddresses} is {@code null}
*/
public List<SubscribeResult> subscribeBulk(int serverHandle, List<String> tagAddresses) {
Objects.requireNonNull(tagAddresses, "tagAddresses");
MxCommandReply reply = invokeCommand(MxCommand.newBuilder()
@@ -221,6 +400,16 @@ public final class MxGatewaySession implements AutoCloseable {
return reply.getSubscribeBulk().getResultsList();
}
/**
* Invokes the gateway {@code UnsubscribeBulk} convenience that combines
* UnAdvise and RemoveItem for the supplied item handles.
*
* @param serverHandle the {@code ServerHandle} owning the items
* @param itemHandles the {@code ItemHandle} list to unsubscribe
* @return a per-item {@link SubscribeResult} list
* @throws MxGatewayException on transport or protocol failure
* @throws NullPointerException if {@code itemHandles} is {@code null}
*/
public List<SubscribeResult> unsubscribeBulk(int serverHandle, List<Integer> itemHandles) {
Objects.requireNonNull(itemHandles, "itemHandles");
MxCommandReply reply = invokeCommand(MxCommand.newBuilder()
@@ -232,10 +421,29 @@ public final class MxGatewaySession implements AutoCloseable {
return reply.getUnsubscribeBulk().getResultsList();
}
/**
* Invokes MXAccess {@code Write}.
*
* @param serverHandle the {@code ServerHandle} owning the item
* @param itemHandle the {@code ItemHandle} to write
* @param value the value to write
* @param userId the MXAccess user id used for security checks
* @throws MxGatewayException on transport or protocol failure
*/
public void write(int serverHandle, int itemHandle, MxValue value, int userId) {
writeRaw(serverHandle, itemHandle, value, userId);
}
/**
* Invokes MXAccess {@code Write} and returns the raw reply.
*
* @param serverHandle the {@code ServerHandle} owning the item
* @param itemHandle the {@code ItemHandle} to write
* @param value the value to write
* @param userId the MXAccess user id used for security checks
* @return the raw command reply
* @throws MxGatewayException on transport or protocol failure
*/
public MxCommandReply writeRaw(int serverHandle, int itemHandle, MxValue value, int userId) {
return invokeCommand(MxCommand.newBuilder()
.setKind(MxCommandKind.MX_COMMAND_KIND_WRITE)
@@ -247,6 +455,16 @@ public final class MxGatewaySession implements AutoCloseable {
.build());
}
/**
* Invokes MXAccess {@code Write2}, which carries an explicit timestamp.
*
* @param serverHandle the {@code ServerHandle} owning the item
* @param itemHandle the {@code ItemHandle} to write
* @param value the value to write
* @param timestampValue the timestamp value to associate with the write
* @param userId the MXAccess user id used for security checks
* @throws MxGatewayException on transport or protocol failure
*/
public void write2(int serverHandle, int itemHandle, MxValue value, MxValue timestampValue, int userId) {
invokeCommand(MxCommand.newBuilder()
.setKind(MxCommandKind.MX_COMMAND_KIND_WRITE2)
@@ -259,10 +477,24 @@ public final class MxGatewaySession implements AutoCloseable {
.build());
}
/**
* Subscribes to gateway events for this session starting from the
* beginning of the worker event log.
*
* @return an iterator-style stream of events
*/
public MxEventStream streamEvents() {
return streamEventsAfter(0);
}
/**
* Subscribes to gateway events for this session starting after the
* supplied worker sequence number.
*
* @param afterWorkerSequence the resume cursor; events with worker sequence
* greater than this value are delivered
* @return an iterator-style stream of events
*/
public MxEventStream streamEventsAfter(long afterWorkerSequence) {
return client.streamEvents(StreamEventsRequest.newBuilder()
.setSessionId(sessionId())
@@ -270,6 +502,14 @@ public final class MxGatewaySession implements AutoCloseable {
.build());
}
/**
* Sends a pre-built {@link MxCommand} for this session and returns the raw
* reply, attaching a freshly generated client correlation id.
*
* @param command the command to send
* @return the raw command reply
* @throws MxGatewayException on transport or protocol failure
*/
public MxCommandReply invokeCommand(MxCommand command) {
return client.invoke(MxCommandRequest.newBuilder()
.setSessionId(sessionId())
@@ -2,14 +2,29 @@ package com.dohertylan.mxgateway.client;
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatus;
/**
* Thrown when the gateway reports a session-related protocol failure such as
* {@code SESSION_NOT_FOUND} or {@code SESSION_NOT_READY}.
*/
public final class MxGatewaySessionException extends MxGatewayException {
private final ProtocolStatus protocolStatus;
/**
* Creates a new session exception from a protocol status.
*
* @param operation human-readable name of the failing operation
* @param protocolStatus protocol status returned by the gateway
*/
public MxGatewaySessionException(String operation, ProtocolStatus protocolStatus) {
super(MxGatewayErrors.protocolStatusMessage(operation, protocolStatus));
this.protocolStatus = protocolStatus;
}
/**
* Returns the gateway protocol status that triggered this exception.
*
* @return the protocol status, or {@code null} if none was supplied
*/
public ProtocolStatus protocolStatus() {
return protocolStatus;
}
@@ -2,14 +2,29 @@ package com.dohertylan.mxgateway.client;
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatus;
/**
* Thrown when the gateway reports a worker-side protocol failure such as
* {@code WORKER_UNAVAILABLE} or {@code PROTOCOL_VIOLATION}.
*/
public final class MxGatewayWorkerException extends MxGatewayException {
private final ProtocolStatus protocolStatus;
/**
* Creates a new worker exception from a protocol status.
*
* @param operation human-readable name of the failing operation
* @param protocolStatus protocol status returned by the gateway
*/
public MxGatewayWorkerException(String operation, ProtocolStatus protocolStatus) {
super(MxGatewayErrors.protocolStatusMessage(operation, protocolStatus));
this.protocolStatus = protocolStatus;
}
/**
* Returns the gateway protocol status that triggered this exception.
*
* @return the protocol status, or {@code null} if none was supplied
*/
public ProtocolStatus protocolStatus() {
return protocolStatus;
}
@@ -4,43 +4,104 @@ import mxaccess_gateway.v1.MxaccessGateway.MxStatusCategory;
import mxaccess_gateway.v1.MxaccessGateway.MxStatusProxy;
import mxaccess_gateway.v1.MxaccessGateway.MxStatusSource;
/**
* Helpers for inspecting {@link MxStatusProxy} values returned by the gateway.
*
* <p>An {@code MxStatusProxy} mirrors the MXAccess COM {@code MXSTATUS_PROXY}
* struct. The success flag uses the MXAccess convention where any non-zero
* value indicates success.
*/
public final class MxStatuses {
private MxStatuses() {
}
/**
* Returns whether the supplied status proxy reports success.
*
* @param status the status proxy, may be {@code null}
* @return {@code true} if {@code status} is {@code null} or its success
* flag is non-zero, {@code false} otherwise
*/
public static boolean succeeded(MxStatusProxy status) {
return status == null || status.getSuccess() != 0;
}
/**
* Wraps a raw {@link MxStatusProxy} in an accessor view that exposes its
* fields with idiomatic Java getters.
*
* @param status the raw status proxy
* @return a view backed by {@code status}
*/
public static MxStatusView view(MxStatusProxy status) {
return new MxStatusView(status);
}
/**
* Idiomatic-Java accessor view over a raw {@link MxStatusProxy}.
*
* @param raw the underlying status proxy this view delegates to
*/
public record MxStatusView(MxStatusProxy raw) {
/**
* Returns the raw success flag (non-zero indicates success).
*
* @return the success flag value
*/
public int success() {
return raw.getSuccess();
}
/**
* Returns the high-level status category.
*
* @return the status category enum value
*/
public MxStatusCategory category() {
return raw.getCategory();
}
/**
* Returns which subsystem detected the status.
*
* @return the detection source enum value
*/
public MxStatusSource detectedBy() {
return raw.getDetectedBy();
}
/**
* Returns the detail code accompanying the status.
*
* @return the raw detail code
*/
public int detail() {
return raw.getDetail();
}
/**
* Returns the raw, unmapped category code from MXAccess.
*
* @return the raw category integer
*/
public int rawCategory() {
return raw.getRawCategory();
}
/**
* Returns the raw, unmapped detection-source code from MXAccess.
*
* @return the raw detection-source integer
*/
public int rawDetectedBy() {
return raw.getRawDetectedBy();
}
/**
* Returns the diagnostic text supplied by MXAccess, if any.
*
* @return the diagnostic message, possibly empty
*/
public String diagnosticText() {
return raw.getDiagnosticText();
}
@@ -17,10 +17,24 @@ import mxaccess_gateway.v1.MxaccessGateway.RawArray;
import mxaccess_gateway.v1.MxaccessGateway.StringArray;
import mxaccess_gateway.v1.MxaccessGateway.TimestampArray;
/**
* Factory helpers for building {@link MxValue} and {@link MxArray} protobuf
* messages and for converting them back into native Java types.
*
* <p>Each {@code *Value} factory sets the matching {@code MxDataType} and
* COM {@code variant} type string so the worker can round-trip the value
* through MXAccess without further coercion.
*/
public final class MxValues {
private MxValues() {
}
/**
* Builds a boolean {@link MxValue}.
*
* @param value the boolean payload
* @return a populated {@code MxValue}
*/
public static MxValue boolValue(boolean value) {
return MxValue.newBuilder()
.setDataType(MxDataType.MX_DATA_TYPE_BOOLEAN)
@@ -29,6 +43,12 @@ public final class MxValues {
.build();
}
/**
* Builds a 32-bit integer {@link MxValue}.
*
* @param value the int32 payload
* @return a populated {@code MxValue}
*/
public static MxValue int32Value(int value) {
return MxValue.newBuilder()
.setDataType(MxDataType.MX_DATA_TYPE_INTEGER)
@@ -37,6 +57,12 @@ public final class MxValues {
.build();
}
/**
* Builds a 64-bit integer {@link MxValue}.
*
* @param value the int64 payload
* @return a populated {@code MxValue}
*/
public static MxValue int64Value(long value) {
return MxValue.newBuilder()
.setDataType(MxDataType.MX_DATA_TYPE_INTEGER)
@@ -45,6 +71,12 @@ public final class MxValues {
.build();
}
/**
* Builds a 32-bit floating-point {@link MxValue}.
*
* @param value the float payload
* @return a populated {@code MxValue}
*/
public static MxValue floatValue(float value) {
return MxValue.newBuilder()
.setDataType(MxDataType.MX_DATA_TYPE_FLOAT)
@@ -53,6 +85,12 @@ public final class MxValues {
.build();
}
/**
* Builds a 64-bit floating-point {@link MxValue}.
*
* @param value the double payload
* @return a populated {@code MxValue}
*/
public static MxValue doubleValue(double value) {
return MxValue.newBuilder()
.setDataType(MxDataType.MX_DATA_TYPE_DOUBLE)
@@ -61,6 +99,12 @@ public final class MxValues {
.build();
}
/**
* Builds a string {@link MxValue}.
*
* @param value the string payload
* @return a populated {@code MxValue}
*/
public static MxValue stringValue(String value) {
return MxValue.newBuilder()
.setDataType(MxDataType.MX_DATA_TYPE_STRING)
@@ -69,6 +113,12 @@ public final class MxValues {
.build();
}
/**
* Builds a timestamp {@link MxValue} from an {@link Instant}.
*
* @param value the instant to encode as MXAccess time
* @return a populated {@code MxValue}
*/
public static MxValue timestampValue(Instant value) {
return MxValue.newBuilder()
.setDataType(MxDataType.MX_DATA_TYPE_TIME)
@@ -80,6 +130,14 @@ public final class MxValues {
.build();
}
/**
* Converts an {@link MxValue} back into a native Java value.
*
* @param value the MXAccess value, may be {@code null} or marked as null
* @return the boxed primitive, {@code String}, {@link Instant}, byte array,
* {@code List} of array elements, or {@code null} when the value
* carries no payload
*/
public static Object nativeValue(MxValue value) {
if (value == null || value.getIsNull()) {
return null;
@@ -99,6 +157,14 @@ public final class MxValues {
};
}
/**
* Converts an {@link MxArray} into a native Java {@link List}.
*
* @param array the MXAccess array, may be {@code null}
* @return a list of boxed primitives, strings, instants, or byte arrays;
* an empty list when the array carries no elements;
* {@code null} when {@code array} is {@code null}
*/
public static Object nativeArray(MxArray array) {
if (array == null) {
return null;
@@ -117,6 +183,12 @@ public final class MxValues {
};
}
/**
* Builds an {@link MxArray} of strings.
*
* @param values the string elements; the resulting array carries the size as a single dimension
* @return a populated {@code MxArray}
*/
public static MxArray stringArray(List<String> values) {
return MxArray.newBuilder()
.setElementDataType(MxDataType.MX_DATA_TYPE_STRING)
@@ -126,6 +198,12 @@ public final class MxValues {
.build();
}
/**
* Builds an {@link MxArray} of 32-bit integers.
*
* @param values the int32 elements; the resulting array carries the size as a single dimension
* @return a populated {@code MxArray}
*/
public static MxArray int32Array(List<Integer> values) {
return MxArray.newBuilder()
.setElementDataType(MxDataType.MX_DATA_TYPE_INTEGER)
@@ -135,6 +213,12 @@ public final class MxValues {
.build();
}
/**
* Returns a stable name for the {@link MxValue} kind, useful for logs.
*
* @param value the MXAccess value, may be {@code null}
* @return the {@code KindCase} name, or {@code "KIND_NOT_SET"} when {@code value} is {@code null}
*/
public static String kindName(MxValue value) {
return value == null ? "KIND_NOT_SET" : value.getKindCase().name();
}