blob: f583be334cfce9dbf93083cc687dc1b3f1c69ad5 [file] [log] [blame]
package com.intellij.tasks.bugzilla;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.Version;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.tasks.LocalTask;
import com.intellij.tasks.Task;
import com.intellij.tasks.TaskRepositoryType;
import com.intellij.tasks.TaskState;
import com.intellij.tasks.impl.BaseRepository;
import com.intellij.tasks.impl.BaseRepositoryImpl;
import com.intellij.tasks.impl.RequestFailedException;
import com.intellij.util.Function;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.HashMap;
import com.intellij.util.xmlb.annotations.Tag;
import org.apache.xmlrpc.CommonsXmlRpcTransport;
import org.apache.xmlrpc.XmlRpcClient;
import org.apache.xmlrpc.XmlRpcException;
import org.apache.xmlrpc.XmlRpcRequest;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @author Mikhail Golubev
*/
@SuppressWarnings({"UseOfObsoleteCollectionType", "unchecked"})
@Tag("Bugzilla")
public class BugzillaRepository extends BaseRepositoryImpl {
private static final Logger LOG = Logger.getInstance(BugzillaRepository.class);
// Copied from SendTimeTrackingInformationDialog
public static final Pattern TIME_SPENT_PATTERN = Pattern.compile("([0-9]+)d ([0-9]+)h ([0-9]+)m");
private Version myVersion;
private boolean myAuthenticated;
private String myAuthenticationToken;
private String myProductName = "";
private String myComponentName = "";
/**
* Serialization constructor
*/
@SuppressWarnings("UnusedDeclaration")
public BugzillaRepository() {
// empty
}
/**
* Normal instantiation constructor
*/
public BugzillaRepository(TaskRepositoryType type) {
super(type);
setUseHttpAuthentication(false);
setUrl("http://myserver.com/xmlrpc.cgi");
}
/**
* Cloning constructor
*/
public BugzillaRepository(BugzillaRepository other) {
super(other);
myProductName = other.myProductName;
myComponentName = other.myComponentName;
}
@NotNull
@Override
public BaseRepository clone() {
return new BugzillaRepository(this);
}
@Override
public Task[] getIssues(@Nullable String query, int offset, int limit, boolean withClosed, @NotNull ProgressIndicator cancelled)
throws Exception {
// Method search appeared in Bugzilla 3.4, ensureVersionDiscovered() checks minimal
// supported version requirement.
Hashtable<String, Object> response = createIssueSearchRequest(query, offset, limit, withClosed).execute();
Vector<Hashtable<String, Object>> bugs = (Vector<Hashtable<String, Object>>)response.get("bugs");
return ContainerUtil.map2Array(bugs, BugzillaTask.class, new Function<Hashtable<String, Object>, BugzillaTask>() {
@Override
public BugzillaTask fun(Hashtable<String, Object> hashTable) {
return new BugzillaTask(hashTable, BugzillaRepository.this);
}
});
}
private BugzillaXmlRpcRequest createIssueSearchRequest(String query, int offset, int limit, boolean withClosed) throws Exception {
// Method search appeared in Bugzilla 3.4, ensureVersionDiscovered() checks minimal
// supported version requirement.
return new BugzillaXmlRpcRequest("Bug.search")
.requireAuthentication(true)
.withParameter("summary", StringUtil.isNotEmpty(query) ? newVector(query.split("\\s+")) : null)
.withParameter("product", StringUtil.nullize(myProductName))
// Bugzilla's API allows to specify component even without parental project
.withParameter("component", StringUtil.nullize(myComponentName))
.withParameter("offset", offset)
.withParameter("limit", limit)
.withParameter("assigned_to", getUsername())
.withParameter("resolution", !withClosed ? "" : null);
}
@Nullable
@Override
public Task findTask(@NotNull String id) throws Exception {
Hashtable<String, Object> response;
try {
// In Bugzilla 3.0 this method is called "get_bugs".
response = new BugzillaXmlRpcRequest("Bug.get").requireAuthentication(true).withParameter("ids", newVector(id)).execute();
}
catch (XmlRpcException e) {
if (e.code == 101 && e.getMessage().contains("does not exist")) {
return null;
}
throw e;
}
Vector<Hashtable<String, Object>> bugs = (Vector<Hashtable<String, Object>>)response.get("bugs");
if (bugs == null || bugs.isEmpty()) {
return null;
}
return new BugzillaTask(bugs.get(0), this);
}
private void ensureVersionDiscovered() throws Exception {
if (myVersion == null) {
Hashtable<String, Object> result = new BugzillaXmlRpcRequest("Bugzilla.version").execute();
String version = (String)result.get("version");
String[] parts = version.split("\\.", 3);
myVersion = new Version(Integer.parseInt(parts[0]), Integer.parseInt(parts[1]), Integer.parseInt(parts[2]));
if (myVersion.lessThan(3, 4)) {
throw new RequestFailedException("Bugzilla before 3.4 is not supported");
}
}
}
private void ensureUserAuthenticated() throws Exception {
ensureVersionDiscovered();
if (!myAuthenticated) {
Hashtable<String, Object> response = new BugzillaXmlRpcRequest("User.login")
.withParameter("login", getUsername())
.withParameter("password", getPassword())
.execute();
myAuthenticated = true;
// Not available in Bugzilla before 4.4
myAuthenticationToken = (String)response.get("token");
}
}
@Override
public void setTaskState(@NotNull Task task, @NotNull TaskState state) throws Exception {
BugzillaXmlRpcRequest request = new BugzillaXmlRpcRequest("Bug.update")
.requireAuthentication(true)
.withParameter("ids", newVector(task.getId()));
switch (state) {
case IN_PROGRESS:
request.withParameter("status", "IN_PROGRESS");
break;
case RESOLVED:
request.withParameter("status", "RESOLVED").withParameter("resolution", "FIXED");
break;
default:
return;
}
request.execute();
}
@Override
public void updateTimeSpent(@NotNull LocalTask task, @NotNull String timeSpent, @NotNull String comment) throws Exception {
LOG.debug(String.format("Last post: %s, time spent from last: %s, time spent: %s",
task.getLastPost(), task.getTimeSpentFromLastPost(), timeSpent));
Matcher matcher = TIME_SPENT_PATTERN.matcher(timeSpent);
if (matcher.find()) {
int days = Integer.valueOf(matcher.group(1));
int hours = Integer.valueOf(matcher.group(2));
int minutes = Integer.valueOf(matcher.group(3));
BugzillaXmlRpcRequest request = new BugzillaXmlRpcRequest("Bug.update")
.requireAuthentication(true)
.withParameter("ids", newVector(task.getId()))
// the number of hours worked on the bug as double
.withParameter("work_time", days * 24 + hours + minutes / 60.0);
if (!StringUtil.isEmptyOrSpaces(comment)) {
request.withParameter("comment", newHashTable("body", comment, "is_private", false));
}
request.execute();
} else {
LOG.error("Illegal time spent format: " + timeSpent);
}
}
/**
* @return pair where first element is list of project names and second is list of component names (will be empty for Bugzilla < 4.2)
*/
@NotNull
public Pair<List<String>, List<String>> fetchProductAndComponentNames() throws Exception {
Hashtable<String, Vector<Integer>> productIdsResponse = new BugzillaXmlRpcRequest("Product.get_selectable_products")
.requireAuthentication(true)
.execute();
Hashtable<String, Object> productInfoResponse = new BugzillaXmlRpcRequest("Product.get")
.requireAuthentication(true)
.withParameter("ids", productIdsResponse.get("ids"))
.execute();
List<String> productNames = new ArrayList<String>();
List<String> componentNames = new ArrayList<String>();
for (Hashtable<String, Object> info : (Vector<Hashtable<String, Object>>)productInfoResponse.get("products")) {
productNames.add((String)info.get("name"));
if (myVersion != null && myVersion.isOrGreaterThan(4, 2)) {
for (Hashtable<String, Object> component : (Vector<Hashtable<String, Object>>)info.get("components")) {
componentNames.add((String)component.get("name"));
}
}
}
return Pair.create(productNames, componentNames);
}
@Nullable
@Override
public CancellableConnection createCancellableConnection() {
return new CancellableConnection() {
BugzillaXmlRpcRequest myRequest;
@Override
protected void doTest() throws Exception {
// Reset information about server.
myVersion = null;
myAuthenticated = false;
myAuthenticationToken = null;
myRequest = createIssueSearchRequest(null, 0, 1, true);
myRequest.execute();
}
@Override
public void cancel() {
myRequest.cancel();
}
};
}
private static <T> Vector<T> newVector(T... elements) {
return new Vector<T>(Arrays.asList(elements));
}
private static <K, V> Hashtable<K, V> newHashTable(Object... pairs) {
assert pairs.length % 2 == 0;
Hashtable<K, V> table = new Hashtable<K, V>();
for (int i = 0; i < pairs.length; i += 2) {
// Null values are not allowed, because Bugzilla reacts unexpectedly on them.
if (pairs[i + 1] != null) {
table.put((K)pairs[i], (V)pairs[i + 1]);
}
}
return table;
}
@Nullable
@Override
public String extractId(@NotNull String taskName) {
String id = taskName.trim();
return id.matches("\\d+") ? id : null;
}
@Override
public boolean isConfigured() {
return super.isConfigured() && StringUtil.isNotEmpty(getUsername()) && StringUtil.isNotEmpty(getPassword());
}
@Override
protected int getFeatures() {
int features = super.getFeatures();
// Status and work time updates are available through Bug.update method which is available only in Bugzilla 4.0+
if (myVersion != null && myVersion.isOrGreaterThan(4, 0)) {
return features | STATE_UPDATING | TIME_MANAGEMENT;
}
return features;
}
private class BugzillaXmlRpcRequest {
// Copied from Trac repository
private class Transport extends CommonsXmlRpcTransport {
public Transport() throws MalformedURLException {
super(new URL(getUrl()), getHttpClient());
}
public void cancel() {
method.abort();
}
}
private final String myMethodName;
private boolean myRequireAuthentication;
private final HashMap<String, Object> myParameters = new HashMap<String, Object>();
private final Transport myTransport;
public BugzillaXmlRpcRequest(@NotNull String methodName) throws MalformedURLException {
myMethodName = methodName;
myTransport = new Transport();
}
public BugzillaXmlRpcRequest withParameter(@NotNull String name, @Nullable Object value) {
if (value != null) {
myParameters.put(name, value);
}
return this;
}
public BugzillaXmlRpcRequest requireAuthentication(boolean require) {
myRequireAuthentication = require;
return this;
}
public void cancel() {
myTransport.cancel();
}
public <T> T execute() throws Exception {
if (myRequireAuthentication) {
ensureUserAuthenticated();
// Bugzilla [3.0, 4.4) uses cookies authentication.
// Bugzilla [3.6, ...) allows to send login ("Bugzilla_login") and password ("Bugzilla_password")
// with every requests for automatic authentication (not used here).
// Bugzilla [4.4, ...) also allows to send token ("Bugzilla_token") returned by call to User.login
// with any request to its API.
if (myVersion.isOrGreaterThan(4, 4) && myAuthenticationToken != null) {
myParameters.put("Bugzilla_token", myAuthenticationToken);
}
}
Vector<Hashtable<String, Object>> parameters = new Vector<Hashtable<String, Object>>();
parameters.add(new Hashtable<String, Object>(myParameters));
return (T)new XmlRpcClient(getUrl()).execute(new XmlRpcRequest(myMethodName, parameters), myTransport);
}
}
@Override
public boolean equals(Object o) {
if (!super.equals(o)) return false;
if (!(o instanceof BugzillaRepository)) return false;
BugzillaRepository repository = (BugzillaRepository)o;
if (!Comparing.equal(myProductName, repository.getProductName())) return false;
if (!Comparing.equal(myComponentName, repository.getComponentName())) return false;
return true;
}
@NotNull
public String getProductName() {
return myProductName;
}
public void setProductName(@NotNull String productName) {
myProductName = productName;
}
@NotNull
public String getComponentName() {
return myComponentName;
}
public void setComponentName(@NotNull String componentName) {
myComponentName = componentName;
}
}