rename: apply ZB.MOM.WW prefix to all client SDKs + fix pre-existing alarm-RPC breaks
Rename across every client surface using each language's idiomatic convention:
* .NET clients/dotnet/MxGateway.Client[.Cli|.Tests]/
-> clients/dotnet/ZB.MOM.WW.MxGateway.Client[.Cli|.Tests]/
namespaces -> ZB.MOM.WW.MxGateway.Client[.Cli|.Tests]
contracts ProjectReference repointed to ZB.MOM.WW.MxGateway.Contracts
sln migrated to slnx (dotnet sln migrate)
* Python src/mxgateway -> src/zb_mom_ww_mxgateway
src/mxgateway_cli -> src/zb_mom_ww_mxgateway_cli
distribution: mxaccess-gateway-client -> zb-mom-ww-mxaccess-gateway-client
* Rust crate: mxgateway-client -> zb-mom-ww-mxgateway-client
build.rs proto path repointed
* Java subprojects: mxgateway-{client,cli} -> zb-mom-ww-mxgateway-{client,cli}
packages com.dohertylan.mxgateway -> com.zb.mom.ww.mxgateway
group com.dohertylan.mxgateway -> com.zb.mom.ww.mxgateway
rootProject mxaccessgw-java -> zb-mom-ww-mxaccessgw-java
* Go generate-proto.ps1 proto path repointed; module path and
package mxgateway kept (Go convention).
* proto-inputs.json: generatedOutputs.python updated to new package path.
* scripts/run-client-e2e-tests.ps1: Java CLI install path + gradle task
updated to zb-mom-ww-mxgateway-cli.
CLI binary names (mxgw, mxgw-py, mxgw-go, mxgateway-cli) and wire-level
identifiers (MXGATEWAY_* env vars, the mxgw_<id>_<secret> API key
prefix, protobuf package names like mxaccess_gateway.v1, all MXAccess
references) intentionally NOT renamed.
Fix pre-existing alarms-over-gateway breaks unblocked by the rename:
* mxaccess_gateway.proto: add missing public message QueryActiveAlarmsRequest
{session_id, client_correlation_id, alarm_filter_prefix} and missing
rpc QueryActiveAlarms(QueryActiveAlarmsRequest) returns
(stream ActiveAlarmSnapshot). All four typed clients referenced
these but they were absent from the proto.
* MxAccessGatewayService.QueryActiveAlarms: implement the new RPC on
the server, streaming from IGatewayAlarmService.CurrentAlarms with
optional alarm_filter_prefix filter.
* clients/dotnet/.../DiscoverHierarchyOptions.cs: add the hand-written
.NET POCO that wraps DiscoverHierarchyRequest (referenced by
GalaxyRepositoryClient.DiscoverHierarchyAsync but never authored).
* Drop retired session_id field references from
AcknowledgeAlarmRequest/AcknowledgeAlarmReply test fixtures across
.NET, Rust, Go, and Python clients.
* Rust integration test: add the missing stream_alarms impl on the
fake MxAccessGateway server (the trait gained the method, fake
didn't).
* Rust CLI test: bump expected gatewayProtocolVersion 2 -> 3.
Regenerated artifacts updated in this commit:
* src/ZB.MOM.WW.MxGateway.Contracts/Generated/{MxaccessGateway,MxaccessGatewayGrpc}.cs
* clients/python/src/zb_mom_ww_mxgateway/generated/*_pb2{,_grpc}.py
* clients/go/internal/generated/*.pb.go
(C# regenerated by Grpc.Tools on contracts build; Python and Go via
their generate-proto.ps1 scripts; Rust regenerates from .proto via
tonic-build at compile time so no checked-in artefact.)
Verification: 472 server tests, 275 worker tests (9 dev-rig skipped),
18 integration tests (live MxAccess + LDAP + Galaxy), 57 .NET client
tests, 32 Rust workspace tests, 39 Python tests, all Go packages, and
gradle build for Java all pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+136
@@ -0,0 +1,136 @@
|
||||
package com.zb.mom.ww.mxgateway.client;
|
||||
|
||||
import galaxy_repository.v1.GalaxyRepositoryOuterClass.DeployEvent;
|
||||
import galaxy_repository.v1.GalaxyRepositoryOuterClass.WatchDeployEventsRequest;
|
||||
import io.grpc.Status;
|
||||
import io.grpc.StatusRuntimeException;
|
||||
import io.grpc.stub.ClientCallStreamObserver;
|
||||
import io.grpc.stub.ClientResponseObserver;
|
||||
import java.util.Iterator;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* Iterator-style adaptor over the {@code WatchDeployEvents} server-streaming
|
||||
* RPC. Mirrors {@link MxEventStream}: events arrive on a background gRPC thread
|
||||
* and are buffered in a bounded blocking queue; the iterator drains them.
|
||||
* Closing the stream cancels the underlying gRPC call.
|
||||
*/
|
||||
public final class DeployEventStream implements Iterator<DeployEvent>, AutoCloseable {
|
||||
private static final Object END = new Object();
|
||||
|
||||
private final BlockingQueue<Object> queue;
|
||||
private final AtomicBoolean closed = new AtomicBoolean();
|
||||
private volatile ClientCallStreamObserver<WatchDeployEventsRequest> requestStream;
|
||||
private Object next;
|
||||
|
||||
DeployEventStream(int capacity) {
|
||||
queue = new ArrayBlockingQueue<>(capacity);
|
||||
}
|
||||
|
||||
ClientResponseObserver<WatchDeployEventsRequest, DeployEvent> observer() {
|
||||
return new ClientResponseObserver<>() {
|
||||
@Override
|
||||
public void beforeStart(ClientCallStreamObserver<WatchDeployEventsRequest> requestStream) {
|
||||
DeployEventStream.this.requestStream = requestStream;
|
||||
if (closed.get()) {
|
||||
requestStream.cancel("client cancelled deploy event stream", null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNext(DeployEvent value) {
|
||||
offer(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable error) {
|
||||
if (Status.fromThrowable(error).getCode() == Status.Code.CANCELLED && closed.get()) {
|
||||
offer(END);
|
||||
return;
|
||||
}
|
||||
offer(error);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCompleted() {
|
||||
offer(END);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
if (next == END) {
|
||||
return false;
|
||||
}
|
||||
if (next == null) {
|
||||
next = take();
|
||||
}
|
||||
if (next instanceof RuntimeException runtimeException) {
|
||||
next = END;
|
||||
throw runtimeException;
|
||||
}
|
||||
if (next instanceof Throwable throwable) {
|
||||
next = END;
|
||||
throw new MxGatewayException(
|
||||
"galaxy watch deploy events failed: " + throwable.getMessage(), throwable);
|
||||
}
|
||||
return next != END;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeployEvent next() {
|
||||
if (!hasNext()) {
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
Object value = next;
|
||||
next = null;
|
||||
return (DeployEvent) value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
closed.set(true);
|
||||
ClientCallStreamObserver<WatchDeployEventsRequest> stream = requestStream;
|
||||
if (stream != null) {
|
||||
stream.cancel("client cancelled deploy event stream", null);
|
||||
}
|
||||
offer(END);
|
||||
}
|
||||
|
||||
private Object take() {
|
||||
while (true) {
|
||||
try {
|
||||
return queue.take();
|
||||
} catch (InterruptedException error) {
|
||||
Thread.currentThread().interrupt();
|
||||
return new StatusRuntimeException(
|
||||
Status.CANCELLED.withDescription("interrupted while reading deploy events"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void offer(Object value) {
|
||||
Objects.requireNonNull(value, "value");
|
||||
if (value == END) {
|
||||
if (!queue.offer(value)) {
|
||||
queue.clear();
|
||||
queue.offer(value);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!queue.offer(value)) {
|
||||
ClientCallStreamObserver<WatchDeployEventsRequest> stream = requestStream;
|
||||
if (stream != null) {
|
||||
stream.cancel("client deploy event stream queue overflowed", null);
|
||||
}
|
||||
queue.clear();
|
||||
queue.offer(new MxGatewayException("galaxy watch deploy events queue overflowed"));
|
||||
queue.offer(END);
|
||||
}
|
||||
}
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
package com.zb.mom.ww.mxgateway.client;
|
||||
|
||||
import galaxy_repository.v1.GalaxyRepositoryOuterClass.DeployEvent;
|
||||
import galaxy_repository.v1.GalaxyRepositoryOuterClass.WatchDeployEventsRequest;
|
||||
import io.grpc.stub.ClientCallStreamObserver;
|
||||
import io.grpc.stub.ClientResponseObserver;
|
||||
import io.grpc.stub.StreamObserver;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/**
|
||||
* Cancellable handle returned by the async {@code watchDeployEvents} variant.
|
||||
* Mirrors {@link MxGatewayEventSubscription} but for the Galaxy Repository
|
||||
* deploy-event stream.
|
||||
*/
|
||||
public final class DeployEventSubscription implements AutoCloseable {
|
||||
private final AtomicReference<ClientCallStreamObserver<WatchDeployEventsRequest>> requestStream =
|
||||
new AtomicReference<>();
|
||||
private final AtomicBoolean cancelled = new AtomicBoolean();
|
||||
|
||||
ClientResponseObserver<WatchDeployEventsRequest, DeployEvent> wrap(StreamObserver<DeployEvent> observer) {
|
||||
return new ClientResponseObserver<>() {
|
||||
@Override
|
||||
public void beforeStart(ClientCallStreamObserver<WatchDeployEventsRequest> stream) {
|
||||
requestStream.set(stream);
|
||||
if (cancelled.get()) {
|
||||
stream.cancel("client cancelled deploy event stream", null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNext(DeployEvent value) {
|
||||
observer.onNext(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable error) {
|
||||
observer.onError(error);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCompleted() {
|
||||
observer.onCompleted();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
if (stream != null) {
|
||||
stream.cancel("client cancelled deploy event stream", null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
cancel();
|
||||
}
|
||||
}
|
||||
+392
@@ -0,0 +1,392 @@
|
||||
package com.zb.mom.ww.mxgateway.client;
|
||||
|
||||
import com.google.common.util.concurrent.FutureCallback;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
import galaxy_repository.v1.GalaxyRepositoryGrpc;
|
||||
import galaxy_repository.v1.GalaxyRepositoryOuterClass.DeployEvent;
|
||||
import galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyReply;
|
||||
import galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest;
|
||||
import galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject;
|
||||
import galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeReply;
|
||||
import galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeRequest;
|
||||
import galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionReply;
|
||||
import galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionRequest;
|
||||
import galaxy_repository.v1.GalaxyRepositoryOuterClass.WatchDeployEventsRequest;
|
||||
import com.google.protobuf.Timestamp;
|
||||
import io.grpc.Channel;
|
||||
import io.grpc.ClientInterceptors;
|
||||
import io.grpc.ManagedChannel;
|
||||
import io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts;
|
||||
import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder;
|
||||
import io.grpc.stub.StreamObserver;
|
||||
import java.time.Instant;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import javax.net.ssl.SSLException;
|
||||
|
||||
/**
|
||||
* Thin wrapper around the generated {@link GalaxyRepositoryGrpc} stubs that
|
||||
* exposes the three metadata-only RPCs of the Galaxy Repository service in
|
||||
* idiomatic Java types. Mirrors the constructor and option-handling style of
|
||||
* {@link MxGatewayClient}.
|
||||
*/
|
||||
public final class GalaxyRepositoryClient implements AutoCloseable {
|
||||
private static final int DISCOVER_HIERARCHY_PAGE_SIZE = 5000;
|
||||
|
||||
private final ManagedChannel ownedChannel;
|
||||
private final MxGatewayClientOptions options;
|
||||
private final GalaxyRepositoryGrpc.GalaxyRepositoryBlockingStub blockingStub;
|
||||
private final GalaxyRepositoryGrpc.GalaxyRepositoryFutureStub futureStub;
|
||||
private final GalaxyRepositoryGrpc.GalaxyRepositoryStub asyncStub;
|
||||
|
||||
private GalaxyRepositoryClient(ManagedChannel channel, MxGatewayClientOptions options) {
|
||||
this.ownedChannel = channel;
|
||||
this.options = options;
|
||||
Channel intercepted = ClientInterceptors.intercept(channel, new MxGatewayAuthInterceptor(options.apiKey()));
|
||||
blockingStub = GalaxyRepositoryGrpc.newBlockingStub(intercepted);
|
||||
futureStub = GalaxyRepositoryGrpc.newFutureStub(intercepted);
|
||||
asyncStub = GalaxyRepositoryGrpc.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 GalaxyRepositoryClient(Channel channel, MxGatewayClientOptions options) {
|
||||
this.ownedChannel = null;
|
||||
this.options = Objects.requireNonNull(options, "options");
|
||||
Channel intercepted = ClientInterceptors.intercept(channel, new MxGatewayAuthInterceptor(options.apiKey()));
|
||||
blockingStub = GalaxyRepositoryGrpc.newBlockingStub(intercepted);
|
||||
futureStub = GalaxyRepositoryGrpc.newFutureStub(intercepted);
|
||||
asyncStub = GalaxyRepositoryGrpc.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 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
TestConnectionReply reply = rawBlockingStub().testConnection(TestConnectionRequest.getDefaultInstance());
|
||||
return reply.getOk();
|
||||
} catch (RuntimeException error) {
|
||||
if (error instanceof MxGatewayException) {
|
||||
throw error;
|
||||
}
|
||||
throw MxGatewayErrors.fromGrpc("galaxy test connection", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
GetLastDeployTimeReply reply =
|
||||
rawBlockingStub().getLastDeployTime(GetLastDeployTimeRequest.getDefaultInstance());
|
||||
return mapDeployTime(reply);
|
||||
} catch (RuntimeException error) {
|
||||
if (error instanceof MxGatewayException) {
|
||||
throw error;
|
||||
}
|
||||
throw MxGatewayErrors.fromGrpc("galaxy get last deploy time", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
java.util.ArrayList<GalaxyObject> objects = new java.util.ArrayList<>();
|
||||
java.util.HashSet<String> seenPageTokens = new java.util.HashSet<>();
|
||||
String pageToken = "";
|
||||
do {
|
||||
DiscoverHierarchyReply reply = rawBlockingStub().discoverHierarchy(DiscoverHierarchyRequest.newBuilder()
|
||||
.setPageSize(DISCOVER_HIERARCHY_PAGE_SIZE)
|
||||
.setPageToken(pageToken)
|
||||
.build());
|
||||
objects.addAll(reply.getObjectsList());
|
||||
pageToken = reply.getNextPageToken();
|
||||
if (!pageToken.isBlank() && !seenPageTokens.add(pageToken)) {
|
||||
throw new MxGatewayException(
|
||||
"galaxy discover hierarchy returned repeated page token: " + pageToken);
|
||||
}
|
||||
} while (!pageToken.isBlank());
|
||||
return objects;
|
||||
} catch (RuntimeException error) {
|
||||
if (error instanceof MxGatewayException) {
|
||||
throw error;
|
||||
}
|
||||
throw MxGatewayErrors.fromGrpc("galaxy discover hierarchy", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 discoverHierarchyPageAsync("", new java.util.ArrayList<>(), new java.util.HashSet<>());
|
||||
}
|
||||
|
||||
/**
|
||||
* 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-{@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);
|
||||
withStreamDeadline(rawAsyncStub()).watchDeployEvents(buildWatchRequest(lastSeenDeployTime), stream.observer());
|
||||
return stream;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
Objects.requireNonNull(observer, "observer");
|
||||
DeployEventSubscription subscription = new DeployEventSubscription();
|
||||
withStreamDeadline(rawAsyncStub())
|
||||
.watchDeployEvents(buildWatchRequest(lastSeenDeployTime), subscription.wrap(observer));
|
||||
return subscription;
|
||||
}
|
||||
|
||||
private static WatchDeployEventsRequest buildWatchRequest(Instant lastSeenDeployTime) {
|
||||
WatchDeployEventsRequest.Builder builder = WatchDeployEventsRequest.newBuilder();
|
||||
if (lastSeenDeployTime != null) {
|
||||
builder.setLastSeenDeployTime(Timestamp.newBuilder()
|
||||
.setSeconds(lastSeenDeployTime.getEpochSecond())
|
||||
.setNanos(lastSeenDeployTime.getNano())
|
||||
.build());
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private <T extends io.grpc.stub.AbstractStub<T>> T withStreamDeadline(T stub) {
|
||||
if (options.streamTimeout() == null || options.streamTimeout().isNegative()) {
|
||||
return stub;
|
||||
}
|
||||
return stub.withDeadlineAfter(options.streamTimeout().toNanos(), TimeUnit.NANOSECONDS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (ownedChannel != null) {
|
||||
ownedChannel.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
if (!ownedChannel.awaitTermination(options.connectTimeout().toMillis(), TimeUnit.MILLISECONDS)) {
|
||||
ownedChannel.shutdownNow();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Optional<Instant> mapDeployTime(GetLastDeployTimeReply reply) {
|
||||
if (!reply.getPresent()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
Timestamp ts = reply.getTimeOfLastDeploy();
|
||||
return Optional.of(Instant.ofEpochSecond(ts.getSeconds(), ts.getNanos()));
|
||||
}
|
||||
|
||||
private static ManagedChannel createChannel(MxGatewayClientOptions options) {
|
||||
NettyChannelBuilder builder = NettyChannelBuilder.forTarget(options.endpoint())
|
||||
.maxInboundMessageSize(options.maxGrpcMessageBytes());
|
||||
if (!options.connectTimeout().isNegative()) {
|
||||
builder.withOption(
|
||||
io.grpc.netty.shaded.io.netty.channel.ChannelOption.CONNECT_TIMEOUT_MILLIS,
|
||||
Math.toIntExact(options.connectTimeout().toMillis()));
|
||||
}
|
||||
if (options.plaintext()) {
|
||||
builder.usePlaintext();
|
||||
} else if (options.caCertificatePath() != null) {
|
||||
try {
|
||||
builder.sslContext(GrpcSslContexts.forClient()
|
||||
.trustManager(options.caCertificatePath().toFile())
|
||||
.build());
|
||||
} catch (SSLException error) {
|
||||
throw new MxGatewayException("failed to configure galaxy repository TLS", error);
|
||||
}
|
||||
} else {
|
||||
builder.useTransportSecurity();
|
||||
}
|
||||
if (!options.serverNameOverride().isBlank()) {
|
||||
builder.overrideAuthority(options.serverNameOverride());
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private <T extends io.grpc.stub.AbstractStub<T>> T withDeadline(T stub) {
|
||||
if (options.callTimeout().isNegative()) {
|
||||
return stub;
|
||||
}
|
||||
return stub.withDeadlineAfter(options.callTimeout().toNanos(), TimeUnit.NANOSECONDS);
|
||||
}
|
||||
|
||||
private CompletableFuture<List<GalaxyObject>> discoverHierarchyPageAsync(
|
||||
String pageToken, java.util.ArrayList<GalaxyObject> objects, java.util.HashSet<String> seenPageTokens) {
|
||||
DiscoverHierarchyRequest request = DiscoverHierarchyRequest.newBuilder()
|
||||
.setPageSize(DISCOVER_HIERARCHY_PAGE_SIZE)
|
||||
.setPageToken(pageToken)
|
||||
.build();
|
||||
return toCompletable(rawFutureStub().discoverHierarchy(request)).thenCompose(reply -> {
|
||||
objects.addAll(reply.getObjectsList());
|
||||
if (reply.getNextPageToken().isBlank()) {
|
||||
return CompletableFuture.completedFuture(objects);
|
||||
}
|
||||
if (!seenPageTokens.add(reply.getNextPageToken())) {
|
||||
CompletableFuture<List<GalaxyObject>> failed = new CompletableFuture<>();
|
||||
failed.completeExceptionally(new MxGatewayException(
|
||||
"galaxy discover hierarchy returned repeated page token: " + reply.getNextPageToken()));
|
||||
return failed;
|
||||
}
|
||||
return discoverHierarchyPageAsync(reply.getNextPageToken(), objects, seenPageTokens);
|
||||
});
|
||||
}
|
||||
|
||||
private static <T> CompletableFuture<T> toCompletable(com.google.common.util.concurrent.ListenableFuture<T> source) {
|
||||
CompletableFuture<T> target = new CompletableFuture<>();
|
||||
Futures.addCallback(
|
||||
source,
|
||||
new FutureCallback<>() {
|
||||
@Override
|
||||
public void onSuccess(T result) {
|
||||
target.complete(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable error) {
|
||||
if (error instanceof RuntimeException runtimeException) {
|
||||
target.completeExceptionally(MxGatewayErrors.fromGrpc("galaxy async call", runtimeException));
|
||||
return;
|
||||
}
|
||||
target.completeExceptionally(error);
|
||||
}
|
||||
},
|
||||
MoreExecutors.directExecutor());
|
||||
target.whenComplete((ignoredResult, ignoredError) -> {
|
||||
if (target.isCancelled()) {
|
||||
source.cancel(true);
|
||||
}
|
||||
});
|
||||
return target;
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package com.zb.mom.ww.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);
|
||||
}
|
||||
}
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
package com.zb.mom.ww.mxgateway.client;
|
||||
|
||||
import io.grpc.Status;
|
||||
import io.grpc.StatusRuntimeException;
|
||||
import io.grpc.stub.ClientCallStreamObserver;
|
||||
import io.grpc.stub.ClientResponseObserver;
|
||||
import java.util.Iterator;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
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();
|
||||
|
||||
private final BlockingQueue<Object> queue;
|
||||
private volatile ClientCallStreamObserver<StreamEventsRequest> requestStream;
|
||||
private volatile boolean closed;
|
||||
private Object next;
|
||||
|
||||
MxEventStream(int capacity) {
|
||||
queue = new ArrayBlockingQueue<>(capacity);
|
||||
}
|
||||
|
||||
ClientResponseObserver<StreamEventsRequest, MxEvent> observer() {
|
||||
return new ClientResponseObserver<>() {
|
||||
@Override
|
||||
public void beforeStart(ClientCallStreamObserver<StreamEventsRequest> requestStream) {
|
||||
MxEventStream.this.requestStream = requestStream;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNext(MxEvent value) {
|
||||
offer(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable error) {
|
||||
if (Status.fromThrowable(error).getCode() == Status.Code.CANCELLED && closed) {
|
||||
offer(END);
|
||||
return;
|
||||
}
|
||||
offer(error);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCompleted() {
|
||||
offer(END);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
if (next == END) {
|
||||
return false;
|
||||
}
|
||||
if (next == null) {
|
||||
next = take();
|
||||
}
|
||||
if (next instanceof RuntimeException runtimeException) {
|
||||
next = END;
|
||||
throw runtimeException;
|
||||
}
|
||||
if (next instanceof Throwable throwable) {
|
||||
next = END;
|
||||
throw new MxGatewayException("gateway stream events failed: " + throwable.getMessage(), throwable);
|
||||
}
|
||||
return next != END;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MxEvent next() {
|
||||
if (!hasNext()) {
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
Object value = next;
|
||||
next = null;
|
||||
return (MxEvent) value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
closed = true;
|
||||
ClientCallStreamObserver<StreamEventsRequest> stream = requestStream;
|
||||
if (stream != null) {
|
||||
stream.cancel("client cancelled event stream", null);
|
||||
}
|
||||
offer(END);
|
||||
}
|
||||
|
||||
private Object take() {
|
||||
while (true) {
|
||||
try {
|
||||
return queue.take();
|
||||
} catch (InterruptedException error) {
|
||||
Thread.currentThread().interrupt();
|
||||
return new StatusRuntimeException(Status.CANCELLED.withDescription("interrupted while reading events"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void offer(Object value) {
|
||||
Objects.requireNonNull(value, "value");
|
||||
if (value == END) {
|
||||
if (!queue.offer(value)) {
|
||||
queue.clear();
|
||||
queue.offer(value);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!queue.offer(value)) {
|
||||
ClientCallStreamObserver<StreamEventsRequest> stream = requestStream;
|
||||
if (stream != null) {
|
||||
stream.cancel("client event stream queue overflowed", null);
|
||||
}
|
||||
queue.clear();
|
||||
queue.offer(new MxGatewayException("gateway stream events queue overflowed"));
|
||||
queue.offer(END);
|
||||
}
|
||||
}
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
package com.zb.mom.ww.mxgateway.client;
|
||||
|
||||
import io.grpc.stub.ClientCallStreamObserver;
|
||||
import io.grpc.stub.ClientResponseObserver;
|
||||
import io.grpc.stub.StreamObserver;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.ActiveAlarmSnapshot;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.QueryActiveAlarmsRequest;
|
||||
|
||||
/**
|
||||
* Cancellable handle returned by {@code queryActiveAlarms}.
|
||||
*
|
||||
* <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 MxGatewayActiveAlarmsSubscription implements AutoCloseable {
|
||||
private final AtomicReference<ClientCallStreamObserver<QueryActiveAlarmsRequest>> requestStream = new AtomicReference<>();
|
||||
private final AtomicBoolean cancelled = new AtomicBoolean();
|
||||
|
||||
ClientResponseObserver<QueryActiveAlarmsRequest, ActiveAlarmSnapshot> wrap(StreamObserver<ActiveAlarmSnapshot> observer) {
|
||||
return new ClientResponseObserver<>() {
|
||||
@Override
|
||||
public void beforeStart(ClientCallStreamObserver<QueryActiveAlarmsRequest> stream) {
|
||||
requestStream.set(stream);
|
||||
if (cancelled.get()) {
|
||||
stream.cancel("client cancelled active-alarms query", null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNext(ActiveAlarmSnapshot value) {
|
||||
observer.onNext(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable error) {
|
||||
observer.onError(error);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCompleted() {
|
||||
observer.onCompleted();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<QueryActiveAlarmsRequest> stream = requestStream.get();
|
||||
if (stream != null) {
|
||||
stream.cancel("client cancelled active-alarms query", null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
cancel();
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package com.zb.mom.ww.mxgateway.client;
|
||||
|
||||
import io.grpc.CallOptions;
|
||||
import io.grpc.Channel;
|
||||
import io.grpc.ClientCall;
|
||||
import io.grpc.ClientInterceptor;
|
||||
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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
|
||||
MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) {
|
||||
ClientCall<ReqT, RespT> call = next.newCall(method, callOptions);
|
||||
if (apiKey.isBlank()) {
|
||||
return call;
|
||||
}
|
||||
|
||||
return new ForwardingClientCall.SimpleForwardingClientCall<>(call) {
|
||||
@Override
|
||||
public void start(Listener<RespT> responseListener, Metadata headers) {
|
||||
headers.put(AUTHORIZATION_HEADER, "Bearer " + apiKey);
|
||||
super.start(responseListener, headers);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package com.zb.mom.ww.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);
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package com.zb.mom.ww.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);
|
||||
}
|
||||
}
|
||||
+418
@@ -0,0 +1,418 @@
|
||||
package com.zb.mom.ww.mxgateway.client;
|
||||
|
||||
import com.google.common.util.concurrent.FutureCallback;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
import com.google.protobuf.Duration;
|
||||
import io.grpc.Channel;
|
||||
import io.grpc.ClientInterceptors;
|
||||
import io.grpc.ManagedChannel;
|
||||
import io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts;
|
||||
import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder;
|
||||
import io.grpc.stub.StreamObserver;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import javax.net.ssl.SSLException;
|
||||
import mxaccess_gateway.v1.MxAccessGatewayGrpc;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.AcknowledgeAlarmReply;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.AcknowledgeAlarmRequest;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.ActiveAlarmSnapshot;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.CloseSessionReply;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.CloseSessionRequest;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.MxCommandReply;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.MxCommandRequest;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.MxEvent;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.OpenSessionReply;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.OpenSessionRequest;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatusCode;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.QueryActiveAlarmsRequest;
|
||||
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;
|
||||
private final MxAccessGatewayGrpc.MxAccessGatewayBlockingStub blockingStub;
|
||||
private final MxAccessGatewayGrpc.MxAccessGatewayFutureStub futureStub;
|
||||
private final MxAccessGatewayGrpc.MxAccessGatewayStub asyncStub;
|
||||
|
||||
private MxGatewayClient(ManagedChannel channel, MxGatewayClientOptions options) {
|
||||
this.ownedChannel = channel;
|
||||
this.options = options;
|
||||
Channel intercepted = ClientInterceptors.intercept(channel, new MxGatewayAuthInterceptor(options.apiKey()));
|
||||
blockingStub = MxAccessGatewayGrpc.newBlockingStub(intercepted);
|
||||
futureStub = MxAccessGatewayGrpc.newFutureStub(intercepted);
|
||||
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");
|
||||
Channel intercepted = ClientInterceptors.intercept(channel, new MxGatewayAuthInterceptor(options.apiKey()));
|
||||
blockingStub = MxAccessGatewayGrpc.newBlockingStub(intercepted);
|
||||
futureStub = MxAccessGatewayGrpc.newFutureStub(intercepted);
|
||||
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)
|
||||
.setCommandTimeout(Duration.newBuilder()
|
||||
.setSeconds(options.callTimeout().toSeconds())
|
||||
.setNanos(options.callTimeout().toNanosPart())
|
||||
.build())
|
||||
.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);
|
||||
MxGatewayErrors.ensureProtocolSuccess("open session", reply.getProtocolStatus(), null);
|
||||
return reply;
|
||||
} catch (RuntimeException error) {
|
||||
if (error instanceof MxGatewayException) {
|
||||
throw error;
|
||||
}
|
||||
throw MxGatewayErrors.fromGrpc("open session", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 -> {
|
||||
MxGatewayErrors.ensureProtocolSuccess("open session", reply.getProtocolStatus(), null);
|
||||
return reply;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
MxGatewayErrors.ensureProtocolSuccess("invoke", reply.getProtocolStatus(), reply);
|
||||
MxGatewayErrors.ensureMxAccessSuccess("invoke", reply);
|
||||
return reply;
|
||||
} catch (RuntimeException error) {
|
||||
if (error instanceof MxGatewayException) {
|
||||
throw error;
|
||||
}
|
||||
throw MxGatewayErrors.fromGrpc("invoke", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 -> {
|
||||
MxGatewayErrors.ensureProtocolSuccess("invoke", reply.getProtocolStatus(), reply);
|
||||
MxGatewayErrors.ensureMxAccessSuccess("invoke", reply);
|
||||
return reply;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
MxGatewayErrors.ensureProtocolSuccess("close session", reply.getProtocolStatus(), null);
|
||||
return reply;
|
||||
} catch (RuntimeException error) {
|
||||
if (error instanceof MxGatewayException) {
|
||||
throw error;
|
||||
}
|
||||
throw MxGatewayErrors.fromGrpc("close session", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
withStreamDeadline(rawAsyncStub()).streamEvents(request, subscription.wrap(observer));
|
||||
return subscription;
|
||||
}
|
||||
|
||||
/**
|
||||
* Acknowledges an active MXAccess alarm condition through the gateway.
|
||||
*
|
||||
* <p>The gateway authenticates the request against the API key's
|
||||
* {@code invoke:alarm-ack} scope and forwards the acknowledge to the
|
||||
* worker's MXAccess session; the resulting native MxStatus is returned
|
||||
* in the reply. Acks are idempotent at the MxAccess layer.
|
||||
*
|
||||
* @param request the {@code AcknowledgeAlarmRequest}
|
||||
* @return the raw acknowledge reply
|
||||
* @throws MxGatewayException on transport or protocol failure
|
||||
*/
|
||||
public AcknowledgeAlarmReply acknowledgeAlarm(AcknowledgeAlarmRequest request) {
|
||||
try {
|
||||
AcknowledgeAlarmReply reply = rawBlockingStub().acknowledgeAlarm(request);
|
||||
MxGatewayErrors.ensureProtocolSuccess("acknowledge alarm", reply.getProtocolStatus(), null);
|
||||
return reply;
|
||||
} catch (RuntimeException error) {
|
||||
if (error instanceof MxGatewayException) {
|
||||
throw error;
|
||||
}
|
||||
throw MxGatewayErrors.fromGrpc("acknowledge alarm", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Acknowledges an active MXAccess alarm condition asynchronously.
|
||||
*
|
||||
* @param request the {@code AcknowledgeAlarmRequest}
|
||||
* @return a future completed with the raw reply, or completed exceptionally
|
||||
* with {@link MxGatewayException} on failure
|
||||
*/
|
||||
public CompletableFuture<AcknowledgeAlarmReply> acknowledgeAlarmAsync(AcknowledgeAlarmRequest request) {
|
||||
CompletableFuture<AcknowledgeAlarmReply> future = toCompletable(rawFutureStub().acknowledgeAlarm(request));
|
||||
return future.thenApply(reply -> {
|
||||
MxGatewayErrors.ensureProtocolSuccess("acknowledge alarm", reply.getProtocolStatus(), null);
|
||||
return reply;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Streams a snapshot of all alarms currently Active or ActiveAcked — the
|
||||
* gateway's ConditionRefresh equivalent. Used after reconnect to seed
|
||||
* local Part 9 state.
|
||||
*
|
||||
* @param request the {@code QueryActiveAlarmsRequest}, optionally scoped by
|
||||
* alarm-reference prefix
|
||||
* @param observer caller-supplied observer that receives snapshots and completion
|
||||
* @return a cancellable subscription handle
|
||||
*/
|
||||
public MxGatewayActiveAlarmsSubscription queryActiveAlarms(
|
||||
QueryActiveAlarmsRequest request, StreamObserver<ActiveAlarmSnapshot> observer) {
|
||||
MxGatewayActiveAlarmsSubscription subscription = new MxGatewayActiveAlarmsSubscription();
|
||||
withStreamDeadline(rawAsyncStub()).queryActiveAlarms(request, subscription.wrap(observer));
|
||||
return subscription;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (ownedChannel != null) {
|
||||
ownedChannel.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
if (!ownedChannel.awaitTermination(options.connectTimeout().toMillis(), TimeUnit.MILLISECONDS)) {
|
||||
ownedChannel.shutdownNow();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static ManagedChannel createChannel(MxGatewayClientOptions options) {
|
||||
NettyChannelBuilder builder = NettyChannelBuilder.forTarget(options.endpoint())
|
||||
.maxInboundMessageSize(options.maxGrpcMessageBytes());
|
||||
if (!options.connectTimeout().isNegative()) {
|
||||
builder.withOption(
|
||||
io.grpc.netty.shaded.io.netty.channel.ChannelOption.CONNECT_TIMEOUT_MILLIS,
|
||||
Math.toIntExact(options.connectTimeout().toMillis()));
|
||||
}
|
||||
if (options.plaintext()) {
|
||||
builder.usePlaintext();
|
||||
} else if (options.caCertificatePath() != null) {
|
||||
try {
|
||||
builder.sslContext(GrpcSslContexts.forClient()
|
||||
.trustManager(options.caCertificatePath().toFile())
|
||||
.build());
|
||||
} catch (SSLException error) {
|
||||
throw new MxGatewayException("failed to configure gateway TLS", error);
|
||||
}
|
||||
} else {
|
||||
builder.useTransportSecurity();
|
||||
}
|
||||
if (!options.serverNameOverride().isBlank()) {
|
||||
builder.overrideAuthority(options.serverNameOverride());
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private <T extends io.grpc.stub.AbstractStub<T>> T withDeadline(T stub) {
|
||||
if (options.callTimeout().isNegative()) {
|
||||
return stub;
|
||||
}
|
||||
return stub.withDeadlineAfter(options.callTimeout().toNanos(), TimeUnit.NANOSECONDS);
|
||||
}
|
||||
|
||||
private <T extends io.grpc.stub.AbstractStub<T>> T withStreamDeadline(T stub) {
|
||||
if (options.streamTimeout() == null || options.streamTimeout().isNegative()) {
|
||||
return stub;
|
||||
}
|
||||
return stub.withDeadlineAfter(options.streamTimeout().toNanos(), TimeUnit.NANOSECONDS);
|
||||
}
|
||||
|
||||
private static <T> CompletableFuture<T> toCompletable(com.google.common.util.concurrent.ListenableFuture<T> source) {
|
||||
CompletableFuture<T> target = new CompletableFuture<>();
|
||||
Futures.addCallback(
|
||||
source,
|
||||
new FutureCallback<>() {
|
||||
@Override
|
||||
public void onSuccess(T result) {
|
||||
target.complete(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable error) {
|
||||
if (error instanceof RuntimeException runtimeException) {
|
||||
target.completeExceptionally(MxGatewayErrors.fromGrpc("async call", runtimeException));
|
||||
return;
|
||||
}
|
||||
target.completeExceptionally(error);
|
||||
}
|
||||
},
|
||||
MoreExecutors.directExecutor());
|
||||
target.whenComplete((ignoredResult, ignoredError) -> {
|
||||
if (target.isCancelled()) {
|
||||
source.cancel(true);
|
||||
}
|
||||
});
|
||||
return target;
|
||||
}
|
||||
|
||||
static ProtocolStatusCode okStatusCode() {
|
||||
return ProtocolStatusCode.PROTOCOL_STATUS_CODE_OK;
|
||||
}
|
||||
}
|
||||
+295
@@ -0,0 +1,295 @@
|
||||
package com.zb.mom.ww.mxgateway.client;
|
||||
|
||||
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);
|
||||
private static final int DEFAULT_MAX_GRPC_MESSAGE_BYTES = 16 * 1024 * 1024;
|
||||
|
||||
private final String endpoint;
|
||||
private final String apiKey;
|
||||
private final boolean plaintext;
|
||||
private final Path caCertificatePath;
|
||||
private final String serverNameOverride;
|
||||
private final Duration connectTimeout;
|
||||
private final Duration callTimeout;
|
||||
private final Duration streamTimeout;
|
||||
private final int maxGrpcMessageBytes;
|
||||
|
||||
private MxGatewayClientOptions(Builder builder) {
|
||||
endpoint = requireText(builder.endpoint, "endpoint");
|
||||
apiKey = builder.apiKey == null ? "" : builder.apiKey;
|
||||
plaintext = builder.plaintext;
|
||||
caCertificatePath = builder.caCertificatePath;
|
||||
serverNameOverride = builder.serverNameOverride == null ? "" : builder.serverNameOverride;
|
||||
connectTimeout = builder.connectTimeout == null ? DEFAULT_CONNECT_TIMEOUT : builder.connectTimeout;
|
||||
callTimeout = builder.callTimeout == null ? DEFAULT_CALL_TIMEOUT : builder.callTimeout;
|
||||
streamTimeout = builder.streamTimeout;
|
||||
maxGrpcMessageBytes = builder.maxGrpcMessageBytes <= 0
|
||||
? DEFAULT_MAX_GRPC_MESSAGE_BYTES
|
||||
: builder.maxGrpcMessageBytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
public int maxGrpcMessageBytes() {
|
||||
return maxGrpcMessageBytes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "MxGatewayClientOptions{"
|
||||
+ "endpoint='"
|
||||
+ endpoint
|
||||
+ '\''
|
||||
+ ", apiKey='"
|
||||
+ redactedApiKey()
|
||||
+ '\''
|
||||
+ ", plaintext="
|
||||
+ plaintext
|
||||
+ ", caCertificatePath="
|
||||
+ caCertificatePath
|
||||
+ ", serverNameOverride='"
|
||||
+ serverNameOverride
|
||||
+ '\''
|
||||
+ ", connectTimeout="
|
||||
+ connectTimeout
|
||||
+ ", callTimeout="
|
||||
+ callTimeout
|
||||
+ ", streamTimeout="
|
||||
+ streamTimeout
|
||||
+ ", maxGrpcMessageBytes="
|
||||
+ maxGrpcMessageBytes
|
||||
+ '}';
|
||||
}
|
||||
|
||||
private static String requireText(String value, String name) {
|
||||
if (value == null || value.isBlank()) {
|
||||
throw new IllegalArgumentException(name + " is required");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mutable builder for {@link MxGatewayClientOptions}.
|
||||
*/
|
||||
public static final class Builder {
|
||||
private String endpoint;
|
||||
private String apiKey;
|
||||
private boolean plaintext;
|
||||
private Path caCertificatePath;
|
||||
private String serverNameOverride;
|
||||
private Duration connectTimeout;
|
||||
private Duration callTimeout;
|
||||
private Duration streamTimeout;
|
||||
private int maxGrpcMessageBytes;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public Builder maxGrpcMessageBytes(int value) {
|
||||
maxGrpcMessageBytes = value;
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package com.zb.mom.ww.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 = 3;
|
||||
private static final int WORKER_PROTOCOL_VERSION = 1;
|
||||
private static final String CLIENT_VERSION = "0.1.0";
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package com.zb.mom.ww.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;
|
||||
}
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
package com.zb.mom.ww.mxgateway.client;
|
||||
|
||||
import io.grpc.Status;
|
||||
import io.grpc.StatusRuntimeException;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.MxCommandReply;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatus;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatusCode;
|
||||
|
||||
final class MxGatewayErrors {
|
||||
private MxGatewayErrors() {
|
||||
}
|
||||
|
||||
static RuntimeException fromGrpc(String operation, RuntimeException error) {
|
||||
if (error instanceof StatusRuntimeException statusError) {
|
||||
Status status = statusError.getStatus();
|
||||
String message = MxGatewaySecrets.redactCredentials(status.getDescription());
|
||||
return switch (status.getCode()) {
|
||||
case UNAUTHENTICATED -> new MxGatewayAuthenticationException(
|
||||
"authentication failed: " + message, statusError);
|
||||
case PERMISSION_DENIED -> new MxGatewayAuthorizationException(
|
||||
"authorization failed: " + message, statusError);
|
||||
case DEADLINE_EXCEEDED -> new MxGatewayException("gateway call timed out: " + message, statusError);
|
||||
case CANCELLED -> new MxGatewayException("gateway call cancelled: " + message, statusError);
|
||||
default -> new MxGatewayException("gateway " + operation + " failed: " + message, statusError);
|
||||
};
|
||||
}
|
||||
|
||||
return new MxGatewayException("gateway " + operation + " failed: " + error.getMessage(), error);
|
||||
}
|
||||
|
||||
static void ensureProtocolSuccess(String operation, ProtocolStatus status, MxCommandReply reply) {
|
||||
if (status == null || status.getCode() == ProtocolStatusCode.PROTOCOL_STATUS_CODE_OK) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw switch (status.getCode()) {
|
||||
case PROTOCOL_STATUS_CODE_SESSION_NOT_FOUND, PROTOCOL_STATUS_CODE_SESSION_NOT_READY ->
|
||||
new MxGatewaySessionException(operation, status);
|
||||
case PROTOCOL_STATUS_CODE_WORKER_UNAVAILABLE, PROTOCOL_STATUS_CODE_PROTOCOL_VIOLATION ->
|
||||
new MxGatewayWorkerException(operation, status);
|
||||
case PROTOCOL_STATUS_CODE_MXACCESS_FAILURE -> new MxAccessException(operation, status, reply);
|
||||
default -> new MxGatewayCommandException(operation, status, reply);
|
||||
};
|
||||
}
|
||||
|
||||
static void ensureMxAccessSuccess(String operation, MxCommandReply reply) {
|
||||
if (reply == null) {
|
||||
return;
|
||||
}
|
||||
if (reply.hasHresult() && reply.getHresult() != 0) {
|
||||
throw new MxAccessException(operation, reply);
|
||||
}
|
||||
for (var status : reply.getStatusesList()) {
|
||||
if (!MxStatuses.succeeded(status)) {
|
||||
throw new MxAccessException(operation, reply);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static String protocolStatusMessage(String operation, ProtocolStatus status) {
|
||||
if (status == null) {
|
||||
return "mxgateway " + operation + " failed with missing protocol status";
|
||||
}
|
||||
if (status.getMessage().isBlank()) {
|
||||
return "mxgateway " + operation + " failed with protocol status " + status.getCode();
|
||||
}
|
||||
return "mxgateway " + operation + " failed with protocol status "
|
||||
+ status.getCode()
|
||||
+ ": "
|
||||
+ status.getMessage();
|
||||
}
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
package com.zb.mom.ww.mxgateway.client;
|
||||
|
||||
import io.grpc.stub.ClientCallStreamObserver;
|
||||
import io.grpc.stub.ClientResponseObserver;
|
||||
import io.grpc.stub.StreamObserver;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
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();
|
||||
|
||||
ClientResponseObserver<StreamEventsRequest, MxEvent> wrap(StreamObserver<MxEvent> observer) {
|
||||
return new ClientResponseObserver<>() {
|
||||
@Override
|
||||
public void beforeStart(ClientCallStreamObserver<StreamEventsRequest> stream) {
|
||||
requestStream.set(stream);
|
||||
if (cancelled.get()) {
|
||||
stream.cancel("client cancelled event stream", null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNext(MxEvent value) {
|
||||
observer.onNext(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable error) {
|
||||
observer.onError(error);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCompleted() {
|
||||
observer.onCompleted();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
if (stream != null) {
|
||||
stream.cancel("client cancelled event stream", null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
cancel();
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package com.zb.mom.ww.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);
|
||||
}
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
package com.zb.mom.ww.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 "";
|
||||
}
|
||||
if (apiKey.length() <= 8) {
|
||||
return "<redacted>";
|
||||
}
|
||||
|
||||
return apiKey.substring(0, 4)
|
||||
+ "*".repeat(apiKey.length() - 8)
|
||||
+ 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;
|
||||
}
|
||||
|
||||
String[] parts = value.split("\\s+");
|
||||
for (int index = 0; index < parts.length; index++) {
|
||||
if (parts[index].startsWith("mxgw_") || parts[index].equalsIgnoreCase("bearer")) {
|
||||
parts[index] = "<redacted>";
|
||||
}
|
||||
}
|
||||
return String.join(" ", parts);
|
||||
}
|
||||
}
|
||||
+526
@@ -0,0 +1,526 @@
|
||||
package com.zb.mom.ww.mxgateway.client;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.util.HexFormat;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.AddItem2Command;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.AddItemBulkCommand;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.AddItemCommand;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.AdviseItemBulkCommand;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.AdviseCommand;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.CloseSessionReply;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.CloseSessionRequest;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.MxCommand;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.MxCommandKind;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.MxCommandReply;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.MxCommandRequest;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.MxValue;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.OpenSessionReply;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.RegisterCommand;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.RemoveItemBulkCommand;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.RemoveItemCommand;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.StreamEventsRequest;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.SubscribeBulkCommand;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.SubscribeResult;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.UnAdviseCommand;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.UnAdviseItemBulkCommand;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.UnsubscribeBulkCommand;
|
||||
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();
|
||||
|
||||
private final MxGatewayClient client;
|
||||
private final OpenSessionReply openReply;
|
||||
private CloseSessionReply closeReply;
|
||||
|
||||
MxGatewaySession(MxGatewayClient client, OpenSessionReply openReply) {
|
||||
this.client = Objects.requireNonNull(client, "client");
|
||||
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()
|
||||
.setSessionId(sessionId())
|
||||
.setClientCorrelationId(newCorrelationId())
|
||||
.build());
|
||||
}
|
||||
return closeReply;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
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()) {
|
||||
return reply.getRegister().getServerHandle();
|
||||
}
|
||||
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)
|
||||
.setRegister(RegisterCommand.newBuilder().setClientName(clientName))
|
||||
.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)
|
||||
.setUnregister(UnregisterCommand.newBuilder().setServerHandle(serverHandle))
|
||||
.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()) {
|
||||
return reply.getAddItem().getItemHandle();
|
||||
}
|
||||
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)
|
||||
.setAddItem(AddItemCommand.newBuilder()
|
||||
.setServerHandle(serverHandle)
|
||||
.setItemDefinition(itemDefinition))
|
||||
.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()) {
|
||||
return reply.getAddItem2().getItemHandle();
|
||||
}
|
||||
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)
|
||||
.setAddItem2(AddItem2Command.newBuilder()
|
||||
.setServerHandle(serverHandle)
|
||||
.setItemDefinition(itemDefinition)
|
||||
.setItemContext(itemContext))
|
||||
.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)
|
||||
.setRemoveItem(RemoveItemCommand.newBuilder()
|
||||
.setServerHandle(serverHandle)
|
||||
.setItemHandle(itemHandle))
|
||||
.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)
|
||||
.setAdvise(AdviseCommand.newBuilder()
|
||||
.setServerHandle(serverHandle)
|
||||
.setItemHandle(itemHandle))
|
||||
.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)
|
||||
.setUnAdvise(UnAdviseCommand.newBuilder()
|
||||
.setServerHandle(serverHandle)
|
||||
.setItemHandle(itemHandle))
|
||||
.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()
|
||||
.setKind(MxCommandKind.MX_COMMAND_KIND_ADD_ITEM_BULK)
|
||||
.setAddItemBulk(AddItemBulkCommand.newBuilder()
|
||||
.setServerHandle(serverHandle)
|
||||
.addAllTagAddresses(tagAddresses))
|
||||
.build());
|
||||
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()
|
||||
.setKind(MxCommandKind.MX_COMMAND_KIND_ADVISE_ITEM_BULK)
|
||||
.setAdviseItemBulk(AdviseItemBulkCommand.newBuilder()
|
||||
.setServerHandle(serverHandle)
|
||||
.addAllItemHandles(itemHandles))
|
||||
.build());
|
||||
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()
|
||||
.setKind(MxCommandKind.MX_COMMAND_KIND_REMOVE_ITEM_BULK)
|
||||
.setRemoveItemBulk(RemoveItemBulkCommand.newBuilder()
|
||||
.setServerHandle(serverHandle)
|
||||
.addAllItemHandles(itemHandles))
|
||||
.build());
|
||||
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()
|
||||
.setKind(MxCommandKind.MX_COMMAND_KIND_UN_ADVISE_ITEM_BULK)
|
||||
.setUnAdviseItemBulk(UnAdviseItemBulkCommand.newBuilder()
|
||||
.setServerHandle(serverHandle)
|
||||
.addAllItemHandles(itemHandles))
|
||||
.build());
|
||||
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()
|
||||
.setKind(MxCommandKind.MX_COMMAND_KIND_SUBSCRIBE_BULK)
|
||||
.setSubscribeBulk(SubscribeBulkCommand.newBuilder()
|
||||
.setServerHandle(serverHandle)
|
||||
.addAllTagAddresses(tagAddresses))
|
||||
.build());
|
||||
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()
|
||||
.setKind(MxCommandKind.MX_COMMAND_KIND_UNSUBSCRIBE_BULK)
|
||||
.setUnsubscribeBulk(UnsubscribeBulkCommand.newBuilder()
|
||||
.setServerHandle(serverHandle)
|
||||
.addAllItemHandles(itemHandles))
|
||||
.build());
|
||||
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)
|
||||
.setWrite(WriteCommand.newBuilder()
|
||||
.setServerHandle(serverHandle)
|
||||
.setItemHandle(itemHandle)
|
||||
.setValue(value)
|
||||
.setUserId(userId))
|
||||
.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)
|
||||
.setWrite2(Write2Command.newBuilder()
|
||||
.setServerHandle(serverHandle)
|
||||
.setItemHandle(itemHandle)
|
||||
.setValue(value)
|
||||
.setTimestampValue(timestampValue)
|
||||
.setUserId(userId))
|
||||
.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())
|
||||
.setAfterWorkerSequence(afterWorkerSequence)
|
||||
.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())
|
||||
.setClientCorrelationId(newCorrelationId())
|
||||
.setCommand(command)
|
||||
.build());
|
||||
}
|
||||
|
||||
private static String newCorrelationId() {
|
||||
byte[] bytes = new byte[16];
|
||||
RANDOM.nextBytes(bytes);
|
||||
return HexFormat.of().formatHex(bytes);
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package com.zb.mom.ww.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;
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package com.zb.mom.ww.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;
|
||||
}
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
package com.zb.mom.ww.mxgateway.client;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+254
@@ -0,0 +1,254 @@
|
||||
package com.zb.mom.ww.mxgateway.client;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
import com.google.protobuf.Timestamp;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.BoolArray;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.DoubleArray;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.FloatArray;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.Int32Array;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.Int64Array;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.MxArray;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.MxDataType;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.MxValue;
|
||||
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)
|
||||
.setVariantType("VT_BOOL")
|
||||
.setBoolValue(value)
|
||||
.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)
|
||||
.setVariantType("VT_I4")
|
||||
.setInt32Value(value)
|
||||
.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)
|
||||
.setVariantType("VT_I8")
|
||||
.setInt64Value(value)
|
||||
.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)
|
||||
.setVariantType("VT_R4")
|
||||
.setFloatValue(value)
|
||||
.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)
|
||||
.setVariantType("VT_R8")
|
||||
.setDoubleValue(value)
|
||||
.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)
|
||||
.setVariantType("VT_BSTR")
|
||||
.setStringValue(value)
|
||||
.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)
|
||||
.setVariantType("VT_DATE")
|
||||
.setTimestampValue(Timestamp.newBuilder()
|
||||
.setSeconds(value.getEpochSecond())
|
||||
.setNanos(value.getNano())
|
||||
.build())
|
||||
.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;
|
||||
}
|
||||
|
||||
return switch (value.getKindCase()) {
|
||||
case BOOL_VALUE -> value.getBoolValue();
|
||||
case INT32_VALUE -> value.getInt32Value();
|
||||
case INT64_VALUE -> value.getInt64Value();
|
||||
case FLOAT_VALUE -> value.getFloatValue();
|
||||
case DOUBLE_VALUE -> value.getDoubleValue();
|
||||
case STRING_VALUE -> value.getStringValue();
|
||||
case TIMESTAMP_VALUE -> instant(value.getTimestampValue());
|
||||
case ARRAY_VALUE -> nativeArray(value.getArrayValue());
|
||||
case RAW_VALUE -> value.getRawValue().toByteArray();
|
||||
case KIND_NOT_SET -> null;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
return switch (array.getValuesCase()) {
|
||||
case BOOL_VALUES -> List.copyOf(array.getBoolValues().getValuesList());
|
||||
case INT32_VALUES -> List.copyOf(array.getInt32Values().getValuesList());
|
||||
case INT64_VALUES -> List.copyOf(array.getInt64Values().getValuesList());
|
||||
case FLOAT_VALUES -> List.copyOf(array.getFloatValues().getValuesList());
|
||||
case DOUBLE_VALUES -> List.copyOf(array.getDoubleValues().getValuesList());
|
||||
case STRING_VALUES -> List.copyOf(array.getStringValues().getValuesList());
|
||||
case TIMESTAMP_VALUES -> timestampValues(array.getTimestampValues());
|
||||
case RAW_VALUES -> rawValues(array.getRawValues());
|
||||
case VALUES_NOT_SET -> List.of();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
.setVariantType("VT_ARRAY|VT_BSTR")
|
||||
.addDimensions(values.size())
|
||||
.setStringValues(StringArray.newBuilder().addAllValues(values))
|
||||
.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)
|
||||
.setVariantType("VT_ARRAY|VT_I4")
|
||||
.addDimensions(values.size())
|
||||
.setInt32Values(Int32Array.newBuilder().addAllValues(values))
|
||||
.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();
|
||||
}
|
||||
|
||||
private static Instant instant(Timestamp timestamp) {
|
||||
return Instant.ofEpochSecond(timestamp.getSeconds(), timestamp.getNanos());
|
||||
}
|
||||
|
||||
private static List<Instant> timestampValues(TimestampArray array) {
|
||||
List<Instant> values = new ArrayList<>();
|
||||
for (Timestamp timestamp : array.getValuesList()) {
|
||||
values.add(instant(timestamp));
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
private static List<byte[]> rawValues(RawArray array) {
|
||||
List<byte[]> values = new ArrayList<>();
|
||||
for (ByteString rawValue : array.getValuesList()) {
|
||||
values.add(rawValue.toByteArray());
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static void generatedTypeReferences(
|
||||
BoolArray boolArray,
|
||||
Int64Array int64Array,
|
||||
FloatArray floatArray,
|
||||
DoubleArray doubleArray) {
|
||||
// Keeps generated repeated-value imports visible for javadocs and IDE navigation.
|
||||
}
|
||||
}
|
||||
+426
@@ -0,0 +1,426 @@
|
||||
package com.zb.mom.ww.mxgateway.client;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import com.google.protobuf.Timestamp;
|
||||
import galaxy_repository.v1.GalaxyRepositoryGrpc;
|
||||
import galaxy_repository.v1.GalaxyRepositoryOuterClass.DeployEvent;
|
||||
import galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyReply;
|
||||
import galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest;
|
||||
import galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute;
|
||||
import galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject;
|
||||
import galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeReply;
|
||||
import galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeRequest;
|
||||
import galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionReply;
|
||||
import galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionRequest;
|
||||
import galaxy_repository.v1.GalaxyRepositoryOuterClass.WatchDeployEventsRequest;
|
||||
import io.grpc.ManagedChannel;
|
||||
import io.grpc.Metadata;
|
||||
import io.grpc.Server;
|
||||
import io.grpc.ServerCall;
|
||||
import io.grpc.ServerCallHandler;
|
||||
import io.grpc.ServerInterceptor;
|
||||
import io.grpc.inprocess.InProcessChannelBuilder;
|
||||
import io.grpc.inprocess.InProcessServerBuilder;
|
||||
import io.grpc.stub.ClientCallStreamObserver;
|
||||
import io.grpc.stub.ClientResponseObserver;
|
||||
import io.grpc.stub.StreamObserver;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class GalaxyRepositoryClientTests {
|
||||
@Test
|
||||
void testConnectionReturnsOkAndSendsAuthMetadata() throws Exception {
|
||||
AtomicReference<String> authorization = new AtomicReference<>();
|
||||
TestService service = new TestService() {
|
||||
@Override
|
||||
public void testConnection(
|
||||
TestConnectionRequest request, StreamObserver<TestConnectionReply> responseObserver) {
|
||||
responseObserver.onNext(TestConnectionReply.newBuilder().setOk(true).build());
|
||||
responseObserver.onCompleted();
|
||||
}
|
||||
};
|
||||
|
||||
try (InProcessGalaxy g = InProcessGalaxy.start(service, authorization);
|
||||
GalaxyRepositoryClient client = g.client("mxgw_galaxy_secret")) {
|
||||
assertTrue(client.testConnection());
|
||||
assertEquals("Bearer mxgw_galaxy_secret", authorization.get());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void getLastDeployTimeReturnsEmptyWhenPresentFalse() throws Exception {
|
||||
TestService service = new TestService() {
|
||||
@Override
|
||||
public void getLastDeployTime(
|
||||
GetLastDeployTimeRequest request, StreamObserver<GetLastDeployTimeReply> responseObserver) {
|
||||
responseObserver.onNext(
|
||||
GetLastDeployTimeReply.newBuilder().setPresent(false).build());
|
||||
responseObserver.onCompleted();
|
||||
}
|
||||
};
|
||||
|
||||
try (InProcessGalaxy g = InProcessGalaxy.start(service, new AtomicReference<>());
|
||||
GalaxyRepositoryClient client = g.client("")) {
|
||||
Optional<Instant> result = client.getLastDeployTime();
|
||||
assertFalse(result.isPresent());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void getLastDeployTimeReturnsInstantWhenPresent() throws Exception {
|
||||
Timestamp expected = Timestamp.newBuilder().setSeconds(1_700_000_000L).setNanos(123_000_000).build();
|
||||
TestService service = new TestService() {
|
||||
@Override
|
||||
public void getLastDeployTime(
|
||||
GetLastDeployTimeRequest request, StreamObserver<GetLastDeployTimeReply> responseObserver) {
|
||||
responseObserver.onNext(GetLastDeployTimeReply.newBuilder()
|
||||
.setPresent(true)
|
||||
.setTimeOfLastDeploy(expected)
|
||||
.build());
|
||||
responseObserver.onCompleted();
|
||||
}
|
||||
};
|
||||
|
||||
try (InProcessGalaxy g = InProcessGalaxy.start(service, new AtomicReference<>());
|
||||
GalaxyRepositoryClient client = g.client("")) {
|
||||
Optional<Instant> result = client.getLastDeployTime();
|
||||
assertTrue(result.isPresent());
|
||||
assertEquals(Instant.ofEpochSecond(1_700_000_000L, 123_000_000), result.get());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void discoverHierarchyReturnsObjectsAndAttributes() throws Exception {
|
||||
AtomicReference<DiscoverHierarchyRequest> firstRequest = new AtomicReference<>();
|
||||
AtomicReference<DiscoverHierarchyRequest> secondRequest = new AtomicReference<>();
|
||||
TestService service = new TestService() {
|
||||
@Override
|
||||
public void discoverHierarchy(
|
||||
DiscoverHierarchyRequest request, StreamObserver<DiscoverHierarchyReply> responseObserver) {
|
||||
if (request.getPageToken().isEmpty()) {
|
||||
firstRequest.set(request);
|
||||
responseObserver.onNext(DiscoverHierarchyReply.newBuilder()
|
||||
.setNextPageToken("page-2")
|
||||
.setTotalObjectCount(2)
|
||||
.addObjects(GalaxyObject.newBuilder()
|
||||
.setGobjectId(7)
|
||||
.setTagName("Pump_001")
|
||||
.setContainedName("Pump")
|
||||
.setBrowseName("Pump")
|
||||
.setParentGobjectId(1)
|
||||
.setIsArea(false)
|
||||
.setCategoryId(3)
|
||||
.setHostedByGobjectId(0)
|
||||
.addTemplateChain("$Pump")
|
||||
.addAttributes(GalaxyAttribute.newBuilder()
|
||||
.setAttributeName("Speed")
|
||||
.setFullTagReference("Pump_001.Speed")
|
||||
.setMxDataType(5)
|
||||
.setDataTypeName("MxFloat")
|
||||
.setIsArray(false)
|
||||
.setIsHistorized(true)))
|
||||
.build());
|
||||
} else {
|
||||
secondRequest.set(request);
|
||||
responseObserver.onNext(DiscoverHierarchyReply.newBuilder()
|
||||
.setTotalObjectCount(2)
|
||||
.addObjects(GalaxyObject.newBuilder()
|
||||
.setGobjectId(8)
|
||||
.setTagName("Pump_002"))
|
||||
.build());
|
||||
}
|
||||
responseObserver.onCompleted();
|
||||
}
|
||||
};
|
||||
|
||||
try (InProcessGalaxy g = InProcessGalaxy.start(service, new AtomicReference<>());
|
||||
GalaxyRepositoryClient client = g.client("")) {
|
||||
List<GalaxyObject> objects = client.discoverHierarchy();
|
||||
assertEquals(2, objects.size());
|
||||
assertEquals(5000, firstRequest.get().getPageSize());
|
||||
assertEquals("", firstRequest.get().getPageToken());
|
||||
assertEquals("page-2", secondRequest.get().getPageToken());
|
||||
GalaxyObject only = objects.get(0);
|
||||
assertEquals(7, only.getGobjectId());
|
||||
assertEquals("Pump_001", only.getTagName());
|
||||
assertEquals(1, only.getAttributesCount());
|
||||
assertEquals("Pump_001.Speed", only.getAttributes(0).getFullTagReference());
|
||||
assertTrue(only.getAttributes(0).getIsHistorized());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void deployEventStreamCloseBeforeBeforeStartCancelsStream() {
|
||||
DeployEventStream stream = new DeployEventStream(4);
|
||||
ClientResponseObserver<WatchDeployEventsRequest, DeployEvent> observer = stream.observer();
|
||||
RecordingClientCallStreamObserver requestStream = new RecordingClientCallStreamObserver();
|
||||
|
||||
stream.close();
|
||||
observer.beforeStart(requestStream);
|
||||
|
||||
assertTrue(requestStream.cancelled);
|
||||
assertEquals("client cancelled deploy event stream", requestStream.cancelMessage);
|
||||
assertFalse(stream.hasNext());
|
||||
}
|
||||
|
||||
@Test
|
||||
void discoverHierarchyRejectsRepeatedPageToken() throws Exception {
|
||||
TestService service = new TestService() {
|
||||
@Override
|
||||
public void discoverHierarchy(
|
||||
DiscoverHierarchyRequest request, StreamObserver<DiscoverHierarchyReply> responseObserver) {
|
||||
responseObserver.onNext(DiscoverHierarchyReply.newBuilder()
|
||||
.setNextPageToken("7:1")
|
||||
.build());
|
||||
responseObserver.onCompleted();
|
||||
}
|
||||
};
|
||||
|
||||
try (InProcessGalaxy g = InProcessGalaxy.start(service, new AtomicReference<>());
|
||||
GalaxyRepositoryClient client = g.client("")) {
|
||||
MxGatewayException error = assertThrows(MxGatewayException.class, client::discoverHierarchy);
|
||||
|
||||
assertTrue(error.getMessage().contains("repeated page token"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void watchDeployEventsReceivesEventsInOrder() throws Exception {
|
||||
DeployEvent first = DeployEvent.newBuilder()
|
||||
.setSequence(1)
|
||||
.setObservedAt(Timestamp.newBuilder().setSeconds(1_700_000_000L).build())
|
||||
.setTimeOfLastDeploy(Timestamp.newBuilder().setSeconds(1_699_999_000L).build())
|
||||
.setTimeOfLastDeployPresent(true)
|
||||
.setObjectCount(42)
|
||||
.setAttributeCount(123)
|
||||
.build();
|
||||
DeployEvent second = DeployEvent.newBuilder()
|
||||
.setSequence(2)
|
||||
.setObservedAt(Timestamp.newBuilder().setSeconds(1_700_000_100L).build())
|
||||
.setTimeOfLastDeployPresent(false)
|
||||
.setObjectCount(43)
|
||||
.setAttributeCount(125)
|
||||
.build();
|
||||
|
||||
TestService service = new TestService() {
|
||||
@Override
|
||||
public void watchDeployEvents(
|
||||
WatchDeployEventsRequest request, StreamObserver<DeployEvent> responseObserver) {
|
||||
responseObserver.onNext(first);
|
||||
responseObserver.onNext(second);
|
||||
responseObserver.onCompleted();
|
||||
}
|
||||
};
|
||||
|
||||
try (InProcessGalaxy g = InProcessGalaxy.start(service, new AtomicReference<>());
|
||||
GalaxyRepositoryClient client = g.client("")) {
|
||||
try (DeployEventStream stream = client.watchDeployEvents(null)) {
|
||||
assertTrue(stream.hasNext());
|
||||
DeployEvent event1 = stream.next();
|
||||
assertEquals(1L, event1.getSequence());
|
||||
assertEquals(42, event1.getObjectCount());
|
||||
assertTrue(event1.getTimeOfLastDeployPresent());
|
||||
|
||||
assertTrue(stream.hasNext());
|
||||
DeployEvent event2 = stream.next();
|
||||
assertEquals(2L, event2.getSequence());
|
||||
assertFalse(event2.getTimeOfLastDeployPresent());
|
||||
|
||||
assertFalse(stream.hasNext());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void watchDeployEventsPropagatesLastSeenDeployTime() throws Exception {
|
||||
AtomicReference<WatchDeployEventsRequest> seen = new AtomicReference<>();
|
||||
Instant lastSeen = Instant.ofEpochSecond(1_700_000_000L, 250_000_000);
|
||||
|
||||
TestService service = new TestService() {
|
||||
@Override
|
||||
public void watchDeployEvents(
|
||||
WatchDeployEventsRequest request, StreamObserver<DeployEvent> responseObserver) {
|
||||
seen.set(request);
|
||||
responseObserver.onCompleted();
|
||||
}
|
||||
};
|
||||
|
||||
try (InProcessGalaxy g = InProcessGalaxy.start(service, new AtomicReference<>());
|
||||
GalaxyRepositoryClient client = g.client("")) {
|
||||
try (DeployEventStream stream = client.watchDeployEvents(lastSeen)) {
|
||||
assertFalse(stream.hasNext());
|
||||
}
|
||||
}
|
||||
|
||||
WatchDeployEventsRequest request = seen.get();
|
||||
assertNotNull(request);
|
||||
Timestamp expected = request.getLastSeenDeployTime();
|
||||
assertEquals(lastSeen.getEpochSecond(), expected.getSeconds());
|
||||
assertEquals(lastSeen.getNano(), expected.getNanos());
|
||||
}
|
||||
|
||||
@Test
|
||||
void watchDeployEventsClientCancellationTearsDownCleanly() throws Exception {
|
||||
CountDownLatch cancelObserved = new CountDownLatch(1);
|
||||
|
||||
TestService service = new TestService() {
|
||||
@Override
|
||||
public void watchDeployEvents(
|
||||
WatchDeployEventsRequest request, StreamObserver<DeployEvent> responseObserver) {
|
||||
io.grpc.stub.ServerCallStreamObserver<DeployEvent> serverObserver =
|
||||
(io.grpc.stub.ServerCallStreamObserver<DeployEvent>) responseObserver;
|
||||
serverObserver.setOnCancelHandler(cancelObserved::countDown);
|
||||
|
||||
DeployEvent bootstrap = DeployEvent.newBuilder()
|
||||
.setSequence(1)
|
||||
.setObservedAt(Timestamp.newBuilder().setSeconds(1L).build())
|
||||
.build();
|
||||
responseObserver.onNext(bootstrap);
|
||||
// Server holds the stream open; cancellation must come from the client.
|
||||
}
|
||||
};
|
||||
|
||||
try (InProcessGalaxy g = InProcessGalaxy.start(service, new AtomicReference<>());
|
||||
GalaxyRepositoryClient client = g.client("")) {
|
||||
DeployEventStream stream = client.watchDeployEvents(null);
|
||||
assertTrue(stream.hasNext());
|
||||
assertEquals(1L, stream.next().getSequence());
|
||||
|
||||
stream.close();
|
||||
assertTrue(
|
||||
cancelObserved.await(5, TimeUnit.SECONDS),
|
||||
"server should observe client-side cancellation");
|
||||
assertFalse(stream.hasNext());
|
||||
}
|
||||
}
|
||||
|
||||
private abstract static class TestService extends GalaxyRepositoryGrpc.GalaxyRepositoryImplBase {
|
||||
@Override
|
||||
public void testConnection(
|
||||
TestConnectionRequest request, StreamObserver<TestConnectionReply> responseObserver) {
|
||||
responseObserver.onNext(TestConnectionReply.newBuilder().setOk(true).build());
|
||||
responseObserver.onCompleted();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getLastDeployTime(
|
||||
GetLastDeployTimeRequest request, StreamObserver<GetLastDeployTimeReply> responseObserver) {
|
||||
responseObserver.onNext(GetLastDeployTimeReply.newBuilder().setPresent(false).build());
|
||||
responseObserver.onCompleted();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void discoverHierarchy(
|
||||
DiscoverHierarchyRequest request, StreamObserver<DiscoverHierarchyReply> responseObserver) {
|
||||
responseObserver.onNext(DiscoverHierarchyReply.getDefaultInstance());
|
||||
responseObserver.onCompleted();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void watchDeployEvents(
|
||||
WatchDeployEventsRequest request, StreamObserver<DeployEvent> responseObserver) {
|
||||
responseObserver.onCompleted();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class RecordingClientCallStreamObserver
|
||||
extends ClientCallStreamObserver<WatchDeployEventsRequest> {
|
||||
private boolean cancelled;
|
||||
private String cancelMessage;
|
||||
|
||||
@Override
|
||||
public boolean isReady() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOnReadyHandler(Runnable onReadyHandler) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disableAutoInboundFlowControl() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void request(int count) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMessageCompression(boolean enable) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel(String message, Throwable cause) {
|
||||
cancelled = true;
|
||||
cancelMessage = message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNext(WatchDeployEventsRequest value) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable error) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCompleted() {
|
||||
}
|
||||
}
|
||||
|
||||
private record InProcessGalaxy(Server server, ManagedChannel channel) implements AutoCloseable {
|
||||
static InProcessGalaxy start(
|
||||
GalaxyRepositoryGrpc.GalaxyRepositoryImplBase service, AtomicReference<String> authorization)
|
||||
throws Exception {
|
||||
String serverName = "mxgw-galaxy-java-" + UUID.randomUUID();
|
||||
ServerInterceptor interceptor = new ServerInterceptor() {
|
||||
@Override
|
||||
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
|
||||
ServerCall<ReqT, RespT> call,
|
||||
Metadata headers,
|
||||
ServerCallHandler<ReqT, RespT> next) {
|
||||
authorization.set(headers.get(MxGatewayAuthInterceptor.AUTHORIZATION_HEADER));
|
||||
return next.startCall(call, headers);
|
||||
}
|
||||
};
|
||||
Server server = InProcessServerBuilder.forName(serverName)
|
||||
.directExecutor()
|
||||
.addService(io.grpc.ServerInterceptors.intercept(service, interceptor))
|
||||
.build()
|
||||
.start();
|
||||
ManagedChannel channel = InProcessChannelBuilder.forName(serverName)
|
||||
.directExecutor()
|
||||
.build();
|
||||
return new InProcessGalaxy(server, channel);
|
||||
}
|
||||
|
||||
GalaxyRepositoryClient client(String apiKey) {
|
||||
return new GalaxyRepositoryClient(
|
||||
channel,
|
||||
MxGatewayClientOptions.builder()
|
||||
.endpoint("in-process")
|
||||
.apiKey(apiKey)
|
||||
.plaintext(true)
|
||||
.callTimeout(Duration.ofSeconds(5))
|
||||
.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
channel.shutdownNow();
|
||||
server.shutdownNow();
|
||||
}
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package com.zb.mom.ww.mxgateway.client;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import mxaccess_gateway.v1.MxAccessGatewayGrpc;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.OpenSessionRequest;
|
||||
import mxaccess_worker.v1.MxaccessWorker.WorkerEnvelope;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class GeneratedContractSmokeTests {
|
||||
@Test
|
||||
void generatedGatewayAndWorkerContractsCompile() {
|
||||
OpenSessionRequest request = OpenSessionRequest.newBuilder()
|
||||
.setClientSessionName("junit")
|
||||
.build();
|
||||
WorkerEnvelope envelope = WorkerEnvelope.newBuilder()
|
||||
.setProtocolVersion(MxGatewayClientVersion.workerProtocolVersion())
|
||||
.build();
|
||||
|
||||
assertEquals("junit", request.getClientSessionName());
|
||||
assertEquals("mxaccess_gateway.v1.MxAccessGateway", MxAccessGatewayGrpc.SERVICE_NAME);
|
||||
assertEquals(1, envelope.getProtocolVersion());
|
||||
}
|
||||
|
||||
@Test
|
||||
void javaTwentyOneToolchainRunsTests() {
|
||||
assertEquals(21, Runtime.version().feature());
|
||||
}
|
||||
}
|
||||
+282
@@ -0,0 +1,282 @@
|
||||
package com.zb.mom.ww.mxgateway.client;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import io.grpc.Context;
|
||||
import io.grpc.ManagedChannel;
|
||||
import io.grpc.Metadata;
|
||||
import io.grpc.Server;
|
||||
import io.grpc.ServerCall;
|
||||
import io.grpc.ServerCallHandler;
|
||||
import io.grpc.ServerInterceptor;
|
||||
import io.grpc.inprocess.InProcessChannelBuilder;
|
||||
import io.grpc.inprocess.InProcessServerBuilder;
|
||||
import io.grpc.stub.ServerCallStreamObserver;
|
||||
import io.grpc.stub.StreamObserver;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import mxaccess_gateway.v1.MxAccessGatewayGrpc;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.AddItemReply;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.BulkSubscribeReply;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.CloseSessionReply;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.CloseSessionRequest;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.MxCommandKind;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.MxCommandReply;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.MxCommandRequest;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.MxEvent;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.OpenSessionReply;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.OpenSessionRequest;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatus;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatusCode;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.RegisterReply;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.SessionState;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.StreamEventsRequest;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.SubscribeResult;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class MxGatewayClientSessionTests {
|
||||
@Test
|
||||
void unaryCallsCarryAuthMetadataAndDeadline() throws Exception {
|
||||
AtomicReference<String> authorization = new AtomicReference<>();
|
||||
AtomicReference<MxCommandRequest> commandRequest = new AtomicReference<>();
|
||||
AtomicReference<Boolean> deadlineSeen = new AtomicReference<>(false);
|
||||
|
||||
TestGatewayService service = new TestGatewayService() {
|
||||
@Override
|
||||
public void openSession(OpenSessionRequest request, StreamObserver<OpenSessionReply> responseObserver) {
|
||||
deadlineSeen.set(Context.current().getDeadline() != null);
|
||||
responseObserver.onNext(OpenSessionReply.newBuilder()
|
||||
.setSessionId("session-java")
|
||||
.setProtocolStatus(ok())
|
||||
.build());
|
||||
responseObserver.onCompleted();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invoke(MxCommandRequest request, StreamObserver<MxCommandReply> responseObserver) {
|
||||
commandRequest.set(request);
|
||||
responseObserver.onNext(MxCommandReply.newBuilder()
|
||||
.setSessionId(request.getSessionId())
|
||||
.setKind(request.getCommand().getKind())
|
||||
.setProtocolStatus(ok())
|
||||
.setRegister(RegisterReply.newBuilder().setServerHandle(42))
|
||||
.build());
|
||||
responseObserver.onCompleted();
|
||||
}
|
||||
};
|
||||
|
||||
try (InProcessGateway gateway = InProcessGateway.start(service, authorization);
|
||||
MxGatewayClient client = gateway.client("mxgw_visible_secret", Duration.ofSeconds(5))) {
|
||||
MxGatewaySession session = client.openSession("junit-session");
|
||||
|
||||
int serverHandle = session.register("java-test-client");
|
||||
|
||||
assertEquals(42, serverHandle);
|
||||
assertEquals("Bearer mxgw_visible_secret", authorization.get());
|
||||
assertEquals("session-java", commandRequest.get().getSessionId());
|
||||
assertEquals(MxCommandKind.MX_COMMAND_KIND_REGISTER, commandRequest.get().getCommand().getKind());
|
||||
assertTrue(deadlineSeen.get());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void methodHelpersReturnTypedHandlesAndRawReplies() throws Exception {
|
||||
TestGatewayService service = new TestGatewayService() {
|
||||
@Override
|
||||
public void invoke(MxCommandRequest request, StreamObserver<MxCommandReply> responseObserver) {
|
||||
MxCommandReply.Builder reply = MxCommandReply.newBuilder()
|
||||
.setSessionId(request.getSessionId())
|
||||
.setKind(request.getCommand().getKind())
|
||||
.setProtocolStatus(ok());
|
||||
if (request.getCommand().getKind() == MxCommandKind.MX_COMMAND_KIND_ADD_ITEM) {
|
||||
reply.setAddItem(AddItemReply.newBuilder().setItemHandle(7));
|
||||
}
|
||||
responseObserver.onNext(reply.build());
|
||||
responseObserver.onCompleted();
|
||||
}
|
||||
};
|
||||
|
||||
try (InProcessGateway gateway = InProcessGateway.start(service, new AtomicReference<>());
|
||||
MxGatewayClient client = gateway.client("", Duration.ofSeconds(5))) {
|
||||
MxGatewaySession session = MxGatewaySession.forSessionId(client, "existing-session");
|
||||
|
||||
int itemHandle = session.addItem(12, "TestObject.TestInt");
|
||||
MxCommandReply raw = session.adviseRaw(12, itemHandle);
|
||||
|
||||
assertEquals(7, itemHandle);
|
||||
assertEquals(MxCommandKind.MX_COMMAND_KIND_ADVISE, raw.getKind());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void subscribeBulkBuildsOneBulkCommandAndReturnsResults() throws Exception {
|
||||
AtomicReference<MxCommandRequest> commandRequest = new AtomicReference<>();
|
||||
TestGatewayService service = new TestGatewayService() {
|
||||
@Override
|
||||
public void invoke(MxCommandRequest request, StreamObserver<MxCommandReply> responseObserver) {
|
||||
commandRequest.set(request);
|
||||
responseObserver.onNext(MxCommandReply.newBuilder()
|
||||
.setSessionId(request.getSessionId())
|
||||
.setKind(request.getCommand().getKind())
|
||||
.setProtocolStatus(ok())
|
||||
.setSubscribeBulk(BulkSubscribeReply.newBuilder()
|
||||
.addResults(SubscribeResult.newBuilder()
|
||||
.setServerHandle(12)
|
||||
.setTagAddress("Area001.Pump001.Speed")
|
||||
.setItemHandle(34)
|
||||
.setWasSuccessful(true)))
|
||||
.build());
|
||||
responseObserver.onCompleted();
|
||||
}
|
||||
};
|
||||
|
||||
try (InProcessGateway gateway = InProcessGateway.start(service, new AtomicReference<>());
|
||||
MxGatewayClient client = gateway.client("", Duration.ofSeconds(5))) {
|
||||
MxGatewaySession session = MxGatewaySession.forSessionId(client, "existing-session");
|
||||
|
||||
List<SubscribeResult> results = session.subscribeBulk(12, List.of("Area001.Pump001.Speed"));
|
||||
|
||||
assertEquals(34, results.get(0).getItemHandle());
|
||||
assertEquals(MxCommandKind.MX_COMMAND_KIND_SUBSCRIBE_BULK, commandRequest.get().getCommand().getKind());
|
||||
assertEquals(
|
||||
List.of("Area001.Pump001.Speed"),
|
||||
commandRequest.get().getCommand().getSubscribeBulk().getTagAddressesList());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void streamCancellationCancelsServerCall() throws Exception {
|
||||
CountDownLatch cancelled = new CountDownLatch(1);
|
||||
TestGatewayService service = new TestGatewayService() {
|
||||
@Override
|
||||
public void streamEvents(StreamEventsRequest request, StreamObserver<MxEvent> responseObserver) {
|
||||
ServerCallStreamObserver<MxEvent> serverObserver =
|
||||
(ServerCallStreamObserver<MxEvent>) responseObserver;
|
||||
serverObserver.setOnCancelHandler(cancelled::countDown);
|
||||
responseObserver.onNext(MxEvent.newBuilder()
|
||||
.setSessionId(request.getSessionId())
|
||||
.setWorkerSequence(1)
|
||||
.build());
|
||||
}
|
||||
};
|
||||
|
||||
try (InProcessGateway gateway = InProcessGateway.start(service, new AtomicReference<>());
|
||||
MxGatewayClient client = gateway.client("", Duration.ofSeconds(5))) {
|
||||
MxEventStream events = MxGatewaySession.forSessionId(client, "stream-session").streamEvents();
|
||||
|
||||
assertTrue(events.hasNext());
|
||||
assertEquals(1, events.next().getWorkerSequence());
|
||||
events.close();
|
||||
|
||||
assertTrue(cancelled.await(5, TimeUnit.SECONDS));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void commandFailureKeepsRawReply() throws Exception {
|
||||
TestGatewayService service = new TestGatewayService() {
|
||||
@Override
|
||||
public void invoke(MxCommandRequest request, StreamObserver<MxCommandReply> responseObserver) {
|
||||
responseObserver.onNext(MxCommandReply.newBuilder()
|
||||
.setSessionId(request.getSessionId())
|
||||
.setKind(MxCommandKind.MX_COMMAND_KIND_WRITE)
|
||||
.setProtocolStatus(ProtocolStatus.newBuilder()
|
||||
.setCode(ProtocolStatusCode.PROTOCOL_STATUS_CODE_MXACCESS_FAILURE)
|
||||
.setMessage("MXAccess rejected the write."))
|
||||
.setHresult(-2147220992)
|
||||
.build());
|
||||
responseObserver.onCompleted();
|
||||
}
|
||||
};
|
||||
|
||||
try (InProcessGateway gateway = InProcessGateway.start(service, new AtomicReference<>());
|
||||
MxGatewayClient client = gateway.client("", Duration.ofSeconds(5))) {
|
||||
MxGatewaySession session = MxGatewaySession.forSessionId(client, "failure-session");
|
||||
|
||||
MxAccessException error = assertThrows(
|
||||
MxAccessException.class,
|
||||
() -> session.write(1, 2, MxValues.int32Value(123), 0));
|
||||
|
||||
assertNotNull(error.reply());
|
||||
assertEquals(-2147220992, error.reply().getHresult());
|
||||
}
|
||||
}
|
||||
|
||||
private static ProtocolStatus ok() {
|
||||
return ProtocolStatus.newBuilder()
|
||||
.setCode(ProtocolStatusCode.PROTOCOL_STATUS_CODE_OK)
|
||||
.build();
|
||||
}
|
||||
|
||||
private abstract static class TestGatewayService extends MxAccessGatewayGrpc.MxAccessGatewayImplBase {
|
||||
@Override
|
||||
public void openSession(OpenSessionRequest request, StreamObserver<OpenSessionReply> responseObserver) {
|
||||
responseObserver.onNext(OpenSessionReply.newBuilder()
|
||||
.setSessionId("session-java")
|
||||
.setProtocolStatus(ok())
|
||||
.build());
|
||||
responseObserver.onCompleted();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void closeSession(CloseSessionRequest request, StreamObserver<CloseSessionReply> responseObserver) {
|
||||
responseObserver.onNext(CloseSessionReply.newBuilder()
|
||||
.setSessionId(request.getSessionId())
|
||||
.setFinalState(SessionState.SESSION_STATE_CLOSED)
|
||||
.setProtocolStatus(ok())
|
||||
.build());
|
||||
responseObserver.onCompleted();
|
||||
}
|
||||
}
|
||||
|
||||
private record InProcessGateway(Server server, ManagedChannel channel) implements AutoCloseable {
|
||||
static InProcessGateway start(
|
||||
MxAccessGatewayGrpc.MxAccessGatewayImplBase service, AtomicReference<String> authorization)
|
||||
throws Exception {
|
||||
String serverName = "mxgw-java-" + UUID.randomUUID();
|
||||
ServerInterceptor interceptor = new ServerInterceptor() {
|
||||
@Override
|
||||
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
|
||||
ServerCall<ReqT, RespT> call,
|
||||
Metadata headers,
|
||||
ServerCallHandler<ReqT, RespT> next) {
|
||||
authorization.set(headers.get(MxGatewayAuthInterceptor.AUTHORIZATION_HEADER));
|
||||
return next.startCall(call, headers);
|
||||
}
|
||||
};
|
||||
Server server = InProcessServerBuilder.forName(serverName)
|
||||
.directExecutor()
|
||||
.addService(io.grpc.ServerInterceptors.intercept(service, interceptor))
|
||||
.build()
|
||||
.start();
|
||||
ManagedChannel channel = InProcessChannelBuilder.forName(serverName)
|
||||
.directExecutor()
|
||||
.build();
|
||||
return new InProcessGateway(server, channel);
|
||||
}
|
||||
|
||||
MxGatewayClient client(String apiKey, Duration callTimeout) {
|
||||
return new MxGatewayClient(
|
||||
channel,
|
||||
MxGatewayClientOptions.builder()
|
||||
.endpoint("in-process")
|
||||
.apiKey(apiKey)
|
||||
.plaintext(true)
|
||||
.callTimeout(callTimeout)
|
||||
.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
channel.shutdownNow();
|
||||
server.shutdownNow();
|
||||
}
|
||||
}
|
||||
}
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
package com.zb.mom.ww.mxgateway.client;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.google.protobuf.util.JsonFormat;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.MxCommandReply;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.MxStatusCategory;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.MxStatusProxy;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.MxValue;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatusCode;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class MxGatewayFixtureTests {
|
||||
@Test
|
||||
void valueFixtureCasesExposeNativeProjectionAndRawMetadata() throws Exception {
|
||||
JsonArray cases = readFixture("values/value-conversion-cases.json").getAsJsonArray("cases");
|
||||
|
||||
for (var element : cases) {
|
||||
JsonObject testCase = element.getAsJsonObject();
|
||||
MxValue.Builder builder = MxValue.newBuilder();
|
||||
JsonFormat.parser().merge(testCase.getAsJsonObject("value").toString(), builder);
|
||||
MxValue value = builder.build();
|
||||
|
||||
assertEquals(testCase.get("expectedKind").getAsString(), lowerCamelKind(value));
|
||||
if ("timestamp.utc".equals(testCase.get("id").getAsString())) {
|
||||
assertEquals(Instant.parse("2026-01-01T00:00:04Z"), MxValues.nativeValue(value));
|
||||
}
|
||||
if ("string-array".equals(testCase.get("id").getAsString())) {
|
||||
assertEquals(List.of("alpha", "beta"), MxValues.nativeValue(value));
|
||||
}
|
||||
if ("raw-fallback.variant".equals(testCase.get("id").getAsString())) {
|
||||
assertEquals("No lossless typed projection exists for this VARIANT.", value.getRawDiagnostic());
|
||||
assertArrayEquals(new byte[] {1, 2, 3, 4, 5}, (byte[]) MxValues.nativeValue(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void statusFixtureCasesPreserveRawFields() throws Exception {
|
||||
JsonArray cases = readFixture("statuses/status-conversion-cases.json").getAsJsonArray("cases");
|
||||
|
||||
for (var element : cases) {
|
||||
JsonObject testCase = element.getAsJsonObject();
|
||||
MxStatusProxy.Builder builder = MxStatusProxy.newBuilder();
|
||||
JsonFormat.parser().merge(testCase.getAsJsonObject("status").toString(), builder);
|
||||
MxStatusProxy status = builder.build();
|
||||
|
||||
assertEquals(
|
||||
testCase.getAsJsonObject("status").get("rawCategory").getAsInt(),
|
||||
status.getRawCategory());
|
||||
if ("ok.responding-lmx".equals(testCase.get("id").getAsString())) {
|
||||
assertTrue(MxStatuses.succeeded(status));
|
||||
}
|
||||
if ("security-error.requesting-lmx".equals(testCase.get("id").getAsString())) {
|
||||
assertFalse(MxStatuses.succeeded(status));
|
||||
assertEquals(MxStatusCategory.MX_STATUS_CATEGORY_SECURITY_ERROR, MxStatuses.view(status).category());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void mxAccessFailureFixtureMapsToRichCommandException() throws Exception {
|
||||
MxCommandReply.Builder builder = MxCommandReply.newBuilder();
|
||||
JsonFormat.parser().merge(
|
||||
Files.readString(fixtureRoot().resolve("command-replies/write.mxaccess-failure.reply.json")),
|
||||
builder);
|
||||
MxCommandReply reply = builder.build();
|
||||
|
||||
try {
|
||||
MxGatewayErrors.ensureProtocolSuccess("invoke", reply.getProtocolStatus(), reply);
|
||||
} catch (MxAccessException error) {
|
||||
assertEquals(ProtocolStatusCode.PROTOCOL_STATUS_CODE_MXACCESS_FAILURE, error.protocolStatus().getCode());
|
||||
assertEquals(-2147220992, error.reply().getHresult());
|
||||
assertEquals(2, error.reply().getStatusesCount());
|
||||
return;
|
||||
}
|
||||
|
||||
throw new AssertionError("expected MxAccessException");
|
||||
}
|
||||
|
||||
@Test
|
||||
void grpcAuthErrorsAreClassifiedAndRedacted() {
|
||||
RuntimeException authError = MxGatewayErrors.fromGrpc(
|
||||
"open session",
|
||||
new io.grpc.StatusRuntimeException(io.grpc.Status.UNAUTHENTICATED.withDescription(
|
||||
"invalid API key mxgw_visible_secret")));
|
||||
RuntimeException permissionError = MxGatewayErrors.fromGrpc(
|
||||
"write",
|
||||
new io.grpc.StatusRuntimeException(io.grpc.Status.PERMISSION_DENIED.withDescription(
|
||||
"missing scope mxaccess.write")));
|
||||
|
||||
assertInstanceOf(MxGatewayAuthenticationException.class, authError);
|
||||
assertInstanceOf(MxGatewayAuthorizationException.class, permissionError);
|
||||
assertTrue(authError.getMessage().contains("<redacted>"));
|
||||
assertFalse(authError.getMessage().contains("visible_secret"));
|
||||
}
|
||||
|
||||
private static JsonObject readFixture(String relativePath) throws Exception {
|
||||
return JsonParser.parseString(Files.readString(fixtureRoot().resolve(relativePath))).getAsJsonObject();
|
||||
}
|
||||
|
||||
private static Path fixtureRoot() {
|
||||
Path current = Path.of(System.getProperty("user.dir")).toAbsolutePath();
|
||||
for (Path path = current; path != null; path = path.getParent()) {
|
||||
Path candidate = path.resolve("clients/proto/fixtures/behavior");
|
||||
if (Files.exists(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
candidate = path.resolve("../proto/fixtures/behavior").normalize();
|
||||
if (Files.exists(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException("could not locate behavior fixtures from " + current);
|
||||
}
|
||||
|
||||
private static String lowerCamelKind(MxValue value) {
|
||||
String[] parts = value.getKindCase().name().toLowerCase().split("_");
|
||||
StringBuilder result = new StringBuilder(parts[0]);
|
||||
for (int index = 1; index < parts.length; index++) {
|
||||
result.append(Character.toUpperCase(parts[index].charAt(0))).append(parts[index].substring(1));
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user