YoutrackAPI.java 15.7 KB
package pl.itcrowd.youtrack.api;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.NameValuePair;
import org.apache.http.StatusLine;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.HttpResponseException;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.PoolingClientConnectionManager;
import org.apache.http.impl.conn.SchemeRegistryFactory;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import pl.itcrowd.youtrack.api.defaults.Fields;
import pl.itcrowd.youtrack.api.exceptions.NoResultFoundException;
import pl.itcrowd.youtrack.api.exceptions.YoutrackAPIException;
import pl.itcrowd.youtrack.api.exceptions.YoutrackErrorException;
import pl.itcrowd.youtrack.api.rest.AssigneeList;
import pl.itcrowd.youtrack.api.rest.Enumeration;
import pl.itcrowd.youtrack.api.rest.Issue;
import pl.itcrowd.youtrack.api.rest.Issues;
import pl.itcrowd.youtrack.api.rest.User;
import pl.itcrowd.youtrack.api.rest.UserRefs;

import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.namespace.QName;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static pl.itcrowd.youtrack.api.URIUtils.buildURI;

public class YoutrackAPI {

    private final static QName Enumeration_QNAME = new QName("", "enumeration");

    private final static QName Issue_QNAME = new QName("", "issue");

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

    private HttpClient httpClient;

    private String serviceLocation;

    private URI serviceLocationURI;

    private static HttpClient getDefaultHttpClient()
    {
        SSLContext sslContext;
        try {
            sslContext = SSLContext.getInstance("SSL");
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
        // set up a TrustManager that trusts everything
        try {
            sslContext.init(null, new TrustManager[]{new X509TrustManager() {
                public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException
                {
                }

                public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException
                {
                }

                public X509Certificate[] getAcceptedIssuers()
                {
                    return new X509Certificate[0];
                }
            }}, new SecureRandom());
        } catch (KeyManagementException e) {
            throw new RuntimeException(e);
        }

        SSLSocketFactory sf = new SSLSocketFactory(sslContext);
        Scheme httpsScheme = new Scheme("https", 443, sf);
        SchemeRegistry schemeRegistry = SchemeRegistryFactory.createDefault();
        schemeRegistry.register(schemeRegistry.unregister("https"));
        schemeRegistry.register(httpsScheme);

        ClientConnectionManager cm = new PoolingClientConnectionManager(schemeRegistry);
        return new DefaultHttpClient(cm);
    }

    private static boolean isBlank(String str)
    {
        int strLen;
        if (str == null || (strLen = str.length()) == 0) {
            return true;
        }
        for (int i = 0; i < strLen; i++) {
            if ((!Character.isWhitespace(str.charAt(i)))) {
                return false;
            }
        }
        return true;
    }

    public YoutrackAPI(String serviceLocation)
    {
        this(serviceLocation, null);
    }

    public YoutrackAPI(String serviceLocation, HttpClient httpClient)
    {
        if (serviceLocation == null) {
            throw new IllegalArgumentException("serviceLocation cannot be null");
        }
        this.serviceLocation = serviceLocation;
        try {
            serviceLocationURI = new URI(this.serviceLocation);
        } catch (URISyntaxException e) {
            throw new RuntimeException(e);
        }
        this.httpClient = httpClient == null ? getDefaultHttpClient() : httpClient;
    }

    public YoutrackAPI(String serviceLocation, String username, String password) throws IOException, JAXBException
    {
        this(serviceLocation, null, username, password);
    }

    public YoutrackAPI(String serviceLocation, HttpClient httpClient, String username, String password) throws IOException, JAXBException
    {
        this(serviceLocation, httpClient);
        login(username, password);
    }

    public String getServiceLocation()
    {
        return serviceLocation;
    }

    public void command(String issueId, String command) throws IOException
    {
        command(issueId, command, null, null, null, null);
    }

    public void command(String issueId, Command command) throws IOException
    {
        command(issueId, command.toString());
    }

    public void command(String issueId, String command, String comment, String group, Boolean disableNotifications, String runAs) throws IOException
    {
        final HttpPost request = new HttpPost(buildURI(serviceLocationURI, "/rest/issue/" + issueId + "/execute"));
        final List<BasicNameValuePair> parameters = new ArrayList<BasicNameValuePair>();
        parameters.add(new BasicNameValuePair("command", command));
        if (!isBlank(comment)) {
            parameters.add(new BasicNameValuePair("comment", comment));
        }
        if (!isBlank(group)) {
            parameters.add(new BasicNameValuePair("group", group));
        }
        if (disableNotifications != null) {
            parameters.add(new BasicNameValuePair("disableNotifications", disableNotifications.toString()));
        }
        if (!isBlank(runAs)) {
            parameters.add(new BasicNameValuePair("runAs", runAs));
        }
        request.setEntity(new UrlEncodedFormEntity(parameters));
        execute(request);
    }

    /**
     * Creates new issue on Youtrack.
     *
     * @param project     project to create issue in
     * @param summary     summary of the issue
     * @param description longer description of the issue
     *
     * @return issue id of created issue
     *
     * @throws IOException in case of communication problems
     */
    public String createIssue(String project, String summary, String description) throws IOException
    {
        final HttpPut request = createPutRequest(buildURI(serviceLocationURI, "/rest/issue"), new BasicNameValuePair("project", project),
            new BasicNameValuePair("summary", summary), new BasicNameValuePair("description", description));
        final HttpResponse response = httpClient.execute(request);
        final StatusLine statusLine = response.getStatusLine();
        final HttpEntity entity = response.getEntity();
        final String responseText = entity == null ? null : EntityUtils.toString(entity);
        throwExceptionsIfNeeded(statusLine, responseText);

        final Header header = response.getFirstHeader(HttpHeaders.LOCATION);
        if (header == null) {
            throw new YoutrackAPIException("Missing location header despite positive status code: " + statusLine.getStatusCode());
        }
        final String issueURL = header.getValue();
        final Matcher matcher = Pattern.compile(".*(" + project + "-\\d+)").matcher(issueURL);
        if (!matcher.find() || matcher.groupCount() < 1) {
            throw new YoutrackAPIException("Cannot extract issue id from issue URL: " + issueURL);
        }
        return matcher.group(1);
    }

    public void deleteIssue(String issueId) throws IOException
    {
        execute(new HttpDelete(buildURI(serviceLocationURI, "/rest/issue/" + issueId)));
    }

    public AssigneeList getAssignees(String project) throws IOException, JAXBException
    {
        final String responseString = execute(new HttpGet(buildURI(serviceLocationURI, "/rest/admin/project/" + project + "/assignee")));
        final Object result = YoutrackUnmarshaller.unmarshall(responseString);
        if (result instanceof AssigneeList) {
            return (AssigneeList) result;
        } else {
            throw new YoutrackAPIException("Unexpected type: " + result);
        }
    }

    public Enumeration getBundle(String customField) throws IOException, JAXBException
    {
        final Object result = YoutrackUnmarshaller.unmarshall(
            execute(new HttpGet(buildURI(serviceLocationURI, "/rest/admin/customfield/bundle/" + customField))));
        if (result instanceof Enumeration) {
            return (Enumeration) result;
        } else if (result instanceof JAXBElement) {
            final JAXBElement jaxbElement = (JAXBElement) result;
            if (Enumeration_QNAME.equals(jaxbElement.getName())) {
                return (Enumeration) ((JAXBElement) result).getValue();
            } else {
                throw new YoutrackAPIException("Unexpected type: " + jaxbElement.getValue());
            }
        } else {
            throw new YoutrackAPIException("Unexpected type: " + result);
        }
    }

    public List<User> getIndividualAssignees(String project) throws IOException, JAXBException
    {
        final String responseString = execute(new HttpGet(buildURI(serviceLocationURI, "/rest/admin/project/" + project + "/assignee/individual")));
        final Object result = YoutrackUnmarshaller.unmarshall(responseString);
        if (result instanceof UserRefs) {
            return ((UserRefs) result).getUsers();
        } else {
            throw new YoutrackAPIException("Unexpected type: " + result);
        }
    }

    public IssueWrapper getIssue(String issueId) throws IOException, JAXBException
    {
        final String responseString;
        try {
            responseString = execute(new HttpGet(buildURI(serviceLocationURI, "/rest/issue/" + issueId)));
        } catch (YoutrackErrorException e) {
            if (e.getStatusCode() == HttpStatus.SC_NOT_FOUND) {
                throw new NoResultFoundException(e.getMessage(), e);
            } else {
                throw e;
            }
        }
        final Object result = YoutrackUnmarshaller.unmarshall(responseString);
        if (result instanceof pl.itcrowd.youtrack.api.rest.Issue) {
            return new IssueWrapper((Issue) result);
        } else if (result instanceof JAXBElement) {
            final JAXBElement jaxbElement = (JAXBElement) result;
            if (Issue_QNAME.equals(jaxbElement.getName())) {
                return new IssueWrapper((Issue) jaxbElement.getValue());
            } else {
                throw new YoutrackAPIException("Unexpected type: " + jaxbElement.getValue());
            }
        } else {
            throw new YoutrackAPIException("Unexpected type " + result);
        }
    }

    public void login(String username, String password) throws IOException, JAXBException
    {
        final HttpPost request = new HttpPost(buildURI(serviceLocationURI, "/rest/user/login"));
        request.setEntity(new UrlEncodedFormEntity(Arrays.asList(new BasicNameValuePair("login", username), new BasicNameValuePair("password", password))));
        execute(request);
    }

    public List<IssueWrapper> searchIssuesByProject(String project, Object filter) throws JAXBException, IOException
    {
        final Object result = YoutrackUnmarshaller.unmarshall(
            execute(new HttpGet(buildURI(serviceLocationURI, "/rest/issue/byproject/" + project, "filter=" + (filter == null ? "" : filter)))));
        if (!(result instanceof Issues)) {
            throw new YoutrackAPIException("Unmarshalling problem. Expected Issues, received: " + result.getClass() + " " + result);
        }
        List<Issue> issues = ((Issues) result).getIssues();
        List<IssueWrapper> wrappedIssues = new ArrayList<IssueWrapper>();
        for (Issue issue : issues) {
            wrappedIssues.add(new IssueWrapper(issue));
        }
        return wrappedIssues;
    }

    public void updateIssue(String issueId, String summary, String description) throws IOException
    {
        final HttpPost request = createPostRequest(buildURI(serviceLocationURI, "/rest/issue/" + issueId),
            new BasicNameValuePair(Fields.summary.name(), summary), new BasicNameValuePair(Fields.description.name(), description));
        final HttpResponse response = httpClient.execute(request);
        final StatusLine statusLine = response.getStatusLine();
        final HttpEntity entity = response.getEntity();
        final String responseText = entity == null ? null : EntityUtils.toString(entity);
        throwExceptionsIfNeeded(statusLine, responseText);
    }

    private HttpPost createPostRequest(URI uri, NameValuePair... nameValuePair) throws UnsupportedEncodingException
    {
        return setEntity(new HttpPost(uri), nameValuePair);
    }

    private HttpPut createPutRequest(URI uri, NameValuePair... nameValuePair) throws UnsupportedEncodingException
    {
        return setEntity(new HttpPut(uri), nameValuePair);
    }

    private String execute(HttpUriRequest request) throws IOException
    {
        request.addHeader("Accept","application/xml");
        final HttpResponse response = httpClient.execute(request);
        final StatusLine statusLine = response.getStatusLine();
        final HttpEntity entity = response.getEntity();
        String responseText = entity == null ? null : EntityUtils.toString(entity);
        if (statusLine.getStatusCode() >= 300) {
            try {
                final String error = YoutrackUnmarshaller.unmarshalError(responseText);
                throw new YoutrackErrorException(error, statusLine.getStatusCode());
            } catch (JAXBException e) {
                LOG.error("Cannot unmarshal following response text:\n" + responseText, e);
                throw new HttpResponseException(statusLine.getStatusCode(), responseText);
            }
        }
        if (entity == null) {
            throw new ClientProtocolException("Response contains no content");
        }
        return responseText;
    }

    private <T extends HttpEntityEnclosingRequestBase> T setEntity(T request, NameValuePair[] nameValuePair) throws UnsupportedEncodingException
    {
        final ArrayList<NameValuePair> list = new ArrayList<NameValuePair>();
        Collections.addAll(list, nameValuePair);
        request.setEntity(new UrlEncodedFormEntity(list));
        return request;
    }

    private void throwExceptionsIfNeeded(StatusLine statusLine, String responseText) throws IOException
    {
        if (statusLine.getStatusCode() >= 300) {
            throw new HttpResponseException(statusLine.getStatusCode(), responseText);
        }
    }
}