blob: a4b10386aeba03f1cbd7d96149f83130c129529c [file] [log] [blame]
package com.intellij.tasks.redmine;
import com.google.gson.Gson;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.tasks.Task;
import com.intellij.tasks.impl.RequestFailedException;
import com.intellij.tasks.impl.gson.GsonUtil;
import com.intellij.tasks.impl.httpclient.NewBaseRepositoryImpl;
import com.intellij.tasks.redmine.model.RedmineIssue;
import com.intellij.tasks.redmine.model.RedmineProject;
import com.intellij.util.Function;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.xmlb.annotations.Tag;
import com.intellij.util.xmlb.annotations.Transient;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.StatusLine;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.utils.URIBuilder;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Pattern;
import static com.intellij.tasks.impl.httpclient.ResponseUtil.GsonSingleObjectDeserializer;
import static com.intellij.tasks.redmine.model.RedmineResponseWrapper.*;
/**
* @author Mikhail Golubev
* @author Dennis.Ushakov
*/
@Tag("Redmine")
public class RedmineRepository extends NewBaseRepositoryImpl {
private static final Logger LOG = Logger.getInstance(RedmineRepository.class);
private static final Gson GSON = GsonUtil.createDefaultBuilder().create();
private static final Pattern ID_PATTERN = Pattern.compile("\\d+");
public static final RedmineProject UNSPECIFIED_PROJECT = new RedmineProject() {
@NotNull
@Override
public String getName() {
return "-- from all projects --";
}
@Nullable
@Override
public String getIdentifier() {
return getName();
}
@Override
public int getId() {
return -1;
}
};
private String myAPIKey = "";
private RedmineProject myCurrentProject;
private List<RedmineProject> myProjects = null;
/**
* Serialization constructor
*/
@SuppressWarnings({"UnusedDeclaration"})
public RedmineRepository() {
// empty
}
/**
* Normal instantiation constructor
*/
public RedmineRepository(RedmineRepositoryType type) {
super(type);
setUseHttpAuthentication(true);
}
/**
* Cloning constructor
*/
public RedmineRepository(RedmineRepository other) {
super(other);
setAPIKey(other.myAPIKey);
setCurrentProject(other.getCurrentProject());
}
@Override
public boolean equals(Object o) {
if (!super.equals(o)) return false;
if (!(o instanceof RedmineRepository)) return false;
RedmineRepository that = (RedmineRepository)o;
if (!Comparing.equal(getAPIKey(), that.getAPIKey())) return false;
if (!Comparing.equal(getCurrentProject(), that.getCurrentProject())) return false;
return true;
}
@NotNull
@Override
public RedmineRepository clone() {
return new RedmineRepository(this);
}
@Nullable
@Override
public CancellableConnection createCancellableConnection() {
return new NewBaseRepositoryImpl.HttpTestConnection(new HttpGet()) {
@Override
protected void test() throws Exception {
// Strangely, Redmine doesn't return 401 or 403 error codes, if client sent wrong credentials, and instead
// merely returns empty array of issues with status code of 200. This means that we should attempt to fetch
// something more specific than issues to test proper configuration, e.g. current user information at
// /users/current.json. Unfortunately this endpoint may be unavailable on some old servers (see IDEA-122845)
// and in this case we have to come back to requesting issues in this case to test anything at all.
URIBuilder uriBuilder = new URIBuilder(getRestApiUrl("users", "current.json"));
if (isUseApiKeyAuthentication()) {
uriBuilder.addParameter("key", getAPIKey());
}
myCurrentRequest.setURI(uriBuilder.build());
HttpClient client = getHttpClient();
HttpResponse httpResponse = client.execute(myCurrentRequest);
StatusLine statusLine = httpResponse.getStatusLine();
if (statusLine != null && statusLine.getStatusCode() == HttpStatus.SC_NOT_FOUND) {
myCurrentRequest = new HttpGet(getIssuesUrl(0, 1, true));
statusLine = client.execute(myCurrentRequest).getStatusLine();
}
if (statusLine != null && statusLine.getStatusCode() != HttpStatus.SC_OK) {
throw RequestFailedException.forStatusCode(statusLine.getStatusCode(), statusLine.getReasonPhrase());
}
}
};
}
@Override
public Task[] getIssues(@Nullable String query, int offset, int limit, boolean withClosed) throws Exception {
List<RedmineIssue> issues = fetchIssues(query, offset, limit, withClosed);
return ContainerUtil.map2Array(issues, RedmineTask.class, new Function<RedmineIssue, RedmineTask>() {
@Override
public RedmineTask fun(RedmineIssue issue) {
return new RedmineTask(RedmineRepository.this, issue);
}
});
}
public List<RedmineIssue> fetchIssues(String query, int offset, int limit, boolean withClosed) throws Exception {
ensureProjectsDiscovered();
// Legacy API, can't find proper documentation
//if (StringUtil.isNotEmpty(query)) {
// builder.addParameter("fields[]", "subject").addParameter("operators[subject]", "~").addParameter("values[subject][]", query);
//}
// If project was not chosen, all available issues still fetched. Such behavior may seems strange to user.
//if (myCurrentProject != null && myCurrentProject != UNSPECIFIED_PROJECT) {
// builder.addParameter("project_id", String.valueOf(myCurrentProject.getId()));
//}
HttpClient client = getHttpClient();
HttpGet method = new HttpGet(getIssuesUrl(offset, limit, withClosed));
IssuesWrapper wrapper = client.execute(method, new GsonSingleObjectDeserializer<IssuesWrapper>(GSON, IssuesWrapper.class));
return wrapper == null ? Collections.<RedmineIssue>emptyList() : wrapper.getIssues();
}
private URI getIssuesUrl(int offset, int limit, boolean withClosed) throws URISyntaxException {
URIBuilder builder = new URIBuilder(getRestApiUrl("issues.json"))
.addParameter("offset", String.valueOf(offset))
.addParameter("limit", String.valueOf(limit))
.addParameter("status_id", withClosed ? "*" : "open")
.addParameter("assigned_to_id", "me");
if (isUseApiKeyAuthentication()) {
builder.addParameter("key", myAPIKey);
}
return builder.build();
}
public List<RedmineProject> fetchProjects() throws Exception {
HttpClient client = getHttpClient();
// Download projects with pagination (IDEA-125056, IDEA-125157)
List<RedmineProject> allProjects = new ArrayList<RedmineProject>();
int offset = 0;
ProjectsWrapper wrapper;
do {
URIBuilder builder = new URIBuilder(getRestApiUrl("projects.json"));
builder.addParameter("offset", String.valueOf(offset));
builder.addParameter("limit", "50");
if (isUseApiKeyAuthentication()) {
builder.addParameter("key", myAPIKey);
}
HttpGet method = new HttpGet(builder.toString());
wrapper = client.execute(method, new GsonSingleObjectDeserializer<ProjectsWrapper>(GSON, ProjectsWrapper.class));
offset += wrapper.getProjects().size();
allProjects.addAll(wrapper.getProjects());
}
while (wrapper.getTotalCount() > allProjects.size() || wrapper.getProjects().isEmpty());
myProjects = allProjects;
return Collections.unmodifiableList(myProjects);
}
@Nullable
@Override
public Task findTask(@NotNull String id) throws Exception {
ensureProjectsDiscovered();
HttpGet method = new HttpGet(getRestApiUrl("issues", id + ".json"));
IssueWrapper wrapper = getHttpClient().execute(method, new GsonSingleObjectDeserializer<IssueWrapper>(GSON, IssueWrapper.class, true));
if (wrapper == null) {
return null;
}
return new RedmineTask(this, wrapper.getIssue());
}
public String getAPIKey() {
return myAPIKey;
}
public void setAPIKey(String APIKey) {
myAPIKey = APIKey;
}
private boolean isUseApiKeyAuthentication() {
return !isUseHttpAuthentication() && StringUtil.isNotEmpty(myAPIKey);
}
@Override
public String getPresentableName() {
String name = super.getPresentableName();
if (myCurrentProject != null && myCurrentProject != UNSPECIFIED_PROJECT) {
name += "/projects/" + myCurrentProject.getIdentifier();
}
return name;
}
@Override
public boolean isConfigured() {
if (!super.isConfigured()) return false;
if (isUseHttpAuthentication()) {
return StringUtil.isNotEmpty(myPassword) && StringUtil.isNotEmpty(myUsername);
}
return StringUtil.isNotEmpty(myAPIKey);
}
@Nullable
@Override
public String extractId(@NotNull String taskName) {
return ID_PATTERN.matcher(taskName).matches() ? taskName : null;
}
@Override
protected int getFeatures() {
return super.getFeatures() & ~NATIVE_SEARCH | BASIC_HTTP_AUTHORIZATION;
}
@Nullable
public RedmineProject getCurrentProject() {
return myCurrentProject;
}
public void setCurrentProject(@Nullable RedmineProject project) {
myCurrentProject = project != null && project.getId() == -1 ? UNSPECIFIED_PROJECT : project;
}
@NotNull
public List<RedmineProject> getProjects() {
try {
ensureProjectsDiscovered();
}
catch (Exception ignored) {
return Collections.emptyList();
}
return Collections.unmodifiableList(myProjects);
}
private void ensureProjectsDiscovered() throws Exception {
if (myProjects == null) {
fetchProjects();
}
}
@TestOnly
@Transient
public void setProjects(@NotNull List<RedmineProject> projects) {
myProjects = projects;
}
}