2 * Copyright 2000-2014 JetBrains s.r.o.
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
16 package com.intellij.tasks.youtrack;
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;
48 import java.io.InputStream;
49 import java.io.StringReader;
50 import java.util.Date;
51 import java.util.List;
55 * @author Dmitry Avdeev
58 public class YouTrackRepository extends BaseRepositoryImpl {
60 private String myDefaultSearch = "Assignee: me sort by: updated #Unresolved";
65 @SuppressWarnings({"UnusedDeclaration"})
66 public YouTrackRepository() {
69 public YouTrackRepository(TaskRepositoryType type) {
75 public BaseRepository clone() {
76 return new YouTrackRepository(this);
79 private YouTrackRepository(YouTrackRepository other) {
81 myDefaultSearch = other.getDefaultSearch();
84 public Task[] getIssues(@Nullable String request, int max, long since) throws Exception {
86 String query = getDefaultSearch();
87 if (StringUtil.isNotEmpty(request)) {
88 query += " " + request;
90 String requestUrl = "/rest/project/issues/?filter=" + encodeUrl(query) + "&max=" + max + "&updatedAfter" + since;
91 HttpMethod method = doREST(requestUrl, false);
93 InputStream stream = method.getResponseBodyAsStream();
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), ' ');
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();
110 catch (JDOMException e) {
111 LOG.error("Can't parse YouTrack response for " + requestUrl, e);
114 if ("error".equals(element.getName())) {
115 throw new Exception("Error from YouTrack for " + requestUrl + ": '" + element.getText() + "'");
118 List<Element> children = element.getChildren("issue");
120 final List<Task> tasks = ContainerUtil.mapNotNull(children, new NullableFunction<Element, Task>() {
121 public Task fun(Element o) {
122 return createIssue(o);
125 return tasks.toArray(new Task[tasks.size()]);
128 method.releaseConnection();
134 public CancellableConnection createCancellableConnection() {
135 PostMethod method = new PostMethod(getUrl() + "/rest/user/login");
136 return new HttpTestConnection<PostMethod>(method) {
138 protected void doTest(PostMethod method) throws Exception {
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);
154 if (method.getStatusCode() != 200) {
155 throw new Exception("Cannot login: HTTP status code " + method.getStatusCode());
157 response = method.getResponseBodyAsString(1000);
160 method.releaseConnection();
162 if (response == null) {
163 throw new NullPointerException();
165 if (!response.contains("<login>ok</login>")) {
166 int pos = response.indexOf("</error>");
167 int length = "<error>".length();
169 response = response.substring(length, pos);
171 throw new Exception("Cannot login: " + response);
177 public Task findTask(@NotNull String id) throws Exception {
178 final Element element = fetchRequestAsElement(id);
179 return element.getName().equals("issue") ? createIssue(element) : null;
184 public Element fetchRequestAsElement(@NotNull String id) throws Exception {
185 final HttpMethod method = doREST("/rest/issue/byid/" + id, false);
187 final InputStream stream = method.getResponseBodyAsStream();
188 return new SAXBuilder(false).build(stream).getRootElement();
191 method.releaseConnection();
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);
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());
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();
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);
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>() {
227 public CustomTaskState fun(Element element) {
228 final String stateName = element.getChildText("option");
229 return new CustomTaskState(stateName, stateName);
234 method.releaseConnection();
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");
246 String type = element.getAttributeValue("type");
247 TaskType taskType = TaskType.OTHER;
250 taskType = TaskType.valueOf(type.toUpperCase());
252 catch (IllegalArgumentException e) {
256 final TaskType finalTaskType = taskType;
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;
264 public boolean isIssue() {
269 public String getIssueUrl() {
270 return getUrl() + "/issue/" + getId();
275 public String getId() {
281 public String getSummary() {
285 public String getDescription() {
291 public Comment[] getComments() {
292 return Comment.EMPTY_ARRAY;
297 public Icon getIcon() {
298 return LocalTaskImpl.getIconFromType(getType(), isIssue());
303 public TaskType getType() {
304 return finalTaskType;
309 public Date getUpdated() {
315 public Date getCreated() {
320 public boolean isClosed() {
326 public TaskRepository getRepository() {
327 return YouTrackRepository.this;
332 public String getDefaultSearch() {
333 return myDefaultSearch;
336 public void setDefaultSearch(String defaultSearch) {
337 if (defaultSearch != null) {
338 myDefaultSearch = defaultSearch;
342 @SuppressWarnings({"EqualsWhichDoesntCheckParameterClass"})
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());
350 private static final Logger LOG = Logger.getInstance("#com.intellij.tasks.youtrack.YouTrackRepository");
353 public void updateTimeSpent(@NotNull LocalTask task, @NotNull String timeSpent, @NotNull String comment) throws Exception {
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);
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);
365 method.releaseConnection();
369 private void checkVersion() throws Exception {
370 HttpMethod method = doREST("/rest/workflow/version", false);
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");
380 method.releaseConnection();
385 protected int getFeatures() {
386 return super.getFeatures() | TIME_MANAGEMENT | STATE_UPDATING;