blob: ae4414699e3d413360cbc58ca836352b3db12af6 [file] [log] [blame]
// 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 static com.googlesource.gerrit.plugins.eventseiffel.eiffel.dto.EiffelLinkType.CHANGE;
import static com.googlesource.gerrit.plugins.eventseiffel.eiffel.dto.EiffelLinkType.PREVIOUS_VERSION;
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.EventKey;
import com.googlesource.gerrit.plugins.eventseiffel.eiffel.SourceChangeEventKey;
import com.googlesource.gerrit.plugins.eventseiffel.eiffel.dto.EiffelLinkInfo;
import com.googlesource.gerrit.plugins.eventseiffel.eiffel.dto.EiffelLinkType;
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.Arrays;
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;
import java.util.stream.Stream;
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;
}
public Optional<List<UUID>> getParentLinks(EventKey key) throws EventStorageException {
String query;
switch (key.type()) {
case SCC:
SourceChangeEventKey sccKey = (SourceChangeEventKey) key;
query =
String.format(
LINKS_ID_URL,
sccKey.type().getType(),
sccKey.repo(),
sccKey.branch(),
sccKey.commit());
break;
case SCS:
SourceChangeEventKey scsKey = (SourceChangeEventKey) key;
query =
String.format(
LINKS_ID_URL,
scsKey.type().getType(),
scsKey.repo(),
scsKey.branch(),
scsKey.commit());
break;
case ARTC:
case CD:
default:
return Optional.empty();
}
QueryResult qr = restQuery(query);
if (qr == null || qr.isEmpty()) {
return Optional.empty();
}
if (qr.nbrOfFoundEvents() > 1) {
logger.atWarning().log("More than one event found (using first) for query:\"%s\"", query);
}
return Optional.of(qr.getParentLinks());
}
public Optional<UUID> getSccEventLink(SourceChangeEventKey key) throws EventStorageException {
String query;
switch (key.type()) {
case SCS:
query =
String.format(
LINKS_ID_URL, key.type().getType(), key.repo(), key.branch(), key.commit());
break;
case ARTC:
case CD:
case SCC:
default:
return Optional.empty();
}
QueryResult qr = restQuery(query);
if (qr == null || qr.isEmpty()) {
return Optional.empty();
}
if (qr.nbrOfFoundEvents() > 1) {
logger.atWarning().log("More than one event found (using first) for query:\"%s\"", query);
}
return qr.getSccLink();
}
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;
}
int nbrOfFoundEvents() {
return items != null ? items.size() : 0;
}
boolean isEmpty() {
return items == null || items.isEmpty();
}
List<UUID> getParentLinks() {
return getLinks(PREVIOUS_VERSION).collect(Collectors.toList());
}
Optional<UUID> getSccLink() {
return getLinks(CHANGE).findAny();
}
private Stream<UUID> getLinks(EiffelLinkType linkType) {
return Arrays.stream(items.get(0).links)
.filter(link -> link.type == linkType)
.map(link -> link.target);
}
}
}