diff --git a/clients/dotnet/MxGateway.Client.Cli/MxGatewayClientCli.cs b/clients/dotnet/MxGateway.Client.Cli/MxGatewayClientCli.cs index 54723e1..2fcc66f 100644 --- a/clients/dotnet/MxGateway.Client.Cli/MxGatewayClientCli.cs +++ b/clients/dotnet/MxGateway.Client.Cli/MxGatewayClientCli.cs @@ -824,9 +824,7 @@ public static class MxGatewayClientCli TextWriter output, CancellationToken cancellationToken) { - DiscoverHierarchyReply reply = await client.GalaxyDiscoverHierarchyAsync( - new DiscoverHierarchyRequest(), - cancellationToken) + DiscoverHierarchyReply reply = await DiscoverAllGalaxyHierarchyAsync(client, cancellationToken) .ConfigureAwait(false); if (arguments.HasFlag("json")) @@ -844,6 +842,39 @@ public static class MxGatewayClientCli return 0; } + private static async Task DiscoverAllGalaxyHierarchyAsync( + IMxGatewayCliClient client, + CancellationToken cancellationToken) + { + DiscoverHierarchyReply aggregate = new(); + HashSet seenPageTokens = new(StringComparer.Ordinal); + string pageToken = string.Empty; + do + { + DiscoverHierarchyReply page = await client.GalaxyDiscoverHierarchyAsync( + new DiscoverHierarchyRequest + { + PageSize = 5000, + PageToken = pageToken, + }, + cancellationToken) + .ConfigureAwait(false); + + aggregate.Objects.Add(page.Objects); + aggregate.TotalObjectCount = page.TotalObjectCount; + pageToken = page.NextPageToken; + if (!string.IsNullOrWhiteSpace(pageToken) + && !seenPageTokens.Add(pageToken)) + { + throw new MxGatewayException( + $"Galaxy DiscoverHierarchy returned a repeated page token '{pageToken}'."); + } + } + while (!string.IsNullOrWhiteSpace(pageToken)); + + return aggregate; + } + private static async Task GalaxyWatchAsync( CliArguments arguments, IMxGatewayCliClient client, diff --git a/clients/dotnet/MxGateway.Client.Tests/FakeGalaxyRepositoryTransport.cs b/clients/dotnet/MxGateway.Client.Tests/FakeGalaxyRepositoryTransport.cs index 4e9c912..302775b 100644 --- a/clients/dotnet/MxGateway.Client.Tests/FakeGalaxyRepositoryTransport.cs +++ b/clients/dotnet/MxGateway.Client.Tests/FakeGalaxyRepositoryTransport.cs @@ -48,6 +48,8 @@ internal sealed class FakeGalaxyRepositoryTransport(MxGatewayClientOptions optio /// public DiscoverHierarchyReply DiscoverHierarchyReply { get; set; } = new(); + public Queue DiscoverHierarchyReplies { get; } = new(); + /// /// Gets the queue of exceptions to throw from TestConnection; dequeued in FIFO order. /// @@ -114,7 +116,10 @@ internal sealed class FakeGalaxyRepositoryTransport(MxGatewayClientOptions optio throw exception; } - return Task.FromResult(DiscoverHierarchyReply); + return Task.FromResult( + DiscoverHierarchyReplies.TryDequeue(out DiscoverHierarchyReply? reply) + ? reply + : DiscoverHierarchyReply); } /// diff --git a/clients/dotnet/MxGateway.Client.Tests/GalaxyRepositoryClientTests.cs b/clients/dotnet/MxGateway.Client.Tests/GalaxyRepositoryClientTests.cs index 4c0ecfb..f7c815e 100644 --- a/clients/dotnet/MxGateway.Client.Tests/GalaxyRepositoryClientTests.cs +++ b/clients/dotnet/MxGateway.Client.Tests/GalaxyRepositoryClientTests.cs @@ -83,8 +83,10 @@ public sealed class GalaxyRepositoryClientTests public async Task DiscoverHierarchyAsync_ReturnsObjectsFromReply() { FakeGalaxyRepositoryTransport transport = CreateTransport(); - transport.DiscoverHierarchyReply = new DiscoverHierarchyReply + transport.DiscoverHierarchyReplies.Enqueue(new DiscoverHierarchyReply { + NextPageToken = "page-2", + TotalObjectCount = 2, Objects = { new GalaxyObject @@ -106,12 +108,29 @@ public sealed class GalaxyRepositoryClientTests }, }, }, - }; + }); + transport.DiscoverHierarchyReplies.Enqueue(new DiscoverHierarchyReply + { + TotalObjectCount = 2, + Objects = + { + new GalaxyObject + { + GobjectId = 13, + TagName = "DelmiaReceiver_002", + }, + }, + }); await using GalaxyRepositoryClient client = CreateClient(transport); IReadOnlyList objects = await client.DiscoverHierarchyAsync(); - GalaxyObject obj = Assert.Single(objects); + Assert.Equal(2, objects.Count); + Assert.Equal(2, transport.DiscoverHierarchyCalls.Count); + Assert.Equal(5000, transport.DiscoverHierarchyCalls[0].Request.PageSize); + Assert.Equal("", transport.DiscoverHierarchyCalls[0].Request.PageToken); + Assert.Equal("page-2", transport.DiscoverHierarchyCalls[1].Request.PageToken); + GalaxyObject obj = objects[0]; Assert.Equal(12, obj.GobjectId); Assert.Equal("DelmiaReceiver_001", obj.TagName); GalaxyAttribute attribute = Assert.Single(obj.Attributes); @@ -142,6 +161,57 @@ public sealed class GalaxyRepositoryClientTests /// /// Verifies that TestConnectionAsync retries on transient gRPC failures. /// + [Fact] + public async Task DiscoverHierarchyAsync_WithRepeatedPageToken_ThrowsProtocolError() + { + FakeGalaxyRepositoryTransport transport = CreateTransport(); + transport.DiscoverHierarchyReplies.Enqueue(new DiscoverHierarchyReply + { + NextPageToken = "7:1", + }); + transport.DiscoverHierarchyReplies.Enqueue(new DiscoverHierarchyReply + { + NextPageToken = "7:1", + }); + await using GalaxyRepositoryClient client = CreateClient(transport); + + MxGatewayException exception = await Assert.ThrowsAsync( + async () => await client.DiscoverHierarchyAsync()); + + Assert.Contains("repeated page token", exception.Message, StringComparison.Ordinal); + } + + [Fact] + public async Task DiscoverHierarchyAsync_WithOptions_MapsTypedFilters() + { + FakeGalaxyRepositoryTransport transport = CreateTransport(); + await using GalaxyRepositoryClient client = CreateClient(transport); + + await client.DiscoverHierarchyAsync(new DiscoverHierarchyOptions + { + RootContainedPath = "Area1/Line3", + MaxDepth = 2, + CategoryIds = [10, 13], + TemplateChainContains = ["Pump"], + TagNameGlob = "Pump_*", + IncludeAttributes = false, + AlarmBearingOnly = true, + HistorizedOnly = true, + }); + + DiscoverHierarchyRequest request = Assert.Single(transport.DiscoverHierarchyCalls).Request; + Assert.Equal(DiscoverHierarchyRequest.RootOneofCase.RootContainedPath, request.RootCase); + Assert.Equal("Area1/Line3", request.RootContainedPath); + Assert.Equal(2, request.MaxDepth); + Assert.Equal([10, 13], request.CategoryIds); + Assert.Equal(["Pump"], request.TemplateChainContains); + Assert.Equal("Pump_*", request.TagNameGlob); + Assert.True(request.HasIncludeAttributes); + Assert.False(request.IncludeAttributes); + Assert.True(request.AlarmBearingOnly); + Assert.True(request.HistorizedOnly); + } + [Fact] public async Task TestConnectionAsync_RetriesOnTransientGrpcFailure() { diff --git a/clients/dotnet/MxGateway.Client.Tests/MxGatewayClientCliTests.cs b/clients/dotnet/MxGateway.Client.Tests/MxGatewayClientCliTests.cs index e293ee4..dbc7583 100644 --- a/clients/dotnet/MxGateway.Client.Tests/MxGatewayClientCliTests.cs +++ b/clients/dotnet/MxGateway.Client.Tests/MxGatewayClientCliTests.cs @@ -18,7 +18,7 @@ public sealed class MxGatewayClientCliTests var exitCode = MxGatewayClientCli.Run(["version"], output, error); Assert.Equal(0, exitCode); - Assert.Contains("gateway-protocol=1", output.ToString()); + Assert.Contains("gateway-protocol=2", output.ToString()); Assert.Contains("worker-protocol=1", output.ToString()); Assert.Equal(string.Empty, error.ToString()); } @@ -33,7 +33,7 @@ public sealed class MxGatewayClientCliTests int exitCode = await MxGatewayClientCli.RunAsync(["version", "--json"], output, error); Assert.Equal(0, exitCode); - Assert.Contains("\"gatewayProtocolVersion\":1", output.ToString()); + Assert.Contains("\"gatewayProtocolVersion\":2", output.ToString()); Assert.Equal(string.Empty, error.ToString()); } @@ -216,8 +216,10 @@ public sealed class MxGatewayClientCliTests using var output = new StringWriter(); using var error = new StringWriter(); FakeCliClient fakeClient = new(); - fakeClient.GalaxyDiscoverHierarchyReply = new DiscoverHierarchyReply + fakeClient.GalaxyDiscoverHierarchyReplies.Enqueue(new DiscoverHierarchyReply { + NextPageToken = "7:1", + TotalObjectCount = 2, Objects = { new GalaxyObject @@ -236,7 +238,21 @@ public sealed class MxGatewayClientCliTests }, }, }, - }; + }); + fakeClient.GalaxyDiscoverHierarchyReplies.Enqueue(new DiscoverHierarchyReply + { + TotalObjectCount = 2, + Objects = + { + new GalaxyObject + { + GobjectId = 8, + TagName = "DelmiaReceiver_002", + ContainedName = "DelmiaReceiver", + ParentGobjectId = 1, + }, + }, + }); int exitCode = await MxGatewayClientCli.RunAsync( [ @@ -251,10 +267,14 @@ public sealed class MxGatewayClientCliTests _ => fakeClient); Assert.Equal(0, exitCode); - Assert.Single(fakeClient.GalaxyDiscoverHierarchyRequests); + Assert.Equal(2, fakeClient.GalaxyDiscoverHierarchyRequests.Count); + Assert.Equal(5000, fakeClient.GalaxyDiscoverHierarchyRequests[0].PageSize); + Assert.Equal("", fakeClient.GalaxyDiscoverHierarchyRequests[0].PageToken); + Assert.Equal("7:1", fakeClient.GalaxyDiscoverHierarchyRequests[1].PageToken); string text = output.ToString(); - Assert.Contains("objects=1", text); + Assert.Contains("objects=2", text); Assert.Contains("DelmiaReceiver_001", text); + Assert.Contains("DelmiaReceiver_002", text); Assert.Contains("attributes=1", text); Assert.Equal(string.Empty, error.ToString()); } @@ -436,6 +456,8 @@ public sealed class MxGatewayClientCliTests /// Galaxy discover hierarchy reply to return. public DiscoverHierarchyReply GalaxyDiscoverHierarchyReply { get; set; } = new(); + public Queue GalaxyDiscoverHierarchyReplies { get; } = new(); + /// List of received galaxy test connection requests. public List GalaxyTestConnectionRequests { get; } = []; @@ -469,7 +491,10 @@ public sealed class MxGatewayClientCliTests CancellationToken cancellationToken) { GalaxyDiscoverHierarchyRequests.Add(request); - return Task.FromResult(GalaxyDiscoverHierarchyReply); + return Task.FromResult( + GalaxyDiscoverHierarchyReplies.TryDequeue(out DiscoverHierarchyReply? reply) + ? reply + : GalaxyDiscoverHierarchyReply); } /// List of received galaxy watch deploy events requests. diff --git a/clients/dotnet/MxGateway.Client/GalaxyRepositoryClient.cs b/clients/dotnet/MxGateway.Client/GalaxyRepositoryClient.cs index 4894af8..77c2272 100644 --- a/clients/dotnet/MxGateway.Client/GalaxyRepositoryClient.cs +++ b/clients/dotnet/MxGateway.Client/GalaxyRepositoryClient.cs @@ -18,6 +18,8 @@ namespace MxGateway.Client; /// public sealed class GalaxyRepositoryClient : IAsyncDisposable { + private const int DiscoverHierarchyPageSize = 5000; + private readonly GrpcChannel? _channel; private readonly IGalaxyRepositoryClientTransport _transport; private readonly ResiliencePipeline _safeUnaryRetryPipeline; @@ -84,6 +86,8 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable { HttpHandler = handler, LoggerFactory = options.LoggerFactory, + MaxReceiveMessageSize = options.MaxGrpcMessageBytes, + MaxSendMessageSize = options.MaxGrpcMessageBytes, }); return new GalaxyRepositoryClient( @@ -175,12 +179,81 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable /// The collection of Galaxy objects in the hierarchy. public async Task> DiscoverHierarchyAsync(CancellationToken cancellationToken = default) { - DiscoverHierarchyReply reply = await DiscoverHierarchyRawAsync( - new DiscoverHierarchyRequest(), - cancellationToken) - .ConfigureAwait(false); + return await DiscoverHierarchyAsync(new DiscoverHierarchyOptions(), cancellationToken).ConfigureAwait(false); + } - return reply.Objects; + public async Task> DiscoverHierarchyAsync( + DiscoverHierarchyOptions options, + CancellationToken cancellationToken = default) + { + List objects = []; + HashSet seenPageTokens = new(StringComparer.Ordinal); + string pageToken = string.Empty; + do + { + DiscoverHierarchyRequest request = CreateDiscoverHierarchyRequest(options); + request.PageSize = DiscoverHierarchyPageSize; + request.PageToken = pageToken; + DiscoverHierarchyReply reply = await DiscoverHierarchyRawAsync( + request, + cancellationToken) + .ConfigureAwait(false); + + objects.AddRange(reply.Objects); + pageToken = reply.NextPageToken; + if (!string.IsNullOrWhiteSpace(pageToken) + && !seenPageTokens.Add(pageToken)) + { + throw new MxGatewayException( + $"Galaxy DiscoverHierarchy returned a repeated page token '{pageToken}'."); + } + } + while (!string.IsNullOrWhiteSpace(pageToken)); + + return objects; + } + + private static DiscoverHierarchyRequest CreateDiscoverHierarchyRequest(DiscoverHierarchyOptions options) + { + ArgumentNullException.ThrowIfNull(options); + + DiscoverHierarchyRequest request = new() + { + AlarmBearingOnly = options.AlarmBearingOnly, + HistorizedOnly = options.HistorizedOnly, + }; + + if (options.RootGobjectId.HasValue) + { + request.RootGobjectId = options.RootGobjectId.Value; + } + else if (!string.IsNullOrWhiteSpace(options.RootTagName)) + { + request.RootTagName = options.RootTagName; + } + else if (!string.IsNullOrWhiteSpace(options.RootContainedPath)) + { + request.RootContainedPath = options.RootContainedPath; + } + + if (options.MaxDepth.HasValue) + { + request.MaxDepth = options.MaxDepth.Value; + } + + request.CategoryIds.Add(options.CategoryIds); + request.TemplateChainContains.Add(options.TemplateChainContains); + if (!string.IsNullOrWhiteSpace(options.TagNameGlob)) + { + request.TagNameGlob = options.TagNameGlob; + } + + if (options.IncludeAttributes.HasValue) + { + request.IncludeAttributes = options.IncludeAttributes.Value; + } + + return request; } /// diff --git a/clients/dotnet/MxGateway.Client/MxGatewayClient.cs b/clients/dotnet/MxGateway.Client/MxGatewayClient.cs index 4d29cf4..cc11c5b 100644 --- a/clients/dotnet/MxGateway.Client/MxGatewayClient.cs +++ b/clients/dotnet/MxGateway.Client/MxGatewayClient.cs @@ -80,6 +80,8 @@ public sealed class MxGatewayClient : IAsyncDisposable { HttpHandler = handler, LoggerFactory = options.LoggerFactory, + MaxReceiveMessageSize = options.MaxGrpcMessageBytes, + MaxSendMessageSize = options.MaxGrpcMessageBytes, }); return new MxGatewayClient( diff --git a/clients/dotnet/MxGateway.Client/MxGatewayClientOptions.cs b/clients/dotnet/MxGateway.Client/MxGatewayClientOptions.cs index 5cd4551..bb900c9 100644 --- a/clients/dotnet/MxGateway.Client/MxGatewayClientOptions.cs +++ b/clients/dotnet/MxGateway.Client/MxGatewayClientOptions.cs @@ -47,6 +47,8 @@ public sealed class MxGatewayClientOptions /// public TimeSpan? StreamTimeout { get; init; } + public int MaxGrpcMessageBytes { get; init; } = 16 * 1024 * 1024; + /// /// Gets the retry configuration for safe unary calls. /// @@ -102,6 +104,13 @@ public sealed class MxGatewayClientOptions "The stream timeout must be greater than zero when configured."); } + if (MaxGrpcMessageBytes <= 0) + { + throw new ArgumentOutOfRangeException( + nameof(MaxGrpcMessageBytes), + "The maximum gRPC message size must be greater than zero."); + } + if (UseTls && Endpoint.Scheme != Uri.UriSchemeHttps) { throw new ArgumentException( diff --git a/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/DeployEventStream.java b/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/DeployEventStream.java index 5d09b89..41d41b6 100644 --- a/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/DeployEventStream.java +++ b/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/DeployEventStream.java @@ -11,6 +11,7 @@ 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 @@ -22,8 +23,8 @@ public final class DeployEventStream implements Iterator, AutoClose private static final Object END = new Object(); private final BlockingQueue queue; + private final AtomicBoolean closed = new AtomicBoolean(); private volatile ClientCallStreamObserver requestStream; - private volatile boolean closed; private Object next; DeployEventStream(int capacity) { @@ -35,6 +36,9 @@ public final class DeployEventStream implements Iterator, AutoClose @Override public void beforeStart(ClientCallStreamObserver requestStream) { DeployEventStream.this.requestStream = requestStream; + if (closed.get()) { + requestStream.cancel("client cancelled deploy event stream", null); + } } @Override @@ -44,7 +48,7 @@ public final class DeployEventStream implements Iterator, AutoClose @Override public void onError(Throwable error) { - if (Status.fromThrowable(error).getCode() == Status.Code.CANCELLED && closed) { + if (Status.fromThrowable(error).getCode() == Status.Code.CANCELLED && closed.get()) { offer(END); return; } @@ -90,7 +94,7 @@ public final class DeployEventStream implements Iterator, AutoClose @Override public void close() { - closed = true; + closed.set(true); ClientCallStreamObserver stream = requestStream; if (stream != null) { stream.cancel("client cancelled deploy event stream", null); diff --git a/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/GalaxyRepositoryClient.java b/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/GalaxyRepositoryClient.java index 15607a4..9200dda 100644 --- a/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/GalaxyRepositoryClient.java +++ b/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/GalaxyRepositoryClient.java @@ -36,6 +36,8 @@ import javax.net.ssl.SSLException; * {@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; @@ -177,9 +179,22 @@ public final class GalaxyRepositoryClient implements AutoCloseable { */ public List discoverHierarchy() { try { - DiscoverHierarchyReply reply = - rawBlockingStub().discoverHierarchy(DiscoverHierarchyRequest.getDefaultInstance()); - return reply.getObjectsList(); + java.util.ArrayList objects = new java.util.ArrayList<>(); + java.util.HashSet 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; @@ -195,8 +210,7 @@ public final class GalaxyRepositoryClient implements AutoCloseable { * exceptionally with {@link MxGatewayException} on failure */ public CompletableFuture> discoverHierarchyAsync() { - return toCompletable(rawFutureStub().discoverHierarchy(DiscoverHierarchyRequest.getDefaultInstance())) - .thenApply(DiscoverHierarchyReply::getObjectsList); + return discoverHierarchyPageAsync("", new java.util.ArrayList<>(), new java.util.HashSet<>()); } /** @@ -295,7 +309,7 @@ public final class GalaxyRepositoryClient implements AutoCloseable { private static ManagedChannel createChannel(MxGatewayClientOptions options) { NettyChannelBuilder builder = NettyChannelBuilder.forTarget(options.endpoint()) - .maxInboundMessageSize(16 * 1024 * 1024); + .maxInboundMessageSize(options.maxGrpcMessageBytes()); if (!options.connectTimeout().isNegative()) { builder.withOption( io.grpc.netty.shaded.io.netty.channel.ChannelOption.CONNECT_TIMEOUT_MILLIS, @@ -327,6 +341,27 @@ public final class GalaxyRepositoryClient implements AutoCloseable { return stub.withDeadlineAfter(options.callTimeout().toNanos(), TimeUnit.NANOSECONDS); } + private CompletableFuture> discoverHierarchyPageAsync( + String pageToken, java.util.ArrayList objects, java.util.HashSet 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> 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 CompletableFuture toCompletable(com.google.common.util.concurrent.ListenableFuture source) { CompletableFuture target = new CompletableFuture<>(); Futures.addCallback( diff --git a/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewayClient.java b/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewayClient.java index db8a59a..e81e29e 100644 --- a/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewayClient.java +++ b/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewayClient.java @@ -284,7 +284,7 @@ public final class MxGatewayClient implements AutoCloseable { private static ManagedChannel createChannel(MxGatewayClientOptions options) { NettyChannelBuilder builder = NettyChannelBuilder.forTarget(options.endpoint()) - .maxInboundMessageSize(16 * 1024 * 1024); + .maxInboundMessageSize(options.maxGrpcMessageBytes()); if (!options.connectTimeout().isNegative()) { builder.withOption( io.grpc.netty.shaded.io.netty.channel.ChannelOption.CONNECT_TIMEOUT_MILLIS, diff --git a/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewayClientOptions.java b/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewayClientOptions.java index 5ef5dd4..47b95d0 100644 --- a/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewayClientOptions.java +++ b/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewayClientOptions.java @@ -14,6 +14,7 @@ import java.util.Objects; 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; @@ -23,6 +24,7 @@ public final class MxGatewayClientOptions { 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"); @@ -33,6 +35,9 @@ public final class MxGatewayClientOptions { 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; } /** @@ -126,6 +131,10 @@ public final class MxGatewayClientOptions { return streamTimeout; } + public int maxGrpcMessageBytes() { + return maxGrpcMessageBytes; + } + @Override public String toString() { return "MxGatewayClientOptions{" @@ -148,6 +157,8 @@ public final class MxGatewayClientOptions { + callTimeout + ", streamTimeout=" + streamTimeout + + ", maxGrpcMessageBytes=" + + maxGrpcMessageBytes + '}'; } @@ -170,6 +181,7 @@ public final class MxGatewayClientOptions { private Duration connectTimeout; private Duration callTimeout; private Duration streamTimeout; + private int maxGrpcMessageBytes; private Builder() { } @@ -265,6 +277,11 @@ public final class MxGatewayClientOptions { return this; } + public Builder maxGrpcMessageBytes(int value) { + maxGrpcMessageBytes = value; + return this; + } + /** * Builds an immutable {@link MxGatewayClientOptions} from the current state. * diff --git a/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewayClientVersion.java b/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewayClientVersion.java index 983d782..2f30be3 100644 --- a/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewayClientVersion.java +++ b/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewayClientVersion.java @@ -7,7 +7,7 @@ package com.dohertylan.mxgateway.client; * worker speak the same protocol version as the client. */ public final class MxGatewayClientVersion { - private static final int GATEWAY_PROTOCOL_VERSION = 1; + private static final int GATEWAY_PROTOCOL_VERSION = 2; private static final int WORKER_PROTOCOL_VERSION = 1; private static final String CLIENT_VERSION = "0.1.0"; diff --git a/clients/java/mxgateway-client/src/test/java/com/dohertylan/mxgateway/client/GalaxyRepositoryClientTests.java b/clients/java/mxgateway-client/src/test/java/com/dohertylan/mxgateway/client/GalaxyRepositoryClientTests.java index 3ee0a73..034e43d 100644 --- a/clients/java/mxgateway-client/src/test/java/com/dohertylan/mxgateway/client/GalaxyRepositoryClientTests.java +++ b/clients/java/mxgateway-client/src/test/java/com/dohertylan/mxgateway/client/GalaxyRepositoryClientTests.java @@ -3,6 +3,7 @@ package com.dohertylan.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; @@ -25,6 +26,8 @@ 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; @@ -100,31 +103,44 @@ final class GalaxyRepositoryClientTests { @Test void discoverHierarchyReturnsObjectsAndAttributes() throws Exception { - AtomicReference seenRequest = new AtomicReference<>(); + AtomicReference firstRequest = new AtomicReference<>(); + AtomicReference secondRequest = new AtomicReference<>(); TestService service = new TestService() { @Override public void discoverHierarchy( DiscoverHierarchyRequest request, StreamObserver responseObserver) { - seenRequest.set(request); - responseObserver.onNext(DiscoverHierarchyReply.newBuilder() - .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()); + 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(); } }; @@ -132,7 +148,10 @@ final class GalaxyRepositoryClientTests { try (InProcessGalaxy g = InProcessGalaxy.start(service, new AtomicReference<>()); GalaxyRepositoryClient client = g.client("")) { List objects = client.discoverHierarchy(); - assertEquals(1, objects.size()); + 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()); @@ -142,6 +161,41 @@ final class GalaxyRepositoryClientTests { } } + @Test + void deployEventStreamCloseBeforeBeforeStartCancelsStream() { + DeployEventStream stream = new DeployEventStream(4); + ClientResponseObserver 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 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() @@ -281,6 +335,51 @@ final class GalaxyRepositoryClientTests { } } + private static final class RecordingClientCallStreamObserver + extends ClientCallStreamObserver { + 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 authorization) diff --git a/clients/java/src/main/generated/main/java/galaxy_repository/v1/GalaxyRepositoryOuterClass.java b/clients/java/src/main/generated/main/java/galaxy_repository/v1/GalaxyRepositoryOuterClass.java index 5fc4aa6..be8f55c 100644 --- a/clients/java/src/main/generated/main/java/galaxy_repository/v1/GalaxyRepositoryOuterClass.java +++ b/clients/java/src/main/generated/main/java/galaxy_repository/v1/GalaxyRepositoryOuterClass.java @@ -1750,7 +1750,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera * .google.protobuf.Timestamp time_of_last_deploy = 2; */ private com.google.protobuf.SingleFieldBuilder< - com.google.protobuf.Timestamp, com.google.protobuf.Timestamp.Builder, com.google.protobuf.TimestampOrBuilder> + com.google.protobuf.Timestamp, com.google.protobuf.Timestamp.Builder, com.google.protobuf.TimestampOrBuilder> internalGetTimeOfLastDeployFieldBuilder() { if (timeOfLastDeployBuilder_ == null) { timeOfLastDeployBuilder_ = new com.google.protobuf.SingleFieldBuilder< @@ -1817,6 +1817,243 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera public interface DiscoverHierarchyRequestOrBuilder extends // @@protoc_insertion_point(interface_extends:galaxy_repository.v1.DiscoverHierarchyRequest) com.google.protobuf.MessageOrBuilder { + + /** + *
+     * Maximum number of objects to return. The server applies its default when
+     * unset and rejects non-positive values.
+     * 
+ * + * int32 page_size = 1; + * @return The pageSize. + */ + int getPageSize(); + + /** + *
+     * Opaque token returned by a previous DiscoverHierarchy response.
+     * 
+ * + * string page_token = 2; + * @return The pageToken. + */ + java.lang.String getPageToken(); + /** + *
+     * Opaque token returned by a previous DiscoverHierarchy response.
+     * 
+ * + * string page_token = 2; + * @return The bytes for pageToken. + */ + com.google.protobuf.ByteString + getPageTokenBytes(); + + /** + * int32 root_gobject_id = 3; + * @return Whether the rootGobjectId field is set. + */ + boolean hasRootGobjectId(); + /** + * int32 root_gobject_id = 3; + * @return The rootGobjectId. + */ + int getRootGobjectId(); + + /** + * string root_tag_name = 4; + * @return Whether the rootTagName field is set. + */ + boolean hasRootTagName(); + /** + * string root_tag_name = 4; + * @return The rootTagName. + */ + java.lang.String getRootTagName(); + /** + * string root_tag_name = 4; + * @return The bytes for rootTagName. + */ + com.google.protobuf.ByteString + getRootTagNameBytes(); + + /** + * string root_contained_path = 5; + * @return Whether the rootContainedPath field is set. + */ + boolean hasRootContainedPath(); + /** + * string root_contained_path = 5; + * @return The rootContainedPath. + */ + java.lang.String getRootContainedPath(); + /** + * string root_contained_path = 5; + * @return The bytes for rootContainedPath. + */ + com.google.protobuf.ByteString + getRootContainedPathBytes(); + + /** + *
+     * Optional. Cap on descendant depth from root. Zero returns only the root.
+     * Unset means unlimited depth.
+     * 
+ * + * .google.protobuf.Int32Value max_depth = 6; + * @return Whether the maxDepth field is set. + */ + boolean hasMaxDepth(); + /** + *
+     * Optional. Cap on descendant depth from root. Zero returns only the root.
+     * Unset means unlimited depth.
+     * 
+ * + * .google.protobuf.Int32Value max_depth = 6; + * @return The maxDepth. + */ + com.google.protobuf.Int32Value getMaxDepth(); + /** + *
+     * Optional. Cap on descendant depth from root. Zero returns only the root.
+     * Unset means unlimited depth.
+     * 
+ * + * .google.protobuf.Int32Value max_depth = 6; + */ + com.google.protobuf.Int32ValueOrBuilder getMaxDepthOrBuilder(); + + /** + *
+     * Optional object category id filters.
+     * 
+ * + * repeated int32 category_ids = 7; + * @return A list containing the categoryIds. + */ + java.util.List getCategoryIdsList(); + /** + *
+     * Optional object category id filters.
+     * 
+ * + * repeated int32 category_ids = 7; + * @return The count of categoryIds. + */ + int getCategoryIdsCount(); + /** + *
+     * Optional object category id filters.
+     * 
+ * + * repeated int32 category_ids = 7; + * @param index The index of the element to return. + * @return The categoryIds at the given index. + */ + int getCategoryIds(int index); + + /** + *
+     * Optional case-insensitive substring filters against template names.
+     * 
+ * + * repeated string template_chain_contains = 8; + * @return A list containing the templateChainContains. + */ + java.util.List + getTemplateChainContainsList(); + /** + *
+     * Optional case-insensitive substring filters against template names.
+     * 
+ * + * repeated string template_chain_contains = 8; + * @return The count of templateChainContains. + */ + int getTemplateChainContainsCount(); + /** + *
+     * Optional case-insensitive substring filters against template names.
+     * 
+ * + * repeated string template_chain_contains = 8; + * @param index The index of the element to return. + * @return The templateChainContains at the given index. + */ + java.lang.String getTemplateChainContains(int index); + /** + *
+     * Optional case-insensitive substring filters against template names.
+     * 
+ * + * repeated string template_chain_contains = 8; + * @param index The index of the value to return. + * @return The bytes of the templateChainContains at the given index. + */ + com.google.protobuf.ByteString + getTemplateChainContainsBytes(int index); + + /** + *
+     * Optional anchored, case-insensitive glob over object tag_name.
+     * 
+ * + * string tag_name_glob = 9; + * @return The tagNameGlob. + */ + java.lang.String getTagNameGlob(); + /** + *
+     * Optional anchored, case-insensitive glob over object tag_name.
+     * 
+ * + * string tag_name_glob = 9; + * @return The bytes for tagNameGlob. + */ + com.google.protobuf.ByteString + getTagNameGlobBytes(); + + /** + *
+     * Optional. Unset or true includes attributes. False returns object skeletons.
+     * 
+ * + * optional bool include_attributes = 10; + * @return Whether the includeAttributes field is set. + */ + boolean hasIncludeAttributes(); + /** + *
+     * Optional. Unset or true includes attributes. False returns object skeletons.
+     * 
+ * + * optional bool include_attributes = 10; + * @return The includeAttributes. + */ + boolean getIncludeAttributes(); + + /** + *
+     * Optional. Return only objects with at least one alarm-bearing attribute.
+     * 
+ * + * bool alarm_bearing_only = 11; + * @return The alarmBearingOnly. + */ + boolean getAlarmBearingOnly(); + + /** + *
+     * Optional. Return only objects with at least one historized attribute.
+     * 
+ * + * bool historized_only = 12; + * @return The historizedOnly. + */ + boolean getHistorizedOnly(); + + galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest.RootCase getRootCase(); } /** * Protobuf type {@code galaxy_repository.v1.DiscoverHierarchyRequest} @@ -1840,6 +2077,11 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera super(builder); } private DiscoverHierarchyRequest() { + pageToken_ = ""; + categoryIds_ = emptyIntList(); + templateChainContains_ = + com.google.protobuf.LazyStringArrayList.emptyList(); + tagNameGlob_ = ""; } public static final com.google.protobuf.Descriptors.Descriptor @@ -1855,6 +2097,479 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest.class, galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest.Builder.class); } + private int bitField0_; + private int rootCase_ = 0; + @SuppressWarnings("serial") + private java.lang.Object root_; + public enum RootCase + implements com.google.protobuf.Internal.EnumLite, + com.google.protobuf.AbstractMessage.InternalOneOfEnum { + ROOT_GOBJECT_ID(3), + ROOT_TAG_NAME(4), + ROOT_CONTAINED_PATH(5), + ROOT_NOT_SET(0); + private final int value; + private RootCase(int value) { + this.value = value; + } + /** + * @param value The number of the enum to look for. + * @return The enum associated with the given number. + * @deprecated Use {@link #forNumber(int)} instead. + */ + @java.lang.Deprecated + public static RootCase valueOf(int value) { + return forNumber(value); + } + + public static RootCase forNumber(int value) { + switch (value) { + case 3: return ROOT_GOBJECT_ID; + case 4: return ROOT_TAG_NAME; + case 5: return ROOT_CONTAINED_PATH; + case 0: return ROOT_NOT_SET; + default: return null; + } + } + public int getNumber() { + return this.value; + } + }; + + public RootCase + getRootCase() { + return RootCase.forNumber( + rootCase_); + } + + public static final int PAGE_SIZE_FIELD_NUMBER = 1; + private int pageSize_ = 0; + /** + *
+     * Maximum number of objects to return. The server applies its default when
+     * unset and rejects non-positive values.
+     * 
+ * + * int32 page_size = 1; + * @return The pageSize. + */ + @java.lang.Override + public int getPageSize() { + return pageSize_; + } + + public static final int PAGE_TOKEN_FIELD_NUMBER = 2; + @SuppressWarnings("serial") + private volatile java.lang.Object pageToken_ = ""; + /** + *
+     * Opaque token returned by a previous DiscoverHierarchy response.
+     * 
+ * + * string page_token = 2; + * @return The pageToken. + */ + @java.lang.Override + public java.lang.String getPageToken() { + java.lang.Object ref = pageToken_; + if (ref instanceof java.lang.String) { + return (java.lang.String) ref; + } else { + com.google.protobuf.ByteString bs = + (com.google.protobuf.ByteString) ref; + java.lang.String s = bs.toStringUtf8(); + pageToken_ = s; + return s; + } + } + /** + *
+     * Opaque token returned by a previous DiscoverHierarchy response.
+     * 
+ * + * string page_token = 2; + * @return The bytes for pageToken. + */ + @java.lang.Override + public com.google.protobuf.ByteString + getPageTokenBytes() { + java.lang.Object ref = pageToken_; + if (ref instanceof java.lang.String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8( + (java.lang.String) ref); + pageToken_ = b; + return b; + } else { + return (com.google.protobuf.ByteString) ref; + } + } + + public static final int ROOT_GOBJECT_ID_FIELD_NUMBER = 3; + /** + * int32 root_gobject_id = 3; + * @return Whether the rootGobjectId field is set. + */ + @java.lang.Override + public boolean hasRootGobjectId() { + return rootCase_ == 3; + } + /** + * int32 root_gobject_id = 3; + * @return The rootGobjectId. + */ + @java.lang.Override + public int getRootGobjectId() { + if (rootCase_ == 3) { + return (java.lang.Integer) root_; + } + return 0; + } + + public static final int ROOT_TAG_NAME_FIELD_NUMBER = 4; + /** + * string root_tag_name = 4; + * @return Whether the rootTagName field is set. + */ + public boolean hasRootTagName() { + return rootCase_ == 4; + } + /** + * string root_tag_name = 4; + * @return The rootTagName. + */ + public java.lang.String getRootTagName() { + java.lang.Object ref = ""; + if (rootCase_ == 4) { + ref = root_; + } + if (ref instanceof java.lang.String) { + return (java.lang.String) ref; + } else { + com.google.protobuf.ByteString bs = + (com.google.protobuf.ByteString) ref; + java.lang.String s = bs.toStringUtf8(); + if (rootCase_ == 4) { + root_ = s; + } + return s; + } + } + /** + * string root_tag_name = 4; + * @return The bytes for rootTagName. + */ + public com.google.protobuf.ByteString + getRootTagNameBytes() { + java.lang.Object ref = ""; + if (rootCase_ == 4) { + ref = root_; + } + if (ref instanceof java.lang.String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8( + (java.lang.String) ref); + if (rootCase_ == 4) { + root_ = b; + } + return b; + } else { + return (com.google.protobuf.ByteString) ref; + } + } + + public static final int ROOT_CONTAINED_PATH_FIELD_NUMBER = 5; + /** + * string root_contained_path = 5; + * @return Whether the rootContainedPath field is set. + */ + public boolean hasRootContainedPath() { + return rootCase_ == 5; + } + /** + * string root_contained_path = 5; + * @return The rootContainedPath. + */ + public java.lang.String getRootContainedPath() { + java.lang.Object ref = ""; + if (rootCase_ == 5) { + ref = root_; + } + if (ref instanceof java.lang.String) { + return (java.lang.String) ref; + } else { + com.google.protobuf.ByteString bs = + (com.google.protobuf.ByteString) ref; + java.lang.String s = bs.toStringUtf8(); + if (rootCase_ == 5) { + root_ = s; + } + return s; + } + } + /** + * string root_contained_path = 5; + * @return The bytes for rootContainedPath. + */ + public com.google.protobuf.ByteString + getRootContainedPathBytes() { + java.lang.Object ref = ""; + if (rootCase_ == 5) { + ref = root_; + } + if (ref instanceof java.lang.String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8( + (java.lang.String) ref); + if (rootCase_ == 5) { + root_ = b; + } + return b; + } else { + return (com.google.protobuf.ByteString) ref; + } + } + + public static final int MAX_DEPTH_FIELD_NUMBER = 6; + private com.google.protobuf.Int32Value maxDepth_; + /** + *
+     * Optional. Cap on descendant depth from root. Zero returns only the root.
+     * Unset means unlimited depth.
+     * 
+ * + * .google.protobuf.Int32Value max_depth = 6; + * @return Whether the maxDepth field is set. + */ + @java.lang.Override + public boolean hasMaxDepth() { + return ((bitField0_ & 0x00000001) != 0); + } + /** + *
+     * Optional. Cap on descendant depth from root. Zero returns only the root.
+     * Unset means unlimited depth.
+     * 
+ * + * .google.protobuf.Int32Value max_depth = 6; + * @return The maxDepth. + */ + @java.lang.Override + public com.google.protobuf.Int32Value getMaxDepth() { + return maxDepth_ == null ? com.google.protobuf.Int32Value.getDefaultInstance() : maxDepth_; + } + /** + *
+     * Optional. Cap on descendant depth from root. Zero returns only the root.
+     * Unset means unlimited depth.
+     * 
+ * + * .google.protobuf.Int32Value max_depth = 6; + */ + @java.lang.Override + public com.google.protobuf.Int32ValueOrBuilder getMaxDepthOrBuilder() { + return maxDepth_ == null ? com.google.protobuf.Int32Value.getDefaultInstance() : maxDepth_; + } + + public static final int CATEGORY_IDS_FIELD_NUMBER = 7; + @SuppressWarnings("serial") + private com.google.protobuf.Internal.IntList categoryIds_ = + emptyIntList(); + /** + *
+     * Optional object category id filters.
+     * 
+ * + * repeated int32 category_ids = 7; + * @return A list containing the categoryIds. + */ + @java.lang.Override + public java.util.List + getCategoryIdsList() { + return categoryIds_; + } + /** + *
+     * Optional object category id filters.
+     * 
+ * + * repeated int32 category_ids = 7; + * @return The count of categoryIds. + */ + public int getCategoryIdsCount() { + return categoryIds_.size(); + } + /** + *
+     * Optional object category id filters.
+     * 
+ * + * repeated int32 category_ids = 7; + * @param index The index of the element to return. + * @return The categoryIds at the given index. + */ + public int getCategoryIds(int index) { + return categoryIds_.getInt(index); + } + private int categoryIdsMemoizedSerializedSize = -1; + + public static final int TEMPLATE_CHAIN_CONTAINS_FIELD_NUMBER = 8; + @SuppressWarnings("serial") + private com.google.protobuf.LazyStringArrayList templateChainContains_ = + com.google.protobuf.LazyStringArrayList.emptyList(); + /** + *
+     * Optional case-insensitive substring filters against template names.
+     * 
+ * + * repeated string template_chain_contains = 8; + * @return A list containing the templateChainContains. + */ + public com.google.protobuf.ProtocolStringList + getTemplateChainContainsList() { + return templateChainContains_; + } + /** + *
+     * Optional case-insensitive substring filters against template names.
+     * 
+ * + * repeated string template_chain_contains = 8; + * @return The count of templateChainContains. + */ + public int getTemplateChainContainsCount() { + return templateChainContains_.size(); + } + /** + *
+     * Optional case-insensitive substring filters against template names.
+     * 
+ * + * repeated string template_chain_contains = 8; + * @param index The index of the element to return. + * @return The templateChainContains at the given index. + */ + public java.lang.String getTemplateChainContains(int index) { + return templateChainContains_.get(index); + } + /** + *
+     * Optional case-insensitive substring filters against template names.
+     * 
+ * + * repeated string template_chain_contains = 8; + * @param index The index of the value to return. + * @return The bytes of the templateChainContains at the given index. + */ + public com.google.protobuf.ByteString + getTemplateChainContainsBytes(int index) { + return templateChainContains_.getByteString(index); + } + + public static final int TAG_NAME_GLOB_FIELD_NUMBER = 9; + @SuppressWarnings("serial") + private volatile java.lang.Object tagNameGlob_ = ""; + /** + *
+     * Optional anchored, case-insensitive glob over object tag_name.
+     * 
+ * + * string tag_name_glob = 9; + * @return The tagNameGlob. + */ + @java.lang.Override + public java.lang.String getTagNameGlob() { + java.lang.Object ref = tagNameGlob_; + if (ref instanceof java.lang.String) { + return (java.lang.String) ref; + } else { + com.google.protobuf.ByteString bs = + (com.google.protobuf.ByteString) ref; + java.lang.String s = bs.toStringUtf8(); + tagNameGlob_ = s; + return s; + } + } + /** + *
+     * Optional anchored, case-insensitive glob over object tag_name.
+     * 
+ * + * string tag_name_glob = 9; + * @return The bytes for tagNameGlob. + */ + @java.lang.Override + public com.google.protobuf.ByteString + getTagNameGlobBytes() { + java.lang.Object ref = tagNameGlob_; + if (ref instanceof java.lang.String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8( + (java.lang.String) ref); + tagNameGlob_ = b; + return b; + } else { + return (com.google.protobuf.ByteString) ref; + } + } + + public static final int INCLUDE_ATTRIBUTES_FIELD_NUMBER = 10; + private boolean includeAttributes_ = false; + /** + *
+     * Optional. Unset or true includes attributes. False returns object skeletons.
+     * 
+ * + * optional bool include_attributes = 10; + * @return Whether the includeAttributes field is set. + */ + @java.lang.Override + public boolean hasIncludeAttributes() { + return ((bitField0_ & 0x00000002) != 0); + } + /** + *
+     * Optional. Unset or true includes attributes. False returns object skeletons.
+     * 
+ * + * optional bool include_attributes = 10; + * @return The includeAttributes. + */ + @java.lang.Override + public boolean getIncludeAttributes() { + return includeAttributes_; + } + + public static final int ALARM_BEARING_ONLY_FIELD_NUMBER = 11; + private boolean alarmBearingOnly_ = false; + /** + *
+     * Optional. Return only objects with at least one alarm-bearing attribute.
+     * 
+ * + * bool alarm_bearing_only = 11; + * @return The alarmBearingOnly. + */ + @java.lang.Override + public boolean getAlarmBearingOnly() { + return alarmBearingOnly_; + } + + public static final int HISTORIZED_ONLY_FIELD_NUMBER = 12; + private boolean historizedOnly_ = false; + /** + *
+     * Optional. Return only objects with at least one historized attribute.
+     * 
+ * + * bool historized_only = 12; + * @return The historizedOnly. + */ + @java.lang.Override + public boolean getHistorizedOnly() { + return historizedOnly_; + } + private byte memoizedIsInitialized = -1; @java.lang.Override public final boolean isInitialized() { @@ -1869,6 +2584,48 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera @java.lang.Override public void writeTo(com.google.protobuf.CodedOutputStream output) throws java.io.IOException { + getSerializedSize(); + if (pageSize_ != 0) { + output.writeInt32(1, pageSize_); + } + if (!com.google.protobuf.GeneratedMessage.isStringEmpty(pageToken_)) { + com.google.protobuf.GeneratedMessage.writeString(output, 2, pageToken_); + } + if (rootCase_ == 3) { + output.writeInt32( + 3, (int)((java.lang.Integer) root_)); + } + if (rootCase_ == 4) { + com.google.protobuf.GeneratedMessage.writeString(output, 4, root_); + } + if (rootCase_ == 5) { + com.google.protobuf.GeneratedMessage.writeString(output, 5, root_); + } + if (((bitField0_ & 0x00000001) != 0)) { + output.writeMessage(6, getMaxDepth()); + } + if (getCategoryIdsList().size() > 0) { + output.writeUInt32NoTag(58); + output.writeUInt32NoTag(categoryIdsMemoizedSerializedSize); + } + for (int i = 0; i < categoryIds_.size(); i++) { + output.writeInt32NoTag(categoryIds_.getInt(i)); + } + for (int i = 0; i < templateChainContains_.size(); i++) { + com.google.protobuf.GeneratedMessage.writeString(output, 8, templateChainContains_.getRaw(i)); + } + if (!com.google.protobuf.GeneratedMessage.isStringEmpty(tagNameGlob_)) { + com.google.protobuf.GeneratedMessage.writeString(output, 9, tagNameGlob_); + } + if (((bitField0_ & 0x00000002) != 0)) { + output.writeBool(10, includeAttributes_); + } + if (alarmBearingOnly_ != false) { + output.writeBool(11, alarmBearingOnly_); + } + if (historizedOnly_ != false) { + output.writeBool(12, historizedOnly_); + } getUnknownFields().writeTo(output); } @@ -1878,6 +2635,65 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera if (size != -1) return size; size = 0; + if (pageSize_ != 0) { + size += com.google.protobuf.CodedOutputStream + .computeInt32Size(1, pageSize_); + } + if (!com.google.protobuf.GeneratedMessage.isStringEmpty(pageToken_)) { + size += com.google.protobuf.GeneratedMessage.computeStringSize(2, pageToken_); + } + if (rootCase_ == 3) { + size += com.google.protobuf.CodedOutputStream + .computeInt32Size( + 3, (int)((java.lang.Integer) root_)); + } + if (rootCase_ == 4) { + size += com.google.protobuf.GeneratedMessage.computeStringSize(4, root_); + } + if (rootCase_ == 5) { + size += com.google.protobuf.GeneratedMessage.computeStringSize(5, root_); + } + if (((bitField0_ & 0x00000001) != 0)) { + size += com.google.protobuf.CodedOutputStream + .computeMessageSize(6, getMaxDepth()); + } + { + int dataSize = 0; + for (int i = 0; i < categoryIds_.size(); i++) { + dataSize += com.google.protobuf.CodedOutputStream + .computeInt32SizeNoTag(categoryIds_.getInt(i)); + } + size += dataSize; + if (!getCategoryIdsList().isEmpty()) { + size += 1; + size += com.google.protobuf.CodedOutputStream + .computeInt32SizeNoTag(dataSize); + } + categoryIdsMemoizedSerializedSize = dataSize; + } + { + int dataSize = 0; + for (int i = 0; i < templateChainContains_.size(); i++) { + dataSize += computeStringSizeNoTag(templateChainContains_.getRaw(i)); + } + size += dataSize; + size += 1 * getTemplateChainContainsList().size(); + } + if (!com.google.protobuf.GeneratedMessage.isStringEmpty(tagNameGlob_)) { + size += com.google.protobuf.GeneratedMessage.computeStringSize(9, tagNameGlob_); + } + if (((bitField0_ & 0x00000002) != 0)) { + size += com.google.protobuf.CodedOutputStream + .computeBoolSize(10, includeAttributes_); + } + if (alarmBearingOnly_ != false) { + size += com.google.protobuf.CodedOutputStream + .computeBoolSize(11, alarmBearingOnly_); + } + if (historizedOnly_ != false) { + size += com.google.protobuf.CodedOutputStream + .computeBoolSize(12, historizedOnly_); + } size += getUnknownFields().getSerializedSize(); memoizedSize = size; return size; @@ -1893,6 +2709,47 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera } galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest other = (galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest) obj; + if (getPageSize() + != other.getPageSize()) return false; + if (!getPageToken() + .equals(other.getPageToken())) return false; + if (hasMaxDepth() != other.hasMaxDepth()) return false; + if (hasMaxDepth()) { + if (!getMaxDepth() + .equals(other.getMaxDepth())) return false; + } + if (!getCategoryIdsList() + .equals(other.getCategoryIdsList())) return false; + if (!getTemplateChainContainsList() + .equals(other.getTemplateChainContainsList())) return false; + if (!getTagNameGlob() + .equals(other.getTagNameGlob())) return false; + if (hasIncludeAttributes() != other.hasIncludeAttributes()) return false; + if (hasIncludeAttributes()) { + if (getIncludeAttributes() + != other.getIncludeAttributes()) return false; + } + if (getAlarmBearingOnly() + != other.getAlarmBearingOnly()) return false; + if (getHistorizedOnly() + != other.getHistorizedOnly()) return false; + if (!getRootCase().equals(other.getRootCase())) return false; + switch (rootCase_) { + case 3: + if (getRootGobjectId() + != other.getRootGobjectId()) return false; + break; + case 4: + if (!getRootTagName() + .equals(other.getRootTagName())) return false; + break; + case 5: + if (!getRootContainedPath() + .equals(other.getRootContainedPath())) return false; + break; + case 0: + default: + } if (!getUnknownFields().equals(other.getUnknownFields())) return false; return true; } @@ -1904,6 +2761,51 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera } int hash = 41; hash = (19 * hash) + getDescriptor().hashCode(); + hash = (37 * hash) + PAGE_SIZE_FIELD_NUMBER; + hash = (53 * hash) + getPageSize(); + hash = (37 * hash) + PAGE_TOKEN_FIELD_NUMBER; + hash = (53 * hash) + getPageToken().hashCode(); + if (hasMaxDepth()) { + hash = (37 * hash) + MAX_DEPTH_FIELD_NUMBER; + hash = (53 * hash) + getMaxDepth().hashCode(); + } + if (getCategoryIdsCount() > 0) { + hash = (37 * hash) + CATEGORY_IDS_FIELD_NUMBER; + hash = (53 * hash) + getCategoryIdsList().hashCode(); + } + if (getTemplateChainContainsCount() > 0) { + hash = (37 * hash) + TEMPLATE_CHAIN_CONTAINS_FIELD_NUMBER; + hash = (53 * hash) + getTemplateChainContainsList().hashCode(); + } + hash = (37 * hash) + TAG_NAME_GLOB_FIELD_NUMBER; + hash = (53 * hash) + getTagNameGlob().hashCode(); + if (hasIncludeAttributes()) { + hash = (37 * hash) + INCLUDE_ATTRIBUTES_FIELD_NUMBER; + hash = (53 * hash) + com.google.protobuf.Internal.hashBoolean( + getIncludeAttributes()); + } + hash = (37 * hash) + ALARM_BEARING_ONLY_FIELD_NUMBER; + hash = (53 * hash) + com.google.protobuf.Internal.hashBoolean( + getAlarmBearingOnly()); + hash = (37 * hash) + HISTORIZED_ONLY_FIELD_NUMBER; + hash = (53 * hash) + com.google.protobuf.Internal.hashBoolean( + getHistorizedOnly()); + switch (rootCase_) { + case 3: + hash = (37 * hash) + ROOT_GOBJECT_ID_FIELD_NUMBER; + hash = (53 * hash) + getRootGobjectId(); + break; + case 4: + hash = (37 * hash) + ROOT_TAG_NAME_FIELD_NUMBER; + hash = (53 * hash) + getRootTagName().hashCode(); + break; + case 5: + hash = (37 * hash) + ROOT_CONTAINED_PATH_FIELD_NUMBER; + hash = (53 * hash) + getRootContainedPath().hashCode(); + break; + case 0: + default: + } hash = (29 * hash) + getUnknownFields().hashCode(); memoizedHashCode = hash; return hash; @@ -2023,17 +2925,40 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera // Construct using galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest.newBuilder() private Builder() { - + maybeForceBuilderInitialization(); } private Builder( com.google.protobuf.GeneratedMessage.BuilderParent parent) { super(parent); - + maybeForceBuilderInitialization(); + } + private void maybeForceBuilderInitialization() { + if (com.google.protobuf.GeneratedMessage + .alwaysUseFieldBuilders) { + internalGetMaxDepthFieldBuilder(); + } } @java.lang.Override public Builder clear() { super.clear(); + bitField0_ = 0; + pageSize_ = 0; + pageToken_ = ""; + maxDepth_ = null; + if (maxDepthBuilder_ != null) { + maxDepthBuilder_.dispose(); + maxDepthBuilder_ = null; + } + categoryIds_ = emptyIntList(); + templateChainContains_ = + com.google.protobuf.LazyStringArrayList.emptyList(); + tagNameGlob_ = ""; + includeAttributes_ = false; + alarmBearingOnly_ = false; + historizedOnly_ = false; + rootCase_ = 0; + root_ = null; return this; } @@ -2060,10 +2985,56 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera @java.lang.Override public galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest buildPartial() { galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest result = new galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest(this); + if (bitField0_ != 0) { buildPartial0(result); } + buildPartialOneofs(result); onBuilt(); return result; } + private void buildPartial0(galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest result) { + int from_bitField0_ = bitField0_; + if (((from_bitField0_ & 0x00000001) != 0)) { + result.pageSize_ = pageSize_; + } + if (((from_bitField0_ & 0x00000002) != 0)) { + result.pageToken_ = pageToken_; + } + int to_bitField0_ = 0; + if (((from_bitField0_ & 0x00000020) != 0)) { + result.maxDepth_ = maxDepthBuilder_ == null + ? maxDepth_ + : maxDepthBuilder_.build(); + to_bitField0_ |= 0x00000001; + } + if (((from_bitField0_ & 0x00000040) != 0)) { + categoryIds_.makeImmutable(); + result.categoryIds_ = categoryIds_; + } + if (((from_bitField0_ & 0x00000080) != 0)) { + templateChainContains_.makeImmutable(); + result.templateChainContains_ = templateChainContains_; + } + if (((from_bitField0_ & 0x00000100) != 0)) { + result.tagNameGlob_ = tagNameGlob_; + } + if (((from_bitField0_ & 0x00000200) != 0)) { + result.includeAttributes_ = includeAttributes_; + to_bitField0_ |= 0x00000002; + } + if (((from_bitField0_ & 0x00000400) != 0)) { + result.alarmBearingOnly_ = alarmBearingOnly_; + } + if (((from_bitField0_ & 0x00000800) != 0)) { + result.historizedOnly_ = historizedOnly_; + } + result.bitField0_ |= to_bitField0_; + } + + private void buildPartialOneofs(galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest result) { + result.rootCase_ = rootCase_; + result.root_ = this.root_; + } + @java.lang.Override public Builder mergeFrom(com.google.protobuf.Message other) { if (other instanceof galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest) { @@ -2076,6 +3047,73 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera public Builder mergeFrom(galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest other) { if (other == galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest.getDefaultInstance()) return this; + if (other.getPageSize() != 0) { + setPageSize(other.getPageSize()); + } + if (!other.getPageToken().isEmpty()) { + pageToken_ = other.pageToken_; + bitField0_ |= 0x00000002; + onChanged(); + } + if (other.hasMaxDepth()) { + mergeMaxDepth(other.getMaxDepth()); + } + if (!other.categoryIds_.isEmpty()) { + if (categoryIds_.isEmpty()) { + categoryIds_ = other.categoryIds_; + categoryIds_.makeImmutable(); + bitField0_ |= 0x00000040; + } else { + ensureCategoryIdsIsMutable(); + categoryIds_.addAll(other.categoryIds_); + } + onChanged(); + } + if (!other.templateChainContains_.isEmpty()) { + if (templateChainContains_.isEmpty()) { + templateChainContains_ = other.templateChainContains_; + bitField0_ |= 0x00000080; + } else { + ensureTemplateChainContainsIsMutable(); + templateChainContains_.addAll(other.templateChainContains_); + } + onChanged(); + } + if (!other.getTagNameGlob().isEmpty()) { + tagNameGlob_ = other.tagNameGlob_; + bitField0_ |= 0x00000100; + onChanged(); + } + if (other.hasIncludeAttributes()) { + setIncludeAttributes(other.getIncludeAttributes()); + } + if (other.getAlarmBearingOnly() != false) { + setAlarmBearingOnly(other.getAlarmBearingOnly()); + } + if (other.getHistorizedOnly() != false) { + setHistorizedOnly(other.getHistorizedOnly()); + } + switch (other.getRootCase()) { + case ROOT_GOBJECT_ID: { + setRootGobjectId(other.getRootGobjectId()); + break; + } + case ROOT_TAG_NAME: { + rootCase_ = 4; + root_ = other.root_; + onChanged(); + break; + } + case ROOT_CONTAINED_PATH: { + rootCase_ = 5; + root_ = other.root_; + onChanged(); + break; + } + case ROOT_NOT_SET: { + break; + } + } this.mergeUnknownFields(other.getUnknownFields()); onChanged(); return this; @@ -2102,6 +3140,82 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera case 0: done = true; break; + case 8: { + pageSize_ = input.readInt32(); + bitField0_ |= 0x00000001; + break; + } // case 8 + case 18: { + pageToken_ = input.readStringRequireUtf8(); + bitField0_ |= 0x00000002; + break; + } // case 18 + case 24: { + root_ = input.readInt32(); + rootCase_ = 3; + break; + } // case 24 + case 34: { + java.lang.String s = input.readStringRequireUtf8(); + rootCase_ = 4; + root_ = s; + break; + } // case 34 + case 42: { + java.lang.String s = input.readStringRequireUtf8(); + rootCase_ = 5; + root_ = s; + break; + } // case 42 + case 50: { + input.readMessage( + internalGetMaxDepthFieldBuilder().getBuilder(), + extensionRegistry); + bitField0_ |= 0x00000020; + break; + } // case 50 + case 56: { + int v = input.readInt32(); + ensureCategoryIdsIsMutable(); + categoryIds_.addInt(v); + break; + } // case 56 + case 58: { + int length = input.readRawVarint32(); + int limit = input.pushLimit(length); + ensureCategoryIdsIsMutable(); + while (input.getBytesUntilLimit() > 0) { + categoryIds_.addInt(input.readInt32()); + } + input.popLimit(limit); + break; + } // case 58 + case 66: { + java.lang.String s = input.readStringRequireUtf8(); + ensureTemplateChainContainsIsMutable(); + templateChainContains_.add(s); + break; + } // case 66 + case 74: { + tagNameGlob_ = input.readStringRequireUtf8(); + bitField0_ |= 0x00000100; + break; + } // case 74 + case 80: { + includeAttributes_ = input.readBool(); + bitField0_ |= 0x00000200; + break; + } // case 80 + case 88: { + alarmBearingOnly_ = input.readBool(); + bitField0_ |= 0x00000400; + break; + } // case 88 + case 96: { + historizedOnly_ = input.readBool(); + bitField0_ |= 0x00000800; + break; + } // case 96 default: { if (!super.parseUnknownField(input, extensionRegistry, tag)) { done = true; // was an endgroup tag @@ -2117,6 +3231,1050 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera } // finally return this; } + private int rootCase_ = 0; + private java.lang.Object root_; + public RootCase + getRootCase() { + return RootCase.forNumber( + rootCase_); + } + + public Builder clearRoot() { + rootCase_ = 0; + root_ = null; + onChanged(); + return this; + } + + private int bitField0_; + + private int pageSize_ ; + /** + *
+       * Maximum number of objects to return. The server applies its default when
+       * unset and rejects non-positive values.
+       * 
+ * + * int32 page_size = 1; + * @return The pageSize. + */ + @java.lang.Override + public int getPageSize() { + return pageSize_; + } + /** + *
+       * Maximum number of objects to return. The server applies its default when
+       * unset and rejects non-positive values.
+       * 
+ * + * int32 page_size = 1; + * @param value The pageSize to set. + * @return This builder for chaining. + */ + public Builder setPageSize(int value) { + + pageSize_ = value; + bitField0_ |= 0x00000001; + onChanged(); + return this; + } + /** + *
+       * Maximum number of objects to return. The server applies its default when
+       * unset and rejects non-positive values.
+       * 
+ * + * int32 page_size = 1; + * @return This builder for chaining. + */ + public Builder clearPageSize() { + bitField0_ = (bitField0_ & ~0x00000001); + pageSize_ = 0; + onChanged(); + return this; + } + + private java.lang.Object pageToken_ = ""; + /** + *
+       * Opaque token returned by a previous DiscoverHierarchy response.
+       * 
+ * + * string page_token = 2; + * @return The pageToken. + */ + public java.lang.String getPageToken() { + java.lang.Object ref = pageToken_; + if (!(ref instanceof java.lang.String)) { + com.google.protobuf.ByteString bs = + (com.google.protobuf.ByteString) ref; + java.lang.String s = bs.toStringUtf8(); + pageToken_ = s; + return s; + } else { + return (java.lang.String) ref; + } + } + /** + *
+       * Opaque token returned by a previous DiscoverHierarchy response.
+       * 
+ * + * string page_token = 2; + * @return The bytes for pageToken. + */ + public com.google.protobuf.ByteString + getPageTokenBytes() { + java.lang.Object ref = pageToken_; + if (ref instanceof String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8( + (java.lang.String) ref); + pageToken_ = b; + return b; + } else { + return (com.google.protobuf.ByteString) ref; + } + } + /** + *
+       * Opaque token returned by a previous DiscoverHierarchy response.
+       * 
+ * + * string page_token = 2; + * @param value The pageToken to set. + * @return This builder for chaining. + */ + public Builder setPageToken( + java.lang.String value) { + if (value == null) { throw new NullPointerException(); } + pageToken_ = value; + bitField0_ |= 0x00000002; + onChanged(); + return this; + } + /** + *
+       * Opaque token returned by a previous DiscoverHierarchy response.
+       * 
+ * + * string page_token = 2; + * @return This builder for chaining. + */ + public Builder clearPageToken() { + pageToken_ = getDefaultInstance().getPageToken(); + bitField0_ = (bitField0_ & ~0x00000002); + onChanged(); + return this; + } + /** + *
+       * Opaque token returned by a previous DiscoverHierarchy response.
+       * 
+ * + * string page_token = 2; + * @param value The bytes for pageToken to set. + * @return This builder for chaining. + */ + public Builder setPageTokenBytes( + com.google.protobuf.ByteString value) { + if (value == null) { throw new NullPointerException(); } + checkByteStringIsUtf8(value); + pageToken_ = value; + bitField0_ |= 0x00000002; + onChanged(); + return this; + } + + /** + * int32 root_gobject_id = 3; + * @return Whether the rootGobjectId field is set. + */ + public boolean hasRootGobjectId() { + return rootCase_ == 3; + } + /** + * int32 root_gobject_id = 3; + * @return The rootGobjectId. + */ + public int getRootGobjectId() { + if (rootCase_ == 3) { + return (java.lang.Integer) root_; + } + return 0; + } + /** + * int32 root_gobject_id = 3; + * @param value The rootGobjectId to set. + * @return This builder for chaining. + */ + public Builder setRootGobjectId(int value) { + + rootCase_ = 3; + root_ = value; + onChanged(); + return this; + } + /** + * int32 root_gobject_id = 3; + * @return This builder for chaining. + */ + public Builder clearRootGobjectId() { + if (rootCase_ == 3) { + rootCase_ = 0; + root_ = null; + onChanged(); + } + return this; + } + + /** + * string root_tag_name = 4; + * @return Whether the rootTagName field is set. + */ + @java.lang.Override + public boolean hasRootTagName() { + return rootCase_ == 4; + } + /** + * string root_tag_name = 4; + * @return The rootTagName. + */ + @java.lang.Override + public java.lang.String getRootTagName() { + java.lang.Object ref = ""; + if (rootCase_ == 4) { + ref = root_; + } + if (!(ref instanceof java.lang.String)) { + com.google.protobuf.ByteString bs = + (com.google.protobuf.ByteString) ref; + java.lang.String s = bs.toStringUtf8(); + if (rootCase_ == 4) { + root_ = s; + } + return s; + } else { + return (java.lang.String) ref; + } + } + /** + * string root_tag_name = 4; + * @return The bytes for rootTagName. + */ + @java.lang.Override + public com.google.protobuf.ByteString + getRootTagNameBytes() { + java.lang.Object ref = ""; + if (rootCase_ == 4) { + ref = root_; + } + if (ref instanceof String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8( + (java.lang.String) ref); + if (rootCase_ == 4) { + root_ = b; + } + return b; + } else { + return (com.google.protobuf.ByteString) ref; + } + } + /** + * string root_tag_name = 4; + * @param value The rootTagName to set. + * @return This builder for chaining. + */ + public Builder setRootTagName( + java.lang.String value) { + if (value == null) { throw new NullPointerException(); } + rootCase_ = 4; + root_ = value; + onChanged(); + return this; + } + /** + * string root_tag_name = 4; + * @return This builder for chaining. + */ + public Builder clearRootTagName() { + if (rootCase_ == 4) { + rootCase_ = 0; + root_ = null; + onChanged(); + } + return this; + } + /** + * string root_tag_name = 4; + * @param value The bytes for rootTagName to set. + * @return This builder for chaining. + */ + public Builder setRootTagNameBytes( + com.google.protobuf.ByteString value) { + if (value == null) { throw new NullPointerException(); } + checkByteStringIsUtf8(value); + rootCase_ = 4; + root_ = value; + onChanged(); + return this; + } + + /** + * string root_contained_path = 5; + * @return Whether the rootContainedPath field is set. + */ + @java.lang.Override + public boolean hasRootContainedPath() { + return rootCase_ == 5; + } + /** + * string root_contained_path = 5; + * @return The rootContainedPath. + */ + @java.lang.Override + public java.lang.String getRootContainedPath() { + java.lang.Object ref = ""; + if (rootCase_ == 5) { + ref = root_; + } + if (!(ref instanceof java.lang.String)) { + com.google.protobuf.ByteString bs = + (com.google.protobuf.ByteString) ref; + java.lang.String s = bs.toStringUtf8(); + if (rootCase_ == 5) { + root_ = s; + } + return s; + } else { + return (java.lang.String) ref; + } + } + /** + * string root_contained_path = 5; + * @return The bytes for rootContainedPath. + */ + @java.lang.Override + public com.google.protobuf.ByteString + getRootContainedPathBytes() { + java.lang.Object ref = ""; + if (rootCase_ == 5) { + ref = root_; + } + if (ref instanceof String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8( + (java.lang.String) ref); + if (rootCase_ == 5) { + root_ = b; + } + return b; + } else { + return (com.google.protobuf.ByteString) ref; + } + } + /** + * string root_contained_path = 5; + * @param value The rootContainedPath to set. + * @return This builder for chaining. + */ + public Builder setRootContainedPath( + java.lang.String value) { + if (value == null) { throw new NullPointerException(); } + rootCase_ = 5; + root_ = value; + onChanged(); + return this; + } + /** + * string root_contained_path = 5; + * @return This builder for chaining. + */ + public Builder clearRootContainedPath() { + if (rootCase_ == 5) { + rootCase_ = 0; + root_ = null; + onChanged(); + } + return this; + } + /** + * string root_contained_path = 5; + * @param value The bytes for rootContainedPath to set. + * @return This builder for chaining. + */ + public Builder setRootContainedPathBytes( + com.google.protobuf.ByteString value) { + if (value == null) { throw new NullPointerException(); } + checkByteStringIsUtf8(value); + rootCase_ = 5; + root_ = value; + onChanged(); + return this; + } + + private com.google.protobuf.Int32Value maxDepth_; + private com.google.protobuf.SingleFieldBuilder< + com.google.protobuf.Int32Value, com.google.protobuf.Int32Value.Builder, com.google.protobuf.Int32ValueOrBuilder> maxDepthBuilder_; + /** + *
+       * Optional. Cap on descendant depth from root. Zero returns only the root.
+       * Unset means unlimited depth.
+       * 
+ * + * .google.protobuf.Int32Value max_depth = 6; + * @return Whether the maxDepth field is set. + */ + public boolean hasMaxDepth() { + return ((bitField0_ & 0x00000020) != 0); + } + /** + *
+       * Optional. Cap on descendant depth from root. Zero returns only the root.
+       * Unset means unlimited depth.
+       * 
+ * + * .google.protobuf.Int32Value max_depth = 6; + * @return The maxDepth. + */ + public com.google.protobuf.Int32Value getMaxDepth() { + if (maxDepthBuilder_ == null) { + return maxDepth_ == null ? com.google.protobuf.Int32Value.getDefaultInstance() : maxDepth_; + } else { + return maxDepthBuilder_.getMessage(); + } + } + /** + *
+       * Optional. Cap on descendant depth from root. Zero returns only the root.
+       * Unset means unlimited depth.
+       * 
+ * + * .google.protobuf.Int32Value max_depth = 6; + */ + public Builder setMaxDepth(com.google.protobuf.Int32Value value) { + if (maxDepthBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + maxDepth_ = value; + } else { + maxDepthBuilder_.setMessage(value); + } + bitField0_ |= 0x00000020; + onChanged(); + return this; + } + /** + *
+       * Optional. Cap on descendant depth from root. Zero returns only the root.
+       * Unset means unlimited depth.
+       * 
+ * + * .google.protobuf.Int32Value max_depth = 6; + */ + public Builder setMaxDepth( + com.google.protobuf.Int32Value.Builder builderForValue) { + if (maxDepthBuilder_ == null) { + maxDepth_ = builderForValue.build(); + } else { + maxDepthBuilder_.setMessage(builderForValue.build()); + } + bitField0_ |= 0x00000020; + onChanged(); + return this; + } + /** + *
+       * Optional. Cap on descendant depth from root. Zero returns only the root.
+       * Unset means unlimited depth.
+       * 
+ * + * .google.protobuf.Int32Value max_depth = 6; + */ + public Builder mergeMaxDepth(com.google.protobuf.Int32Value value) { + if (maxDepthBuilder_ == null) { + if (((bitField0_ & 0x00000020) != 0) && + maxDepth_ != null && + maxDepth_ != com.google.protobuf.Int32Value.getDefaultInstance()) { + getMaxDepthBuilder().mergeFrom(value); + } else { + maxDepth_ = value; + } + } else { + maxDepthBuilder_.mergeFrom(value); + } + if (maxDepth_ != null) { + bitField0_ |= 0x00000020; + onChanged(); + } + return this; + } + /** + *
+       * Optional. Cap on descendant depth from root. Zero returns only the root.
+       * Unset means unlimited depth.
+       * 
+ * + * .google.protobuf.Int32Value max_depth = 6; + */ + public Builder clearMaxDepth() { + bitField0_ = (bitField0_ & ~0x00000020); + maxDepth_ = null; + if (maxDepthBuilder_ != null) { + maxDepthBuilder_.dispose(); + maxDepthBuilder_ = null; + } + onChanged(); + return this; + } + /** + *
+       * Optional. Cap on descendant depth from root. Zero returns only the root.
+       * Unset means unlimited depth.
+       * 
+ * + * .google.protobuf.Int32Value max_depth = 6; + */ + public com.google.protobuf.Int32Value.Builder getMaxDepthBuilder() { + bitField0_ |= 0x00000020; + onChanged(); + return internalGetMaxDepthFieldBuilder().getBuilder(); + } + /** + *
+       * Optional. Cap on descendant depth from root. Zero returns only the root.
+       * Unset means unlimited depth.
+       * 
+ * + * .google.protobuf.Int32Value max_depth = 6; + */ + public com.google.protobuf.Int32ValueOrBuilder getMaxDepthOrBuilder() { + if (maxDepthBuilder_ != null) { + return maxDepthBuilder_.getMessageOrBuilder(); + } else { + return maxDepth_ == null ? + com.google.protobuf.Int32Value.getDefaultInstance() : maxDepth_; + } + } + /** + *
+       * Optional. Cap on descendant depth from root. Zero returns only the root.
+       * Unset means unlimited depth.
+       * 
+ * + * .google.protobuf.Int32Value max_depth = 6; + */ + private com.google.protobuf.SingleFieldBuilder< + com.google.protobuf.Int32Value, com.google.protobuf.Int32Value.Builder, com.google.protobuf.Int32ValueOrBuilder> + internalGetMaxDepthFieldBuilder() { + if (maxDepthBuilder_ == null) { + maxDepthBuilder_ = new com.google.protobuf.SingleFieldBuilder< + com.google.protobuf.Int32Value, com.google.protobuf.Int32Value.Builder, com.google.protobuf.Int32ValueOrBuilder>( + getMaxDepth(), + getParentForChildren(), + isClean()); + maxDepth_ = null; + } + return maxDepthBuilder_; + } + + private com.google.protobuf.Internal.IntList categoryIds_ = emptyIntList(); + private void ensureCategoryIdsIsMutable() { + if (!categoryIds_.isModifiable()) { + categoryIds_ = makeMutableCopy(categoryIds_); + } + bitField0_ |= 0x00000040; + } + /** + *
+       * Optional object category id filters.
+       * 
+ * + * repeated int32 category_ids = 7; + * @return A list containing the categoryIds. + */ + public java.util.List + getCategoryIdsList() { + categoryIds_.makeImmutable(); + return categoryIds_; + } + /** + *
+       * Optional object category id filters.
+       * 
+ * + * repeated int32 category_ids = 7; + * @return The count of categoryIds. + */ + public int getCategoryIdsCount() { + return categoryIds_.size(); + } + /** + *
+       * Optional object category id filters.
+       * 
+ * + * repeated int32 category_ids = 7; + * @param index The index of the element to return. + * @return The categoryIds at the given index. + */ + public int getCategoryIds(int index) { + return categoryIds_.getInt(index); + } + /** + *
+       * Optional object category id filters.
+       * 
+ * + * repeated int32 category_ids = 7; + * @param index The index to set the value at. + * @param value The categoryIds to set. + * @return This builder for chaining. + */ + public Builder setCategoryIds( + int index, int value) { + + ensureCategoryIdsIsMutable(); + categoryIds_.setInt(index, value); + bitField0_ |= 0x00000040; + onChanged(); + return this; + } + /** + *
+       * Optional object category id filters.
+       * 
+ * + * repeated int32 category_ids = 7; + * @param value The categoryIds to add. + * @return This builder for chaining. + */ + public Builder addCategoryIds(int value) { + + ensureCategoryIdsIsMutable(); + categoryIds_.addInt(value); + bitField0_ |= 0x00000040; + onChanged(); + return this; + } + /** + *
+       * Optional object category id filters.
+       * 
+ * + * repeated int32 category_ids = 7; + * @param values The categoryIds to add. + * @return This builder for chaining. + */ + public Builder addAllCategoryIds( + java.lang.Iterable values) { + ensureCategoryIdsIsMutable(); + com.google.protobuf.AbstractMessageLite.Builder.addAll( + values, categoryIds_); + bitField0_ |= 0x00000040; + onChanged(); + return this; + } + /** + *
+       * Optional object category id filters.
+       * 
+ * + * repeated int32 category_ids = 7; + * @return This builder for chaining. + */ + public Builder clearCategoryIds() { + categoryIds_ = emptyIntList(); + bitField0_ = (bitField0_ & ~0x00000040); + onChanged(); + return this; + } + + private com.google.protobuf.LazyStringArrayList templateChainContains_ = + com.google.protobuf.LazyStringArrayList.emptyList(); + private void ensureTemplateChainContainsIsMutable() { + if (!templateChainContains_.isModifiable()) { + templateChainContains_ = new com.google.protobuf.LazyStringArrayList(templateChainContains_); + } + bitField0_ |= 0x00000080; + } + /** + *
+       * Optional case-insensitive substring filters against template names.
+       * 
+ * + * repeated string template_chain_contains = 8; + * @return A list containing the templateChainContains. + */ + public com.google.protobuf.ProtocolStringList + getTemplateChainContainsList() { + templateChainContains_.makeImmutable(); + return templateChainContains_; + } + /** + *
+       * Optional case-insensitive substring filters against template names.
+       * 
+ * + * repeated string template_chain_contains = 8; + * @return The count of templateChainContains. + */ + public int getTemplateChainContainsCount() { + return templateChainContains_.size(); + } + /** + *
+       * Optional case-insensitive substring filters against template names.
+       * 
+ * + * repeated string template_chain_contains = 8; + * @param index The index of the element to return. + * @return The templateChainContains at the given index. + */ + public java.lang.String getTemplateChainContains(int index) { + return templateChainContains_.get(index); + } + /** + *
+       * Optional case-insensitive substring filters against template names.
+       * 
+ * + * repeated string template_chain_contains = 8; + * @param index The index of the value to return. + * @return The bytes of the templateChainContains at the given index. + */ + public com.google.protobuf.ByteString + getTemplateChainContainsBytes(int index) { + return templateChainContains_.getByteString(index); + } + /** + *
+       * Optional case-insensitive substring filters against template names.
+       * 
+ * + * repeated string template_chain_contains = 8; + * @param index The index to set the value at. + * @param value The templateChainContains to set. + * @return This builder for chaining. + */ + public Builder setTemplateChainContains( + int index, java.lang.String value) { + if (value == null) { throw new NullPointerException(); } + ensureTemplateChainContainsIsMutable(); + templateChainContains_.set(index, value); + bitField0_ |= 0x00000080; + onChanged(); + return this; + } + /** + *
+       * Optional case-insensitive substring filters against template names.
+       * 
+ * + * repeated string template_chain_contains = 8; + * @param value The templateChainContains to add. + * @return This builder for chaining. + */ + public Builder addTemplateChainContains( + java.lang.String value) { + if (value == null) { throw new NullPointerException(); } + ensureTemplateChainContainsIsMutable(); + templateChainContains_.add(value); + bitField0_ |= 0x00000080; + onChanged(); + return this; + } + /** + *
+       * Optional case-insensitive substring filters against template names.
+       * 
+ * + * repeated string template_chain_contains = 8; + * @param values The templateChainContains to add. + * @return This builder for chaining. + */ + public Builder addAllTemplateChainContains( + java.lang.Iterable values) { + ensureTemplateChainContainsIsMutable(); + com.google.protobuf.AbstractMessageLite.Builder.addAll( + values, templateChainContains_); + bitField0_ |= 0x00000080; + onChanged(); + return this; + } + /** + *
+       * Optional case-insensitive substring filters against template names.
+       * 
+ * + * repeated string template_chain_contains = 8; + * @return This builder for chaining. + */ + public Builder clearTemplateChainContains() { + templateChainContains_ = + com.google.protobuf.LazyStringArrayList.emptyList(); + bitField0_ = (bitField0_ & ~0x00000080);; + onChanged(); + return this; + } + /** + *
+       * Optional case-insensitive substring filters against template names.
+       * 
+ * + * repeated string template_chain_contains = 8; + * @param value The bytes of the templateChainContains to add. + * @return This builder for chaining. + */ + public Builder addTemplateChainContainsBytes( + com.google.protobuf.ByteString value) { + if (value == null) { throw new NullPointerException(); } + checkByteStringIsUtf8(value); + ensureTemplateChainContainsIsMutable(); + templateChainContains_.add(value); + bitField0_ |= 0x00000080; + onChanged(); + return this; + } + + private java.lang.Object tagNameGlob_ = ""; + /** + *
+       * Optional anchored, case-insensitive glob over object tag_name.
+       * 
+ * + * string tag_name_glob = 9; + * @return The tagNameGlob. + */ + public java.lang.String getTagNameGlob() { + java.lang.Object ref = tagNameGlob_; + if (!(ref instanceof java.lang.String)) { + com.google.protobuf.ByteString bs = + (com.google.protobuf.ByteString) ref; + java.lang.String s = bs.toStringUtf8(); + tagNameGlob_ = s; + return s; + } else { + return (java.lang.String) ref; + } + } + /** + *
+       * Optional anchored, case-insensitive glob over object tag_name.
+       * 
+ * + * string tag_name_glob = 9; + * @return The bytes for tagNameGlob. + */ + public com.google.protobuf.ByteString + getTagNameGlobBytes() { + java.lang.Object ref = tagNameGlob_; + if (ref instanceof String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8( + (java.lang.String) ref); + tagNameGlob_ = b; + return b; + } else { + return (com.google.protobuf.ByteString) ref; + } + } + /** + *
+       * Optional anchored, case-insensitive glob over object tag_name.
+       * 
+ * + * string tag_name_glob = 9; + * @param value The tagNameGlob to set. + * @return This builder for chaining. + */ + public Builder setTagNameGlob( + java.lang.String value) { + if (value == null) { throw new NullPointerException(); } + tagNameGlob_ = value; + bitField0_ |= 0x00000100; + onChanged(); + return this; + } + /** + *
+       * Optional anchored, case-insensitive glob over object tag_name.
+       * 
+ * + * string tag_name_glob = 9; + * @return This builder for chaining. + */ + public Builder clearTagNameGlob() { + tagNameGlob_ = getDefaultInstance().getTagNameGlob(); + bitField0_ = (bitField0_ & ~0x00000100); + onChanged(); + return this; + } + /** + *
+       * Optional anchored, case-insensitive glob over object tag_name.
+       * 
+ * + * string tag_name_glob = 9; + * @param value The bytes for tagNameGlob to set. + * @return This builder for chaining. + */ + public Builder setTagNameGlobBytes( + com.google.protobuf.ByteString value) { + if (value == null) { throw new NullPointerException(); } + checkByteStringIsUtf8(value); + tagNameGlob_ = value; + bitField0_ |= 0x00000100; + onChanged(); + return this; + } + + private boolean includeAttributes_ ; + /** + *
+       * Optional. Unset or true includes attributes. False returns object skeletons.
+       * 
+ * + * optional bool include_attributes = 10; + * @return Whether the includeAttributes field is set. + */ + @java.lang.Override + public boolean hasIncludeAttributes() { + return ((bitField0_ & 0x00000200) != 0); + } + /** + *
+       * Optional. Unset or true includes attributes. False returns object skeletons.
+       * 
+ * + * optional bool include_attributes = 10; + * @return The includeAttributes. + */ + @java.lang.Override + public boolean getIncludeAttributes() { + return includeAttributes_; + } + /** + *
+       * Optional. Unset or true includes attributes. False returns object skeletons.
+       * 
+ * + * optional bool include_attributes = 10; + * @param value The includeAttributes to set. + * @return This builder for chaining. + */ + public Builder setIncludeAttributes(boolean value) { + + includeAttributes_ = value; + bitField0_ |= 0x00000200; + onChanged(); + return this; + } + /** + *
+       * Optional. Unset or true includes attributes. False returns object skeletons.
+       * 
+ * + * optional bool include_attributes = 10; + * @return This builder for chaining. + */ + public Builder clearIncludeAttributes() { + bitField0_ = (bitField0_ & ~0x00000200); + includeAttributes_ = false; + onChanged(); + return this; + } + + private boolean alarmBearingOnly_ ; + /** + *
+       * Optional. Return only objects with at least one alarm-bearing attribute.
+       * 
+ * + * bool alarm_bearing_only = 11; + * @return The alarmBearingOnly. + */ + @java.lang.Override + public boolean getAlarmBearingOnly() { + return alarmBearingOnly_; + } + /** + *
+       * Optional. Return only objects with at least one alarm-bearing attribute.
+       * 
+ * + * bool alarm_bearing_only = 11; + * @param value The alarmBearingOnly to set. + * @return This builder for chaining. + */ + public Builder setAlarmBearingOnly(boolean value) { + + alarmBearingOnly_ = value; + bitField0_ |= 0x00000400; + onChanged(); + return this; + } + /** + *
+       * Optional. Return only objects with at least one alarm-bearing attribute.
+       * 
+ * + * bool alarm_bearing_only = 11; + * @return This builder for chaining. + */ + public Builder clearAlarmBearingOnly() { + bitField0_ = (bitField0_ & ~0x00000400); + alarmBearingOnly_ = false; + onChanged(); + return this; + } + + private boolean historizedOnly_ ; + /** + *
+       * Optional. Return only objects with at least one historized attribute.
+       * 
+ * + * bool historized_only = 12; + * @return The historizedOnly. + */ + @java.lang.Override + public boolean getHistorizedOnly() { + return historizedOnly_; + } + /** + *
+       * Optional. Return only objects with at least one historized attribute.
+       * 
+ * + * bool historized_only = 12; + * @param value The historizedOnly to set. + * @return This builder for chaining. + */ + public Builder setHistorizedOnly(boolean value) { + + historizedOnly_ = value; + bitField0_ |= 0x00000800; + onChanged(); + return this; + } + /** + *
+       * Optional. Return only objects with at least one historized attribute.
+       * 
+ * + * bool historized_only = 12; + * @return This builder for chaining. + */ + public Builder clearHistorizedOnly() { + bitField0_ = (bitField0_ & ~0x00000800); + historizedOnly_ = false; + onChanged(); + return this; + } // @@protoc_insertion_point(builder_scope:galaxy_repository.v1.DiscoverHierarchyRequest) } @@ -2176,7 +4334,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera /** * repeated .galaxy_repository.v1.GalaxyObject objects = 1; */ - java.util.List + java.util.List getObjectsList(); /** * repeated .galaxy_repository.v1.GalaxyObject objects = 1; @@ -2189,13 +4347,43 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera /** * repeated .galaxy_repository.v1.GalaxyObject objects = 1; */ - java.util.List + java.util.List getObjectsOrBuilderList(); /** * repeated .galaxy_repository.v1.GalaxyObject objects = 1; */ galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObjectOrBuilder getObjectsOrBuilder( int index); + + /** + *
+     * Non-empty when another page is available.
+     * 
+ * + * string next_page_token = 2; + * @return The nextPageToken. + */ + java.lang.String getNextPageToken(); + /** + *
+     * Non-empty when another page is available.
+     * 
+ * + * string next_page_token = 2; + * @return The bytes for nextPageToken. + */ + com.google.protobuf.ByteString + getNextPageTokenBytes(); + + /** + *
+     * Total number of objects in the cached hierarchy at the time of the call.
+     * 
+ * + * int32 total_object_count = 3; + * @return The totalObjectCount. + */ + int getTotalObjectCount(); } /** * Protobuf type {@code galaxy_repository.v1.DiscoverHierarchyReply} @@ -2220,6 +4408,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera } private DiscoverHierarchyReply() { objects_ = java.util.Collections.emptyList(); + nextPageToken_ = ""; } public static final com.google.protobuf.Descriptors.Descriptor @@ -2249,7 +4438,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera * repeated .galaxy_repository.v1.GalaxyObject objects = 1; */ @java.lang.Override - public java.util.List + public java.util.List getObjectsOrBuilderList() { return objects_; } @@ -2276,6 +4465,68 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera return objects_.get(index); } + public static final int NEXT_PAGE_TOKEN_FIELD_NUMBER = 2; + @SuppressWarnings("serial") + private volatile java.lang.Object nextPageToken_ = ""; + /** + *
+     * Non-empty when another page is available.
+     * 
+ * + * string next_page_token = 2; + * @return The nextPageToken. + */ + @java.lang.Override + public java.lang.String getNextPageToken() { + java.lang.Object ref = nextPageToken_; + if (ref instanceof java.lang.String) { + return (java.lang.String) ref; + } else { + com.google.protobuf.ByteString bs = + (com.google.protobuf.ByteString) ref; + java.lang.String s = bs.toStringUtf8(); + nextPageToken_ = s; + return s; + } + } + /** + *
+     * Non-empty when another page is available.
+     * 
+ * + * string next_page_token = 2; + * @return The bytes for nextPageToken. + */ + @java.lang.Override + public com.google.protobuf.ByteString + getNextPageTokenBytes() { + java.lang.Object ref = nextPageToken_; + if (ref instanceof java.lang.String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8( + (java.lang.String) ref); + nextPageToken_ = b; + return b; + } else { + return (com.google.protobuf.ByteString) ref; + } + } + + public static final int TOTAL_OBJECT_COUNT_FIELD_NUMBER = 3; + private int totalObjectCount_ = 0; + /** + *
+     * Total number of objects in the cached hierarchy at the time of the call.
+     * 
+ * + * int32 total_object_count = 3; + * @return The totalObjectCount. + */ + @java.lang.Override + public int getTotalObjectCount() { + return totalObjectCount_; + } + private byte memoizedIsInitialized = -1; @java.lang.Override public final boolean isInitialized() { @@ -2293,6 +4544,12 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera for (int i = 0; i < objects_.size(); i++) { output.writeMessage(1, objects_.get(i)); } + if (!com.google.protobuf.GeneratedMessage.isStringEmpty(nextPageToken_)) { + com.google.protobuf.GeneratedMessage.writeString(output, 2, nextPageToken_); + } + if (totalObjectCount_ != 0) { + output.writeInt32(3, totalObjectCount_); + } getUnknownFields().writeTo(output); } @@ -2306,6 +4563,13 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera size += com.google.protobuf.CodedOutputStream .computeMessageSize(1, objects_.get(i)); } + if (!com.google.protobuf.GeneratedMessage.isStringEmpty(nextPageToken_)) { + size += com.google.protobuf.GeneratedMessage.computeStringSize(2, nextPageToken_); + } + if (totalObjectCount_ != 0) { + size += com.google.protobuf.CodedOutputStream + .computeInt32Size(3, totalObjectCount_); + } size += getUnknownFields().getSerializedSize(); memoizedSize = size; return size; @@ -2323,6 +4587,10 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera if (!getObjectsList() .equals(other.getObjectsList())) return false; + if (!getNextPageToken() + .equals(other.getNextPageToken())) return false; + if (getTotalObjectCount() + != other.getTotalObjectCount()) return false; if (!getUnknownFields().equals(other.getUnknownFields())) return false; return true; } @@ -2338,6 +4606,10 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera hash = (37 * hash) + OBJECTS_FIELD_NUMBER; hash = (53 * hash) + getObjectsList().hashCode(); } + hash = (37 * hash) + NEXT_PAGE_TOKEN_FIELD_NUMBER; + hash = (53 * hash) + getNextPageToken().hashCode(); + hash = (37 * hash) + TOTAL_OBJECT_COUNT_FIELD_NUMBER; + hash = (53 * hash) + getTotalObjectCount(); hash = (29 * hash) + getUnknownFields().hashCode(); memoizedHashCode = hash; return hash; @@ -2476,6 +4748,8 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera objectsBuilder_.clear(); } bitField0_ = (bitField0_ & ~0x00000001); + nextPageToken_ = ""; + totalObjectCount_ = 0; return this; } @@ -2522,6 +4796,12 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera private void buildPartial0(galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyReply result) { int from_bitField0_ = bitField0_; + if (((from_bitField0_ & 0x00000002) != 0)) { + result.nextPageToken_ = nextPageToken_; + } + if (((from_bitField0_ & 0x00000004) != 0)) { + result.totalObjectCount_ = totalObjectCount_; + } } @java.lang.Override @@ -2554,7 +4834,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera objectsBuilder_ = null; objects_ = other.objects_; bitField0_ = (bitField0_ & ~0x00000001); - objectsBuilder_ = + objectsBuilder_ = com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders ? internalGetObjectsFieldBuilder() : null; } else { @@ -2562,6 +4842,14 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera } } } + if (!other.getNextPageToken().isEmpty()) { + nextPageToken_ = other.nextPageToken_; + bitField0_ |= 0x00000002; + onChanged(); + } + if (other.getTotalObjectCount() != 0) { + setTotalObjectCount(other.getTotalObjectCount()); + } this.mergeUnknownFields(other.getUnknownFields()); onChanged(); return this; @@ -2601,6 +4889,16 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera } break; } // case 10 + case 18: { + nextPageToken_ = input.readStringRequireUtf8(); + bitField0_ |= 0x00000002; + break; + } // case 18 + case 24: { + totalObjectCount_ = input.readInt32(); + bitField0_ |= 0x00000004; + break; + } // case 24 default: { if (!super.parseUnknownField(input, extensionRegistry, tag)) { done = true; // was an endgroup tag @@ -2813,7 +5111,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera /** * repeated .galaxy_repository.v1.GalaxyObject objects = 1; */ - public java.util.List + public java.util.List getObjectsOrBuilderList() { if (objectsBuilder_ != null) { return objectsBuilder_.getMessageOrBuilderList(); @@ -2839,12 +5137,12 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera /** * repeated .galaxy_repository.v1.GalaxyObject objects = 1; */ - public java.util.List + public java.util.List getObjectsBuilderList() { return internalGetObjectsFieldBuilder().getBuilderList(); } private com.google.protobuf.RepeatedFieldBuilder< - galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject, galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject.Builder, galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObjectOrBuilder> + galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject, galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject.Builder, galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObjectOrBuilder> internalGetObjectsFieldBuilder() { if (objectsBuilder_ == null) { objectsBuilder_ = new com.google.protobuf.RepeatedFieldBuilder< @@ -2858,6 +5156,142 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera return objectsBuilder_; } + private java.lang.Object nextPageToken_ = ""; + /** + *
+       * Non-empty when another page is available.
+       * 
+ * + * string next_page_token = 2; + * @return The nextPageToken. + */ + public java.lang.String getNextPageToken() { + java.lang.Object ref = nextPageToken_; + if (!(ref instanceof java.lang.String)) { + com.google.protobuf.ByteString bs = + (com.google.protobuf.ByteString) ref; + java.lang.String s = bs.toStringUtf8(); + nextPageToken_ = s; + return s; + } else { + return (java.lang.String) ref; + } + } + /** + *
+       * Non-empty when another page is available.
+       * 
+ * + * string next_page_token = 2; + * @return The bytes for nextPageToken. + */ + public com.google.protobuf.ByteString + getNextPageTokenBytes() { + java.lang.Object ref = nextPageToken_; + if (ref instanceof String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8( + (java.lang.String) ref); + nextPageToken_ = b; + return b; + } else { + return (com.google.protobuf.ByteString) ref; + } + } + /** + *
+       * Non-empty when another page is available.
+       * 
+ * + * string next_page_token = 2; + * @param value The nextPageToken to set. + * @return This builder for chaining. + */ + public Builder setNextPageToken( + java.lang.String value) { + if (value == null) { throw new NullPointerException(); } + nextPageToken_ = value; + bitField0_ |= 0x00000002; + onChanged(); + return this; + } + /** + *
+       * Non-empty when another page is available.
+       * 
+ * + * string next_page_token = 2; + * @return This builder for chaining. + */ + public Builder clearNextPageToken() { + nextPageToken_ = getDefaultInstance().getNextPageToken(); + bitField0_ = (bitField0_ & ~0x00000002); + onChanged(); + return this; + } + /** + *
+       * Non-empty when another page is available.
+       * 
+ * + * string next_page_token = 2; + * @param value The bytes for nextPageToken to set. + * @return This builder for chaining. + */ + public Builder setNextPageTokenBytes( + com.google.protobuf.ByteString value) { + if (value == null) { throw new NullPointerException(); } + checkByteStringIsUtf8(value); + nextPageToken_ = value; + bitField0_ |= 0x00000002; + onChanged(); + return this; + } + + private int totalObjectCount_ ; + /** + *
+       * Total number of objects in the cached hierarchy at the time of the call.
+       * 
+ * + * int32 total_object_count = 3; + * @return The totalObjectCount. + */ + @java.lang.Override + public int getTotalObjectCount() { + return totalObjectCount_; + } + /** + *
+       * Total number of objects in the cached hierarchy at the time of the call.
+       * 
+ * + * int32 total_object_count = 3; + * @param value The totalObjectCount to set. + * @return This builder for chaining. + */ + public Builder setTotalObjectCount(int value) { + + totalObjectCount_ = value; + bitField0_ |= 0x00000004; + onChanged(); + return this; + } + /** + *
+       * Total number of objects in the cached hierarchy at the time of the call.
+       * 
+ * + * int32 total_object_count = 3; + * @return This builder for chaining. + */ + public Builder clearTotalObjectCount() { + bitField0_ = (bitField0_ & ~0x00000004); + totalObjectCount_ = 0; + onChanged(); + return this; + } + // @@protoc_insertion_point(builder_scope:galaxy_repository.v1.DiscoverHierarchyReply) } @@ -3490,7 +5924,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera * .google.protobuf.Timestamp last_seen_deploy_time = 1; */ private com.google.protobuf.SingleFieldBuilder< - com.google.protobuf.Timestamp, com.google.protobuf.Timestamp.Builder, com.google.protobuf.TimestampOrBuilder> + com.google.protobuf.Timestamp, com.google.protobuf.Timestamp.Builder, com.google.protobuf.TimestampOrBuilder> internalGetLastSeenDeployTimeFieldBuilder() { if (lastSeenDeployTimeBuilder_ == null) { lastSeenDeployTimeBuilder_ = new com.google.protobuf.SingleFieldBuilder< @@ -4437,7 +6871,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera * .google.protobuf.Timestamp observed_at = 2; */ private com.google.protobuf.SingleFieldBuilder< - com.google.protobuf.Timestamp, com.google.protobuf.Timestamp.Builder, com.google.protobuf.TimestampOrBuilder> + com.google.protobuf.Timestamp, com.google.protobuf.Timestamp.Builder, com.google.protobuf.TimestampOrBuilder> internalGetObservedAtFieldBuilder() { if (observedAtBuilder_ == null) { observedAtBuilder_ = new com.google.protobuf.SingleFieldBuilder< @@ -4594,7 +7028,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera * .google.protobuf.Timestamp time_of_last_deploy = 3; */ private com.google.protobuf.SingleFieldBuilder< - com.google.protobuf.Timestamp, com.google.protobuf.Timestamp.Builder, com.google.protobuf.TimestampOrBuilder> + com.google.protobuf.Timestamp, com.google.protobuf.Timestamp.Builder, com.google.protobuf.TimestampOrBuilder> internalGetTimeOfLastDeployFieldBuilder() { if (timeOfLastDeployBuilder_ == null) { timeOfLastDeployBuilder_ = new com.google.protobuf.SingleFieldBuilder< @@ -4852,7 +7286,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera /** * repeated .galaxy_repository.v1.GalaxyAttribute attributes = 10; */ - java.util.List + java.util.List getAttributesList(); /** * repeated .galaxy_repository.v1.GalaxyAttribute attributes = 10; @@ -4865,7 +7299,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera /** * repeated .galaxy_repository.v1.GalaxyAttribute attributes = 10; */ - java.util.List + java.util.List getAttributesOrBuilderList(); /** * repeated .galaxy_repository.v1.GalaxyAttribute attributes = 10; @@ -4940,7 +7374,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera if (ref instanceof java.lang.String) { return (java.lang.String) ref; } else { - com.google.protobuf.ByteString bs = + com.google.protobuf.ByteString bs = (com.google.protobuf.ByteString) ref; java.lang.String s = bs.toStringUtf8(); tagName_ = s; @@ -4956,7 +7390,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera getTagNameBytes() { java.lang.Object ref = tagName_; if (ref instanceof java.lang.String) { - com.google.protobuf.ByteString b = + com.google.protobuf.ByteString b = com.google.protobuf.ByteString.copyFromUtf8( (java.lang.String) ref); tagName_ = b; @@ -4979,7 +7413,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera if (ref instanceof java.lang.String) { return (java.lang.String) ref; } else { - com.google.protobuf.ByteString bs = + com.google.protobuf.ByteString bs = (com.google.protobuf.ByteString) ref; java.lang.String s = bs.toStringUtf8(); containedName_ = s; @@ -4995,7 +7429,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera getContainedNameBytes() { java.lang.Object ref = containedName_; if (ref instanceof java.lang.String) { - com.google.protobuf.ByteString b = + com.google.protobuf.ByteString b = com.google.protobuf.ByteString.copyFromUtf8( (java.lang.String) ref); containedName_ = b; @@ -5018,7 +7452,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera if (ref instanceof java.lang.String) { return (java.lang.String) ref; } else { - com.google.protobuf.ByteString bs = + com.google.protobuf.ByteString bs = (com.google.protobuf.ByteString) ref; java.lang.String s = bs.toStringUtf8(); browseName_ = s; @@ -5034,7 +7468,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera getBrowseNameBytes() { java.lang.Object ref = browseName_; if (ref instanceof java.lang.String) { - com.google.protobuf.ByteString b = + com.google.protobuf.ByteString b = com.google.protobuf.ByteString.copyFromUtf8( (java.lang.String) ref); browseName_ = b; @@ -5139,7 +7573,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera * repeated .galaxy_repository.v1.GalaxyAttribute attributes = 10; */ @java.lang.Override - public java.util.List + public java.util.List getAttributesOrBuilderList() { return attributes_; } @@ -5625,7 +8059,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera attributesBuilder_ = null; attributes_ = other.attributes_; bitField0_ = (bitField0_ & ~0x00000200); - attributesBuilder_ = + attributesBuilder_ = com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders ? internalGetAttributesFieldBuilder() : null; } else { @@ -5792,7 +8226,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera getTagNameBytes() { java.lang.Object ref = tagName_; if (ref instanceof String) { - com.google.protobuf.ByteString b = + com.google.protobuf.ByteString b = com.google.protobuf.ByteString.copyFromUtf8( (java.lang.String) ref); tagName_ = b; @@ -5864,7 +8298,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera getContainedNameBytes() { java.lang.Object ref = containedName_; if (ref instanceof String) { - com.google.protobuf.ByteString b = + com.google.protobuf.ByteString b = com.google.protobuf.ByteString.copyFromUtf8( (java.lang.String) ref); containedName_ = b; @@ -5936,7 +8370,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera getBrowseNameBytes() { java.lang.Object ref = browseName_; if (ref instanceof String) { - com.google.protobuf.ByteString b = + com.google.protobuf.ByteString b = com.google.protobuf.ByteString.copyFromUtf8( (java.lang.String) ref); browseName_ = b; @@ -6417,7 +8851,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera /** * repeated .galaxy_repository.v1.GalaxyAttribute attributes = 10; */ - public java.util.List + public java.util.List getAttributesOrBuilderList() { if (attributesBuilder_ != null) { return attributesBuilder_.getMessageOrBuilderList(); @@ -6443,12 +8877,12 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera /** * repeated .galaxy_repository.v1.GalaxyAttribute attributes = 10; */ - public java.util.List + public java.util.List getAttributesBuilderList() { return internalGetAttributesFieldBuilder().getBuilderList(); } private com.google.protobuf.RepeatedFieldBuilder< - galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute, galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute.Builder, galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttributeOrBuilder> + galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute, galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute.Builder, galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttributeOrBuilder> internalGetAttributesFieldBuilder() { if (attributesBuilder_ == null) { attributesBuilder_ = new com.google.protobuf.RepeatedFieldBuilder< @@ -6654,7 +9088,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera if (ref instanceof java.lang.String) { return (java.lang.String) ref; } else { - com.google.protobuf.ByteString bs = + com.google.protobuf.ByteString bs = (com.google.protobuf.ByteString) ref; java.lang.String s = bs.toStringUtf8(); attributeName_ = s; @@ -6670,7 +9104,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera getAttributeNameBytes() { java.lang.Object ref = attributeName_; if (ref instanceof java.lang.String) { - com.google.protobuf.ByteString b = + com.google.protobuf.ByteString b = com.google.protobuf.ByteString.copyFromUtf8( (java.lang.String) ref); attributeName_ = b; @@ -6693,7 +9127,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera if (ref instanceof java.lang.String) { return (java.lang.String) ref; } else { - com.google.protobuf.ByteString bs = + com.google.protobuf.ByteString bs = (com.google.protobuf.ByteString) ref; java.lang.String s = bs.toStringUtf8(); fullTagReference_ = s; @@ -6709,7 +9143,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera getFullTagReferenceBytes() { java.lang.Object ref = fullTagReference_; if (ref instanceof java.lang.String) { - com.google.protobuf.ByteString b = + com.google.protobuf.ByteString b = com.google.protobuf.ByteString.copyFromUtf8( (java.lang.String) ref); fullTagReference_ = b; @@ -6743,7 +9177,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera if (ref instanceof java.lang.String) { return (java.lang.String) ref; } else { - com.google.protobuf.ByteString bs = + com.google.protobuf.ByteString bs = (com.google.protobuf.ByteString) ref; java.lang.String s = bs.toStringUtf8(); dataTypeName_ = s; @@ -6759,7 +9193,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera getDataTypeNameBytes() { java.lang.Object ref = dataTypeName_; if (ref instanceof java.lang.String) { - com.google.protobuf.ByteString b = + com.google.protobuf.ByteString b = com.google.protobuf.ByteString.copyFromUtf8( (java.lang.String) ref); dataTypeName_ = b; @@ -7401,7 +9835,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera getAttributeNameBytes() { java.lang.Object ref = attributeName_; if (ref instanceof String) { - com.google.protobuf.ByteString b = + com.google.protobuf.ByteString b = com.google.protobuf.ByteString.copyFromUtf8( (java.lang.String) ref); attributeName_ = b; @@ -7473,7 +9907,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera getFullTagReferenceBytes() { java.lang.Object ref = fullTagReference_; if (ref instanceof String) { - com.google.protobuf.ByteString b = + com.google.protobuf.ByteString b = com.google.protobuf.ByteString.copyFromUtf8( (java.lang.String) ref); fullTagReference_ = b; @@ -7577,7 +10011,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera getDataTypeNameBytes() { java.lang.Object ref = dataTypeName_; if (ref instanceof String) { - com.google.protobuf.ByteString b = + com.google.protobuf.ByteString b = com.google.protobuf.ByteString.copyFromUtf8( (java.lang.String) ref); dataTypeName_ = b; @@ -7901,52 +10335,52 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera private static final com.google.protobuf.Descriptors.Descriptor internal_static_galaxy_repository_v1_TestConnectionRequest_descriptor; - private static final + private static final com.google.protobuf.GeneratedMessage.FieldAccessorTable internal_static_galaxy_repository_v1_TestConnectionRequest_fieldAccessorTable; private static final com.google.protobuf.Descriptors.Descriptor internal_static_galaxy_repository_v1_TestConnectionReply_descriptor; - private static final + private static final com.google.protobuf.GeneratedMessage.FieldAccessorTable internal_static_galaxy_repository_v1_TestConnectionReply_fieldAccessorTable; private static final com.google.protobuf.Descriptors.Descriptor internal_static_galaxy_repository_v1_GetLastDeployTimeRequest_descriptor; - private static final + private static final com.google.protobuf.GeneratedMessage.FieldAccessorTable internal_static_galaxy_repository_v1_GetLastDeployTimeRequest_fieldAccessorTable; private static final com.google.protobuf.Descriptors.Descriptor internal_static_galaxy_repository_v1_GetLastDeployTimeReply_descriptor; - private static final + private static final com.google.protobuf.GeneratedMessage.FieldAccessorTable internal_static_galaxy_repository_v1_GetLastDeployTimeReply_fieldAccessorTable; private static final com.google.protobuf.Descriptors.Descriptor internal_static_galaxy_repository_v1_DiscoverHierarchyRequest_descriptor; - private static final + private static final com.google.protobuf.GeneratedMessage.FieldAccessorTable internal_static_galaxy_repository_v1_DiscoverHierarchyRequest_fieldAccessorTable; private static final com.google.protobuf.Descriptors.Descriptor internal_static_galaxy_repository_v1_DiscoverHierarchyReply_descriptor; - private static final + private static final com.google.protobuf.GeneratedMessage.FieldAccessorTable internal_static_galaxy_repository_v1_DiscoverHierarchyReply_fieldAccessorTable; private static final com.google.protobuf.Descriptors.Descriptor internal_static_galaxy_repository_v1_WatchDeployEventsRequest_descriptor; - private static final + private static final com.google.protobuf.GeneratedMessage.FieldAccessorTable internal_static_galaxy_repository_v1_WatchDeployEventsRequest_fieldAccessorTable; private static final com.google.protobuf.Descriptors.Descriptor internal_static_galaxy_repository_v1_DeployEvent_descriptor; - private static final + private static final com.google.protobuf.GeneratedMessage.FieldAccessorTable internal_static_galaxy_repository_v1_DeployEvent_fieldAccessorTable; private static final com.google.protobuf.Descriptors.Descriptor internal_static_galaxy_repository_v1_GalaxyObject_descriptor; - private static final + private static final com.google.protobuf.GeneratedMessage.FieldAccessorTable internal_static_galaxy_repository_v1_GalaxyObject_fieldAccessorTable; private static final com.google.protobuf.Descriptors.Descriptor internal_static_galaxy_repository_v1_GalaxyAttribute_descriptor; - private static final + private static final com.google.protobuf.GeneratedMessage.FieldAccessorTable internal_static_galaxy_repository_v1_GalaxyAttribute_fieldAccessorTable; @@ -7960,54 +10394,66 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera java.lang.String[] descriptorData = { "\n\027galaxy_repository.proto\022\024galaxy_reposi" + "tory.v1\032\037google/protobuf/timestamp.proto" + - "\"\027\n\025TestConnectionRequest\"!\n\023TestConnect" + - "ionReply\022\n\n\002ok\030\001 \001(\010\"\032\n\030GetLastDeployTim" + - "eRequest\"b\n\026GetLastDeployTimeReply\022\017\n\007pr" + - "esent\030\001 \001(\010\0227\n\023time_of_last_deploy\030\002 \001(\013" + - "2\032.google.protobuf.Timestamp\"\032\n\030Discover" + - "HierarchyRequest\"M\n\026DiscoverHierarchyRep" + - "ly\0223\n\007objects\030\001 \003(\0132\".galaxy_repository." + - "v1.GalaxyObject\"U\n\030WatchDeployEventsRequ" + - "est\0229\n\025last_seen_deploy_time\030\001 \001(\0132\032.goo" + - "gle.protobuf.Timestamp\"\335\001\n\013DeployEvent\022\020" + - "\n\010sequence\030\001 \001(\004\022/\n\013observed_at\030\002 \001(\0132\032." + - "google.protobuf.Timestamp\0227\n\023time_of_las" + - "t_deploy\030\003 \001(\0132\032.google.protobuf.Timesta" + - "mp\022#\n\033time_of_last_deploy_present\030\004 \001(\010\022" + - "\024\n\014object_count\030\005 \001(\005\022\027\n\017attribute_count" + - "\030\006 \001(\005\"\223\002\n\014GalaxyObject\022\022\n\ngobject_id\030\001 " + - "\001(\005\022\020\n\010tag_name\030\002 \001(\t\022\026\n\016contained_name\030" + - "\003 \001(\t\022\023\n\013browse_name\030\004 \001(\t\022\031\n\021parent_gob" + - "ject_id\030\005 \001(\005\022\017\n\007is_area\030\006 \001(\010\022\023\n\013catego" + - "ry_id\030\007 \001(\005\022\034\n\024hosted_by_gobject_id\030\010 \001(" + - "\005\022\026\n\016template_chain\030\t \003(\t\0229\n\nattributes\030" + - "\n \003(\0132%.galaxy_repository.v1.GalaxyAttri" + - "bute\"\250\002\n\017GalaxyAttribute\022\026\n\016attribute_na" + - "me\030\001 \001(\t\022\032\n\022full_tag_reference\030\002 \001(\t\022\024\n\014" + - "mx_data_type\030\003 \001(\005\022\026\n\016data_type_name\030\004 \001" + - "(\t\022\020\n\010is_array\030\005 \001(\010\022\027\n\017array_dimension\030" + - "\006 \001(\005\022\037\n\027array_dimension_present\030\007 \001(\010\022\035" + - "\n\025mx_attribute_category\030\010 \001(\005\022\037\n\027securit" + - "y_classification\030\t \001(\005\022\025\n\ris_historized\030" + - "\n \001(\010\022\020\n\010is_alarm\030\013 \001(\0102\314\003\n\020GalaxyReposi" + - "tory\022h\n\016TestConnection\022+.galaxy_reposito" + - "ry.v1.TestConnectionRequest\032).galaxy_rep" + - "ository.v1.TestConnectionReply\022q\n\021GetLas" + - "tDeployTime\022..galaxy_repository.v1.GetLa" + - "stDeployTimeRequest\032,.galaxy_repository." + - "v1.GetLastDeployTimeReply\022q\n\021DiscoverHie" + - "rarchy\022..galaxy_repository.v1.DiscoverHi" + - "erarchyRequest\032,.galaxy_repository.v1.Di" + - "scoverHierarchyReply\022h\n\021WatchDeployEvent" + - "s\022..galaxy_repository.v1.WatchDeployEven" + - "tsRequest\032!.galaxy_repository.v1.DeployE" + - "vent0\001B#\252\002 MxGateway.Contracts.Proto.Gal" + - "axyb\006proto3" + "\032\036google/protobuf/wrappers.proto\"\027\n\025Test" + + "ConnectionRequest\"!\n\023TestConnectionReply" + + "\022\n\n\002ok\030\001 \001(\010\"\032\n\030GetLastDeployTimeRequest" + + "\"b\n\026GetLastDeployTimeReply\022\017\n\007present\030\001 " + + "\001(\010\0227\n\023time_of_last_deploy\030\002 \001(\0132\032.googl" + + "e.protobuf.Timestamp\"\207\003\n\030DiscoverHierarc" + + "hyRequest\022\021\n\tpage_size\030\001 \001(\005\022\022\n\npage_tok" + + "en\030\002 \001(\t\022\031\n\017root_gobject_id\030\003 \001(\005H\000\022\027\n\rr" + + "oot_tag_name\030\004 \001(\tH\000\022\035\n\023root_contained_p" + + "ath\030\005 \001(\tH\000\022.\n\tmax_depth\030\006 \001(\0132\033.google." + + "protobuf.Int32Value\022\024\n\014category_ids\030\007 \003(" + + "\005\022\037\n\027template_chain_contains\030\010 \003(\t\022\025\n\rta" + + "g_name_glob\030\t \001(\t\022\037\n\022include_attributes\030" + + "\n \001(\010H\001\210\001\001\022\032\n\022alarm_bearing_only\030\013 \001(\010\022\027" + + "\n\017historized_only\030\014 \001(\010B\006\n\004rootB\025\n\023_incl" + + "ude_attributes\"\202\001\n\026DiscoverHierarchyRepl" + + "y\0223\n\007objects\030\001 \003(\0132\".galaxy_repository.v" + + "1.GalaxyObject\022\027\n\017next_page_token\030\002 \001(\t\022" + + "\032\n\022total_object_count\030\003 \001(\005\"U\n\030WatchDepl" + + "oyEventsRequest\0229\n\025last_seen_deploy_time" + + "\030\001 \001(\0132\032.google.protobuf.Timestamp\"\335\001\n\013D" + + "eployEvent\022\020\n\010sequence\030\001 \001(\004\022/\n\013observed" + + "_at\030\002 \001(\0132\032.google.protobuf.Timestamp\0227\n" + + "\023time_of_last_deploy\030\003 \001(\0132\032.google.prot" + + "obuf.Timestamp\022#\n\033time_of_last_deploy_pr" + + "esent\030\004 \001(\010\022\024\n\014object_count\030\005 \001(\005\022\027\n\017att" + + "ribute_count\030\006 \001(\005\"\223\002\n\014GalaxyObject\022\022\n\ng" + + "object_id\030\001 \001(\005\022\020\n\010tag_name\030\002 \001(\t\022\026\n\016con" + + "tained_name\030\003 \001(\t\022\023\n\013browse_name\030\004 \001(\t\022\031" + + "\n\021parent_gobject_id\030\005 \001(\005\022\017\n\007is_area\030\006 \001" + + "(\010\022\023\n\013category_id\030\007 \001(\005\022\034\n\024hosted_by_gob" + + "ject_id\030\010 \001(\005\022\026\n\016template_chain\030\t \003(\t\0229\n" + + "\nattributes\030\n \003(\0132%.galaxy_repository.v1" + + ".GalaxyAttribute\"\250\002\n\017GalaxyAttribute\022\026\n\016" + + "attribute_name\030\001 \001(\t\022\032\n\022full_tag_referen" + + "ce\030\002 \001(\t\022\024\n\014mx_data_type\030\003 \001(\005\022\026\n\016data_t" + + "ype_name\030\004 \001(\t\022\020\n\010is_array\030\005 \001(\010\022\027\n\017arra" + + "y_dimension\030\006 \001(\005\022\037\n\027array_dimension_pre" + + "sent\030\007 \001(\010\022\035\n\025mx_attribute_category\030\010 \001(" + + "\005\022\037\n\027security_classification\030\t \001(\005\022\025\n\ris" + + "_historized\030\n \001(\010\022\020\n\010is_alarm\030\013 \001(\0102\314\003\n\020" + + "GalaxyRepository\022h\n\016TestConnection\022+.gal" + + "axy_repository.v1.TestConnectionRequest\032" + + ").galaxy_repository.v1.TestConnectionRep" + + "ly\022q\n\021GetLastDeployTime\022..galaxy_reposit" + + "ory.v1.GetLastDeployTimeRequest\032,.galaxy" + + "_repository.v1.GetLastDeployTimeReply\022q\n" + + "\021DiscoverHierarchy\022..galaxy_repository.v" + + "1.DiscoverHierarchyRequest\032,.galaxy_repo" + + "sitory.v1.DiscoverHierarchyReply\022h\n\021Watc" + + "hDeployEvents\022..galaxy_repository.v1.Wat" + + "chDeployEventsRequest\032!.galaxy_repositor" + + "y.v1.DeployEvent0\001B#\252\002 MxGateway.Contrac" + + "ts.Proto.Galaxyb\006proto3" }; descriptor = com.google.protobuf.Descriptors.FileDescriptor .internalBuildGeneratedFileFrom(descriptorData, new com.google.protobuf.Descriptors.FileDescriptor[] { com.google.protobuf.TimestampProto.getDescriptor(), + com.google.protobuf.WrappersProto.getDescriptor(), }); internal_static_galaxy_repository_v1_TestConnectionRequest_descriptor = getDescriptor().getMessageType(0); @@ -8038,13 +10484,13 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera internal_static_galaxy_repository_v1_DiscoverHierarchyRequest_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_galaxy_repository_v1_DiscoverHierarchyRequest_descriptor, - new java.lang.String[] { }); + new java.lang.String[] { "PageSize", "PageToken", "RootGobjectId", "RootTagName", "RootContainedPath", "MaxDepth", "CategoryIds", "TemplateChainContains", "TagNameGlob", "IncludeAttributes", "AlarmBearingOnly", "HistorizedOnly", "Root", }); internal_static_galaxy_repository_v1_DiscoverHierarchyReply_descriptor = getDescriptor().getMessageType(5); internal_static_galaxy_repository_v1_DiscoverHierarchyReply_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_galaxy_repository_v1_DiscoverHierarchyReply_descriptor, - new java.lang.String[] { "Objects", }); + new java.lang.String[] { "Objects", "NextPageToken", "TotalObjectCount", }); internal_static_galaxy_repository_v1_WatchDeployEventsRequest_descriptor = getDescriptor().getMessageType(6); internal_static_galaxy_repository_v1_WatchDeployEventsRequest_fieldAccessorTable = new @@ -8071,6 +10517,7 @@ public final class GalaxyRepositoryOuterClass extends com.google.protobuf.Genera new java.lang.String[] { "AttributeName", "FullTagReference", "MxDataType", "DataTypeName", "IsArray", "ArrayDimension", "ArrayDimensionPresent", "MxAttributeCategory", "SecurityClassification", "IsHistorized", "IsAlarm", }); descriptor.resolveAllFeaturesImmutable(); com.google.protobuf.TimestampProto.getDescriptor(); + com.google.protobuf.WrappersProto.getDescriptor(); } // @@protoc_insertion_point(outer_class_scope) diff --git a/clients/proto/descriptors/mxaccessgw-client-v1.protoset b/clients/proto/descriptors/mxaccessgw-client-v1.protoset index f04fccc..09a9e6e 100644 Binary files a/clients/proto/descriptors/mxaccessgw-client-v1.protoset and b/clients/proto/descriptors/mxaccessgw-client-v1.protoset differ diff --git a/clients/proto/fixtures/behavior/manifest.json b/clients/proto/fixtures/behavior/manifest.json index 7585f94..ff802a0 100644 --- a/clients/proto/fixtures/behavior/manifest.json +++ b/clients/proto/fixtures/behavior/manifest.json @@ -2,7 +2,7 @@ "schemaVersion": 1, "fixtureSet": "mxaccess-gateway-client-behavior", "contractName": "mxaccess-gateway", - "gatewayProtocolVersion": 1, + "gatewayProtocolVersion": 2, "workerProtocolVersion": 1, "protoInputManifest": "clients/proto/proto-inputs.json", "fixtures": [ diff --git a/clients/proto/fixtures/golden/open-session-reply.ok.json b/clients/proto/fixtures/golden/open-session-reply.ok.json index 8d5bb49..e242b6d 100644 --- a/clients/proto/fixtures/golden/open-session-reply.ok.json +++ b/clients/proto/fixtures/golden/open-session-reply.ok.json @@ -3,7 +3,7 @@ "backendName": "mxaccess-worker", "workerProcessId": 1234, "workerProtocolVersion": 1, - "gatewayProtocolVersion": 1, + "gatewayProtocolVersion": 2, "capabilities": [ "unary-open-session", "unary-close-session", diff --git a/clients/proto/fixtures/parity/parity-fixture-matrix.json b/clients/proto/fixtures/parity/parity-fixture-matrix.json index 14b9fd0..e4f0a5e 100644 --- a/clients/proto/fixtures/parity/parity-fixture-matrix.json +++ b/clients/proto/fixtures/parity/parity-fixture-matrix.json @@ -2,7 +2,7 @@ "schemaVersion": 1, "fixtureSet": "mxaccess-gateway-parity-fixture-matrix", "contractName": "mxaccess-gateway", - "gatewayProtocolVersion": 1, + "gatewayProtocolVersion": 2, "workerProtocolVersion": 1, "sourceCaptureRoot": "C:/Users/dohertj2/Desktop/mxaccess/captures", "sourceDocs": [ diff --git a/clients/proto/proto-inputs.json b/clients/proto/proto-inputs.json index c349f63..9d424b0 100644 --- a/clients/proto/proto-inputs.json +++ b/clients/proto/proto-inputs.json @@ -1,7 +1,7 @@ { "schemaVersion": 1, "contractName": "mxaccess-gateway", - "gatewayProtocolVersion": 1, + "gatewayProtocolVersion": 2, "workerProtocolVersion": 1, "protoRoot": "src/MxGateway.Contracts/Protos", "sourceFiles": [ diff --git a/clients/python/src/mxgateway/galaxy.py b/clients/python/src/mxgateway/galaxy.py index 8ad6617..09069f7 100644 --- a/clients/python/src/mxgateway/galaxy.py +++ b/clients/python/src/mxgateway/galaxy.py @@ -18,11 +18,13 @@ import grpc from google.protobuf.timestamp_pb2 import Timestamp from .auth import merge_metadata -from .errors import map_rpc_error +from .errors import MxGatewayError, map_rpc_error from .generated import galaxy_repository_pb2 as galaxy_pb from .generated import galaxy_repository_pb2_grpc as galaxy_pb_grpc from .options import ClientOptions, create_channel +_DISCOVER_HIERARCHY_PAGE_SIZE = 5000 + class GalaxyRepositoryClient: """Async client for the Galaxy Repository gRPC service.""" @@ -115,12 +117,27 @@ class GalaxyRepositoryClient: async def discover_hierarchy(self) -> list[galaxy_pb.GalaxyObject]: """Return the deployed Galaxy object hierarchy as raw proto messages.""" - reply = await self._unary( - "discover hierarchy", - self.raw_stub.DiscoverHierarchy, - galaxy_pb.DiscoverHierarchyRequest(), - ) - return list(reply.objects) + objects: list[galaxy_pb.GalaxyObject] = [] + seen_page_tokens: set[str] = set() + page_token = "" + while True: + reply = await self._unary( + "discover hierarchy", + self.raw_stub.DiscoverHierarchy, + galaxy_pb.DiscoverHierarchyRequest( + page_size=_DISCOVER_HIERARCHY_PAGE_SIZE, + page_token=page_token, + ), + ) + objects.extend(reply.objects) + page_token = reply.next_page_token + if not page_token: + return objects + if page_token in seen_page_tokens: + raise MxGatewayError( + f"galaxy discover hierarchy returned repeated page token {page_token!r}" + ) + seen_page_tokens.add(page_token) def watch_deploy_events( self, diff --git a/clients/python/src/mxgateway/generated/galaxy_repository_pb2.py b/clients/python/src/mxgateway/generated/galaxy_repository_pb2.py index 8abaf20..a15dfdf 100644 --- a/clients/python/src/mxgateway/generated/galaxy_repository_pb2.py +++ b/clients/python/src/mxgateway/generated/galaxy_repository_pb2.py @@ -23,9 +23,10 @@ _sym_db = _symbol_database.Default() from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 +from google.protobuf import wrappers_pb2 as google_dot_protobuf_dot_wrappers__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x17galaxy_repository.proto\x12\x14galaxy_repository.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"\x17\n\x15TestConnectionRequest\"!\n\x13TestConnectionReply\x12\n\n\x02ok\x18\x01 \x01(\x08\"\x1a\n\x18GetLastDeployTimeRequest\"b\n\x16GetLastDeployTimeReply\x12\x0f\n\x07present\x18\x01 \x01(\x08\x12\x37\n\x13time_of_last_deploy\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\x1a\n\x18\x44iscoverHierarchyRequest\"M\n\x16\x44iscoverHierarchyReply\x12\x33\n\x07objects\x18\x01 \x03(\x0b\x32\".galaxy_repository.v1.GalaxyObject\"U\n\x18WatchDeployEventsRequest\x12\x39\n\x15last_seen_deploy_time\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\xdd\x01\n\x0b\x44\x65ployEvent\x12\x10\n\x08sequence\x18\x01 \x01(\x04\x12/\n\x0bobserved_at\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x37\n\x13time_of_last_deploy\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12#\n\x1btime_of_last_deploy_present\x18\x04 \x01(\x08\x12\x14\n\x0cobject_count\x18\x05 \x01(\x05\x12\x17\n\x0f\x61ttribute_count\x18\x06 \x01(\x05\"\x93\x02\n\x0cGalaxyObject\x12\x12\n\ngobject_id\x18\x01 \x01(\x05\x12\x10\n\x08tag_name\x18\x02 \x01(\t\x12\x16\n\x0e\x63ontained_name\x18\x03 \x01(\t\x12\x13\n\x0b\x62rowse_name\x18\x04 \x01(\t\x12\x19\n\x11parent_gobject_id\x18\x05 \x01(\x05\x12\x0f\n\x07is_area\x18\x06 \x01(\x08\x12\x13\n\x0b\x63\x61tegory_id\x18\x07 \x01(\x05\x12\x1c\n\x14hosted_by_gobject_id\x18\x08 \x01(\x05\x12\x16\n\x0etemplate_chain\x18\t \x03(\t\x12\x39\n\nattributes\x18\n \x03(\x0b\x32%.galaxy_repository.v1.GalaxyAttribute\"\xa8\x02\n\x0fGalaxyAttribute\x12\x16\n\x0e\x61ttribute_name\x18\x01 \x01(\t\x12\x1a\n\x12\x66ull_tag_reference\x18\x02 \x01(\t\x12\x14\n\x0cmx_data_type\x18\x03 \x01(\x05\x12\x16\n\x0e\x64\x61ta_type_name\x18\x04 \x01(\t\x12\x10\n\x08is_array\x18\x05 \x01(\x08\x12\x17\n\x0f\x61rray_dimension\x18\x06 \x01(\x05\x12\x1f\n\x17\x61rray_dimension_present\x18\x07 \x01(\x08\x12\x1d\n\x15mx_attribute_category\x18\x08 \x01(\x05\x12\x1f\n\x17security_classification\x18\t \x01(\x05\x12\x15\n\ris_historized\x18\n \x01(\x08\x12\x10\n\x08is_alarm\x18\x0b \x01(\x08\x32\xcc\x03\n\x10GalaxyRepository\x12h\n\x0eTestConnection\x12+.galaxy_repository.v1.TestConnectionRequest\x1a).galaxy_repository.v1.TestConnectionReply\x12q\n\x11GetLastDeployTime\x12..galaxy_repository.v1.GetLastDeployTimeRequest\x1a,.galaxy_repository.v1.GetLastDeployTimeReply\x12q\n\x11\x44iscoverHierarchy\x12..galaxy_repository.v1.DiscoverHierarchyRequest\x1a,.galaxy_repository.v1.DiscoverHierarchyReply\x12h\n\x11WatchDeployEvents\x12..galaxy_repository.v1.WatchDeployEventsRequest\x1a!.galaxy_repository.v1.DeployEvent0\x01\x42#\xaa\x02 MxGateway.Contracts.Proto.Galaxyb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x17galaxy_repository.proto\x12\x14galaxy_repository.v1\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1egoogle/protobuf/wrappers.proto\"\x17\n\x15TestConnectionRequest\"!\n\x13TestConnectionReply\x12\n\n\x02ok\x18\x01 \x01(\x08\"\x1a\n\x18GetLastDeployTimeRequest\"b\n\x16GetLastDeployTimeReply\x12\x0f\n\x07present\x18\x01 \x01(\x08\x12\x37\n\x13time_of_last_deploy\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\x87\x03\n\x18\x44iscoverHierarchyRequest\x12\x11\n\tpage_size\x18\x01 \x01(\x05\x12\x12\n\npage_token\x18\x02 \x01(\t\x12\x19\n\x0froot_gobject_id\x18\x03 \x01(\x05H\x00\x12\x17\n\rroot_tag_name\x18\x04 \x01(\tH\x00\x12\x1d\n\x13root_contained_path\x18\x05 \x01(\tH\x00\x12.\n\tmax_depth\x18\x06 \x01(\x0b\x32\x1b.google.protobuf.Int32Value\x12\x14\n\x0c\x63\x61tegory_ids\x18\x07 \x03(\x05\x12\x1f\n\x17template_chain_contains\x18\x08 \x03(\t\x12\x15\n\rtag_name_glob\x18\t \x01(\t\x12\x1f\n\x12include_attributes\x18\n \x01(\x08H\x01\x88\x01\x01\x12\x1a\n\x12\x61larm_bearing_only\x18\x0b \x01(\x08\x12\x17\n\x0fhistorized_only\x18\x0c \x01(\x08\x42\x06\n\x04rootB\x15\n\x13_include_attributes\"\x82\x01\n\x16\x44iscoverHierarchyReply\x12\x33\n\x07objects\x18\x01 \x03(\x0b\x32\".galaxy_repository.v1.GalaxyObject\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\t\x12\x1a\n\x12total_object_count\x18\x03 \x01(\x05\"U\n\x18WatchDeployEventsRequest\x12\x39\n\x15last_seen_deploy_time\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\xdd\x01\n\x0b\x44\x65ployEvent\x12\x10\n\x08sequence\x18\x01 \x01(\x04\x12/\n\x0bobserved_at\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x37\n\x13time_of_last_deploy\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12#\n\x1btime_of_last_deploy_present\x18\x04 \x01(\x08\x12\x14\n\x0cobject_count\x18\x05 \x01(\x05\x12\x17\n\x0f\x61ttribute_count\x18\x06 \x01(\x05\"\x93\x02\n\x0cGalaxyObject\x12\x12\n\ngobject_id\x18\x01 \x01(\x05\x12\x10\n\x08tag_name\x18\x02 \x01(\t\x12\x16\n\x0e\x63ontained_name\x18\x03 \x01(\t\x12\x13\n\x0b\x62rowse_name\x18\x04 \x01(\t\x12\x19\n\x11parent_gobject_id\x18\x05 \x01(\x05\x12\x0f\n\x07is_area\x18\x06 \x01(\x08\x12\x13\n\x0b\x63\x61tegory_id\x18\x07 \x01(\x05\x12\x1c\n\x14hosted_by_gobject_id\x18\x08 \x01(\x05\x12\x16\n\x0etemplate_chain\x18\t \x03(\t\x12\x39\n\nattributes\x18\n \x03(\x0b\x32%.galaxy_repository.v1.GalaxyAttribute\"\xa8\x02\n\x0fGalaxyAttribute\x12\x16\n\x0e\x61ttribute_name\x18\x01 \x01(\t\x12\x1a\n\x12\x66ull_tag_reference\x18\x02 \x01(\t\x12\x14\n\x0cmx_data_type\x18\x03 \x01(\x05\x12\x16\n\x0e\x64\x61ta_type_name\x18\x04 \x01(\t\x12\x10\n\x08is_array\x18\x05 \x01(\x08\x12\x17\n\x0f\x61rray_dimension\x18\x06 \x01(\x05\x12\x1f\n\x17\x61rray_dimension_present\x18\x07 \x01(\x08\x12\x1d\n\x15mx_attribute_category\x18\x08 \x01(\x05\x12\x1f\n\x17security_classification\x18\t \x01(\x05\x12\x15\n\ris_historized\x18\n \x01(\x08\x12\x10\n\x08is_alarm\x18\x0b \x01(\x08\x32\xcc\x03\n\x10GalaxyRepository\x12h\n\x0eTestConnection\x12+.galaxy_repository.v1.TestConnectionRequest\x1a).galaxy_repository.v1.TestConnectionReply\x12q\n\x11GetLastDeployTime\x12..galaxy_repository.v1.GetLastDeployTimeRequest\x1a,.galaxy_repository.v1.GetLastDeployTimeReply\x12q\n\x11\x44iscoverHierarchy\x12..galaxy_repository.v1.DiscoverHierarchyRequest\x1a,.galaxy_repository.v1.DiscoverHierarchyReply\x12h\n\x11WatchDeployEvents\x12..galaxy_repository.v1.WatchDeployEventsRequest\x1a!.galaxy_repository.v1.DeployEvent0\x01\x42#\xaa\x02 MxGateway.Contracts.Proto.Galaxyb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -33,26 +34,26 @@ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'galaxy_repository_pb2', _gl if not _descriptor._USE_C_DESCRIPTORS: _globals['DESCRIPTOR']._loaded_options = None _globals['DESCRIPTOR']._serialized_options = b'\252\002 MxGateway.Contracts.Proto.Galaxy' - _globals['_TESTCONNECTIONREQUEST']._serialized_start=82 - _globals['_TESTCONNECTIONREQUEST']._serialized_end=105 - _globals['_TESTCONNECTIONREPLY']._serialized_start=107 - _globals['_TESTCONNECTIONREPLY']._serialized_end=140 - _globals['_GETLASTDEPLOYTIMEREQUEST']._serialized_start=142 - _globals['_GETLASTDEPLOYTIMEREQUEST']._serialized_end=168 - _globals['_GETLASTDEPLOYTIMEREPLY']._serialized_start=170 - _globals['_GETLASTDEPLOYTIMEREPLY']._serialized_end=268 - _globals['_DISCOVERHIERARCHYREQUEST']._serialized_start=270 - _globals['_DISCOVERHIERARCHYREQUEST']._serialized_end=296 - _globals['_DISCOVERHIERARCHYREPLY']._serialized_start=298 - _globals['_DISCOVERHIERARCHYREPLY']._serialized_end=375 - _globals['_WATCHDEPLOYEVENTSREQUEST']._serialized_start=377 - _globals['_WATCHDEPLOYEVENTSREQUEST']._serialized_end=462 - _globals['_DEPLOYEVENT']._serialized_start=465 - _globals['_DEPLOYEVENT']._serialized_end=686 - _globals['_GALAXYOBJECT']._serialized_start=689 - _globals['_GALAXYOBJECT']._serialized_end=964 - _globals['_GALAXYATTRIBUTE']._serialized_start=967 - _globals['_GALAXYATTRIBUTE']._serialized_end=1263 - _globals['_GALAXYREPOSITORY']._serialized_start=1266 - _globals['_GALAXYREPOSITORY']._serialized_end=1726 + _globals['_TESTCONNECTIONREQUEST']._serialized_start=114 + _globals['_TESTCONNECTIONREQUEST']._serialized_end=137 + _globals['_TESTCONNECTIONREPLY']._serialized_start=139 + _globals['_TESTCONNECTIONREPLY']._serialized_end=172 + _globals['_GETLASTDEPLOYTIMEREQUEST']._serialized_start=174 + _globals['_GETLASTDEPLOYTIMEREQUEST']._serialized_end=200 + _globals['_GETLASTDEPLOYTIMEREPLY']._serialized_start=202 + _globals['_GETLASTDEPLOYTIMEREPLY']._serialized_end=300 + _globals['_DISCOVERHIERARCHYREQUEST']._serialized_start=303 + _globals['_DISCOVERHIERARCHYREQUEST']._serialized_end=694 + _globals['_DISCOVERHIERARCHYREPLY']._serialized_start=697 + _globals['_DISCOVERHIERARCHYREPLY']._serialized_end=827 + _globals['_WATCHDEPLOYEVENTSREQUEST']._serialized_start=829 + _globals['_WATCHDEPLOYEVENTSREQUEST']._serialized_end=914 + _globals['_DEPLOYEVENT']._serialized_start=917 + _globals['_DEPLOYEVENT']._serialized_end=1138 + _globals['_GALAXYOBJECT']._serialized_start=1141 + _globals['_GALAXYOBJECT']._serialized_end=1416 + _globals['_GALAXYATTRIBUTE']._serialized_start=1419 + _globals['_GALAXYATTRIBUTE']._serialized_end=1715 + _globals['_GALAXYREPOSITORY']._serialized_start=1718 + _globals['_GALAXYREPOSITORY']._serialized_end=2178 # @@protoc_insertion_point(module_scope) diff --git a/clients/python/src/mxgateway/options.py b/clients/python/src/mxgateway/options.py index c0577d1..59fe092 100644 --- a/clients/python/src/mxgateway/options.py +++ b/clients/python/src/mxgateway/options.py @@ -21,6 +21,7 @@ class ClientOptions: server_name_override: str | None = None call_timeout: float | None = 30.0 stream_timeout: float | None = None + max_grpc_message_bytes: int = 16 * 1024 * 1024 def __post_init__(self) -> None: """Validate options; raise `ValueError` for invalid combinations.""" @@ -33,6 +34,8 @@ class ClientOptions: raise ValueError("call_timeout must be greater than zero") if self.stream_timeout is not None and self.stream_timeout <= 0: raise ValueError("stream_timeout must be greater than zero") + if self.max_grpc_message_bytes <= 0: + raise ValueError("max_grpc_message_bytes must be greater than zero") def __repr__(self) -> str: """Return a repr that redacts the API key value.""" @@ -43,14 +46,18 @@ class ClientOptions: f"ca_file={self.ca_file!r}, " f"server_name_override={self.server_name_override!r}, " f"call_timeout={self.call_timeout!r}, " - f"stream_timeout={self.stream_timeout!r})" + f"stream_timeout={self.stream_timeout!r}, " + f"max_grpc_message_bytes={self.max_grpc_message_bytes!r})" ) def create_channel(options: ClientOptions) -> grpc.aio.Channel: """Create a plaintext or TLS `grpc.aio` channel from client options.""" - channel_options: list[tuple[str, str]] = [] + channel_options: list[tuple[str, str | int]] = [ + ("grpc.max_receive_message_length", options.max_grpc_message_bytes), + ("grpc.max_send_message_length", options.max_grpc_message_bytes), + ] if options.server_name_override: channel_options.append(("grpc.ssl_target_name_override", options.server_name_override)) diff --git a/clients/python/tests/test_auth_options.py b/clients/python/tests/test_auth_options.py index 522e017..34bd2bf 100644 --- a/clients/python/tests/test_auth_options.py +++ b/clients/python/tests/test_auth_options.py @@ -61,7 +61,15 @@ def test_create_channel_uses_plaintext_channel(monkeypatch: pytest.MonkeyPatch) channel = create_channel(ClientOptions(endpoint="localhost:5000", plaintext=True)) assert channel == "plain-channel" - assert calls == [("localhost:5000", [])] + assert calls == [ + ( + "localhost:5000", + [ + ("grpc.max_receive_message_length", 16 * 1024 * 1024), + ("grpc.max_send_message_length", 16 * 1024 * 1024), + ], + ), + ] def test_create_channel_uses_tls_channel(monkeypatch: pytest.MonkeyPatch) -> None: @@ -95,9 +103,13 @@ def test_create_channel_uses_tls_channel(monkeypatch: pytest.MonkeyPatch) -> Non assert channel == "tls-channel" assert calls == [ - ( - "gateway.example:5001", - "creds", - [("grpc.ssl_target_name_override", "gateway.test")], - ), - ] + ( + "gateway.example:5001", + "creds", + [ + ("grpc.max_receive_message_length", 16 * 1024 * 1024), + ("grpc.max_send_message_length", 16 * 1024 * 1024), + ("grpc.ssl_target_name_override", "gateway.test"), + ], + ), + ] diff --git a/clients/python/tests/test_galaxy.py b/clients/python/tests/test_galaxy.py index f176195..ee58f55 100644 --- a/clients/python/tests/test_galaxy.py +++ b/clients/python/tests/test_galaxy.py @@ -98,6 +98,8 @@ async def test_discover_hierarchy_returns_proto_objects() -> None: stub = FakeGalaxyStub() stub.discover_hierarchy.replies = [ galaxy_pb.DiscoverHierarchyReply( + next_page_token="page-2", + total_object_count=2, objects=[ galaxy_pb.GalaxyObject( gobject_id=1, @@ -106,6 +108,11 @@ async def test_discover_hierarchy_returns_proto_objects() -> None: browse_name="TestMachine_001", is_area=True, ), + ], + ), + galaxy_pb.DiscoverHierarchyReply( + total_object_count=2, + objects=[ galaxy_pb.GalaxyObject( gobject_id=2, tag_name="DelmiaReceiver_001", @@ -133,10 +140,30 @@ async def test_discover_hierarchy_returns_proto_objects() -> None: assert isinstance(objects, list) assert len(objects) == 2 + assert len(stub.discover_hierarchy.requests) == 2 + assert stub.discover_hierarchy.requests[0].page_size == 5000 + assert stub.discover_hierarchy.requests[0].page_token == "" + assert stub.discover_hierarchy.requests[1].page_token == "page-2" assert objects[0].tag_name == "TestMachine_001" assert objects[1].attributes[0].full_tag_reference == "DelmiaReceiver_001.DownloadPath" +@pytest.mark.asyncio +async def test_discover_hierarchy_rejects_repeated_page_token() -> None: + stub = FakeGalaxyStub() + stub.discover_hierarchy.replies = [ + galaxy_pb.DiscoverHierarchyReply(next_page_token="7:1"), + galaxy_pb.DiscoverHierarchyReply(next_page_token="7:1"), + ] + client = await GalaxyRepositoryClient.connect( + ClientOptions(endpoint="fake", plaintext=True), + stub=stub, + ) + + with pytest.raises(Exception, match="repeated page token"): + await client.discover_hierarchy() + + @pytest.mark.asyncio async def test_watch_deploy_events_yields_events_in_order() -> None: ts1 = Timestamp() diff --git a/clients/rust/crates/mxgw-cli/src/main.rs b/clients/rust/crates/mxgw-cli/src/main.rs index 667e058..25815cf 100644 --- a/clients/rust/crates/mxgw-cli/src/main.rs +++ b/clients/rust/crates/mxgw-cli/src/main.rs @@ -1048,7 +1048,7 @@ mod tests { fn version_json_output_has_protocol_versions() { let value = super::version_json(); - assert_eq!(value["gatewayProtocolVersion"], 1); + assert_eq!(value["gatewayProtocolVersion"], 2); assert_eq!(value["workerProtocolVersion"], 1); } diff --git a/clients/rust/src/client.rs b/clients/rust/src/client.rs index c59a975..219b897 100644 --- a/clients/rust/src/client.rs +++ b/clients/rust/src/client.rs @@ -79,9 +79,12 @@ impl GatewayClient { let channel = endpoint.connect().await?; let interceptor = AuthInterceptor::new(options.api_key().cloned()); + let max_grpc_message_bytes = options.max_grpc_message_bytes(); Ok(Self { - inner: MxAccessGatewayClient::with_interceptor(channel, interceptor), + inner: MxAccessGatewayClient::with_interceptor(channel, interceptor) + .max_decoding_message_size(max_grpc_message_bytes) + .max_encoding_message_size(max_grpc_message_bytes), call_timeout: options.call_timeout(), stream_timeout: options.stream_timeout(), }) diff --git a/clients/rust/src/galaxy.rs b/clients/rust/src/galaxy.rs index df3cdf0..4349f03 100644 --- a/clients/rust/src/galaxy.rs +++ b/clients/rust/src/galaxy.rs @@ -21,6 +21,8 @@ use crate::generated::galaxy_repository::v1::{ }; use crate::options::ClientOptions; +const DISCOVER_HIERARCHY_PAGE_SIZE: i32 = 5000; + /// Convenience alias for the generated Galaxy client wrapped in the /// authentication interceptor. pub type RawGalaxyClient = GalaxyRepositoryClient>; @@ -77,9 +79,12 @@ impl GalaxyClient { let channel = endpoint.connect().await?; let interceptor = AuthInterceptor::new(options.api_key().cloned()); + let max_grpc_message_bytes = options.max_grpc_message_bytes(); Ok(Self { - inner: GalaxyRepositoryClient::with_interceptor(channel, interceptor), + inner: GalaxyRepositoryClient::with_interceptor(channel, interceptor) + .max_decoding_message_size(max_grpc_message_bytes) + .max_encoding_message_size(max_grpc_message_bytes), call_timeout: options.call_timeout(), stream_timeout: options.stream_timeout(), }) @@ -89,8 +94,11 @@ impl GalaxyClient { /// channel. Tests use this to wire up an in-memory transport. pub fn from_channel(channel: Channel, options: &ClientOptions) -> Self { let interceptor = AuthInterceptor::new(options.api_key().cloned()); + let max_grpc_message_bytes = options.max_grpc_message_bytes(); Self { - inner: GalaxyRepositoryClient::with_interceptor(channel, interceptor), + inner: GalaxyRepositoryClient::with_interceptor(channel, interceptor) + .max_decoding_message_size(max_grpc_message_bytes) + .max_encoding_message_size(max_grpc_message_bytes), call_timeout: options.call_timeout(), stream_timeout: options.stream_timeout(), } @@ -135,11 +143,33 @@ impl GalaxyClient { /// Walk the deployed object hierarchy. Each [`GalaxyObject`] contains /// the object's identifying names plus its dynamic attributes. pub async fn discover_hierarchy(&mut self) -> Result, Error> { - let response = self - .inner - .discover_hierarchy(self.unary_request(DiscoverHierarchyRequest {})) - .await?; - Ok(response.into_inner().objects) + let mut objects = Vec::new(); + let mut seen_page_tokens = std::collections::HashSet::new(); + let mut page_token = String::new(); + loop { + let response = self + .inner + .discover_hierarchy(self.unary_request(DiscoverHierarchyRequest { + page_size: DISCOVER_HIERARCHY_PAGE_SIZE, + page_token, + ..Default::default() + })) + .await?; + let reply = response.into_inner(); + objects.extend(reply.objects); + page_token = reply.next_page_token; + if page_token.is_empty() { + return Ok(objects); + } + if !seen_page_tokens.insert(page_token.clone()) { + return Err(Error::InvalidArgument { + name: "page_token".to_owned(), + detail: format!( + "galaxy discover hierarchy returned repeated page token `{page_token}`" + ), + }); + } + } } /// Subscribe to the server-streamed deploy-event feed. @@ -217,6 +247,8 @@ mod tests { present: Mutex, last_deploy: Mutex>, objects: Mutex>, + discover_requests: Mutex>, + discover_replies: Mutex>, watch_requests: Mutex>, watch_events: Mutex>, watch_senders: Mutex>, @@ -256,10 +288,21 @@ mod tests { async fn discover_hierarchy( &self, - _request: Request, + request: Request, ) -> Result, Status> { + self.state + .discover_requests + .lock() + .unwrap() + .push(request.into_inner()); + if let Some(reply) = self.state.discover_replies.lock().unwrap().pop_front() { + return Ok(Response::new(reply)); + } + Ok(Response::new(DiscoverHierarchyReply { objects: self.state.objects.lock().unwrap().clone(), + next_page_token: String::new(), + total_object_count: self.state.objects.lock().unwrap().len() as i32, })) } @@ -409,30 +452,58 @@ mod tests { #[tokio::test] async fn discover_hierarchy_returns_objects_with_attributes() { let state = Arc::new(FakeState::default()); - *state.objects.lock().unwrap() = vec![GalaxyObject { - gobject_id: 42, - tag_name: "DelmiaReceiver_001".to_owned(), - contained_name: "DelmiaReceiver".to_owned(), - browse_name: "TestMachine_001/DelmiaReceiver".to_owned(), - parent_gobject_id: 7, - is_area: false, - category_id: 3, - hosted_by_gobject_id: 1, - template_chain: vec!["$UserDefined".to_owned(), "$DelmiaReceiver".to_owned()], - attributes: vec![GalaxyAttribute { - attribute_name: "DownloadPath".to_owned(), - full_tag_reference: "DelmiaReceiver_001.DownloadPath".to_owned(), - mx_data_type: 8, - data_type_name: "MxString".to_owned(), - is_array: false, - array_dimension: 0, - array_dimension_present: false, - mx_attribute_category: 2, - security_classification: 1, - is_historized: false, - is_alarm: false, - }], - }]; + state + .discover_replies + .lock() + .unwrap() + .push_back(DiscoverHierarchyReply { + objects: vec![GalaxyObject { + gobject_id: 42, + tag_name: "DelmiaReceiver_001".to_owned(), + contained_name: "DelmiaReceiver".to_owned(), + browse_name: "TestMachine_001/DelmiaReceiver".to_owned(), + parent_gobject_id: 7, + is_area: false, + category_id: 3, + hosted_by_gobject_id: 1, + template_chain: vec!["$UserDefined".to_owned(), "$DelmiaReceiver".to_owned()], + attributes: vec![GalaxyAttribute { + attribute_name: "DownloadPath".to_owned(), + full_tag_reference: "DelmiaReceiver_001.DownloadPath".to_owned(), + mx_data_type: 8, + data_type_name: "MxString".to_owned(), + is_array: false, + array_dimension: 0, + array_dimension_present: false, + mx_attribute_category: 2, + security_classification: 1, + is_historized: false, + is_alarm: false, + }], + }], + next_page_token: "page-2".to_owned(), + total_object_count: 2, + }); + state + .discover_replies + .lock() + .unwrap() + .push_back(DiscoverHierarchyReply { + objects: vec![GalaxyObject { + gobject_id: 43, + tag_name: "DelmiaReceiver_002".to_owned(), + contained_name: String::new(), + browse_name: String::new(), + parent_gobject_id: 0, + is_area: false, + category_id: 0, + hosted_by_gobject_id: 0, + template_chain: Vec::new(), + attributes: Vec::new(), + }], + next_page_token: String::new(), + total_object_count: 2, + }); let endpoint = spawn_fake(state.clone()).await; let mut client = GalaxyClient::connect(ClientOptions::new(endpoint)) @@ -441,7 +512,12 @@ mod tests { let objects = client.discover_hierarchy().await.unwrap(); - assert_eq!(objects.len(), 1); + assert_eq!(objects.len(), 2); + let requests = state.discover_requests.lock().unwrap(); + assert_eq!(requests.len(), 2); + assert_eq!(requests[0].page_size, 5000); + assert_eq!(requests[0].page_token, ""); + assert_eq!(requests[1].page_token, "page-2"); assert_eq!(objects[0].tag_name, "DelmiaReceiver_001"); assert_eq!(objects[0].attributes.len(), 1); assert_eq!(objects[0].attributes[0].attribute_name, "DownloadPath"); @@ -451,6 +527,37 @@ mod tests { ); } + #[tokio::test] + async fn discover_hierarchy_rejects_repeated_page_token() { + let state = Arc::new(FakeState::default()); + state + .discover_replies + .lock() + .unwrap() + .push_back(DiscoverHierarchyReply { + objects: Vec::new(), + next_page_token: "7:1".to_owned(), + total_object_count: 1, + }); + state + .discover_replies + .lock() + .unwrap() + .push_back(DiscoverHierarchyReply { + objects: Vec::new(), + next_page_token: "7:1".to_owned(), + total_object_count: 1, + }); + let endpoint = spawn_fake(state).await; + let mut client = GalaxyClient::connect(ClientOptions::new(endpoint)) + .await + .unwrap(); + + let error = client.discover_hierarchy().await.unwrap_err(); + + assert!(error.to_string().contains("repeated page token")); + } + #[tokio::test] async fn watch_deploy_events_yields_events_in_order() { let state = Arc::new(FakeState::default()); diff --git a/clients/rust/src/options.rs b/clients/rust/src/options.rs index 9c3517c..e9c601c 100644 --- a/clients/rust/src/options.rs +++ b/clients/rust/src/options.rs @@ -8,6 +8,8 @@ use std::time::Duration; use crate::auth::ApiKey; +const DEFAULT_MAX_GRPC_MESSAGE_BYTES: usize = 16 * 1024 * 1024; + /// Configuration for connecting to a gateway endpoint. /// /// Defaults are 10s connect timeout, 30s call timeout, no streaming timeout, @@ -24,6 +26,7 @@ pub struct ClientOptions { connect_timeout: Duration, call_timeout: Duration, stream_timeout: Option, + max_grpc_message_bytes: usize, } impl ClientOptions { @@ -39,6 +42,7 @@ impl ClientOptions { connect_timeout: Duration::from_secs(10), call_timeout: Duration::from_secs(30), stream_timeout: None, + max_grpc_message_bytes: DEFAULT_MAX_GRPC_MESSAGE_BYTES, } } @@ -91,6 +95,11 @@ impl ClientOptions { self } + pub fn with_max_grpc_message_bytes(mut self, max_grpc_message_bytes: usize) -> Self { + self.max_grpc_message_bytes = max_grpc_message_bytes; + self + } + /// Configured endpoint URL. pub fn endpoint(&self) -> &str { &self.endpoint @@ -130,6 +139,10 @@ impl ClientOptions { pub fn stream_timeout(&self) -> Option { self.stream_timeout } + + pub fn max_grpc_message_bytes(&self) -> usize { + self.max_grpc_message_bytes + } } impl Default for ClientOptions { @@ -150,6 +163,7 @@ impl fmt::Debug for ClientOptions { .field("connect_timeout", &self.connect_timeout) .field("call_timeout", &self.call_timeout) .field("stream_timeout", &self.stream_timeout) + .field("max_grpc_message_bytes", &self.max_grpc_message_bytes) .finish() } } diff --git a/clients/rust/src/version.rs b/clients/rust/src/version.rs index c49ebb9..c5a7627 100644 --- a/clients/rust/src/version.rs +++ b/clients/rust/src/version.rs @@ -7,7 +7,7 @@ pub const CLIENT_VERSION: &str = "0.1.0-dev"; /// Public gateway gRPC protocol version this client targets. -pub const GATEWAY_PROTOCOL_VERSION: u32 = 1; +pub const GATEWAY_PROTOCOL_VERSION: u32 = 2; /// Internal worker IPC protocol version this client expects sessions to use. pub const WORKER_PROTOCOL_VERSION: u32 = 1; diff --git a/docs/Authorization.md b/docs/Authorization.md index c212bfc..97eff1a 100644 --- a/docs/Authorization.md +++ b/docs/Authorization.md @@ -6,7 +6,7 @@ what an authenticated API key can browse, read, or write inside the Galaxy. ## Overview -Authorization runs as a single gRPC server interceptor registered for every call on the gateway. It pulls the authenticated identity for the current request, derives the scope that the request type requires, and either lets the call continue or fails the call with a gRPC status. The pipeline keeps service classes free of cross-cutting checks, which matches the AGENTS.md "thin gRPC layer" rule that service handlers translate between contracts and domain code without owning policy. +Authorization runs as a single gRPC server interceptor registered for every call on the gateway. It pulls the authenticated identity for the current request, derives the scope that the request type requires, and either lets the call continue or fails the call with a gRPC status. The pipeline keeps service classes free of cross-cutting checks, which matches the `gateway.md` "thin gRPC layer" rule that service handlers translate between contracts and domain code without owning policy. The participating types live under `src/MxGateway.Server/Security/Authorization/`: diff --git a/docs/Diagnostics.md b/docs/Diagnostics.md index bc9d3b9..f9e5589 100644 --- a/docs/Diagnostics.md +++ b/docs/Diagnostics.md @@ -4,7 +4,7 @@ The diagnostics subsystem provides structured logging, credential redaction, and ## Goals -The subsystem exists to satisfy two security rules from `AGENTS.md`: never log passwords or raw credential values for `AuthenticateUser`, `WriteSecured`, or related secured operations, and never log full MXAccess values by default. Code paths that touch credentials or tag values must therefore route through `GatewayLogRedactor` rather than emitting them directly. +The subsystem exists to satisfy two security rules from `gateway.md`: never log passwords or raw credential values for `AuthenticateUser`, `WriteSecured`, or related secured operations, and never log full MXAccess values by default. Code paths that touch credentials or tag values must therefore route through `GatewayLogRedactor` rather than emitting them directly. A second goal is parity-test diagnosability. Because MXAccess sessions, workers, correlation ids, and command methods are the units of comparison, every log entry produced inside a request scope must carry those identifiers without each call site having to format them. @@ -80,7 +80,7 @@ public static bool IsCredentialBearingCommand(string? commandMethod) } ``` -The names match the MXAccess command list in `AGENTS.md` exactly. `Write` and `Write2` are not in the set because their payloads are tag values, not credentials, and are governed by the `valueLoggingEnabled` flag described below. +The names match the MXAccess command list in `gateway.md` exactly. `Write` and `Write2` are not in the set because their payloads are tag values, not credentials, and are governed by the `valueLoggingEnabled` flag described below. ### API key redaction diff --git a/docs/Grpc.md b/docs/Grpc.md index d0c958b..8cb1eef 100644 --- a/docs/Grpc.md +++ b/docs/Grpc.md @@ -4,7 +4,7 @@ The gRPC service layer is the public entry point for client traffic. It is inten ## Layer Responsibilities -The architecture rule (from `AGENTS.md`) is that the gRPC layer must "validate request, find session, call the session worker client, map worker replies to public replies, and stream events". Anything else — caching, retries, worker process lifetime, event ordering — lives behind `ISessionManager` and the worker client. Keeping the layer thin lets the same session/worker code be reused by future transports (for example, an in-process host or an alternate IPC) without having to re-derive validation or mapping rules. +The architecture rule (from `gateway.md`) is that the gRPC layer must "validate request, find session, call the session worker client, map worker replies to public replies, and stream events". Anything else — caching, retries, worker process lifetime, event ordering — lives behind `ISessionManager` and the worker client. Keeping the layer thin lets the same session/worker code be reused by future transports (for example, an in-process host or an alternate IPC) without having to re-derive validation or mapping rules. The layer is composed of four collaborators: diff --git a/docs/WorkerBootstrap.md b/docs/WorkerBootstrap.md index c553eb9..001cf37 100644 --- a/docs/WorkerBootstrap.md +++ b/docs/WorkerBootstrap.md @@ -177,7 +177,7 @@ private void Write( ### What the redactor redacts and why -`AGENTS.md` "Security And Logging" requires that the worker never log raw credential values for `AuthenticateUser`, `WriteSecured`, or related secured operations. The bootstrap nonce is also a credential: anyone who reads it can impersonate the worker to the gateway pipe. `WorkerLogRedactor` enforces this by replacing values whose field name contains any of these substrings (case-insensitive) with the literal `[redacted]`: +`gateway.md` "Security" requires that the worker never log raw credential values for `AuthenticateUser`, `WriteSecured`, or related secured operations. The bootstrap nonce is also a credential: anyone who reads it can impersonate the worker to the gateway pipe. `WorkerLogRedactor` enforces this by replacing values whose field name contains any of these substrings (case-insensitive) with the literal `[redacted]`: ```csharp private static readonly string[] SensitiveFieldNameParts = diff --git a/docs/WorkerConversion.md b/docs/WorkerConversion.md index ece0c32..696cc53 100644 --- a/docs/WorkerConversion.md +++ b/docs/WorkerConversion.md @@ -4,7 +4,7 @@ The conversion layer in `MxGateway.Worker.Conversion` projects COM `VARIANT` pay ## Overview -`AGENTS.md` (section "Value And Status Rules") requires that the wire format use a value union capable of representing COM `VARIANT` values and arrays, that lossy conversions retain both the typed projection and raw diagnostic metadata, and that `MXSTATUS_PROXY` arrays never collapse to a single success flag. The types in `src/MxGateway.Worker/Conversion/` are the worker-side enforcement of those rules. +`gateway.md` (sections "Value Model" and "Status Model") requires that the wire format use a value union capable of representing COM `VARIANT` values and arrays, that lossy conversions retain both the typed projection and raw diagnostic metadata, and that `MXSTATUS_PROXY` arrays never collapse to a single success flag. The types in `src/MxGateway.Worker/Conversion/` are the worker-side enforcement of those rules. The layer is split into three concerns: @@ -90,7 +90,7 @@ MxDataType elementDataType = ResolveArrayElementDataType(elementType, expectedEl mxArray.ElementDataType = elementDataType; ``` -When the element type cannot be classified, `ConvertArray` does not throw. It downgrades the result to `MxDataType.Unknown`, records the original expected type in `RawElementDataType`, and serializes each element via `ConvertRawArray` as a UTF-8 byte string. This satisfies the AGENTS.md requirement to keep both the best typed projection and the raw diagnostic metadata. +When the element type cannot be classified, `ConvertArray` does not throw. It downgrades the result to `MxDataType.Unknown`, records the original expected type in `RawElementDataType`, and serializes each element via `ConvertRawArray` as a UTF-8 byte string. This satisfies the `gateway.md` requirement to keep both the best typed projection and the raw diagnostic metadata. ```csharp default: @@ -201,7 +201,7 @@ foreach (object? status in statuses) ### Why arrays are not collapsed -A single MXAccess command (notably `Read`, `Write`, and event callbacks) can return one status per item handle. AGENTS.md requires that the wire format represent each entry independently, because collapsing them to a Boolean success flag hides partial failures: a 50-item write where one item fails would be indistinguishable from a 50-item write where every item failed. Preserving the array per-position lets clients correlate each `MxStatusProxy` with its item handle and `MxValue`. +A single MXAccess command (notably `Read`, `Write`, and event callbacks) can return one status per item handle. `gateway.md` requires that the wire format represent each entry independently, because collapsing them to a Boolean success flag hides partial failures: a 50-item write where one item fails would be indistinguishable from a 50-item write where every item failed. Preserving the array per-position lets clients correlate each `MxStatusProxy` with its item handle and `MxValue`. ### Completion-only status fallback diff --git a/docs/WorkerSta.md b/docs/WorkerSta.md index 54e9376..5065728 100644 --- a/docs/WorkerSta.md +++ b/docs/WorkerSta.md @@ -4,7 +4,7 @@ The worker STA runtime owns the dedicated single-threaded apartment thread that ## Why an STA Is Required -The installed MXAccess interop assembly declares an `Apartment` threading model (see `AGENTS.md` under "Worker Rules"). COM objects with that model must be created and called on a thread initialized as a single-threaded apartment, and any callbacks the object raises (event sink calls) are delivered through the thread's Windows message queue. A plain blocking queue is not sufficient: the STA loop must pump Windows messages so that the COM marshaler can deliver event invocations on the same thread that holds the object. Because of that constraint, every MXAccess operation in the worker is funneled through the types in `src/MxGateway.Worker/Sta/`. +The installed MXAccess interop assembly declares an `Apartment` threading model (see `gateway.md` under "STA Worker Thread Model"). COM objects with that model must be created and called on a thread initialized as a single-threaded apartment, and any callbacks the object raises (event sink calls) are delivered through the thread's Windows message queue. A plain blocking queue is not sufficient: the STA loop must pump Windows messages so that the COM marshaler can deliver event invocations on the same thread that holds the object. Because of that constraint, every MXAccess operation in the worker is funneled through the types in `src/MxGateway.Worker/Sta/`. ## Types @@ -131,11 +131,11 @@ finally `SetCurrentCommand` records the in-flight `CorrelationId` so `PopulateHeartbeat` can publish both `PendingCommandCount` and `CurrentCommandCorrelationId` on the worker heartbeat. Exceptions are converted through `HResultConverter` so the IPC reply still carries a structured `ProtocolStatus`, an HRESULT, and a diagnostic message instead of an unhandled fault. `NormalizeReply` backfills `SessionId`, `CorrelationId`, `Kind`, and a default `ProtocolStatusCode.Ok` so executors can return minimal replies without restating the envelope. -`CancelQueuedCommand` walks the queue and completes a single matching entry with `ProtocolStatusCode.Canceled`. It cannot abort a command already running on the STA: per `AGENTS.md`, "Canceling a gRPC call should stop waiting in the gateway, but it cannot safely abort an in-flight COM call on the STA. Hard cancellation means killing the worker process." +`CancelQueuedCommand` walks the queue and completes a single matching entry with `ProtocolStatusCode.Canceled`. It cannot abort a command already running on the STA: per `gateway.md`, "Canceling a gRPC call should stop waiting in the gateway, but it cannot safely abort an in-flight COM call on the STA. Hard cancellation means killing the worker process." ## Why the STA Loop Cannot Block on I/O -`AGENTS.md` states explicitly: "Do not block the STA on pipe writes, gRPC calls, or slow consumers. Event handlers should convert event args, enqueue outbound events, and return to pumping messages." The STA thread is the only thread that can service COM event callbacks, so any work that blocks it stalls every event the MXAccess object would otherwise deliver. The runtime keeps to that rule by giving the STA only two responsibilities inside `ThreadMain`: executing already-queued work items and pumping messages. Outbound event delivery and pipe writes happen on threads that observe the queues populated from the STA, never on the STA itself. +`gateway.md` states explicitly: "Do not block the STA on pipe writes, gRPC calls, or slow consumers. Event handlers should convert event args, enqueue outbound events, and return to pumping messages." The STA thread is the only thread that can service COM event callbacks, so any work that blocks it stalls every event the MXAccess object would otherwise deliver. The runtime keeps to that rule by giving the STA only two responsibilities inside `ThreadMain`: executing already-queued work items and pumping messages. Outbound event delivery and pipe writes happen on threads that observe the queues populated from the STA, never on the STA itself. ## Shutdown Sequence diff --git a/src/MxGateway.Contracts/GatewayContractInfo.cs b/src/MxGateway.Contracts/GatewayContractInfo.cs index cfc34d9..a6b6c92 100644 --- a/src/MxGateway.Contracts/GatewayContractInfo.cs +++ b/src/MxGateway.Contracts/GatewayContractInfo.cs @@ -6,7 +6,7 @@ namespace MxGateway.Contracts; /// public static class GatewayContractInfo { - public const uint GatewayProtocolVersion = 1; + public const uint GatewayProtocolVersion = 2; public const uint WorkerProtocolVersion = 1; diff --git a/src/MxGateway.Contracts/Generated/GalaxyRepository.cs b/src/MxGateway.Contracts/Generated/GalaxyRepository.cs index 517e7f9..3224a65 100644 --- a/src/MxGateway.Contracts/Generated/GalaxyRepository.cs +++ b/src/MxGateway.Contracts/Generated/GalaxyRepository.cs @@ -25,54 +25,64 @@ namespace MxGateway.Contracts.Proto.Galaxy { byte[] descriptorData = global::System.Convert.FromBase64String( string.Concat( "ChdnYWxheHlfcmVwb3NpdG9yeS5wcm90bxIUZ2FsYXh5X3JlcG9zaXRvcnku", - "djEaH2dvb2dsZS9wcm90b2J1Zi90aW1lc3RhbXAucHJvdG8iFwoVVGVzdENv", - "bm5lY3Rpb25SZXF1ZXN0IiEKE1Rlc3RDb25uZWN0aW9uUmVwbHkSCgoCb2sY", - "ASABKAgiGgoYR2V0TGFzdERlcGxveVRpbWVSZXF1ZXN0ImIKFkdldExhc3RE", - "ZXBsb3lUaW1lUmVwbHkSDwoHcHJlc2VudBgBIAEoCBI3ChN0aW1lX29mX2xh", - "c3RfZGVwbG95GAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCIa", - "ChhEaXNjb3ZlckhpZXJhcmNoeVJlcXVlc3QiTQoWRGlzY292ZXJIaWVyYXJj", - "aHlSZXBseRIzCgdvYmplY3RzGAEgAygLMiIuZ2FsYXh5X3JlcG9zaXRvcnku", - "djEuR2FsYXh5T2JqZWN0IlUKGFdhdGNoRGVwbG95RXZlbnRzUmVxdWVzdBI5", - "ChVsYXN0X3NlZW5fZGVwbG95X3RpbWUYASABKAsyGi5nb29nbGUucHJvdG9i", - "dWYuVGltZXN0YW1wIt0BCgtEZXBsb3lFdmVudBIQCghzZXF1ZW5jZRgBIAEo", - "BBIvCgtvYnNlcnZlZF9hdBgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1l", - "c3RhbXASNwoTdGltZV9vZl9sYXN0X2RlcGxveRgDIAEoCzIaLmdvb2dsZS5w", - "cm90b2J1Zi5UaW1lc3RhbXASIwobdGltZV9vZl9sYXN0X2RlcGxveV9wcmVz", - "ZW50GAQgASgIEhQKDG9iamVjdF9jb3VudBgFIAEoBRIXCg9hdHRyaWJ1dGVf", - "Y291bnQYBiABKAUikwIKDEdhbGF4eU9iamVjdBISCgpnb2JqZWN0X2lkGAEg", - "ASgFEhAKCHRhZ19uYW1lGAIgASgJEhYKDmNvbnRhaW5lZF9uYW1lGAMgASgJ", - "EhMKC2Jyb3dzZV9uYW1lGAQgASgJEhkKEXBhcmVudF9nb2JqZWN0X2lkGAUg", - "ASgFEg8KB2lzX2FyZWEYBiABKAgSEwoLY2F0ZWdvcnlfaWQYByABKAUSHAoU", - "aG9zdGVkX2J5X2dvYmplY3RfaWQYCCABKAUSFgoOdGVtcGxhdGVfY2hhaW4Y", - "CSADKAkSOQoKYXR0cmlidXRlcxgKIAMoCzIlLmdhbGF4eV9yZXBvc2l0b3J5", - "LnYxLkdhbGF4eUF0dHJpYnV0ZSKoAgoPR2FsYXh5QXR0cmlidXRlEhYKDmF0", - "dHJpYnV0ZV9uYW1lGAEgASgJEhoKEmZ1bGxfdGFnX3JlZmVyZW5jZRgCIAEo", - "CRIUCgxteF9kYXRhX3R5cGUYAyABKAUSFgoOZGF0YV90eXBlX25hbWUYBCAB", - "KAkSEAoIaXNfYXJyYXkYBSABKAgSFwoPYXJyYXlfZGltZW5zaW9uGAYgASgF", - "Eh8KF2FycmF5X2RpbWVuc2lvbl9wcmVzZW50GAcgASgIEh0KFW14X2F0dHJp", - "YnV0ZV9jYXRlZ29yeRgIIAEoBRIfChdzZWN1cml0eV9jbGFzc2lmaWNhdGlv", - "bhgJIAEoBRIVCg1pc19oaXN0b3JpemVkGAogASgIEhAKCGlzX2FsYXJtGAsg", - "ASgIMswDChBHYWxheHlSZXBvc2l0b3J5EmgKDlRlc3RDb25uZWN0aW9uEisu", - "Z2FsYXh5X3JlcG9zaXRvcnkudjEuVGVzdENvbm5lY3Rpb25SZXF1ZXN0Giku", - "Z2FsYXh5X3JlcG9zaXRvcnkudjEuVGVzdENvbm5lY3Rpb25SZXBseRJxChFH", - "ZXRMYXN0RGVwbG95VGltZRIuLmdhbGF4eV9yZXBvc2l0b3J5LnYxLkdldExh", - "c3REZXBsb3lUaW1lUmVxdWVzdBosLmdhbGF4eV9yZXBvc2l0b3J5LnYxLkdl", - "dExhc3REZXBsb3lUaW1lUmVwbHkScQoRRGlzY292ZXJIaWVyYXJjaHkSLi5n", - "YWxheHlfcmVwb3NpdG9yeS52MS5EaXNjb3ZlckhpZXJhcmNoeVJlcXVlc3Qa", - "LC5nYWxheHlfcmVwb3NpdG9yeS52MS5EaXNjb3ZlckhpZXJhcmNoeVJlcGx5", - "EmgKEVdhdGNoRGVwbG95RXZlbnRzEi4uZ2FsYXh5X3JlcG9zaXRvcnkudjEu", - "V2F0Y2hEZXBsb3lFdmVudHNSZXF1ZXN0GiEuZ2FsYXh5X3JlcG9zaXRvcnku", - "djEuRGVwbG95RXZlbnQwAUIjqgIgTXhHYXRld2F5LkNvbnRyYWN0cy5Qcm90", - "by5HYWxheHliBnByb3RvMw==")); + "djEaH2dvb2dsZS9wcm90b2J1Zi90aW1lc3RhbXAucHJvdG8aHmdvb2dsZS9w", + "cm90b2J1Zi93cmFwcGVycy5wcm90byIXChVUZXN0Q29ubmVjdGlvblJlcXVl", + "c3QiIQoTVGVzdENvbm5lY3Rpb25SZXBseRIKCgJvaxgBIAEoCCIaChhHZXRM", + "YXN0RGVwbG95VGltZVJlcXVlc3QiYgoWR2V0TGFzdERlcGxveVRpbWVSZXBs", + "eRIPCgdwcmVzZW50GAEgASgIEjcKE3RpbWVfb2ZfbGFzdF9kZXBsb3kYAiAB", + "KAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIocDChhEaXNjb3Zlckhp", + "ZXJhcmNoeVJlcXVlc3QSEQoJcGFnZV9zaXplGAEgASgFEhIKCnBhZ2VfdG9r", + "ZW4YAiABKAkSGQoPcm9vdF9nb2JqZWN0X2lkGAMgASgFSAASFwoNcm9vdF90", + "YWdfbmFtZRgEIAEoCUgAEh0KE3Jvb3RfY29udGFpbmVkX3BhdGgYBSABKAlI", + "ABIuCgltYXhfZGVwdGgYBiABKAsyGy5nb29nbGUucHJvdG9idWYuSW50MzJW", + "YWx1ZRIUCgxjYXRlZ29yeV9pZHMYByADKAUSHwoXdGVtcGxhdGVfY2hhaW5f", + "Y29udGFpbnMYCCADKAkSFQoNdGFnX25hbWVfZ2xvYhgJIAEoCRIfChJpbmNs", + "dWRlX2F0dHJpYnV0ZXMYCiABKAhIAYgBARIaChJhbGFybV9iZWFyaW5nX29u", + "bHkYCyABKAgSFwoPaGlzdG9yaXplZF9vbmx5GAwgASgIQgYKBHJvb3RCFQoT", + "X2luY2x1ZGVfYXR0cmlidXRlcyKCAQoWRGlzY292ZXJIaWVyYXJjaHlSZXBs", + "eRIzCgdvYmplY3RzGAEgAygLMiIuZ2FsYXh5X3JlcG9zaXRvcnkudjEuR2Fs", + "YXh5T2JqZWN0EhcKD25leHRfcGFnZV90b2tlbhgCIAEoCRIaChJ0b3RhbF9v", + "YmplY3RfY291bnQYAyABKAUiVQoYV2F0Y2hEZXBsb3lFdmVudHNSZXF1ZXN0", + "EjkKFWxhc3Rfc2Vlbl9kZXBsb3lfdGltZRgBIAEoCzIaLmdvb2dsZS5wcm90", + "b2J1Zi5UaW1lc3RhbXAi3QEKC0RlcGxveUV2ZW50EhAKCHNlcXVlbmNlGAEg", + "ASgEEi8KC29ic2VydmVkX2F0GAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRp", + "bWVzdGFtcBI3ChN0aW1lX29mX2xhc3RfZGVwbG95GAMgASgLMhouZ29vZ2xl", + "LnByb3RvYnVmLlRpbWVzdGFtcBIjCht0aW1lX29mX2xhc3RfZGVwbG95X3By", + "ZXNlbnQYBCABKAgSFAoMb2JqZWN0X2NvdW50GAUgASgFEhcKD2F0dHJpYnV0", + "ZV9jb3VudBgGIAEoBSKTAgoMR2FsYXh5T2JqZWN0EhIKCmdvYmplY3RfaWQY", + "ASABKAUSEAoIdGFnX25hbWUYAiABKAkSFgoOY29udGFpbmVkX25hbWUYAyAB", + "KAkSEwoLYnJvd3NlX25hbWUYBCABKAkSGQoRcGFyZW50X2dvYmplY3RfaWQY", + "BSABKAUSDwoHaXNfYXJlYRgGIAEoCBITCgtjYXRlZ29yeV9pZBgHIAEoBRIc", + "ChRob3N0ZWRfYnlfZ29iamVjdF9pZBgIIAEoBRIWCg50ZW1wbGF0ZV9jaGFp", + "bhgJIAMoCRI5CgphdHRyaWJ1dGVzGAogAygLMiUuZ2FsYXh5X3JlcG9zaXRv", + "cnkudjEuR2FsYXh5QXR0cmlidXRlIqgCCg9HYWxheHlBdHRyaWJ1dGUSFgoO", + "YXR0cmlidXRlX25hbWUYASABKAkSGgoSZnVsbF90YWdfcmVmZXJlbmNlGAIg", + "ASgJEhQKDG14X2RhdGFfdHlwZRgDIAEoBRIWCg5kYXRhX3R5cGVfbmFtZRgE", + "IAEoCRIQCghpc19hcnJheRgFIAEoCBIXCg9hcnJheV9kaW1lbnNpb24YBiAB", + "KAUSHwoXYXJyYXlfZGltZW5zaW9uX3ByZXNlbnQYByABKAgSHQoVbXhfYXR0", + "cmlidXRlX2NhdGVnb3J5GAggASgFEh8KF3NlY3VyaXR5X2NsYXNzaWZpY2F0", + "aW9uGAkgASgFEhUKDWlzX2hpc3Rvcml6ZWQYCiABKAgSEAoIaXNfYWxhcm0Y", + "CyABKAgyzAMKEEdhbGF4eVJlcG9zaXRvcnkSaAoOVGVzdENvbm5lY3Rpb24S", + "Ky5nYWxheHlfcmVwb3NpdG9yeS52MS5UZXN0Q29ubmVjdGlvblJlcXVlc3Qa", + "KS5nYWxheHlfcmVwb3NpdG9yeS52MS5UZXN0Q29ubmVjdGlvblJlcGx5EnEK", + "EUdldExhc3REZXBsb3lUaW1lEi4uZ2FsYXh5X3JlcG9zaXRvcnkudjEuR2V0", + "TGFzdERlcGxveVRpbWVSZXF1ZXN0GiwuZ2FsYXh5X3JlcG9zaXRvcnkudjEu", + "R2V0TGFzdERlcGxveVRpbWVSZXBseRJxChFEaXNjb3ZlckhpZXJhcmNoeRIu", + "LmdhbGF4eV9yZXBvc2l0b3J5LnYxLkRpc2NvdmVySGllcmFyY2h5UmVxdWVz", + "dBosLmdhbGF4eV9yZXBvc2l0b3J5LnYxLkRpc2NvdmVySGllcmFyY2h5UmVw", + "bHkSaAoRV2F0Y2hEZXBsb3lFdmVudHMSLi5nYWxheHlfcmVwb3NpdG9yeS52", + "MS5XYXRjaERlcGxveUV2ZW50c1JlcXVlc3QaIS5nYWxheHlfcmVwb3NpdG9y", + "eS52MS5EZXBsb3lFdmVudDABQiOqAiBNeEdhdGV3YXkuQ29udHJhY3RzLlBy", + "b3RvLkdhbGF4eWIGcHJvdG8z")); descriptor = pbr::FileDescriptor.FromGeneratedCode(descriptorData, - new pbr::FileDescriptor[] { global::Google.Protobuf.WellKnownTypes.TimestampReflection.Descriptor, }, + new pbr::FileDescriptor[] { global::Google.Protobuf.WellKnownTypes.TimestampReflection.Descriptor, global::Google.Protobuf.WellKnownTypes.WrappersReflection.Descriptor, }, new pbr::GeneratedClrTypeInfo(null, null, new pbr::GeneratedClrTypeInfo[] { new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.Galaxy.TestConnectionRequest), global::MxGateway.Contracts.Proto.Galaxy.TestConnectionRequest.Parser, null, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.Galaxy.TestConnectionReply), global::MxGateway.Contracts.Proto.Galaxy.TestConnectionReply.Parser, new[]{ "Ok" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeRequest), global::MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeRequest.Parser, null, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeReply), global::MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeReply.Parser, new[]{ "Present", "TimeOfLastDeploy" }, null, null, null, null), - new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyRequest), global::MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyRequest.Parser, null, null, null, null, null), - new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyReply), global::MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyReply.Parser, new[]{ "Objects" }, null, null, null, null), + new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyRequest), global::MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyRequest.Parser, new[]{ "PageSize", "PageToken", "RootGobjectId", "RootTagName", "RootContainedPath", "MaxDepth", "CategoryIds", "TemplateChainContains", "TagNameGlob", "IncludeAttributes", "AlarmBearingOnly", "HistorizedOnly" }, new[]{ "Root", "IncludeAttributes" }, null, null, null), + new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyReply), global::MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyReply.Parser, new[]{ "Objects", "NextPageToken", "TotalObjectCount" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.Galaxy.WatchDeployEventsRequest), global::MxGateway.Contracts.Proto.Galaxy.WatchDeployEventsRequest.Parser, new[]{ "LastSeenDeployTime" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.Galaxy.DeployEvent), global::MxGateway.Contracts.Proto.Galaxy.DeployEvent.Parser, new[]{ "Sequence", "ObservedAt", "TimeOfLastDeploy", "TimeOfLastDeployPresent", "ObjectCount", "AttributeCount" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.Galaxy.GalaxyObject), global::MxGateway.Contracts.Proto.Galaxy.GalaxyObject.Parser, new[]{ "GobjectId", "TagName", "ContainedName", "BrowseName", "ParentGobjectId", "IsArea", "CategoryId", "HostedByGobjectId", "TemplateChain", "Attributes" }, null, null, null, null), @@ -855,6 +865,7 @@ namespace MxGateway.Contracts.Proto.Galaxy { { private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new DiscoverHierarchyRequest()); private pb::UnknownFieldSet _unknownFields; + private int _hasBits0; [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pb::MessageParser Parser { get { return _parser; } } @@ -882,6 +893,28 @@ namespace MxGateway.Contracts.Proto.Galaxy { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public DiscoverHierarchyRequest(DiscoverHierarchyRequest other) : this() { + _hasBits0 = other._hasBits0; + pageSize_ = other.pageSize_; + pageToken_ = other.pageToken_; + MaxDepth = other.MaxDepth; + categoryIds_ = other.categoryIds_.Clone(); + templateChainContains_ = other.templateChainContains_.Clone(); + tagNameGlob_ = other.tagNameGlob_; + includeAttributes_ = other.includeAttributes_; + alarmBearingOnly_ = other.alarmBearingOnly_; + historizedOnly_ = other.historizedOnly_; + switch (other.RootCase) { + case RootOneofCase.RootGobjectId: + RootGobjectId = other.RootGobjectId; + break; + case RootOneofCase.RootTagName: + RootTagName = other.RootTagName; + break; + case RootOneofCase.RootContainedPath: + RootContainedPath = other.RootContainedPath; + break; + } + _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); } @@ -891,6 +924,258 @@ namespace MxGateway.Contracts.Proto.Galaxy { return new DiscoverHierarchyRequest(this); } + /// Field number for the "page_size" field. + public const int PageSizeFieldNumber = 1; + private int pageSize_; + /// + /// Maximum number of objects to return. The server applies its default when + /// unset and rejects non-positive values. + /// + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public int PageSize { + get { return pageSize_; } + set { + pageSize_ = value; + } + } + + /// Field number for the "page_token" field. + public const int PageTokenFieldNumber = 2; + private string pageToken_ = ""; + /// + /// Opaque token returned by a previous DiscoverHierarchy response. + /// + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public string PageToken { + get { return pageToken_; } + set { + pageToken_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); + } + } + + /// Field number for the "root_gobject_id" field. + public const int RootGobjectIdFieldNumber = 3; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public int RootGobjectId { + get { return HasRootGobjectId ? (int) root_ : 0; } + set { + root_ = value; + rootCase_ = RootOneofCase.RootGobjectId; + } + } + /// Gets whether the "root_gobject_id" field is set + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public bool HasRootGobjectId { + get { return rootCase_ == RootOneofCase.RootGobjectId; } + } + /// Clears the value of the oneof if it's currently set to "root_gobject_id" + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void ClearRootGobjectId() { + if (HasRootGobjectId) { + ClearRoot(); + } + } + + /// Field number for the "root_tag_name" field. + public const int RootTagNameFieldNumber = 4; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public string RootTagName { + get { return HasRootTagName ? (string) root_ : ""; } + set { + root_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); + rootCase_ = RootOneofCase.RootTagName; + } + } + /// Gets whether the "root_tag_name" field is set + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public bool HasRootTagName { + get { return rootCase_ == RootOneofCase.RootTagName; } + } + /// Clears the value of the oneof if it's currently set to "root_tag_name" + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void ClearRootTagName() { + if (HasRootTagName) { + ClearRoot(); + } + } + + /// Field number for the "root_contained_path" field. + public const int RootContainedPathFieldNumber = 5; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public string RootContainedPath { + get { return HasRootContainedPath ? (string) root_ : ""; } + set { + root_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); + rootCase_ = RootOneofCase.RootContainedPath; + } + } + /// Gets whether the "root_contained_path" field is set + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public bool HasRootContainedPath { + get { return rootCase_ == RootOneofCase.RootContainedPath; } + } + /// Clears the value of the oneof if it's currently set to "root_contained_path" + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void ClearRootContainedPath() { + if (HasRootContainedPath) { + ClearRoot(); + } + } + + /// Field number for the "max_depth" field. + public const int MaxDepthFieldNumber = 6; + private static readonly pb::FieldCodec _single_maxDepth_codec = pb::FieldCodec.ForStructWrapper(50); + private int? maxDepth_; + /// + /// Optional. Cap on descendant depth from root. Zero returns only the root. + /// Unset means unlimited depth. + /// + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public int? MaxDepth { + get { return maxDepth_; } + set { + maxDepth_ = value; + } + } + + + /// Field number for the "category_ids" field. + public const int CategoryIdsFieldNumber = 7; + private static readonly pb::FieldCodec _repeated_categoryIds_codec + = pb::FieldCodec.ForInt32(58); + private readonly pbc::RepeatedField categoryIds_ = new pbc::RepeatedField(); + /// + /// Optional object category id filters. + /// + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public pbc::RepeatedField CategoryIds { + get { return categoryIds_; } + } + + /// Field number for the "template_chain_contains" field. + public const int TemplateChainContainsFieldNumber = 8; + private static readonly pb::FieldCodec _repeated_templateChainContains_codec + = pb::FieldCodec.ForString(66); + private readonly pbc::RepeatedField templateChainContains_ = new pbc::RepeatedField(); + /// + /// Optional case-insensitive substring filters against template names. + /// + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public pbc::RepeatedField TemplateChainContains { + get { return templateChainContains_; } + } + + /// Field number for the "tag_name_glob" field. + public const int TagNameGlobFieldNumber = 9; + private string tagNameGlob_ = ""; + /// + /// Optional anchored, case-insensitive glob over object tag_name. + /// + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public string TagNameGlob { + get { return tagNameGlob_; } + set { + tagNameGlob_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); + } + } + + /// Field number for the "include_attributes" field. + public const int IncludeAttributesFieldNumber = 10; + private readonly static bool IncludeAttributesDefaultValue = false; + + private bool includeAttributes_; + /// + /// Optional. Unset or true includes attributes. False returns object skeletons. + /// + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public bool IncludeAttributes { + get { if ((_hasBits0 & 1) != 0) { return includeAttributes_; } else { return IncludeAttributesDefaultValue; } } + set { + _hasBits0 |= 1; + includeAttributes_ = value; + } + } + /// Gets whether the "include_attributes" field is set + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public bool HasIncludeAttributes { + get { return (_hasBits0 & 1) != 0; } + } + /// Clears the value of the "include_attributes" field + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void ClearIncludeAttributes() { + _hasBits0 &= ~1; + } + + /// Field number for the "alarm_bearing_only" field. + public const int AlarmBearingOnlyFieldNumber = 11; + private bool alarmBearingOnly_; + /// + /// Optional. Return only objects with at least one alarm-bearing attribute. + /// + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public bool AlarmBearingOnly { + get { return alarmBearingOnly_; } + set { + alarmBearingOnly_ = value; + } + } + + /// Field number for the "historized_only" field. + public const int HistorizedOnlyFieldNumber = 12; + private bool historizedOnly_; + /// + /// Optional. Return only objects with at least one historized attribute. + /// + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public bool HistorizedOnly { + get { return historizedOnly_; } + set { + historizedOnly_ = value; + } + } + + private object root_; + /// Enum of possible cases for the "root" oneof. + public enum RootOneofCase { + None = 0, + RootGobjectId = 3, + RootTagName = 4, + RootContainedPath = 5, + } + private RootOneofCase rootCase_ = RootOneofCase.None; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public RootOneofCase RootCase { + get { return rootCase_; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void ClearRoot() { + rootCase_ = RootOneofCase.None; + root_ = null; + } + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public override bool Equals(object other) { @@ -906,6 +1191,19 @@ namespace MxGateway.Contracts.Proto.Galaxy { if (ReferenceEquals(other, this)) { return true; } + if (PageSize != other.PageSize) return false; + if (PageToken != other.PageToken) return false; + if (RootGobjectId != other.RootGobjectId) return false; + if (RootTagName != other.RootTagName) return false; + if (RootContainedPath != other.RootContainedPath) return false; + if (MaxDepth != other.MaxDepth) return false; + if(!categoryIds_.Equals(other.categoryIds_)) return false; + if(!templateChainContains_.Equals(other.templateChainContains_)) return false; + if (TagNameGlob != other.TagNameGlob) return false; + if (IncludeAttributes != other.IncludeAttributes) return false; + if (AlarmBearingOnly != other.AlarmBearingOnly) return false; + if (HistorizedOnly != other.HistorizedOnly) return false; + if (RootCase != other.RootCase) return false; return Equals(_unknownFields, other._unknownFields); } @@ -913,6 +1211,19 @@ namespace MxGateway.Contracts.Proto.Galaxy { [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public override int GetHashCode() { int hash = 1; + if (PageSize != 0) hash ^= PageSize.GetHashCode(); + if (PageToken.Length != 0) hash ^= PageToken.GetHashCode(); + if (HasRootGobjectId) hash ^= RootGobjectId.GetHashCode(); + if (HasRootTagName) hash ^= RootTagName.GetHashCode(); + if (HasRootContainedPath) hash ^= RootContainedPath.GetHashCode(); + if (maxDepth_ != null) hash ^= MaxDepth.GetHashCode(); + hash ^= categoryIds_.GetHashCode(); + hash ^= templateChainContains_.GetHashCode(); + if (TagNameGlob.Length != 0) hash ^= TagNameGlob.GetHashCode(); + if (HasIncludeAttributes) hash ^= IncludeAttributes.GetHashCode(); + if (AlarmBearingOnly != false) hash ^= AlarmBearingOnly.GetHashCode(); + if (HistorizedOnly != false) hash ^= HistorizedOnly.GetHashCode(); + hash ^= (int) rootCase_; if (_unknownFields != null) { hash ^= _unknownFields.GetHashCode(); } @@ -931,6 +1242,47 @@ namespace MxGateway.Contracts.Proto.Galaxy { #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE output.WriteRawMessage(this); #else + if (PageSize != 0) { + output.WriteRawTag(8); + output.WriteInt32(PageSize); + } + if (PageToken.Length != 0) { + output.WriteRawTag(18); + output.WriteString(PageToken); + } + if (HasRootGobjectId) { + output.WriteRawTag(24); + output.WriteInt32(RootGobjectId); + } + if (HasRootTagName) { + output.WriteRawTag(34); + output.WriteString(RootTagName); + } + if (HasRootContainedPath) { + output.WriteRawTag(42); + output.WriteString(RootContainedPath); + } + if (maxDepth_ != null) { + _single_maxDepth_codec.WriteTagAndValue(output, MaxDepth); + } + categoryIds_.WriteTo(output, _repeated_categoryIds_codec); + templateChainContains_.WriteTo(output, _repeated_templateChainContains_codec); + if (TagNameGlob.Length != 0) { + output.WriteRawTag(74); + output.WriteString(TagNameGlob); + } + if (HasIncludeAttributes) { + output.WriteRawTag(80); + output.WriteBool(IncludeAttributes); + } + if (AlarmBearingOnly != false) { + output.WriteRawTag(88); + output.WriteBool(AlarmBearingOnly); + } + if (HistorizedOnly != false) { + output.WriteRawTag(96); + output.WriteBool(HistorizedOnly); + } if (_unknownFields != null) { _unknownFields.WriteTo(output); } @@ -941,6 +1293,47 @@ namespace MxGateway.Contracts.Proto.Galaxy { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) { + if (PageSize != 0) { + output.WriteRawTag(8); + output.WriteInt32(PageSize); + } + if (PageToken.Length != 0) { + output.WriteRawTag(18); + output.WriteString(PageToken); + } + if (HasRootGobjectId) { + output.WriteRawTag(24); + output.WriteInt32(RootGobjectId); + } + if (HasRootTagName) { + output.WriteRawTag(34); + output.WriteString(RootTagName); + } + if (HasRootContainedPath) { + output.WriteRawTag(42); + output.WriteString(RootContainedPath); + } + if (maxDepth_ != null) { + _single_maxDepth_codec.WriteTagAndValue(ref output, MaxDepth); + } + categoryIds_.WriteTo(ref output, _repeated_categoryIds_codec); + templateChainContains_.WriteTo(ref output, _repeated_templateChainContains_codec); + if (TagNameGlob.Length != 0) { + output.WriteRawTag(74); + output.WriteString(TagNameGlob); + } + if (HasIncludeAttributes) { + output.WriteRawTag(80); + output.WriteBool(IncludeAttributes); + } + if (AlarmBearingOnly != false) { + output.WriteRawTag(88); + output.WriteBool(AlarmBearingOnly); + } + if (HistorizedOnly != false) { + output.WriteRawTag(96); + output.WriteBool(HistorizedOnly); + } if (_unknownFields != null) { _unknownFields.WriteTo(ref output); } @@ -951,6 +1344,38 @@ namespace MxGateway.Contracts.Proto.Galaxy { [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public int CalculateSize() { int size = 0; + if (PageSize != 0) { + size += 1 + pb::CodedOutputStream.ComputeInt32Size(PageSize); + } + if (PageToken.Length != 0) { + size += 1 + pb::CodedOutputStream.ComputeStringSize(PageToken); + } + if (HasRootGobjectId) { + size += 1 + pb::CodedOutputStream.ComputeInt32Size(RootGobjectId); + } + if (HasRootTagName) { + size += 1 + pb::CodedOutputStream.ComputeStringSize(RootTagName); + } + if (HasRootContainedPath) { + size += 1 + pb::CodedOutputStream.ComputeStringSize(RootContainedPath); + } + if (maxDepth_ != null) { + size += _single_maxDepth_codec.CalculateSizeWithTag(MaxDepth); + } + size += categoryIds_.CalculateSize(_repeated_categoryIds_codec); + size += templateChainContains_.CalculateSize(_repeated_templateChainContains_codec); + if (TagNameGlob.Length != 0) { + size += 1 + pb::CodedOutputStream.ComputeStringSize(TagNameGlob); + } + if (HasIncludeAttributes) { + size += 1 + 1; + } + if (AlarmBearingOnly != false) { + size += 1 + 1; + } + if (HistorizedOnly != false) { + size += 1 + 1; + } if (_unknownFields != null) { size += _unknownFields.CalculateSize(); } @@ -963,6 +1388,43 @@ namespace MxGateway.Contracts.Proto.Galaxy { if (other == null) { return; } + if (other.PageSize != 0) { + PageSize = other.PageSize; + } + if (other.PageToken.Length != 0) { + PageToken = other.PageToken; + } + if (other.maxDepth_ != null) { + if (maxDepth_ == null || other.MaxDepth != 0) { + MaxDepth = other.MaxDepth; + } + } + categoryIds_.Add(other.categoryIds_); + templateChainContains_.Add(other.templateChainContains_); + if (other.TagNameGlob.Length != 0) { + TagNameGlob = other.TagNameGlob; + } + if (other.HasIncludeAttributes) { + IncludeAttributes = other.IncludeAttributes; + } + if (other.AlarmBearingOnly != false) { + AlarmBearingOnly = other.AlarmBearingOnly; + } + if (other.HistorizedOnly != false) { + HistorizedOnly = other.HistorizedOnly; + } + switch (other.RootCase) { + case RootOneofCase.RootGobjectId: + RootGobjectId = other.RootGobjectId; + break; + case RootOneofCase.RootTagName: + RootTagName = other.RootTagName; + break; + case RootOneofCase.RootContainedPath: + RootContainedPath = other.RootContainedPath; + break; + } + _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); } @@ -982,6 +1444,58 @@ namespace MxGateway.Contracts.Proto.Galaxy { default: _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input); break; + case 8: { + PageSize = input.ReadInt32(); + break; + } + case 18: { + PageToken = input.ReadString(); + break; + } + case 24: { + RootGobjectId = input.ReadInt32(); + break; + } + case 34: { + RootTagName = input.ReadString(); + break; + } + case 42: { + RootContainedPath = input.ReadString(); + break; + } + case 50: { + int? value = _single_maxDepth_codec.Read(input); + if (maxDepth_ == null || value != 0) { + MaxDepth = value; + } + break; + } + case 58: + case 56: { + categoryIds_.AddEntriesFrom(input, _repeated_categoryIds_codec); + break; + } + case 66: { + templateChainContains_.AddEntriesFrom(input, _repeated_templateChainContains_codec); + break; + } + case 74: { + TagNameGlob = input.ReadString(); + break; + } + case 80: { + IncludeAttributes = input.ReadBool(); + break; + } + case 88: { + AlarmBearingOnly = input.ReadBool(); + break; + } + case 96: { + HistorizedOnly = input.ReadBool(); + break; + } } } #endif @@ -1001,6 +1515,58 @@ namespace MxGateway.Contracts.Proto.Galaxy { default: _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input); break; + case 8: { + PageSize = input.ReadInt32(); + break; + } + case 18: { + PageToken = input.ReadString(); + break; + } + case 24: { + RootGobjectId = input.ReadInt32(); + break; + } + case 34: { + RootTagName = input.ReadString(); + break; + } + case 42: { + RootContainedPath = input.ReadString(); + break; + } + case 50: { + int? value = _single_maxDepth_codec.Read(ref input); + if (maxDepth_ == null || value != 0) { + MaxDepth = value; + } + break; + } + case 58: + case 56: { + categoryIds_.AddEntriesFrom(ref input, _repeated_categoryIds_codec); + break; + } + case 66: { + templateChainContains_.AddEntriesFrom(ref input, _repeated_templateChainContains_codec); + break; + } + case 74: { + TagNameGlob = input.ReadString(); + break; + } + case 80: { + IncludeAttributes = input.ReadBool(); + break; + } + case 88: { + AlarmBearingOnly = input.ReadBool(); + break; + } + case 96: { + HistorizedOnly = input.ReadBool(); + break; + } } } } @@ -1044,6 +1610,8 @@ namespace MxGateway.Contracts.Proto.Galaxy { [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public DiscoverHierarchyReply(DiscoverHierarchyReply other) : this() { objects_ = other.objects_.Clone(); + nextPageToken_ = other.nextPageToken_; + totalObjectCount_ = other.totalObjectCount_; _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); } @@ -1064,6 +1632,36 @@ namespace MxGateway.Contracts.Proto.Galaxy { get { return objects_; } } + /// Field number for the "next_page_token" field. + public const int NextPageTokenFieldNumber = 2; + private string nextPageToken_ = ""; + /// + /// Non-empty when another page is available. + /// + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public string NextPageToken { + get { return nextPageToken_; } + set { + nextPageToken_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); + } + } + + /// Field number for the "total_object_count" field. + public const int TotalObjectCountFieldNumber = 3; + private int totalObjectCount_; + /// + /// Total number of objects in the cached hierarchy at the time of the call. + /// + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public int TotalObjectCount { + get { return totalObjectCount_; } + set { + totalObjectCount_ = value; + } + } + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public override bool Equals(object other) { @@ -1080,6 +1678,8 @@ namespace MxGateway.Contracts.Proto.Galaxy { return true; } if(!objects_.Equals(other.objects_)) return false; + if (NextPageToken != other.NextPageToken) return false; + if (TotalObjectCount != other.TotalObjectCount) return false; return Equals(_unknownFields, other._unknownFields); } @@ -1088,6 +1688,8 @@ namespace MxGateway.Contracts.Proto.Galaxy { public override int GetHashCode() { int hash = 1; hash ^= objects_.GetHashCode(); + if (NextPageToken.Length != 0) hash ^= NextPageToken.GetHashCode(); + if (TotalObjectCount != 0) hash ^= TotalObjectCount.GetHashCode(); if (_unknownFields != null) { hash ^= _unknownFields.GetHashCode(); } @@ -1107,6 +1709,14 @@ namespace MxGateway.Contracts.Proto.Galaxy { output.WriteRawMessage(this); #else objects_.WriteTo(output, _repeated_objects_codec); + if (NextPageToken.Length != 0) { + output.WriteRawTag(18); + output.WriteString(NextPageToken); + } + if (TotalObjectCount != 0) { + output.WriteRawTag(24); + output.WriteInt32(TotalObjectCount); + } if (_unknownFields != null) { _unknownFields.WriteTo(output); } @@ -1118,6 +1728,14 @@ namespace MxGateway.Contracts.Proto.Galaxy { [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) { objects_.WriteTo(ref output, _repeated_objects_codec); + if (NextPageToken.Length != 0) { + output.WriteRawTag(18); + output.WriteString(NextPageToken); + } + if (TotalObjectCount != 0) { + output.WriteRawTag(24); + output.WriteInt32(TotalObjectCount); + } if (_unknownFields != null) { _unknownFields.WriteTo(ref output); } @@ -1129,6 +1747,12 @@ namespace MxGateway.Contracts.Proto.Galaxy { public int CalculateSize() { int size = 0; size += objects_.CalculateSize(_repeated_objects_codec); + if (NextPageToken.Length != 0) { + size += 1 + pb::CodedOutputStream.ComputeStringSize(NextPageToken); + } + if (TotalObjectCount != 0) { + size += 1 + pb::CodedOutputStream.ComputeInt32Size(TotalObjectCount); + } if (_unknownFields != null) { size += _unknownFields.CalculateSize(); } @@ -1142,6 +1766,12 @@ namespace MxGateway.Contracts.Proto.Galaxy { return; } objects_.Add(other.objects_); + if (other.NextPageToken.Length != 0) { + NextPageToken = other.NextPageToken; + } + if (other.TotalObjectCount != 0) { + TotalObjectCount = other.TotalObjectCount; + } _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); } @@ -1165,6 +1795,14 @@ namespace MxGateway.Contracts.Proto.Galaxy { objects_.AddEntriesFrom(input, _repeated_objects_codec); break; } + case 18: { + NextPageToken = input.ReadString(); + break; + } + case 24: { + TotalObjectCount = input.ReadInt32(); + break; + } } } #endif @@ -1188,6 +1826,14 @@ namespace MxGateway.Contracts.Proto.Galaxy { objects_.AddEntriesFrom(ref input, _repeated_objects_codec); break; } + case 18: { + NextPageToken = input.ReadString(); + break; + } + case 24: { + TotalObjectCount = input.ReadInt32(); + break; + } } } } diff --git a/src/MxGateway.Contracts/Protos/galaxy_repository.proto b/src/MxGateway.Contracts/Protos/galaxy_repository.proto index c4a87f5..3701f04 100644 --- a/src/MxGateway.Contracts/Protos/galaxy_repository.proto +++ b/src/MxGateway.Contracts/Protos/galaxy_repository.proto @@ -5,6 +5,7 @@ package galaxy_repository.v1; option csharp_namespace = "MxGateway.Contracts.Proto.Galaxy"; import "google/protobuf/timestamp.proto"; +import "google/protobuf/wrappers.proto"; // Read-only browse over the AVEVA System Platform Galaxy Repository (ZB SQL // database). Lets clients enumerate the deployed object hierarchy and each @@ -37,10 +38,42 @@ message GetLastDeployTimeReply { google.protobuf.Timestamp time_of_last_deploy = 2; } -message DiscoverHierarchyRequest {} +message DiscoverHierarchyRequest { + // Maximum number of objects to return. The server applies its default when + // unset and rejects non-positive values. + int32 page_size = 1; + // Opaque token returned by a previous DiscoverHierarchy response. + string page_token = 2; + // Optional. When set, return only this object and its descendants. + // Empty = full hierarchy. + oneof root { + int32 root_gobject_id = 3; + string root_tag_name = 4; + string root_contained_path = 5; + } + // Optional. Cap on descendant depth from root. Zero returns only the root. + // Unset means unlimited depth. + google.protobuf.Int32Value max_depth = 6; + // Optional object category id filters. + repeated int32 category_ids = 7; + // Optional case-insensitive substring filters against template names. + repeated string template_chain_contains = 8; + // Optional anchored, case-insensitive glob over object tag_name. + string tag_name_glob = 9; + // Optional. Unset or true includes attributes. False returns object skeletons. + optional bool include_attributes = 10; + // Optional. Return only objects with at least one alarm-bearing attribute. + bool alarm_bearing_only = 11; + // Optional. Return only objects with at least one historized attribute. + bool historized_only = 12; +} message DiscoverHierarchyReply { repeated GalaxyObject objects = 1; + // Non-empty when another page is available. + string next_page_token = 2; + // Total number of objects in the cached hierarchy at the time of the call. + int32 total_object_count = 3; } message WatchDeployEventsRequest { diff --git a/src/MxGateway.IntegrationTests/WorkerLiveMxAccessSmokeTests.cs b/src/MxGateway.IntegrationTests/WorkerLiveMxAccessSmokeTests.cs index 951fc50..a20fa94 100644 --- a/src/MxGateway.IntegrationTests/WorkerLiveMxAccessSmokeTests.cs +++ b/src/MxGateway.IntegrationTests/WorkerLiveMxAccessSmokeTests.cs @@ -9,6 +9,7 @@ using MxGateway.Contracts.Proto; using MxGateway.Server.Configuration; using MxGateway.Server.Grpc; using MxGateway.Server.Metrics; +using MxGateway.Server.Security.Authentication; using MxGateway.Server.Security.Authorization; using MxGateway.Server.Sessions; using MxGateway.Server.Workers; @@ -260,6 +261,7 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output) Service = new MxAccessGatewayService( sessionManager, new GatewayRequestIdentityAccessor(), + new AllowAllConstraintEnforcer(), new MxAccessGrpcRequestValidator(), mapper, eventStreamService, @@ -592,4 +594,33 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output) } } } + + private sealed class AllowAllConstraintEnforcer : IConstraintEnforcer + { + public Task CheckReadTagAsync( + ApiKeyIdentity? identity, + string tagAddress, + CancellationToken cancellationToken) => Task.FromResult(null); + + public Task CheckReadHandleAsync( + ApiKeyIdentity? identity, + GatewaySession session, + int serverHandle, + int itemHandle, + CancellationToken cancellationToken) => Task.FromResult(null); + + public Task CheckWriteHandleAsync( + ApiKeyIdentity? identity, + GatewaySession session, + int serverHandle, + int itemHandle, + CancellationToken cancellationToken) => Task.FromResult(null); + + public Task RecordDenialAsync( + ApiKeyIdentity? identity, + string commandKind, + string target, + ConstraintFailure failure, + CancellationToken cancellationToken) => Task.CompletedTask; + } } diff --git a/src/MxGateway.Server/Configuration/EffectiveGatewayConfiguration.cs b/src/MxGateway.Server/Configuration/EffectiveGatewayConfiguration.cs index 5782537..e06096b 100644 --- a/src/MxGateway.Server/Configuration/EffectiveGatewayConfiguration.cs +++ b/src/MxGateway.Server/Configuration/EffectiveGatewayConfiguration.cs @@ -2,6 +2,7 @@ namespace MxGateway.Server.Configuration; public sealed record EffectiveGatewayConfiguration( EffectiveAuthenticationConfiguration Authentication, + EffectiveLdapConfiguration Ldap, EffectiveWorkerConfiguration Worker, EffectiveSessionConfiguration Sessions, EffectiveEventConfiguration Events, diff --git a/src/MxGateway.Server/Configuration/EffectiveProtocolConfiguration.cs b/src/MxGateway.Server/Configuration/EffectiveProtocolConfiguration.cs index 00b0d7e..c54e184 100644 --- a/src/MxGateway.Server/Configuration/EffectiveProtocolConfiguration.cs +++ b/src/MxGateway.Server/Configuration/EffectiveProtocolConfiguration.cs @@ -1,3 +1,5 @@ namespace MxGateway.Server.Configuration; -public sealed record EffectiveProtocolConfiguration(uint WorkerProtocolVersion); +public sealed record EffectiveProtocolConfiguration( + uint WorkerProtocolVersion, + int MaxGrpcMessageBytes); diff --git a/src/MxGateway.Server/Configuration/EffectiveSessionConfiguration.cs b/src/MxGateway.Server/Configuration/EffectiveSessionConfiguration.cs index 24e9e74..9753c20 100644 --- a/src/MxGateway.Server/Configuration/EffectiveSessionConfiguration.cs +++ b/src/MxGateway.Server/Configuration/EffectiveSessionConfiguration.cs @@ -3,4 +3,7 @@ namespace MxGateway.Server.Configuration; public sealed record EffectiveSessionConfiguration( int DefaultCommandTimeoutSeconds, int MaxSessions, + int MaxPendingCommandsPerSession, + int DefaultLeaseSeconds, + int LeaseSweepIntervalSeconds, bool AllowMultipleEventSubscribers); diff --git a/src/MxGateway.Server/Configuration/GatewayConfigurationProvider.cs b/src/MxGateway.Server/Configuration/GatewayConfigurationProvider.cs index 868954c..63d4112 100644 --- a/src/MxGateway.Server/Configuration/GatewayConfigurationProvider.cs +++ b/src/MxGateway.Server/Configuration/GatewayConfigurationProvider.cs @@ -19,6 +19,19 @@ public sealed class GatewayConfigurationProvider(IOptions option SqlitePath: value.Authentication.SqlitePath, PepperSecretName: RedactedValue, RunMigrationsOnStartup: value.Authentication.RunMigrationsOnStartup), + Ldap: new EffectiveLdapConfiguration( + Enabled: value.Ldap.Enabled, + Server: value.Ldap.Server, + Port: value.Ldap.Port, + UseTls: value.Ldap.UseTls, + AllowInsecureLdap: value.Ldap.AllowInsecureLdap, + SearchBase: value.Ldap.SearchBase, + ServiceAccountDn: value.Ldap.ServiceAccountDn, + ServiceAccountPassword: RedactedValue, + UserNameAttribute: value.Ldap.UserNameAttribute, + DisplayNameAttribute: value.Ldap.DisplayNameAttribute, + GroupAttribute: value.Ldap.GroupAttribute, + RequiredGroup: value.Ldap.RequiredGroup), Worker: new EffectiveWorkerConfiguration( ExecutablePath: value.Worker.ExecutablePath, WorkingDirectory: value.Worker.WorkingDirectory, @@ -31,6 +44,9 @@ public sealed class GatewayConfigurationProvider(IOptions option Sessions: new EffectiveSessionConfiguration( DefaultCommandTimeoutSeconds: value.Sessions.DefaultCommandTimeoutSeconds, MaxSessions: value.Sessions.MaxSessions, + MaxPendingCommandsPerSession: value.Sessions.MaxPendingCommandsPerSession, + DefaultLeaseSeconds: value.Sessions.DefaultLeaseSeconds, + LeaseSweepIntervalSeconds: value.Sessions.LeaseSweepIntervalSeconds, AllowMultipleEventSubscribers: value.Sessions.AllowMultipleEventSubscribers), Events: new EffectiveEventConfiguration( QueueCapacity: value.Events.QueueCapacity, @@ -44,6 +60,8 @@ public sealed class GatewayConfigurationProvider(IOptions option RecentFaultLimit: value.Dashboard.RecentFaultLimit, RecentSessionLimit: value.Dashboard.RecentSessionLimit, ShowTagValues: value.Dashboard.ShowTagValues), - Protocol: new EffectiveProtocolConfiguration(value.Protocol.WorkerProtocolVersion)); + Protocol: new EffectiveProtocolConfiguration( + value.Protocol.WorkerProtocolVersion, + value.Protocol.MaxGrpcMessageBytes)); } } diff --git a/src/MxGateway.Server/Configuration/GatewayOptions.cs b/src/MxGateway.Server/Configuration/GatewayOptions.cs index 5c735b0..d2a7713 100644 --- a/src/MxGateway.Server/Configuration/GatewayOptions.cs +++ b/src/MxGateway.Server/Configuration/GatewayOptions.cs @@ -9,6 +9,8 @@ public sealed class GatewayOptions /// public AuthenticationOptions Authentication { get; init; } = new(); + public LdapOptions Ldap { get; init; } = new(); + /// /// Gets worker process configuration options. /// diff --git a/src/MxGateway.Server/Configuration/GatewayOptionsValidator.cs b/src/MxGateway.Server/Configuration/GatewayOptionsValidator.cs index 4cd0aaa..3c1b459 100644 --- a/src/MxGateway.Server/Configuration/GatewayOptionsValidator.cs +++ b/src/MxGateway.Server/Configuration/GatewayOptionsValidator.cs @@ -19,6 +19,7 @@ public sealed class GatewayOptionsValidator : IValidateOptions List failures = []; ValidateAuthentication(options.Authentication, failures); + ValidateLdap(options.Ldap, failures); ValidateWorker(options.Worker, failures); ValidateSessions(options.Sessions, failures); ValidateEvents(options.Events, failures); @@ -55,6 +56,47 @@ public sealed class GatewayOptionsValidator : IValidateOptions } } + private static void ValidateLdap(LdapOptions options, List failures) + { + if (!options.Enabled) + { + return; + } + + AddIfBlank(options.Server, "MxGateway:Ldap:Server is required when LDAP login is enabled.", failures); + AddIfBlank(options.SearchBase, "MxGateway:Ldap:SearchBase is required when LDAP login is enabled.", failures); + AddIfBlank( + options.ServiceAccountDn, + "MxGateway:Ldap:ServiceAccountDn is required when LDAP login is enabled.", + failures); + AddIfBlank( + options.ServiceAccountPassword, + "MxGateway:Ldap:ServiceAccountPassword is required when LDAP login is enabled.", + failures); + AddIfBlank( + options.UserNameAttribute, + "MxGateway:Ldap:UserNameAttribute is required when LDAP login is enabled.", + failures); + AddIfBlank( + options.DisplayNameAttribute, + "MxGateway:Ldap:DisplayNameAttribute is required when LDAP login is enabled.", + failures); + AddIfBlank( + options.GroupAttribute, + "MxGateway:Ldap:GroupAttribute is required when LDAP login is enabled.", + failures); + AddIfBlank( + options.RequiredGroup, + "MxGateway:Ldap:RequiredGroup is required when LDAP login is enabled.", + failures); + AddIfNotPositive(options.Port, "MxGateway:Ldap:Port must be greater than zero.", failures); + + if (!options.UseTls && !options.AllowInsecureLdap) + { + failures.Add("MxGateway:Ldap:AllowInsecureLdap must be true when UseTls is false."); + } + } + private static void ValidateWorker(WorkerOptions options, List failures) { AddIfBlank(options.ExecutablePath, "MxGateway:Worker:ExecutablePath is required.", failures); @@ -135,6 +177,14 @@ public sealed class GatewayOptionsValidator : IValidateOptions options.MaxPendingCommandsPerSession, "MxGateway:Sessions:MaxPendingCommandsPerSession must be greater than zero.", failures); + AddIfNotPositive( + options.DefaultLeaseSeconds, + "MxGateway:Sessions:DefaultLeaseSeconds must be greater than zero.", + failures); + AddIfNotPositive( + options.LeaseSweepIntervalSeconds, + "MxGateway:Sessions:LeaseSweepIntervalSeconds must be greater than zero.", + failures); if (options.AllowMultipleEventSubscribers) { @@ -185,6 +235,12 @@ public sealed class GatewayOptionsValidator : IValidateOptions failures.Add( $"MxGateway:Protocol:WorkerProtocolVersion must be {GatewayContractInfo.WorkerProtocolVersion}."); } + + if (options.MaxGrpcMessageBytes is < MinimumMaxMessageBytes or > MaximumMaxMessageBytes) + { + failures.Add( + $"MxGateway:Protocol:MaxGrpcMessageBytes must be between {MinimumMaxMessageBytes} and {MaximumMaxMessageBytes}."); + } } private static void AddIfBlank(string? value, string message, List failures) diff --git a/src/MxGateway.Server/Configuration/ProtocolOptions.cs b/src/MxGateway.Server/Configuration/ProtocolOptions.cs index f569a91..9cf6816 100644 --- a/src/MxGateway.Server/Configuration/ProtocolOptions.cs +++ b/src/MxGateway.Server/Configuration/ProtocolOptions.cs @@ -11,4 +11,6 @@ public sealed class ProtocolOptions /// Gets or sets the worker protocol version. /// public uint WorkerProtocolVersion { get; init; } = GatewayContractInfo.WorkerProtocolVersion; + + public int MaxGrpcMessageBytes { get; init; } = 16 * 1024 * 1024; } diff --git a/src/MxGateway.Server/Configuration/SessionOptions.cs b/src/MxGateway.Server/Configuration/SessionOptions.cs index 5b31638..f74539b 100644 --- a/src/MxGateway.Server/Configuration/SessionOptions.cs +++ b/src/MxGateway.Server/Configuration/SessionOptions.cs @@ -17,6 +17,10 @@ public sealed class SessionOptions /// public int MaxPendingCommandsPerSession { get; init; } = 128; + public int DefaultLeaseSeconds { get; init; } = 1800; + + public int LeaseSweepIntervalSeconds { get; init; } = 30; + /// /// Gets a value indicating whether multiple event subscribers are allowed per session. /// diff --git a/src/MxGateway.Server/Dashboard/Components/Layout/DashboardLayout.razor b/src/MxGateway.Server/Dashboard/Components/Layout/DashboardLayout.razor index 1a9106e..09dfa1e 100644 --- a/src/MxGateway.Server/Dashboard/Components/Layout/DashboardLayout.razor +++ b/src/MxGateway.Server/Dashboard/Components/Layout/DashboardLayout.razor @@ -26,14 +26,27 @@ + -
- - - + + +
+ @authState.User.Identity?.Name +
+ + + +
+
+ + Sign in + +
diff --git a/src/MxGateway.Server/Dashboard/Components/Pages/GalaxyPage.razor b/src/MxGateway.Server/Dashboard/Components/Pages/GalaxyPage.razor index a858e77..b9f64fd 100644 --- a/src/MxGateway.Server/Dashboard/Components/Pages/GalaxyPage.razor +++ b/src/MxGateway.Server/Dashboard/Components/Pages/GalaxyPage.razor @@ -190,6 +190,8 @@ else private int CommandTimeoutSeconds() => GalaxyOptions.Value.CommandTimeoutSeconds; - private string? GalaxyConnectionStringDisplay() => - DashboardRedactor.Redact(GalaxyOptions.Value.ConnectionString); + private string GalaxyConnectionStringDisplay() + { + return DashboardConnectionStringDisplay.GalaxyRepositoryConnectionString(GalaxyOptions.Value.ConnectionString); + } } diff --git a/src/MxGateway.Server/Dashboard/Components/Pages/SettingsPage.razor b/src/MxGateway.Server/Dashboard/Components/Pages/SettingsPage.razor index 46ddfdd..ebf9930 100644 --- a/src/MxGateway.Server/Dashboard/Components/Pages/SettingsPage.razor +++ b/src/MxGateway.Server/Dashboard/Components/Pages/SettingsPage.razor @@ -25,6 +25,15 @@ else Auth database@Snapshot.Configuration.Authentication.SqlitePath Pepper secret@Snapshot.Configuration.Authentication.PepperSecretName Run migrations@Snapshot.Configuration.Authentication.RunMigrationsOnStartup + LDAP enabled@Snapshot.Configuration.Ldap.Enabled + LDAP server@Snapshot.Configuration.Ldap.Server:@Snapshot.Configuration.Ldap.Port + LDAP TLS@Snapshot.Configuration.Ldap.UseTls + LDAP search base@Snapshot.Configuration.Ldap.SearchBase + LDAP service account@Snapshot.Configuration.Ldap.ServiceAccountDn + LDAP service password@Snapshot.Configuration.Ldap.ServiceAccountPassword + LDAP username attribute@Snapshot.Configuration.Ldap.UserNameAttribute + LDAP group attribute@Snapshot.Configuration.Ldap.GroupAttribute + LDAP required group@Snapshot.Configuration.Ldap.RequiredGroup Worker executable@Snapshot.Configuration.Worker.ExecutablePath Worker architecture@Snapshot.Configuration.Worker.RequiredArchitecture Startup timeout@Snapshot.Configuration.Worker.StartupTimeoutSeconds seconds diff --git a/src/MxGateway.Server/Dashboard/Components/_Imports.razor b/src/MxGateway.Server/Dashboard/Components/_Imports.razor index 56c742a..fbfa9f0 100644 --- a/src/MxGateway.Server/Dashboard/Components/_Imports.razor +++ b/src/MxGateway.Server/Dashboard/Components/_Imports.razor @@ -8,5 +8,6 @@ @using MxGateway.Server.Dashboard @using MxGateway.Server.Dashboard.Components.Layout @using MxGateway.Server.Dashboard.Components.Shared +@using MxGateway.Server.Security.Authorization @using MxGateway.Server.Workers @using static Microsoft.AspNetCore.Components.Web.RenderMode diff --git a/src/MxGateway.Server/Dashboard/DashboardAuthenticationDefaults.cs b/src/MxGateway.Server/Dashboard/DashboardAuthenticationDefaults.cs index 84cea7a..aac0b96 100644 --- a/src/MxGateway.Server/Dashboard/DashboardAuthenticationDefaults.cs +++ b/src/MxGateway.Server/Dashboard/DashboardAuthenticationDefaults.cs @@ -5,6 +5,7 @@ public static class DashboardAuthenticationDefaults public const string AuthenticationScheme = "MxGateway.Dashboard"; public const string AuthorizationPolicy = "MxGateway.Dashboard"; public const string ScopeClaimType = "scope"; + public const string LdapGroupClaimType = "mxgateway:ldap_group"; public const string KeyPrefixClaimType = "mxgateway:key_prefix"; public const string CookieName = "__Host-MxGatewayDashboard"; } diff --git a/src/MxGateway.Server/Dashboard/DashboardAuthenticator.cs b/src/MxGateway.Server/Dashboard/DashboardAuthenticator.cs index 5dda85a..f985ba9 100644 --- a/src/MxGateway.Server/Dashboard/DashboardAuthenticator.cs +++ b/src/MxGateway.Server/Dashboard/DashboardAuthenticator.cs @@ -1,81 +1,258 @@ using System.Security.Claims; +using System.Text; using Microsoft.Extensions.Options; using MxGateway.Server.Configuration; -using MxGateway.Server.Security.Authentication; -using MxGateway.Server.Security.Authorization; +using Novell.Directory.Ldap; namespace MxGateway.Server.Dashboard; public sealed class DashboardAuthenticator( - IApiKeyVerifier apiKeyVerifier, - IOptions options) : IDashboardAuthenticator + IOptions options, + ILogger logger) : IDashboardAuthenticator { - private const string GenericFailureMessage = "The API key is invalid or is not authorized for dashboard access."; + private const string GenericFailureMessage = "The username or password is invalid, or the user is not authorized."; /// public async Task AuthenticateAsync( - string? apiKey, + string? username, + string? password, CancellationToken cancellationToken) { - if (options.Value.Authentication.Mode == AuthenticationMode.Disabled) - { - return DashboardAuthenticationResult.Success(CreatePrincipal(new ApiKeyIdentity( - KeyId: "authentication-disabled", - KeyPrefix: "authentication-disabled", - DisplayName: "Authentication Disabled", - Scopes: new HashSet([GatewayScopes.Admin], StringComparer.Ordinal)))); - } - - if (string.IsNullOrWhiteSpace(apiKey)) + LdapOptions ldapOptions = options.Value.Ldap; + if (!ldapOptions.Enabled + || string.IsNullOrWhiteSpace(username) + || string.IsNullOrWhiteSpace(password)) { return DashboardAuthenticationResult.Fail(GenericFailureMessage); } - ApiKeyVerificationResult verificationResult = await apiKeyVerifier - .VerifyAsync(FormatAuthorizationHeader(apiKey), cancellationToken) + if (!ldapOptions.UseTls && !ldapOptions.AllowInsecureLdap) + { + return DashboardAuthenticationResult.Fail(GenericFailureMessage); + } + + string normalizedUsername = username.Trim(); + + try + { + using LdapConnection connection = new(); + connection.SecureSocketLayer = ldapOptions.UseTls; + + await Task.Run( + () => connection.Connect(ldapOptions.Server, ldapOptions.Port), + cancellationToken) + .ConfigureAwait(false); + + await BindServiceAccountAsync(connection, ldapOptions, cancellationToken).ConfigureAwait(false); + LdapEntry? candidate = await SearchUserAsync( + connection, + ldapOptions, + normalizedUsername, + cancellationToken) + .ConfigureAwait(false); + + if (candidate is null) + { + return DashboardAuthenticationResult.Fail(GenericFailureMessage); + } + + await Task.Run( + () => connection.Bind(candidate.Dn, password), + cancellationToken) + .ConfigureAwait(false); + + await BindServiceAccountAsync(connection, ldapOptions, cancellationToken).ConfigureAwait(false); + LdapEntry? authenticatedEntry = await SearchUserAsync( + connection, + ldapOptions, + normalizedUsername, + cancellationToken) + .ConfigureAwait(false); + + if (authenticatedEntry is null) + { + return DashboardAuthenticationResult.Fail(GenericFailureMessage); + } + + string displayName = ReadAttribute(authenticatedEntry, ldapOptions.DisplayNameAttribute) + ?? normalizedUsername; + IReadOnlyList groups = ReadAttributeValues(authenticatedEntry, ldapOptions.GroupAttribute); + + if (!IsMemberOfRequiredGroup(groups, ldapOptions.RequiredGroup)) + { + logger.LogInformation( + "LDAP dashboard login denied for user {User}: missing required group {RequiredGroup}.", + normalizedUsername, + ldapOptions.RequiredGroup); + + return DashboardAuthenticationResult.Fail(GenericFailureMessage); + } + + return DashboardAuthenticationResult.Success(CreatePrincipal( + normalizedUsername, + displayName, + groups)); + } + catch (OperationCanceledException) + { + throw; + } + catch (LdapException ex) + { + logger.LogInformation( + "LDAP dashboard login rejected for user {User}: result code {ResultCode}.", + normalizedUsername, + ex.ResultCode); + + return DashboardAuthenticationResult.Fail(GenericFailureMessage); + } + catch (Exception ex) + { + logger.LogError(ex, "Unexpected LDAP dashboard login error for user {User}.", normalizedUsername); + + return DashboardAuthenticationResult.Fail(GenericFailureMessage); + } + } + + internal static string EscapeLdapFilter(string value) + { + StringBuilder builder = new(value.Length); + foreach (char character in value) + { + builder.Append(character switch + { + '\\' => @"\5c", + '*' => @"\2a", + '(' => @"\28", + ')' => @"\29", + '\0' => @"\00", + _ => character.ToString() + }); + } + + return builder.ToString(); + } + + internal static bool IsMemberOfRequiredGroup(IEnumerable groups, string requiredGroup) + { + string normalizedRequiredGroup = requiredGroup.Trim(); + if (string.IsNullOrWhiteSpace(normalizedRequiredGroup)) + { + return false; + } + + foreach (string group in groups) + { + string normalizedGroup = group.Trim(); + if (string.Equals(normalizedGroup, normalizedRequiredGroup, StringComparison.OrdinalIgnoreCase) + || string.Equals( + ExtractFirstRdnValue(normalizedGroup), + normalizedRequiredGroup, + StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + return false; + } + + internal static string ExtractFirstRdnValue(string distinguishedName) + { + int equalsIndex = distinguishedName.IndexOf('='); + if (equalsIndex < 0) + { + return distinguishedName; + } + + int valueStart = equalsIndex + 1; + int commaIndex = distinguishedName.IndexOf(',', valueStart); + + return commaIndex > valueStart + ? distinguishedName[valueStart..commaIndex] + : distinguishedName[valueStart..]; + } + + private static Task BindServiceAccountAsync( + LdapConnection connection, + LdapOptions ldapOptions, + CancellationToken cancellationToken) + { + return Task.Run( + () => connection.Bind(ldapOptions.ServiceAccountDn, ldapOptions.ServiceAccountPassword), + cancellationToken); + } + + private static async Task SearchUserAsync( + LdapConnection connection, + LdapOptions ldapOptions, + string username, + CancellationToken cancellationToken) + { + string filter = $"({ldapOptions.UserNameAttribute}={EscapeLdapFilter(username)})"; + ILdapSearchResults results = await Task.Run( + () => connection.Search( + ldapOptions.SearchBase, + LdapConnection.ScopeSub, + filter, + attrs: null, + typesOnly: false), + cancellationToken) .ConfigureAwait(false); - if (!verificationResult.Succeeded || verificationResult.Identity is null) + LdapEntry? entry = null; + while (results.HasMore()) { - return DashboardAuthenticationResult.Fail(GenericFailureMessage); + LdapEntry next = results.Next(); + if (entry is not null) + { + return null; + } + + entry = next; } - if (options.Value.Dashboard.RequireAdminScope - && !verificationResult.Identity.Scopes.Contains(GatewayScopes.Admin)) - { - return DashboardAuthenticationResult.Fail(GenericFailureMessage); - } - - return DashboardAuthenticationResult.Success(CreatePrincipal(verificationResult.Identity)); + return entry; } - private static string FormatAuthorizationHeader(string apiKey) + private static string? ReadAttribute(LdapEntry entry, string attributeName) { - string trimmedApiKey = apiKey.Trim(); - - return trimmedApiKey.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase) - ? trimmedApiKey - : $"Bearer {trimmedApiKey}"; + return ReadLdapAttribute(entry, attributeName)?.StringValue; } - private static ClaimsPrincipal CreatePrincipal(ApiKeyIdentity identity) + private static IReadOnlyList ReadAttributeValues(LdapEntry entry, string attributeName) + { + LdapAttribute? attribute = ReadLdapAttribute(entry, attributeName); + return attribute?.StringValueArray ?? []; + } + + private static LdapAttribute? ReadLdapAttribute(LdapEntry entry, string attributeName) + { + return entry.GetAttribute(attributeName) + ?? entry.GetAttribute(attributeName.ToLowerInvariant()) + ?? entry.GetAttribute(attributeName.ToUpperInvariant()); + } + + private static ClaimsPrincipal CreatePrincipal( + string username, + string displayName, + IEnumerable groups) { List claims = [ - new Claim(ClaimTypes.NameIdentifier, identity.KeyId), - new Claim(ClaimTypes.Name, identity.DisplayName), - new Claim(DashboardAuthenticationDefaults.KeyPrefixClaimType, identity.KeyPrefix) + new Claim(ClaimTypes.NameIdentifier, username), + new Claim(ClaimTypes.Name, displayName) ]; - claims.AddRange(identity.Scopes.Select(scope => new Claim( - DashboardAuthenticationDefaults.ScopeClaimType, - scope))); + claims.AddRange(groups.Select(group => new Claim( + DashboardAuthenticationDefaults.LdapGroupClaimType, + group))); ClaimsIdentity claimsIdentity = new( claims, DashboardAuthenticationDefaults.AuthenticationScheme, ClaimTypes.Name, - DashboardAuthenticationDefaults.ScopeClaimType); + DashboardAuthenticationDefaults.LdapGroupClaimType); return new ClaimsPrincipal(claimsIdentity); } diff --git a/src/MxGateway.Server/Dashboard/DashboardEndpointRouteBuilderExtensions.cs b/src/MxGateway.Server/Dashboard/DashboardEndpointRouteBuilderExtensions.cs index d7ed4d8..71fcce4 100644 --- a/src/MxGateway.Server/Dashboard/DashboardEndpointRouteBuilderExtensions.cs +++ b/src/MxGateway.Server/Dashboard/DashboardEndpointRouteBuilderExtensions.cs @@ -43,18 +43,17 @@ public static class DashboardEndpointRouteBuilderExtensions dashboard.MapPost( "/logout", (HttpContext httpContext, IAntiforgery antiforgery) => PostLogoutAsync(httpContext, antiforgery, pathBase)) - .RequireAuthorization(DashboardAuthenticationDefaults.AuthorizationPolicy) + .AllowAnonymous() .WithName("DashboardLogout"); dashboard.MapGet("/denied", () => Results.Content( - RenderPage("Access denied", "

The signed-in API key is not authorized for dashboard access.

"), + RenderPage("Access denied", "

The signed-in user is not authorized for dashboard access.

"), "text/html")) .AllowAnonymous() .WithName("DashboardAccessDenied"); dashboard.MapRazorComponents() - .AddInteractiveServerRenderMode() - .RequireAuthorization(DashboardAuthenticationDefaults.AuthorizationPolicy); + .AddInteractiveServerRenderMode(); return endpoints; } @@ -89,7 +88,10 @@ public static class DashboardEndpointRouteBuilderExtensions pathBase); DashboardAuthenticationResult result = await authenticator - .AuthenticateAsync(form["apiKey"].ToString(), httpContext.RequestAborted) + .AuthenticateAsync( + form["username"].ToString(), + form["password"].ToString(), + httpContext.RequestAborted) .ConfigureAwait(false); if (!result.Succeeded || result.Principal is null) @@ -131,7 +133,7 @@ public static class DashboardEndpointRouteBuilderExtensions string requestToken = tokens.RequestToken ?? string.Empty; string alert = string.IsNullOrWhiteSpace(failureMessage) ? string.Empty - : $"

{HtmlEncoder.Default.Encode(failureMessage)}

"; + : $"

{HtmlEncoder.Default.Encode(failureMessage)}

"; string body = $"""