IDEA-111299 Task management: JIRA: NPE at JiraIssue$Fields.access$1000() on calling code completion in Open Task

* Return compatibility with JIRA 4.x.x (REST API v2.0.alpha1)
* Exceptions thrown in JiraRepository may contain meaningful error messages from server
This commit is contained in:
Mikhail Golubev
2013-08-01 20:35:54 +04:00
parent 476c8ad3ce
commit 14e4e1b11e
15 changed files with 742 additions and 185 deletions

View File

@@ -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<JiraIssue> issues = wrapper.getIssues();
LOG.debug("Total " + issues.size() + " issues downloaded");
List<JiraIssue> issues = myRestApiVersion.findIssues(jqlQuery, max);
return ContainerUtil.map2Array(issues, Task.class, new Function<JiraIssue, Task>() {
@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<GetMethod>(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;
}
}

View File

@@ -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<JiraIssue> findIssues(String jql, int max) throws Exception {
GetMethod method = getMultipleIssuesSearchMethod(jql, max);
String response = myRepository.executeMethod(method);
List<JiraIssue> 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<JiraIssue> 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();
}

View File

@@ -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();
}

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -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<JiraComment> getComments() {
return fields.comment == null ? ContainerUtil.<JiraComment>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<JiraComment> getComments();
@NotNull
public abstract JiraStatus getStatus();
}

View File

@@ -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;
}
}

View File

@@ -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<JiraIssue> 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<T extends JiraIssue> extends JiraResponseWrapper {
private List<T> issues = ContainerUtil.emptyList();
@NotNull
public List<JiraIssue> getIssues() {
public List<T> getIssues() {
return issues;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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<JiraComment> getComments() {
return fields.comment == null ? ContainerUtil.<JiraComment>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;
}
}

View File

@@ -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<JiraResponseWrapper.Issues<JiraIssueApi2>>() { /* 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<JiraIssue> parseIssues(String response) {
JiraResponseWrapper.Issues<JiraIssueApi2> wrapper = JiraUtil.GSON.fromJson(response, ISSUES_WRAPPER_TYPE);
return new ArrayList<JiraIssue>(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";
}
}

View File

@@ -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<JiraComment> getComments() {
return fields.comment.getValue();
}
@NotNull
@Override
public JiraStatus getStatus() {
return fields.status.getValue();
}
public static class FieldWrapper<T> {
/**
* Serialization constructor
*/
public FieldWrapper() {
}
public FieldWrapper(T value) {
this.value = value;
}
T value;
public T getValue() {
return value;
}
}
public static class Fields {
private FieldWrapper<JiraUser> reporter;
private FieldWrapper<JiraUser> assignee;
private FieldWrapper<String > summary;
private FieldWrapper<String> description;
private FieldWrapper<Date> created;
private FieldWrapper<Date> updated;
private FieldWrapper<Date> resolutiondate;
private FieldWrapper<Date> duedate;
private FieldWrapper<JiraStatus> status;
private FieldWrapper<JiraIssueType> issuetype;
private FieldWrapper<List<JiraComment>> 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<JiraIssueType>(issueType);
}
}

View File

@@ -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<JiraResponseWrapper.Issues<JiraIssueApi20Alpha1>>() { /* 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<JiraIssue> parseIssues(String response) {
JiraResponseWrapper.Issues<JiraIssueApi20Alpha1> wrapper = JiraUtil.GSON.fromJson(response, ISSUES_WRAPPER_TYPE);
List<JiraIssueApi20Alpha1> incompleteIssues = wrapper.getIssues();
List<JiraIssue> updatedIssues = new ArrayList<JiraIssue>();
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";
}
}

View File

@@ -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();