diff --git a/plugins/tasks/tasks-core/src/com/intellij/tasks/jira/JiraRepository.java b/plugins/tasks/tasks-core/src/com/intellij/tasks/jira/JiraRepository.java index d386ec76dd95..a23b0369ef65 100644 --- a/plugins/tasks/tasks-core/src/com/intellij/tasks/jira/JiraRepository.java +++ b/plugins/tasks/tasks-core/src/com/intellij/tasks/jira/JiraRepository.java @@ -1,5 +1,6 @@ package com.intellij.tasks.jira; +import com.google.gson.JsonObject; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.util.Comparing; import com.intellij.openapi.util.io.StreamUtil; @@ -7,14 +8,14 @@ import com.intellij.openapi.util.text.StringUtil; import com.intellij.tasks.Task; import com.intellij.tasks.impl.BaseRepositoryImpl; import com.intellij.tasks.jira.model.JiraIssue; -import com.intellij.tasks.jira.model.JiraResponseWrapper; import com.intellij.util.Function; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.xmlb.annotations.Tag; -import org.apache.commons.httpclient.HttpClient; +import org.apache.commons.httpclient.Header; +import org.apache.commons.httpclient.HttpMethod; import org.apache.commons.httpclient.HttpStatus; -import org.apache.commons.httpclient.NameValuePair; import org.apache.commons.httpclient.methods.GetMethod; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.List; @@ -29,8 +30,13 @@ public class JiraRepository extends BaseRepositoryImpl { public static final String LOGIN_FAILED_CHECK_YOUR_PERMISSIONS = "Login failed. Check your permissions."; public static final String REST_API_PATH_SUFFIX = "/rest/api/latest"; + /** + * Default JQL query + */ private String mySearchQuery = "assignee = currentUser() order by duedate"; + private JiraRestApi myRestApiVersion; + /** * Serialization constructor */ @@ -64,8 +70,9 @@ public class JiraRepository extends BaseRepositoryImpl { } public Task[] getIssues(@Nullable String searchQuery, int max, long since) throws Exception { - HttpClient client = getHttpClient(); - GetMethod method = new GetMethod(getUrl() + REST_API_PATH_SUFFIX + "/search"); + if (myRestApiVersion == null) { + myRestApiVersion = discoverRestApiVersion(); + } String jqlQuery = mySearchQuery; if (!StringUtil.isEmpty(searchQuery)) { if (JiraUtil.ANY_ISSUE_KEY_REGEX.matcher(searchQuery).matches()) { @@ -75,24 +82,7 @@ public class JiraRepository extends BaseRepositoryImpl { jqlQuery += String.format(" and summary ~ \"%s\"", searchQuery); } } - method.setQueryString(new NameValuePair[]{ - new NameValuePair("jql", jqlQuery), - // by default comment field will be skipped - //new NameValuePair("fields", "*all"), - new NameValuePair("fields", JiraIssue.REQUIRED_RESPONSE_FIELDS), - new NameValuePair("maxResults", String.valueOf(max)) - }); - LOG.debug("URI is " + method.getURI()); - int statusCode = client.executeMethod(method); - LOG.debug("Status code is " + statusCode); - String entityContent = StreamUtil.readText(method.getResponseBodyAsStream(), "utf-8"); - LOG.debug(entityContent); - if (statusCode != HttpStatus.SC_OK) { - return Task.EMPTY_ARRAY; - } - JiraResponseWrapper.Issues wrapper = JiraUtil.GSON.fromJson(entityContent, JiraResponseWrapper.Issues.class); - List issues = wrapper.getIssues(); - LOG.debug("Total " + issues.size() + " issues downloaded"); + List issues = myRestApiVersion.findIssues(jqlQuery, max); return ContainerUtil.map2Array(issues, Task.class, new Function() { @Override public JiraTask fun(JiraIssue issue) { @@ -105,34 +95,21 @@ public class JiraRepository extends BaseRepositoryImpl { @Nullable @Override public Task findTask(String id) throws Exception { - HttpClient client = getHttpClient(); - GetMethod method = new GetMethod(getUrl() + REST_API_PATH_SUFFIX + "/issue/" + id); - method.setQueryString("fields=" + encodeUrl(JiraIssue.REQUIRED_RESPONSE_FIELDS)); - int statusCode = client.executeMethod(method); - LOG.debug("Status code is " + statusCode); - String entityContent = StreamUtil.readText(method.getResponseBodyAsStream(), "utf-8"); - LOG.debug(entityContent); - if (statusCode != HttpStatus.SC_OK) { - return null; + if (myRestApiVersion == null) { + myRestApiVersion = discoverRestApiVersion(); } - return new JiraTask(JiraUtil.GSON.fromJson(entityContent, JiraIssue.class), this); + JiraIssue issue = myRestApiVersion.findIssue(id); + return issue == null? null : new JiraTask(issue, this); } @Nullable @Override public CancellableConnection createCancellableConnection() { - String uri = getUrl() + REST_API_PATH_SUFFIX + "/search?maxResults=1"; + String uri = getUrl() + REST_API_PATH_SUFFIX + "/search?maxResults=1&jql=" + encodeUrl(mySearchQuery); return new HttpTestConnection(new GetMethod(uri)) { @Override public void doTest(GetMethod method) throws Exception { - HttpClient client = getHttpClient(); - int statusCode = client.executeMethod(myMethod); - if (statusCode == HttpStatus.SC_UNAUTHORIZED) { - throw new Exception(LOGIN_FAILED_CHECK_YOUR_PERMISSIONS); - } - else if (statusCode != HttpStatus.SC_OK) { - throw new Exception("Error while connecting to server: " + HttpStatus.getStatusText(statusCode)); - } + executeMethod(method); } }; } @@ -146,7 +123,6 @@ public class JiraRepository extends BaseRepositoryImpl { return TIME_MANAGEMENT; } - public String getSearchQuery() { return mySearchQuery; } @@ -154,4 +130,51 @@ public class JiraRepository extends BaseRepositoryImpl { public void setSearchQuery(String searchQuery) { mySearchQuery = searchQuery; } + + @NotNull + public JiraRestApi discoverRestApiVersion() throws Exception { + String responseBody; + try { + responseBody = executeMethod(new GetMethod(getUrl() + REST_API_PATH_SUFFIX + "/serverInfo")); + } + catch (Exception e) { + LOG.warn("Can't find out JIRA REST API version"); + throw e; + } + JsonObject object = JiraUtil.GSON.fromJson(responseBody, JsonObject.class); + return JiraRestApi.fromJiraVersion(object.get("version").getAsString(), this); + } + + @NotNull + public String executeMethod(HttpMethod method) throws Exception { + LOG.debug("URI is " + method.getURI()); + int statusCode; + String entityContent; + try { + statusCode = getHttpClient().executeMethod(method); + LOG.debug("Status code is " + statusCode); + entityContent = StreamUtil.readText(method.getResponseBodyAsStream(), "utf-8"); + LOG.debug(entityContent); + } + finally { + method.releaseConnection(); + } + if (statusCode == HttpStatus.SC_UNAUTHORIZED) { + throw new Exception(LOGIN_FAILED_CHECK_YOUR_PERMISSIONS); + } + else if (statusCode != HttpStatus.SC_OK) { + String reason = method.getStatusText(); + Header contentType = method.getResponseHeader("Content-Type"); + if (contentType.getValue().startsWith("application/json")) { + JsonObject object = JiraUtil.GSON.fromJson(entityContent, JsonObject.class); + if (object.has("errorMessages")) { + reason = StringUtil.join(object.getAsJsonArray("errorMessages"), " "); + // something meaningful to user, e.g. invalid field name in JQL query + LOG.warn(reason); + } + } + throw new Exception("Request failed with error: " + reason); + } + return entityContent; + } } diff --git a/plugins/tasks/tasks-core/src/com/intellij/tasks/jira/JiraRestApi.java b/plugins/tasks/tasks-core/src/com/intellij/tasks/jira/JiraRestApi.java new file mode 100644 index 000000000000..329dace071ce --- /dev/null +++ b/plugins/tasks/tasks-core/src/com/intellij/tasks/jira/JiraRestApi.java @@ -0,0 +1,86 @@ +package com.intellij.tasks.jira; + +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.tasks.jira.model.JiraIssue; +import com.intellij.tasks.jira.model.api2.JiraRestApi2; +import com.intellij.tasks.jira.model.api20alpha1.JiraRestApi20Alpha1; +import org.apache.commons.httpclient.NameValuePair; +import org.apache.commons.httpclient.methods.GetMethod; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +/** + * @author Mikhail Golubev + */ +public abstract class JiraRestApi { + private static final Logger LOG = Logger.getInstance(JiraRestApi.class); + protected final JiraRepository myRepository; + + public static JiraRestApi fromJiraVersion(@NotNull JiraVersion jiraVersion, @NotNull JiraRepository repository) { + LOG.debug("JIRA version is " + jiraVersion); + if (jiraVersion.getMajorNumber() == 4) { + return new JiraRestApi20Alpha1(repository); + } + else if (jiraVersion.getMajorNumber() >= 5) { + return new JiraRestApi2(repository); + } + else { + LOG.warn("JIRA below 4.0.0 doesn't support REST API (" + jiraVersion + " used)"); + return null; + } + } + + public static JiraRestApi fromJiraVersion(@NotNull String version, @NotNull JiraRepository repository) { + return fromJiraVersion(new JiraVersion(version), repository); + } + + protected JiraRestApi(JiraRepository repository) { + myRepository = repository; + } + + @NotNull + public final List findIssues(String jql, int max) throws Exception { + GetMethod method = getMultipleIssuesSearchMethod(jql, max); + String response = myRepository.executeMethod(method); + List issues = parseIssues(response); + LOG.debug("Total " + issues.size() + " downloaded"); + return issues; + } + + @Nullable + public final JiraIssue findIssue(String key) throws Exception { + GetMethod method = getSingleIssueSearchMethod(key); + return parseIssue(myRepository.executeMethod(method)); + } + + @NotNull + protected GetMethod getSingleIssueSearchMethod(String key) { + return new GetMethod(myRepository.getUrl() + JiraRepository.REST_API_PATH_SUFFIX + "/issue/" + key); + } + + @NotNull + protected GetMethod getMultipleIssuesSearchMethod(String jql, int max) { + GetMethod method = new GetMethod(myRepository.getUrl() + JiraRepository.REST_API_PATH_SUFFIX + "/search"); + method.setQueryString(new NameValuePair[]{ + new NameValuePair("jql", jql), + new NameValuePair("maxResults", String.valueOf(max)) + }); + return method; + } + + @NotNull + protected abstract List parseIssues(String response); + + @Nullable + protected abstract JiraIssue parseIssue(String response); + + @Override + public String toString() { + return String.format("JiraRestAPI(%s)", getVersionName()); + } + + @NotNull + public abstract String getVersionName(); +} diff --git a/plugins/tasks/tasks-core/src/com/intellij/tasks/jira/JiraTask.java b/plugins/tasks/tasks-core/src/com/intellij/tasks/jira/JiraTask.java index cbab1c877fa5..8c078875ad6a 100644 --- a/plugins/tasks/tasks-core/src/com/intellij/tasks/jira/JiraTask.java +++ b/plugins/tasks/tasks-core/src/com/intellij/tasks/jira/JiraTask.java @@ -33,7 +33,7 @@ import java.util.Date; /** * @author Dmitry Avdeev */ -class JiraTask extends Task { +public class JiraTask extends Task { private final JiraIssue myJiraIssue; private final TaskRepository myRepository; @@ -72,6 +72,7 @@ class JiraTask extends Task { public Icon getIcon() { JiraIssueType issueType = myJiraIssue.getIssueType(); String iconUrl = issueType.getIconUrl(); + // iconUrl will be null in JIRA versions prior 5.x.x final Icon icon = iconUrl == null ? TasksIcons.JIRA : isClosed() ? CachedIconLoader.getDisabledIcon(iconUrl) : CachedIconLoader.getIcon(iconUrl); @@ -138,7 +139,6 @@ class JiraTask extends Task { @Override public String getIssueUrl() { - //return myJiraIssue.getIssueUrl(); return myRepository.getUrl() + "/browse/" + myJiraIssue.getKey(); } diff --git a/plugins/tasks/tasks-core/src/com/intellij/tasks/jira/JiraVersion.java b/plugins/tasks/tasks-core/src/com/intellij/tasks/jira/JiraVersion.java new file mode 100644 index 000000000000..41e69820c183 --- /dev/null +++ b/plugins/tasks/tasks-core/src/com/intellij/tasks/jira/JiraVersion.java @@ -0,0 +1,57 @@ +package com.intellij.tasks.jira; + + +import org.jetbrains.annotations.NotNull; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * @author Mikhail Golubev + */ +public class JiraVersion { + private static final Pattern VERSION_PATTERN = Pattern.compile("(\\d+)(?:\\.(\\d+))?(?:\\.(\\d+))?"); + + private final int myMajorNumber, myMinorNumber, myBuildNumber; + + public JiraVersion(int majorNumber) { + this(majorNumber, 0, 0); + } + + public JiraVersion(int majorNumber, int minorNumber) { + this(majorNumber, minorNumber, 0); + } + + public JiraVersion(int majorNumber, int minorNumber, int buildNumber) { + myMajorNumber = majorNumber; + myMinorNumber = minorNumber; + myBuildNumber = buildNumber; + } + + public JiraVersion(@NotNull String version) { + Matcher m = VERSION_PATTERN.matcher(version); + if (!m.matches()) { + throw new IllegalArgumentException("Illegal JIRA version number: " + version); + } + myMajorNumber = m.group(1) == null ? 0 : Integer.parseInt(m.group(1)); + myMinorNumber = m.group(2) == null ? 0 : Integer.parseInt(m.group(2)); + myBuildNumber = m.group(3) == null ? 0 : Integer.parseInt(m.group(3)); + } + + public int getMajorNumber() { + return myMajorNumber; + } + + public int getMinorNumber() { + return myMinorNumber; + } + + public int getBuildNumber() { + return myBuildNumber; + } + + @Override + public String toString() { + return String.format("%d.%d.%d", myMajorNumber, myMinorNumber, myBuildNumber); + } +} diff --git a/plugins/tasks/tasks-core/src/com/intellij/tasks/jira/model/JiraComment.java b/plugins/tasks/tasks-core/src/com/intellij/tasks/jira/model/JiraComment.java index 196c49d5b401..df2e37b30ceb 100644 --- a/plugins/tasks/tasks-core/src/com/intellij/tasks/jira/model/JiraComment.java +++ b/plugins/tasks/tasks-core/src/com/intellij/tasks/jira/model/JiraComment.java @@ -15,6 +15,8 @@ */ package com.intellij.tasks.jira.model; +import org.jetbrains.annotations.NotNull; + import java.util.Date; /** @@ -23,36 +25,37 @@ import java.util.Date; public class JiraComment { private JiraUser author; private JiraUser updateAuthor; - private Date update; + private Date updated; private Date created; - private String id; private String self; private String body; + @NotNull public JiraUser getAuthor() { return author; } + @NotNull public JiraUser getUpdateAuthor() { return updateAuthor; } - public Date getUpdate() { - return update; + @NotNull + public Date getUpdated() { + return updated; } + @NotNull public Date getCreated() { return created; } - public String getId() { - return id; - } - - public String getSelf() { + @NotNull + public String getCommentUrl() { return self; } + @NotNull public String getBody() { return body; } diff --git a/plugins/tasks/tasks-core/src/com/intellij/tasks/jira/model/JiraIssue.java b/plugins/tasks/tasks-core/src/com/intellij/tasks/jira/model/JiraIssue.java index 30e21a48f6b9..f987d3526836 100644 --- a/plugins/tasks/tasks-core/src/com/intellij/tasks/jira/model/JiraIssue.java +++ b/plugins/tasks/tasks-core/src/com/intellij/tasks/jira/model/JiraIssue.java @@ -1,21 +1,5 @@ -/* - * Copyright 2000-2013 JetBrains s.r.o. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ package com.intellij.tasks.jira.model; -import com.intellij.util.containers.ContainerUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -25,107 +9,47 @@ import java.util.List; /** * @author Mikhail Golubev */ -public class JiraIssue { - /** - * JIRA by default will return enormous amount of fields for every task. - * "fields" query parameter may be used for filtering however - */ - public static final String REQUIRED_RESPONSE_FIELDS = "id,key,summary,description," + - "created,updated,duedate,resolutiondate" + - "assignee,reporter,issuetype,comment,status"; - - private String id; - private String key; - private String self; - private Fields fields; - - @Override +public abstract class JiraIssue { public String toString() { - return String.format("JiraIssue(id=%s, summary=%s)", id, fields.summary); + return String.format("JiraIssue(key=%s, summary=%s)", getKey(), getSummary()); } @NotNull - public String getId() { - return id; - } + public abstract String getKey(); @NotNull - public String getKey() { - return key; - } + public abstract String getIssueUrl(); @NotNull - public String getIssueUrl() { - return self; - } - - @NotNull - public String getSummary() { - return fields.summary; - } - - @NotNull - public String getDescription() { - return fields.description; - } - - @NotNull - public Date getCreated() { - return fields.created; - } - - @NotNull - public Date getUpdated() { - return fields.updated; - } + public abstract String getSummary(); @Nullable - public Date getResolutionDate() { - return fields.resolutiondate; - } - - @Nullable - public Date getDueDate() { - return fields.duedate; - } + public abstract String getDescription(); @NotNull - public JiraIssueType getIssueType() { - return fields.issuetype; - } - - @Nullable - public JiraUser getAssignee() { - return fields.assignee; - } - - @Nullable - public JiraUser getReporter() { - return fields.reporter; - } + public abstract Date getCreated(); @NotNull - public List getComments() { - return fields.comment == null ? ContainerUtil.emptyList() : fields.comment.getComments(); - } + public abstract Date getUpdated(); - public JiraStatus getStatus() { - return fields.status; - } + @Nullable + public abstract Date getResolutionDate(); - public static class Fields { - private String summary; - private String description; - private Date created; - private Date updated; - private Date resolutiondate; - private Date duedate; - private JiraResponseWrapper.Comments comment; + @Nullable + public abstract Date getDueDate(); - private JiraUser assignee; - private JiraUser reporter; + @NotNull + public abstract JiraIssueType getIssueType(); - private JiraIssueType issuetype; - private JiraStatus status; - } + @Nullable + public abstract JiraUser getAssignee(); + + @Nullable + public abstract JiraUser getReporter(); + + @NotNull + public abstract List getComments(); + + @NotNull + public abstract JiraStatus getStatus(); } diff --git a/plugins/tasks/tasks-core/src/com/intellij/tasks/jira/model/JiraIssueType.java b/plugins/tasks/tasks-core/src/com/intellij/tasks/jira/model/JiraIssueType.java index 9bf12da6156a..0c4dfb19348a 100644 --- a/plugins/tasks/tasks-core/src/com/intellij/tasks/jira/model/JiraIssueType.java +++ b/plugins/tasks/tasks-core/src/com/intellij/tasks/jira/model/JiraIssueType.java @@ -15,27 +15,23 @@ */ package com.intellij.tasks.jira.model; +import org.jetbrains.annotations.Nullable; + /** * @author Mikhail Golubev */ public class JiraIssueType { - private String id; private String self; private String name; private String description; private String iconUrl; - private boolean subtask; @Override public String toString() { return String.format("JiraIssueType(name=%s)", name); } - public String getId() { - return id; - } - public String getIssueTypeUrl() { return self; } @@ -44,15 +40,21 @@ public class JiraIssueType { return name; } - public String getDescription() { - return description; - } - + /** + * Will be available in JIRA > 5.x.x and omitted in earlier releases + * due to REST API differences. + */ + @Nullable public String getIconUrl() { return iconUrl; } - public boolean isSubtask() { - return subtask; + /** + * Will be available in JIRA > 5.x.x and omitted in earlier releases + * due to REST API differences. + */ + @Nullable + public String getDescription() { + return description; } } diff --git a/plugins/tasks/tasks-core/src/com/intellij/tasks/jira/model/JiraResponseWrapper.java b/plugins/tasks/tasks-core/src/com/intellij/tasks/jira/model/JiraResponseWrapper.java index ab308c203a94..9280070a2403 100644 --- a/plugins/tasks/tasks-core/src/com/intellij/tasks/jira/model/JiraResponseWrapper.java +++ b/plugins/tasks/tasks-core/src/com/intellij/tasks/jira/model/JiraResponseWrapper.java @@ -15,6 +15,7 @@ */ package com.intellij.tasks.jira.model; +import com.intellij.tasks.jira.model.api2.JiraIssueApi2; import com.intellij.util.containers.ContainerUtil; import org.jetbrains.annotations.NotNull; @@ -43,11 +44,15 @@ public abstract class JiraResponseWrapper { return total; } - public static class Issues extends JiraResponseWrapper { - private List issues = ContainerUtil.emptyList(); + /** + * JSON representation of issue differs dramatically between REST API 2.0 and 2.0alpha1, + * that's why this wrapper was generified and JiraIssue extracted to abstract class + */ + public static class Issues extends JiraResponseWrapper { + private List issues = ContainerUtil.emptyList(); @NotNull - public List getIssues() { + public List getIssues() { return issues; } } diff --git a/plugins/tasks/tasks-core/src/com/intellij/tasks/jira/model/JiraStatus.java b/plugins/tasks/tasks-core/src/com/intellij/tasks/jira/model/JiraStatus.java index e36dc196c1cd..83d049357806 100644 --- a/plugins/tasks/tasks-core/src/com/intellij/tasks/jira/model/JiraStatus.java +++ b/plugins/tasks/tasks-core/src/com/intellij/tasks/jira/model/JiraStatus.java @@ -15,10 +15,16 @@ */ package com.intellij.tasks.jira.model; +import org.jetbrains.annotations.NotNull; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + /** * @author Mikhail Golubev */ public class JiraStatus { + private static final Pattern ID_PATTERN = Pattern.compile(".*/(\\d+)/?$"); private String id; private String self; private String name; @@ -29,19 +35,29 @@ public class JiraStatus { return String.format("JiraStatus(name=%s)", name); } + /** + * Status id is necessary to determine issue status regardless of the language + * used in JIRA installation. However it omitted in case of REST API version 2.0.alpha1. + * Anyway it still may be extracted from status URL which always presents. + */ + @NotNull public String getId() { + if (id == null) { + Matcher m = ID_PATTERN.matcher(self); + if (m.matches()) { + return m.group(1); + } + } return id; } - public String getSelf() { + @NotNull + public String getStatusUrl() { return self; } + @NotNull public String getName() { return name; } - - public String getDescription() { - return description; - } } diff --git a/plugins/tasks/tasks-core/src/com/intellij/tasks/jira/model/JiraUser.java b/plugins/tasks/tasks-core/src/com/intellij/tasks/jira/model/JiraUser.java index 4122ae9622ce..9046b2a8e39a 100644 --- a/plugins/tasks/tasks-core/src/com/intellij/tasks/jira/model/JiraUser.java +++ b/plugins/tasks/tasks-core/src/com/intellij/tasks/jira/model/JiraUser.java @@ -15,12 +15,12 @@ */ package com.intellij.tasks.jira.model; +import org.jetbrains.annotations.NotNull; + /** * @author Mikhail Golubev */ public class JiraUser { - private String emailAddress; - private boolean active; private String name, displayName; private String self; @@ -29,22 +29,17 @@ public class JiraUser { return String.format("JiraUser(name=%s)", name); } - public String getEmailAddress() { - return emailAddress; - } - - public boolean isActive() { - return active; - } - + @NotNull public String getName() { return name; } + @NotNull public String getDisplayName() { return displayName; } + @NotNull public String getUserUrl() { return self; } diff --git a/plugins/tasks/tasks-core/src/com/intellij/tasks/jira/model/api2/JiraIssueApi2.java b/plugins/tasks/tasks-core/src/com/intellij/tasks/jira/model/api2/JiraIssueApi2.java new file mode 100644 index 000000000000..12adfdfae1a5 --- /dev/null +++ b/plugins/tasks/tasks-core/src/com/intellij/tasks/jira/model/api2/JiraIssueApi2.java @@ -0,0 +1,141 @@ +/* + * Copyright 2000-2013 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.tasks.jira.model.api2; + +import com.intellij.tasks.jira.model.*; +import com.intellij.util.containers.ContainerUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Date; +import java.util.List; + +/** + * @author Mikhail Golubev + */ +public class JiraIssueApi2 extends JiraIssue { + /** + * JIRA by default will return enormous amount of fields for every task. + * "fields" query parameter may be used for filtering however + */ + public static final String REQUIRED_RESPONSE_FIELDS = "id,key,summary,description," + + "created,updated,duedate,resolutiondate," + + "assignee,reporter,issuetype,comment,status"; + + private String id; + private String key; + private String self; + private Fields fields; + + @Override + public String toString() { + return String.format("JiraIssue(id=%s, summary=%s)", id, fields.summary); + } + + @NotNull + @Override + public String getKey() { + return key; + } + + @NotNull + @Override + public String getIssueUrl() { + return self; + } + + @NotNull + @Override + public String getSummary() { + return fields.summary; + } + + @Nullable + @Override + public String getDescription() { + return fields.description; + } + + @NotNull + @Override + public Date getCreated() { + return fields.created; + } + + @NotNull + @Override + public Date getUpdated() { + return fields.updated; + } + + @Nullable + @Override + public Date getResolutionDate() { + return fields.resolutiondate; + } + + @Nullable + @Override + public Date getDueDate() { + return fields.duedate; + } + + @NotNull + @Override + public JiraIssueType getIssueType() { + return fields.issuetype; + } + + @Nullable + @Override + public JiraUser getAssignee() { + return fields.assignee; + } + + @Nullable + @Override + public JiraUser getReporter() { + return fields.reporter; + } + + @NotNull + @Override + public List getComments() { + return fields.comment == null ? ContainerUtil.emptyList() : fields.comment.getComments(); + } + + @NotNull + @Override + public JiraStatus getStatus() { + return fields.status; + } + + public static class Fields { + private String summary; + private String description; + private Date created; + private Date updated; + private Date resolutiondate; + private Date duedate; + private JiraResponseWrapper.Comments comment; + + private JiraUser assignee; + private JiraUser reporter; + + private JiraIssueType issuetype; + private JiraStatus status; + } +} diff --git a/plugins/tasks/tasks-core/src/com/intellij/tasks/jira/model/api2/JiraRestApi2.java b/plugins/tasks/tasks-core/src/com/intellij/tasks/jira/model/api2/JiraRestApi2.java new file mode 100644 index 000000000000..918f97275685 --- /dev/null +++ b/plugins/tasks/tasks-core/src/com/intellij/tasks/jira/model/api2/JiraRestApi2.java @@ -0,0 +1,63 @@ +package com.intellij.tasks.jira.model.api2; + +import com.google.gson.reflect.TypeToken; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.tasks.jira.JiraRepository; +import com.intellij.tasks.jira.JiraRestApi; +import com.intellij.tasks.jira.JiraUtil; +import com.intellij.tasks.jira.model.JiraIssue; +import com.intellij.tasks.jira.model.JiraResponseWrapper; +import org.apache.commons.httpclient.methods.GetMethod; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; + +/** + * This REST API version is used in JIRA 5.1.8 and above (including JIRA 6.x.x). + * @author Mikhail Golubev + */ +public class JiraRestApi2 extends JiraRestApi { + private static final Logger LOG = Logger.getInstance(JiraIssueApi2.class); + private static final Type ISSUES_WRAPPER_TYPE = new TypeToken>() { /* empty */ }.getType(); + public JiraRestApi2(JiraRepository repository) { + super(repository); + } + + @NotNull + @Override + protected GetMethod getMultipleIssuesSearchMethod(String jql, int max) { + GetMethod method = super.getMultipleIssuesSearchMethod(jql, max); + method.setQueryString(method.getQueryString() + "&fields=" + JiraIssueApi2.REQUIRED_RESPONSE_FIELDS); + return method; + } + + @NotNull + @Override + protected List parseIssues(String response) { + JiraResponseWrapper.Issues wrapper = JiraUtil.GSON.fromJson(response, ISSUES_WRAPPER_TYPE); + return new ArrayList(wrapper.getIssues()); + } + + @NotNull + @Override + protected GetMethod getSingleIssueSearchMethod(String key) { + GetMethod method = super.getSingleIssueSearchMethod(key); + method.setQueryString(method.getQueryString() + "&fields=" + JiraIssueApi2.REQUIRED_RESPONSE_FIELDS); + return method; + } + + @Nullable + @Override + protected JiraIssue parseIssue(String response) { + return JiraUtil.GSON.fromJson(response, JiraIssueApi2.class); + } + + @NotNull + @Override + public String getVersionName() { + return "2.0"; + } +} diff --git a/plugins/tasks/tasks-core/src/com/intellij/tasks/jira/model/api20alpha1/JiraIssueApi20Alpha1.java b/plugins/tasks/tasks-core/src/com/intellij/tasks/jira/model/api20alpha1/JiraIssueApi20Alpha1.java new file mode 100644 index 000000000000..1c4f5f9ce0de --- /dev/null +++ b/plugins/tasks/tasks-core/src/com/intellij/tasks/jira/model/api20alpha1/JiraIssueApi20Alpha1.java @@ -0,0 +1,136 @@ +package com.intellij.tasks.jira.model.api20alpha1; + +import com.intellij.tasks.jira.model.*; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Date; +import java.util.List; + +/** + * @author Mikhail Golubev + */ +public class JiraIssueApi20Alpha1 extends JiraIssue { + private Fields fields; + private String self; + private String key; + + + @NotNull + @Override + public String getKey() { + return key; + } + + @NotNull + @Override + public String getIssueUrl() { + return self; + } + + @NotNull + @Override + public String getSummary() { + return fields.summary.getValue(); + } + + @Nullable + @Override + public String getDescription() { + return fields.description.getValue(); + } + + @NotNull + @Override + public Date getCreated() { + return fields.created.getValue(); + } + + @NotNull + @Override + public Date getUpdated() { + return fields.updated.getValue(); + } + + @Nullable + @Override + public Date getResolutionDate() { + return fields.resolutiondate.getValue(); + } + + @Nullable + @Override + public Date getDueDate() { + return fields.duedate.getValue(); + } + + @NotNull + @Override + public JiraIssueType getIssueType() { + return fields.issuetype.getValue(); + } + + @Nullable + @Override + public JiraUser getAssignee() { + return fields.assignee.getValue(); + } + + @Nullable + @Override + public JiraUser getReporter() { + return fields.reporter.getValue(); + } + + @NotNull + @Override + public List getComments() { + return fields.comment.getValue(); + } + + @NotNull + @Override + public JiraStatus getStatus() { + return fields.status.getValue(); + } + + public static class FieldWrapper { + /** + * Serialization constructor + */ + public FieldWrapper() { + } + + public FieldWrapper(T value) { + this.value = value; + } + + T value; + + public T getValue() { + return value; + } + } + + public static class Fields { + private FieldWrapper reporter; + private FieldWrapper assignee; + private FieldWrapper summary; + private FieldWrapper description; + private FieldWrapper created; + private FieldWrapper updated; + private FieldWrapper resolutiondate; + private FieldWrapper duedate; + private FieldWrapper status; + private FieldWrapper issuetype; + private FieldWrapper> comment; + } + + /** + * Downloaded separately because of iconUrl field, not included in server response in + * REST API 2.0.alpha1 + */ + public void setIssueType(JiraIssueType issueType) { + fields.issuetype = new FieldWrapper(issueType); + } +} diff --git a/plugins/tasks/tasks-core/src/com/intellij/tasks/jira/model/api20alpha1/JiraRestApi20Alpha1.java b/plugins/tasks/tasks-core/src/com/intellij/tasks/jira/model/api20alpha1/JiraRestApi20Alpha1.java new file mode 100644 index 000000000000..9eb6376facdb --- /dev/null +++ b/plugins/tasks/tasks-core/src/com/intellij/tasks/jira/model/api20alpha1/JiraRestApi20Alpha1.java @@ -0,0 +1,56 @@ +package com.intellij.tasks.jira.model.api20alpha1; + +import com.google.gson.reflect.TypeToken; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.tasks.jira.JiraRepository; +import com.intellij.tasks.jira.JiraRestApi; +import com.intellij.tasks.jira.JiraUtil; +import com.intellij.tasks.jira.model.JiraIssue; +import com.intellij.tasks.jira.model.JiraResponseWrapper; +import org.jetbrains.annotations.NotNull; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; + +/** + * This REST API versions is used in JIRA 4.3.4 and 4.4.1. + * @author Mikhail Golubev + */ +public class JiraRestApi20Alpha1 extends JiraRestApi { + private static final Logger LOG = Logger.getInstance(JiraRestApi20Alpha1.class); + private static final Type ISSUES_WRAPPER_TYPE = new TypeToken>() { /* empty */ + }.getType(); + + public JiraRestApi20Alpha1(JiraRepository repository) { + super(repository); + } + + @Override + protected JiraIssue parseIssue(String response) { + return JiraUtil.GSON.fromJson(response, JiraIssueApi20Alpha1.class); + } + + @NotNull + @Override + protected List parseIssues(String response) { + JiraResponseWrapper.Issues wrapper = JiraUtil.GSON.fromJson(response, ISSUES_WRAPPER_TYPE); + List incompleteIssues = wrapper.getIssues(); + List updatedIssues = new ArrayList(); + for (JiraIssueApi20Alpha1 issue : incompleteIssues) { + try { + updatedIssues.add(findIssue(issue.getKey())); + } + catch (Exception e) { + LOG.warn("Can't fetch detailed info about issue: " + issue); + } + } + return updatedIssues; + } + + @NotNull + @Override + public String getVersionName() { + return "2.0.alpha1"; + } +} diff --git a/plugins/tasks/tasks-tests/test/com/intellij/tasks/integration/JiraIntegrationTest.java b/plugins/tasks/tasks-tests/test/com/intellij/tasks/integration/JiraIntegrationTest.java index a03906966b01..1937958758c9 100644 --- a/plugins/tasks/tasks-tests/test/com/intellij/tasks/integration/JiraIntegrationTest.java +++ b/plugins/tasks/tasks-tests/test/com/intellij/tasks/integration/JiraIntegrationTest.java @@ -46,6 +46,56 @@ public class JiraIntegrationTest extends TaskManagerTestCase { assertEquals(JiraRepository.LOGIN_FAILED_CHECK_YOUR_PERMISSIONS, exception.getMessage()); } + /** + * JIRA 5.0.6, REST API 2.0 + */ + public void testVersionDiscovery1() throws Exception { + myRepository.setUrl("http://trackers-tests.labs.intellij.net:8015"); + myRepository.setUsername("deva"); + myRepository.setPassword("deva"); + assertEquals("2.0", myRepository.discoverRestApiVersion().getVersionName()); + } + + /** + * JIRA 4.4.5, REST API 2.0.alpha1 + */ + public void testVersionDiscovery2() throws Exception { + myRepository.setUrl("http://trackers-tests.labs.intellij.net:8014"); + myRepository.setUsername("deva"); + myRepository.setPassword("deva"); + assertEquals("2.0.alpha1", myRepository.discoverRestApiVersion().getVersionName()); + } + + public void testJqlQuery() throws Exception { + myRepository.setUsername("deva"); + myRepository.setPassword("deva"); + myRepository.setSearchQuery("assignee = currentUser() AND project = PRJONE"); + assertEquals(5, myRepository.getIssues("", 50, 0).length); + } + + /** + * Holds only for JIRA > 5.x.x + */ + public void testExtractedErrorMessage() throws Exception { + myRepository.setUsername("deva"); + myRepository.setPassword("deva"); + myRepository.setSearchQuery("foo < bar"); + try { + myRepository.getIssues("", 50, 0); + fail(); + } + catch (Exception e) { + assertEquals("Request failed with error: \"Field 'foo' does not exist or you do not have permission to view it.\"", e.getMessage()); + } + } + + public void testEmptyQuerySelectsAllIssues() throws Exception { + myRepository.setUsername("deva"); + myRepository.setPassword("deva"); + myRepository.setSearchQuery(""); + assertEquals(13, myRepository.getIssues("", 50, 0).length); + } + @Override public void setUp() throws Exception { super.setUp();