org.jboss.set.aphrodite.issue.trackers.bugzilla.BugzillaClient.java Source code

Java tutorial

Introduction

Here is the source code for org.jboss.set.aphrodite.issue.trackers.bugzilla.BugzillaClient.java

Source

/*
 * JBoss, Home of Professional Open Source.
 * Copyright (c) 2016, Red Hat, Inc., and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

package org.jboss.set.aphrodite.issue.trackers.bugzilla;

import static org.jboss.set.aphrodite.issue.trackers.bugzilla.BugzillaFields.API_URL;
import static org.jboss.set.aphrodite.issue.trackers.bugzilla.BugzillaFields.COMMENT;
import static org.jboss.set.aphrodite.issue.trackers.bugzilla.BugzillaFields.COMMENT_BODY;
import static org.jboss.set.aphrodite.issue.trackers.bugzilla.BugzillaFields.COMMENT_FIELDS;
import static org.jboss.set.aphrodite.issue.trackers.bugzilla.BugzillaFields.COMMENT_ID;
import static org.jboss.set.aphrodite.issue.trackers.bugzilla.BugzillaFields.COMMENT_IS_PRIVATE;
import static org.jboss.set.aphrodite.issue.trackers.bugzilla.BugzillaFields.ESTIMATED_TIME;
import static org.jboss.set.aphrodite.issue.trackers.bugzilla.BugzillaFields.FILTER_SHARER_ID;
import static org.jboss.set.aphrodite.issue.trackers.bugzilla.BugzillaFields.ID;
import static org.jboss.set.aphrodite.issue.trackers.bugzilla.BugzillaFields.ISSUE_IDS;
import static org.jboss.set.aphrodite.issue.trackers.bugzilla.BugzillaFields.LOGIN;
import static org.jboss.set.aphrodite.issue.trackers.bugzilla.BugzillaFields.METHOD_ADD_COMMENT;
import static org.jboss.set.aphrodite.issue.trackers.bugzilla.BugzillaFields.METHOD_FILTER_SEARCH;
import static org.jboss.set.aphrodite.issue.trackers.bugzilla.BugzillaFields.METHOD_GET_BUG;
import static org.jboss.set.aphrodite.issue.trackers.bugzilla.BugzillaFields.METHOD_GET_COMMENT;
import static org.jboss.set.aphrodite.issue.trackers.bugzilla.BugzillaFields.METHOD_SEARCH;
import static org.jboss.set.aphrodite.issue.trackers.bugzilla.BugzillaFields.METHOD_UPDATE_BUG;
import static org.jboss.set.aphrodite.issue.trackers.bugzilla.BugzillaFields.METHOD_USER_LOGIN;
import static org.jboss.set.aphrodite.issue.trackers.bugzilla.BugzillaFields.NAME;
import static org.jboss.set.aphrodite.issue.trackers.bugzilla.BugzillaFields.PASSWORD;
import static org.jboss.set.aphrodite.issue.trackers.bugzilla.BugzillaFields.PRIVATE_COMMENT;
import static org.jboss.set.aphrodite.issue.trackers.bugzilla.BugzillaFields.RESULT_BUGS;
import static org.jboss.set.aphrodite.issue.trackers.bugzilla.BugzillaFields.RESULT_FIELDS;
import static org.jboss.set.aphrodite.issue.trackers.bugzilla.BugzillaFields.RESULT_INCLUDE_FIELDS;
import static org.jboss.set.aphrodite.issue.trackers.bugzilla.BugzillaFields.RESULT_PERMISSIVE_SEARCH;
import static org.jboss.set.aphrodite.issue.trackers.bugzilla.BugzillaFields.STATUS;
import static org.jboss.set.aphrodite.issue.trackers.bugzilla.BugzillaFields.TARGET_RELEASE;
import static org.jboss.set.aphrodite.issue.trackers.bugzilla.BugzillaFields.UPDATE_FIELDS;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.xmlrpc.XmlRpcException;
import org.apache.xmlrpc.client.XmlRpcClient;
import org.apache.xmlrpc.client.XmlRpcClientConfig;
import org.apache.xmlrpc.client.XmlRpcClientConfigImpl;
import org.jboss.set.aphrodite.common.Utils;
import org.jboss.set.aphrodite.domain.Comment;
import org.jboss.set.aphrodite.domain.FlagStatus;
import org.jboss.set.aphrodite.domain.Issue;
import org.jboss.set.aphrodite.domain.IssueStatus;
import org.jboss.set.aphrodite.domain.SearchCriteria;
import org.jboss.set.aphrodite.spi.AphroditeException;
import org.jboss.set.aphrodite.spi.NotFoundException;

/**
 * @author Ryan Emerson
 */
public class BugzillaClient {

    private static final Log LOG = LogFactory.getLog(BugzillaClient.class);

    static final Pattern ID_PARAM_PATTERN = Pattern.compile("id=([^&]+)");
    static final Pattern FILTER_NAME_PARAM_PATTERN = Pattern.compile("namedcmd=([^&]+)");
    static final Pattern SHARER_ID_PARAM_PATTERN = Pattern.compile("sharer_id=([^&]+)");

    private final ExecutorService executorService;
    private final IssueWrapper WRAPPER = new IssueWrapper();
    private final URL baseURL;
    private final Map<String, Object> loginDetails;

    public BugzillaClient(URL baseURL, String login, String password, ExecutorService executorService)
            throws IllegalStateException {
        this.executorService = executorService;
        this.baseURL = baseURL;

        Map<String, String> params = new HashMap<>();
        if (login != null)
            params.put(LOGIN, login);
        if (password != null)
            params.put(PASSWORD, password);
        loginDetails = Collections.unmodifiableMap(params);

        // Check that the provided login details are correct - Fail fast.
        runCommand(METHOD_USER_LOGIN, params);
    }

    public Issue getIssue(String trackerId) throws NotFoundException {
        Map<String, Object> params = new HashMap<>(loginDetails);
        params.put(RESULT_INCLUDE_FIELDS, RESULT_FIELDS);
        params.put(ISSUE_IDS, trackerId);
        params.put(RESULT_PERMISSIVE_SEARCH, true);

        Map<String, ?> resultMap = executeRequest(XMLRPC.RPC_STRUCT, METHOD_GET_BUG, params);
        Object[] bugs = (Object[]) resultMap.get(RESULT_BUGS);
        if (bugs.length == 1) {
            @SuppressWarnings("unchecked")
            Map<String, Object> results = (Map<String, Object>) bugs[0];
            return WRAPPER.bugzillaBugToIssue(results, baseURL);
        } else {
            Utils.logWarnMessage(LOG, "Zero or more than one bug found with id: " + trackerId);
        }
        throw new NotFoundException("No issues found with id: " + trackerId);
    }

    public List<Issue> getIssues(Collection<URL> urls) {
        List<String> ids = new ArrayList<>();
        for (URL url : urls) {
            try {
                ids.add(Utils.getParamaterFromUrl(ID_PARAM_PATTERN, url));
            } catch (NotFoundException e) {
                if (LOG.isWarnEnabled())
                    LOG.warn("Unable to extract trackerId from: " + url);
            }
        }

        Map<String, Object> params = new HashMap<>(loginDetails);
        params.put(RESULT_INCLUDE_FIELDS, RESULT_FIELDS);
        params.put(ISSUE_IDS, ids.toArray());
        params.put(RESULT_PERMISSIVE_SEARCH, true);

        Map<String, ?> resultMap = executeRequest(XMLRPC.RPC_STRUCT, METHOD_GET_BUG, params);
        Object[] bugs = (Object[]) resultMap.get(RESULT_BUGS);

        List<Issue> issues = new ArrayList<>();
        for (Object bugObject : bugs) {
            @SuppressWarnings("unchecked")
            Map<String, Object> bug = (Map<String, Object>) bugObject;
            issues.add(WRAPPER.bugzillaBugToIssue(bug, baseURL));
        }
        return issues;
    }

    public Issue getIssueWithComments(URL url) throws NotFoundException {
        String trackerId = Utils.getParamaterFromUrl(ID_PARAM_PATTERN, url);
        return getIssueWithComments(trackerId);
    }

    public Issue getIssueWithComments(String trackerId) throws NotFoundException {
        Issue issue = getIssue(trackerId);
        setCommentsForIssue(issue);
        return issue;
    }

    private void setCommentsForIssue(Issue issue) {
        try {
            issue.setComments(getCommentsForIssue(issue));
        } catch (NotFoundException e) {
            Utils.logException(LOG, "Unable to retrieve comments for issue: ", e);
        }
    }

    public List<Comment> getCommentsForIssue(Issue issue) throws NotFoundException {
        if (issue == null)
            throw new IllegalArgumentException("The provided issue cannot be null.");

        if (issue.getTrackerId().isPresent())
            return getCommentsForIssue(issue.getTrackerId().get());

        return getCommentsForIssue(Utils.getParamaterFromUrl(ID_PARAM_PATTERN, issue.getURL()));
    }

    public Map<String, List<Comment>> getCommentsForIssues(Map<String, Issue> issues) {
        if (issues == null || issues.isEmpty()) {
            Collections.emptyMap();
        }

        Map<String, Object> params = new HashMap<>(loginDetails);
        params.put(ISSUE_IDS, extractIssueIdsList(issues.values()));
        params.put(RESULT_INCLUDE_FIELDS, COMMENT_FIELDS);

        return buildMapOfCommentsIndexedByBugId(executeRequest(XMLRPC.RPC_STRUCT, METHOD_GET_COMMENT, params));
    }

    private Map<String, List<Comment>> buildMapOfCommentsIndexedByBugId(Map<String, Object> results) {
        Map<String, List<Comment>> commentsMap = new HashMap<>();
        if (results != null && !results.isEmpty() && results.containsKey(RESULT_BUGS)) {
            for (Map<String, Object> comments : XMLRPC.iterable(XMLRPC.RPC_STRUCT, results.values())) {
                for (Entry<String, Object> comment : comments.entrySet()) {
                    final String bugId = comment.getKey();
                    commentsMap.put(bugId, buildCommentsForBug(XMLRPC.cast(XMLRPC.RPC_ARRAY,
                            XMLRPC.cast(XMLRPC.RPC_STRUCT, comment.getValue()).get("comments"))));
                }
            }
        }
        return commentsMap;
    }

    private List<Comment> buildCommentsForBug(final Object[] commentObjArray) {
        List<Comment> comments = new ArrayList<>(commentObjArray.length);
        for (Object o : commentObjArray) {
            comments.add(buildComment(XMLRPC.cast(XMLRPC.RPC_STRUCT, o)));
        }
        return comments;
    }

    private Comment buildComment(Map<String, Object> comment) {
        String id = String.valueOf(comment.get(COMMENT_ID));
        String body = (String) comment.get(COMMENT_BODY);
        boolean isPrivate = (Boolean) comment.get(COMMENT_IS_PRIVATE);
        return new Comment(id, body, isPrivate);
    }

    private Object[] extractIssueIdsList(Collection<Issue> collection) {
        return collection.stream().filter(Objects::nonNull).map(issue -> issue.getTrackerId().get())
                .collect(Collectors.toList()).toArray();
    }

    public List<Comment> getCommentsForIssue(String trackerId) {
        Map<String, Object> params = new HashMap<>(loginDetails);
        params.put(ISSUE_IDS, trackerId);
        params.put(RESULT_INCLUDE_FIELDS, COMMENT_FIELDS);
        Map<String, ?> results = executeRequest(XMLRPC.RPC_STRUCT, METHOD_GET_COMMENT, params);

        if (results != null && !results.isEmpty() && results.containsKey(RESULT_BUGS)) {
            Map<String, Object> issues = XMLRPC.cast(XMLRPC.RPC_STRUCT, results.get(RESULT_BUGS));
            return getCommentList(XMLRPC.cast(XMLRPC.RPC_STRUCT, issues.get(trackerId)));
        }
        return new ArrayList<>();
    }

    public List<Issue> searchIssuesByFilter(URL filterUrl) throws NotFoundException {
        String filterName = Utils.getParamaterFromUrl(FILTER_NAME_PARAM_PATTERN, filterUrl);
        int sharerId = Integer.parseInt(Utils.getParamaterFromUrl(SHARER_ID_PARAM_PATTERN, filterUrl));
        Map<String, Object> queryMap = new HashMap<>(loginDetails);
        queryMap.put(METHOD_FILTER_SEARCH, filterName);
        queryMap.put(FILTER_SHARER_ID, sharerId);
        queryMap.put(RESULT_INCLUDE_FIELDS, RESULT_FIELDS);

        try {
            return searchIssues(queryMap);
        } catch (RuntimeException e) {
            throw new NotFoundException("Unable to retrieve issues associated with filter url: " + filterUrl, e);
        }
    }

    public List<Issue> searchIssues(SearchCriteria criteria) {
        return searchIssues(criteria, -1);
    }

    public List<Issue> searchIssues(SearchCriteria criteria, int defaultIssueLimit) {
        Map<String, Object> queryMap = new BugzillaQueryBuilder(criteria, loginDetails, defaultIssueLimit)
                .getQueryMap();
        if (queryMap == null)
            return new ArrayList<>();
        return searchIssues(queryMap);
    }

    private List<Issue> searchIssues(Map<String, Object> queryMap) {
        List<Issue> issueList = new ArrayList<>(0);
        Map<String, ?> resultMap = executeRequest(XMLRPC.RPC_STRUCT, METHOD_SEARCH, queryMap);
        if (resultMap != null && !resultMap.isEmpty()) {
            Map<String, Issue> issues = fetchAllIssues(XMLRPC.cast(XMLRPC.RPC_ARRAY, resultMap.get(RESULT_BUGS)));
            Map<String, List<Comment>> comments = getCommentsForIssues(issues);

            issueList = issues.keySet().stream().filter(Objects::nonNull)
                    .map(id -> associateCommentsToIssue(issues.get(id), comments)).collect(Collectors.toList());
        }
        return issueList;
    }

    private Issue associateCommentsToIssue(Issue issue, Map<String, List<Comment>> comments) {
        issue.setComments(comments.get(issue.getTrackerId().get()));
        return issue;
    }

    private Map<String, Issue> fetchAllIssues(final Object[] bugs) {
        Map<String, Issue> issues = new HashMap<>();
        for (Map<String, Object> struct : XMLRPC.iterable(XMLRPC.RPC_STRUCT, bugs)) {
            Issue issue = WRAPPER.bugzillaBugToIssue(struct, baseURL);
            issues.put(issue.getTrackerId().get(), issue);
        }
        return issues;
    }

    public boolean updateIssue(Issue issue) throws AphroditeException {
        Map<String, Object> params = WRAPPER.issueToBugzillaBug(issue, loginDetails);
        return runCommand(METHOD_UPDATE_BUG, params);
    }

    public boolean updateTargetRelease(int id, final String... targetRelease) {
        return updateField(id, TARGET_RELEASE, targetRelease);
    }

    public boolean updateStatus(int id, IssueStatus status) {
        return updateField(id, STATUS, status);
    }

    public boolean updateTargetMilestone(int id, String targetMilestone) {
        return updateField(id, TARGET_RELEASE, targetMilestone);
    }

    public boolean updateEstimate(int id, double worktime) {
        return updateField(id, ESTIMATED_TIME, worktime);
    }

    public boolean postComment(Issue issue, Comment comment) throws NotFoundException {
        String trackerId = issue.getTrackerId().orElse(Utils.getParamaterFromUrl(ID_PARAM_PATTERN, issue.getURL()));
        return postComment(new Integer(trackerId), comment.getBody(), comment.isPrivate());
    }

    public boolean postComment(int id, String comment, boolean isPrivate) {
        Map<String, Object> params = new HashMap<>(loginDetails);
        params.put(ID, id);
        params.put(COMMENT, comment);
        params.put(PRIVATE_COMMENT, isPrivate);
        return runCommand(METHOD_ADD_COMMENT, params);
    }

    public boolean postComment(Map<Issue, Comment> commentMap) {
        List<CompletableFuture<Boolean>> requests = commentMap.entrySet().stream()
                .map(entry -> CompletableFuture.supplyAsync(
                        () -> postCommentAndLogExceptions(entry.getKey(), entry.getValue()), executorService))
                .collect(Collectors.toList());

        return requests.stream().map(CompletableFuture::join).noneMatch(failed -> !failed);
    }

    public boolean postComment(Collection<Issue> issues, Comment comment) {
        List<CompletableFuture<Boolean>> requests = issues
                .stream().map(issue -> CompletableFuture
                        .supplyAsync(() -> postCommentAndLogExceptions(issue, comment), executorService))
                .collect(Collectors.toList());

        return requests.stream().map(CompletableFuture::join).noneMatch(failed -> !failed);
    }

    private boolean postCommentAndLogExceptions(Issue issue, Comment comment) {
        try {
            return postComment(issue, comment);
        } catch (NotFoundException e) {
            Utils.logException(LOG, e);
            return false;
        }
    }

    public boolean updateFlags(int ids, String name, FlagStatus status) {
        String flagStatus = status.getSymbol();
        Map<String, String> updates = new HashMap<>();
        updates.put(NAME, name);
        updates.put(STATUS, flagStatus);
        Object[] updateArray = { updates };

        Map<String, Object> params = new HashMap<>(loginDetails);
        params.put(ISSUE_IDS, ids);
        params.put(UPDATE_FIELDS, updateArray);
        params.put(RESULT_PERMISSIVE_SEARCH, true);

        return runCommand(METHOD_UPDATE_BUG, params);
    }

    private List<Comment> getCommentList(Map<String, Object> issues) {
        List<Comment> issueComments = new ArrayList<>();
        for (Object[] comments : XMLRPC.iterable(XMLRPC.RPC_ARRAY, issues.values())) {
            for (Map<String, Object> comment : XMLRPC.iterable(XMLRPC.RPC_STRUCT, comments)) {
                issueComments.add(buildComment(comment));
            }
        }
        return issueComments;
    }

    private boolean updateField(int bugzillaId, String field, Object content) {
        Map<String, Object> params = new HashMap<>(loginDetails);
        params.put(ID, bugzillaId);
        params.put(field, content);
        return runCommand(METHOD_UPDATE_BUG, params);
    }

    private <T> T executeRequest(final XMLRPC<T> type, String method, Object... params) {
        try {
            return type.cast(getRpcClient().execute(method, params));
        } catch (XmlRpcException e) {
            Utils.logException(LOG, e);
            throw new RuntimeException(e); // TODO improve exception handling
        }
    }

    private XmlRpcClient getRpcClient() {
        String apiURL = baseURL + API_URL;
        XmlRpcClient rpcClient;
        rpcClient = new XmlRpcClient();

        try {
            URL url = new URL(apiURL);
            rpcClient.setConfig(getClientConfig(url));
        } catch (MalformedURLException e) {
            Utils.logException(LOG, e);
            throw new RuntimeException(e);
        }
        return rpcClient;
    }

    private XmlRpcClientConfig getClientConfig(URL apiURL) {
        XmlRpcClientConfigImpl config;
        config = new XmlRpcClientConfigImpl();
        config.setServerURL(apiURL);
        return config;
    }

    private boolean runCommand(String method, Object... params) {
        try {
            getRpcClient().execute(method, params);
            return true;
        } catch (XmlRpcException e) {
            throw new IllegalStateException(e);
        }
    }

    // TODO is there a cleaner way to do this?
    private static class XMLRPC<T> {
        static final XMLRPC<Object[]> RPC_ARRAY = new XMLRPC<>(Object[].class);
        static final XMLRPC<Map<String, Object>> RPC_STRUCT = new XMLRPC<>(Map.class);

        final Class<T> cls;

        @SuppressWarnings("unchecked")
        XMLRPC(final Class<?> cls) {
            this.cls = (Class<T>) cls;
        }

        T cast(final Object obj) {
            return cls.cast(obj);
        }

        static <T> T cast(final XMLRPC<T> type, Object obj) {
            return type.cast(obj);
        }

        static <T> Iterable<T> iterable(final XMLRPC<T> type, final Collection<Object> c) {
            final Iterator<Object> it = c.iterator();
            return () -> new Iterator<T>() {
                @Override
                public boolean hasNext() {
                    return it.hasNext();
                }

                @Override
                public T next() {
                    return type.cast(it.next());
                }

                @Override
                public void remove() {
                    it.remove();
                }
            };
        }

        static <T> Iterable<T> iterable(final XMLRPC<T> type, final Object[] array) {
            final Iterator<Object> it = Arrays.asList(array).iterator();
            return () -> new Iterator<T>() {
                @Override
                public boolean hasNext() {
                    return it.hasNext();
                }

                @Override
                public T next() {
                    return type.cast(it.next());
                }

                @Override
                public void remove() {
                    it.remove();
                }
            };
        }
    }
}