com.ruesga.rview.misc.ContinuousIntegrationHelper.java Source code

Java tutorial

Introduction

Here is the source code for com.ruesga.rview.misc.ContinuousIntegrationHelper.java

Source

/*
 * Copyright (C) 2016 Jorge Ruesga
 *
 * 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.ruesga.rview.misc;

import android.text.TextUtils;
import android.util.Log;

import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.ruesga.rview.gerrit.model.ChangeMessageInfo;
import com.ruesga.rview.model.ContinuousIntegrationInfo;
import com.ruesga.rview.model.ContinuousIntegrationInfo.BuildStatus;
import com.ruesga.rview.model.Repository;

import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.regex.Matcher;

import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

import static com.ruesga.rview.widget.RegExLinkifyTextView.WEB_LINK_REGEX;

public class ContinuousIntegrationHelper {

    private static final String TAG = "ContinuousIntegration";

    private static final String CI_REGEXP_1 = "\\* .*::((#)?\\d+::)?((OK, )|(WARN, )|(IGNORE, )|(ERROR, )).*";

    public static List<ContinuousIntegrationInfo> getContinuousIntegrationStatus(Repository repository,
            String changeId, int revisionNumber) {
        List<ContinuousIntegrationInfo> ci = new ArrayList<>();
        if (!TextUtils.isEmpty(repository.mCiStatusMode) && !TextUtils.isEmpty(repository.mCiStatusUrl)) {
            // Check status mode
            switch (repository.mCiStatusMode) {
            case "cq":
                // BuildBot CQ status type
                return obtainFromCQServer(repository, changeId, revisionNumber);
            }
        }
        return sort(ci);
    }

    @SuppressWarnings({ "ConstantConditions", "TryFinallyCanBeTryWithResources" })
    private static List<ContinuousIntegrationInfo> obtainFromCQServer(Repository repository, String changeId,
            int revisionNumber) {
        List<ContinuousIntegrationInfo> statuses = new ArrayList<>();

        // NOTE: Do not fix "Redundant character escape" lint warning. Remove trailing '\'
        // will cause an PatternSyntaxException
        String url = repository.mCiStatusUrl.replaceFirst("\\{change\\}", changeId).replaceFirst("\\{revision\\}",
                String.valueOf(revisionNumber));

        try {
            OkHttpClient okhttp = NetworkingHelper.createNetworkClient().build();
            Request request = new Request.Builder().url(url).build();

            Response response = okhttp.newCall(request).execute();
            try {
                String json = response.body().string();
                JsonObject root = new JsonParser().parse(json).getAsJsonObject();
                if (!root.has("builds")) {
                    return statuses;
                }
                JsonArray o = root.getAsJsonArray("builds");
                int c1 = o.size();
                for (int i = 0; i < c1; i++) {
                    JsonObject b = o.get(i).getAsJsonObject();
                    try {
                        String status = b.get("status").getAsString();
                        String ciUrl = b.get("url").getAsString();
                        String ciName = null;
                        JsonArray tags = b.get("tags").getAsJsonArray();
                        int c2 = tags.size();
                        for (int j = 0; j < c2; j++) {
                            String tag = tags.get(j).getAsJsonPrimitive().getAsString();
                            if (tag.startsWith("builder:")) {
                                ciName = tag.substring(tag.indexOf(":") + 1);
                            }
                        }

                        if (!TextUtils.isEmpty(status) && !TextUtils.isEmpty(ciUrl) && !TextUtils.isEmpty(ciName)) {
                            BuildStatus buildStatus = BuildStatus.RUNNING;
                            if (status.equals("COMPLETED")) {
                                String result = b.get("result").getAsString();

                                switch (result) {
                                case "SUCCESS":
                                    buildStatus = BuildStatus.SUCCESS;
                                    break;
                                case "FAILURE":
                                    buildStatus = BuildStatus.FAILURE;
                                    break;
                                default:
                                    buildStatus = BuildStatus.SKIPPED;
                                    break;
                                }
                            }

                            ContinuousIntegrationInfo c = findContinuousIntegrationInfo(statuses, ciName);
                            // Attempts are sorted in reversed order, so we need the first one
                            if (c == null) {
                                statuses.add(new ContinuousIntegrationInfo(ciName, ciUrl, buildStatus));
                            }
                        }

                    } catch (Exception ex) {
                        Log.w(TAG, "Failed to parse ci object" + b, ex);
                    }

                }

            } finally {
                response.close();
            }

        } catch (Exception ex) {
            Log.w(TAG, "Failed to obtain ci status from " + url, ex);
        }

        return statuses;
    }

    public static List<ContinuousIntegrationInfo> extractContinuousIntegrationInfo(int patchSet,
            ChangeMessageInfo[] messages, Repository repository) {
        List<ContinuousIntegrationInfo> ci = new ArrayList<>();
        Set<String> ciNames = new HashSet<>();
        try {
            boolean checkFromPrevious = false;
            for (ChangeMessageInfo message : messages) {
                if (message.revisionNumber == patchSet) {
                    if (ModelHelper.isCIAccount(message.author, repository)) {
                        final String[] lines = message.message.split("\n");
                        String prevLine = null;
                        for (String line : lines) {
                            // A CI line/s should have:
                            //   - Not a reply
                            //   - Have an url (to the ci backend)
                            //   - Have a build status
                            //   - Have a job name

                            if (line.trim().startsWith(">")) {
                                continue;
                            }

                            Matcher webMatcher = WEB_LINK_REGEX.mPattern.matcher(line);
                            ContinuousIntegrationInfo cii = containsBuild(line, ci);
                            if (checkFromPrevious && cii != null) {
                                BuildStatus status = getBuildStatus(line);
                                if (cii.mStatus == null || (status != null && !cii.mStatus.equals(status))) {
                                    cii.mStatus = status;
                                }
                                if (cii.mUrl == null && webMatcher.find()) {
                                    cii.mUrl = extractUrl(webMatcher);
                                }

                            } else if (webMatcher.find()) {
                                String url = extractUrl(webMatcher);
                                if (url == null) {
                                    continue;
                                }

                                BuildStatus status = null;
                                String name = null;
                                for (int i = 0; i <= 1; i++) {
                                    String l = i == 0 ? line : prevLine;
                                    if (status == null) {
                                        status = getBuildStatus(l);
                                    }
                                    if (name == null) {
                                        name = getJobName(l, status, url);
                                    }
                                    if (status != null && name != null) {
                                        if (!ciNames.contains(name)) {
                                            ci.add(new ContinuousIntegrationInfo(name, url, status));
                                            ciNames.add(name);
                                        } else if (cii != null && cii.mStatus.equals(BuildStatus.RUNNING)) {
                                            cii.mStatus = status;
                                        }
                                        break;
                                    }
                                }
                            } else if (line.startsWith("Building") && line.contains("target(s): ")) {
                                String l = line.substring(line.indexOf("target(s): ") + 10);
                                String[] jobs = l.trim().replaceAll("\\.", "").split(" ");
                                for (String job : jobs) {
                                    if (!job.isEmpty()) {
                                        ci.add(new ContinuousIntegrationInfo(job, null, BuildStatus.SUCCESS));
                                    }
                                }
                                checkFromPrevious = true;
                            } else if (line.matches(CI_REGEXP_1)) {
                                cii = fromRegExp1(line);
                                if (cii != null) {
                                    ci.add(cii);
                                }
                            }

                            prevLine = line;
                        }
                    }
                }
            }
        } catch (Exception ex) {
            Log.d(TAG, "Failed to obtain continuous integration", ex);
            ci.clear();
        }
        return sort(ci);
    }

    private static BuildStatus getBuildStatus(String line) {
        if (line == null || line.trim().length() == 0) {
            return null;
        }

        if (line.contains("PASS: ") || line.contains(": SUCCESS")
                || (line.contains(" succeeded (") && line.contains(" - ")) || line.contains("\u2705")
                || line.contains("Build Successful")) {
            return BuildStatus.SUCCESS;
        }
        if (line.contains("FAIL: ") || line.contains(": FAILURE") || line.contains("_FAILURE")
                || (line.contains(" failed (") && line.contains(" - ")) || line.contains("\u274C")) {
            return BuildStatus.FAILURE;
        }
        if (line.contains("SKIPPED: ") || line.contains(": ABORTED") || line.contains("RETRY_LIMIT")) {
            return BuildStatus.SKIPPED;
        }
        if (line.contains("Build Started")) {
            return BuildStatus.RUNNING;
        }
        return null;
    }

    private static String getJobName(String line, BuildStatus status, String url) throws Exception {
        if (line == null || line.trim().length() == 0) {
            return null;
        }

        String l = line.replace(url, "");
        String decodedUrl = URLDecoder.decode(url, "UTF-8");

        // Try to extract the name from the url
        int c = 0;
        for (String part : decodedUrl.split("/")) {
            c++;

            if (c <= 3 || part.length() == 0) {
                continue;
            }

            if (StringHelper.isOnlyNumeric(part)) {
                continue;
            }

            if (l.toLowerCase(Locale.US).contains(part.toLowerCase(Locale.US))) {
                // Found a valid job name
                return part;
            }
        }

        // Extract from the initial line
        int end = l.indexOf(" : ");
        if (end > 0) {
            int start = l.indexOf(" ");
            return l.substring(start == -1 ? 0 : start, end).trim();
        }

        // Extract from Url
        if (status != null) {
            List<String> parts = Arrays.asList(decodedUrl.split("/"));
            Collections.reverse(parts);
            c = 0;
            String bestPart = null;
            boolean foundOnlyNumeric = false;
            for (String part : parts) {
                c++;
                if (part.equalsIgnoreCase("console") || part.equalsIgnoreCase("build")
                        || part.equalsIgnoreCase("builds") || part.equalsIgnoreCase("job")) {
                    foundOnlyNumeric = false;
                    continue;
                }

                if (StringHelper.isOnlyNumeric(part)) {
                    foundOnlyNumeric = true;
                    continue;
                }

                if (c <= 1) {
                    foundOnlyNumeric = false;
                    continue;
                }

                // Found a valid job name
                if (foundOnlyNumeric) {
                    return part;
                }
                if (bestPart == null) {
                    bestPart = part;
                }
            }

            if (bestPart != null) {
                return bestPart;
            }
        }

        // Can't find a valid job name
        return null;
    }

    private static String extractUrl(Matcher m) {
        String url = m.group();
        if (url == null) {
            return null;
        }
        if (StringHelper.endsWithPunctuationMark(url)) {
            url = url.substring(0, url.length() - 1);
        }
        return url;
    }

    private static ContinuousIntegrationInfo containsBuild(String line, List<ContinuousIntegrationInfo> cii) {
        for (ContinuousIntegrationInfo ci : cii) {
            if (!TextUtils.isEmpty(ci.mName) && line.contains(ci.mName)) {
                return ci;
            }
        }
        return null;
    }

    private static ContinuousIntegrationInfo findContinuousIntegrationInfo(List<ContinuousIntegrationInfo> ci,
            String name) {
        for (ContinuousIntegrationInfo c : ci) {
            if (c.mName.equals(name)) {
                return c;
            }
        }
        return null;
    }

    private static List<ContinuousIntegrationInfo> sort(List<ContinuousIntegrationInfo> ci) {
        Collections.sort(ci, (c1, c2) -> c1.mName.compareTo(c2.mName));
        return ci;
    }

    private static ContinuousIntegrationInfo fromRegExp1(String line) {
        try {
            String[] split = line.split("::");
            String s = split[split.length - 1].substring(0, split[split.length - 1].indexOf(","));
            BuildStatus status = null;
            switch (s) {
            case "OK":
                status = BuildStatus.SUCCESS;
                break;
            case "IGNORE":
                status = BuildStatus.SKIPPED;
                break;
            case "WARN":
            case "ERROR":
                status = BuildStatus.FAILURE;
                break;
            }
            return new ContinuousIntegrationInfo(split[0].substring(2), null, status);
        } catch (Exception ex) {
            // Ignore
        }
        return null;
    }
}