Blame view

src/main/java/pl/itcrowd/youtrack/api/YoutrackAPI.java 15.6 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 37 38 39 40
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;
bernard authored
41
bernard authored
42 43 44 45
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import javax.xml.bind.JAXBElement;
bernard authored
46
import javax.xml.bind.JAXBException;
bernard authored
47
import javax.xml.namespace.QName;
bernard authored
48
import java.io.IOException;
bernard authored
49
import java.io.UnsupportedEncodingException;
bernard authored
50 51 52 53 54 55 56
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
57
import java.util.ArrayList;
bernard authored
58
import java.util.Arrays;
bernard authored
59
import java.util.Collections;
bernard authored
60
import java.util.List;
bernard authored
61 62
import java.util.regex.Matcher;
import java.util.regex.Pattern;
bernard authored
63
bernard authored
64
import static pl.itcrowd.youtrack.api.URIUtils.buildURI;
bernard authored
65
bernard authored
66 67
public class YoutrackAPI {
bernard authored
68
    private final static QName Enumeration_QNAME = new QName("", "enumeration");
bernard authored
69
bernard authored
70
    private final static QName Issue_QNAME = new QName("", "issue");
bernard authored
71
bernard authored
72
    private static Log LOG = LogFactory.getLog(YoutrackAPI.class);
bernard authored
73
bernard authored
74
    private HttpClient httpClient;
bernard authored
75
bernard authored
76
    private String serviceLocation;
bernard authored
77
bernard authored
78
    private URI serviceLocationURI;
bernard authored
79
bernard authored
80 81 82 83 84 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
    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);
bernard authored
113
bernard authored
114 115
        ClientConnectionManager cm = new PoolingClientConnectionManager(schemeRegistry);
        return new DefaultHttpClient(cm);
bernard authored
116
    }
bernard authored
117
bernard authored
118 119 120 121 122
    private static boolean isBlank(String str)
    {
        int strLen;
        if (str == null || (strLen = str.length()) == 0) {
            return true;
bernard authored
123
        }
bernard authored
124 125 126 127
        for (int i = 0; i < strLen; i++) {
            if ((!Character.isWhitespace(str.charAt(i)))) {
                return false;
            }
bernard authored
128
        }
bernard authored
129
        return true;
bernard authored
130
    }
bernard authored
131
bernard authored
132 133 134
    public YoutrackAPI(String serviceLocation)
    {
        this(serviceLocation, null);
bernard authored
135
    }
bernard authored
136 137 138 139 140 141 142 143 144 145 146 147 148

    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;
bernard authored
149 150
    }
bernard authored
151 152 153 154
    public YoutrackAPI(String serviceLocation, String username, String password) throws IOException, JAXBException
    {
        this(serviceLocation, null, username, password);
    }
bernard authored
155
bernard authored
156 157 158 159 160
    public YoutrackAPI(String serviceLocation, HttpClient httpClient, String username, String password) throws IOException, JAXBException
    {
        this(serviceLocation, httpClient);
        login(username, password);
    }
bernard authored
161
bernard authored
162 163 164
    public String getServiceLocation()
    {
        return serviceLocation;
bernard authored
165
    }
bernard authored
166 167 168 169

    public void command(String issueId, String command) throws IOException
    {
        command(issueId, command, null, null, null, null);
bernard authored
170
    }
bernard authored
171 172 173 174

    public void command(String issueId, Command command) throws IOException
    {
        command(issueId, command.toString());
bernard authored
175
    }
bernard authored
176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195

    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
196
    }
bernard authored
197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228

    /**
     * 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
229
    }
bernard authored
230 231 232 233

    public void deleteIssue(String issueId) throws IOException
    {
        execute(new HttpDelete(buildURI(serviceLocationURI, "/rest/issue/" + issueId)));
bernard authored
234
    }
bernard authored
235 236 237 238 239 240 241 242 243 244

    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);
        }
bernard authored
245
    }
bernard authored
246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262

    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);
        }
bernard authored
263
    }
bernard authored
264 265 266 267 268 269 270 271 272 273

    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);
        }
bernard authored
274
    }
bernard authored
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

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

    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);
bernard authored
308
    }
bernard authored
309 310 311 312 313 314 315 316 317 318 319 320 321 322

    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
323
    }
bernard authored
324 325 326 327 328 329 330 331 332 333

    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
334
    }
bernard authored
335 336 337 338

    private HttpPost createPostRequest(URI uri, NameValuePair... nameValuePair) throws UnsupportedEncodingException
    {
        return setEntity(new HttpPost(uri), nameValuePair);
bernard authored
339
    }
bernard authored
340 341 342 343

    private HttpPut createPutRequest(URI uri, NameValuePair... nameValuePair) throws UnsupportedEncodingException
    {
        return setEntity(new HttpPut(uri), nameValuePair);
bernard authored
344
    }
bernard authored
345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364

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

    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
373
    }
bernard authored
374 375 376 377 378 379

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