services.plugins.atlassian.jira.jira1.client.JiraService.java Source code

Java tutorial

Introduction

Here is the source code for services.plugins.atlassian.jira.jira1.client.JiraService.java

Source

/*! LICENSE
 *
 * Copyright (c) 2015, The Agile Factory SA and/or its affiliates. All rights
 * reserved.
 *
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software
 * Foundation; version 2 of the License.
 *
 * This program 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 General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program. If not, see <http://www.gnu.org/licenses/>.
 */
package services.plugins.atlassian.jira.jira1.client;

import java.net.URI;
import java.net.URL;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.http.NameValuePair;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.message.BasicNameValuePair;

import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import models.pmo.PortfolioEntry;
import play.Logger;
import play.libs.F.Function;
import play.libs.F.Promise;
import play.libs.ws.WSClient;
import play.libs.ws.WSRequest;
import play.libs.ws.WSResponse;
import services.plugins.atlassian.jira.jira1.client.model.CreateProjectResponse;
import services.plugins.atlassian.jira.jira1.client.model.ErrorResponse;
import services.plugins.atlassian.jira.jira1.client.model.GetIssuesRequest;
import services.plugins.atlassian.jira.jira1.client.model.Issue;
import services.plugins.atlassian.jira.jira1.client.model.JiraConfig;
import services.plugins.atlassian.jira.jira1.client.model.Project;

/**
 * The Jira service for the Jira BizDock plugin.
 * 
 * @author Johann Kohler
 * 
 */
public class JiraService {
    private static Logger.ALogger log = Logger.of(JiraService.class);

    private WSClient wsClient;
    private String hostUrl;
    private String key;
    private String version;

    private static final long WS_TIMEOUT = 10000;

    private static final String API_PATH = "/rest/taf_api/{version}/api";

    private static final String TIMESTAMP_HEADER = "x-jira-bizdock-timestamp";
    private static final String AUTHENTICATION_DIGEST_HEADER = "x-jira-bizdock-auth";

    private static final String PING_ACTION = "/ping";
    private static final String CONFIG_ACTION = "/config";
    private static final String CREATE_PROJECT_ACTION = "/projects/create";
    private static final String GET_PROJECTS_ACTION = "/projects/all";
    private static final String GET_PROJECT_ACTION = "/projects/find";
    private static final String GET_NEEDS_ACTION = "/needs/find";
    private static final String GET_DEFECTS_ACTION = "/defects/find";

    /**
     * Get an instance of the Jira service.
     * 
     * @param wsClient
     *            a Web Service client instance to be used for WS calls
     * @param version
     *            the plugin version
     * @param hostUrl
     *            the Jira host URL
     * @param key
     *            the Jira plugin key
     */
    public static JiraService get(WSClient wsClient, String version, String hostUrl, String key) {

        JiraService jiraService = new JiraService();

        jiraService.wsClient = wsClient;
        jiraService.hostUrl = hostUrl;
        jiraService.key = key;
        jiraService.version = version;

        return jiraService;
    }

    /**
     * Constructor.
     */
    public JiraService() {
    }

    /**
     * Get the API URL.
     */
    private String getApiUrl() {
        return this.hostUrl + API_PATH.replace("{version}", version);
    }

    /**
     * Get the URL of an action.
     * 
     * @param action
     *            the action name
     */
    private String getActionUrl(String action) {
        return this.getApiUrl() + action;
    }

    /**
     * Get the URI of an action.
     * 
     * @param action
     *            the action name
     * @param queryParams
     *            the query params
     */
    private String getActionUri(String action, List<NameValuePair> queryParams) {
        try {
            URL url = new URL(getActionUrl(action));
            URI uri = url.toURI();

            String queryParamsString = "";
            if (queryParams != null && queryParams.size() > 0) {
                queryParamsString = "?" + URLEncodedUtils.format(queryParams, "UTF-8");
            }

            return uri.getPath() + queryParamsString;
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * Return true if the connection with Jira is OK, including the
     * authentication.
     */
    public boolean isAvailable() {

        try {
            JsonNode response = this.callGet(PING_ACTION);
            return response.get("authenticated").asBoolean();
        } catch (JiraServiceException e) {
            log.warn("Jira service not available", e);
            return false;
        }

    }

    /**
     * Get the configuration (list of status priorities and severities).
     */
    public JiraConfig getConfig() throws JiraServiceException {

        JsonNode response = this.callGet(CONFIG_ACTION);

        try {
            ObjectMapper objectMapper = new ObjectMapper();
            return objectMapper.treeToValue(response, JiraConfig.class);
        } catch (JsonProcessingException e) {
            throw new JiraServiceException(
                    "JiraService/getConfig: error when processing the reponse to JiraConfig.class", e);
        }

    }

    /**
     * Create a Jira project.
     * 
     * Return the id of the created project.
     * 
     * Return null if the project is already existing in Jira.
     * 
     * @param project
     *            the project to create
     */
    public String createProject(Project project) throws JiraServiceException {

        JsonNode content = null;

        try {
            ObjectMapper mapper = new ObjectMapper();
            mapper.setSerializationInclusion(Include.NON_NULL);
            content = mapper.valueToTree(project);
            Logger.debug("project: " + mapper.writeValueAsString(project));
        } catch (JsonProcessingException e) {
            throw new JiraServiceException(
                    "JiraService/createProject: error when processing the project to a Json node", e);
        }

        JsonNode response = this.callPost(CREATE_PROJECT_ACTION, content);

        CreateProjectResponse createProjectResponse = null;
        try {
            ObjectMapper objectMapper = new ObjectMapper();
            createProjectResponse = objectMapper.treeToValue(response, CreateProjectResponse.class);
        } catch (JsonProcessingException e) {
            throw new JiraServiceException(
                    "JiraService/createProject: error when processing the response to CreateProjectResponse.class",
                    e);
        }

        if (createProjectResponse.success) {
            return createProjectResponse.projectRefId;
        } else {
            if (createProjectResponse.alreadyExists) {
                return null;
            } else {
                throw new JiraServiceException(
                        "JiraService/createProject: the response is a 200 with an unknown error.");
            }
        }

    }

    /**
     * Get all Jira projects.
     */
    public List<Project> getProjects() throws JiraServiceException {

        JsonNode response = this.callGet(GET_PROJECTS_ACTION);

        List<Project> projects = new ArrayList<>();
        try {
            ObjectMapper objectMapper = new ObjectMapper();

            for (final JsonNode projectNode : response) {
                projects.add(objectMapper.treeToValue(projectNode, Project.class));
            }

            return projects;

        } catch (JsonProcessingException e) {
            throw new JiraServiceException(
                    "JiraService/getProjects: error when processing the response to Project.class", e);
        }

    }

    /**
     * Get a Jira project by id.
     * 
     * @param projectRefId
     *            the Jira project id
     */
    public Project getProject(String projectRefId) throws JiraServiceException {

        List<NameValuePair> queryParams = new ArrayList<NameValuePair>();
        queryParams.add(new BasicNameValuePair("projectRefId", projectRefId));

        JsonNode response = this.callGet(GET_PROJECT_ACTION, queryParams);

        try {
            ObjectMapper objectMapper = new ObjectMapper();
            return objectMapper.treeToValue(response, Project.class);
        } catch (JsonProcessingException e) {
            throw new JiraServiceException(
                    "JiraService/getProject: error when processing the response to Project.class", e);
        }

    }

    /**
     * Get the needs of a project.
     * 
     * @param projectRefId
     *            the Jira project id
     * @param portfolioEntry
     *            the portfolio entry
     */
    public List<Issue> getNeeds(String projectRefId, PortfolioEntry portfolioEntry) throws JiraServiceException {
        return getIssues(false, projectRefId, portfolioEntry);
    }

    /**
     * Get the defects of a project.
     * 
     * @param projectRefId
     *            the Jira project id
     * @param portfolioEntry
     *            the portfolio entry
     */
    public List<Issue> getDefects(String projectRefId, PortfolioEntry portfolioEntry) throws JiraServiceException {
        return getIssues(true, projectRefId, portfolioEntry);
    }

    /**
     * Get the issues of a project.
     * 
     * @param isDefect
     *            set to true to get the defects, else the needs.
     * @param projectRefId
     *            the Jira project id
     * @param portfolioEntry
     *            the portfolio entry
     */
    private List<Issue> getIssues(boolean isDefect, String projectRefId, PortfolioEntry portfolioEntry)
            throws JiraServiceException {

        JsonNode content = null;

        GetIssuesRequest getIssuesRequest = new GetIssuesRequest(projectRefId, portfolioEntry);
        try {
            ObjectMapper mapper = new ObjectMapper();
            content = mapper.valueToTree(getIssuesRequest);
            Logger.debug("issueRequest: " + mapper.writeValueAsString(getIssuesRequest));
        } catch (JsonProcessingException e) {
            throw new JiraServiceException(
                    "JiraService/getIssues: error when processing the getIssuesRequest to a Json node", e);
        }

        String action = null;
        if (isDefect) {
            action = GET_DEFECTS_ACTION;
        } else {
            action = GET_NEEDS_ACTION;
        }

        JsonNode response = this.callPost(action, content);

        List<Issue> issues = new ArrayList<>();
        try {
            ObjectMapper objectMapper = new ObjectMapper();

            for (final JsonNode issueNode : response) {
                issues.add(objectMapper.treeToValue(issueNode, Issue.class));
            }

            return issues;

        } catch (JsonProcessingException e) {
            throw new JiraServiceException(
                    "JiraService/getIssues: error when processing the response to Issue.class", e);
        }
    }

    /**
     * Perform a call with GET method.
     * 
     * @param action
     *            the action name
     */
    private JsonNode callGet(String action) throws JiraServiceException {
        return callGet(action, new ArrayList<NameValuePair>());
    }

    /**
     * Perform a call with GET method.
     * 
     * @param action
     *            the action name
     * @param queryParams
     *            the query parameters
     */
    private JsonNode callGet(String action, List<NameValuePair> queryParams) throws JiraServiceException {
        return call(HttpMethod.GET, action, queryParams, null);
    }

    /**
     * Perform a call with POST method.
     * 
     * @param action
     *            the action name
     * @param content
     *            the request content
     */
    private JsonNode callPost(String action, JsonNode content) throws JiraServiceException {
        return callPost(action, new ArrayList<NameValuePair>(), content);
    }

    /**
     * Perform a call with POST method.
     * 
     * @param action
     *            the action name
     * @param queryParams
     *            the query parameters
     * @param content
     *            the request content
     */
    private JsonNode callPost(String action, List<NameValuePair> queryParams, JsonNode content)
            throws JiraServiceException {
        return call(HttpMethod.POST, action, queryParams, content);
    }

    /**
     * Perform a call.
     * 
     * @param httpMethod
     *            the HTTP method (GET, POST...)
     * @param action
     *            the action name
     * @param queryParams
     *            the query parameters
     * @param content
     *            the request content (for POST)
     */
    private JsonNode call(HttpMethod httpMethod, String action, List<NameValuePair> queryParams, JsonNode content)
            throws JiraServiceException {

        Date timestamp = new Date();

        Logger.debug("URL: " + this.getActionUrl(action));
        Logger.debug("URI: " + this.getActionUri(action, queryParams));
        Logger.debug(TIMESTAMP_HEADER + ": " + String.valueOf(timestamp.getTime()));
        Logger.debug(AUTHENTICATION_DIGEST_HEADER + ": "
                + getAuthenticationDigest(timestamp.getTime(), this.getActionUri(action, queryParams)));

        WSRequest request = getWsClient().url(this.getActionUrl(action));
        request.setHeader(TIMESTAMP_HEADER, String.valueOf(timestamp.getTime()));
        request.setHeader(AUTHENTICATION_DIGEST_HEADER,
                getAuthenticationDigest(timestamp.getTime(), this.getActionUri(action, queryParams)));

        for (NameValuePair param : queryParams) {
            request = request.setQueryParameter(param.getName(), param.getValue());
        }

        Promise<WSResponse> reponse = null;

        switch (httpMethod) {
        case GET:
            reponse = request.get();
            break;
        case POST:
            reponse = request.post(content);
            break;
        }

        Promise<Pair<Integer, JsonNode>> jsonPromise = reponse
                .map(new Function<WSResponse, Pair<Integer, JsonNode>>() {
                    public Pair<Integer, JsonNode> apply(WSResponse response) {
                        if (log.isDebugEnabled()) {
                            log.debug(response.getBody());
                        }
                        return Pair.of(response.getStatus(), response.asJson());
                    }
                });

        Pair<Integer, JsonNode> response = jsonPromise.get(WS_TIMEOUT);

        Logger.debug("STATUS CODE: " + response.getLeft());

        if (response.getLeft().equals(200)) {
            return response.getRight();
        } else if (response.getLeft().equals(400)) {
            try {
                ObjectMapper objectMapper = new ObjectMapper();
                ErrorResponse errorResponse = objectMapper.treeToValue(response.getRight(), ErrorResponse.class);
                Logger.error("API Message: " + errorResponse.message + " / API code: " + errorResponse.code);
                Logger.debug(errorResponse.trace);
                throw new JiraServiceException("JiraService: the call for the action '" + action
                        + "' returns a 400, please refer above for more details.");
            } catch (JsonProcessingException e) {
                throw new JiraServiceException("JiraService: the call for the action '" + action
                        + "' returns a 400 but an error occurred when processing the response to ErrorResponse.class",
                        e);
            }

        } else {
            throw new JiraServiceException(
                    "JiraService: the call for the action '" + action + "' returns a " + response.getLeft());
        }

    }

    /**
     * Get the authentication digest for a request.
     * 
     * The authentication is based on a hash:<br/>
     * Base64(SHA256([secret key]+"#" + requestUri + "#" + timestamp))
     * 
     * @param timestamp
     *            the timestamp
     * @param requestUri
     *            the request URI
     */
    private String getAuthenticationDigest(long timestamp, String requestUri) throws JiraServiceException {
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            String clearHash = this.key + "#" + requestUri + "#" + timestamp;
            return new String(Base64.encodeBase64URLSafe(digest.digest(clearHash.getBytes())));
        } catch (NoSuchAlgorithmException e) {
            throw new JiraServiceException(
                    "JiraService/getAuthenticationDigest: error when creating the authentication digest", e);
        }
    }

    /**
     * The Jira service exception.
     * 
     * @author Johann Kohler
     * 
     */
    public static class JiraServiceException extends Exception {

        private static final long serialVersionUID = 1L;

        /**
         * Default constructor.
         * 
         * @param message
         *            the exception message
         */
        public JiraServiceException(String message) {
            super(message);
        }

        /**
         * Default constructor including the initial exception.
         * 
         * @param message
         *            the exception message
         * @param e
         *            the exception
         */
        public JiraServiceException(String message, Exception e) {
            super(message, e);
        }

    }

    /**
     * The possible HTTP method.
     * 
     * @author Johann Kohler
     * 
     */
    private static enum HttpMethod {
        GET, POST;
    }

    private WSClient getWsClient() {
        return wsClient;
    }

}