Allow sharing the same gRPC channel between clients

This change introduces a refactoring of the gRPC protocol subsystem that
allows the creation of a gRPC chanel independently of the client, while
allowing multiple clients to share the same channel (e.g. as in Stratum
where we use 3 clients).

Moreover, we refactor the P4RuntimeClient API to support multiple
P4Runtime-internal device ID using the same client. While before the
client was associated to one of such ID.

Finally, we provide an abstract implementation for gRPC-based driver
behaviors, reducing code duplication in P4Runtime, gNMI and gNOI drivers.

Change-Id: I1a46352bbbef1e0d24042f169ae8ba580202944f
diff --git a/protocols/grpc/api/src/main/java/org/onosproject/grpc/api/GrpcChannelController.java b/protocols/grpc/api/src/main/java/org/onosproject/grpc/api/GrpcChannelController.java
index ad4324b..42a9248 100644
--- a/protocols/grpc/api/src/main/java/org/onosproject/grpc/api/GrpcChannelController.java
+++ b/protocols/grpc/api/src/main/java/org/onosproject/grpc/api/GrpcChannelController.java
@@ -19,75 +19,84 @@
 import com.google.common.annotations.Beta;
 import io.grpc.ManagedChannel;
 import io.grpc.ManagedChannelBuilder;
-import io.grpc.StatusRuntimeException;
 
-import java.util.Map;
+import java.net.URI;
 import java.util.Optional;
-import java.util.concurrent.CompletableFuture;
 
 /**
- * Abstraction of a gRPC controller that stores and manages gRPC channels.
+ * Abstraction of a gRPC controller that creates, stores, and manages gRPC
+ * channels.
  */
 @Beta
 public interface GrpcChannelController {
 
-    int CONNECTION_TIMEOUT_SECONDS = 20;
+    /**
+     * Creates a gRPC managed channel to the server identified by the given
+     * channel URI. The channel is created using the information contained in the
+     * URI, as such, the URI is expected to have absolute server-based form,
+     * where the scheme can be either {@code grpc:} or {@code grpcs:}, to
+     * indicated respectively a plaintext or secure channel.
+     * <p>
+     * Example of valid URIs are: <pre> {@code
+     * grpc://10.0.0.1:50001
+     * grpcs://10.0.0.1:50001
+     * grpcs://myserver.local:50001
+     * }</pre>
+     * <p>
+     * This method creates and stores the channel instance associating it to the
+     * passed URI, but it does not make any attempt to connect the channel or
+     * verify server reachability.
+     * <p>
+     * If another channel with the same  URI already exists, an {@link
+     * IllegalArgumentException} is thrown. To create multiple channels to the
+     * same server-port combination, URI file or query parameters can be used.
+     * For example: <pre> {@code
+     * grpc://10.0.0.1:50001/foo
+     * grpc://10.0.0.1:50001/bar
+     * grpc://10.0.0.1:50001/bar?param=1
+     * grpc://10.0.0.1:50001/bar?param=2
+     * }</pre>
+     * <p>
+     * When creating secure channels (i.e., {@code grpcs:)}, the current
+     * implementation provides encryption but not authentication, any server
+     * certificate, even if insecure, will be accepted.
+     *
+     * @param channelUri channel URI
+     * @return the managed channel created
+     * @throws IllegalArgumentException if a channel with the same channel URI
+     *                                  already exists
+     */
+    ManagedChannel create(URI channelUri);
 
     /**
-     * Creates a gRPC managed channel from the given builder and opens the
-     * connection. If the connection is successful, returns the managed channel
-     * object and stores the channel internally, associated with the given
-     * channel ID.
-     * <p>
-     * This method blocks until the channel is open or a timeout expires. By
-     * default the timeout is {@link #CONNECTION_TIMEOUT_SECONDS} seconds. If
-     * the timeout expires, a {@link StatusRuntimeException} is thrown. If
-     * another channel with the same ID already exists, an {@link
-     * IllegalArgumentException} is thrown.
+     * Similar to {@link #create(URI)} but does not create the chanel instance,
+     * instead, it uses the given channel builder to create it. As such, there
+     * is no requirement on the format of the URI, any URI can be used. The
+     * implementation might modify the passed builder for purposes specific to
+     * this controller, such as to enable gRPC message logging.
      *
-     * @param channelId      ID of the channel
+     * @param channelUri      URI identifying the channel
      * @param channelBuilder builder of the managed channel
      * @return the managed channel created
-     * @throws StatusRuntimeException   if the channel cannot be opened
      * @throws IllegalArgumentException if a channel with the same ID already
      *                                  exists
      */
-    ManagedChannel connectChannel(GrpcChannelId channelId,
-                                  ManagedChannelBuilder<?> channelBuilder);
+    ManagedChannel create(URI channelUri,
+                          ManagedChannelBuilder<?> channelBuilder);
 
     /**
-     * Closes the gRPC managed channel (i.e., disconnects from the gRPC server)
-     * and removes any internal state associated to it.
+     * Closes and destroys the gRPC channel associated to the given URI and
+     * removes any internal state associated to it.
      *
-     * @param channelId ID of the channel to remove
+     * @param channelUri URI of the channel to remove
      */
-    void disconnectChannel(GrpcChannelId channelId);
+    void destroy(URI channelUri);
 
     /**
-     * Returns all channels known by this controller, each one mapped to the ID
-     * passed at creation time.
+     * If present, returns the channel associated with the given URI.
      *
-     * @return map of all the channels with their ID as stored in this
-     * controller
-     */
-    Map<GrpcChannelId, ManagedChannel> getChannels();
-
-    /**
-     * If present, returns the channel associated with the given ID.
-     *
-     * @param channelId channel ID
+     * @param channelUri channel URI
      * @return optional channel
      */
-    Optional<ManagedChannel> getChannel(GrpcChannelId channelId);
-
-    /**
-     * Probes the server at the endpoint of the given channel. Returns true if
-     * the server responded to the probe, false otherwise or if the channel does
-     * not exist.
-     *
-     * @param channelId channel ID
-     * @return completable future eventually true if the gRPC server responded
-     * to the probe; false otherwise
-     */
-    CompletableFuture<Boolean> probeChannel(GrpcChannelId channelId);
+    Optional<ManagedChannel> get(URI channelUri);
 }
diff --git a/protocols/grpc/api/src/main/java/org/onosproject/grpc/api/GrpcChannelId.java b/protocols/grpc/api/src/main/java/org/onosproject/grpc/api/GrpcChannelId.java
deleted file mode 100644
index 6db331a..0000000
--- a/protocols/grpc/api/src/main/java/org/onosproject/grpc/api/GrpcChannelId.java
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Copyright 2017-present Open Networking Foundation
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.onosproject.grpc.api;
-
-import com.google.common.annotations.Beta;
-import org.onlab.util.Identifier;
-
-/**
- * gRPC channel identifier, unique in the scope of an ONOS node.
- */
-@Beta
-public final class GrpcChannelId extends Identifier<String> {
-
-    private GrpcChannelId(String channelName) {
-        super(channelName);
-    }
-
-    /**
-     * Instantiates a new channel ID.
-     *
-     * @param channelName name of the channel
-     * @return channel ID
-     */
-    public static GrpcChannelId of(String channelName) {
-        return new GrpcChannelId(channelName);
-    }
-}
diff --git a/protocols/grpc/api/src/main/java/org/onosproject/grpc/api/GrpcClientController.java b/protocols/grpc/api/src/main/java/org/onosproject/grpc/api/GrpcClientController.java
index b47d499..3edb36d 100644
--- a/protocols/grpc/api/src/main/java/org/onosproject/grpc/api/GrpcClientController.java
+++ b/protocols/grpc/api/src/main/java/org/onosproject/grpc/api/GrpcClientController.java
@@ -17,6 +17,7 @@
 package org.onosproject.grpc.api;
 
 import com.google.common.annotations.Beta;
+import io.grpc.ManagedChannel;
 import org.onosproject.net.DeviceId;
 import org.onosproject.net.device.DeviceAgentListener;
 import org.onosproject.net.provider.ProviderId;
@@ -24,76 +25,54 @@
 /**
  * Abstraction of controller that manages gRPC clients.
  *
- * @param <K> the gRPC client key
  * @param <C> the gRPC client type
  */
 @Beta
-public interface GrpcClientController<K extends GrpcClientKey, C extends GrpcClient> {
+public interface GrpcClientController<C extends GrpcClient> {
 
     /**
-     * Instantiates a new client to operate on a gRPC server identified by the
-     * given information. As a result of this method, a client can be later
-     * obtained by invoking {@link #getClient(DeviceId)}.
+     * Instantiates a new client to operate on the given gRPC channel. Returns
+     * true if  the client was created successfully, false otherwise. Clients
+     * are identified by device IDs and once created they can be obtained by
+     * invoking {@link #get(DeviceId)}.
      * <p>
-     * Upon creation, a connection to the server is automatically started, which
-     * blocks execution. If the connection is successful, the client is created
-     * and this method returns true, otherwise (e.g., socket error) any state
-     * associated with this client is destroyed and returns false.
-     * <p>
-     * Only one client can exist for the same device ID. Calls to this method
-     * are idempotent fot the same client key, i.e. returns true if such client
-     * already exists. Otherwise, if a client for the same device ID but
-     * different client key already exists, throws an exception.
+     * Only one client can exist for the same device ID. If a client for the
+     * given device ID already exists, throws an exception.
      *
-     * @param clientKey the client key
-     * @return true if the client was created and the channel to the server is
-     * open; false otherwise
-     * @throws IllegalArgumentException if a client for the same device ID but
-     *                                  different client key already exists.
+     * @param deviceId device ID
+     * @param channel  gRPC managed channel
+     * @return true if the client was created, false otherwise
+     * @throws IllegalArgumentException if a client for the same device ID
+     *                                  already exists.
      */
-    boolean createClient(K clientKey);
+    boolean create(DeviceId deviceId, ManagedChannel channel);
 
     /**
-     * Returns the gRPC client previously created for the given device, or null
-     * if such client does not exist.
-     *
-     * @param deviceId the device identifier
-     * @return the gRPC client of the device if exists; null otherwise
-     */
-    C getClient(DeviceId deviceId);
-
-    /**
-     * Returns the gRPC client previously created for the given client key, or
+     * Returns the gRPC client previously created for the given device ID, or
      * null if such client does not exist.
      *
-     * @param clientKey client key
+     * @param deviceId the device ID
      * @return the gRPC client of the device if exists; null otherwise
      */
-    C getClient(K clientKey);
+    C get(DeviceId deviceId);
 
     /**
      * Removes the gRPC client for the given device and any gRPC channel state
      * associated to it. If no client exists for the given device, the result is
      * a no-op.
      *
-     * @param deviceId the device identifier
+     * @param deviceId the device ID
      */
-    void removeClient(DeviceId deviceId);
+    void remove(DeviceId deviceId);
 
     /**
-     * Similar to {@link #removeClient(DeviceId)} but uses the client key to
-     * identify the client to remove.
+     * Adds a listener for device agent events for the given provider. If a
+     * listener already exists for the given device ID and provider ID, then it
+     * will be replaced by the new one.
      *
-     * @param clientKey the client key
-     */
-    void removeClient(K clientKey);
-
-    /**
-     * Adds a listener for device agent events for the given provider.
-     *
-     * @param deviceId device identifier
+     * @param deviceId   device ID
      * @param providerId provider ID
-     * @param listener the device agent listener
+     * @param listener   the device agent listener
      */
     void addDeviceAgentListener(DeviceId deviceId, ProviderId providerId,
                                 DeviceAgentListener listener);
@@ -102,7 +81,7 @@
      * Removes the listener for device agent events that was previously
      * registered for the given provider.
      *
-     * @param deviceId   device identifier
+     * @param deviceId   device ID
      * @param providerId the provider ID
      */
     void removeDeviceAgentListener(DeviceId deviceId, ProviderId providerId);
diff --git a/protocols/grpc/api/src/main/java/org/onosproject/grpc/api/GrpcClientKey.java b/protocols/grpc/api/src/main/java/org/onosproject/grpc/api/GrpcClientKey.java
deleted file mode 100644
index d1d0b0f..0000000
--- a/protocols/grpc/api/src/main/java/org/onosproject/grpc/api/GrpcClientKey.java
+++ /dev/null
@@ -1,126 +0,0 @@
-/*
- * Copyright 2018-present Open Networking Foundation
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.onosproject.grpc.api;
-
-import com.google.common.annotations.Beta;
-import com.google.common.base.Objects;
-import org.onosproject.net.DeviceId;
-
-import java.net.URI;
-
-import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.common.base.Preconditions.checkNotNull;
-import static com.google.common.base.Strings.isNullOrEmpty;
-import static java.lang.String.format;
-
-/**
- * Key that uniquely identifies a gRPC client.
- */
-@Beta
-public class GrpcClientKey {
-
-    private static final String GRPC = "grpc";
-    private static final String GRPCS = "grpcs";
-
-    private final String serviceName;
-    private final DeviceId deviceId;
-    private final URI serverUri;
-
-    /**
-     * Creates a new client key.
-     *
-     * @param serviceName gRPC service name of the client
-     * @param deviceId    ONOS device ID
-     * @param serverUri   gRPC server URI
-     */
-    public GrpcClientKey(String serviceName, DeviceId deviceId, URI serverUri) {
-        checkNotNull(serviceName);
-        checkNotNull(deviceId);
-        checkNotNull(serverUri);
-        checkArgument(!serviceName.isEmpty(),
-                      "Service name can not be null");
-        checkArgument(serverUri.getScheme().equals(GRPC)
-                              || serverUri.getScheme().equals(GRPCS),
-                      format("Server URI scheme must be %s or %s", GRPC, GRPCS));
-        checkArgument(!isNullOrEmpty(serverUri.getHost()),
-                      "Server host address should not be empty");
-        checkArgument(serverUri.getPort() > 0 && serverUri.getPort() <= 65535, "Invalid server port");
-        this.serviceName = serviceName;
-        this.deviceId = deviceId;
-        this.serverUri = serverUri;
-    }
-
-    /**
-     * Gets the gRPC service name of the client.
-     *
-     * @return the service name
-     */
-    public String serviceName() {
-        return serviceName;
-    }
-
-    /**
-     * Gets the device ID.
-     *
-     * @return the device ID
-     */
-    public DeviceId deviceId() {
-        return deviceId;
-    }
-
-    /**
-     * Returns the gRPC server URI.
-     *
-     * @return the gRPC server URI.
-     */
-    public URI serveUri() {
-        return serverUri;
-    }
-
-    /**
-     * Returns true if the client requires TLS/SSL, false otherwise.
-     *
-     * @return boolean
-     */
-    public boolean requiresSecureChannel() {
-        return serverUri.getScheme().equals(GRPCS);
-    }
-
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) {
-            return true;
-        }
-        if (o == null || getClass() != o.getClass()) {
-            return false;
-        }
-        GrpcClientKey that = (GrpcClientKey) o;
-        return Objects.equal(serviceName, that.serviceName) &&
-                Objects.equal(deviceId, that.deviceId) &&
-                Objects.equal(serverUri, that.serverUri);
-    }
-
-    @Override
-    public int hashCode() {
-        return Objects.hashCode(serviceName, deviceId, serverUri);
-    }
-
-    @Override
-    public String toString() {
-        return format("%s/%s@%s", deviceId, serviceName, serverUri);
-    }
-}