com.github.mike10004.jenkinsbbhook.WebhookHandler.java Source code

Java tutorial

Introduction

Here is the source code for com.github.mike10004.jenkinsbbhook.WebhookHandler.java

Source

/*
 * (c) 2015 Mike Chaberski, distributed under MIT License
 */
package com.github.mike10004.jenkinsbbhook;

import com.google.common.annotations.VisibleForTesting;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.base.Strings;
import com.google.common.base.Supplier;
import com.google.common.collect.Iterators;
import com.google.common.io.CharStreams;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonSyntaxException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URI;
import java.net.URL;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.BadRequestException;
import javax.ws.rs.core.Context;
import javax.ws.rs.Consumes;
import javax.ws.rs.InternalServerErrorException;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.UriBuilder;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.AuthCache;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.impl.auth.BasicScheme;
import org.apache.http.impl.client.BasicAuthCache;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;

/**
 * REST Web Service
 *
 * @author mchaberski
 */
@Path("build")
public class WebhookHandler {

    private static final Logger log = Logger.getLogger(WebhookHandler.class.getName());

    @Context
    private ServletContext context;

    private transient final Supplier<? extends CloseableHttpClient> httpClientFactory;

    private transient final Gson gson;

    /**
     * Creates a new instance of WebhookResource
     */
    public WebhookHandler() {
        this(new SystemHttpClientSupplier(), (ServletContext) null);
    }

    @VisibleForTesting
    WebhookHandler(Supplier<? extends CloseableHttpClient> httpClientFactory, @Nullable ServletContext context) {
        this.httpClientFactory = checkNotNull(httpClientFactory);
        this.context = context;
        gson = new GsonBuilder().setPrettyPrinting().create();
    }

    /**
     * POST method for handling webhook.
     * @param content representation for the resource
     * @return an HTTP response with content of the updated or created resource.
     */
    @POST
    @Consumes("application/json")
    @Produces("application/json")
    public String relayBuildRequest(@Context HttpServletRequest request, @QueryParam("job") String jobName,
            @QueryParam("project_token") String projectToken, @QueryParam("username") String username,
            @QueryParam("api_token") String apiToken, String content) throws IOException {
        if (!"repo:push".equals(request.getHeader("X-Event-Key"))) {
            throw new BadRequestException("unhandled event key header value");
        }
        Push push;
        try {
            push = gson.fromJson(content, Push.class);
        } catch (JsonSyntaxException e) {
            throw new BadRequestException("malformed json in request body", e);
        }
        jobName = Strings.nullToEmpty(jobName);
        projectToken = Strings.nullToEmpty(projectToken);
        username = Strings.nullToEmpty(username);
        apiToken = Strings.nullToEmpty(apiToken);
        log.log(Level.INFO, "jobName={0}; projectToken.length={1}; username={2}, apiToken.length={3}; {4}",
                new Object[] { jobName, projectToken.length(), username, apiToken.length(), push });
        if (projectToken.isEmpty()) {
            throw new BadRequestException("project_token query parameter absent or empty");
        }
        if (jobName.isEmpty()) {
            throw new BadRequestException("could not derive job name from path segment");
        }
        if (apiToken.isEmpty()) {
            throw new BadRequestException("api_token query parameter absent or empty");
        }
        if (username.isEmpty()) {
            throw new BadRequestException("username query parameter absent or empty");
        }
        AppParams appParams = loadAppParams();
        if (appParams.getJenkinsBaseUrl() == null) {
            throw new InternalServerErrorException("server not configured: init param "
                    + ContextAppParams.PARAM_JENKINS_BASE_URL + " is required");
        }
        PostRequestRelayer relayer = constructRelayer(appParams);
        CrumbData crumbData = relayer.fetchCrumbData(appParams, username, apiToken);
        log.log(Level.FINER, "crumb data: {0}", crumbData);
        URI postedUri = relayer.sendBuildPostRequest(appParams, crumbData, jobName, projectToken);
        ResponseData responseData = new ResponseData(appParams.getJenkinsBaseUrl(), crumbData, jobName,
                postedUri.toString(), appParams.isSimulation());
        return gson.toJson(responseData);
    }

    public static interface PostRequestRelayer {
        CrumbData fetchCrumbData(AppParams appParams, String username, String apiToken) throws IOException;

        URI sendBuildPostRequest(AppParams appParams, CrumbData crumbData, String jobName, String token)
                throws IOException;
    }

    protected PostRequestRelayer constructRelayer(AppParams appParams) {
        if (appParams.isSimulation()) {
            return new PostRequestRelayer() {

                @Override
                public CrumbData fetchCrumbData(AppParams appParams, String username, String apiToken)
                        throws IOException {
                    CrumbData crumbData = new CrumbData(UUID.randomUUID().toString(), ".crumb");
                    return crumbData;
                }

                @Override
                public URI sendBuildPostRequest(AppParams appParams, CrumbData crumbData, String jobName,
                        String token) throws IOException {
                    return buildJenkinsBuildUri(appParams, jobName, token);
                }
            };
        } else {
            return new DefaultPostRequestRelayer();
        }
    }

    public static class ResponseData {
        public String jenkinsBaseUrl;
        public CrumbData crumbData;
        public String jobName;
        public String jobUrl;
        public Boolean simulation;

        public ResponseData() {
        }

        public ResponseData(String jenkinsBaseUrl, CrumbData crumbData, String jobName, String jobUrl,
                boolean simulation) {
            this.jenkinsBaseUrl = jenkinsBaseUrl;
            this.crumbData = crumbData;
            this.jobName = jobName;
            this.jobUrl = jobUrl;
            this.simulation = simulation;
        }

    }

    protected AppParams loadAppParams() {
        log.log(Level.FINER, "loading app params; defined: {0}",
                Iterators.toString(Iterators.forEnumeration(context.getInitParameterNames())));
        return new ContextAppParams(new ServletInitParamValueProvider(context));
    }

    protected URI buildJenkinsBuildUri(AppParams appParams, String jobName, String projectToken) {
        return UriBuilder.fromUri(appParams.getJenkinsBaseUrl()).path("job/{jobName}/build")
                .queryParam("token", projectToken).build(jobName);
    }

    protected static int getPortOrDefault(URL url) {
        int port = url.getPort();
        if (port == -1) {
            port = url.getDefaultPort();
        }
        return port;
    }

    public class DefaultPostRequestRelayer implements PostRequestRelayer {

        @Override
        public CrumbData fetchCrumbData(AppParams appParams, String username, String apiToken) throws IOException {
            String jenkinsBaseUrl = appParams.getJenkinsBaseUrl();
            URI crumbIssuerUri = UriBuilder.fromUri(jenkinsBaseUrl).path("crumbIssuer/api/json").build();
            URL crumbIssuerUrl = crumbIssuerUri.toURL();
            HttpHost crumbHost = new HttpHost(crumbIssuerUrl.getHost(), getPortOrDefault(crumbIssuerUrl),
                    crumbIssuerUrl.getProtocol());
            CredentialsProvider credentialsProvider = asCredentialsProvider(crumbIssuerUri.toURL(), appParams,
                    username, apiToken);
            CloseableHttpClient client = httpClientFactory.get();
            RequestConfig requestConfig = RequestConfig.custom().setAuthenticationEnabled(true).build();
            AuthCache authCache = new BasicAuthCache();
            BasicScheme basicAuth = new BasicScheme();
            authCache.put(crumbHost, basicAuth);
            HttpClientContext clientContext = HttpClientContext.create();
            clientContext.setRequestConfig(requestConfig);
            clientContext.setCredentialsProvider(credentialsProvider);
            clientContext.setAuthCache(authCache);
            HttpGet request = new HttpGet(crumbIssuerUri);
            String crumbJson;
            CrumbData crumbData;
            try (CloseableHttpResponse response = client.execute(request, clientContext);
                    Reader reader = newReader(response.getEntity())) {
                crumbJson = CharStreams.toString(reader);
            }
            try {
                crumbData = gson.fromJson(crumbJson, CrumbData.class);
            } catch (JsonSyntaxException e) {
                log.log(Level.INFO, "response from crumb issuer is malformed json: {0}",
                        StringUtils.abbreviate(crumbJson, 512));
                throw e;
            }
            if (crumbData == null) {
                throw new InternalServerErrorException(
                        "crumb not present in entity returned from " + crumbIssuerUri);
            }
            return crumbData;
        }

        @Override
        public URI sendBuildPostRequest(AppParams appParams, CrumbData crumbData, String jobName, String token)
                throws IOException {
            URI jenkinsBuildUri = buildJenkinsBuildUri(appParams, jobName, token);
            log.log(Level.FINER, "jenkins build uri = {0}", jenkinsBuildUri);
            CloseableHttpClient client = httpClientFactory.get();
            HttpPost request = new HttpPost(jenkinsBuildUri);
            request.setHeader(crumbData.crumbRequestField, crumbData.crumb);
            try (CloseableHttpResponse response = client.execute(request)) {
                int status = response.getStatusLine().getStatusCode();
                log.log(Level.INFO, "response from jenkins (base={0}) is {1}",
                        new Object[] { appParams.getJenkinsBaseUrl(), response.getStatusLine() });
                if (!isSuccessStatus(status)) {
                    throw new InternalServerErrorException(
                            "response to POST request to jenkins project build URL has status code " + status);
                }
            }
            return jenkinsBuildUri;
        }
    }

    protected boolean isSuccessStatus(int statusCode) {
        return statusCode >= 200 && statusCode < 300;
    }

    protected Reader newReader(HttpEntity entity) throws IOException {
        return new InputStreamReader(entity.getContent());
    }

    protected CredentialsProvider asCredentialsProvider(URL jenkinsCrumbUrl, AppParams appParams, String username,
            String apiToken) {
        CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
        String jenkinsHost = jenkinsCrumbUrl.getHost();
        int jenkinsPort = jenkinsCrumbUrl.getPort();
        if (jenkinsPort == -1) {
            jenkinsPort = jenkinsCrumbUrl.getDefaultPort();
        }
        AuthScope scope = new AuthScope(jenkinsHost, jenkinsPort);
        credentialsProvider.setCredentials(scope, new UsernamePasswordCredentials(username, apiToken));
        return credentialsProvider;
    }

    public static class CrumbData {

        public String crumb;
        public String crumbRequestField;

        public CrumbData(String crumb, String crumbRequestField) {
            this.crumb = crumb;
            this.crumbRequestField = crumbRequestField;
        }

        public CrumbData() {
        }

        @Override
        public String toString() {
            return "CrumbData{" + "crumb=" + crumb + ", crumbRequestField=" + crumbRequestField + '}';
        }

    }

    private static class SystemHttpClientSupplier implements Supplier<CloseableHttpClient> {

        public SystemHttpClientSupplier() {
        }

        @Override
        public CloseableHttpClient get() {
            return HttpClients.createSystem();
        }
    }

}