diff --git a/plugins/tasks/tasks-core/src/META-INF/plugin.xml b/plugins/tasks/tasks-core/src/META-INF/plugin.xml index 44c5fd3904ed..dd1d123199be 100644 --- a/plugins/tasks/tasks-core/src/META-INF/plugin.xml +++ b/plugins/tasks/tasks-core/src/META-INF/plugin.xml @@ -112,6 +112,7 @@ + diff --git a/plugins/tasks/tasks-core/src/com/intellij/tasks/fogbugz/FogBugzRepository.java b/plugins/tasks/tasks-core/src/com/intellij/tasks/fogbugz/FogBugzRepository.java new file mode 100644 index 000000000000..0ca29d3f1b6e --- /dev/null +++ b/plugins/tasks/tasks-core/src/com/intellij/tasks/fogbugz/FogBugzRepository.java @@ -0,0 +1,266 @@ +/* + * Copyright 2000-2012 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.fogbugz; + +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.tasks.Comment; +import com.intellij.tasks.Task; +import com.intellij.tasks.TaskRepositoryType; +import com.intellij.tasks.TaskType; +import com.intellij.tasks.impl.BaseRepository; +import com.intellij.tasks.impl.BaseRepositoryImpl; +import com.intellij.tasks.impl.LocalTaskImpl; +import com.intellij.util.NotNullFunction; +import com.intellij.util.containers.ContainerUtil; +import com.intellij.util.xmlb.annotations.Tag; +import org.apache.commons.httpclient.HttpClient; +import org.apache.commons.httpclient.methods.PostMethod; +import org.jdom.Document; +import org.jdom.Element; +import org.jdom.input.SAXBuilder; +import org.jdom.xpath.XPath; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.xml.datatype.DatatypeConfigurationException; +import javax.xml.datatype.DatatypeFactory; +import java.util.Date; +import java.util.List; + +/** + * @author mkennedy + */ +@Tag("FogBugz") +public class FogBugzRepository extends BaseRepositoryImpl { + + private static final Logger LOG = Logger.getInstance("#com.intellij.tasks.fogbugz.FogBugzRepository"); + private transient String token; + + public FogBugzRepository(TaskRepositoryType type) { + super(type); + setUrl("https://example.fogbugz.com"); + } + + private FogBugzRepository(FogBugzRepository other) { + super(other); + } + + @SuppressWarnings({"UnusedDeclaration"}) + public FogBugzRepository() { + } + + @Override + public Task[] getIssues(@Nullable String query, int max, final long since) throws Exception { + return getCases(StringUtil.notNullize(query)); + } + + private Task[] getCases(String q) throws Exception { + HttpClient client = login(getLoginMethod()); + PostMethod method = new PostMethod(getUrl() + "/api.asp"); + method.addParameter("token", token); + method.addParameter("cmd", "search"); + method.addParameter("q", q); + method.addParameter("cols", "sTitle,fOpen,dtOpened,dtLastUpdated,ixCategory"); + int status = client.executeMethod(method); + if (status != 200) { + throw new Exception("Error listing cases: " + method.getStatusLine()); + } + Document document = new SAXBuilder(false).build(method.getResponseBodyAsStream()).getDocument(); + XPath path = XPath.newInstance("/response/cases/case"); + final XPath commentPath = XPath.newInstance("events/event"); + @SuppressWarnings("unchecked") final List nodes = (List)path.selectNodes(document); + List tasks = ContainerUtil.mapNotNull(nodes, new NotNullFunction() { + @NotNull + @Override + public Task fun(Element element) { + return createCase(element, commentPath); + + } + }); + return tasks.toArray(new Task[tasks.size()]); + } + + private static TaskType getType(Element element) { + String category = element.getChildText("ixCategory"); + if ("1".equals(category)) { + return TaskType.BUG; + } + else if ("2".equals(category)) { + return TaskType.FEATURE; + } + return TaskType.OTHER; + } + + @NotNull + private Task createCase(final Element element, final XPath commentPath) { + String id = element.getAttributeValue("ixBug"); + String title = element.getChildTextTrim("sTitle"); + TaskType type = getType(element); + LocalTaskImpl task = new LocalTaskImpl(id, title) { + + @Nullable + @Override + public String getDescription() { + return null; + } + + @NotNull + @Override + @SuppressWarnings("unchecked") + public Comment[] getComments() { + List nodes; + try { + nodes = commentPath.selectNodes(element); + } + catch (Exception e) { + throw new RuntimeException("Error selecting comment nodes", e); + } + List comments = ContainerUtil.mapNotNull(nodes, new NotNullFunction() { + @NotNull + @Override + public Comment fun(Element element) { + return createComment(element); + } + + private Comment createComment(final Element element) { + return new Comment() { + @Override + public String getText() { + return element.getChildTextTrim("s"); + } + + @Nullable + @Override + public String getAuthor() { + return element.getChildTextTrim("sPerson"); + } + + @Nullable + @Override + public Date getDate() { + return parseDate(element.getChildTextTrim("dt")); + } + }; + } + }); + return comments.toArray(new Comment[comments.size()]); + } + + @Nullable + @Override + public Date getUpdated() { + return parseDate(element.getChildText("dtLastUpdated")); + } + + @Nullable + @Override + public Date getCreated() { + return parseDate(element.getChildTextTrim("dtOpened")); + } + + @Override + public boolean isClosed() { + return !Boolean.valueOf(element.getChildTextTrim("fOpen")); + } + + @Override + public boolean isIssue() { + return true; + } + + @Nullable + @Override + public String getIssueUrl() { + return getUrl() + "/default.asp?" + getId(); + } + }; + task.setType(type); + return task; + } + + @Nullable + @Override + public Task findTask(String id) throws Exception { + Task[] tasks = getCases(id); + switch (tasks.length) { + case 0: + return null; + case 1: + return tasks[0]; + default: + LOG.warn("Expected unique case for case id: " + id + ", got " + tasks.length + " instead. Using the first one."); + return tasks[0]; + } + } + + @Override + public BaseRepository clone() { + return new FogBugzRepository(this); + } + + private HttpClient login(PostMethod method) throws Exception { + HttpClient client = getHttpClient(); + int status = client.executeMethod(method); + if (status != 200) { + throw new Exception("Error logging in: " + method.getStatusLine()); + } + Document document = new SAXBuilder(false).build(method.getResponseBodyAsStream()).getDocument(); + XPath path = XPath.newInstance("/response/token"); + Element result = (Element)path.selectSingleNode(document); + if (result == null) { + Element error = (Element)XPath.newInstance("/response/error").selectSingleNode(document); + throw new Exception(error == null ? "Error logging in" : error.getText()); + } + token = result.getTextTrim(); + return client; + } + + private PostMethod getLoginMethod() { + PostMethod method = new PostMethod(getUrl() + "/api.asp"); + method.addParameter("cmd", "logon"); + method.addParameter("email", getUsername()); + method.addParameter("password", getPassword()); + return method; + } + + @Nullable + @Override + public CancellableConnection createCancellableConnection() { + return new HttpTestConnection(getLoginMethod()) { + @Override + protected void doTest(PostMethod method) throws Exception { + login(method); + } + }; + } + + @NotNull + private static Date parseDate(@NotNull String string) { + try { + return DatatypeFactory.newInstance().newXMLGregorianCalendar(string).toGregorianCalendar().getTime(); + } + catch (DatatypeConfigurationException e) { + throw new RuntimeException("Error configuring datatype factory", e); + } + } + + @Override + public String getComment() { + return "{id} (e.g. 2344245), {summary}"; + } + +} diff --git a/plugins/tasks/tasks-core/src/com/intellij/tasks/fogbugz/FogBugzRepositoryType.java b/plugins/tasks/tasks-core/src/com/intellij/tasks/fogbugz/FogBugzRepositoryType.java new file mode 100644 index 000000000000..7cfddc0d9bb9 --- /dev/null +++ b/plugins/tasks/tasks-core/src/com/intellij/tasks/fogbugz/FogBugzRepositoryType.java @@ -0,0 +1,51 @@ +/* + * Copyright 2000-2012 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.fogbugz; + +import com.intellij.tasks.TaskRepository; +import com.intellij.tasks.impl.BaseRepositoryType; +import icons.TasksCoreIcons; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; + +/** + * @author mkennedy + */ +public class FogBugzRepositoryType extends BaseRepositoryType { + + @NotNull + @Override + public String getName() { + return "FogBugz"; + } + + @Override + public Icon getIcon() { + return TasksCoreIcons.Fogbugz; + } + + @NotNull + @Override + public TaskRepository createRepository() { + return new FogBugzRepository(this); + } + + @Override + public Class getRepositoryClass() { + return FogBugzRepository.class; + } +} diff --git a/plugins/tasks/tasks-core/src/icons/TasksCoreIcons.java b/plugins/tasks/tasks-core/src/icons/TasksCoreIcons.java index f6c0310f982f..3dada66bf858 100644 --- a/plugins/tasks/tasks-core/src/icons/TasksCoreIcons.java +++ b/plugins/tasks/tasks-core/src/icons/TasksCoreIcons.java @@ -16,6 +16,7 @@ public class TasksCoreIcons { public static final Icon Bug = load("/icons/bug.png"); // 16x16 public static final Icon Exception = load("/icons/exception.png"); // 16x16 public static final Icon Feature = load("/icons/feature.png"); // 16x16 + public static final Icon Fogbugz = load("/icons/fogbugz.png"); // 16x16 public static final Icon Github = load("/icons/github.png"); // 16x16 public static final Icon Lighthouse = load("/icons/lighthouse.gif"); // 16x16 public static final Icon Mantis = load("/icons/mantis.png"); // 16x16 diff --git a/plugins/tasks/tasks-core/src/icons/fogbugz.png b/plugins/tasks/tasks-core/src/icons/fogbugz.png new file mode 100644 index 000000000000..dbd2e8e497ba Binary files /dev/null and b/plugins/tasks/tasks-core/src/icons/fogbugz.png differ