| // Copyright (C) 2021 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.collect.Lists; |
| import com.google.common.flogger.FluentLogger; |
| import com.google.gson.Gson; |
| import com.google.gson.JsonSyntaxException; |
| import com.googlesource.gerrit.plugins.eventseiffel.eiffel.ArtifactEventKey; |
| import com.googlesource.gerrit.plugins.eventseiffel.eiffel.CompositionDefinedEventKey; |
| import com.googlesource.gerrit.plugins.eventseiffel.eiffel.EventKey; |
| import com.googlesource.gerrit.plugins.eventseiffel.eiffel.SourceChangeEventKey; |
| import java.io.IOException; |
| import java.net.URI; |
| import java.net.http.HttpClient; |
| import java.net.http.HttpRequest; |
| import java.net.http.HttpRequest.BodyPublishers; |
| import java.net.http.HttpResponse; |
| import java.net.http.HttpResponse.BodyHandlers; |
| import java.util.List; |
| import java.util.Optional; |
| import java.util.UUID; |
| import java.util.concurrent.ExecutionException; |
| import java.util.concurrent.TimeUnit; |
| import java.util.stream.Collectors; |
| |
| 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. */ |
| private static String MDB_QUERY = |
| "{ 'data.gitIdentifier.repoName':'%s'," |
| + "'data.gitIdentifier.branch':'%s'," |
| + "'data.gitIdentifier.commitId':'%s' }"; |
| private static final String SCC_ID_QUERY = |
| "{\"query\": \"{sourceChangeCreated(search: \\\" " |
| + MDB_QUERY |
| + "\\\")" |
| + "{edges{node{meta{id}}}}}\"}"; |
| |
| private static final String SCS_ID_QUERY = |
| "{\"query\": \"{sourceChangeSubmitted(search: \\\" " |
| + MDB_QUERY |
| + "\\\")" |
| + "{edges{node{meta{id}}}}}\"}"; |
| |
| private static final String SCS_IDS_QUERY = |
| "{\"query\": \"{sourceChangeSubmitted(search: \\\" " |
| + "{ 'data.gitIdentifier.repoName':'%s'," |
| + "'data.gitIdentifier.commitId':'%s' }" |
| + "\\\")" |
| + "{edges{node{meta{id}}}}}\"}"; |
| |
| private static final String ARTC_ID_QUERY = |
| "{\"query\": \"{artifactCreated(search: \\\" " |
| + "{ 'data.identity':'%s' }" |
| + "\\\")" |
| + "{edges{node{meta{id}}}}}\"}"; |
| |
| private static final String CD_ID_QUERY = |
| "{\"query\": \"{compositionDefined(search: \\\" " |
| + "{ 'data.name':'%s'," |
| + "'data.version':'%s' }" |
| + "\\\")" |
| + "{edges{node{meta{id}}}}}\"}"; |
| |
| private HttpClient client; |
| private URI graphQlUrl; |
| |
| public EiffelGraphQlClient(HttpClient client, URI graphQlUrl) { |
| this.client = client; |
| this.graphQlUrl = graphQlUrl; |
| } |
| |
| public Optional<UUID> getEventId(EventKey key) throws EventStorageException { |
| String query = getQueryFor(key); |
| List<UUID> ids = query(query).getIds(); |
| switch (ids.size()) { |
| case 0: |
| return Optional.empty(); |
| case 1: |
| return Optional.of(ids.get(0)); |
| default: |
| logger.atWarning().log( |
| "More than one id found [%s] for query:\"%s\"", |
| String.join(", ", ids.stream().map(UUID::toString).collect(Collectors.toList())), |
| query); |
| return Optional.of(ids.get(0)); |
| } |
| } |
| |
| public List<UUID> getScsIds(String repo, String commit) throws EventStorageException { |
| return query(String.format(SCS_IDS_QUERY, repo, commit)).getIds(); |
| } |
| |
| private QueryResult query(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(() -> post(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()); |
| } |
| |
| if (result.errors != null) { |
| throw new EventStorageException( |
| "Query \"%s\" failed: %s", query, String.join(" | ", result.getErrorMessages())); |
| } |
| |
| return result; |
| } |
| |
| private HttpResponse<String> post(String query) throws IOException, InterruptedException { |
| return client.send( |
| HttpRequest.newBuilder() |
| .uri(graphQlUrl) |
| .header("Content-Type", "application/json") |
| .POST(BodyPublishers.ofString(query)) |
| .build(), |
| BodyHandlers.ofString()); |
| } |
| |
| private static String getQueryFor(EventKey key) { |
| switch (key.type()) { |
| case SCC: |
| SourceChangeEventKey sccKey = (SourceChangeEventKey) key; |
| return String.format(SCC_ID_QUERY, sccKey.repo(), sccKey.branch(), sccKey.commit()); |
| case SCS: |
| SourceChangeEventKey scsKey = (SourceChangeEventKey) key; |
| return String.format(SCS_ID_QUERY, scsKey.repo(), scsKey.branch(), scsKey.commit()); |
| case ARTC: |
| ArtifactEventKey artcKey = (ArtifactEventKey) key; |
| return String.format(ARTC_ID_QUERY, artcKey.identity()); |
| case CD: |
| CompositionDefinedEventKey cdKey = (CompositionDefinedEventKey) key; |
| return String.format(CD_ID_QUERY, cdKey.name(), cdKey.version()); |
| default: |
| return "No such event type"; |
| } |
| } |
| |
| @VisibleForTesting |
| static class QueryResult { |
| Data data; |
| List<Error> errors; |
| |
| List<Data.Field.Edge> getEdges() { |
| if (data != null) { |
| if (data.sourceChangeCreated != null) { |
| return data.sourceChangeCreated.edges; |
| } |
| if (data.sourceChangeSubmitted != null) { |
| return data.sourceChangeSubmitted.edges; |
| } |
| } |
| return Lists.newArrayList(); |
| } |
| |
| List<UUID> getIds() { |
| return toIds(getEdges()); |
| } |
| |
| List<String> getErrorMessages() { |
| return errors.stream().map(e -> e.message).collect(Collectors.toList()); |
| } |
| |
| private List<UUID> toIds(List<Data.Field.Edge> edges) { |
| return edges.stream().map(e -> e.node.meta.id).collect(Collectors.toList()); |
| } |
| } |
| |
| @VisibleForTesting |
| static class Data { |
| Field sourceChangeCreated; |
| Field sourceChangeSubmitted; |
| |
| class Field { |
| List<Edge> edges; |
| Meta meta; |
| |
| class Edge { |
| Node node; |
| |
| class Node { |
| List<Data> links; |
| |
| @SuppressWarnings("hiding") |
| Meta meta; |
| } |
| } |
| |
| class Meta { |
| UUID id; |
| } |
| } |
| } |
| |
| @VisibleForTesting |
| static class Error { |
| String message; |
| } |
| } |