com.wso2.code.quality.matrices.ChangesFinder.java Source code

Java tutorial

Introduction

Here is the source code for com.wso2.code.quality.matrices.ChangesFinder.java

Source

/*
 * Copyright (c) 2017, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
 *
 * WSO2 Inc. licenses this file to you 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.wso2.code.quality.matrices;

import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;
import org.json.JSONArray;
import org.json.JSONObject;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.stream.IntStream;

/**
 * This class is used for getting the blame information on relevant lines changed from the given patch
 *
 * @since 1.0.0
 */

public class ChangesFinder {

    private String urlForObtainingCommits;
    protected ArrayList<String> fileNames = new ArrayList<String>();
    protected ArrayList<String> patchString = new ArrayList<>();
    protected List<ArrayList<String>> lineRangesChanged = new ArrayList<ArrayList<String>>(); // for saving the line no that are changed
    JSONObject graphqlApiJsonObject = new JSONObject();
    Map<String, Set<String>> commitHashesMapOfTheParent;
    Set<String> authorNames = new HashSet<String>(); //as the authors are for all the commits that exists in the relevant patch
    protected Set<String> commitHashObtainedForPRReview = new HashSet<String>(); //  relevant commits in old file that need to find the PR Reviewer
    private String repoLocation[];
    GraphQlApiCaller graphQlApiCaller = new GraphQlApiCaller();

    private static final Logger logger = Logger.getLogger(ChangesFinder.class);
    // constants for accessing the Github API responses
    private static final String GITHUB_SEARCH_API_ITEMS_KEY_STRING = "items";
    private static final String GITHUB_SEARCH_API_REPOSITORY_KEY_STRING = "repository";
    private static final String GITHUB_SEARCH_API_FULL_NAME_OF_REPOSITORY_KEY_STRING = "full_name";
    private static final String GITHUB_GRAPHQL_API_STARTING_LINE_KEY_STRING = "startingLine";
    private static final String GITHUB_GRAPHQL_API_ENDING_LINE_KEY_STRING = "endingLine";
    private static final String GITHUB_GRAPHQL_API_AGE_KEY_STRING = "age";
    private static final String GITHUB_GRAPHQL_API_DATA_KEY_STRING = "data";
    private static final String GITHUB_GRAPHQL_API_REPOSITORY_KEY_STRING = "repository";
    private static final String GITHUB_GRAPHQL_API_OBJECT_KEY_STRING = "object";
    private static final String GITHUB_GRAPHQL_API_BLAME_KEY_STRING = "blame";
    private static final String GITHUB_GRAPHQL_API_RANGES_KEY_STRING = "ranges";
    private static final String GITHUB_GRAPHQL_API_COMMIT_KEY_STRING = "commit";
    private static final String GITHUB_GRAPHQL_API_AUTHOR_KEY_STRING = "author";
    private static final String GITHUB_GRAPHQL_API_NAME_KEY_STRING = "name";
    private static final String GITHUB_GRAPHQL_API_URL_KEY_STRING = "url";
    private static final String GITHUB_GRAPHQL_API_HISTORY_KEY_STRING = "history";
    private static final String GITHUB_GRAPHQL_API_EDGE_KEY_STRING = "edges";
    private static final String GITHUB_GRAPHQL_API_NODE_KEY_STRING = "node";

    public String getUrlForSearchingCommits() {
        return urlForObtainingCommits;
    }

    public void setUrlForSearchingCommits(String commitHash) {
        this.urlForObtainingCommits = "https://api.github.com/search/commits?q=hash%3A" + commitHash;
    }

    /**
     * This method is used for obtaining the repositories that contain the relevant commits belongs to the given patch
     *
     * @param gitHubToken            Github token
     * @param commitsInTheGivenPatch Commits that belongs to the given patch
     * @param restApiCaller          Instance of the RestApiCaller class for accessing the REST APIs
     * @return a Set <String> containing the commit hashes that needs to be checked for reviewers
     */
    public Set obtainRepoNamesForCommitHashes(String gitHubToken, String[] commitsInTheGivenPatch,
            RestApiCaller restApiCaller) {

        //calling the API calling method
        IntStream.range(0, commitsInTheGivenPatch.length).mapToObj(i -> commitsInTheGivenPatch[i])
                .forEach(commitHash -> {
                    setUrlForSearchingCommits(commitHash);
                    JSONObject jsonObject = null;
                    try {
                        jsonObject = (JSONObject) restApiCaller.callApi(getUrlForSearchingCommits(), gitHubToken,
                                true, false);
                    } catch (CodeQualityMatricesException e) {
                        logger.error(e.getMessage(), e.getCause());
                        System.exit(1);
                    }

                    saveRepoNamesInAnArray(jsonObject, commitHash, gitHubToken);
                });
        return commitHashObtainedForPRReview;
    }

    /**
     * saving the  Repo Names in the array and calling to Get files content
     *
     * @param rootJsonObject JSON object containing the repositories which are having the current selected commit from the given patch
     * @param commitHash     the current selected commit hash
     * @param gitHubToken    github token for accessing the github REST API
     */

    public void saveRepoNamesInAnArray(JSONObject rootJsonObject, String commitHash, String gitHubToken) {
        JSONArray jsonArrayOfItems = (JSONArray) rootJsonObject.get(GITHUB_SEARCH_API_ITEMS_KEY_STRING);
        // setting the size of the repoLocationArray
        repoLocation = new String[jsonArrayOfItems.length()];
        //adding the repo name to the array
        IntStream.range(0, jsonArrayOfItems.length()).forEach(i -> {
            JSONObject jsonObject = (JSONObject) jsonArrayOfItems.get(i);
            JSONObject repositoryJsonObject = (JSONObject) jsonObject.get(GITHUB_SEARCH_API_REPOSITORY_KEY_STRING);
            repoLocation[i] = (String) repositoryJsonObject
                    .get(GITHUB_SEARCH_API_FULL_NAME_OF_REPOSITORY_KEY_STRING);
        });
        logger.info("Repo names having the given commit are successfully saved in an array");

        SdkGitHubClient sdkGitHubClient = new SdkGitHubClient(gitHubToken);

        //        for running through the repoName Array
        IntStream.range(0, repoLocation.length).filter(i -> StringUtils.contains(repoLocation[i], "wso2/"))
                .forEach(i -> {
                    //clearing all the data in the current fileNames and lineRangesChanged arraylists for each repository
                    //authorNames.clear();
                    fileNames.clear();
                    lineRangesChanged.clear();
                    patchString.clear();
                    Map<String, ArrayList<String>> mapWithFileNamesAndPatch = null;
                    try {
                        mapWithFileNamesAndPatch = sdkGitHubClient.getFilesChanged(repoLocation[i], commitHash);
                    } catch (CodeQualityMatricesException e) {
                        logger.error(e.getMessage(), e.getCause()); // as exceptions cannot be thrown inside a lambda expression
                        System.exit(2);
                    }
                    fileNames = mapWithFileNamesAndPatch.get("fileNames");
                    patchString = mapWithFileNamesAndPatch.get("patchString");
                    saveRelaventEditLineNumbers(fileNames, patchString);
                    try {
                        iterateOverFileChanges(repoLocation[i], commitHash, gitHubToken);
                    } catch (Exception e) {
                        logger.error(e.getMessage(), e.getCause()); // as exceptions cannot be thrown inside a lambda expression
                        System.exit(3);
                    }
                });

        // for printing the author names and commit hashes for a certain commit.
        System.out.println(authorNames);
        System.out.println(commitHashObtainedForPRReview);
    }

    /**
     * This method is used to save the line ranges being modified in a given file to a list and add that list to the root list of
     *
     * @param fileNames   Arraylist of files names that are being affected by the relevant commit
     * @param patchString Array list having the patch string value for each of the file being changed
     */

    public void saveRelaventEditLineNumbers(ArrayList<String> fileNames, ArrayList<String> patchString) {
        //filtering only the line ranges that are modified and saving to a string array

        // cannot ues parallel streams here as the order of the line changes must be preserved
        patchString.stream().map(patch -> StringUtils.substringsBetween(patch, "@@ ", " @@"))
                .forEach(lineChanges -> {
                    //filtering the lines ranges that existed in the previous file, that exists in the new file and saving them in to the same array
                    IntStream.range(0, lineChanges.length).forEach(j -> {
                        //@@ -22,7 +22,7 @@ => -22,7 +22,7 => 22,28/22,28
                        String tempString = lineChanges[j];
                        String lineRangeInTheOldFileBeingModified = StringUtils.substringBetween(tempString, "-",
                                " +"); // for taking the authors and commit hashes of the previous lines
                        String lineRangeInTheNewFileResultedFromModification = StringUtils
                                .substringAfter(tempString, "+"); // for taking the parent commit

                        int intialLineNoInOldFile = Integer
                                .parseInt(StringUtils.substringBefore(lineRangeInTheOldFileBeingModified, ","));
                        int tempEndLineNoInOldFile = Integer
                                .parseInt(StringUtils.substringAfter(lineRangeInTheOldFileBeingModified, ","));
                        int endLineNoOfOldFile;
                        if (intialLineNoInOldFile != 0) {
                            // to filterout the newly created files
                            endLineNoOfOldFile = intialLineNoInOldFile + (tempEndLineNoInOldFile - 1);
                        } else {
                            endLineNoOfOldFile = tempEndLineNoInOldFile;
                        }
                        int intialLineNoInNewFile = Integer.parseInt(
                                StringUtils.substringBefore(lineRangeInTheNewFileResultedFromModification, ","));
                        int tempEndLineNoInNewFile = Integer.parseInt(
                                StringUtils.substringAfter(lineRangeInTheNewFileResultedFromModification, ","));
                        int endLineNoOfNewFile = intialLineNoInNewFile + (tempEndLineNoInNewFile - 1);
                        // storing the line ranges that are being modified in the same array by replacing values
                        lineChanges[j] = intialLineNoInOldFile + "," + endLineNoOfOldFile + "/"
                                + intialLineNoInNewFile + "," + endLineNoOfNewFile;
                    });
                    ArrayList<String> tempArrayList = new ArrayList<>(Arrays.asList(lineChanges));
                    //adding to the array list which keep track of the line ranges being changed
                    lineRangesChanged.add(tempArrayList);
                });
        System.out.println("done saving file names and their relevant modification line ranges");
        System.out.println(fileNames);
        System.out.println(lineRangesChanged + "\n");
    }

    /**
     * This method will iterate over the saved filenames and their relevant changed line ranges and calls the github graphQL API
     * for getting blame details for each of the files
     *
     * @param repoLocation current selected repository
     * @param commitHash   current selected repository
     * @param gitHubToken  github token for accessing github GraphQL API
     */
    public void iterateOverFileChanges(String repoLocation, String commitHash, String gitHubToken) {

        // filtering the owner and the repository name from the repoLocation
        String owner = StringUtils.substringBefore(repoLocation, "/");
        String repositoryName = StringUtils.substringAfter(repoLocation, "/");
        //        iterating over the fileNames arraylist for the given commit
        //         cannot use parallel streams here as the order of the file names is important in the process
        fileNames.stream().forEach(fileName -> {
            int index = fileNames.indexOf(fileName);
            // the relevant arraylist of changed lines for that file
            ArrayList<String> arrayListOfRelevantChangedLinesOfSelectedFile = lineRangesChanged.get(index);
            commitHashesMapOfTheParent = new HashMap<>(); // for storing the parent commit hashes for all the changed line ranges of the relevant file
            graphqlApiJsonObject.put("query", "{repository(owner:\"" + owner + "\",name:\"" + repositoryName
                    + "\"){object(expression:\"" + commitHash + "\"){ ... on Commit{blame(path:\"" + fileName
                    + "\"){ranges{startingLine endingLine age commit{history(first: 2) { edges { node {  message url } } } author { name email } } } } } } } }");
            JSONObject rootJsonObject = null;
            try {
                //            calling the graphql API for getting blame information for the current file.
                rootJsonObject = (JSONObject) graphQlApiCaller.callGraphQlApi(graphqlApiJsonObject, gitHubToken);
            } catch (CodeQualityMatricesException e) {
                logger.error(e.getMessage(), e.getCause()); // as exceptions cannot be thrown inside lambda expression
                System.exit(1);
            }
            //            reading the above saved output for the current selected file name
            readBlameReceivedForAFile(rootJsonObject, arrayListOfRelevantChangedLinesOfSelectedFile, false, null);
            logger.info("Parent Commits hashes of the lines which are being fixed by the patch in file " + fileName
                    + " are saved to commitHashesMapOfTheParent map successfully ");

            iterateOverToFindAuthors(owner, repositoryName, fileName, arrayListOfRelevantChangedLinesOfSelectedFile,
                    gitHubToken);
            logger.info(
                    "Authors of the bug lines of code which are being fixed from the given patch are saved successfully to authorNames SET");
        });
    }

    /**
     * Reading the blame received for a current selected file name and insert the parent commits of the changed lines,
     * relevant authors and the relevant commits hashes to look for the reviewers of those line ranges
     *
     * @param rootJsonObject                                JSONObject containing blame information for current selected file
     * @param arrayListOfRelevantChangedLinesOfSelectedFile arraylist containing the changed line ranges of the current selected file
     * @param gettingPr                                     should be true if running this method for finding the authors of buggy lines which are being fixed from  the patch
     */
    public void readBlameReceivedForAFile(JSONObject rootJsonObject,
            ArrayList<String> arrayListOfRelevantChangedLinesOfSelectedFile, boolean gettingPr, String oldRange) {

        //running a iterator for fileName arrayList to get the location of the above saved file
        JSONObject dataJSONObject = (JSONObject) rootJsonObject.get(GITHUB_GRAPHQL_API_DATA_KEY_STRING);
        JSONObject repositoryJSONObect = (JSONObject) dataJSONObject.get(GITHUB_GRAPHQL_API_REPOSITORY_KEY_STRING);
        JSONObject objectJSONObject = (JSONObject) repositoryJSONObect.get(GITHUB_GRAPHQL_API_OBJECT_KEY_STRING);
        JSONObject blameJSONObject = (JSONObject) objectJSONObject.get(GITHUB_GRAPHQL_API_BLAME_KEY_STRING);
        JSONArray rangeJSONArray = (JSONArray) blameJSONObject.get(GITHUB_GRAPHQL_API_RANGES_KEY_STRING);

        //getting the starting line no of the range of lines that are modified from the patch
        // parallel streams are not used in here as the order of the arraylist is important in the process
        arrayListOfRelevantChangedLinesOfSelectedFile.stream().forEach(lineRanges -> {
            int startingLineNo = 0;
            int endLineNo = 0;
            String oldFileRange = StringUtils.substringBefore(lineRanges, "/");
            String newFileRange = StringUtils.substringAfter(lineRanges, "/");
            // need to skip the newly created files from taking the blame as they contain no previous commits
            if (!oldFileRange.equals("0,0")) {
                if (gettingPr && oldRange.equals(oldFileRange)) {
                    // need to consider the line range in the old file for finding authors and reviewers
                    startingLineNo = Integer.parseInt(StringUtils.substringBefore(oldFileRange, ","));
                    endLineNo = Integer.parseInt(StringUtils.substringAfter(oldFileRange, ","));
                } else if (!gettingPr && oldRange == null) {
                    // need to consider the line range in the new file resulted from applying the commit, for finding parent commits
                    startingLineNo = Integer.parseInt(StringUtils.substringBefore(newFileRange, ","));
                    endLineNo = Integer.parseInt(StringUtils.substringAfter(newFileRange, ","));
                } else {
                    return; // to skip the to the next iteration if oldRange != oldFileRange when finding authornames and commits for obtaining PRs
                }

                // as a new mapForStoringAgeAndIndex map should be available for each line range to find the most recent change
                Map<Integer, ArrayList<Integer>> mapForStoringAgeAndIndex = new HashMap<Integer, ArrayList<Integer>>();

                //checking line by line by iterating the startingLineNo
                while (endLineNo >= startingLineNo) {
                    // since the index value is required for later processing, without Java 8 features "for loop" is used for iteration
                    for (int i = 0; i < rangeJSONArray.length(); i++) {
                        JSONObject rangeJSONObject = (JSONObject) rangeJSONArray.get(i);
                        int tempStartingLineNo = (int) rangeJSONObject
                                .get(GITHUB_GRAPHQL_API_STARTING_LINE_KEY_STRING);
                        int tempEndingLineNo = (int) rangeJSONObject.get(GITHUB_GRAPHQL_API_ENDING_LINE_KEY_STRING);

                        //checking whether the line belongs to that line range group
                        if ((tempStartingLineNo <= startingLineNo) && (tempEndingLineNo >= startingLineNo)) {
                            // so the relevant startingLineNo belongs in this line range in other words in this JSONObject
                            if (!gettingPr) {
                                int age = (int) rangeJSONObject.get(GITHUB_GRAPHQL_API_AGE_KEY_STRING);
                                // storing the age field with relevant index of the JSONObject
                                mapForStoringAgeAndIndex.putIfAbsent(age, new ArrayList<Integer>());
                                if (!mapForStoringAgeAndIndex.get(age).contains(i)) {
                                    mapForStoringAgeAndIndex.get(age).add(i); // adding if the index is not present in the array list for the relevant age
                                }

                            } else {
                                //for saving the author names of commiters
                                JSONObject commitJSONObject = (JSONObject) rangeJSONObject
                                        .get(GITHUB_GRAPHQL_API_COMMIT_KEY_STRING);

                                JSONObject authorJSONObject = (JSONObject) commitJSONObject
                                        .get(GITHUB_GRAPHQL_API_AUTHOR_KEY_STRING);
                                String nameOfTheAuthor = (String) authorJSONObject
                                        .get(GITHUB_GRAPHQL_API_NAME_KEY_STRING);
                                authorNames.add(nameOfTheAuthor); // authors are added to the Set

                                String urlOfCommit = (String) commitJSONObject
                                        .get(GITHUB_GRAPHQL_API_URL_KEY_STRING);
                                String commitHashForPRReview = StringUtils.substringAfter(urlOfCommit, "commit/");
                                commitHashObtainedForPRReview.add(commitHashForPRReview);
                            }
                            break;
                        } else {
                            continue; // to skip to the next JSON Object in the rangeJSONArray
                        }
                    }
                    startingLineNo++; // to check for other line numbers
                }

                //for the above line range getting the lastest commit which modified the lines
                if (!gettingPr) {
                    //converting the map into a treeMap to get it ordered
                    TreeMap<Integer, ArrayList<Integer>> treeMap = new TreeMap<>(mapForStoringAgeAndIndex);
                    int minimumKeyOfMapForStoringAgeAndIndex = treeMap.firstKey(); // getting the minimum key
                    //getting the relevant JSONObject indexes which consists of the recent change with in the relevant line range
                    ArrayList<Integer> indexesOfJsonObjectForRecentCommit = mapForStoringAgeAndIndex
                            .get(minimumKeyOfMapForStoringAgeAndIndex);
                    // the order of the indexesOfJsonObjectForRecentCommit is not important as we only need to get the parent commit hashes
                    indexesOfJsonObjectForRecentCommit.parallelStream().forEach(index -> {
                        JSONObject rangeJSONObject = (JSONObject) rangeJSONArray.get(index);
                        JSONObject commitJSONObject = (JSONObject) rangeJSONObject
                                .get(GITHUB_GRAPHQL_API_COMMIT_KEY_STRING);
                        JSONObject historyJSONObject = (JSONObject) commitJSONObject
                                .get(GITHUB_GRAPHQL_API_HISTORY_KEY_STRING);
                        JSONArray edgesJSONArray = (JSONArray) historyJSONObject
                                .get(GITHUB_GRAPHQL_API_EDGE_KEY_STRING);
                        //getting the second json object from the array as it contain the commit of the parent which modified the above line range
                        JSONObject edgeJSONObject = (JSONObject) edgesJSONArray.get(1);
                        JSONObject nodeJSONObject = (JSONObject) edgeJSONObject
                                .get(GITHUB_GRAPHQL_API_NODE_KEY_STRING);
                        String urlOfTheParentCommit = (String) nodeJSONObject
                                .get(GITHUB_GRAPHQL_API_URL_KEY_STRING); // this contain the URL of the parent commit
                        String commitHash = (String) StringUtils.substringAfter(urlOfTheParentCommit, "commit/");
                        //                                        commitHashesOfTheParent.add(commitHash);    // commitHashesof the parent for the selected file

                        commitHashesMapOfTheParent.putIfAbsent(oldFileRange, new HashSet<String>());
                        if (!commitHashesMapOfTheParent.get(oldFileRange).contains(commitHash)) {
                            commitHashesMapOfTheParent.get(oldFileRange).add(commitHash);
                        }
                    });
                }

            }

        });
    }

    /**
     * Finding the authors of the commits
     *
     * @param owner                                         owner of the repository
     * @param repositoryName                                repository name
     * @param fileName                                      name of the file which is required to get blame details
     * @param arrayListOfRelevantChangedLinesOfSelectedFile arraylist containing the changed line ranges of the current selected file
     * @param gitHubToken                                   github token for accessing github GraphQL API
     */
    public void iterateOverToFindAuthors(String owner, String repositoryName, String fileName,
            ArrayList<String> arrayListOfRelevantChangedLinesOfSelectedFile, String gitHubToken) {

        for (Map.Entry m : commitHashesMapOfTheParent.entrySet()) {
            String oldRange = (String) m.getKey();
            Set<String> commitHashes = (Set<String>) m.getValue();
            commitHashes.parallelStream().forEach(parentCommitHashForCallingGraphQl -> {
                graphqlApiJsonObject.put("query", "{repository(owner:\"" + owner + "\",name:\"" + repositoryName
                        + "\"){object(expression:\"" + parentCommitHashForCallingGraphQl
                        + "\"){ ... on Commit{blame(path:\"" + fileName
                        + "\"){ranges{startingLine endingLine age commit{ url author { name email } } } } } } } }");
                JSONObject rootJsonObject = null;
                try {
                    rootJsonObject = (JSONObject) graphQlApiCaller.callGraphQlApi(graphqlApiJsonObject,
                            gitHubToken);
                    readBlameReceivedForAFile(rootJsonObject, arrayListOfRelevantChangedLinesOfSelectedFile, true,
                            oldRange);
                } catch (CodeQualityMatricesException e) {
                    logger.error(e.getMessage(), e.getCause());
                    System.exit(1);
                }
            });
        }
    }
}