Skip to main content
aboutsummaryrefslogtreecommitdiffstats
blob: 17171f60183c7a10b6c452cbb10a8a744fce2db2 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
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
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
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
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
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
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
/*******************************************************************************
 * Copyright (c) 2011 Red Hat and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     David Green <david.green@tasktop.com> - initial contribution
 *     Christian Trutz <christian.trutz@gmail.com> - initial contribution
 *     Chris Aniszczyk <caniszczyk@gmail.com> - initial contribution
 *******************************************************************************/
package org.eclipse.mylyn.github.internal;

import java.io.IOException;

import org.apache.commons.httpclient.Credentials;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.NameValuePair;
import org.apache.commons.httpclient.UsernamePasswordCredentials;
import org.apache.commons.httpclient.auth.AuthScope;
import org.apache.commons.httpclient.auth.BasicScheme;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.google.gson.Gson;

/**
 * Facility to perform API operations on a GitHub issue tracker.
 *
 * TODO: This class needs to be refactored
 */
public class GitHubService {

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

	/**
	 * GitHub Issues API Documentation: http://develop.github.com/p/issues.html
	 */
	private final String gitURLBase = "https://github.com/api/v2/json/";
	private final String gistPostURL = "https://gist.github.com/gists";

	private final String gitIssueRoot = "issues/";
	private final String gitUserRoot = "user/";

	private final HttpClient httpClient;

	private final Gson gson;

	/**
	 * Helper class, describing all of the possible GitHub API actions.
	 */
	private final static String OPEN = "open/"; // Implemented
	private static final String REOPEN = "reopen/";
	private final static String CLOSE = "close/";
	private final static String EDIT = "edit/"; // Implemented
	private final static String COMMENT = "comment/";
	// private final static String VIEW = "view/";
	private final static String SHOW = "show/"; // :user/:repo/:number
	private final static String LIST = "list/"; // Implemented
	private final static String SEARCH = "search/"; // Implemented
	// private final static String REOPEN = "reopen/";
	// private final static String COMMENT = "comment/";
	private final static String ADD_LABEL = "label/add/"; // Implemented
	private final static String REMOVE_LABEL = "label/remove/"; // Implemented
	private final static String TOKEN = "/token";

	private static final String EMAILS = "emails";


	/**
	 * Constructor, create the client and JSON/Java interface object.
	 */
	public GitHubService() {
		httpClient = new HttpClient();
		gson = new Gson();
	}

	/**
	 * Set credentials to use for authentication
	 *
	 * @param credentials
	 * @return this service
	 */
	public GitHubService setCredentials(GitHubCredentials credentials) {
		Credentials apiCredentials = new UsernamePasswordCredentials(
				credentials.getUsername() + TOKEN, credentials.getApiToken());
		this.httpClient.getState().setCredentials(AuthScope.ANY, apiCredentials);
		return this;
	}

	/**
	 * Set method defaults
	 *
	 * @param method
	 * @return method param
	 */
	protected HttpMethod setMethodDefaults(HttpMethod method) {
		if (this.httpClient.getState().getCredentials(AuthScope.ANY) != null) {
			method.setDoAuthentication(true);
			method.getHostAuthState().setPreemptive();
			method.getHostAuthState().setAuthScheme(new BasicScheme());
		}
		return method;
	}

	/**
	 * Verify that the provided credentials are correct
	 * @param credentials
	 *
	 * @return true if and only if the credentials are correct
	 */
	public boolean verifyCredentials(GitHubCredentials credentials) throws GitHubServiceException {
		GetMethod method = null;

		boolean success = false;

		try {
			method = new GetMethod(gitURLBase + gitUserRoot + EMAILS);
			
			setCredentials(credentials);

			executeMethod(method);
			
			// if we reach here we know that credentials were good
			success = true;
		} catch (PermissionDeniedException e) {
			// if we provide bad credentials, GitHub will return 403 or 401
			return false;
		} catch (GitHubServiceException e) {
			throw e;
		} catch (final RuntimeException runtimeException) {
			throw runtimeException;
		} catch (final Exception exception) {
			throw new GitHubServiceException(exception);
		} finally {
			if (method != null)
				method.releaseConnection();
		}
		return success;
	}
	
	/**
	 * Search the GitHub Issues API for a given search term
	 * 
	 * @param user
	 *            - The user the repository is owned by
	 * @param repo
	 *            - The Git repository where the issue tracker is hosted
	 * @param state
	 *            - The issue state you want to filter your search by
	 * @param searchTerm
	 *            - The text search term to find in the issues.
	 * @param credentials
	 *            GitHub credentials to use for authentication
	 * 
	 * @return A GitHubIssues object containing all issues from the search
	 *         results
	 * 
	 * @throws GitHubServiceException
	 * 
	 * @note API Doc: /issues/search/:user/:repo/:state/:search_term
	 */
	public GitHubIssues searchIssues(final String user, final String repo,
			final String state, final String searchTerm, GitHubCredentials credentials)
			throws GitHubServiceException {
		GitHubIssues issues = null;
		GetMethod method = null;
		try {
			// build HTTP GET method
			if (searchTerm.trim().length() == 0) { // no search term: list all
				method = new GetMethod(gitURLBase + gitIssueRoot + LIST + user
						+ "/" + repo + "/" + state);
			} else {
				method = new GetMethod(gitURLBase + gitIssueRoot + SEARCH
						+ user + "/" + repo + "/" + state + "/" + searchTerm);
			}
			setCredentials(credentials);
			// execute HTTP GET method
			executeMethod(method);
			// transform JSON to Java object
			issues = gson.fromJson(new String(method.getResponseBody()),
					GitHubIssues.class);
		} catch (GitHubServiceException e) {
			throw e;
		} catch (final RuntimeException runtimeException) {
			throw runtimeException;
		} catch (final Exception exception) {
			throw new GitHubServiceException(exception);
		} finally {
			if (method != null)
				method.releaseConnection();
		}
		return issues;
	}

	/**
	 * Add a label to an existing GitHub issue.
	 * 
	 * @param user
	 *            - The user the repository is owned by
	 * @param repo
	 *            - The git repository where the issue tracker is hosted
	 * @param label
	 *            - The text label to add to the existing issue
	 * @param issueNumber
	 *            - The issue number to add a label to
	 * @param api
	 *            - The users GitHub
	 * 
	 * @return A boolean representing the success of the function call
	 * 
	 * @throws GitHubServiceException
	 * 
	 * @note API Doc: issues/label/add/:user/:repo/:label/:number API POST
	 */
	public boolean addLabel(final String user, final String repo,
			final String label, final int issueNumber,final GitHubCredentials credentials)
			throws GitHubServiceException {
		PostMethod method = null;

		boolean success = false;

		try {
			// build HTTP GET method
			method = new PostMethod(gitURLBase + gitIssueRoot + ADD_LABEL
					+ user + "/" + repo + "/" + label + "/"
					+ Integer.toString(issueNumber));

			setCredentials(credentials);

			// execute HTTP GET method
			executeMethod(method);
			// Check the response, make sure the action was successful
			final String response = method.getResponseBodyAsString();
			if (response.contains(label.subSequence(0, label.length()))) {
				success = true;
			}

			if (LOG.isDebugEnabled()) {
				LOG.debug("Response: " + method.getResponseBodyAsString());
				LOG.debug("URL: " + method.getURI());
			}
		} catch (GitHubServiceException e) {
			throw e;
		} catch (final RuntimeException runtimeException) {
			throw runtimeException;
		} catch (final Exception exception) {
			throw new GitHubServiceException(exception);
		} finally {
			if (method != null)
				method.releaseConnection();
		}
		return success;
	}

	/**
	 * Remove an existing label from an existing GitHub issue.
	 * 
	 * @param user
	 *            - The user the repository is owned by
	 * @param repo
	 *            - The git repository where the issue tracker is hosted
	 * @param label
	 * @param issueNumber
	 * @param api
	 * 
	 * @return A list of GitHub issues in the response text.
	 * 
	 * @throws GitHubServiceException
	 * 
	 *             API Doc: issues/label/remove/:user/:repo/:label/:number API
	 */
	public boolean removeLabel(final String user, final String repo,
			final String label, final int issueNumber, final GitHubCredentials credentials)
			throws GitHubServiceException {
		PostMethod method = null;
		boolean success = false;
		try {
			// build HTTP GET method
			method = new PostMethod(gitURLBase + gitIssueRoot + REMOVE_LABEL
					+ user + "/" + repo + "/" + label + "/"
					+ Integer.toString(issueNumber));

			// Set the users login and API token
			final NameValuePair login = new NameValuePair("login", credentials.getUsername());
			final NameValuePair token = new NameValuePair("token", credentials.getUsername());
			method.setRequestBody(new NameValuePair[] { login, token });

			// execute HTTP GET method
			executeMethod(method);
			// Check the response, make sure the action was successful
			final String response = method.getResponseBodyAsString();
			if (!response.contains(label.subSequence(0, label.length()))) {
				success = true;
			}
			if (LOG.isDebugEnabled()) {
				LOG.debug("Response: " + method.getResponseBodyAsString());
				LOG.debug("URL: " + method.getURI());
			}
		} catch (GitHubServiceException e) {
			throw e;
		} catch (final RuntimeException runtimeException) {
			throw runtimeException;
		} catch (final Exception exception) {
			throw new GitHubServiceException(exception);
		} finally {
			if (method != null)
				method.releaseConnection();
		}
		return success;
	}

	/**
	 * Open a new issue using the GitHub Issues API.
	 * 
	 * @param user
	 *            - The user the repository is owned by
	 * @param repo
	 *            - The git repository where the issue tracker is hosted
	 * @param issue
	 *            - The GitHub issue object to create on the issue tracker.
	 * 
	 * @return the issue that was created
	 * 
	 * @throws GitHubServiceException
	 * 
	 *             API Doc: issues/open/:user/:repo
	 *             API POST Variables: title, body
	 */
	public GitHubIssue openIssue(final String user, final String repo,
			final GitHubIssue issue, final GitHubCredentials credentials)
			throws GitHubServiceException {

		GitHubShowIssue showIssue = null;

		PostMethod method = null;
		try {
			// Create the HTTP POST method
			method = new PostMethod(gitURLBase + gitIssueRoot + OPEN + user
					+ "/" + repo);
			setCredentials(credentials);
			final NameValuePair body = new NameValuePair("body", issue
					.getBody());
			final NameValuePair title = new NameValuePair("title", issue
					.getTitle());

			method.setRequestBody(new NameValuePair[] { body, title });

			executeMethod(method);
			showIssue = gson.fromJson(new String(method.getResponseBody()),
					GitHubShowIssue.class);

			
			if (showIssue == null || showIssue.getIssue() == null) {
				if (LOG.isErrorEnabled()) {
					LOG.error("Unexpected server response: "+method.getResponseBodyAsString());
				}
				throw new GitHubServiceException("Unexpected server response");
			}
			if (LOG.isDebugEnabled()) {
				LOG.debug("Response: " + method.getResponseBodyAsString());
				LOG.debug("URL: " + method.getURI());
			}
			return showIssue.getIssue();
		} catch (GitHubServiceException e) {
			throw e;
		} catch (final RuntimeException runTimeException) {
			throw runTimeException;
		} catch (final Exception e) {
			throw new GitHubServiceException(e);
		} finally {
			if (method != null) {
				method.releaseConnection();
			}
		}
	}

	/**
	 * Create a new gist
	 *
	 * @return the url of the gist that was created
	 *
	 * @throws GitHubServiceException
	 * @throws IOException
	 */
	public String createGist(final String title, final String extension, final String gist, final GitHubCredentials credentials)
			throws GitHubServiceException, IOException {

		PostMethod method = null;
		try {
			// Create the HTTP POST method
			setCredentials(credentials);
			method = new PostMethod(gistPostURL);
			final NameValuePair name = new NameValuePair("file_name[gistfile1]", title);
			final NameValuePair ext = new NameValuePair("file_ext[gistfile1]", extension);
			final NameValuePair content = new NameValuePair("file_contents[gistfile1]", gist);
			method.addParameters(new NameValuePair[] { name, ext, content });
			executeMethod(method);

			return method.getResponseHeader("Location").getValue();
		} catch (GitHubServiceException e) {
			throw e;
		} catch (final RuntimeException runTimeException) {
			throw runTimeException;
		} catch (final Exception e) {
			throw new GitHubServiceException(e);
		} finally {
			if (method != null) {
				method.releaseConnection();
			}
		}
	}

	/**
	 * Edit an existing issue using the GitHub Issues API.
	 * 
	 * @param user
	 *            - The user the repository is owned by
	 * @param repo
	 *            - The git repository where the issue tracker is hosted
	 * @param issue
	 *            - The GitHub issue object to create on the issue tracker.
	 * 
	 * @return the issue with changes
	 * 
	 * @throws GitHubServiceException
	 * 
	 *             API Doc: issues/edit/:user/:repo/:number API POST Variables:
	 *             title, body
	 */
	public GitHubIssue editIssue(final String user, final String repo,
			final GitHubIssue issue,final GitHubCredentials credentials)
			throws GitHubServiceException {
		PostMethod method = null;
		try {
			
			// Create the HTTP POST method
			method = new PostMethod(gitURLBase + gitIssueRoot + EDIT + user
					+ "/" + repo + "/" + issue.getNumber());
			
			setCredentials(credentials);
			final NameValuePair body = new NameValuePair("body", issue
					.getBody());
			final NameValuePair title = new NameValuePair("title", issue
					.getTitle());

			method.setRequestBody(new NameValuePair[] { body, title });

			executeMethod(method);
			GitHubShowIssue showIssue = gson.fromJson(method.getResponseBodyAsString(),
						GitHubShowIssue.class);
				
			// Make sure the changes were made properly
			if (showIssue == null || showIssue.getIssue() == null) {
				if (LOG.isErrorEnabled()) {
					LOG.error("Unexpected server response: "+method.getResponseBodyAsString());
				}
				throw new GitHubServiceException("Unexpected server response");
			}

			if (LOG.isDebugEnabled()) {
				LOG.debug("Response: " + method.getResponseBodyAsString());
				LOG.debug("URL: " + method.getURI());
			}
			return showIssue.getIssue();
		} catch (final RuntimeException runTimeException) {
			throw runTimeException;
		} catch (final Exception e) {
			throw new GitHubServiceException(e);
		} finally {
			if (method != null) {
				method.releaseConnection();
			}
		}
	}

	public GitHubIssue showIssue(final String user, final String repo,
			final String issueNumber, final GitHubCredentials credentials)
			throws GitHubServiceException {
		GetMethod method = null;
		try {
			// build HTTP GET method
			method = new GetMethod(gitURLBase + gitIssueRoot + SHOW 
            + user + "/" + repo + "/" + issueNumber);
			
			setCredentials(credentials);
			// execute HTTP GET method
			executeMethod(method);
			// transform JSON to Java object
			GitHubShowIssue issue = gson.fromJson(new String(method.getResponseBody()),
					GitHubShowIssue.class);
			
			return issue.getIssue();
		} catch (GitHubServiceException e) {
			throw e;
		} catch (final RuntimeException runtimeException) {
			throw runtimeException;
		} catch (final Exception exception) {
			throw new GitHubServiceException(exception);
		} finally {
			if (method != null) {
				method.releaseConnection();
			}
		}
	}

	private void executeMethod(HttpMethod method) throws GitHubServiceException {
		int status;
		try {
			setMethodDefaults(method);
			status = httpClient.executeMethod(method);
		} catch (HttpException e) { 
			throw new GitHubServiceException(e);
		} catch (IOException e) {
			throw new GitHubServiceException(e);
		}
		if (status != HttpStatus.SC_OK) {
			switch (status) {
			case HttpStatus.SC_CREATED:
			case HttpStatus.SC_MOVED_TEMPORARILY:
				break;
			case HttpStatus.SC_UNAUTHORIZED:
			case HttpStatus.SC_FORBIDDEN:
				throw new PermissionDeniedException(method.getStatusLine());
			default:
				throw new GitHubServiceException(method.getStatusLine());
			}
		}
	}

	/**
	 * Edit an existing issue using the GitHub Issues API and change its status to open.
	 * 
	 * @param user
	 *            - The user the repository is owned by
	 * @param repo
	 *            - The git repository where the issue tracker is hosted
	 * @param issue
	 *            - The GitHub issue object to create on the issue tracker.
	 * 
	 * @return the issue with changes
	 * 
	 * @throws GitHubServiceException
	 * 
	 *             API Doc: issues/reopen/:user/:repo/:number API POST Variables:
	 *             title, body
	 */
	public GitHubIssue reopenIssue(String user, String repo, GitHubIssue issue,
			GitHubCredentials credentials) throws GitHubServiceException {
		issue = editIssue(user, repo, issue, credentials);
		return changeIssueStatus(user, repo, REOPEN, issue, credentials);
	}

	/**
	 * Edit an existing issue using the GitHub Issues API and change its status to closed.
	 * 
	 * @param user
	 *            - The user the repository is owned by
	 * @param repo
	 *            - The git repository where the issue tracker is hosted
	 * @param issue
	 *            - The GitHub issue object to create on the issue tracker.
	 * 
	 * @return the issue with changes
	 * 
	 * @throws GitHubServiceException
	 * 
	 *             API Doc: issues/close/:user/:repo/:number API POST Variables:
	 *             title, body
	 */
	public GitHubIssue closeIssue(String user, String repo, GitHubIssue issue,
			GitHubCredentials credentials) throws GitHubServiceException {
		issue = editIssue(user, repo, issue, credentials);
		return changeIssueStatus(user, repo, CLOSE, issue, credentials);
		
	}
	
	private GitHubIssue changeIssueStatus(final String user, final String repo,
			String githubOperation, final GitHubIssue issue,
			final GitHubCredentials credentials) throws GitHubServiceException {
		PostMethod method = null;
		try {
			
			// Create the HTTP POST method
			method = new PostMethod(gitURLBase + gitIssueRoot + githubOperation + user
					+ "/" + repo + "/" + issue.getNumber());
			
			setCredentials(credentials);

			executeMethod(method);
			GitHubShowIssue showIssue = gson.fromJson(method.getResponseBodyAsString(),
						GitHubShowIssue.class);
				
			// Make sure the changes were made properly
			if (showIssue == null || showIssue.getIssue() == null) {
				if (LOG.isErrorEnabled()) {
					LOG.error("Unexpected server response: "+method.getResponseBodyAsString());
				}
				throw new GitHubServiceException("Unexpected server response");
			}

			if (LOG.isDebugEnabled()) {
				LOG.debug("Response: " + method.getResponseBodyAsString());
				LOG.debug("URL: " + method.getURI());
			}
			return showIssue.getIssue();
		} catch (final RuntimeException runTimeException) {
			throw runTimeException;
		} catch (final Exception e) {
			throw new GitHubServiceException(e);
		} finally {
			if (method != null) {
				method.releaseConnection();
			}
		}
	}

	/**
	 * Add comment on issues.
	 *
	 * @param user
	 *            - The user the repository is owned by
	 * @param repo
	 *            - The git repository where the issue tracker is hosted
	 * @param issue
	 * @param credentials
	 * @throws GitHubServiceException
	 */
	public void addComment(final String user, final String repo,
			final GitHubIssue issue,final GitHubCredentials credentials)
	throws GitHubServiceException {
		PostMethod method = null;
		try {
			method = new PostMethod(gitURLBase + gitIssueRoot + COMMENT + user
					+ "/" + repo + "/" + issue.getNumber());
			final NameValuePair comment = new NameValuePair("comment", issue
					.getComment_new());
			method.setRequestBody(new NameValuePair[] { comment });
			setCredentials(credentials);
			executeMethod(method);
		}catch (final RuntimeException runTimeException) {
			throw runTimeException;
		} catch (final Exception e) {
			throw new GitHubServiceException(e);
		} finally {
			if (method != null) {
				method.releaseConnection();
			}
		}
	}
}

Back to the top