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.IntelliSense; import pl.itcrowd.youtrack.api.rest.Issue; import pl.itcrowd.youtrack.api.rest.IssueCompacts; import pl.itcrowd.youtrack.api.rest.IssueItem; 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.HashMap; import java.util.List; import java.util.Map; 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; 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); } 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 X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { } public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { } }}, 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 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 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 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 parameters = new ArrayList(); 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 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 searchIntellisenseIssuesByProject(String project, Object filter) throws JAXBException, IOException { final String query = "project=" + project + "&filter=" + (filter == null ? "" : filter); final URI buildURI = buildURI(serviceLocationURI, "/rest/issue/intellisense", query); final HttpGet request = new HttpGet(buildURI); final String execute = execute(request); final Object result = YoutrackUnmarshaller.unmarshall(execute); if (!(result instanceof IntelliSense)) { throw new YoutrackAPIException("Unmarshalling problem. Expected Issues, received: " + result.getClass() + " " + result); } IntelliSense.Suggest suggest = ((IntelliSense) result).getSuggest(); List issueItems = suggest.getItems(); List wrappedIssueItems = new ArrayList(); for (IssueItem issueItem : issueItems) { wrappedIssueItems.add(new IssueItemWrapper(issueItem)); } return wrappedIssueItems; } public List searchIssues(Object filter, Integer maxResults, Integer after) throws JAXBException, IOException { final Map params = new HashMap(); params.put("filter", filter); params.put("max", maxResults); params.put("after", after); final Object result = YoutrackUnmarshaller.unmarshall(execute(new HttpGet(buildURI(serviceLocationURI, "/rest/issue", params)))); if (!(result instanceof IssueCompacts)) { throw new YoutrackAPIException("Unmarshalling problem. Expected Issues, received: " + result.getClass() + " " + result); } List issues = ((IssueCompacts) result).getIssues(); List wrappedIssues = new ArrayList(); for (Issue issue : issues) { wrappedIssues.add(new IssueWrapper(issue)); } return wrappedIssues; } public List 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 issues = ((Issues) result).getIssues(); List wrappedIssues = new ArrayList(); 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 setEntity(T request, NameValuePair[] nameValuePair) throws UnsupportedEncodingException { final ArrayList list = new ArrayList(); 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); } } }