code: /*
   * Copyright (c) 2021 the Eclipse Milo Authors
   *
   * This program and the accompanying materials are made
   * available under the terms of the Eclipse Public License 2.0
   * which is available at https://www.eclipse.org/legal/epl-2.0/
   *
   * SPDX-License-Identifier: EPL-2.0
   */
  
  package org.eclipse.milo.opcua.stack.client;
  
  import java.util.List;
  import java.util.concurrent.CompletableFuture;
  import java.util.function.Consumer;
  
  import com.google.common.base.Strings;
  import org.eclipse.milo.opcua.stack.core.Stack;
  import org.eclipse.milo.opcua.stack.core.StatusCodes;
  import org.eclipse.milo.opcua.stack.core.UaException;
  import org.eclipse.milo.opcua.stack.core.security.SecurityPolicy;
  import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId;
  import org.eclipse.milo.opcua.stack.core.types.enumerated.MessageSecurityMode;
  import org.eclipse.milo.opcua.stack.core.types.structured.ApplicationDescription;
  import org.eclipse.milo.opcua.stack.core.types.structured.EndpointDescription;
  import org.eclipse.milo.opcua.stack.core.types.structured.FindServersRequest;
  import org.eclipse.milo.opcua.stack.core.types.structured.FindServersResponse;
  import org.eclipse.milo.opcua.stack.core.types.structured.GetEndpointsRequest;
  import org.eclipse.milo.opcua.stack.core.types.structured.GetEndpointsResponse;
  import org.eclipse.milo.opcua.stack.core.types.structured.RequestHeader;
  import org.eclipse.milo.opcua.stack.core.util.EndpointUtil;
  
  import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.ubyte;
  import static org.eclipse.milo.opcua.stack.core.util.ConversionUtil.l;
  import static org.eclipse.milo.opcua.stack.core.util.FutureUtils.failedFuture;
  
  public class DiscoveryClient {
  
      private final UaStackClient stackClient;
  
      public DiscoveryClient(UaStackClient stackClient) {
          this.stackClient = stackClient;
      }
  
      public CompletableFuture<DiscoveryClient> connect() {
          return stackClient.connect().thenApply(c -> DiscoveryClient.this);
      }
  
      public CompletableFuture<DiscoveryClient> disconnect() {
          return stackClient.disconnect().thenApply(c -> DiscoveryClient.this);
      }
  
      /**
       * Query the FindServers service at the {@code endpointUrl}.
       * <p>
       * The endpoint URL(s) for each server {@link ApplicationDescription} in the response can then be used in a
       * {@link #getEndpoints(String)} call to discover the endpoints for that server.
       *
       * @param endpointUrl the endpoint URL to find servers at.
       * @param localeIds   list of locales to use. The server should return the applicationName in the
       *                    ApplicationDescription using one of locales specified. If the server supports more than one of
       *                    the requested locales then the server shall use the locale that appears first in this list. If
       *                    the server does not support any of the requested locales it chooses an appropriate default
       *                    locale. The server chooses an appropriate default locale if this list is empty.
       * @param serverUris  list of servers to return. All known servers are returned if the list is empty.
       * @return the {@link FindServersResponse}s returned by the FindServers service.
       */
      public CompletableFuture<FindServersResponse> findServers(
          String endpointUrl,
          String[] localeIds,
          String[] serverUris
      ) {
  
          RequestHeader requestHeader = stackClient.newRequestHeader(
              NodeId.NULL_VALUE,
              stackClient.getConfig().getRequestTimeout()
          );
  
          FindServersRequest request = new FindServersRequest(
              requestHeader,
              endpointUrl,
              localeIds,
              serverUris
          );
  
          return stackClient.sendRequest(request)
              .thenApply(FindServersResponse.class::cast);
      }
  
      /**
       * Query the GetEndpoints service at {@code endpointUrl}.
       *
       * @param endpointUrl the endpoint URL to get endpoints from.
       * @param localeIds   list of locales to use. Specifies the locale to use when returning human-readable strings.
       * @param profileUris list of Transport Profile that the returned Endpoints shall support. All Endpoints are
       *                    returned if the list is empty.
       * @return the {@link GetEndpointsResponse} returned by the GetEndpoints service.
       */
      public CompletableFuture<GetEndpointsResponse> getEndpoints(
          String endpointUrl,
          String[] localeIds,
          String[] profileUris
      ) {
  
          RequestHeader header = stackClient.newRequestHeader(
              NodeId.NULL_VALUE,
              stackClient.getConfig().getRequestTimeout()
          );
  
          GetEndpointsRequest request = new GetEndpointsRequest(
              header,
              endpointUrl,
              localeIds,
              profileUris
          );
  
          return stackClient.sendRequest(request)
              .thenApply(GetEndpointsResponse.class::cast);
      }
  
      /**
       * Query the FindServers service at the {@code endpointUrl}.
       * <p>
       * The discovery URL(s) for each server {@link ApplicationDescription} in the response can then be used in a
       * {@link #getEndpoints(String)} call to discover the endpoints for that server.
       *
       * @param endpointUrl the endpoint URL to find servers at.
       * @return a List of {@link ApplicationDescription}s returned by the FindServers service.
       */
      public static CompletableFuture<List<ApplicationDescription>> findServers(String endpointUrl) {
          return findServers(endpointUrl, b -> {});
      }
  
      /**
       * Query the FindServers service at the {@code endpointUrl}.
       * <p>
       * The discovery URL(s) for each server {@link ApplicationDescription} in the response can then be used in a
       * {@link #getEndpoints(String)} call to discover the endpoints for that server.
       *
       * @param endpointUrl the endpoint URL to find servers at.
       * @param customizer  a {@link Consumer} that accepts a {@link UaStackClientConfigBuilder} for customization.
       * @return a List of {@link ApplicationDescription}s returned by the FindServers service.
       */
      public static CompletableFuture<List<ApplicationDescription>> findServers(
          String endpointUrl,
          Consumer<UaStackClientConfigBuilder> customizer
      ) {
  
          EndpointDescription endpoint = new EndpointDescription(
              endpointUrl,
              null,
              null,
              MessageSecurityMode.None,
              SecurityPolicy.None.getUri(),
              null,
              Stack.TCP_UASC_UABINARY_TRANSPORT_URI,
              ubyte(0)
          );
  
          UaStackClientConfigBuilder builder = UaStackClientConfig.builder();
          builder.setEndpoint(endpoint);
          customizer.accept(builder);
  
          UaStackClientConfig config = builder.build();
  
          try {
              UaStackClient stackClient = UaStackClient.create(config);
  
              DiscoveryClient discoveryClient = new DiscoveryClient(stackClient);
  
              return discoveryClient
                  .connect()
                  .thenCompose(c -> c.findServers(endpointUrl, new String[0], new String[0]))
                  .whenComplete((e, ex) -> discoveryClient.disconnect())
                  .thenApply(response -> l(response.getServers()));
          } catch (UaException e) {
              return failedFuture(e);
          }
      }
  
      /**
       * Query the GetEndpoints service at {@code endpointUrl}.
       *
       * @param endpointUrl the endpoint URL to get endpoints from.
       * @return a List of {@link EndpointDescription}s returned by the GetEndpoints service.
       */
      public static CompletableFuture<List<EndpointDescription>> getEndpoints(String endpointUrl) {
          return getEndpoints(endpointUrl, b -> {});
      }
  
      /**
       * Query the GetEndpoints service at {@code endpointUrl}.
       *
       * @param endpointUrl the endpoint URL to get endpoints from.
       * @param customizer  a {@link Consumer} that accepts a {@link UaStackClientConfigBuilder} for customization.
       * @return a List of {@link EndpointDescription}s returned by the GetEndpoints service.
       */
      public static CompletableFuture<List<EndpointDescription>> getEndpoints(
          String endpointUrl,
          Consumer<UaStackClientConfigBuilder> customizer
      ) {
  
          String scheme = EndpointUtil.getScheme(endpointUrl);
  
          String profileUri;
  
          switch (Strings.nullToEmpty(scheme).toLowerCase()) {
              case "opc.tcp":
                  profileUri = Stack.TCP_UASC_UABINARY_TRANSPORT_URI;
                  break;
  
              case "http":
              case "https":
              case "opc.http":
              case "opc.https":
                  profileUri = Stack.HTTPS_UABINARY_TRANSPORT_URI;
                  break;
  
              case "opc.ws":
              case "opc.wss":
                  profileUri = Stack.WSS_UASC_UABINARY_TRANSPORT_URI;
                  break;
  
              default:
                  return failedFuture(
                      new UaException(
                          StatusCodes.Bad_InternalError,
                          "unsupported protocol: " + scheme));
          }
  
          return getEndpoints(endpointUrl, profileUri, customizer);
      }
  
      private static CompletableFuture<List<EndpointDescription>> getEndpoints(
          String endpointUrl,
          String profileUri,
          Consumer<UaStackClientConfigBuilder> customizer
      ) {
  
          EndpointDescription endpoint = new EndpointDescription(
              endpointUrl,
              null,
              null,
              MessageSecurityMode.None,
              SecurityPolicy.None.getUri(),
              null,
              profileUri,
              ubyte(0)
          );
  
          UaStackClientConfigBuilder builder = UaStackClientConfig.builder();
          builder.setEndpoint(endpoint);
          customizer.accept(builder);
  
          UaStackClientConfig config = builder.build();
  
          try {
              UaStackClient stackClient = UaStackClient.create(config);
  
              DiscoveryClient discoveryClient = new DiscoveryClient(stackClient);
  
              return discoveryClient
                  .connect()
                  .thenCompose(c -> c.getEndpoints(endpointUrl, new String[0], new String[]{profileUri}))
                  .whenComplete((e, ex) -> discoveryClient.disconnect())
                  .thenApply(response -> l(response.getEndpoints()));
          } catch (UaException e) {
              return failedFuture(e);
          }
      }
  
  }
  