mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-03-22 06:50:54 +07:00
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:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user