IDEA-131201 github tasks: allow to specify, whether to load all issues or assigned...
[idea/community.git] / plugins / github / src / org / jetbrains / plugins / github / tasks / GithubRepository.java
1 package org.jetbrains.plugins.github.tasks;
2
3 import com.intellij.openapi.diagnostic.Logger;
4 import com.intellij.openapi.progress.ProgressIndicator;
5 import com.intellij.openapi.util.Comparing;
6 import com.intellij.openapi.util.PasswordUtil;
7 import com.intellij.openapi.util.text.StringUtil;
8 import com.intellij.tasks.*;
9 import com.intellij.tasks.impl.BaseRepository;
10 import com.intellij.tasks.impl.BaseRepositoryImpl;
11 import com.intellij.util.Function;
12 import com.intellij.util.containers.ContainerUtil;
13 import com.intellij.util.xmlb.annotations.Tag;
14 import com.intellij.util.xmlb.annotations.Transient;
15 import icons.TasksIcons;
16 import org.jetbrains.annotations.NotNull;
17 import org.jetbrains.annotations.Nullable;
18 import org.jetbrains.plugins.github.api.GithubApiUtil;
19 import org.jetbrains.plugins.github.api.GithubConnection;
20 import org.jetbrains.plugins.github.api.GithubIssue;
21 import org.jetbrains.plugins.github.api.GithubIssueComment;
22 import org.jetbrains.plugins.github.exceptions.*;
23 import org.jetbrains.plugins.github.util.GithubAuthData;
24 import org.jetbrains.plugins.github.util.GithubUtil;
25
26 import javax.swing.*;
27 import java.util.Date;
28 import java.util.List;
29 import java.util.regex.Matcher;
30 import java.util.regex.Pattern;
31
32 /**
33  * @author Dennis.Ushakov
34  */
35 @Tag("GitHub")
36 public class GithubRepository extends BaseRepositoryImpl {
37   private static final Logger LOG = GithubUtil.LOG;
38
39   private Pattern myPattern = Pattern.compile("($^)");
40   @NotNull private String myRepoAuthor = "";
41   @NotNull private String myRepoName = "";
42   @NotNull private String myUser = "";
43   @NotNull private String myToken = "";
44   private boolean myAssignedIssuesOnly = false;
45
46   @SuppressWarnings({"UnusedDeclaration"})
47   public GithubRepository() {
48   }
49
50   public GithubRepository(GithubRepository other) {
51     super(other);
52     setRepoName(other.myRepoName);
53     setRepoAuthor(other.myRepoAuthor);
54     setToken(other.myToken);
55     setAssignedIssuesOnly(other.myAssignedIssuesOnly);
56   }
57
58   public GithubRepository(GithubRepositoryType type) {
59     super(type);
60     setUrl("https://" + GithubApiUtil.DEFAULT_GITHUB_HOST);
61   }
62
63   @NotNull
64   @Override
65   public CancellableConnection createCancellableConnection() {
66     return new CancellableConnection() {
67       private final GithubConnection myConnection = new GithubConnection(getAuthData(), false);
68
69       @Override
70       protected void doTest() throws Exception {
71         try {
72           GithubApiUtil.getIssuesQueried(myConnection, getRepoAuthor(), getRepoName(), null, null, false);
73         }
74         catch (GithubOperationCanceledException ignore) {
75         }
76       }
77
78       @Override
79       public void cancel() {
80         myConnection.abort();
81       }
82     };
83   }
84
85   @Override
86   public boolean isConfigured() {
87     return super.isConfigured() &&
88            !StringUtil.isEmptyOrSpaces(getRepoAuthor()) &&
89            !StringUtil.isEmptyOrSpaces(getRepoName()) &&
90            !StringUtil.isEmptyOrSpaces(getToken());
91   }
92
93   @Override
94   public String getPresentableName() {
95     final String name = super.getPresentableName();
96     return name +
97            (!StringUtil.isEmpty(getRepoAuthor()) ? "/" + getRepoAuthor() : "") +
98            (!StringUtil.isEmpty(getRepoName()) ? "/" + getRepoName() : "");
99   }
100
101   @Override
102   public Task[] getIssues(@Nullable String query, int offset, int limit, boolean withClosed) throws Exception {
103     try {
104       return getIssues(query, offset + limit, withClosed);
105     }
106     catch (GithubRateLimitExceededException e) {
107       return new Task[0];
108     }
109     catch (GithubAuthenticationException e) {
110       throw new Exception(e.getMessage(), e); // Wrap to show error message
111     }
112     catch (GithubStatusCodeException e) {
113       throw new Exception(e.getMessage(), e);
114     }
115     catch (GithubJsonException e) {
116       throw new Exception("Bad response format", e);
117     }
118   }
119
120   @Override
121   public Task[] getIssues(@Nullable String query, int offset, int limit, boolean withClosed, @NotNull ProgressIndicator cancelled)
122     throws Exception {
123     return getIssues(query, offset, limit, withClosed);
124   }
125
126   @NotNull
127   private Task[] getIssues(@Nullable String query, int max, boolean withClosed) throws Exception {
128     GithubConnection connection = getConnection();
129
130     try {
131       String assigned = null;
132       if (myAssignedIssuesOnly) {
133         if (StringUtil.isEmptyOrSpaces(myUser)) {
134           myUser = GithubApiUtil.getCurrentUser(connection).getLogin();
135         }
136         assigned = myUser;
137       }
138
139       List<GithubIssue> issues;
140       if (StringUtil.isEmptyOrSpaces(query)) {
141         // search queries have way smaller request number limit
142         issues =
143           GithubApiUtil.getIssuesAssigned(connection, getRepoAuthor(), getRepoName(), assigned, max, withClosed);
144       }
145       else {
146         issues =
147           GithubApiUtil.getIssuesQueried(connection, getRepoAuthor(), getRepoName(), assigned, query, withClosed);
148       }
149
150       return ContainerUtil.map2Array(issues, Task.class, new Function<GithubIssue, Task>() {
151         @Override
152         public Task fun(GithubIssue issue) {
153           return createTask(issue);
154         }
155       });
156     }
157     finally {
158       connection.close();
159     }
160   }
161
162   @NotNull
163   private Task createTask(final GithubIssue issue) {
164     return new Task() {
165       @NotNull String myRepoName = getRepoName();
166
167       @Override
168       public boolean isIssue() {
169         return true;
170       }
171
172       @Override
173       public String getIssueUrl() {
174         return issue.getHtmlUrl();
175       }
176
177       @NotNull
178       @Override
179       public String getId() {
180         return myRepoName + "-" + issue.getNumber();
181       }
182
183       @NotNull
184       @Override
185       public String getSummary() {
186         return issue.getTitle();
187       }
188
189       public String getDescription() {
190         return issue.getBody();
191       }
192
193       @NotNull
194       @Override
195       public Comment[] getComments() {
196         try {
197           return fetchComments(issue.getNumber());
198         }
199         catch (Exception e) {
200           LOG.warn("Error fetching comments for " + issue.getNumber(), e);
201           return Comment.EMPTY_ARRAY;
202         }
203       }
204
205       @NotNull
206       @Override
207       public Icon getIcon() {
208         return TasksIcons.Github;
209       }
210
211       @NotNull
212       @Override
213       public TaskType getType() {
214         return TaskType.BUG;
215       }
216
217       @Override
218       public Date getUpdated() {
219         return issue.getUpdatedAt();
220       }
221
222       @Override
223       public Date getCreated() {
224         return issue.getCreatedAt();
225       }
226
227       @Override
228       public boolean isClosed() {
229         return !"open".equals(issue.getState());
230       }
231
232       @Override
233       public TaskRepository getRepository() {
234         return GithubRepository.this;
235       }
236
237       @Override
238       public String getPresentableName() {
239         return getId() + ": " + getSummary();
240       }
241     };
242   }
243
244   private Comment[] fetchComments(final long id) throws Exception {
245     GithubConnection connection = getConnection();
246     try {
247       List<GithubIssueComment> result = GithubApiUtil.getIssueComments(connection, getRepoAuthor(), getRepoName(), id);
248
249       return ContainerUtil.map2Array(result, Comment.class, new Function<GithubIssueComment, Comment>() {
250         @Override
251         public Comment fun(GithubIssueComment comment) {
252           return new GithubComment(comment.getCreatedAt(), comment.getUser().getLogin(), comment.getBodyHtml(),
253                                    comment.getUser().getAvatarUrl(),
254                                    comment.getUser().getHtmlUrl());
255         }
256       });
257     }
258     finally {
259       connection.close();
260     }
261   }
262
263   @Nullable
264   public String extractId(@NotNull String taskName) {
265     Matcher matcher = myPattern.matcher(taskName);
266     return matcher.find() ? matcher.group(1) : null;
267   }
268
269   @Nullable
270   @Override
271   public Task findTask(@NotNull String id) throws Exception {
272     final int index = id.lastIndexOf("-");
273     if (index < 0) {
274       return null;
275     }
276     final String numericId = id.substring(index + 1);
277     GithubConnection connection = getConnection();
278     try {
279       return createTask(GithubApiUtil.getIssue(connection, getRepoAuthor(), getRepoName(), numericId));
280     }
281     catch (GithubStatusCodeException e) {
282       if (e.getStatusCode() == 404) {
283         return null;
284       }
285       throw e;
286     }
287     finally {
288       connection.close();
289     }
290   }
291
292   @Override
293   public void setTaskState(@NotNull Task task, @NotNull TaskState state) throws Exception {
294     GithubConnection connection = getConnection();
295     try {
296       boolean isOpen;
297       switch (state) {
298         case OPEN:
299           isOpen = true;
300           break;
301         case RESOLVED:
302           isOpen = false;
303           break;
304         default:
305           throw new IllegalStateException("Unknown state: " + state);
306       }
307       GithubApiUtil.setIssueState(connection, getRepoAuthor(), getRepoName(), task.getNumber(), isOpen);
308     }
309     finally {
310       connection.close();
311     }
312   }
313
314   @NotNull
315   @Override
316   public BaseRepository clone() {
317     return new GithubRepository(this);
318   }
319
320   @NotNull
321   public String getRepoName() {
322     return myRepoName;
323   }
324
325   public void setRepoName(@NotNull String repoName) {
326     myRepoName = repoName;
327     myPattern = Pattern.compile("(" + StringUtil.escapeToRegexp(repoName) + "\\-\\d+)");
328   }
329
330   @NotNull
331   public String getRepoAuthor() {
332     return myRepoAuthor;
333   }
334
335   public void setRepoAuthor(@NotNull String repoAuthor) {
336     myRepoAuthor = repoAuthor;
337   }
338
339   @NotNull
340   public String getUser() {
341     return myUser;
342   }
343
344   public void setUser(@NotNull String user) {
345     myUser = user;
346   }
347
348   @Transient
349   @NotNull
350   public String getToken() {
351     return myToken;
352   }
353
354   public void setToken(@NotNull String token) {
355     myToken = token;
356     setUser("");
357   }
358
359   public boolean isAssignedIssuesOnly() {
360     return myAssignedIssuesOnly;
361   }
362
363   public void setAssignedIssuesOnly(boolean value) {
364     myAssignedIssuesOnly = value;
365   }
366
367   @Tag("token")
368   public String getEncodedToken() {
369     return PasswordUtil.encodePassword(getToken());
370   }
371
372   public void setEncodedToken(String password) {
373     try {
374       setToken(PasswordUtil.decodePassword(password));
375     }
376     catch (NumberFormatException e) {
377       LOG.warn("Can't decode token", e);
378     }
379   }
380
381   private GithubAuthData getAuthData() {
382     return GithubAuthData.createTokenAuth(getUrl(), getToken(), isUseProxy());
383   }
384
385   private GithubConnection getConnection() {
386     return new GithubConnection(getAuthData(), true);
387   }
388
389   @Override
390   public boolean equals(Object o) {
391     if (!super.equals(o)) return false;
392     if (!(o instanceof GithubRepository)) return false;
393
394     GithubRepository that = (GithubRepository)o;
395     if (!Comparing.equal(getRepoAuthor(), that.getRepoAuthor())) return false;
396     if (!Comparing.equal(getRepoName(), that.getRepoName())) return false;
397     if (!Comparing.equal(getToken(), that.getToken())) return false;
398     if (!Comparing.equal(isAssignedIssuesOnly(), that.isAssignedIssuesOnly())) return false;
399
400     return true;
401   }
402
403   @Override
404   public int hashCode() {
405     return StringUtil.stringHashCode(getRepoName()) +
406            31 * StringUtil.stringHashCode(getRepoAuthor());
407   }
408
409   @Override
410   protected int getFeatures() {
411     return super.getFeatures() | STATE_UPDATING;
412   }
413 }