blob: 8265b4acc8c01d274b49a8e0fad0a025137272cc [file] [log] [blame]
// 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;
}
}