mobi.jenkinsci.ci.client.JenkinsClient.java Source code

Java tutorial

Introduction

Here is the source code for mobi.jenkinsci.ci.client.JenkinsClient.java

Source

// Copyright (C) 2013 GerritForge www.gerritforge.com
//
// 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 mobi.jenkinsci.ci.client;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.servlet.http.HttpServletRequest;

import mobi.jenkinsci.ci.client.sso.AssemblaSsoHandler;
import mobi.jenkinsci.ci.client.sso.GitHubSsoHandler;
import mobi.jenkinsci.ci.client.sso.GoogleSsoHandler;
import mobi.jenkinsci.ci.model.Build;
import mobi.jenkinsci.ci.model.ChangeSet;
import mobi.jenkinsci.ci.model.ChangeSetItem;
import mobi.jenkinsci.ci.model.ChangeSetItem.Issue;
import mobi.jenkinsci.ci.model.ComputerList;
import mobi.jenkinsci.ci.model.FailedSuite;
import mobi.jenkinsci.ci.model.FailedTest;
import mobi.jenkinsci.ci.model.FailedTestsList;
import mobi.jenkinsci.ci.model.JenkinsItem;
import mobi.jenkinsci.ci.model.Job;
import mobi.jenkinsci.ci.model.Queue;
import mobi.jenkinsci.ci.model.View;
import mobi.jenkinsci.ci.model.ViewList;
import mobi.jenkinsci.commons.Account;
import mobi.jenkinsci.commons.Constants;
import mobi.jenkinsci.model.AbstractNode;
import mobi.jenkinsci.model.ResetNode;
import mobi.jenkinsci.net.UrlPath;
import mobi.jenkinsci.plugin.PluginConfig;

import org.apache.http.HttpHeaders;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.log4j.Logger;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;

import com.google.gson.Gson;

public class JenkinsClient {
    public static final String VIEWS_ALL = "_all";
    private static final Logger LOG = Logger.getLogger(JenkinsClient.class);
    private static final HashMap<PluginConfig, JenkinsClient> clients = new HashMap<PluginConfig, JenkinsClient>();
    private static final int QUERY_DEPTH = 1;
    private static final String PRETTY_JSON = Boolean.getBoolean("JENKINS_CLOUD_JSON_PRETTY") ? "&pretty=true" : "";
    private static final String QUERY_STRING = "?depth=" + QUERY_DEPTH + PRETTY_JSON;
    private static final HashSet<String> PROPAGATED_HEADERS_WHITELIST = new HashSet<String>();
    static {
        PROPAGATED_HEADERS_WHITELIST.add(Constants.X_AUTH_OTP_HEADER.toLowerCase());
        PROPAGATED_HEADERS_WHITELIST.add(HttpHeaders.USER_AGENT.toLowerCase());

        GitHubSsoHandler.init();
        GoogleSsoHandler.init();
        AssemblaSsoHandler.init();
    }

    public final JenkinsConfig config;
    public final JenkinsHttpClient http;
    public final Account account;

    public JenkinsClient(final Account account, final JenkinsConfig confObject) throws MalformedURLException {
        this.config = confObject;
        this.http = new JenkinsHttpClient(confObject);
        this.account = account;
    }

    public static JenkinsClient getInstance(final Account account, final PluginConfig pluginConf)
            throws MalformedURLException {
        JenkinsClient client = clients.get(pluginConf);
        if (client == null) {
            client = new JenkinsClient(account, new JenkinsConfig(pluginConf));
            clients.put(pluginConf, client);
        }

        return client;
    }

    public <T> T load(final String url, final String queryString, final Class<T> returnType,
            final HashMap<String, String> extraHeaders) throws IOException {
        final String query = url + "/api/json" + queryString;

        LOG.info("Request to Jenkins: '" + query + "'");

        final Map<String, String> headers = new HashMap<String, String>();
        if (extraHeaders != null) {
            headers.putAll(extraHeaders);
        }
        final HttpGet get = new HttpGet(query);
        for (final Entry<String, String> header : headers.entrySet()) {
            get.addHeader(header.getKey(), header.getValue());
        }
        try {
            final InputStream result = http.getInputStream(get);
            final InputStreamReader jsonReader = new InputStreamReader(result, "UTF-8");
            try {
                final T outObj = new Gson().fromJson(jsonReader, returnType);
                if (outObj instanceof JenkinsItem) {
                    ((JenkinsItem) outObj).init(this);
                }
                return outObj;
            } finally {
                jsonReader.close();
            }
        } finally {
            get.releaseConnection();
        }
    }

    public Document loadPage(final String url, final HashMap<String, String> extraHeaders) throws IOException {
        LOG.info("Request to Jenkins Page: '" + url + "'");

        final Map<String, String> headers = new HashMap<String, String>();
        if (extraHeaders != null) {
            headers.putAll(extraHeaders);
        }
        final HttpGet get = new HttpGet(url);
        for (final Entry<String, String> header : headers.entrySet()) {
            get.addHeader(header.getKey(), header.getValue());
        }
        try {
            final InputStream result = http.getInputStream(get);
            return Jsoup.parse(result, "UTF-8", url);
        } finally {
            get.releaseConnection();
        }
    }

    public ViewList getViewList() throws IOException {
        return getViewList(null);
    }

    public ViewList getViewList(final HttpServletRequest req) throws IOException {
        final String query = QUERY_STRING;
        final ViewList viewList = load(config.getUrl(), query, ViewList.class, getHeaders(req));
        viewList.path = ("views");
        return viewList;
    }

    private HashMap<String, String> getHeaders(final HttpServletRequest req) {
        final HashMap<String, String> propagatedHeader = new HashMap<String, String>();
        if (req != null) {
            final Enumeration<String> headerNames = req.getHeaderNames();
            while (headerNames.hasMoreElements()) {
                final String headerName = headerNames.nextElement();
                if (PROPAGATED_HEADERS_WHITELIST.contains(headerName.toLowerCase())) {
                    propagatedHeader.put(headerName, req.getHeader(headerName));
                }
            }
        }
        return propagatedHeader;
    }

    public Job getJob(final String jobPath) throws IOException {
        final String query = QUERY_STRING;
        final Job job = load(config.getUrl() + "/job/" + jobPath, query, Job.class, null);
        job.path = jobPath;
        return job;
    }

    public static String urlEncode(final String url) {
        // Seems that Jenkins doesn't like spaces URL-encoded as '+'
        try {
            return URLEncoder.encode(url, "UTF-8").replaceAll("[+]", "%20");
        } catch (final UnsupportedEncodingException e) {
            return url;
        }
    }

    public ComputerList getComputerList() throws IOException {
        final ComputerList computerList = load(config.getUrl() + "/computer", "", ComputerList.class, null);
        computerList.path = ("computer");
        return computerList;
    }

    public Queue getQueue() throws IOException {
        final Queue queue = load(config.getUrl() + "/queue", "", Queue.class, null);
        queue.path = ("queue");
        return queue;
    }

    public FailedTestsList getFailedTestsList() throws IOException {
        final String query = "?tree=suites[name,cases[status,name,failedSince]]";
        final FailedTestsList list = load(config.getUrl(), query, FailedTestsList.class, null);
        list.path = ("failedTest");
        filterForTestStatus(list, "FAILED");
        return list;
    }

    private <T> void filterForTestStatus(final T result, final String status) {
        final FailedTestsList list = (FailedTestsList) result;
        final List<FailedSuite> suiteToRemoveList = new LinkedList<FailedSuite>();
        for (final FailedSuite suite : list.getSuites()) {
            final List<FailedTest> toRemove = new LinkedList<FailedTest>();
            for (final FailedTest test : suite.getCases()) {
                if (!test.getStatus().equalsIgnoreCase(status)) {
                    toRemove.add(test);
                }
            }
            // remove
            for (final FailedTest testToRemove : toRemove) {
                suite.getCases().remove(testToRemove);
            }

            if (suite.getCases().size() == 0) {
                suiteToRemoveList.add(suite);
            }
        }

        for (final FailedSuite suiteToRemove : suiteToRemoveList) {
            list.getSuites().remove(suiteToRemove);
        }
    }

    public JenkinsItem getVewDetails(final String viewName) throws IOException {
        final String query = QUERY_STRING;
        final JenkinsItem view = load(
                config.getUrl() + (viewName.equals(VIEWS_ALL) ? "" : "/view/" + urlEncode(viewName)), query,
                View.class, null);
        view.path = (viewName);
        return view;
    }

    public String getCommandUrl(String command) {
        if (command.startsWith("http")) {
            return command;
        }

        String baseUrl = config.getUrl();
        if (!baseUrl.endsWith("/")) {
            baseUrl = baseUrl + "/";
        }
        if (command.startsWith("/")) {
            command = command.substring(1);
        }
        return baseUrl + command;
    }

    public AbstractNode execute(final UrlPath reqPath, final String command) throws IOException {
        final List<String> pathComponents = reqPath.getComponents();
        return execute("job/" + pathComponents.get(pathComponents.size() - 1) + "/" + command + "?delay=0");
    }

    public AbstractNode execute(final String command) throws IOException {
        final HttpPost post = new HttpPost(getCommandUrl(command));
        try {
            final HttpResponse response = http.execute(post);
            final int statusCode = response.getStatusLine().getStatusCode();
            if (statusCode == HttpURLConnection.HTTP_OK || statusCode == HttpURLConnection.HTTP_MOVED_PERM
                    || statusCode == HttpURLConnection.HTTP_MOVED_TEMP) {
                return new ResetNode();
            } else {
                throw new IOException(
                        "Command " + command + " *FAILED* with HTTP Status " + response.getStatusLine());
            }
        } finally {
            post.releaseConnection();
        }
    }

    public ChangeSet getJobChanges(final String jobPath, final int jobBuildNumber) throws IOException {
        final Document changePage = loadPage(
                config.getUrl() + "/job/" + jobPath + "/" + jobBuildNumber + "/changes", null);
        final Element changesList = changePage.select("table[class=pane]").first();
        final HashMap<String, Issue> issues = getIssuesFromTable(changesList);

        final ChangeSet changeSet = load(config.getUrl() + "/job/" + jobPath + "/" + jobBuildNumber, QUERY_STRING,
                Build.class, null).changeSet;
        for (final Iterator<ChangeSetItem> iterator = changeSet.items.iterator(); iterator.hasNext();) {
            final ChangeSetItem changeItem = iterator.next();
            changeItem.issue = issues.get(changeItem.getUniqueId());
        }

        return changeSet;
    }

    private HashMap<String, Issue> getIssuesFromTable(final Element changesTable) {
        final HashMap<String, Issue> issues = new HashMap<String, ChangeSetItem.Issue>();
        if (changesTable == null) {
            return issues;
        }

        if (changesTable.children().size() <= 0) {
            LOG.warn("Cannot find changes TBODY");
            return issues;
        }

        final Element tbody = changesTable.child(0);
        final Elements rows = tbody.children();
        for (final Element row : rows) {
            final String commitId = getCommitIdFromRow(row);
            Issue issue;
            try {
                issue = getIssueFromRow(row);
                if (issue != null) {
                    issues.put(commitId, issue);
                }
            } catch (final MalformedURLException e) {
                LOG.warn("Invalid issue URL for row " + row.toString() + ": skipping", e);
            }
        }

        return issues;
    }

    private Issue getIssueFromRow(final Element row) throws MalformedURLException {
        final Element fullChangeMessage = row.select("div[class=changeset-message]").first();
        if (fullChangeMessage == null) {
            return null;
        }

        final Element issueLink = fullChangeMessage.select("pre").first().select("a").first();
        if (issueLink == null) {
            return null;
        } else {
            final Element issueIcon = issueLink.select("img").first();
            return new Issue(getUrl(issueLink, "href"), issueLink.attr("tooltip"), getUrl(issueIcon, "src"));
        }
    }

    private URL getUrl(final Element issueLink, final String attr) throws MalformedURLException {
        if (issueLink == null) {
            return null;
        }

        final String linkUrl = issueLink.attr(attr);
        if (linkUrl == null) {
            return null;
        }

        if (linkUrl.startsWith("http")) {
            return new URL(linkUrl);
        } else {
            final URL baseUrl = new URL(issueLink.baseUri());
            return new URL(baseUrl, linkUrl);
        }
    }

    private String getCommitIdFromRow(final Element row) {
        final Element fullChangeDesc = row.select("div[class=changeset-message]").first();
        if (fullChangeDesc == null) {
            return null;
        }
        final Element message = fullChangeDesc.select("b").first();
        final String messageText = message.childNode(0).toString();
        final Matcher commitMatch = Pattern.compile("Commit ([^ ]+)").matcher(messageText);
        if (commitMatch.find()) {
            return commitMatch.group(1);
        } else {
            return null;
        }
    }

    public ArtifactFingerprint getArtifactFromMD5(final String md5Fingerprint) throws Exception {
        return load(config.getUrl() + "/fingerprint/" + md5Fingerprint, QUERY_STRING, ArtifactFingerprint.class,
                null);
    }
}