feat(java): add ping + galaxy-browse CLI subcommands and galaxy command aliases
- D4: add 'ping' subcommand (MX_COMMAND_KIND_PING / PingCommand{message}),
accepting --session-id and optional --message (default "ping"); prints the
worker's echoed diagnostic message.
- D8-java: add 'galaxy-browse' subcommand over browse()/LazyBrowseNode.expand()
and raw BrowseChildren paging for --parent. JSON node shape matches the
cross-client surface (flattened object fields + hasChildrenHint + nested
children array).
- D9-java: make galaxy-test-connection / galaxy-last-deploy the primary names,
keeping galaxy-test / galaxy-deploy-time as deprecated picocli aliases.
- Tests for ping, galaxy-browse JSON hasChildrenHint key, and alias resolution.
- README updated for the new/renamed subcommands.
This commit is contained in:
+23
-6
@@ -115,17 +115,33 @@ try (GalaxyRepositoryClient galaxy = GalaxyRepositoryClient.connect(options)) {
|
||||
messages directly so callers can read all fields (including the nested
|
||||
`GalaxyAttribute` list) without an extra DTO layer.
|
||||
|
||||
The CLI exposes matching subcommands: `galaxy-test`, `galaxy-deploy-time`,
|
||||
`galaxy-discover`, and `galaxy-watch`. They take the same `--endpoint`,
|
||||
`--api-key-env`, `--plaintext`, `--ca-file`, `--server-name-override`,
|
||||
`--timeout`, and `--json` options as the gateway commands.
|
||||
The CLI exposes matching subcommands: `galaxy-test-connection`,
|
||||
`galaxy-last-deploy`, `galaxy-discover`, `galaxy-browse`, and `galaxy-watch`.
|
||||
The short names `galaxy-test` and `galaxy-deploy-time` remain as deprecated
|
||||
aliases for `galaxy-test-connection` and `galaxy-last-deploy` so existing
|
||||
scripts keep working. They take the same `--endpoint`, `--api-key-env`,
|
||||
`--plaintext`, `--ca-file`, `--server-name-override`, `--timeout`, and `--json`
|
||||
options as the gateway commands.
|
||||
|
||||
```powershell
|
||||
gradle :zb-mom-ww-mxgateway-cli:run --args="galaxy-test --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --json"
|
||||
gradle :zb-mom-ww-mxgateway-cli:run --args="galaxy-deploy-time --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --json"
|
||||
gradle :zb-mom-ww-mxgateway-cli:run --args="galaxy-test-connection --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --json"
|
||||
gradle :zb-mom-ww-mxgateway-cli:run --args="galaxy-last-deploy --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --json"
|
||||
gradle :zb-mom-ww-mxgateway-cli:run --args="galaxy-discover --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --json"
|
||||
```
|
||||
|
||||
`galaxy-browse` walks the hierarchy via `BrowseChildren`. Without `--parent` it
|
||||
returns the root nodes and eagerly expands `--depth` further levels; with
|
||||
`--parent <gobject-id>` it returns exactly one level of children for that
|
||||
parent. The filter flags (`--category-ids`, `--template-contains`,
|
||||
`--tag-name-glob`, `--alarm-bearing-only`, `--historized-only`,
|
||||
`--include-attributes`) match `galaxy-discover`. The `--json` node shape is the
|
||||
cross-client browse surface: the flattened object fields plus a
|
||||
`hasChildrenHint` flag and a nested `children` array.
|
||||
|
||||
```powershell
|
||||
gradle :zb-mom-ww-mxgateway-cli:run --args="galaxy-browse --depth 1 --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --json"
|
||||
```
|
||||
|
||||
### Browsing lazily
|
||||
|
||||
For UI trees or OPC UA bridges, use `browseChildrenRaw` to walk one level at a
|
||||
@@ -239,6 +255,7 @@ Run the CLI through Gradle:
|
||||
```powershell
|
||||
gradle :zb-mom-ww-mxgateway-cli:run --args="version --json"
|
||||
gradle :zb-mom-ww-mxgateway-cli:run --args="open-session --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --client-session-name java-cli --json"
|
||||
gradle :zb-mom-ww-mxgateway-cli:run --args="ping --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --session-id <id> --message hello --json"
|
||||
gradle :zb-mom-ww-mxgateway-cli:run --args="register --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --session-id <id> --client-name java-cli --json"
|
||||
gradle :zb-mom-ww-mxgateway-cli:run --args="add-item --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --session-id <id> --server-handle 1 --item TestObject.TestInt --json"
|
||||
gradle :zb-mom-ww-mxgateway-cli:run --args="advise --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --session-id <id> --server-handle 1 --item-handle 1 --json"
|
||||
|
||||
+312
-6
@@ -1,7 +1,9 @@
|
||||
package com.zb.mom.ww.mxgateway.cli;
|
||||
|
||||
import com.zb.mom.ww.mxgateway.client.BrowseChildrenOptions;
|
||||
import com.zb.mom.ww.mxgateway.client.DeployEventStream;
|
||||
import com.zb.mom.ww.mxgateway.client.GalaxyRepositoryClient;
|
||||
import com.zb.mom.ww.mxgateway.client.LazyBrowseNode;
|
||||
import com.zb.mom.ww.mxgateway.client.MxEventStream;
|
||||
import com.zb.mom.ww.mxgateway.client.MxGatewayAlarmFeedSubscription;
|
||||
import com.zb.mom.ww.mxgateway.client.MxGatewayClient;
|
||||
@@ -10,6 +12,8 @@ import com.zb.mom.ww.mxgateway.client.MxGatewayClientVersion;
|
||||
import com.zb.mom.ww.mxgateway.client.MxGatewaySecrets;
|
||||
import com.zb.mom.ww.mxgateway.client.MxGatewaySession;
|
||||
import com.zb.mom.ww.mxgateway.client.MxValues;
|
||||
import galaxy_repository.v1.GalaxyRepositoryOuterClass.BrowseChildrenReply;
|
||||
import galaxy_repository.v1.GalaxyRepositoryOuterClass.BrowseChildrenRequest;
|
||||
import galaxy_repository.v1.GalaxyRepositoryOuterClass.DeployEvent;
|
||||
import galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute;
|
||||
import galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject;
|
||||
@@ -26,8 +30,10 @@ import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
@@ -42,11 +48,14 @@ import mxaccess_gateway.v1.MxaccessGateway.AlarmFeedMessage;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.BulkReadResult;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.BulkWriteResult;
|
||||
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.MxEvent;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.MxValue;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.OnAlarmTransitionEvent;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.OpenSessionRequest;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.PingCommand;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.StreamAlarmsRequest;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.SubscribeResult;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.Write2BulkEntry;
|
||||
@@ -126,6 +135,7 @@ public final class MxGatewayCli implements Callable<Integer> {
|
||||
commandLine.addSubcommand("version", new VersionCommand());
|
||||
commandLine.addSubcommand("open-session", new OpenSessionCommand(clientFactory));
|
||||
commandLine.addSubcommand("close-session", new CloseSessionCommand(clientFactory));
|
||||
commandLine.addSubcommand("ping", new PingCommandLine(clientFactory));
|
||||
commandLine.addSubcommand("register", new RegisterCommand(clientFactory));
|
||||
commandLine.addSubcommand("add-item", new AddItemCommand(clientFactory));
|
||||
commandLine.addSubcommand("advise", new AdviseCommand(clientFactory));
|
||||
@@ -142,9 +152,10 @@ public final class MxGatewayCli implements Callable<Integer> {
|
||||
commandLine.addSubcommand("stream-alarms", new StreamAlarmsCommand(clientFactory));
|
||||
commandLine.addSubcommand("acknowledge-alarm", new AcknowledgeAlarmCommand(clientFactory));
|
||||
commandLine.addSubcommand("smoke", new SmokeCommand(clientFactory));
|
||||
commandLine.addSubcommand("galaxy-test", new GalaxyTestConnectionCommand());
|
||||
commandLine.addSubcommand("galaxy-deploy-time", new GalaxyDeployTimeCommand());
|
||||
commandLine.addSubcommand("galaxy-test-connection", new GalaxyTestConnectionCommand());
|
||||
commandLine.addSubcommand("galaxy-last-deploy", new GalaxyDeployTimeCommand());
|
||||
commandLine.addSubcommand("galaxy-discover", new GalaxyDiscoverCommand());
|
||||
commandLine.addSubcommand("galaxy-browse", new GalaxyBrowseCommand());
|
||||
commandLine.addSubcommand("galaxy-watch", new GalaxyWatchCommand());
|
||||
commandLine.addSubcommand("batch", new BatchCommand(clientFactory));
|
||||
return commandLine;
|
||||
@@ -359,7 +370,10 @@ public final class MxGatewayCli implements Callable<Integer> {
|
||||
}
|
||||
}
|
||||
|
||||
@Command(name = "galaxy-test", description = "Calls GalaxyRepository.TestConnection.")
|
||||
@Command(
|
||||
name = "galaxy-test-connection",
|
||||
aliases = {"galaxy-test"},
|
||||
description = "Calls GalaxyRepository.TestConnection.")
|
||||
static final class GalaxyTestConnectionCommand extends GalaxyCommand {
|
||||
@Override
|
||||
public Integer call() {
|
||||
@@ -368,7 +382,7 @@ public final class MxGatewayCli implements Callable<Integer> {
|
||||
PrintWriter out = common.spec.commandLine().getOut();
|
||||
if (json) {
|
||||
Map<String, Object> output = new LinkedHashMap<>();
|
||||
output.put("command", "galaxy-test");
|
||||
output.put("command", "galaxy-test-connection");
|
||||
output.put("options", common.redactedJsonMap());
|
||||
output.put("ok", ok);
|
||||
out.println(jsonObject(output));
|
||||
@@ -380,7 +394,10 @@ public final class MxGatewayCli implements Callable<Integer> {
|
||||
}
|
||||
}
|
||||
|
||||
@Command(name = "galaxy-deploy-time", description = "Calls GalaxyRepository.GetLastDeployTime.")
|
||||
@Command(
|
||||
name = "galaxy-last-deploy",
|
||||
aliases = {"galaxy-deploy-time"},
|
||||
description = "Calls GalaxyRepository.GetLastDeployTime.")
|
||||
static final class GalaxyDeployTimeCommand extends GalaxyCommand {
|
||||
@Override
|
||||
public Integer call() {
|
||||
@@ -389,7 +406,7 @@ public final class MxGatewayCli implements Callable<Integer> {
|
||||
PrintWriter out = common.spec.commandLine().getOut();
|
||||
if (json) {
|
||||
Map<String, Object> output = new LinkedHashMap<>();
|
||||
output.put("command", "galaxy-deploy-time");
|
||||
output.put("command", "galaxy-last-deploy");
|
||||
output.put("options", common.redactedJsonMap());
|
||||
output.put("present", result.isPresent());
|
||||
output.put("timeOfLastDeploy", result.map(Instant::toString).orElse(""));
|
||||
@@ -429,6 +446,260 @@ public final class MxGatewayCli implements Callable<Integer> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Page size used for the raw {@code BrowseChildren} paging loop driven by
|
||||
* the {@code --parent} one-level path. Mirrors {@code BROWSE_CHILDREN_PAGE_SIZE}
|
||||
* in the client library's lazy-browse helper and the other clients' CLI page
|
||||
* size so paging behaviour is consistent across languages.
|
||||
*/
|
||||
private static final int BROWSE_CHILDREN_CLI_PAGE_SIZE = 500;
|
||||
|
||||
@Command(
|
||||
name = "galaxy-browse",
|
||||
description = "Browses the Galaxy hierarchy via GalaxyRepository.BrowseChildren.")
|
||||
static final class GalaxyBrowseCommand extends GalaxyCommand {
|
||||
@Option(
|
||||
names = "--parent",
|
||||
defaultValue = "-1",
|
||||
description = "Parent gobject id to browse one level of children for; omit to walk the roots.")
|
||||
int parent;
|
||||
|
||||
@Option(
|
||||
names = "--depth",
|
||||
defaultValue = "0",
|
||||
description = "When walking roots, eagerly expand this many further levels before printing.")
|
||||
int depth;
|
||||
|
||||
@Option(names = "--category-ids", description = "Comma-separated category ids to include.")
|
||||
String categoryIds;
|
||||
|
||||
@Option(names = "--template-contains", description = "Comma-separated template names each child's chain must contain.")
|
||||
String templateContains;
|
||||
|
||||
@Option(names = "--tag-name-glob", description = "SQL-LIKE-style glob applied to tag_name.")
|
||||
String tagNameGlob;
|
||||
|
||||
@Option(names = "--alarm-bearing-only", description = "Restrict to alarm-bearing objects.")
|
||||
boolean alarmBearingOnly;
|
||||
|
||||
@Option(names = "--historized-only", description = "Restrict to objects with at least one historized attribute.")
|
||||
boolean historizedOnly;
|
||||
|
||||
@Option(names = "--include-attributes", description = "Request attribute population on each returned object.")
|
||||
boolean includeAttributes;
|
||||
|
||||
@Override
|
||||
public Integer call() {
|
||||
if (depth < 0) {
|
||||
throw new IllegalArgumentException("--depth must be non-negative");
|
||||
}
|
||||
BrowseChildrenOptions options = buildOptions();
|
||||
PrintWriter out = common.spec.commandLine().getOut();
|
||||
PrintWriter err = common.spec.commandLine().getErr();
|
||||
try (GalaxyRepositoryClient client = connect()) {
|
||||
if (parent >= 0) {
|
||||
if (depth > 0) {
|
||||
err.println("warning: --depth is ignored when --parent is specified.");
|
||||
}
|
||||
List<BrowseChild> children = browseOneLevel(client, parent, options);
|
||||
if (json) {
|
||||
List<Map<String, Object>> nodes = new ArrayList<>(children.size());
|
||||
for (BrowseChild child : children) {
|
||||
nodes.add(browseNodeMap(child.object(), child.hasChildrenHint(), List.of()));
|
||||
}
|
||||
Map<String, Object> output = new LinkedHashMap<>();
|
||||
output.put("command", "galaxy-browse");
|
||||
output.put("options", common.redactedJsonMap());
|
||||
output.put("parentId", parent);
|
||||
output.put("nodes", nodes);
|
||||
out.println(jsonObject(output));
|
||||
} else {
|
||||
out.println(children.size());
|
||||
for (BrowseChild child : children) {
|
||||
printBrowseChild(out, child);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
List<LazyBrowseNode> roots = client.browse(options);
|
||||
for (LazyBrowseNode root : roots) {
|
||||
expandToDepth(root, depth);
|
||||
}
|
||||
if (json) {
|
||||
List<Map<String, Object>> nodes = new ArrayList<>(roots.size());
|
||||
for (LazyBrowseNode root : roots) {
|
||||
nodes.add(lazyNodeMap(root));
|
||||
}
|
||||
Map<String, Object> output = new LinkedHashMap<>();
|
||||
output.put("command", "galaxy-browse");
|
||||
output.put("options", common.redactedJsonMap());
|
||||
output.put("nodes", nodes);
|
||||
out.println(jsonObject(output));
|
||||
} else {
|
||||
out.println(roots.size());
|
||||
for (LazyBrowseNode root : roots) {
|
||||
printLazyNode(out, root, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private BrowseChildrenOptions buildOptions() {
|
||||
return BrowseChildrenOptions.builder()
|
||||
.categoryIds(parseOptionalIntList(categoryIds))
|
||||
.templateChainContains(parseOptionalStringList(templateContains))
|
||||
.tagNameGlob(tagNameGlob == null ? "" : tagNameGlob)
|
||||
// Tri-state: only override the server default when the flag is present.
|
||||
.includeAttributes(includeAttributes ? Boolean.TRUE : null)
|
||||
.alarmBearingOnly(alarmBearingOnly)
|
||||
.historizedOnly(historizedOnly)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/** One raw {@code BrowseChildren} child paired with its server-supplied has-children hint. */
|
||||
private record BrowseChild(GalaxyObject object, boolean hasChildrenHint) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Drives the raw {@code BrowseChildren} paging loop for a single parent and
|
||||
* returns the flattened one-level child list. Used by the {@code --parent}
|
||||
* path, which surfaces a single level rather than the lazy root-tree walk.
|
||||
*/
|
||||
private static List<BrowseChild> browseOneLevel(
|
||||
GalaxyRepositoryClient client, int parentGobjectId, BrowseChildrenOptions options) {
|
||||
List<BrowseChild> children = new ArrayList<>();
|
||||
Set<String> seenPageTokens = new HashSet<>();
|
||||
String pageToken = "";
|
||||
while (true) {
|
||||
BrowseChildrenRequest.Builder builder = BrowseChildrenRequest.newBuilder()
|
||||
.setPageSize(BROWSE_CHILDREN_CLI_PAGE_SIZE)
|
||||
.setPageToken(pageToken)
|
||||
.setParentGobjectId(parentGobjectId)
|
||||
.setAlarmBearingOnly(options.isAlarmBearingOnly())
|
||||
.setHistorizedOnly(options.isHistorizedOnly());
|
||||
if (!options.getCategoryIds().isEmpty()) {
|
||||
builder.addAllCategoryIds(options.getCategoryIds());
|
||||
}
|
||||
if (!options.getTemplateChainContains().isEmpty()) {
|
||||
builder.addAllTemplateChainContains(options.getTemplateChainContains());
|
||||
}
|
||||
if (!options.getTagNameGlob().isEmpty()) {
|
||||
builder.setTagNameGlob(options.getTagNameGlob());
|
||||
}
|
||||
if (options.getIncludeAttributes() != null) {
|
||||
builder.setIncludeAttributes(options.getIncludeAttributes());
|
||||
}
|
||||
|
||||
BrowseChildrenReply reply = client.browseChildrenRaw(builder.build());
|
||||
for (int i = 0; i < reply.getChildrenCount(); i++) {
|
||||
boolean hint = i < reply.getChildHasChildrenCount() && reply.getChildHasChildren(i);
|
||||
children.add(new BrowseChild(reply.getChildren(i), hint));
|
||||
}
|
||||
|
||||
pageToken = reply.getNextPageToken();
|
||||
if (pageToken == null || pageToken.isEmpty()) {
|
||||
return children;
|
||||
}
|
||||
if (!seenPageTokens.add(pageToken)) {
|
||||
throw new IllegalStateException(
|
||||
"galaxy browse children returned repeated page token: " + pageToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively expands a {@link LazyBrowseNode} up to {@code depth} further
|
||||
* levels. A {@code depth} of 0 leaves the node unexpanded so callers print
|
||||
* only the requested level. Nodes the server reports as childless are not
|
||||
* expanded.
|
||||
*/
|
||||
private static void expandToDepth(LazyBrowseNode node, int depth) {
|
||||
if (depth <= 0) {
|
||||
return;
|
||||
}
|
||||
if (node.hasChildrenHint()) {
|
||||
node.expand();
|
||||
}
|
||||
for (LazyBrowseNode child : node.getChildren()) {
|
||||
expandToDepth(child, depth - 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders one {@link LazyBrowseNode} (and any already-expanded descendants)
|
||||
* as a JSON map. Mirrors the {@code galaxy-discover} object shape with an
|
||||
* added {@code hasChildrenHint} flag and a nested {@code children} array,
|
||||
* matching the cross-client browse JSON surface.
|
||||
*/
|
||||
private static Map<String, Object> lazyNodeMap(LazyBrowseNode node) {
|
||||
List<Map<String, Object>> children = new ArrayList<>();
|
||||
if (node.isExpanded()) {
|
||||
for (LazyBrowseNode child : node.getChildren()) {
|
||||
children.add(lazyNodeMap(child));
|
||||
}
|
||||
}
|
||||
return browseNodeMap(node.getObject(), node.hasChildrenHint(), children);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the per-node browse JSON map: the flattened Galaxy object fields,
|
||||
* the {@code hasChildrenHint} flag, and a nested {@code children} array.
|
||||
* The {@code hasChildrenHint} key is the cross-client standard (Rust /
|
||||
* Python / .NET / Go all use the same key and node shape).
|
||||
*/
|
||||
static Map<String, Object> browseNodeMap(
|
||||
GalaxyObject object, boolean hasChildrenHint, List<Map<String, Object>> children) {
|
||||
Map<String, Object> values = galaxyObjectMap(object);
|
||||
values.put("hasChildrenHint", hasChildrenHint);
|
||||
values.put("children", children);
|
||||
return values;
|
||||
}
|
||||
|
||||
private static void printLazyNode(PrintWriter out, LazyBrowseNode node, int level) {
|
||||
GalaxyObject obj = node.getObject();
|
||||
out.printf(
|
||||
"%s%d\t%s\t%s\t(attrs=%d, hasChildrenHint=%b)%n",
|
||||
" ".repeat(level),
|
||||
obj.getGobjectId(),
|
||||
obj.getTagName(),
|
||||
obj.getBrowseName(),
|
||||
obj.getAttributesCount(),
|
||||
node.hasChildrenHint());
|
||||
if (node.isExpanded()) {
|
||||
for (LazyBrowseNode child : node.getChildren()) {
|
||||
printLazyNode(out, child, level + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void printBrowseChild(PrintWriter out, BrowseChild child) {
|
||||
GalaxyObject obj = child.object();
|
||||
out.printf(
|
||||
"%d\t%s\t%s\t(attrs=%d, hasChildrenHint=%b)%n",
|
||||
obj.getGobjectId(),
|
||||
obj.getTagName(),
|
||||
obj.getBrowseName(),
|
||||
obj.getAttributesCount(),
|
||||
child.hasChildrenHint());
|
||||
}
|
||||
|
||||
private static List<Integer> parseOptionalIntList(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return List.of();
|
||||
}
|
||||
return parseIntList(value);
|
||||
}
|
||||
|
||||
private static List<String> parseOptionalStringList(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return List.of();
|
||||
}
|
||||
return parseStringList(value);
|
||||
}
|
||||
|
||||
@Command(
|
||||
name = "galaxy-watch",
|
||||
description = "Streams GalaxyRepository.WatchDeployEvents until cancelled.")
|
||||
@@ -622,6 +893,31 @@ public final class MxGatewayCli implements Callable<Integer> {
|
||||
}
|
||||
}
|
||||
|
||||
@Command(name = "ping", description = "Sends a diagnostic ping command to the session worker.")
|
||||
static final class PingCommandLine extends GatewayCommand {
|
||||
@Option(names = "--session-id", required = true, description = "Gateway session id.")
|
||||
String sessionId;
|
||||
|
||||
@Option(names = "--message", defaultValue = "ping", description = "Message echoed back in the reply.")
|
||||
String message;
|
||||
|
||||
PingCommandLine(MxGatewayCliClientFactory clientFactory) {
|
||||
super(clientFactory);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer call() {
|
||||
try (MxGatewayCliClient client = clientFactory.connect(common.resolved())) {
|
||||
MxCommandReply reply = client.session(sessionId).pingRaw(message);
|
||||
// The worker echoes the message in the diagnostic message field;
|
||||
// there is no dedicated ping reply payload, so the plain-text path
|
||||
// surfaces that field.
|
||||
writeOutput("ping", common, json, reply, reply::getDiagnosticMessage);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Command(name = "register", description = "Invokes MXAccess Register.")
|
||||
static final class RegisterCommand extends GatewayCommand {
|
||||
@Option(names = "--session-id", required = true, description = "Gateway session id.")
|
||||
@@ -1438,6 +1734,8 @@ public final class MxGatewayCli implements Callable<Integer> {
|
||||
}
|
||||
|
||||
interface MxGatewayCliSession {
|
||||
MxCommandReply pingRaw(String message);
|
||||
|
||||
int register(String clientName);
|
||||
|
||||
MxCommandReply registerRaw(String clientName);
|
||||
@@ -1523,6 +1821,14 @@ public final class MxGatewayCli implements Callable<Integer> {
|
||||
}
|
||||
|
||||
record GrpcMxGatewayCliSession(MxGatewaySession session) implements MxGatewayCliSession {
|
||||
@Override
|
||||
public MxCommandReply pingRaw(String message) {
|
||||
return session.invokeCommand(MxCommand.newBuilder()
|
||||
.setKind(MxCommandKind.MX_COMMAND_KIND_PING)
|
||||
.setPing(PingCommand.newBuilder().setMessage(message))
|
||||
.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int register(String clientName) {
|
||||
return session.register(clientName);
|
||||
|
||||
+128
@@ -6,6 +6,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import com.zb.mom.ww.mxgateway.client.MxGatewayAlarmFeedSubscription;
|
||||
import com.zb.mom.ww.mxgateway.client.MxGatewayClientOptions;
|
||||
import galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject;
|
||||
import io.grpc.stub.StreamObserver;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
@@ -15,6 +16,7 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.AcknowledgeAlarmReply;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.AcknowledgeAlarmRequest;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.ActiveAlarmSnapshot;
|
||||
@@ -124,6 +126,119 @@ final class MxGatewayCliTests {
|
||||
assertTrue(run.output().contains("\"itemHandle\":7"));
|
||||
}
|
||||
|
||||
// ---- ping subcommand (D4) ----
|
||||
|
||||
@Test
|
||||
void pingCommandForwardsMessageAndPrintsEcho() {
|
||||
FakeClientFactory factory = new FakeClientFactory();
|
||||
CliRun run = execute(
|
||||
factory, "ping", "--session-id", "session-cli", "--message", "hello-mxgw");
|
||||
|
||||
assertEquals(0, run.exitCode());
|
||||
assertEquals("hello-mxgw", factory.client.session.lastPingMessage);
|
||||
// The worker echoes the message in the diagnostic message field; the
|
||||
// plain-text path surfaces exactly that echoed value.
|
||||
assertEquals("hello-mxgw", run.output().trim());
|
||||
}
|
||||
|
||||
@Test
|
||||
void pingCommandDefaultsMessageToPing() {
|
||||
FakeClientFactory factory = new FakeClientFactory();
|
||||
CliRun run = execute(factory, "ping", "--session-id", "session-cli");
|
||||
|
||||
assertEquals(0, run.exitCode());
|
||||
assertEquals("ping", factory.client.session.lastPingMessage);
|
||||
}
|
||||
|
||||
@Test
|
||||
void pingCommandJsonIncludesPingKindAndDiagnosticMessage() {
|
||||
FakeClientFactory factory = new FakeClientFactory();
|
||||
CliRun run = execute(
|
||||
factory, "ping", "--session-id", "session-cli", "--message", "diag-1", "--json");
|
||||
|
||||
assertEquals(0, run.exitCode());
|
||||
String out = run.output();
|
||||
assertTrue(out.contains("\"command\":\"ping\""), out);
|
||||
assertTrue(out.contains("\"kind\":\"MX_COMMAND_KIND_PING\""), out);
|
||||
assertTrue(out.contains("diag-1"), out);
|
||||
}
|
||||
|
||||
// ---- galaxy-browse subcommand (D8-java) ----
|
||||
|
||||
@Test
|
||||
void galaxyBrowseNodeJsonUsesHasChildrenHintKeyAndFlattensObjectFields() {
|
||||
GalaxyObject object = GalaxyObject.newBuilder()
|
||||
.setGobjectId(101)
|
||||
.setTagName("Area001")
|
||||
.setBrowseName("Area001")
|
||||
.build();
|
||||
Map<String, Object> leaf = MxGatewayCli.browseNodeMap(
|
||||
GalaxyObject.newBuilder().setGobjectId(202).setTagName("Pump001").build(),
|
||||
false,
|
||||
List.of());
|
||||
Map<String, Object> node = MxGatewayCli.browseNodeMap(object, true, List.of(leaf));
|
||||
|
||||
// Cross-client JSON parity: the per-node "has children" flag MUST use the
|
||||
// key hasChildrenHint (Rust / Python / .NET / Go all standardized on it).
|
||||
assertTrue(node.containsKey("hasChildrenHint"), node.toString());
|
||||
assertEquals(Boolean.TRUE, node.get("hasChildrenHint"));
|
||||
// Object fields are flattened directly into the node (matching the
|
||||
// galaxy-discover object shape), not nested under an "object" key.
|
||||
assertFalse(node.containsKey("object"), node.toString());
|
||||
assertEquals(101L, ((Number) node.get("gobjectId")).longValue());
|
||||
assertEquals("Area001", node.get("tagName"));
|
||||
// Nested children array carries the same node shape recursively.
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> children = (List<Map<String, Object>>) node.get("children");
|
||||
assertEquals(1, children.size());
|
||||
assertTrue(children.get(0).containsKey("hasChildrenHint"));
|
||||
assertEquals(Boolean.FALSE, children.get(0).get("hasChildrenHint"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void galaxyBrowseInvocationsParseCleanly() {
|
||||
// galaxy-browse connects via GalaxyRepositoryClient.connect (a static),
|
||||
// so the full surface is exercised only by the cross-language matrix
|
||||
// against a live gateway. Here we assert the option surface parses.
|
||||
assertReadmeExampleParses(new String[] {"galaxy-browse", "--json"});
|
||||
assertReadmeExampleParses(new String[] {"galaxy-browse", "--parent", "42", "--json"});
|
||||
assertReadmeExampleParses(new String[] {
|
||||
"galaxy-browse",
|
||||
"--depth", "2",
|
||||
"--category-ids", "1,2",
|
||||
"--template-contains", "$Pump",
|
||||
"--tag-name-glob", "Area%",
|
||||
"--alarm-bearing-only",
|
||||
"--historized-only",
|
||||
"--include-attributes",
|
||||
"--json"
|
||||
});
|
||||
}
|
||||
|
||||
// ---- galaxy command-name aliases (D9-java) ----
|
||||
|
||||
@Test
|
||||
void galaxyTestConnectionCanonicalAndDeprecatedAliasResolve() {
|
||||
picocli.CommandLine commandLine = MxGatewayCli.commandLine(new FakeClientFactory());
|
||||
// Both the canonical dash-separated name and the deprecated short alias
|
||||
// must resolve to the same subcommand so existing scripts keep working.
|
||||
assertTrue(commandLine.getSubcommands().containsKey("galaxy-test-connection"));
|
||||
assertTrue(commandLine.getSubcommands().containsKey("galaxy-test"));
|
||||
assertEquals(
|
||||
commandLine.getSubcommands().get("galaxy-test-connection"),
|
||||
commandLine.getSubcommands().get("galaxy-test"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void galaxyLastDeployCanonicalAndDeprecatedAliasResolve() {
|
||||
picocli.CommandLine commandLine = MxGatewayCli.commandLine(new FakeClientFactory());
|
||||
assertTrue(commandLine.getSubcommands().containsKey("galaxy-last-deploy"));
|
||||
assertTrue(commandLine.getSubcommands().containsKey("galaxy-deploy-time"));
|
||||
assertEquals(
|
||||
commandLine.getSubcommands().get("galaxy-last-deploy"),
|
||||
commandLine.getSubcommands().get("galaxy-deploy-time"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void subscribeBulkCommandPrintsResults() {
|
||||
CliRun run = execute(
|
||||
@@ -652,6 +767,19 @@ final class MxGatewayCliTests {
|
||||
private boolean addItemCalled;
|
||||
private boolean adviseCalled;
|
||||
private MxValue lastWriteValue;
|
||||
private String lastPingMessage;
|
||||
|
||||
@Override
|
||||
public MxCommandReply pingRaw(String message) {
|
||||
lastPingMessage = message;
|
||||
// The worker echoes the request message in the diagnostic message
|
||||
// field; there is no dedicated ping reply payload.
|
||||
return MxCommandReply.newBuilder()
|
||||
.setKind(MxCommandKind.MX_COMMAND_KIND_PING)
|
||||
.setProtocolStatus(ok())
|
||||
.setDiagnosticMessage(message)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int register(String clientName) {
|
||||
|
||||
Reference in New Issue
Block a user