YoutrackAPI.java 14.9 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 {
// ------------------------------ FIELDS ------------------------------

  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;

// -------------------------- STATIC METHODS --------------------------

  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;
  }

// --------------------------- CONSTRUCTORS ---------------------------

  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);
  }

// --------------------- GETTER / SETTER METHODS ---------------------

  public String getServiceLocation()
  {
    return serviceLocation;
  }

// -------------------------- OTHER METHODS --------------------------

  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
  {
    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 = ErrorUnmarshaller.unmarshal(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);
    }
  }
}