Blame view

src/main/java/pl/itcrowd/youtrack/api/YoutrackAPI.java 17.8 KB
bernard authored
1
package pl.itcrowd.youtrack.api;
bernard authored
2
bernard authored
3 4
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
bernard authored
5 6 7 8 9
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;
bernard authored
10
import org.apache.http.NameValuePair;
bernard authored
11 12 13 14 15
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;
bernard authored
16
import org.apache.http.client.methods.HttpDelete;
bernard authored
17
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
bernard authored
18 19 20 21 22 23 24 25 26 27 28 29 30
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;
bernard authored
31 32 33 34 35 36
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;
l.gladek authored
37
import pl.itcrowd.youtrack.api.rest.IntelliSense;
bernard authored
38
import pl.itcrowd.youtrack.api.rest.Issue;
Bernard Labno authored
39
import pl.itcrowd.youtrack.api.rest.IssueCompacts;
l.gladek authored
40
import pl.itcrowd.youtrack.api.rest.IssueItem;
bernard authored
41 42 43
import pl.itcrowd.youtrack.api.rest.Issues;
import pl.itcrowd.youtrack.api.rest.User;
import pl.itcrowd.youtrack.api.rest.UserRefs;
bernard authored
44
bernard authored
45 46 47 48
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import javax.xml.bind.JAXBElement;
bernard authored
49
import javax.xml.bind.JAXBException;
bernard authored
50
import javax.xml.namespace.QName;
bernard authored
51
import java.io.IOException;
bernard authored
52
import java.io.UnsupportedEncodingException;
bernard authored
53 54 55 56 57 58 59
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;
bernard authored
60
import java.util.ArrayList;
bernard authored
61
import java.util.Arrays;
bernard authored
62
import java.util.Collections;
Bernard Labno authored
63
import java.util.HashMap;
bernard authored
64
import java.util.List;
Bernard Labno authored
65
import java.util.Map;
bernard authored
66 67
import java.util.regex.Matcher;
import java.util.regex.Pattern;
bernard authored
68
bernard authored
69
import static pl.itcrowd.youtrack.api.URIUtils.buildURI;
bernard authored
70
bernard authored
71 72
public class YoutrackAPI {
bernard authored
73
    private final static QName Enumeration_QNAME = new QName("", "enumeration");
bernard authored
74
bernard authored
75
    private final static QName Issue_QNAME = new QName("", "issue");
bernard authored
76
bernard authored
77
    private static Log LOG = LogFactory.getLog(YoutrackAPI.class);
bernard authored
78
bernard authored
79
    private HttpClient httpClient;
bernard authored
80
bernard authored
81
    private String serviceLocation;
bernard authored
82
bernard authored
83
    private URI serviceLocationURI;
bernard authored
84
Bernard Labno authored
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114
    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);
    }
bernard authored
115 116 117 118 119 120 121 122 123 124 125
    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() {
l.gladek authored
126
                public X509Certificate[] getAcceptedIssuers()
bernard authored
127
                {
l.gladek authored
128
                    return new X509Certificate[0];
bernard authored
129 130
                }
l.gladek authored
131
                public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException
bernard authored
132 133 134
                {
                }
l.gladek authored
135
                public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException
bernard authored
136 137 138 139 140 141 142 143 144 145 146 147
                {
                }
            }}, 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);
bernard authored
148
bernard authored
149 150
        ClientConnectionManager cm = new PoolingClientConnectionManager(schemeRegistry);
        return new DefaultHttpClient(cm);
bernard authored
151
    }
bernard authored
152
bernard authored
153 154 155 156 157
    private static boolean isBlank(String str)
    {
        int strLen;
        if (str == null || (strLen = str.length()) == 0) {
            return true;
bernard authored
158
        }
bernard authored
159 160 161 162
        for (int i = 0; i < strLen; i++) {
            if ((!Character.isWhitespace(str.charAt(i)))) {
                return false;
            }
bernard authored
163
        }
bernard authored
164
        return true;
bernard authored
165
    }
bernard authored
166
Bernard Labno authored
167
    public AssigneeList getAssignees(String project) throws IOException, JAXBException
bernard authored
168
    {
Bernard Labno authored
169 170 171 172 173 174 175
        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);
        }
bernard authored
176
    }
bernard authored
177
Bernard Labno authored
178
    public Enumeration getBundle(String customField) throws IOException, JAXBException
bernard authored
179
    {
Bernard Labno authored
180 181 182 183 184 185 186 187 188 189 190 191 192
        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);
bernard authored
193
        }
bernard authored
194 195
    }
Bernard Labno authored
196
    public List<User> getIndividualAssignees(String project) throws IOException, JAXBException
bernard authored
197
    {
Bernard Labno authored
198 199 200 201 202 203 204
        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);
        }
bernard authored
205
    }
bernard authored
206
Bernard Labno authored
207
    public IssueWrapper getIssue(String issueId) throws IOException, JAXBException
bernard authored
208
    {
Bernard Labno authored
209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231
        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);
        }
bernard authored
232
    }
bernard authored
233
bernard authored
234 235 236
    public String getServiceLocation()
    {
        return serviceLocation;
bernard authored
237
    }
bernard authored
238 239 240 241

    public void command(String issueId, String command) throws IOException
    {
        command(issueId, command, null, null, null, null);
bernard authored
242
    }
bernard authored
243 244 245 246

    public void command(String issueId, Command command) throws IOException
    {
        command(issueId, command.toString());
bernard authored
247
    }
bernard authored
248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267

    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);
bernard authored
268
    }
bernard authored
269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300

    /**
     * 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);
bernard authored
301
    }
bernard authored
302 303 304 305

    public void deleteIssue(String issueId) throws IOException
    {
        execute(new HttpDelete(buildURI(serviceLocationURI, "/rest/issue/" + issueId)));
bernard authored
306
    }
bernard authored
307
Bernard Labno authored
308
    public void login(String username, String password) throws IOException, JAXBException
bernard authored
309
    {
Bernard Labno authored
310 311 312
        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);
bernard authored
313
    }
bernard authored
314
l.gladek authored
315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333
    public List<IssueItemWrapper> 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<IssueItem> issueItems = suggest.getItems();
        List<IssueItemWrapper> wrappedIssueItems = new ArrayList<IssueItemWrapper>();
        for (IssueItem issueItem : issueItems) {
            wrappedIssueItems.add(new IssueItemWrapper(issueItem));
        }
        return wrappedIssueItems;
    }
Bernard Labno authored
334
    public List<IssueWrapper> searchIssues(Object filter, Integer maxResults, Integer after) throws JAXBException, IOException
bernard authored
335
    {
Bernard Labno authored
336 337 338 339 340 341 342
        final Map<String, Object> params = new HashMap<String, Object>();
        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);
bernard authored
343
        }
Bernard Labno authored
344 345 346 347
        List<Issue> issues = ((IssueCompacts) result).getIssues();
        List<IssueWrapper> wrappedIssues = new ArrayList<IssueWrapper>();
        for (Issue issue : issues) {
            wrappedIssues.add(new IssueWrapper(issue));
bernard authored
348
        }
Bernard Labno authored
349
        return wrappedIssues;
bernard authored
350
    }
bernard authored
351 352 353 354 355 356 357 358 359 360 361 362 363 364

    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;
bernard authored
365
    }
bernard authored
366 367 368 369 370 371 372 373 374 375

    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);
bernard authored
376
    }
bernard authored
377 378 379 380

    private HttpPost createPostRequest(URI uri, NameValuePair... nameValuePair) throws UnsupportedEncodingException
    {
        return setEntity(new HttpPost(uri), nameValuePair);
bernard authored
381
    }
bernard authored
382 383 384 385

    private HttpPut createPutRequest(URI uri, NameValuePair... nameValuePair) throws UnsupportedEncodingException
    {
        return setEntity(new HttpPut(uri), nameValuePair);
bernard authored
386
    }
bernard authored
387 388 389

    private String execute(HttpUriRequest request) throws IOException
    {
Bernard Labno authored
390
        request.addHeader("Accept", "application/xml");
bernard authored
391 392 393 394 395 396
        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 {
bernard authored
397
                final String error = YoutrackUnmarshaller.unmarshalError(responseText);
bernard authored
398 399 400 401 402 403 404 405 406 407
                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;
bernard authored
408
    }
bernard authored
409 410 411 412 413 414 415

    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;
bernard authored
416
    }
bernard authored
417 418 419 420 421 422

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