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