Add code that lets you use the GoER REST API

This is done because you are not able to get UUID:s for missing events
when you are using the GraphQl API.

This change consist of an abstration of EiffelGraphQlClient to
EiffelClient. This class includes an instance of EiffelGraphQlClient and
the new EiffelGoRestClient. GraphQlApiConfig is also renamed to
EiffelRepoApiConfig and now includes urls to both the GraphQL API and
the GoER REST API.

Solves: Jira GER-1545
Change-Id: I15467cf21180094c31c147ebcbc9c8f2be0b75c3
diff --git a/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/GraphQlEventStorageProvider.java b/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/EventStorageProvider.java
similarity index 68%
rename from src/main/java/com/googlesource/gerrit/plugins/eventseiffel/GraphQlEventStorageProvider.java
rename to src/main/java/com/googlesource/gerrit/plugins/eventseiffel/EventStorageProvider.java
index 78c5fca..df236b0 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/GraphQlEventStorageProvider.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/EventStorageProvider.java
@@ -16,23 +16,21 @@
 
 import com.google.inject.Inject;
 import com.google.inject.Provider;
-import com.googlesource.gerrit.plugins.eventseiffel.config.GraphQlApiConfig;
-import com.googlesource.gerrit.plugins.eventseiffel.eiffel.api.EiffelGraphQlClient;
+import com.googlesource.gerrit.plugins.eventseiffel.config.EiffelRepoApiConfig;
+import com.googlesource.gerrit.plugins.eventseiffel.eiffel.api.EiffelClient;
 import com.googlesource.gerrit.plugins.eventseiffel.eiffel.api.EventStorage;
 
-public class GraphQlEventStorageProvider implements Provider<EventStorage> {
+public class EventStorageProvider implements Provider<EventStorage> {
 
-  private EiffelGraphQlClient graphQl;
+  private EiffelClient eiffelClient;
 
   @Inject
-  public GraphQlEventStorageProvider(GraphQlApiConfig config) {
-    this.graphQl =
-        new EiffelGraphQlClient(
-            config.url(), config.userName(), config.password(), config.connectTimeout());
+  public EventStorageProvider(EiffelRepoApiConfig config) {
+    this.eiffelClient = new EiffelClient(config);
   }
 
   @Override
   public EventStorage get() {
-    return graphQl;
+    return eiffelClient;
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/Module.java b/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/Module.java
index c5e4c20..469b3c1 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/Module.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/Module.java
@@ -26,12 +26,12 @@
 import com.google.inject.internal.UniqueAnnotations;
 import com.googlesource.gerrit.plugins.eventseiffel.cache.EiffelEventIdCacheImpl;
 import com.googlesource.gerrit.plugins.eventseiffel.config.EiffelConfig;
+import com.googlesource.gerrit.plugins.eventseiffel.config.EiffelRepoApiConfig;
 import com.googlesource.gerrit.plugins.eventseiffel.config.EventIdCacheConfig;
 import com.googlesource.gerrit.plugins.eventseiffel.config.EventListenersConfig;
 import com.googlesource.gerrit.plugins.eventseiffel.config.EventMappingConfig;
 import com.googlesource.gerrit.plugins.eventseiffel.config.EventParsingConfig;
 import com.googlesource.gerrit.plugins.eventseiffel.config.EventsFilter;
-import com.googlesource.gerrit.plugins.eventseiffel.config.GraphQlApiConfig;
 import com.googlesource.gerrit.plugins.eventseiffel.config.RabbitMqConfig;
 import com.googlesource.gerrit.plugins.eventseiffel.eiffel.api.EiffelEventPublisher;
 import com.googlesource.gerrit.plugins.eventseiffel.eiffel.api.EventStorage;
@@ -49,15 +49,15 @@
 class Module extends FactoryModule {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
   private final Provider<RabbitMqConfig> rabbitMq;
-  private final Provider<GraphQlApiConfig> graphQl;
+  private final Provider<EiffelRepoApiConfig> eiffelRepoConfig;
   private final Provider<EventIdCacheConfig> idCache;
 
   @Inject
   public Module(
-      GraphQlApiConfig.Provider graphQlConfig,
+      EiffelRepoApiConfig.Provider eiffelRepoConfig,
       RabbitMqConfig.Provider rabbitMqConfig,
       EventIdCacheConfig.Provider idCache) {
-    this.graphQl = graphQlConfig;
+    this.eiffelRepoConfig = eiffelRepoConfig;
     this.rabbitMq = rabbitMqConfig;
     this.idCache = idCache;
   }
@@ -67,9 +67,11 @@
     if (idCache.get().trustLocalCache()) {
       bind(EventStorage.class).to(EventStorage.NoOpEventStorage.class);
       logger.atWarning().log("GraphQlApi Event Storage is not used, local cache is trusted.");
-    } else if (graphQl.get().isConfigured()) {
-      bind(GraphQlApiConfig.class).toProvider(GraphQlApiConfig.Provider.class).in(Scopes.SINGLETON);
-      bind(EventStorage.class).toProvider(GraphQlEventStorageProvider.class).in(Scopes.SINGLETON);
+    } else if (eiffelRepoConfig.get().isConfigured()) {
+      bind(EiffelRepoApiConfig.class)
+          .toProvider(EiffelRepoApiConfig.Provider.class)
+          .in(Scopes.SINGLETON);
+      bind(EventStorage.class).toProvider(EventStorageProvider.class).in(Scopes.SINGLETON);
     } else {
       logger.atSevere().log("Found no configuration for EventStorage.");
     }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/config/GraphQlApiConfig.java b/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/config/EiffelRepoApiConfig.java
similarity index 65%
rename from src/main/java/com/googlesource/gerrit/plugins/eventseiffel/config/GraphQlApiConfig.java
rename to src/main/java/com/googlesource/gerrit/plugins/eventseiffel/config/EiffelRepoApiConfig.java
index 5803614..2078398 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/config/GraphQlApiConfig.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/config/EiffelRepoApiConfig.java
@@ -24,49 +24,51 @@
 import org.eclipse.jgit.lib.Config;
 
 @Singleton
-public class GraphQlApiConfig {
+public class EiffelRepoApiConfig {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
-  public static class Provider implements com.google.inject.Provider<GraphQlApiConfig> {
+  public static class Provider implements com.google.inject.Provider<EiffelRepoApiConfig> {
 
     private static final String CONNECT_TIMEOUT = "connectTimeout";
-    private static final String GRAPHQL_CLIENT = "GraphQlClient";
+    private static final String EIFFELREPO_CLIENT = "EiffelRepoClient";
     private static final String PASSWORD = "password";
     private static final String USER_NAME = "userName";
-    private static final String URL = "url";
+    private static final String GRAPHQL_URL = "graphQlUrl";
+    private static final String GOREST_URL = "goRestUrl";
     private static final int DEFAULT_CONNECT_TIMEOUT = 20;
 
-    private final GraphQlApiConfig config;
+    private final EiffelRepoApiConfig config;
 
     @Inject
     public Provider(PluginConfigFactory cfgFactory, @PluginName String pluginName) {
       Config cfg = cfgFactory.getGlobalPluginConfig(pluginName);
       config =
-          new GraphQlApiConfig(
-              getHttpUrl(cfg, GRAPHQL_CLIENT, URL),
-              cfg.getString(GRAPHQL_CLIENT, null, USER_NAME),
-              cfg.getString(GRAPHQL_CLIENT, null, PASSWORD),
-              cfg.getInt(GRAPHQL_CLIENT, CONNECT_TIMEOUT, DEFAULT_CONNECT_TIMEOUT));
+          new EiffelRepoApiConfig(
+              getHttpUri(cfg, EIFFELREPO_CLIENT, GRAPHQL_URL),
+              getHttpUri(cfg, EIFFELREPO_CLIENT, GOREST_URL),
+              cfg.getString(EIFFELREPO_CLIENT, null, USER_NAME),
+              cfg.getString(EIFFELREPO_CLIENT, null, PASSWORD),
+              cfg.getInt(EIFFELREPO_CLIENT, CONNECT_TIMEOUT, DEFAULT_CONNECT_TIMEOUT));
     }
 
     @Override
-    public GraphQlApiConfig get() {
+    public EiffelRepoApiConfig get() {
       return config;
     }
   }
 
   /* Check that the url is a valid http url. */
-  private static URI getHttpUrl(Config cfg, String section, String name) {
+  private static URI getHttpUri(Config cfg, String section, String name) {
     String url = cfg.getString(section, null, name);
     String configKey = section + "." + name;
-    URI graphQlUrl = null;
+    URI uri = null;
     try {
-      graphQlUrl = url == null ? null : new URI(url);
+      uri = url == null ? null : new URI(url);
     } catch (URISyntaxException e) {
       logger.atSevere().withCause(e).log("%s \"%s\" is malformed.", configKey, url);
     }
-    if (graphQlUrl != null) {
-      String scheme = graphQlUrl.getScheme();
+    if (uri != null) {
+      String scheme = uri.getScheme();
       if (scheme == null) {
         logger.atSevere().log("%s \"%s\" is missing schema.", configKey, url);
         return null;
@@ -76,36 +78,43 @@
         logger.atSevere().log("%s  \"%s\" scheme is not valid [http|https].", configKey, url);
         return null;
       }
-      if (graphQlUrl.getHost() == null) {
+      if (uri.getHost() == null) {
         logger.atSevere().log("%s \"%s\" is missing host.", configKey, url);
         return null;
       }
     }
-    return graphQlUrl;
+    return uri;
   }
 
-  private final URI url;
-  private final String userName;
+  private final URI graphQlUrl;
+  private final URI goRestUrl;
+  private final String username;
   private final char[] password;
   private final int connectTimeOut;
 
-  private GraphQlApiConfig(URI url, String userName, String password, int connectTimeout) {
-    this.url = url;
-    this.userName = userName;
+  private EiffelRepoApiConfig(
+      URI graphQlUrl, URI goRestUrl, String username, String password, int connectTimeout) {
+    this.graphQlUrl = graphQlUrl;
+    this.goRestUrl = goRestUrl;
+    this.username = username;
     this.password = password == null ? null : password.toCharArray();
     this.connectTimeOut = connectTimeout;
   }
 
   public boolean isConfigured() {
-    return url != null;
+    return graphQlUrl != null && goRestUrl != null;
   }
 
-  public URI url() {
-    return this.url;
+  public URI graphQlUrl() {
+    return this.graphQlUrl;
   }
 
-  public String userName() {
-    return userName;
+  public URI goRestUrl() {
+    return this.goRestUrl;
+  }
+
+  public String username() {
+    return username;
   }
 
   public char[] password() {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/eiffel/api/EiffelClient.java b/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/eiffel/api/EiffelClient.java
new file mode 100644
index 0000000..5bf5c75
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/eiffel/api/EiffelClient.java
@@ -0,0 +1,66 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// 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 com.googlesource.gerrit.plugins.eventseiffel.eiffel.api;
+
+import com.google.common.flogger.FluentLogger;
+import com.googlesource.gerrit.plugins.eventseiffel.config.EiffelRepoApiConfig;
+import com.googlesource.gerrit.plugins.eventseiffel.eiffel.EventKey;
+import java.net.Authenticator;
+import java.net.PasswordAuthentication;
+import java.net.http.HttpClient;
+import java.net.http.HttpClient.Redirect;
+import java.time.Duration;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+public class EiffelClient implements EventStorage {
+  public static FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  private EiffelGraphQlClient graphQlClient;
+  private EiffelGoRestClient restClient;
+
+  public EiffelClient(EiffelRepoApiConfig config) {
+    HttpClient.Builder clientBuilder =
+        HttpClient.newBuilder()
+            .followRedirects(Redirect.NORMAL)
+            .connectTimeout(Duration.ofSeconds(config.connectTimeout()));
+    String username = config.username();
+    char[] password = config.password();
+    if (username != null && password != null) {
+      clientBuilder.authenticator(
+          new Authenticator() {
+
+            @Override
+            public PasswordAuthentication getPasswordAuthentication() {
+              return new PasswordAuthentication(username, password);
+            }
+          });
+    }
+    HttpClient client = clientBuilder.build();
+    this.graphQlClient = new EiffelGraphQlClient(client, config.graphQlUrl());
+    this.restClient = new EiffelGoRestClient(client, config.goRestUrl());
+  }
+
+  @Override
+  public Optional<UUID> getEventId(EventKey key) throws EventStorageException {
+    return graphQlClient.getEventId(key);
+  }
+
+  @Override
+  public List<UUID> getScsIds(String repo, String commit) throws EventStorageException {
+    return graphQlClient.getScsIds(repo, commit);
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/eiffel/api/EiffelGoRestClient.java b/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/eiffel/api/EiffelGoRestClient.java
new file mode 100644
index 0000000..1cece64
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/eiffel/api/EiffelGoRestClient.java
@@ -0,0 +1,99 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// 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 com.googlesource.gerrit.plugins.eventseiffel.eiffel.api;
+
+import com.github.rholder.retry.RetryException;
+import com.github.rholder.retry.Retryer;
+import com.github.rholder.retry.RetryerBuilder;
+import com.github.rholder.retry.StopStrategies;
+import com.github.rholder.retry.WaitStrategies;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.flogger.FluentLogger;
+import com.google.gson.Gson;
+import com.google.gson.JsonSyntaxException;
+import com.googlesource.gerrit.plugins.eventseiffel.eiffel.dto.EiffelLinkInfo;
+import java.io.IOException;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandlers;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+
+public class EiffelGoRestClient {
+  public static FluentLogger logger = FluentLogger.forEnclosingClass();
+  public static final Gson GSON = new Gson();
+
+  private static final String LINKS_ID_URL =
+      "events?meta.type=%s&data.gitIdentifier.repoName=%s&data.gitIdentifier.branch=%s&data.gitIdentifier.commitId=%s";
+
+  private HttpClient client;
+  private URI goRestUrl;
+
+  public EiffelGoRestClient(HttpClient client, URI goRestUrl) {
+    this.client = client;
+    this.goRestUrl = goRestUrl;
+  }
+
+  private QueryResult restQuery(String query) throws EventStorageException {
+    HttpResponse<String> response;
+    try {
+      Retryer<HttpResponse<String>> retryer =
+          RetryerBuilder.<HttpResponse<String>>newBuilder()
+              .retryIfException()
+              .withWaitStrategy(WaitStrategies.fixedWait(10, TimeUnit.SECONDS))
+              .withStopStrategy(StopStrategies.stopAfterAttempt(2))
+              .build();
+      response = retryer.call(() -> get(query));
+    } catch (RetryException | ExecutionException e) {
+      throw new EventStorageException(e, "Query \"%s\" failed.", query);
+    }
+
+    if (response.statusCode() != 200) {
+      throw new EventStorageException(
+          "Query \"%s\" failed: [%d] %s", query, response.statusCode(), response.body());
+    }
+
+    QueryResult result;
+    try {
+      result = GSON.fromJson(response.body(), QueryResult.class);
+    } catch (JsonSyntaxException e) {
+      throw new EventStorageException(
+          e, "Query \"%s\" failed, invalid reply: %s", query, response.body());
+    }
+
+    return result;
+  }
+
+  private HttpResponse<String> get(String query) throws IOException, InterruptedException {
+    return client.send(
+        HttpRequest.newBuilder()
+            .uri(goRestUrl.resolve(query))
+            .header("Content-Type", "application/json")
+            .build(),
+        BodyHandlers.ofString());
+  }
+
+  @VisibleForTesting
+  static class QueryResult {
+    List<Data> items;
+
+    class Data {
+      EiffelLinkInfo[] links;
+    }
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/eiffel/api/EiffelGraphQlClient.java b/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/eiffel/api/EiffelGraphQlClient.java
index ff8bf2b..468d030 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/eiffel/api/EiffelGraphQlClient.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/eiffel/api/EiffelGraphQlClient.java
@@ -29,16 +29,12 @@
 import com.googlesource.gerrit.plugins.eventseiffel.eiffel.EventKey;
 import com.googlesource.gerrit.plugins.eventseiffel.eiffel.SourceChangeEventKey;
 import java.io.IOException;
-import java.net.Authenticator;
-import java.net.PasswordAuthentication;
 import java.net.URI;
 import java.net.http.HttpClient;
-import java.net.http.HttpClient.Redirect;
 import java.net.http.HttpRequest;
 import java.net.http.HttpRequest.BodyPublishers;
 import java.net.http.HttpResponse;
 import java.net.http.HttpResponse.BodyHandlers;
-import java.time.Duration;
 import java.util.List;
 import java.util.Optional;
 import java.util.UUID;
@@ -46,7 +42,7 @@
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 
-public class EiffelGraphQlClient implements EventStorage {
+public class EiffelGraphQlClient {
   public static FluentLogger logger = FluentLogger.forEnclosingClass();
   public static final Gson GSON = new Gson();
   /* The mongodb query is the same for both events. */
@@ -89,30 +85,11 @@
   private HttpClient client;
   private URI graphQlUrl;
 
-  public EiffelGraphQlClient(URI graphQlUrl, int connectionTimeout) {
-    this(graphQlUrl, null, null, connectionTimeout);
-  }
-
-  public EiffelGraphQlClient(URI graphQlUrl, String userName, char[] password, int connectTimeout) {
-    HttpClient.Builder clientBuilder =
-        HttpClient.newBuilder()
-            .followRedirects(Redirect.NORMAL)
-            .connectTimeout(Duration.ofSeconds(connectTimeout));
-    if (userName != null && password != null) {
-      clientBuilder.authenticator(
-          new Authenticator() {
-
-            @Override
-            public PasswordAuthentication getPasswordAuthentication() {
-              return new PasswordAuthentication(userName, password);
-            }
-          });
-    }
-    this.client = clientBuilder.build();
+  public EiffelGraphQlClient(HttpClient client, URI graphQlUrl) {
+    this.client = client;
     this.graphQlUrl = graphQlUrl;
   }
 
-  @Override
   public Optional<UUID> getEventId(EventKey key) throws EventStorageException {
     String query = getQueryFor(key);
     List<UUID> ids = query(query).getIds();
@@ -130,7 +107,6 @@
     }
   }
 
-  @Override
   public List<UUID> getScsIds(String repo, String commit) throws EventStorageException {
     return query(String.format(SCS_IDS_QUERY, repo, commit)).getIds();
   }
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index a241bf9..0bf1344 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -12,8 +12,9 @@
       password = secret
       waitForConfirms = 7 seconds
       maxBatchSize = 100
-    [GraphQlClient]
-      url = https://eiffel.company.com/graphql
+    [EiffelRepoClient]
+      graphQlUrl = https://eiffel.company.com/graphql
+      goRestUrl = https://eiffel.company.com/rest/
       userName = user
       password = secret
     [EventParsing]
@@ -75,17 +76,21 @@
   If confirms are disabled this option has no real effect.
   (Default: _1_.)
 
-## Section "GraphQlClient" ##
+## Section "EiffelRepoClient" ##
 
 Configuration for connecting to the
 [Eiffel GraphQL API](https://github.com/eiffel-community/eiffel-graphql-api).
 
 ### Settings ###
 
-url
+graphQlUrl
 : The url to the GraphQL endpoint.
   (_Required_)
 
+goRestUrl
+: The url to the GoER REST endpoint.
+  (_Required_)
+
 userName
 : The user name for the GraphQL service.