All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.sourcelab.buildkite.api.client.BuildkiteClient Maven / Gradle / Ivy

The newest version!
/**
 * Copyright 2023 SourceLab.org https://github.com/SourceLabOrg/Buildkite-Api-Client
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
 * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
 * persons to whom the Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
 * Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
 * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
 * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

package org.sourcelab.buildkite.api.client;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sourcelab.buildkite.api.client.exception.BuildkiteException;
import org.sourcelab.buildkite.api.client.exception.InvalidAccessTokenException;
import org.sourcelab.buildkite.api.client.exception.InvalidAllowedIpAddressException;
import org.sourcelab.buildkite.api.client.exception.InvalidPagingRequestException;
import org.sourcelab.buildkite.api.client.exception.InvalidRequestException;
import org.sourcelab.buildkite.api.client.exception.NotFoundException;
import org.sourcelab.buildkite.api.client.http.Client;
import org.sourcelab.buildkite.api.client.http.HttpResult;
import org.sourcelab.buildkite.api.client.request.AnnotationFilters;
import org.sourcelab.buildkite.api.client.request.BuildFilters;
import org.sourcelab.buildkite.api.client.request.BuildFiltersBuilder;
import org.sourcelab.buildkite.api.client.request.CancelBuildRequest;
import org.sourcelab.buildkite.api.client.request.CreateBuildOptions;
import org.sourcelab.buildkite.api.client.request.CreateBuildOptionsBuilder;
import org.sourcelab.buildkite.api.client.request.CreateBuildRequest;
import org.sourcelab.buildkite.api.client.request.DeleteAccessTokenRequest;
import org.sourcelab.buildkite.api.client.request.GetAccessTokenRequest;
import org.sourcelab.buildkite.api.client.request.GetAnnotationsForBuildRequest;
import org.sourcelab.buildkite.api.client.request.GetBuildFilters;
import org.sourcelab.buildkite.api.client.request.GetBuildFiltersBuilder;
import org.sourcelab.buildkite.api.client.request.GetBuildRequest;
import org.sourcelab.buildkite.api.client.request.GetMetaRequest;
import org.sourcelab.buildkite.api.client.request.GetOrganizationRequest;
import org.sourcelab.buildkite.api.client.request.GetPipelineRequest;
import org.sourcelab.buildkite.api.client.request.GetUserRequest;
import org.sourcelab.buildkite.api.client.request.ListBuildsRequest;
import org.sourcelab.buildkite.api.client.request.ListEmojisRequest;
import org.sourcelab.buildkite.api.client.request.ListOrganizationsRequest;
import org.sourcelab.buildkite.api.client.request.ListPipelinesRequest;
import org.sourcelab.buildkite.api.client.request.OrganizationFilters;
import org.sourcelab.buildkite.api.client.request.OrganizationFiltersBuilder;
import org.sourcelab.buildkite.api.client.request.PageOptions;
import org.sourcelab.buildkite.api.client.request.PageableRequest;
import org.sourcelab.buildkite.api.client.request.PingRequest;
import org.sourcelab.buildkite.api.client.request.PipelineFilters;
import org.sourcelab.buildkite.api.client.request.PipelineFiltersBuilder;
import org.sourcelab.buildkite.api.client.request.RebuildBuildRequest;
import org.sourcelab.buildkite.api.client.request.Request;
import org.sourcelab.buildkite.api.client.request.RetryJobOptions;
import org.sourcelab.buildkite.api.client.request.RetryJobOptionsBuilder;
import org.sourcelab.buildkite.api.client.request.RetryJobRequest;
import org.sourcelab.buildkite.api.client.request.UnblockJobOptions;
import org.sourcelab.buildkite.api.client.request.UnblockJobOptionsBuilder;
import org.sourcelab.buildkite.api.client.request.UnblockJobRequest;
import org.sourcelab.buildkite.api.client.response.AccessTokenResponse;
import org.sourcelab.buildkite.api.client.response.AnnotationsForBuildResponse;
import org.sourcelab.buildkite.api.client.response.Build;
import org.sourcelab.buildkite.api.client.response.CurrentUserResponse;
import org.sourcelab.buildkite.api.client.response.Emoji;
import org.sourcelab.buildkite.api.client.response.Error;
import org.sourcelab.buildkite.api.client.response.ErrorResponse;
import org.sourcelab.buildkite.api.client.response.Job;
import org.sourcelab.buildkite.api.client.response.ListBuildsResponse;
import org.sourcelab.buildkite.api.client.response.ListOrganizationsResponse;
import org.sourcelab.buildkite.api.client.response.ListPipelinesResponse;
import org.sourcelab.buildkite.api.client.response.MetaResponse;
import org.sourcelab.buildkite.api.client.response.Organization;
import org.sourcelab.buildkite.api.client.response.PageableResponse;
import org.sourcelab.buildkite.api.client.response.PagingLinks;
import org.sourcelab.buildkite.api.client.response.PingResponse;
import org.sourcelab.buildkite.api.client.response.Pipeline;
import org.sourcelab.buildkite.api.client.response.parser.ErrorResponseParser;

import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;

/**
 * API Client for Buildkite's REST Api.
 *
 * See API Documentation: {@see https://buildkite.com/docs/apis/rest-api}
 */
public class BuildkiteClient {
    private static final Logger logger = LoggerFactory.getLogger(BuildkiteClient.class);

    /**
     * User provided configuration.
     */
    private final Configuration configuration;

    /**
     * Underlying HTTP client.
     */
    private final Client httpClient;

    /**
     * Constructor.
     * @param configuration The configuration for the client.
     */
    public BuildkiteClient(final Configuration configuration) {
        this.configuration = configuration;
        this.httpClient = configuration.getClientFactory().createClient(configuration);
    }

    /**
     * Make a 'test' or 'hello world' request to the Buildkite API.  Can be used to validate
     * connectivity to the API.
     * @see https://buildkite.com/docs/apis/rest-api
     *
     * @return Response details from the ping request.
     * @throws BuildkiteException if API returns an error response.
     */
    public PingResponse ping() throws BuildkiteException {
        return executeRequest(new PingRequest());
    }

    /**
     * Retrieve details about the current AccessToken.
     * @see https://buildkite.com/docs/apis/rest-api/access-token
     *
     * @return Details about the current AccessToken.
     * @throws BuildkiteException if API returns an error response.
     */
    public AccessTokenResponse getAccessToken() throws BuildkiteException {
        return executeRequest(new GetAccessTokenRequest());
    }

    /**
     * Deletes/Revokes the current AccessToken.
     * @see https://buildkite.com/docs/apis/rest-api/access-token#revoke-the-current-token
     *
     * @return Details about the current AccessToken.
     * @throws BuildkiteException if API returns an error response.
     */
    public boolean deleteAccessToken() throws BuildkiteException {
        return executeRequest(new DeleteAccessTokenRequest());
    }

    /**
     * Retrieves the current user associated with the current access token.
     * @see https://buildkite.com/docs/apis/rest-api/user#get-the-current-user
     *
     * @return Details about the current User.
     * @throws BuildkiteException if API returns an error response.
     */
    public CurrentUserResponse getUser() throws BuildkiteException {
        return executeRequest(new GetUserRequest());
    }

    /**
     * Retrieve all Organizations accessible to the current user/API access token.
     * Results will be paged.
     *
     * @see https://buildkite.com/docs/apis/rest-api/organizations#list-organizations
     *
     * @return All Organizations accessible to the current user/API access token. Results
     *         will be paged if the number of results exceeds 30.
     * @throws BuildkiteException if API returns an error response.
     */
    public ListOrganizationsResponse listOrganizations() throws BuildkiteException {
        return listOrganizations(OrganizationFilters.newBuilder());
    }

    /**
     * Retrieve all Organizations accessible to the current user/API access token.
     * Results will be paged.
     *
     * @see https://buildkite.com/docs/apis/rest-api/organizations#list-organizations
     *
     * @param filters Filter criteria.
     * @return All Organizations accessible to the current user/API access token. Results
     *         will be paged if the number of results exceeds 30.
     * @throws BuildkiteException if API returns an error response.
     */
    public ListOrganizationsResponse listOrganizations(final OrganizationFiltersBuilder filters) throws BuildkiteException {
        Objects.requireNonNull(filters);
        return listOrganizations(filters.build());
    }

    /**
     * Retrieve all Organizations accessible to the current user/API access token.
     * Results will be paged.
     *
     * @see https://buildkite.com/docs/apis/rest-api/organizations#list-organizations
     *
     * @param filters Filter criteria.
     * @return All Organizations accessible to the current user/API access token. Results
     *         will be paged if the number of results exceeds 30.
     * @throws BuildkiteException if API returns an error response.
     */
    public ListOrganizationsResponse listOrganizations(final OrganizationFilters filters) throws BuildkiteException {
        Objects.requireNonNull(filters);
        return executeRequest(new ListOrganizationsRequest(filters));
    }

    /**
     * Retrieve specific organization via its Organization slug id.
     *
     * @see https://buildkite.com/docs/apis/rest-api/organizations#get-an-organization
     *
     * @param organizationSlugId Slug of the organization to retrieve.
     * @return Organization matching the slug, if found.
     * @throws BuildkiteException if API returns an error response.
     */
    public Optional getOrganization(final String organizationSlugId) throws BuildkiteException {
        Objects.requireNonNull(organizationSlugId);
        final Organization response = executeRequest(new GetOrganizationRequest(organizationSlugId));
        return Optional.ofNullable(response);
    }

    /**
     * Retrieve all Pipeline accessible to the current user/API access token for the given Organization.
     * Results will be paged.
     *
     * @see https://buildkite.com/docs/apis/rest-api/pipelines#list-pipelines
     *
     * @param filters Filter criteria.
     * @return All Pipelines accessible to the current user/API access token. Results
     *         will be paged if the number of results exceeds 30.
     * @throws BuildkiteException if API returns an error response.
     */
    public ListPipelinesResponse listPipelines(final PipelineFiltersBuilder filters) throws BuildkiteException {
        Objects.requireNonNull(filters);
        return listPipelines(filters.build());
    }

    /**
     * Retrieve all Pipelines accessible to the current user/API access token for the given Organization.
     * Results will be paged.
     *
     * @see https://buildkite.com/docs/apis/rest-api/pipelines#list-pipelines
     *
     * @param filters Filter criteria.
     * @return All Pipelines accessible to the current user/API access token. Results
     *         will be paged if the number of results exceeds 30.
     * @throws BuildkiteException if API returns an error response.
     */
    public ListPipelinesResponse listPipelines(final PipelineFilters filters) throws BuildkiteException {
        Objects.requireNonNull(filters);
        return executeRequest(new ListPipelinesRequest(filters));
    }

    /**
     * Retrieve all Pipelines accessible to the current user/API access token for the given Organization.
     * Results will be paged.
     *
     * @see https://buildkite.com/docs/apis/rest-api/pipelines#list-pipelines
     *
     * @param organizationSlugId Organization Slug Id to retrieve pipelines for.
     * @return All Pipelines accessible to the current user/API access token for the given Organization. Results
     *         will be paged if the number of results exceeds 30.
     * @throws BuildkiteException if API returns an error response.
     */
    public ListPipelinesResponse listPipelines(final String organizationSlugId) throws BuildkiteException {
        Objects.requireNonNull(organizationSlugId);
        return listPipelines(PipelineFilters.newBuilder()
            .withOrganization(organizationSlugId)
        );
    }

    /**
     * Retrieve specific pipeline via its Organization and Pipeline Slug Ids.
     *
     * @see https://buildkite.com/docs/apis/rest-api/pipelines#get-a-pipeline
     *
     * @param organizationSlugId Slug of the organization to retrieve.
     * @param pipelineSlugId Slug of the pipeline to retrieve.
     * @return Pipeline matching the slug, if found.
     * @throws BuildkiteException if API returns an error response.
     */
    public Optional getPipeline(final String organizationSlugId, final String pipelineSlugId) throws BuildkiteException {
        Objects.requireNonNull(organizationSlugId);
        Objects.requireNonNull(pipelineSlugId);
        final Pipeline pipeline = executeRequest(new GetPipelineRequest(organizationSlugId, pipelineSlugId));
        return Optional.ofNullable(pipeline);
    }

    /**
     * Retrieve all Builds accessible to the current user/API access token, across all Organizations.
     * Results will be paged.
     *
     * @see https://buildkite.com/docs/apis/rest-api/builds#list-all-builds
     *
     * @return All Builds accessible to the current user/API access token, across all Organizations. Results
     *         will be paged if the number of results exceeds 30.
     * @throws BuildkiteException if API returns an error response.
     */
    public ListBuildsResponse listBuilds() {
        return listBuilds(BuildFilters.newBuilder().build());
    }

    /**
     * Retrieve all builds which match the supplied search criteria.
     *
     * @see https://buildkite.com/docs/apis/rest-api/builds#list-all-builds
     *
     * @param filtersBuilder Filter criteria.
     * @return All builds which match the supplied search criteria. Results will be paged if the number of results
     *         exceeds 30 (or the page limit specified in the search criteria).
     * @throws BuildkiteException if API returns an error response.
     */
    public ListBuildsResponse listBuilds(final BuildFiltersBuilder filtersBuilder) {
        return listBuilds(filtersBuilder.build());
    }

    /**
     * Retrieve all builds which match the supplied search criteria.
     *
     * @see https://buildkite.com/docs/apis/rest-api/builds#list-all-builds
     *
     * @param filters Filter criteria.
     * @return All builds which match the supplied search criteria. Results will be paged if the number of results
     *         exceeds 30 (or the page limit specified in the search criteria).
     * @throws BuildkiteException if API returns an error response.
     */
    public ListBuildsResponse listBuilds(final BuildFilters filters) {
        return executeRequest(new ListBuildsRequest(filters));
    }

    /**
     * Retrieve a specific build based on the filter criteria.
     *
     * @see https://buildkite.com/docs/apis/rest-api/builds#get-a-build
     *
     * @param filters Filter criteria.
     * @return The build which matches the criteria if found.
     * @throws BuildkiteException if API returns an error response.
     */
    public Optional getBuild(final GetBuildFiltersBuilder filters) {
        return getBuild(filters.build());
    }

    /**
     * Retrieve a specific build based on the filter criteria.
     *
     * @see https://buildkite.com/docs/apis/rest-api/builds#get-a-build
     *
     * @param organizationSlugId Organization associated with the build.
     * @param pipelineSlugId Pipeline associated with the build.
     * @param buildNumber The build number.
     * @return The build which matches the criteria if found.
     * @throws BuildkiteException if API returns an error response.
     */
    public Optional getBuild(final String organizationSlugId, final String pipelineSlugId, final long buildNumber) {
        return getBuild(GetBuildFilters.newBuilder()
                .withOrgIdSlug(organizationSlugId)
                .withPipelineIdSlug(pipelineSlugId)
                .withBuildNumber(buildNumber)
        );
    }

    /**
     * Retrieve a specific build based on the filter criteria.
     *
     * @see https://buildkite.com/docs/apis/rest-api/builds#get-a-build
     *
     * @param filters Filter criteria.
     * @return The build which matches the criteria if found.
     * @throws BuildkiteException if API returns an error response.
     */
    public Optional getBuild(final GetBuildFilters filters) {
        final Build build = executeRequest(new GetBuildRequest(filters));
        return Optional.ofNullable(build);
    }

    /**
     * Cancels the build if its state is either scheduled or running.
     *
     * @see https://buildkite.com/docs/apis/rest-api/builds#cancel-a-build
     *
     * @param organizationSlugId Organization associated with the build.
     * @param pipelineSlugId Pipeline associated with the build.
     * @param buildNumber The build number.
     * @return Updated Build instance.
     * @throws BuildkiteException if API returns an error response.
     */
    public Build cancelBuild(final String organizationSlugId, final String pipelineSlugId, final long buildNumber) {
        return executeRequest(new CancelBuildRequest(organizationSlugId, pipelineSlugId, buildNumber));
    }

    /**
     * Creates a new build to be executed.
     *
     * @see https://buildkite.com/docs/apis/rest-api/builds#create-a-build
     *
     * @param createBuildOptions Defines the build to be created.
     * @return Created Build instance.
     * @throws BuildkiteException if API returns an error response.
     */
    public Build createBuild(final CreateBuildOptionsBuilder createBuildOptions) {
        return createBuild(createBuildOptions.build());
    }

    /**
     * Creates a new build to be executed.
     *
     * @see https://buildkite.com/docs/apis/rest-api/builds#create-a-build
     *
     * @param createBuildOptions Defines the build to be created.
     * @return Created Build instance.
     * @throws BuildkiteException if API returns an error response.
     */
    public Build createBuild(final CreateBuildOptions createBuildOptions) {
        return executeRequest(new CreateBuildRequest(createBuildOptions));
    }

    /**
     * Retries a build.
     *
     * @see https://buildkite.com/docs/apis/rest-api/builds#rebuild-a-build
     *
     * @param organizationSlugId Organization associated with the build.
     * @param pipelineSlugId Pipeline associated with the build.
     * @param buildNumber The build number.
     * @return Updated Build instance.
     * @throws BuildkiteException if API returns an error response.
     */
    public Build rebuildBuild(final String organizationSlugId, final String pipelineSlugId, final long buildNumber) {
        return executeRequest(new RebuildBuildRequest(organizationSlugId, pipelineSlugId, buildNumber));
    }

    /**
     * Retries a failed or timed_out job. You can only retry each job.id once.
     * To retry a "second time" use the new job.id returned in the first retry query.
     *
     * @see https://buildkite.com/docs/apis/rest-api/jobs#retry-a-job
     *
     * @param options Defines which Job to retry.
     * @return Updated job instance.
     * @throws BuildkiteException if API returns an error response.
     */
    public Job retryJob(final RetryJobOptionsBuilder options) {
        return retryJob(options.build());
    }

    /**
     * Retries a failed or timed_out job. You can only retry each job.id once.
     * To retry a "second time" use the new job.id returned in the first retry query.
     *
     * @see https://buildkite.com/docs/apis/rest-api/jobs#retry-a-job
     *
     * @param options Defines which Job to retry.
     * @return Updated job instance.
     * @throws BuildkiteException if API returns an error response.
     */
    public Job retryJob(final RetryJobOptions options) {
        return executeRequest(new RetryJobRequest(options));
    }

    /**
     * Unblocks a build's "Block pipeline" job. The job's unblockable property indicates whether it is able to be unblocked,
     * and the unblock_url property points to this endpoint.
     *
     * @see https://buildkite.com/docs/apis/rest-api/jobs#unblock-a-job
     *
     * @param options Defines which Job to unblock.
     * @return Updated job instance.
     * @throws BuildkiteException if API returns an error response.
     */
    public Job unblockJob(final UnblockJobOptionsBuilder options) {
        return unblockJob(options.build());
    }

    /**
     * Unblocks a build's "Block pipeline" job. The job's unblockable property indicates whether it is able to be unblocked,
     * and the unblock_url property points to this endpoint.
     *
     * @see https://buildkite.com/docs/apis/rest-api/jobs#unblock-a-job
     *
     * @param options Defines which Job to unblock.
     * @return Updated job instance.
     * @throws BuildkiteException if API returns an error response.
     */
    public Job unblockJob(final UnblockJobOptions options) {
        return executeRequest(new UnblockJobRequest(options));
    }

    /**
     * List all Emojis defined in the given Organization.
     *
     * @see https://buildkite.com/docs/apis/rest-api/emojis#list-emojis
     *
     * @param orgIdSlug Organization Id slug to retrieve list of emojis for.
     * @return List of Emojis.
     * @throws BuildkiteException if API returns an error response.
     */
    public List listEmojis(final String orgIdSlug) {
        return executeRequest(new ListEmojisRequest(orgIdSlug));
    }

    /**
     * Retrieve annotations for a given build.
     *
     * @see https://buildkite.com/docs/apis/rest-api/annotations#list-annotations-for-a-build
     *
     * @param options Defines which build to retrieve annotations for.
     * @return Annotations associated with the build.
     * @throws BuildkiteException if API returns an error response.
     */
    public AnnotationsForBuildResponse getAnnotationsForBuild(final AnnotationFilters options) {
        return executeRequest(new GetAnnotationsForBuildRequest(options));
    }

    /**
     * Retrieves metadata endpoint.
     * @see https://buildkite.com/docs/apis/rest-api/meta#get-meta-information
     *
     * @return Details about the current User.
     * @throws BuildkiteException if API returns an error response.
     */
    public MetaResponse getMeta() throws BuildkiteException {
        return executeRequest(new GetMetaRequest());
    }

    /**
     * Retrieve the next page of results from the previously retrieved request.
     *
     * @param  The parsed return object representing the result.
     * @param response Previously retrieved result/response to retrieve the next page of results for.
     * @return The next page of results.
     * @throws InvalidPagingRequestException if no next page exists to retrieve.
     * @throws BuildkiteException if API returns an error response.
     */
    public  T nextPage(final PageableResponse response) {
        // Validate
        Objects.requireNonNull(response);
        final PagingLinks pagingLinks = Objects.requireNonNull(response.getPagingLinks());
        if (!pagingLinks.hasNextUrl()) {
            throw new InvalidPagingRequestException(
                "Requested 'Next' page on response " + response.getClass().getSimpleName() + ", but no Next page is available."
            );
        }

        // Update request with appropriate page options.
        final PageOptions pageOptions;
        try {
            pageOptions = PageOptions.fromUrl(pagingLinks.getNextUrl());
        } catch (final IllegalArgumentException ex) {
            throw new InvalidPagingRequestException("Unable to parse URL for paging information", ex);
        }
        final PageableRequest request = response.getOriginalRequest();
        request.updatePageOptions(pageOptions);

        // Execute and return.
        return executeRequest(request);
    }

    /**
     * Retrieve the previous page of results from the previously retrieved request.
     *
     * @param  The parsed return object representing the result.
     * @param response Previously retrieved result/response to retrieve the previous page of results for.
     * @return The previous page of results.
     * @throws InvalidPagingRequestException if no previous page exists to retrieve.
     * @throws BuildkiteException if API returns an error response.
     */
    public  T previousPage(final PageableResponse response) {
        // Validate
        Objects.requireNonNull(response);
        final PagingLinks pagingLinks = Objects.requireNonNull(response.getPagingLinks());
        if (!pagingLinks.hasPrevUrl()) {
            throw new InvalidPagingRequestException(
                "Requested 'Previous' page on response " + response.getClass().getSimpleName() + ", but no Previous page is available."
            );
        }

        // Update request with appropriate page options.
        final PageOptions pageOptions;
        try {
            pageOptions = PageOptions.fromUrl(pagingLinks.getNextUrl());
        } catch (final IllegalArgumentException ex) {
            throw new InvalidPagingRequestException("Unable to parse URL for paging information", ex);
        }
        final PageableRequest request = response.getOriginalRequest();
        request.updatePageOptions(pageOptions);

        // Execute and return.
        return executeRequest(request);
    }

    /**
     * Retrieve the first page of results from the previously retrieved request.
     *
     * @param  The parsed return object representing the result.
     * @param response Previously retrieved result/response to retrieve the first page of results for.
     * @return The first page of results.
     * @throws InvalidPagingRequestException if no previous page exists to retrieve.
     * @throws BuildkiteException if API returns an error response.
     */
    public  T firstPage(final PageableResponse response) {
        // Validate
        Objects.requireNonNull(response);
        final PagingLinks pagingLinks = Objects.requireNonNull(response.getPagingLinks());
        if (!pagingLinks.hasFirstUrl()) {
            throw new InvalidPagingRequestException(
                "Requested 'First' page on response " + response.getClass().getSimpleName() + ", but no First page is available."
            );
        }

        // Update request with appropriate page options.
        final PageOptions pageOptions;
        try {
            pageOptions = PageOptions.fromUrl(pagingLinks.getNextUrl());
        } catch (final IllegalArgumentException ex) {
            throw new InvalidPagingRequestException("Unable to parse URL for paging information", ex);
        }
        final PageableRequest request = response.getOriginalRequest();
        request.updatePageOptions(pageOptions);

        // Execute and return.
        return executeRequest(request);
    }

    /**
     * Retrieve the last page of results from the previously retrieved request.
     *
     * @param  The parsed return object representing the result.
     * @param response Previously retrieved result/response to retrieve the last page of results for.
     * @return The last page of results.
     * @throws InvalidPagingRequestException if no previous page exists to retrieve.
     * @throws BuildkiteException if API returns an error response.
     */
    public  T lastPage(final PageableResponse response) {
        // Validate
        Objects.requireNonNull(response);
        final PagingLinks pagingLinks = Objects.requireNonNull(response.getPagingLinks());
        if (!pagingLinks.hasLastUrl()) {
            throw new InvalidPagingRequestException(
                "Requested 'Last' page on response " + response.getClass().getSimpleName() + ", but no Last page is available."
            );
        }

        // Update request with appropriate page options.
        final PageOptions pageOptions;
        try {
            pageOptions = PageOptions.fromUrl(pagingLinks.getNextUrl());
        } catch (final IllegalArgumentException ex) {
            throw new InvalidPagingRequestException("Unable to parse URL for paging information", ex);
        }
        final PageableRequest request = response.getOriginalRequest();
        request.updatePageOptions(pageOptions);

        // Execute and return.
        return executeRequest(request);
    }

    /**
     * Execute the given request, returning the parsed response, or throwing the appropriate
     * exception if an error was returned from the API.
     *
     * This method scoped public to allow for user defined requests to be executed by the library
     * as an extension point.
     *
     * @param  The parsed response object.
     * @param request The request to execute.
     * @return The parsed response object.
     * @throws BuildkiteException if API returns an error response.
     */
    public  T executeRequest(final Request request) throws BuildkiteException {
        final HttpResult result = httpClient.executeRequest(request);

        // Debug logging of the result.
        logger.trace("HttpResult: {}", result);

        // Handle Errors based on HttpCode.
        if (result.getStatus() != 200 && result.getStatus() != 201 && result.getStatus() != 204) {
            handleError(result);
        }

        // Success response code, parse response into object and return.
        return request.parseResponse(result);
    }

    /**
     * Handle error responses from the API by throwing the appropriate exception.
     * @param errorResult Error response from REST API.
     * @throws BuildkiteException relating to specific underlying API error.
     */
    private void handleError(final HttpResult errorResult) throws BuildkiteException {
        // Attempt to parse error response.
        String errorMessage = null;
        List errors = Collections.emptyList();
        try {
            final ErrorResponse errorResponse = new ErrorResponseParser().parseResponse(errorResult);
            errorMessage = errorResponse.getMessage();
            errors = errorResponse.getErrors();
        } catch (final IOException e) {
            errorMessage = errorResult.getContent();
        }
        if (errorMessage != null && errorMessage.trim().isEmpty()) {
            errorMessage = null;
        }

        switch (errorResult.getStatus()) {
            case 401:
                throw new InvalidAccessTokenException(
                    errorMessage == null ? "Invalid Access Token" : errorMessage
                );
            case 403:
                throw new InvalidAllowedIpAddressException(
                    errorMessage == null
                    ?
                        "API requested from an IP address not specifically allowed by your AccessToken. "
                        + "Check the 'Allowed IP Addresses' field on your Access Token"
                    : errorMessage
                );
            case 404:
                throw new NotFoundException(
                    errorMessage == null
                        ?
                        "The URL or Resource Request could not be found"
                        : errorMessage
                );
            case 422:
                String validationErrorMessage = (( errorMessage != null) ? errorMessage : "The submitted request was invalid. ");
                validationErrorMessage += "\n" + errors.stream()
                    .map((error) -> error.getField() + ": " + error.getCode())
                    .collect(Collectors.joining("\n"));
                throw new InvalidRequestException(validationErrorMessage, errors);
            default:
                throw new BuildkiteException(
                    errorMessage == null ? "Unknown/Unhandled Error HttpCode: " + errorResult.getStatus() : errorMessage
                );
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy