404e76ca7bba885e3535f58d05b98e7226e64f39
[idea/community.git] / plugins / tasks / tasks-core / src / com / intellij / tasks / youtrack / YouTrackRepository.java
1 /*
2  * Copyright 2000-2014 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.tasks.youtrack;
17
18 import com.intellij.openapi.diagnostic.Logger;
19 import com.intellij.openapi.util.Comparing;
20 import com.intellij.openapi.util.io.StreamUtil;
21 import com.intellij.openapi.util.text.StringUtil;
22 import com.intellij.openapi.vfs.CharsetToolkit;
23 import com.intellij.tasks.*;
24 import com.intellij.tasks.impl.BaseRepository;
25 import com.intellij.tasks.impl.BaseRepositoryImpl;
26 import com.intellij.tasks.impl.LocalTaskImpl;
27 import com.intellij.tasks.impl.TaskUtil;
28 import com.intellij.util.Function;
29 import com.intellij.util.NullableFunction;
30 import com.intellij.util.containers.ContainerUtil;
31 import com.intellij.util.text.VersionComparatorUtil;
32 import com.intellij.util.xmlb.annotations.Tag;
33 import org.apache.axis.utils.XMLChar;
34 import org.apache.commons.httpclient.HttpClient;
35 import org.apache.commons.httpclient.HttpMethod;
36 import org.apache.commons.httpclient.UsernamePasswordCredentials;
37 import org.apache.commons.httpclient.auth.AuthScope;
38 import org.apache.commons.httpclient.methods.GetMethod;
39 import org.apache.commons.httpclient.methods.PostMethod;
40 import org.jdom.Element;
41 import org.jdom.JDOMException;
42 import org.jdom.input.SAXBuilder;
43 import org.jetbrains.annotations.NotNull;
44 import org.jetbrains.annotations.Nullable;
45 import org.jetbrains.annotations.TestOnly;
46
47 import javax.swing.*;
48 import java.io.InputStream;
49 import java.io.StringReader;
50 import java.util.Date;
51 import java.util.List;
52 import java.util.Set;
53
54 /**
55  * @author Dmitry Avdeev
56  */
57 @Tag("YouTrack")
58 public class YouTrackRepository extends BaseRepositoryImpl {
59
60   private String myDefaultSearch = "Assignee: me sort by: updated #Unresolved";
61
62   /**
63    * for serialization
64    */
65   @SuppressWarnings({"UnusedDeclaration"})
66   public YouTrackRepository() {
67   }
68
69   public YouTrackRepository(TaskRepositoryType type) {
70     super(type);
71   }
72
73   @NotNull
74   @Override
75   public BaseRepository clone() {
76     return new YouTrackRepository(this);
77   }
78
79   private YouTrackRepository(YouTrackRepository other) {
80     super(other);
81     myDefaultSearch = other.getDefaultSearch();
82   }
83
84   public Task[] getIssues(@Nullable String request, int max, long since) throws Exception {
85
86     String query = getDefaultSearch();
87     if (StringUtil.isNotEmpty(request)) {
88       query += " " + request;
89     }
90     String requestUrl = "/rest/project/issues/?filter=" + encodeUrl(query) + "&max=" + max + "&updatedAfter" + since;
91     HttpMethod method = doREST(requestUrl, false);
92     try {
93       InputStream stream = method.getResponseBodyAsStream();
94
95       // todo workaround for http://youtrack.jetbrains.net/issue/JT-7984
96       String s = StreamUtil.readText(stream, CharsetToolkit.UTF8_CHARSET);
97       for (int i = 0; i < s.length(); i++) {
98         if (!XMLChar.isValid(s.charAt(i))) {
99           s = s.replace(s.charAt(i), ' ');
100         }
101       }
102
103       Element element;
104       try {
105         //InputSource source = new InputSource(stream);
106         //source.setEncoding("UTF-8");
107         //element = new SAXBuilder(false).build(source).getRootElement();
108         element = new SAXBuilder(false).build(new StringReader(s)).getRootElement();
109       }
110       catch (JDOMException e) {
111         LOG.error("Can't parse YouTrack response for " + requestUrl, e);
112         throw e;
113       }
114       if ("error".equals(element.getName())) {
115         throw new Exception("Error from YouTrack for " + requestUrl + ": '" + element.getText() + "'");
116       }
117
118       List<Element> children = element.getChildren("issue");
119
120       final List<Task> tasks = ContainerUtil.mapNotNull(children, new NullableFunction<Element, Task>() {
121         public Task fun(Element o) {
122           return createIssue(o);
123         }
124       });
125       return tasks.toArray(new Task[tasks.size()]);
126     }
127     finally {
128       method.releaseConnection();
129     }
130   }
131
132   @Nullable
133   @Override
134   public CancellableConnection createCancellableConnection() {
135     PostMethod method = new PostMethod(getUrl() + "/rest/user/login");
136     return new HttpTestConnection<PostMethod>(method) {
137       @Override
138       protected void doTest(PostMethod method) throws Exception {
139         login(method);
140       }
141     };
142   }
143
144   private HttpClient login(PostMethod method) throws Exception {
145     HttpClient client = getHttpClient();
146     client.getState().setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(getUsername(), getPassword()));
147     configureHttpMethod(method);
148     method.addParameter("login", getUsername());
149     method.addParameter("password", getPassword());
150     client.getParams().setContentCharset("UTF-8");
151     client.executeMethod(method);
152     String response;
153     try {
154       if (method.getStatusCode() != 200) {
155         throw new Exception("Cannot login: HTTP status code " + method.getStatusCode());
156       }
157       response = method.getResponseBodyAsString(1000);
158     }
159     finally {
160       method.releaseConnection();
161     }
162     if (response == null) {
163       throw new NullPointerException();
164     }
165     if (!response.contains("<login>ok</login>")) {
166       int pos = response.indexOf("</error>");
167       int length = "<error>".length();
168       if (pos > length) {
169         response = response.substring(length, pos);
170       }
171       throw new Exception("Cannot login: " + response);
172     }
173     return client;
174   }
175
176   @Nullable
177   public Task findTask(@NotNull String id) throws Exception {
178     final Element element = fetchRequestAsElement(id);
179     return element.getName().equals("issue") ? createIssue(element) : null;
180   }
181
182   @TestOnly
183   @NotNull
184   public Element fetchRequestAsElement(@NotNull String id) throws Exception {
185     final HttpMethod method = doREST("/rest/issue/byid/" + id, false);
186     try {
187       final InputStream stream = method.getResponseBodyAsStream();
188       return new SAXBuilder(false).build(stream).getRootElement();
189     }
190     finally {
191       method.releaseConnection();
192     }
193   }
194
195
196   HttpMethod doREST(String request, boolean post) throws Exception {
197     HttpClient client = login(new PostMethod(getUrl() + "/rest/user/login"));
198     String uri = getUrl() + request;
199     HttpMethod method = post ? new PostMethod(uri) : new GetMethod(uri);
200     configureHttpMethod(method);
201     int status = client.executeMethod(method);
202     if (status == 400) {
203       InputStream string = method.getResponseBodyAsStream();
204       Element element = new SAXBuilder(false).build(string).getRootElement();
205       TaskUtil.prettyFormatXmlToLog(LOG, element);
206       if ("error".equals(element.getName())) {
207         throw new Exception(element.getText());
208       }
209     }
210     return method;
211   }
212
213   @Override
214   public void setTaskState(@NotNull Task task, @NotNull CustomTaskState state) throws Exception {
215     doREST("/rest/issue/execute/" + task.getId() + "?command=" + encodeUrl("state " + state.getId()), true).releaseConnection();
216   }
217
218   @NotNull
219   @Override
220   public Set<CustomTaskState> getAvailableTaskStates(@NotNull Task task) throws Exception {
221     final HttpMethod method = doREST("/rest/issue/" + task.getId() + "/execute/intellisense?command=" + encodeUrl("state "), false);
222     try {
223       final InputStream stream = method.getResponseBodyAsStream();
224       final Element element = new SAXBuilder(false).build(stream).getRootElement();
225       return ContainerUtil.map2Set(element.getChild("suggest").getChildren("item"), new Function<Element, CustomTaskState>() {
226         @Override
227         public CustomTaskState fun(Element element) {
228           final String stateName = element.getChildText("option");
229           return new CustomTaskState(stateName, stateName);
230         }
231       });
232     }
233     finally {
234       method.releaseConnection();
235     }
236   }
237
238   @Nullable
239   private Task createIssue(Element element) {
240     final String id = element.getAttributeValue("id");
241     if (id == null) return null;
242     final String summary = element.getAttributeValue("summary");
243     if (summary == null) return null;
244     final String description = element.getAttributeValue("description");
245
246     String type = element.getAttributeValue("type");
247     TaskType taskType = TaskType.OTHER;
248     if (type != null) {
249       try {
250         taskType = TaskType.valueOf(type.toUpperCase());
251       }
252       catch (IllegalArgumentException e) {
253         // do nothing
254       }
255     }
256     final TaskType finalTaskType = taskType;
257
258     final Date updated = new Date(Long.parseLong(element.getAttributeValue("updated")));
259     final Date created = new Date(Long.parseLong(element.getAttributeValue("created")));
260     final boolean resolved = element.getAttribute("resolved") != null;
261
262     return new Task() {
263       @Override
264       public boolean isIssue() {
265         return true;
266       }
267
268       @Override
269       public String getIssueUrl() {
270         return getUrl() + "/issue/" + getId();
271       }
272
273       @NotNull
274       @Override
275       public String getId() {
276         return id;
277       }
278
279       @NotNull
280       @Override
281       public String getSummary() {
282         return summary;
283       }
284
285       public String getDescription() {
286         return description;
287       }
288
289       @NotNull
290       @Override
291       public Comment[] getComments() {
292         return Comment.EMPTY_ARRAY;
293       }
294
295       @NotNull
296       @Override
297       public Icon getIcon() {
298         return LocalTaskImpl.getIconFromType(getType(), isIssue());
299       }
300
301       @NotNull
302       @Override
303       public TaskType getType() {
304         return finalTaskType;
305       }
306
307       @Nullable
308       @Override
309       public Date getUpdated() {
310         return updated;
311       }
312
313       @Nullable
314       @Override
315       public Date getCreated() {
316         return created;
317       }
318
319       @Override
320       public boolean isClosed() {
321         // IDEA-118605
322         return resolved;
323       }
324
325       @Override
326       public TaskRepository getRepository() {
327         return YouTrackRepository.this;
328       }
329     };
330   }
331
332   public String getDefaultSearch() {
333     return myDefaultSearch;
334   }
335
336   public void setDefaultSearch(String defaultSearch) {
337     if (defaultSearch != null) {
338       myDefaultSearch = defaultSearch;
339     }
340   }
341
342   @SuppressWarnings({"EqualsWhichDoesntCheckParameterClass"})
343   @Override
344   public boolean equals(Object o) {
345     if (!super.equals(o)) return false;
346     YouTrackRepository repository = (YouTrackRepository)o;
347     return Comparing.equal(repository.getDefaultSearch(), getDefaultSearch());
348   }
349
350   private static final Logger LOG = Logger.getInstance("#com.intellij.tasks.youtrack.YouTrackRepository");
351
352   @Override
353   public void updateTimeSpent(@NotNull LocalTask task, @NotNull String timeSpent, @NotNull String comment) throws Exception {
354     checkVersion();
355     String command = encodeUrl(String.format("work Today %s %s", timeSpent, comment));
356     final HttpMethod method = doREST("/rest/issue/execute/" + task.getId() + "?command=" + command, true);
357     try {
358       if (method.getStatusCode() != 200) {
359         InputStream stream = method.getResponseBodyAsStream();
360         String message = new SAXBuilder(false).build(stream).getRootElement().getText();
361         throw new Exception(message);
362       }
363     }
364     finally {
365       method.releaseConnection();
366     }
367   }
368
369   private void checkVersion() throws Exception {
370     HttpMethod method = doREST("/rest/workflow/version", false);
371     try {
372       InputStream stream = method.getResponseBodyAsStream();
373       Element element = new SAXBuilder(false).build(stream).getRootElement();
374       final boolean timeTrackingAvailable = element.getName().equals("version") && VersionComparatorUtil.compare(element.getChildText("version"), "4.1") >= 0;
375       if (!timeTrackingAvailable) {
376         throw new Exception("Time tracking is not supported in this version of Youtrack");
377       }
378     }
379     finally {
380       method.releaseConnection();
381     }
382   }
383
384   @Override
385   protected int getFeatures() {
386     return super.getFeatures() | TIME_MANAGEMENT | STATE_UPDATING;
387   }
388 }