feat(java-client): CLI-15 ReplayGap surface + CLI-04 typed-command parity (5/5 complete)
Completes both findings across all five clients — done locally on the Mac now that homebrew openjdk@17 is available (JAVA_HOME=/opt/homebrew/opt/openjdk@17). CLI-15: new MxEventStreamItem record + MxEventStream.nextItem() surfaces the gateway's ReplayGap sentinel as a typed, non-terminal signal (isReplayGap()/ replayGap()/event()); existing Iterator<MxEvent> path unchanged, sentinel never swallowed/synthesized. Javadoc covers gap semantics + resume contract. CLI-04: typed single-item helpers on MxGatewaySession — Phase 1 adviseSupervisory/writeSecured/writeSecured2/authenticateUser/archestrAUserToId, Phase 2 addBufferedItem/setBufferedUpdateInterval/suspend/activate (unregister already present). Each routes through invokeCommand -> ensureProtocolSuccess + ensureMxAccessSuccess (same validation as bulk). MXAccess parity preserved. Credentials flow only into the request proto; exceptions carry only the reply and gRPC status text is scrubbed via MxGatewaySecrets.redactCredentials — tests assert the password/secured value is absent from getMessage()/toString()/CLI output. New CLI subcommands write-secured/authenticate-user (credential via --password/--password-env, prints only the user id). gradle test: 106 tests, 0 failures (58 client + 48 cli); no generated churn. Shared docs: ClientLibrariesDesign + CLAUDE.md updated to "all five clients". Claude-Session: https://claude.ai/code/session_01DMXXvNuPekkkrTEyPNxEkW
This commit is contained in:
+36
@@ -21,6 +21,24 @@ import mxaccess_gateway.v1.MxaccessGateway.StreamEventsRequest;
|
||||
* 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}.
|
||||
*
|
||||
* <p><strong>Single consumer.</strong> This stream is not safe to drain from
|
||||
* more than one thread. Interleave {@link #hasNext()}/{@link #next()} (or
|
||||
* {@link #nextItem()}) from a single consumer only; concurrent drains race on
|
||||
* the internal cursor.
|
||||
*
|
||||
* <p><strong>Reconnect-replay gap.</strong> When the stream was resumed via
|
||||
* {@code StreamEventsRequest.after_worker_sequence} and the requested cursor
|
||||
* predates the oldest event the gateway still retains, the gateway emits a
|
||||
* single gap sentinel {@link MxEvent} at the head of the stream with its
|
||||
* {@code replay_gap} field set (family unspecified, body oneof unset). It is a
|
||||
* distinct, non-terminal signal, forwarded verbatim — never synthesized and
|
||||
* never swallowed. {@link #next()} returns it as a normal {@link MxEvent}
|
||||
* (callers can test {@code event.hasReplayGap()}); {@link #nextItem()} wraps it
|
||||
* in an {@link MxEventStreamItem} whose {@link MxEventStreamItem#isReplayGap()}
|
||||
* is {@code true}. On a gap the consumer must discard cached per-item state and
|
||||
* re-snapshot, then resume without a further gap by requesting
|
||||
* {@code oldest_available_sequence - 1} as the next {@code after_worker_sequence}.
|
||||
*/
|
||||
public final class MxEventStream implements Iterator<MxEvent>, AutoCloseable {
|
||||
private static final Object END = new Object();
|
||||
@@ -91,6 +109,24 @@ public final class MxEventStream implements Iterator<MxEvent>, AutoCloseable {
|
||||
return (MxEvent) value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drains the next stream element as a typed {@link MxEventStreamItem} so a
|
||||
* consumer can branch on the reconnect-replay gap sentinel via
|
||||
* {@link MxEventStreamItem#isReplayGap()} without inspecting the raw event.
|
||||
*
|
||||
* <p>Equivalent to wrapping {@link #next()}; the gap sentinel is surfaced as
|
||||
* a distinct typed item rather than being swallowed, and normal events are
|
||||
* returned unchanged on {@link MxEventStreamItem#event()}. Do not mix
|
||||
* {@link #next()} and {@code nextItem()} on the same element — each call
|
||||
* advances the single shared cursor.
|
||||
*
|
||||
* @return the next stream item
|
||||
* @throws NoSuchElementException if the stream is exhausted
|
||||
*/
|
||||
public MxEventStreamItem nextItem() {
|
||||
return new MxEventStreamItem(next());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
closed = true;
|
||||
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
package com.zb.mom.ww.mxgateway.client;
|
||||
|
||||
import java.util.Objects;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.MxEvent;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.ReplayGap;
|
||||
|
||||
/**
|
||||
* Typed view over a single item drained from an {@link MxEventStream}.
|
||||
*
|
||||
* <p>A {@code StreamEvents} stream resumed via {@code after_worker_sequence}
|
||||
* may begin with a gateway reconnect-replay <em>gap sentinel</em>: a single
|
||||
* {@link MxEvent} whose {@code replay_gap} field is set, whose
|
||||
* {@code family} is {@code MX_EVENT_FAMILY_UNSPECIFIED}, and whose {@code body}
|
||||
* oneof is unset. It means the requested resume cursor predates the oldest
|
||||
* event the gateway still retains, so the events in the open interval
|
||||
* {@code (requested_after_sequence, oldest_available_sequence)} were evicted and
|
||||
* cannot be replayed.
|
||||
*
|
||||
* <p>This wrapper lets a consumer branch on that sentinel without inspecting
|
||||
* the raw event: {@link #isReplayGap()} is {@code true} only for the sentinel,
|
||||
* and {@link #replayGap()} exposes the typed {@link ReplayGap} cursors. Normal
|
||||
* MXAccess events return {@code false} from {@link #isReplayGap()} and carry
|
||||
* their payload on {@link #event()} exactly as before.
|
||||
*
|
||||
* <p>The gateway never synthesizes an {@code OperationComplete} or any other
|
||||
* event from the gap; the sentinel is the gateway's own forwarded signal and is
|
||||
* surfaced here untouched. On receiving a gap the consumer must discard any
|
||||
* cached per-item state and re-snapshot, then resume without incurring another
|
||||
* gap by requesting {@code oldest_available_sequence - 1} as the new
|
||||
* {@code after_worker_sequence} cursor.
|
||||
*
|
||||
* @param event the raw event; for a gap sentinel this is the sentinel event
|
||||
* itself (family unspecified, body unset, {@code replay_gap} set)
|
||||
*/
|
||||
public record MxEventStreamItem(MxEvent event) {
|
||||
/**
|
||||
* Creates a stream-item view over the supplied event.
|
||||
*
|
||||
* @param event the raw event; must not be {@code null}
|
||||
*/
|
||||
public MxEventStreamItem {
|
||||
Objects.requireNonNull(event, "event");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether this item is the reconnect-replay gap sentinel rather
|
||||
* than a normal MXAccess event. Detected via the generated
|
||||
* {@code MxEvent.hasReplayGap()} presence flag.
|
||||
*
|
||||
* @return {@code true} for the gap sentinel, {@code false} for normal events
|
||||
*/
|
||||
public boolean isReplayGap() {
|
||||
return event.hasReplayGap();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the typed reconnect-replay gap descriptor when this item is the
|
||||
* gap sentinel.
|
||||
*
|
||||
* @return the {@link ReplayGap} carrying {@code requested_after_sequence}
|
||||
* and {@code oldest_available_sequence}
|
||||
* @throws IllegalStateException if this item is a normal event (check
|
||||
* {@link #isReplayGap()} first)
|
||||
*/
|
||||
public ReplayGap replayGap() {
|
||||
if (!event.hasReplayGap()) {
|
||||
throw new IllegalStateException("stream item is not a replay-gap sentinel");
|
||||
}
|
||||
return event.getReplayGap();
|
||||
}
|
||||
}
|
||||
+289
@@ -7,11 +7,16 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.TreeMap;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.ActivateCommand;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.AddBufferedItemCommand;
|
||||
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.AdviseSupervisoryCommand;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.ArchestrAUserToIdCommand;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.AuthenticateUserCommand;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.BulkReadResult;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.BulkWriteResult;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.CloseSessionReply;
|
||||
@@ -23,15 +28,18 @@ import mxaccess_gateway.v1.MxaccessGateway.MxCommandRequest;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.MxDataType;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.MxSparseArray;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.MxSparseElement;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.MxStatusProxy;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.MxValue;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.OpenSessionReply;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.ReadBulkCommand;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.RegisterCommand;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.RemoveItemBulkCommand;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.RemoveItemCommand;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.SetBufferedUpdateIntervalCommand;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.StreamEventsRequest;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.SubscribeBulkCommand;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.SubscribeResult;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.SuspendCommand;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.UnAdviseCommand;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.UnAdviseItemBulkCommand;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.UnsubscribeBulkCommand;
|
||||
@@ -42,6 +50,8 @@ import mxaccess_gateway.v1.MxaccessGateway.Write2Command;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.WriteBulkCommand;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.WriteBulkEntry;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.WriteCommand;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.WriteSecured2Command;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.WriteSecuredCommand;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.WriteSecured2BulkCommand;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.WriteSecured2BulkEntry;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.WriteSecuredBulkCommand;
|
||||
@@ -697,6 +707,285 @@ public final class MxGatewaySession implements AutoCloseable {
|
||||
.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes MXAccess {@code AdviseSupervisory} so the item accepts
|
||||
* supervisory-attributed writes. Required before a plain {@link #write}
|
||||
* can stamp a Galaxy user id without the verified/secured path.
|
||||
*
|
||||
* @param serverHandle the {@code ServerHandle} owning the item
|
||||
* @param itemHandle the {@code ItemHandle} to advise supervisory
|
||||
* @throws MxGatewayException on transport or protocol failure
|
||||
* @throws MxAccessException when MXAccess reports a COM-side failure
|
||||
*/
|
||||
public void adviseSupervisory(int serverHandle, int itemHandle) {
|
||||
adviseSupervisoryRaw(serverHandle, itemHandle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes MXAccess {@code AdviseSupervisory} and returns the raw reply.
|
||||
*
|
||||
* @param serverHandle the {@code ServerHandle} owning the item
|
||||
* @param itemHandle the {@code ItemHandle} to advise supervisory
|
||||
* @return the raw command reply
|
||||
* @throws MxGatewayException on transport or protocol failure
|
||||
* @throws MxAccessException when MXAccess reports a COM-side failure
|
||||
*/
|
||||
public MxCommandReply adviseSupervisoryRaw(int serverHandle, int itemHandle) {
|
||||
return invokeCommand(MxCommand.newBuilder()
|
||||
.setKind(MxCommandKind.MX_COMMAND_KIND_ADVISE_SUPERVISORY)
|
||||
.setAdviseSupervisory(AdviseSupervisoryCommand.newBuilder()
|
||||
.setServerHandle(serverHandle)
|
||||
.setItemHandle(itemHandle))
|
||||
.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes MXAccess {@code WriteSecured} — a verified write gated by a
|
||||
* previously authenticated Galaxy user.
|
||||
*
|
||||
* <p><strong>MXAccess parity:</strong> the native call fails if the caller
|
||||
* has not first {@link #authenticateUser authenticated} and
|
||||
* {@link #adviseSupervisory advised supervisory}, or if the value body is
|
||||
* absent; that native failure is surfaced (as {@link MxAccessException}),
|
||||
* not papered over.
|
||||
*
|
||||
* <p><strong>Secret handling:</strong> {@code value} may carry a
|
||||
* credential-sensitive payload. It is placed only in the request and never
|
||||
* appears in any surfaced error (exceptions carry the reply, not the
|
||||
* request), so callers must likewise avoid logging it.
|
||||
*
|
||||
* @param serverHandle the {@code ServerHandle} owning the item
|
||||
* @param itemHandle the {@code ItemHandle} to write
|
||||
* @param currentUserId the authenticated (current) Galaxy user id
|
||||
* @param verifierUserId the verifier Galaxy user id (second-signature); use
|
||||
* the same value as {@code currentUserId} when no separate verifier applies
|
||||
* @param value the credential-sensitive value to write
|
||||
* @throws MxGatewayException on transport or protocol failure
|
||||
* @throws MxAccessException when MXAccess reports a COM-side failure
|
||||
*/
|
||||
public void writeSecured(
|
||||
int serverHandle, int itemHandle, int currentUserId, int verifierUserId, MxValue value) {
|
||||
writeSecuredRaw(serverHandle, itemHandle, currentUserId, verifierUserId, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes MXAccess {@code WriteSecured} and returns the raw reply.
|
||||
*
|
||||
* @param serverHandle the {@code ServerHandle} owning the item
|
||||
* @param itemHandle the {@code ItemHandle} to write
|
||||
* @param currentUserId the authenticated (current) Galaxy user id
|
||||
* @param verifierUserId the verifier Galaxy user id
|
||||
* @param value the credential-sensitive value to write
|
||||
* @return the raw command reply
|
||||
* @throws MxGatewayException on transport or protocol failure
|
||||
* @throws MxAccessException when MXAccess reports a COM-side failure
|
||||
*/
|
||||
public MxCommandReply writeSecuredRaw(
|
||||
int serverHandle, int itemHandle, int currentUserId, int verifierUserId, MxValue value) {
|
||||
return invokeCommand(MxCommand.newBuilder()
|
||||
.setKind(MxCommandKind.MX_COMMAND_KIND_WRITE_SECURED)
|
||||
.setWriteSecured(WriteSecuredCommand.newBuilder()
|
||||
.setServerHandle(serverHandle)
|
||||
.setItemHandle(itemHandle)
|
||||
.setCurrentUserId(currentUserId)
|
||||
.setVerifierUserId(verifierUserId)
|
||||
.setValue(value))
|
||||
.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes MXAccess {@code WriteSecured2} — a verified, explicitly
|
||||
* timestamped write. Parity and secret-handling notes mirror
|
||||
* {@link #writeSecured}.
|
||||
*
|
||||
* @param serverHandle the {@code ServerHandle} owning the item
|
||||
* @param itemHandle the {@code ItemHandle} to write
|
||||
* @param currentUserId the authenticated (current) Galaxy user id
|
||||
* @param verifierUserId the verifier Galaxy user id
|
||||
* @param value the credential-sensitive value to write
|
||||
* @param timestampValue the timestamp value to associate with the write
|
||||
* @throws MxGatewayException on transport or protocol failure
|
||||
* @throws MxAccessException when MXAccess reports a COM-side failure
|
||||
*/
|
||||
public void writeSecured2(
|
||||
int serverHandle,
|
||||
int itemHandle,
|
||||
int currentUserId,
|
||||
int verifierUserId,
|
||||
MxValue value,
|
||||
MxValue timestampValue) {
|
||||
writeSecured2Raw(serverHandle, itemHandle, currentUserId, verifierUserId, value, timestampValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes MXAccess {@code WriteSecured2} and returns the raw reply.
|
||||
*
|
||||
* @param serverHandle the {@code ServerHandle} owning the item
|
||||
* @param itemHandle the {@code ItemHandle} to write
|
||||
* @param currentUserId the authenticated (current) Galaxy user id
|
||||
* @param verifierUserId the verifier Galaxy user id
|
||||
* @param value the credential-sensitive value to write
|
||||
* @param timestampValue the timestamp value to associate with the write
|
||||
* @return the raw command reply
|
||||
* @throws MxGatewayException on transport or protocol failure
|
||||
* @throws MxAccessException when MXAccess reports a COM-side failure
|
||||
*/
|
||||
public MxCommandReply writeSecured2Raw(
|
||||
int serverHandle,
|
||||
int itemHandle,
|
||||
int currentUserId,
|
||||
int verifierUserId,
|
||||
MxValue value,
|
||||
MxValue timestampValue) {
|
||||
return invokeCommand(MxCommand.newBuilder()
|
||||
.setKind(MxCommandKind.MX_COMMAND_KIND_WRITE_SECURED2)
|
||||
.setWriteSecured2(WriteSecured2Command.newBuilder()
|
||||
.setServerHandle(serverHandle)
|
||||
.setItemHandle(itemHandle)
|
||||
.setCurrentUserId(currentUserId)
|
||||
.setVerifierUserId(verifierUserId)
|
||||
.setValue(value)
|
||||
.setTimestampValue(timestampValue))
|
||||
.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes MXAccess {@code AuthenticateUser} and returns the resolved
|
||||
* Galaxy user id used to attribute subsequent secured writes.
|
||||
*
|
||||
* <p><strong>Secret handling:</strong> {@code verifyUserPassword} is a raw
|
||||
* MXAccess credential. It is placed only in the request and never appears in
|
||||
* any surfaced error, log, or {@code toString()} (exceptions carry the
|
||||
* reply, not the request). Callers must not log it either.
|
||||
*
|
||||
* @param serverHandle the {@code ServerHandle} for the session
|
||||
* @param verifyUser the Galaxy user name to authenticate
|
||||
* @param verifyUserPassword the user's credential; never logged or surfaced
|
||||
* @return the authenticated Galaxy user id
|
||||
* @throws MxGatewayException on transport or protocol failure
|
||||
* @throws MxAccessException when MXAccess rejects the credential
|
||||
*/
|
||||
public int authenticateUser(int serverHandle, String verifyUser, String verifyUserPassword) {
|
||||
MxCommandReply reply = invokeCommand(MxCommand.newBuilder()
|
||||
.setKind(MxCommandKind.MX_COMMAND_KIND_AUTHENTICATE_USER)
|
||||
.setAuthenticateUser(AuthenticateUserCommand.newBuilder()
|
||||
.setServerHandle(serverHandle)
|
||||
.setVerifyUser(verifyUser)
|
||||
.setVerifyUserPassword(verifyUserPassword))
|
||||
.build());
|
||||
if (reply.hasAuthenticateUser()) {
|
||||
return reply.getAuthenticateUser().getUserId();
|
||||
}
|
||||
return reply.getReturnValue().getInt32Value();
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes MXAccess {@code ArchestrAUserToId}, resolving a Galaxy user GUID
|
||||
* to its integer user id.
|
||||
*
|
||||
* @param serverHandle the {@code ServerHandle} for the session
|
||||
* @param userIdGuid the Galaxy user GUID string
|
||||
* @return the resolved Galaxy user id
|
||||
* @throws MxGatewayException on transport or protocol failure
|
||||
* @throws MxAccessException when MXAccess reports a COM-side failure
|
||||
*/
|
||||
public int archestrAUserToId(int serverHandle, String userIdGuid) {
|
||||
MxCommandReply reply = invokeCommand(MxCommand.newBuilder()
|
||||
.setKind(MxCommandKind.MX_COMMAND_KIND_ARCHESTRA_USER_TO_ID)
|
||||
.setArchestraUserToId(ArchestrAUserToIdCommand.newBuilder()
|
||||
.setServerHandle(serverHandle)
|
||||
.setUserIdGuid(userIdGuid))
|
||||
.build());
|
||||
if (reply.hasArchestraUserToId()) {
|
||||
return reply.getArchestraUserToId().getUserId();
|
||||
}
|
||||
return reply.getReturnValue().getInt32Value();
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes MXAccess {@code AddBufferedItem} and returns the new item handle.
|
||||
* The buffered add family delivers coalesced {@code OnBufferedDataChange}
|
||||
* updates on the interval set by {@link #setBufferedUpdateInterval}.
|
||||
*
|
||||
* @param serverHandle the {@code ServerHandle} owning the item
|
||||
* @param itemDefinition the MXAccess item definition (tag reference)
|
||||
* @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
|
||||
* @throws MxAccessException when MXAccess reports a COM-side failure
|
||||
*/
|
||||
public int addBufferedItem(int serverHandle, String itemDefinition, String itemContext) {
|
||||
MxCommandReply reply = invokeCommand(MxCommand.newBuilder()
|
||||
.setKind(MxCommandKind.MX_COMMAND_KIND_ADD_BUFFERED_ITEM)
|
||||
.setAddBufferedItem(AddBufferedItemCommand.newBuilder()
|
||||
.setServerHandle(serverHandle)
|
||||
.setItemDefinition(itemDefinition)
|
||||
.setItemContext(itemContext))
|
||||
.build());
|
||||
if (reply.hasAddBufferedItem()) {
|
||||
return reply.getAddBufferedItem().getItemHandle();
|
||||
}
|
||||
return reply.getReturnValue().getInt32Value();
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes MXAccess {@code SetBufferedUpdateInterval}, controlling how often
|
||||
* the worker coalesces buffered updates for the given server handle.
|
||||
*
|
||||
* @param serverHandle the {@code ServerHandle} to configure
|
||||
* @param updateIntervalMilliseconds the buffered update interval in milliseconds
|
||||
* @throws MxGatewayException on transport or protocol failure
|
||||
* @throws MxAccessException when MXAccess reports a COM-side failure
|
||||
*/
|
||||
public void setBufferedUpdateInterval(int serverHandle, int updateIntervalMilliseconds) {
|
||||
invokeCommand(MxCommand.newBuilder()
|
||||
.setKind(MxCommandKind.MX_COMMAND_KIND_SET_BUFFERED_UPDATE_INTERVAL)
|
||||
.setSetBufferedUpdateInterval(SetBufferedUpdateIntervalCommand.newBuilder()
|
||||
.setServerHandle(serverHandle)
|
||||
.setUpdateIntervalMilliseconds(updateIntervalMilliseconds))
|
||||
.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes MXAccess {@code Suspend} on an item and returns the reply's
|
||||
* {@link MxStatusProxy}.
|
||||
*
|
||||
* @param serverHandle the {@code ServerHandle} owning the item
|
||||
* @param itemHandle the {@code ItemHandle} to suspend
|
||||
* @return the {@code MxStatusProxy} carried by the suspend reply
|
||||
* @throws MxGatewayException on transport or protocol failure
|
||||
* @throws MxAccessException when MXAccess reports a COM-side failure
|
||||
*/
|
||||
public MxStatusProxy suspend(int serverHandle, int itemHandle) {
|
||||
MxCommandReply reply = invokeCommand(MxCommand.newBuilder()
|
||||
.setKind(MxCommandKind.MX_COMMAND_KIND_SUSPEND)
|
||||
.setSuspend(SuspendCommand.newBuilder()
|
||||
.setServerHandle(serverHandle)
|
||||
.setItemHandle(itemHandle))
|
||||
.build());
|
||||
return reply.getSuspend().getStatus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes MXAccess {@code Activate} on a previously suspended item and
|
||||
* returns the reply's {@link MxStatusProxy}.
|
||||
*
|
||||
* @param serverHandle the {@code ServerHandle} owning the item
|
||||
* @param itemHandle the {@code ItemHandle} to activate
|
||||
* @return the {@code MxStatusProxy} carried by the activate reply
|
||||
* @throws MxGatewayException on transport or protocol failure
|
||||
* @throws MxAccessException when MXAccess reports a COM-side failure
|
||||
*/
|
||||
public MxStatusProxy activate(int serverHandle, int itemHandle) {
|
||||
MxCommandReply reply = invokeCommand(MxCommand.newBuilder()
|
||||
.setKind(MxCommandKind.MX_COMMAND_KIND_ACTIVATE)
|
||||
.setActivate(ActivateCommand.newBuilder()
|
||||
.setServerHandle(serverHandle)
|
||||
.setItemHandle(itemHandle))
|
||||
.build());
|
||||
return reply.getActivate().getStatus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to gateway events for this session starting from the
|
||||
* beginning of the worker event log.
|
||||
|
||||
+230
@@ -1,6 +1,7 @@
|
||||
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.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
@@ -30,6 +31,7 @@ import mxaccess_gateway.v1.MxaccessGateway.AddItemReply;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.AlarmConditionState;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.AlarmFeedMessage;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.AlarmTransitionKind;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.AuthenticateUserReply;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.BulkSubscribeReply;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.OnAlarmTransitionEvent;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.CloseSessionReply;
|
||||
@@ -39,6 +41,7 @@ import mxaccess_gateway.v1.MxaccessGateway.MxCommandReply;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.MxCommandRequest;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.MxDataType;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.MxEvent;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.MxEventFamily;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.MxSparseElement;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.MxValue;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.OpenSessionReply;
|
||||
@@ -47,6 +50,7 @@ import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatus;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatusCode;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.QueryActiveAlarmsRequest;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.RegisterReply;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.ReplayGap;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.SessionState;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.StreamAlarmsRequest;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.StreamEventsRequest;
|
||||
@@ -509,6 +513,232 @@ final class MxGatewayClientSessionTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void streamEventsSurfacesReplayGapSentinelAsTypedItem() throws Exception {
|
||||
// CLI-15: a resumed stream whose cursor predates the retained window
|
||||
// begins with the gateway's replay-gap sentinel. It must surface as a
|
||||
// distinct typed item, and normal events must be unaffected.
|
||||
TestGatewayService service = new TestGatewayService() {
|
||||
@Override
|
||||
public void streamEvents(StreamEventsRequest request, StreamObserver<MxEvent> responseObserver) {
|
||||
responseObserver.onNext(MxEvent.newBuilder()
|
||||
.setSessionId(request.getSessionId())
|
||||
.setReplayGap(ReplayGap.newBuilder()
|
||||
.setRequestedAfterSequence(5)
|
||||
.setOldestAvailableSequence(9))
|
||||
.build());
|
||||
responseObserver.onNext(MxEvent.newBuilder()
|
||||
.setSessionId(request.getSessionId())
|
||||
.setFamily(MxEventFamily.MX_EVENT_FAMILY_ON_DATA_CHANGE)
|
||||
.setWorkerSequence(9)
|
||||
.build());
|
||||
responseObserver.onCompleted();
|
||||
}
|
||||
};
|
||||
|
||||
try (InProcessGateway gateway = InProcessGateway.start(service, new AtomicReference<>());
|
||||
MxGatewayClient client = gateway.client("", Duration.ofSeconds(5));
|
||||
MxEventStream events =
|
||||
MxGatewaySession.forSessionId(client, "resume-session").streamEventsAfter(5)) {
|
||||
assertTrue(events.hasNext());
|
||||
MxEventStreamItem gap = events.nextItem();
|
||||
assertTrue(gap.isReplayGap(), "head sentinel must classify as a replay gap");
|
||||
assertEquals(5, gap.replayGap().getRequestedAfterSequence());
|
||||
assertEquals(9, gap.replayGap().getOldestAvailableSequence());
|
||||
// Sentinel carries no MXAccess payload.
|
||||
assertEquals(MxEventFamily.MX_EVENT_FAMILY_UNSPECIFIED, gap.event().getFamily());
|
||||
|
||||
assertTrue(events.hasNext());
|
||||
MxEventStreamItem normal = events.nextItem();
|
||||
assertFalse(normal.isReplayGap(), "normal event must not classify as a replay gap");
|
||||
assertEquals(9, normal.event().getWorkerSequence());
|
||||
assertEquals(MxEventFamily.MX_EVENT_FAMILY_ON_DATA_CHANGE, normal.event().getFamily());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void adviseSupervisoryBuildsSupervisoryCommand() throws Exception {
|
||||
AtomicReference<MxCommandRequest> commandRequest = new AtomicReference<>();
|
||||
TestGatewayService service = okInvokeService(commandRequest);
|
||||
|
||||
try (InProcessGateway gateway = InProcessGateway.start(service, new AtomicReference<>());
|
||||
MxGatewayClient client = gateway.client("", Duration.ofSeconds(5))) {
|
||||
MxGatewaySession session = MxGatewaySession.forSessionId(client, "advise-super-session");
|
||||
|
||||
session.adviseSupervisory(12, 34);
|
||||
|
||||
assertEquals(
|
||||
MxCommandKind.MX_COMMAND_KIND_ADVISE_SUPERVISORY,
|
||||
commandRequest.get().getCommand().getKind());
|
||||
assertEquals(12, commandRequest.get().getCommand().getAdviseSupervisory().getServerHandle());
|
||||
assertEquals(34, commandRequest.get().getCommand().getAdviseSupervisory().getItemHandle());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeSecuredSurfacesNativeMxAccessFailure() throws Exception {
|
||||
// CLI-04 parity: WriteSecured before authenticate/advise fails natively;
|
||||
// the client surfaces the failure rather than papering over it.
|
||||
TestGatewayService service = new TestGatewayService() {
|
||||
@Override
|
||||
public void invoke(MxCommandRequest request, StreamObserver<MxCommandReply> responseObserver) {
|
||||
responseObserver.onNext(MxCommandReply.newBuilder()
|
||||
.setSessionId(request.getSessionId())
|
||||
.setKind(request.getCommand().getKind())
|
||||
.setProtocolStatus(ProtocolStatus.newBuilder()
|
||||
.setCode(ProtocolStatusCode.PROTOCOL_STATUS_CODE_MXACCESS_FAILURE)
|
||||
.setMessage("WriteSecured rejected: user not authenticated."))
|
||||
.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, "secured-session");
|
||||
|
||||
MxAccessException error = assertThrows(
|
||||
MxAccessException.class,
|
||||
() -> session.writeSecured(1, 2, 100, 100, MxValues.stringValue("secret-payload")));
|
||||
|
||||
assertEquals(-2147220992, error.reply().getHresult());
|
||||
// The credential-bearing value lives only in the request, so it must
|
||||
// not appear in any surfaced error text.
|
||||
assertFalse(String.valueOf(error.getMessage()).contains("secret-payload"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeSecuredScriptedSuccessSendsSecuredCommand() throws Exception {
|
||||
AtomicReference<MxCommandRequest> commandRequest = new AtomicReference<>();
|
||||
TestGatewayService service = okInvokeService(commandRequest);
|
||||
|
||||
try (InProcessGateway gateway = InProcessGateway.start(service, new AtomicReference<>());
|
||||
MxGatewayClient client = gateway.client("", Duration.ofSeconds(5))) {
|
||||
MxGatewaySession session = MxGatewaySession.forSessionId(client, "secured-ok-session");
|
||||
|
||||
session.writeSecured(7, 8, 100, 200, MxValues.int32Value(42));
|
||||
|
||||
var command = commandRequest.get().getCommand();
|
||||
assertEquals(MxCommandKind.MX_COMMAND_KIND_WRITE_SECURED, command.getKind());
|
||||
assertEquals(7, command.getWriteSecured().getServerHandle());
|
||||
assertEquals(8, command.getWriteSecured().getItemHandle());
|
||||
assertEquals(100, command.getWriteSecured().getCurrentUserId());
|
||||
assertEquals(200, command.getWriteSecured().getVerifierUserId());
|
||||
assertEquals(42, command.getWriteSecured().getValue().getInt32Value());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void authenticateUserReturnsUserIdAndForwardsCredential() 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())
|
||||
.setAuthenticateUser(AuthenticateUserReply.newBuilder().setUserId(4242))
|
||||
.build());
|
||||
responseObserver.onCompleted();
|
||||
}
|
||||
};
|
||||
|
||||
try (InProcessGateway gateway = InProcessGateway.start(service, new AtomicReference<>());
|
||||
MxGatewayClient client = gateway.client("", Duration.ofSeconds(5))) {
|
||||
MxGatewaySession session = MxGatewaySession.forSessionId(client, "auth-session");
|
||||
|
||||
int userId = session.authenticateUser(3, "operator", "super-secret-pw");
|
||||
|
||||
assertEquals(4242, userId);
|
||||
// The credential is forwarded in the request only.
|
||||
assertEquals(
|
||||
"super-secret-pw",
|
||||
commandRequest.get().getCommand().getAuthenticateUser().getVerifyUserPassword());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void authenticateUserFailureKeepsCredentialOutOfSurfacedError() throws Exception {
|
||||
TestGatewayService service = new TestGatewayService() {
|
||||
@Override
|
||||
public void invoke(MxCommandRequest request, StreamObserver<MxCommandReply> responseObserver) {
|
||||
responseObserver.onNext(MxCommandReply.newBuilder()
|
||||
.setSessionId(request.getSessionId())
|
||||
.setKind(request.getCommand().getKind())
|
||||
.setProtocolStatus(ProtocolStatus.newBuilder()
|
||||
.setCode(ProtocolStatusCode.PROTOCOL_STATUS_CODE_MXACCESS_FAILURE)
|
||||
.setMessage("AuthenticateUser rejected the credential."))
|
||||
.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, "auth-fail-session");
|
||||
|
||||
MxAccessException error = assertThrows(
|
||||
MxAccessException.class,
|
||||
() -> session.authenticateUser(3, "operator", "super-secret-pw"));
|
||||
|
||||
// The password must never reach the exception message or toString().
|
||||
assertFalse(String.valueOf(error.getMessage()).contains("super-secret-pw"));
|
||||
assertFalse(error.toString().contains("super-secret-pw"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void suspendAndActivateReturnStatusProxy() 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_SUSPEND) {
|
||||
reply.setSuspend(mxaccess_gateway.v1.MxaccessGateway.SuspendReply.newBuilder()
|
||||
.setStatus(mxaccess_gateway.v1.MxaccessGateway.MxStatusProxy.newBuilder()
|
||||
.setSuccess(1)));
|
||||
} else if (request.getCommand().getKind() == MxCommandKind.MX_COMMAND_KIND_ACTIVATE) {
|
||||
reply.setActivate(mxaccess_gateway.v1.MxaccessGateway.ActivateReply.newBuilder()
|
||||
.setStatus(mxaccess_gateway.v1.MxaccessGateway.MxStatusProxy.newBuilder()
|
||||
.setSuccess(1)));
|
||||
}
|
||||
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, "suspend-session");
|
||||
|
||||
assertTrue(MxStatuses.succeeded(session.suspend(1, 2)));
|
||||
assertTrue(MxStatuses.succeeded(session.activate(1, 2)));
|
||||
}
|
||||
}
|
||||
|
||||
private static TestGatewayService okInvokeService(AtomicReference<MxCommandRequest> commandRequest) {
|
||||
return 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())
|
||||
.build());
|
||||
responseObserver.onCompleted();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static ProtocolStatus ok() {
|
||||
return ProtocolStatus.newBuilder()
|
||||
.setCode(ProtocolStatusCode.PROTOCOL_STATUS_CODE_OK)
|
||||
|
||||
Reference in New Issue
Block a user