| // 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); |
| } |
| } |
| } |