Skip to main content
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorspingel2006-08-29 16:01:49 -0400
committerspingel2006-08-29 16:01:49 -0400
commit053b7ad1676c001e694dbeb21ae826a59def1f08 (patch)
tree61e4ae9c5fbd7f428881e7a21efeb3e9c27fc6fc /org.eclipse.mylyn.trac.core/src
parentba276227c0635484a6f9fa29c9a971ad3f4c13d8 (diff)
downloadorg.eclipse.mylyn.tasks-053b7ad1676c001e694dbeb21ae826a59def1f08.tar.gz
org.eclipse.mylyn.tasks-053b7ad1676c001e694dbeb21ae826a59def1f08.tar.xz
org.eclipse.mylyn.tasks-053b7ad1676c001e694dbeb21ae826a59def1f08.zip
Progress on: 154876: extract trac.core plug-in from trac.ui
https://bugs.eclipse.org/bugs/show_bug.cgi?id=154876
Diffstat (limited to 'org.eclipse.mylyn.trac.core/src')
-rw-r--r--org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/AbstractTracClient.java132
-rw-r--r--org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/ITracClient.java161
-rw-r--r--org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/InvalidTicketException.java30
-rw-r--r--org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/Trac09Client.java478
-rw-r--r--org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/TracAttributeFactory.java132
-rw-r--r--org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/TracClientData.java45
-rw-r--r--org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/TracClientFactory.java65
-rw-r--r--org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/TracClientManager.java141
-rw-r--r--org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/TracCorePlugin.java78
-rw-r--r--org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/TracException.java38
-rw-r--r--org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/TracLoginException.java30
-rw-r--r--org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/TracRemoteException.java39
-rw-r--r--org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/TracXmlRpcClient.java449
-rw-r--r--org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/model/TracAttachment.java77
-rw-r--r--org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/model/TracAttribute.java39
-rw-r--r--org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/model/TracComment.java86
-rw-r--r--org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/model/TracComponent.java42
-rw-r--r--org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/model/TracMilestone.java55
-rw-r--r--org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/model/TracPriority.java (renamed from org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/trac/core/internal/DELETE.java)13
-rw-r--r--org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/model/TracSearch.java150
-rw-r--r--org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/model/TracSearchFilter.java108
-rw-r--r--org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/model/TracSeverity.java22
-rw-r--r--org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/model/TracTicket.java225
-rw-r--r--org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/model/TracTicketAttribute.java46
-rw-r--r--org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/model/TracTicketResolution.java22
-rw-r--r--org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/model/TracTicketStatus.java22
-rw-r--r--org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/model/TracTicketType.java22
-rw-r--r--org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/model/TracVersion.java44
-rw-r--r--org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/util/TracHttpClientTransportFactory.java234
-rw-r--r--org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/util/TracUtils.java38
30 files changed, 3061 insertions, 2 deletions
diff --git a/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/AbstractTracClient.java b/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/AbstractTracClient.java
new file mode 100644
index 000000000..345be816a
--- /dev/null
+++ b/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/AbstractTracClient.java
@@ -0,0 +1,132 @@
+/*******************************************************************************
+ * Copyright (c) 2006 - 2006 Mylar eclipse.org project and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Mylar project committers - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.mylar.internal.trac.core;
+
+import java.net.Proxy;
+import java.net.URL;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.mylar.internal.trac.core.model.TracComponent;
+import org.eclipse.mylar.internal.trac.core.model.TracMilestone;
+import org.eclipse.mylar.internal.trac.core.model.TracPriority;
+import org.eclipse.mylar.internal.trac.core.model.TracSeverity;
+import org.eclipse.mylar.internal.trac.core.model.TracTicketResolution;
+import org.eclipse.mylar.internal.trac.core.model.TracTicketStatus;
+import org.eclipse.mylar.internal.trac.core.model.TracTicketType;
+import org.eclipse.mylar.internal.trac.core.model.TracVersion;
+
+/**
+ * @author Steffen Pingel
+ */
+public abstract class AbstractTracClient implements ITracClient {
+
+ protected String username;
+
+ protected String password;
+
+ protected URL repositoryUrl;
+
+ protected Version version;
+
+ protected TracClientData data;
+
+ protected Proxy proxy;
+
+ public AbstractTracClient(URL repositoryUrl, Version version, String username, String password) {
+ this.repositoryUrl = repositoryUrl;
+ this.version = version;
+ this.username = username;
+ this.password = password;
+
+ this.data = new TracClientData();
+ }
+
+ public Version getVersion() {
+ return version;
+ }
+
+ protected boolean hasAuthenticationCredentials() {
+ return username != null && username.length() > 0;
+ }
+
+
+ public TracComponent[] getComponents() {
+ return (data.components != null) ? data.components.toArray(new TracComponent[0]) : null;
+ }
+
+ public TracMilestone[] getMilestones() {
+ return (data.milestones != null) ? data.milestones.toArray(new TracMilestone[0]) : null;
+ }
+
+ public TracPriority[] getPriorities() {
+ return (data.priorities != null) ? data.priorities.toArray(new TracPriority[0]) : null;
+ }
+
+ public TracSeverity[] getSeverities() {
+ return (data.severities != null) ? data.severities.toArray(new TracSeverity[0]) : null;
+ }
+
+ public TracTicketResolution[] getTicketResolutions() {
+ return (data.ticketResolutions != null) ? data.ticketResolutions.toArray(new TracTicketResolution[0]) : null;
+ }
+
+ public TracTicketStatus[] getTicketStatus() {
+ return (data.ticketStatus != null) ? data.ticketStatus.toArray(new TracTicketStatus[0]) : null;
+ }
+
+ public TracTicketType[] getTicketTypes() {
+ return (data.ticketTypes != null) ? data.ticketTypes.toArray(new TracTicketType[0]) : null;
+ }
+
+ public TracVersion[] getVersions() {
+ return (data.versions != null) ? data.versions.toArray(new TracVersion[0]) : null;
+ }
+
+ public void updateAttributes(IProgressMonitor monitor, boolean force) throws TracException {
+ if (data.lastUpdate == 0 || force) {
+ updateAttributes(monitor);
+ data.lastUpdate = System.currentTimeMillis();
+ }
+ }
+
+ public abstract void updateAttributes(IProgressMonitor monitor) throws TracException;
+
+ public void setData(TracClientData data) {
+ this.data = data;
+ }
+
+ public String[] getDefaultTicketResolutions() {
+ return new String[] { "fixed", "invalid", "wontfix", "duplicate", "worksforme" };
+ }
+
+ public String[] getDefaultTicketActions(String status) {
+ if ("new".equals(status)) {
+ return new String[] { "leave", "resolve", "reassign", "accept" };
+ } else if ("assigned".equals(status)) {
+ return new String[] { "leave", "resolve", "reassign" };
+ } else if ("reopened".equals(status)) {
+ return new String[] { "leave", "resolve", "reassign" };
+ } else if ("closed".equals(status)) {
+ return new String[] { "leave", "reopen" };
+ }
+ return null;
+ }
+
+ public void setProxy(Proxy proxy) {
+ this.proxy = proxy;
+ }
+
+ public Proxy getProxy() {
+ return proxy;
+ }
+
+}
diff --git a/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/ITracClient.java b/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/ITracClient.java
new file mode 100644
index 000000000..99e825c49
--- /dev/null
+++ b/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/ITracClient.java
@@ -0,0 +1,161 @@
+/*******************************************************************************
+ * Copyright (c) 2006 - 2006 Mylar eclipse.org project and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Mylar project committers - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.mylar.internal.trac.core;
+
+import java.net.Proxy;
+import java.util.Date;
+import java.util.List;
+import java.util.Set;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.mylar.internal.trac.core.model.TracComponent;
+import org.eclipse.mylar.internal.trac.core.model.TracMilestone;
+import org.eclipse.mylar.internal.trac.core.model.TracPriority;
+import org.eclipse.mylar.internal.trac.core.model.TracSearch;
+import org.eclipse.mylar.internal.trac.core.model.TracSeverity;
+import org.eclipse.mylar.internal.trac.core.model.TracTicket;
+import org.eclipse.mylar.internal.trac.core.model.TracTicketResolution;
+import org.eclipse.mylar.internal.trac.core.model.TracTicketStatus;
+import org.eclipse.mylar.internal.trac.core.model.TracTicketType;
+import org.eclipse.mylar.internal.trac.core.model.TracVersion;
+
+/**
+ * Defines the requirements for classes that provide remote access to Trac
+ * repositories.
+ *
+ * @author Steffen Pingel
+ */
+public interface ITracClient {
+
+ public enum Version {
+ TRAC_0_9, XML_RPC;
+
+ public static Version fromVersion(String version) {
+ try {
+ return Version.valueOf(version);
+ } catch (IllegalArgumentException e) {
+ return null;
+ }
+ }
+
+ @Override
+ public String toString() {
+ switch (this) {
+ case TRAC_0_9:
+ return "Trac 0.9 and later";
+ case XML_RPC:
+ return "XML-RPC Plugin (Rev. " + TracXmlRpcClient.REQUIRED_REVISION + ")";
+ default:
+ return null;
+ }
+ }
+
+ }
+
+ public static final String CHARSET = "UTF-8";
+
+ public static final String TIME_ZONE = "UTC";
+
+ public static final String LOGIN_URL = "/login";
+
+ public static final String QUERY_URL = "/query?format=tab";
+
+ public static final String TICKET_URL = "/ticket/";
+
+ public static final String NEW_TICKET_URL = "/newticket";
+
+ public static final String TICKET_ATTACHMENT_URL = "/attachment/ticket/";
+
+ public static final String DEFAULT_USERNAME = "anonymous";
+
+ /**
+ * Gets ticket with <code>id</code> from repository.
+ *
+ * @param id
+ * the id of the ticket to get
+ * @return the ticket
+ * @throws TracException
+ * thrown in case of a connection error
+ */
+ TracTicket getTicket(int id) throws TracException;
+
+ /**
+ * Returns the access type.
+ */
+ Version getVersion();
+
+ /**
+ * Queries tickets from repository. All found tickets are added to
+ * <code>result</code>.
+ *
+ * @param query
+ * the search criteria
+ * @param result
+ * the list of found tickets
+ * @throws TracException
+ * thrown in case of a connection error
+ */
+ void search(TracSearch query, List<TracTicket> result) throws TracException;
+
+ /**
+ * Validates the repository connection.
+ *
+ * @throws TracException
+ * thrown in case of a connection error
+ */
+ void validate() throws TracException;
+
+ /**
+ * Updates cached repository details: milestones, versions etc.
+ *
+ * @throws TracException
+ * thrown in case of a connection error
+ */
+ void updateAttributes(IProgressMonitor monitor, boolean force) throws TracException;
+
+ TracComponent[] getComponents();
+
+ TracMilestone[] getMilestones();
+
+ TracPriority[] getPriorities();
+
+ TracSeverity[] getSeverities();
+
+ TracTicketResolution[] getTicketResolutions();
+
+ TracTicketStatus[] getTicketStatus();
+
+ TracTicketType[] getTicketTypes();
+
+ TracVersion[] getVersions();
+
+ byte[] getAttachmentData(int id, String filename) throws TracException;
+
+ void putAttachmentData(int id, String name, String description, byte[] data) throws TracException;
+
+ void createTicket(TracTicket ticket) throws TracException;
+
+ void updateTicket(TracTicket ticket, String comment) throws TracException;
+
+ /**
+ * Sets a reference to the cached repository attributes.
+ *
+ * @param data
+ * cached repository attributes
+ */
+ void setData(TracClientData data);
+
+ Set<Integer> getChangedTickets(Date since) throws TracException;
+
+ void setProxy(Proxy proxy);
+
+}
diff --git a/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/InvalidTicketException.java b/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/InvalidTicketException.java
new file mode 100644
index 000000000..f5102eaa9
--- /dev/null
+++ b/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/InvalidTicketException.java
@@ -0,0 +1,30 @@
+/*******************************************************************************
+ * Copyright (c) 2006 - 2006 Mylar eclipse.org project and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Mylar project committers - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.mylar.internal.trac.core;
+
+/**
+ * Indicates an error while parsing a ticket retrieved from a repository.
+ *
+ * @author Steffen Pingel
+ */
+public class InvalidTicketException extends TracException {
+
+ private static final long serialVersionUID = 7716941243394876876L;
+
+ public InvalidTicketException(String message) {
+ super(message);
+ }
+
+ public InvalidTicketException() {
+ }
+
+}
diff --git a/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/Trac09Client.java b/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/Trac09Client.java
new file mode 100644
index 000000000..9e114f26a
--- /dev/null
+++ b/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/Trac09Client.java
@@ -0,0 +1,478 @@
+/*******************************************************************************
+ * Copyright (c) 2006 - 2006 Mylar eclipse.org project and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Mylar project committers - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.mylar.internal.trac.core;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.StringTokenizer;
+
+import javax.security.auth.login.LoginException;
+
+import org.apache.commons.httpclient.Credentials;
+import org.apache.commons.httpclient.HttpClient;
+import org.apache.commons.httpclient.UsernamePasswordCredentials;
+import org.apache.commons.httpclient.auth.AuthScope;
+import org.apache.commons.httpclient.cookie.CookiePolicy;
+import org.apache.commons.httpclient.methods.GetMethod;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.OperationCanceledException;
+import org.eclipse.mylar.context.core.MylarStatusHandler;
+import org.eclipse.mylar.internal.tasks.core.HtmlStreamTokenizer;
+import org.eclipse.mylar.internal.tasks.core.HtmlTag;
+import org.eclipse.mylar.internal.tasks.core.WebClientUtil;
+import org.eclipse.mylar.internal.tasks.core.HtmlStreamTokenizer.Token;
+import org.eclipse.mylar.internal.trac.core.model.TracComponent;
+import org.eclipse.mylar.internal.trac.core.model.TracMilestone;
+import org.eclipse.mylar.internal.trac.core.model.TracPriority;
+import org.eclipse.mylar.internal.trac.core.model.TracSearch;
+import org.eclipse.mylar.internal.trac.core.model.TracSearchFilter;
+import org.eclipse.mylar.internal.trac.core.model.TracSeverity;
+import org.eclipse.mylar.internal.trac.core.model.TracTicket;
+import org.eclipse.mylar.internal.trac.core.model.TracTicketResolution;
+import org.eclipse.mylar.internal.trac.core.model.TracTicketStatus;
+import org.eclipse.mylar.internal.trac.core.model.TracTicketType;
+import org.eclipse.mylar.internal.trac.core.model.TracVersion;
+import org.eclipse.mylar.internal.trac.core.model.TracSearchFilter.CompareOperator;
+import org.eclipse.mylar.internal.trac.core.model.TracTicket.Key;
+import org.eclipse.mylar.internal.trac.core.util.TracHttpClientTransportFactory.TracHttpException;
+
+/**
+ * Represents a Trac repository that is accessed through the Trac's query script
+ * and web interface.
+ *
+ * @author Steffen Pingel
+ */
+public class Trac09Client extends AbstractTracClient {
+
+ private HttpClient httpClient = new HttpClient();
+
+ private boolean authenticated;
+
+ public Trac09Client(URL url, Version version, String username, String password) {
+ super(url, version, username, password);
+ }
+
+ private GetMethod connect(String serverURL) throws TracException {
+ try {
+ return connectInternal(serverURL);
+ } catch (TracException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new TracException(e);
+ }
+ }
+
+ private GetMethod connectInternal(String serverURL) throws TracLoginException, IOException, TracHttpException {
+ WebClientUtil.setupHttpClient(httpClient, proxy, serverURL);
+
+ for (int attempt = 0; attempt < 2; attempt++) {
+ // force authentication
+ if (!authenticated && hasAuthenticationCredentials()) {
+ authenticate();
+ }
+
+ GetMethod method = new GetMethod(WebClientUtil.getRequestPath(serverURL));
+ method.getParams().setCookiePolicy(CookiePolicy.BROWSER_COMPATIBILITY);
+ int code;
+ try {
+ code = httpClient.executeMethod(method);
+ } catch (IOException e) {
+ method.releaseConnection();
+ throw e;
+ }
+
+ if (code == HttpURLConnection.HTTP_OK) {
+ return method;
+ } else if (code == HttpURLConnection.HTTP_UNAUTHORIZED || code == HttpURLConnection.HTTP_FORBIDDEN) {
+ // login or reauthenticate due to an expired session
+ method.releaseConnection();
+ authenticated = false;
+ authenticate();
+ } else {
+ throw new TracHttpException(code);
+ }
+ }
+
+ throw new TracLoginException();
+ }
+
+ private void authenticate() throws TracLoginException, IOException {
+ if (!hasAuthenticationCredentials()) {
+ throw new TracLoginException();
+ }
+
+ Credentials credentials = new UsernamePasswordCredentials(username, password);
+ httpClient.getState().setCredentials(
+ new AuthScope(AuthScope.ANY_HOST, AuthScope.ANY_PORT, AuthScope.ANY_REALM), credentials);
+
+ GetMethod method = new GetMethod(WebClientUtil.getRequestPath(repositoryUrl + LOGIN_URL));
+ method.setFollowRedirects(false);
+
+ try {
+ httpClient.getParams().setAuthenticationPreemptive(true);
+ int code = httpClient.executeMethod(method);
+ if (code == HttpURLConnection.HTTP_UNAUTHORIZED || code == HttpURLConnection.HTTP_FORBIDDEN) {
+ throw new TracLoginException();
+ }
+ } finally {
+ method.releaseConnection();
+ httpClient.getParams().setAuthenticationPreemptive(false);
+ }
+
+ authenticated = true;
+ }
+
+ /**
+ * Fetches the web site of a single ticket and returns the Trac ticket.
+ *
+ * @param id
+ * Trac id of ticket
+ * @throws LoginException
+ */
+ public TracTicket getTicket(int id) throws TracException {
+ GetMethod method = connect(repositoryUrl + ITracClient.TICKET_URL + id);
+ try {
+ TracTicket ticket = new TracTicket(id);
+
+ BufferedReader reader = new BufferedReader(new InputStreamReader(method.getResponseBodyAsStream(),
+ ITracClient.CHARSET));
+ HtmlStreamTokenizer tokenizer = new HtmlStreamTokenizer(reader, null);
+ for (Token token = tokenizer.nextToken(); token.getType() != Token.EOF; token = tokenizer.nextToken()) {
+ if (token.getType() == Token.TAG) {
+ HtmlTag tag = (HtmlTag) token.getValue();
+ if (tag.getTagType() == HtmlTag.Type.TD) {
+ String headers = tag.getAttribute("headers");
+ if ("h_component".equals(headers)) {
+ ticket.putBuiltinValue(Key.COMPONENT, getText(tokenizer));
+ } else if ("h_milestone".equals(headers)) {
+ ticket.putBuiltinValue(Key.MILESTONE, getText(tokenizer));
+ } else if ("h_priority".equals(headers)) {
+ ticket.putBuiltinValue(Key.PRIORITY, getText(tokenizer));
+ } else if ("h_severity".equals(headers)) {
+ ticket.putBuiltinValue(Key.SEVERITY, getText(tokenizer));
+ } else if ("h_version".equals(headers)) {
+ ticket.putBuiltinValue(Key.VERSION, getText(tokenizer));
+ } else if ("h_keywords".equals(headers)) {
+ ticket.putBuiltinValue(Key.KEYWORDS, getText(tokenizer));
+ } else if ("h_cc".equals(headers)) {
+ ticket.putBuiltinValue(Key.CC, getText(tokenizer));
+ } else if ("h_owner".equals(headers)) {
+ ticket.putBuiltinValue(Key.OWNER, getText(tokenizer));
+ } else if ("h_reporter".equals(headers)) {
+ ticket.putBuiltinValue(Key.REPORTER, getText(tokenizer));
+ }
+ // TODO handle custom fields
+ } else if (tag.getTagType() == HtmlTag.Type.H2 && "summary".equals(tag.getAttribute("class"))) {
+ ticket.putBuiltinValue(Key.SUMMARY, getText(tokenizer));
+ } else if (tag.getTagType() == HtmlTag.Type.H3 && "status".equals(tag.getAttribute("class"))) {
+ String text = getStrongText(tokenizer);
+ if (text.length() > 0) {
+ int i = text.indexOf(" (");
+ if (i != -1) {
+ // status contains resolution as well
+ ticket.putBuiltinValue(Key.STATUS, text.substring(0, i));
+ ticket.putBuiltinValue(Key.RESOLUTION, text.substring(i, text.length() - 1));
+ } else {
+ ticket.putBuiltinValue(Key.STATUS, text);
+ }
+ }
+ }
+ // TODO parse description
+ }
+ }
+
+ if (ticket.isValid() && ticket.getValue(Key.SUMMARY) != null) {
+ return ticket;
+ }
+
+ throw new InvalidTicketException();
+ } catch (IOException e) {
+ throw new TracException(e);
+ } catch (ParseException e) {
+ throw new TracException(e);
+ } finally {
+ method.releaseConnection();
+ }
+ }
+
+ public void search(TracSearch query, List<TracTicket> tickets) throws TracException {
+ GetMethod method = connect(repositoryUrl + ITracClient.QUERY_URL + query.toUrl());
+ try {
+ BufferedReader reader = new BufferedReader(new InputStreamReader(method.getResponseBodyAsStream(),
+ ITracClient.CHARSET));
+ String line;
+
+ Map<String, String> constantValues = getExactMatchValues(query);
+
+ // first line contains names of returned ticket fields
+ line = reader.readLine();
+ if (line == null) {
+ throw new InvalidTicketException();
+ }
+ StringTokenizer t = new StringTokenizer(line, "\t");
+ Key[] fields = new Key[t.countTokens()];
+ for (int i = 0; i < fields.length; i++) {
+ fields[i] = Key.fromKey(t.nextToken());
+ }
+
+ // create a ticket for each following line of output
+ while ((line = reader.readLine()) != null) {
+ t = new StringTokenizer(line, "\t");
+ TracTicket ticket = new TracTicket();
+ for (int i = 0; i < fields.length && t.hasMoreTokens(); i++) {
+ if (fields[i] != null) {
+ try {
+ if (fields[i] == Key.ID) {
+ ticket.setId(Integer.parseInt(t.nextToken()));
+ } else if (fields[i] == Key.TIME) {
+ ticket.setCreated(Integer.parseInt(t.nextToken()));
+ } else if (fields[i] == Key.CHANGE_TIME) {
+ ticket.setLastChanged(Integer.parseInt(t.nextToken()));
+ } else {
+ ticket.putBuiltinValue(fields[i], parseTicketValue(t.nextToken()));
+ }
+ } catch (NumberFormatException e) {
+ MylarStatusHandler.log(e, "Error parsing repsonse: " + line);
+ }
+ }
+ }
+
+ if (ticket.isValid()) {
+ for (String key : constantValues.keySet()) {
+ ticket.putValue(key, parseTicketValue(constantValues.get(key)));
+ }
+
+ tickets.add(ticket);
+ }
+ }
+ } catch (IOException e) {
+ throw new TracException(e);
+ } finally {
+ method.releaseConnection();
+ }
+ }
+
+ /**
+ * Trac has sepcial encoding rules for the returned output: None is
+ * represented by "--".
+ */
+ private String parseTicketValue(String value) {
+ if ("--".equals(value)) {
+ return "";
+ }
+ return value;
+ }
+
+ /**
+ * Extracts constant values from <code>query</code>. The Trac query
+ * script does not return fields that matched exactly againt a single value.
+ */
+ private Map<String, String> getExactMatchValues(TracSearch query) {
+ Map<String, String> values = new HashMap<String, String>();
+ List<TracSearchFilter> filters = query.getFilters();
+ for (TracSearchFilter filter : filters) {
+ if (filter.getOperator() == CompareOperator.IS && filter.getValues().size() == 1) {
+ values.put(filter.getFieldName(), filter.getValues().get(0));
+ }
+ }
+ return values;
+ }
+
+ public void validate() throws TracException {
+ GetMethod method = connect(repositoryUrl + "/");
+ try {
+ BufferedReader reader = new BufferedReader(new InputStreamReader(method.getResponseBodyAsStream(),
+ ITracClient.CHARSET));
+ HtmlStreamTokenizer tokenizer = new HtmlStreamTokenizer(reader, null);
+ for (Token token = tokenizer.nextToken(); token.getType() != Token.EOF; token = tokenizer.nextToken()) {
+ if (token.getType() == Token.TAG) {
+ HtmlTag tag = (HtmlTag) token.getValue();
+ if (tag.getTagType() == HtmlTag.Type.A) {
+ String id = tag.getAttribute("id");
+ if ("tracpowered".equals(id)) {
+ return;
+ }
+ }
+ }
+ }
+
+ throw new TracException("Not a valid Trac repository");
+ } catch (IOException e) {
+ throw new TracException(e);
+ } catch (ParseException e) {
+ throw new TracException(e);
+ } finally {
+ method.releaseConnection();
+ }
+ }
+
+ public void updateAttributes(IProgressMonitor monitor) throws TracException {
+ monitor.beginTask("Updating attributes", IProgressMonitor.UNKNOWN);
+
+ GetMethod method = connect(repositoryUrl + ITracClient.NEW_TICKET_URL);
+ try {
+ BufferedReader reader = new BufferedReader(new InputStreamReader(method.getResponseBodyAsStream(),
+ ITracClient.CHARSET));
+ HtmlStreamTokenizer tokenizer = new HtmlStreamTokenizer(reader, null);
+ for (Token token = tokenizer.nextToken(); token.getType() != Token.EOF; token = tokenizer.nextToken()) {
+ if (monitor.isCanceled()) {
+ throw new OperationCanceledException();
+ }
+
+ if (token.getType() == Token.TAG) {
+ HtmlTag tag = (HtmlTag) token.getValue();
+ if (tag.getTagType() == HtmlTag.Type.SELECT) {
+ String name = tag.getAttribute("id");
+ if ("component".equals(name)) {
+ List<String> values = getOptionValues(tokenizer);
+ data.components = new ArrayList<TracComponent>(values.size());
+ for (String value : values) {
+ data.components.add(new TracComponent(value));
+ }
+ } else if ("milestone".equals(name)) {
+ List<String> values = getOptionValues(tokenizer);
+ data.milestones = new ArrayList<TracMilestone>(values.size());
+ for (String value : values) {
+ data.milestones.add(new TracMilestone(value));
+ }
+ } else if ("priority".equals(name)) {
+ List<String> values = getOptionValues(tokenizer);
+ data.priorities = new ArrayList<TracPriority>(values.size());
+ for (int i = 0; i < values.size(); i++) {
+ data.priorities.add(new TracPriority(values.get(i), i + 1));
+ }
+ } else if ("severity".equals(name)) {
+ List<String> values = getOptionValues(tokenizer);
+ data.severities = new ArrayList<TracSeverity>(values.size());
+ for (int i = 0; i < values.size(); i++) {
+ data.severities.add(new TracSeverity(values.get(i), i + 1));
+ }
+ } else if ("type".equals(name)) {
+ List<String> values = getOptionValues(tokenizer);
+ data.ticketTypes = new ArrayList<TracTicketType>(values.size());
+ for (int i = 0; i < values.size(); i++) {
+ data.ticketTypes.add(new TracTicketType(values.get(i), i + 1));
+ }
+ } else if ("version".equals(name)) {
+ List<String> values = getOptionValues(tokenizer);
+ data.versions = new ArrayList<TracVersion>(values.size());
+ for (String value : values) {
+ data.versions.add(new TracVersion(value));
+ }
+ }
+ }
+ }
+ }
+
+ data.ticketResolutions = new ArrayList<TracTicketResolution>(5);
+ data.ticketResolutions.add(new TracTicketResolution("fixed", 1));
+ data.ticketResolutions.add(new TracTicketResolution("invalid", 2));
+ data.ticketResolutions.add(new TracTicketResolution("wontfix", 3));
+ data.ticketResolutions.add(new TracTicketResolution("duplicate", 4));
+ data.ticketResolutions.add(new TracTicketResolution("worksforme", 5));
+
+ data.ticketStatus = new ArrayList<TracTicketStatus>(4);
+ data.ticketStatus.add(new TracTicketStatus("new", 1));
+ data.ticketStatus.add(new TracTicketStatus("assigned", 2));
+ data.ticketStatus.add(new TracTicketStatus("reopened", 3));
+ data.ticketStatus.add(new TracTicketStatus("closed", 4));
+ } catch (IOException e) {
+ throw new TracException(e);
+ } catch (ParseException e) {
+ throw new TracException(e);
+ } finally {
+ method.releaseConnection();
+ }
+ }
+
+ private List<String> getOptionValues(HtmlStreamTokenizer tokenizer) throws IOException, ParseException {
+ List<String> values = new ArrayList<String>();
+ for (Token token = tokenizer.nextToken(); token.getType() != Token.EOF; token = tokenizer.nextToken()) {
+ if (token.getType() == Token.TAG) {
+ HtmlTag tag = (HtmlTag) token.getValue();
+ if (tag.getTagType() == HtmlTag.Type.OPTION && !tag.isEndTag()) {
+ String value = getText(tokenizer).trim();
+ if (value.length() > 0) {
+ values.add(value);
+ }
+ } else {
+ return values;
+ }
+ }
+ }
+ return values;
+ }
+
+ private String getText(HtmlStreamTokenizer tokenizer) throws IOException, ParseException {
+ StringBuffer sb = new StringBuffer();
+ for (Token token = tokenizer.nextToken(); token.getType() != Token.EOF; token = tokenizer.nextToken()) {
+ if (token.getType() == Token.TEXT) {
+ sb.append(token.toString());
+ } else if (token.getType() == Token.COMMENT) {
+ // ignore
+ } else {
+ break;
+ }
+ }
+ return HtmlStreamTokenizer.unescape(sb).toString();
+ }
+
+ /**
+ * Looks for a <code>strong</code> tag and returns the text enclosed by
+ * the tag.
+ */
+ private String getStrongText(HtmlStreamTokenizer tokenizer) throws IOException, ParseException {
+ for (Token token = tokenizer.nextToken(); token.getType() != Token.EOF; token = tokenizer.nextToken()) {
+ if (token.getType() == Token.TAG && ((HtmlTag) token.getValue()).getTagType() == HtmlTag.Type.STRONG) {
+ return getText(tokenizer);
+ } else if (token.getType() == Token.COMMENT) {
+ // ignore
+ } else if (token.getType() == Token.TEXT) {
+ // ignore
+ } else {
+ break;
+ }
+ }
+ return "";
+ }
+
+ public byte[] getAttachmentData(int id, String filename) throws TracException {
+ throw new TracException("Unsupported operation");
+ }
+
+ public void putAttachmentData(int id, String name, String description, byte[] data) throws TracException {
+ throw new TracException("Unsupported operation");
+ }
+
+ public void createTicket(TracTicket ticket) throws TracException {
+ throw new TracException("Unsupported operation");
+ }
+
+ public void updateTicket(TracTicket ticket, String comment) throws TracException {
+ throw new TracException("Unsupported operation");
+ }
+
+ public Set<Integer> getChangedTickets(Date since) throws TracException {
+ return null;
+ }
+
+}
diff --git a/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/TracAttributeFactory.java b/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/TracAttributeFactory.java
new file mode 100644
index 000000000..4c254dfba
--- /dev/null
+++ b/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/TracAttributeFactory.java
@@ -0,0 +1,132 @@
+/*******************************************************************************
+ * Copyright (c) 2004 - 2006 University Of British Columbia and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * University Of British Columbia - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.mylar.internal.trac.core;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.mylar.internal.trac.core.model.TracTicket.Key;
+import org.eclipse.mylar.tasks.core.AbstractAttributeFactory;
+import org.eclipse.mylar.tasks.core.RepositoryTaskAttribute;
+
+/**
+ * Provides a mapping from Mylar task keys to Trac ticket keys.
+ *
+ * @author Steffen Pingel
+ */
+public class TracAttributeFactory extends AbstractAttributeFactory {
+
+ private static final long serialVersionUID = 5333211422546115138L;
+
+ private static Map<String, Attribute> attributeByTracKey = new HashMap<String, Attribute>();
+
+ private static Map<String, String> tracKeyByTaskKey = new HashMap<String, String>();
+
+ public enum Attribute {
+ CC(Key.CC, "CC:", RepositoryTaskAttribute.USER_CC),
+ CHANGE_TIME(Key.CHANGE_TIME, "Last Modification:", RepositoryTaskAttribute.DATE_MODIFIED, true, true),
+ COMPONENT(Key.COMPONENT, "Component:", null),
+ DESCRIPTION(Key.DESCRIPTION, "Description:", RepositoryTaskAttribute.DESCRIPTION, true, false),
+ ID(Key.ID, "<used by search engine>", null, true),
+ KEYWORDS(Key.KEYWORDS, "Keywords:", RepositoryTaskAttribute.KEYWORDS),
+ MILESTONE(Key.MILESTONE, "Milestone:", null),
+ OWNER(Key.OWNER, "Owner:", RepositoryTaskAttribute.USER_OWNER, false, true),
+ PRIORITY(Key.PRIORITY, "Priority:", null),
+ REPORTER(Key.REPORTER, "Reporter:", RepositoryTaskAttribute.USER_REPORTER, false, true),
+ RESOLUTION(Key.RESOLUTION, "Resolution:", RepositoryTaskAttribute.RESOLUTION, false, true),
+ SEVERITY(Key.SEVERITY, "Severity:", null),
+ STATUS(Key.STATUS, "Status:", RepositoryTaskAttribute.STATUS, false, true),
+ SUMMARY(Key.SUMMARY, "Summary:", RepositoryTaskAttribute.SUMMARY, true),
+ TIME(Key.TIME, "Created:", RepositoryTaskAttribute.DATE_CREATION, true, true),
+ TYPE(Key.TYPE, "Type:", null),
+ VERSION(Key.VERSION, "Version:", null);
+
+ private final boolean isHidden;
+
+ private final boolean isReadOnly;
+
+ private final String tracKey;
+
+ private final String prettyName;
+
+ private final String taskKey;
+
+ Attribute(Key key, String prettyName, String taskKey, boolean hidden, boolean readonly) {
+ this.tracKey = key.getKey();
+ this.taskKey = taskKey;
+ this.prettyName = prettyName;
+ this.isHidden = hidden;
+ this.isReadOnly = readonly;
+
+ attributeByTracKey.put(tracKey, this);
+ if (taskKey != null) {
+ tracKeyByTaskKey.put(taskKey, tracKey);
+ }
+ }
+
+ Attribute(Key key, String prettyName, String taskKey, boolean hidden) {
+ this(key, prettyName, taskKey, hidden, false);
+ }
+
+ Attribute(Key key, String prettyName, String taskKey) {
+ this(key, prettyName, taskKey, false, false);
+ }
+
+ public String getTaskKey() {
+ return taskKey;
+ }
+
+ public String getTracKey() {
+ return tracKey;
+ }
+
+ public boolean isHidden() {
+ return isHidden;
+ }
+
+ public boolean isReadOnly() {
+ return isReadOnly;
+ }
+
+ public String toString() {
+ return prettyName;
+ }
+ }
+
+
+ @Override
+ public boolean getIsHidden(String key) {
+ Attribute attribute = attributeByTracKey.get(key);
+ return (attribute != null) ? attribute.isHidden() : false;
+ }
+
+ @Override
+ public String getName(String key) {
+ Attribute attribute = attributeByTracKey.get(key);
+ // TODO if attribute == null it is probably a custom field: need
+ // to query custom field information from repoository
+ return (attribute != null) ? attribute.toString() : key;
+ }
+
+ @Override
+ public boolean isReadOnly(String key) {
+ Attribute attribute = attributeByTracKey.get(key);
+ return (attribute != null) ? attribute.isReadOnly() : false;
+ }
+
+ @Override
+ public String mapCommonAttributeKey(String key) {
+ String tracKey = tracKeyByTaskKey.get(key);
+ return (tracKey != null) ? tracKey : key;
+ }
+
+}
diff --git a/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/TracClientData.java b/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/TracClientData.java
new file mode 100644
index 000000000..41fded092
--- /dev/null
+++ b/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/TracClientData.java
@@ -0,0 +1,45 @@
+/*******************************************************************************
+ * Copyright (c) 2004 - 2006 Mylar committers and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *******************************************************************************/
+
+package org.eclipse.mylar.internal.trac.core;
+
+import java.io.Serializable;
+import java.util.List;
+
+import org.eclipse.mylar.internal.trac.core.model.TracComponent;
+import org.eclipse.mylar.internal.trac.core.model.TracMilestone;
+import org.eclipse.mylar.internal.trac.core.model.TracPriority;
+import org.eclipse.mylar.internal.trac.core.model.TracSeverity;
+import org.eclipse.mylar.internal.trac.core.model.TracTicketResolution;
+import org.eclipse.mylar.internal.trac.core.model.TracTicketStatus;
+import org.eclipse.mylar.internal.trac.core.model.TracTicketType;
+import org.eclipse.mylar.internal.trac.core.model.TracVersion;
+
+public class TracClientData implements Serializable {
+
+ private static final long serialVersionUID = 6891961984245981675L;
+
+ List<TracComponent> components;
+
+ List<TracMilestone> milestones;
+
+ List<TracPriority> priorities;
+
+ List<TracSeverity> severities;
+
+ List<TracTicketResolution> ticketResolutions;
+
+ List<TracTicketStatus> ticketStatus;
+
+ List<TracTicketType> ticketTypes;
+
+ List<TracVersion> versions;
+
+ long lastUpdate;
+
+}
diff --git a/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/TracClientFactory.java b/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/TracClientFactory.java
new file mode 100644
index 000000000..50b7ea6e3
--- /dev/null
+++ b/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/TracClientFactory.java
@@ -0,0 +1,65 @@
+/*******************************************************************************
+ * Copyright (c) 2006 - 2006 Mylar eclipse.org project and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Mylar project committers - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.mylar.internal.trac.core;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import org.eclipse.mylar.internal.trac.core.ITracClient.Version;
+
+/**
+ * @author Steffen Pingel
+ */
+public class TracClientFactory {
+
+ public static ITracClient createClient(String location, Version version, String username, String password)
+ throws MalformedURLException {
+ URL url = new URL(location);
+
+ if (version == Version.TRAC_0_9) {
+ return new Trac09Client(url, version, username, password);
+ } else if (version == Version.XML_RPC) {
+ return new TracXmlRpcClient(url, version, username, password);
+ }
+
+ throw new RuntimeException("Invalid repository version: " + version);
+ }
+
+ /**
+ * Tries all supported access types for <code>location</code> and returns
+ * the corresponding version if successful; throws an exception otherwise.
+ *
+ * <p>
+ * Order of the tried access types: XML-RPC, Trac 0.9
+ */
+ public static Version probeClient(String location, String username, String password) throws MalformedURLException,
+ TracException {
+ URL url = new URL(location);
+ try {
+ ITracClient repository = new TracXmlRpcClient(url, Version.XML_RPC, username, password);
+ repository.validate();
+ return Version.XML_RPC;
+ } catch (TracException e) {
+ try {
+ ITracClient repository = new Trac09Client(url, Version.TRAC_0_9, username, password);
+ repository.validate();
+ return Version.TRAC_0_9;
+ } catch (TracLoginException e2) {
+ throw e;
+ } catch (TracException e2) {
+ }
+ }
+
+ throw new TracException();
+ }
+
+}
diff --git a/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/TracClientManager.java b/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/TracClientManager.java
new file mode 100644
index 000000000..329b7104f
--- /dev/null
+++ b/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/TracClientManager.java
@@ -0,0 +1,141 @@
+/*******************************************************************************
+ * Copyright (c) 2006 - 2006 Mylar eclipse.org project and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Mylar project committers - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.mylar.internal.trac.core;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.net.MalformedURLException;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.mylar.internal.trac.core.ITracClient.Version;
+import org.eclipse.mylar.tasks.core.ITaskRepositoryListener;
+import org.eclipse.mylar.tasks.core.TaskRepository;
+
+/**
+ * Caches {@link ITracClient} objects.
+ *
+ * @author Steffen Pingel
+ */
+public class TracClientManager implements ITaskRepositoryListener {
+
+ private Map<String, ITracClient> clientByUrl = new HashMap<String, ITracClient>();
+
+ private Map<String, TracClientData> clientDataByUrl = new HashMap<String, TracClientData>();
+
+ private File cacheFile;
+
+ public TracClientManager(File cacheFile) {
+ this.cacheFile = cacheFile;
+
+ readCache();
+ }
+
+ public synchronized ITracClient getRepository(TaskRepository taskRepository) throws MalformedURLException {
+ ITracClient repository = clientByUrl.get(taskRepository.getUrl());
+ if (repository == null) {
+ repository = TracClientFactory.createClient(taskRepository.getUrl(), Version.fromVersion(taskRepository
+ .getVersion()), taskRepository.getUserName(), taskRepository.getPassword());
+ clientByUrl.put(taskRepository.getUrl(), repository);
+
+ TracClientData data = clientDataByUrl.get(taskRepository.getUrl());
+ if (data == null) {
+ data = new TracClientData();
+ clientDataByUrl.put(taskRepository.getUrl(), data);
+ }
+ repository.setData(data);
+ }
+ return repository;
+ }
+
+ public void repositoriesRead() {
+ // ignore
+ }
+
+ public synchronized void repositoryAdded(TaskRepository repository) {
+ // make sure there is no stale client still in the cache, bug #149939
+ clientByUrl.remove(repository.getUrl());
+ clientDataByUrl.remove(repository.getUrl());
+ }
+
+ public synchronized void repositoryRemoved(TaskRepository repository) {
+ clientByUrl.remove(repository.getUrl());
+ clientDataByUrl.remove(repository.getUrl());
+ }
+
+ public synchronized void repositorySettingsChanged(TaskRepository repository) {
+ clientByUrl.remove(repository.getUrl());
+ // if url is changed a stale data object will be left in clientDataByUrl, bug #149939
+ }
+
+ @SuppressWarnings("unchecked")
+ public void readCache() {
+ if (cacheFile == null || !cacheFile.exists()) {
+ return;
+ }
+
+ ObjectInputStream in = null;
+ try {
+ in = new ObjectInputStream(new FileInputStream(cacheFile));
+ int size = in.readInt();
+ for (int i = 0; i < size; i++) {
+ String url = (String) in.readObject();
+ TracClientData data = (TracClientData) in.readObject();
+ if (url != null && data != null) {
+ clientDataByUrl.put(url, data);
+ }
+ }
+ } catch (Throwable e) {
+ TracCorePlugin.log(e);
+ } finally {
+ if (in != null) {
+ try {
+ in.close();
+ } catch (IOException e) {
+ // ignore
+ }
+ }
+ }
+
+ }
+
+ public void writeCache() {
+ if (cacheFile == null) {
+ return;
+ }
+
+ ObjectOutputStream out = null;
+ try {
+ out = new ObjectOutputStream(new FileOutputStream(cacheFile));
+ out.writeInt(clientDataByUrl.size());
+ for (String url : clientDataByUrl.keySet()) {
+ out.writeObject(url);
+ out.writeObject(clientDataByUrl.get(url));
+ }
+ } catch (IOException e) {
+ TracCorePlugin.log(e);
+ } finally {
+ if (out != null) {
+ try {
+ out.close();
+ } catch (IOException e) {
+ // ignore
+ }
+ }
+ }
+ }
+
+}
diff --git a/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/TracCorePlugin.java b/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/TracCorePlugin.java
new file mode 100644
index 000000000..a9a2389df
--- /dev/null
+++ b/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/TracCorePlugin.java
@@ -0,0 +1,78 @@
+/*******************************************************************************
+ * Copyright (c) 2003 - 2006 University Of British Columbia and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * University Of British Columbia - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.mylar.internal.trac.core;
+
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Plugin;
+import org.eclipse.core.runtime.Status;
+import org.osgi.framework.BundleContext;
+
+/**
+ * The headless Trac plug-in class.
+ *
+ * @author Steffen Pingel
+ */
+public class TracCorePlugin extends Plugin {
+
+ public static final String PLUGIN_ID = "org.eclipse.mylar.trac.core";
+
+ public static final String ENCODING_UTF_8 = "UTF-8";
+
+ private static TracCorePlugin plugin;
+
+ public final static String REPOSITORY_KIND = "trac";
+
+ public TracCorePlugin() {
+ }
+
+ public static TracCorePlugin getDefault() {
+ return plugin;
+ }
+
+ @Override
+ public void start(BundleContext context) throws Exception {
+ super.start(context);
+ plugin = this;
+ }
+
+ @Override
+ public void stop(BundleContext context) throws Exception {
+ plugin = null;
+ super.stop(context);
+ }
+
+ /**
+ * Convenience method for logging statuses to the plug-in log
+ *
+ * @param status
+ * the status to log
+ */
+ public static void log(IStatus status) {
+ getDefault().getLog().log(status);
+ }
+
+ /**
+ * Convenience method for logging exceptions to the plug-in log
+ *
+ * @param e
+ * the exception to log
+ */
+ public static void log(Throwable e) {
+ String message = e.getMessage();
+ if (e.getMessage() == null) {
+ message = e.getClass().toString();
+ }
+ log(new Status(Status.ERROR, TracCorePlugin.PLUGIN_ID, 0, message, e));
+ }
+
+}
+
+
diff --git a/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/TracException.java b/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/TracException.java
new file mode 100644
index 000000000..f38b02a4a
--- /dev/null
+++ b/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/TracException.java
@@ -0,0 +1,38 @@
+/*******************************************************************************
+ * Copyright (c) 2006 - 2006 Mylar eclipse.org project and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Mylar project committers - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.mylar.internal.trac.core;
+
+/**
+ * Indicates an error during repository access.
+ *
+ * @author Steffen Pingel
+ */
+public class TracException extends Exception {
+
+ private static final long serialVersionUID = 1929614326467463462L;
+
+ public TracException() {
+ }
+
+ public TracException(String message) {
+ super(message);
+ }
+
+ public TracException(Throwable cause) {
+ super(cause);
+ }
+
+ public TracException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+}
diff --git a/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/TracLoginException.java b/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/TracLoginException.java
new file mode 100644
index 000000000..d50aff94b
--- /dev/null
+++ b/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/TracLoginException.java
@@ -0,0 +1,30 @@
+/*******************************************************************************
+ * Copyright (c) 2006 - 2006 Mylar eclipse.org project and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Mylar project committers - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.mylar.internal.trac.core;
+
+/**
+ * Indicates an authentication error during login.
+ *
+ * @author Steffen Pingel
+ */
+public class TracLoginException extends TracException {
+
+ private static final long serialVersionUID = -6128773690643367414L;
+
+ public TracLoginException() {
+ }
+
+ public TracLoginException(String message) {
+ super(message);
+ }
+
+}
diff --git a/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/TracRemoteException.java b/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/TracRemoteException.java
new file mode 100644
index 000000000..3923de9b8
--- /dev/null
+++ b/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/TracRemoteException.java
@@ -0,0 +1,39 @@
+/*******************************************************************************
+ * Copyright (c) 2006 - 2006 Mylar eclipse.org project and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Mylar project committers - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.mylar.internal.trac.core;
+
+/**
+ * Indicates that an exception on the repository side has been encountered while
+ * processing the request.
+ *
+ * @author Steffen Pingel
+ */
+public class TracRemoteException extends TracException {
+
+ private static final long serialVersionUID = -6761365344287289624L;
+
+ public TracRemoteException() {
+ }
+
+ public TracRemoteException(String message) {
+ super(message);
+ }
+
+ public TracRemoteException(Throwable cause) {
+ super(cause);
+ }
+
+ public TracRemoteException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+}
diff --git a/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/TracXmlRpcClient.java b/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/TracXmlRpcClient.java
new file mode 100644
index 000000000..8e7a694fa
--- /dev/null
+++ b/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/TracXmlRpcClient.java
@@ -0,0 +1,449 @@
+package org.eclipse.mylar.internal.trac.core;
+
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TimeZone;
+
+import org.apache.xmlrpc.XmlRpcException;
+import org.apache.xmlrpc.client.XmlRpcClient;
+import org.apache.xmlrpc.client.XmlRpcClientConfigImpl;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.OperationCanceledException;
+import org.eclipse.mylar.context.core.MylarStatusHandler;
+import org.eclipse.mylar.internal.trac.core.model.TracAttachment;
+import org.eclipse.mylar.internal.trac.core.model.TracComment;
+import org.eclipse.mylar.internal.trac.core.model.TracComponent;
+import org.eclipse.mylar.internal.trac.core.model.TracMilestone;
+import org.eclipse.mylar.internal.trac.core.model.TracPriority;
+import org.eclipse.mylar.internal.trac.core.model.TracSearch;
+import org.eclipse.mylar.internal.trac.core.model.TracSeverity;
+import org.eclipse.mylar.internal.trac.core.model.TracTicket;
+import org.eclipse.mylar.internal.trac.core.model.TracTicketResolution;
+import org.eclipse.mylar.internal.trac.core.model.TracTicketStatus;
+import org.eclipse.mylar.internal.trac.core.model.TracTicketType;
+import org.eclipse.mylar.internal.trac.core.model.TracVersion;
+import org.eclipse.mylar.internal.trac.core.model.TracTicket.Key;
+import org.eclipse.mylar.internal.trac.core.util.TracHttpClientTransportFactory;
+import org.eclipse.mylar.internal.trac.core.util.TracUtils;
+import org.eclipse.mylar.internal.trac.core.util.TracHttpClientTransportFactory.TracHttpException;
+
+/**
+ * Represents a Trac repository that is accessed through the Trac XmlRpcPlugin.
+ *
+ * @author Steffen Pingel
+ */
+public class TracXmlRpcClient extends AbstractTracClient {
+
+ public static final String XMLRPC_URL = "/xmlrpc";
+
+ public static final String REQUIRED_REVISION = "1188";
+
+ private XmlRpcClient xmlrpc;
+
+ public TracXmlRpcClient(URL url, Version version, String username, String password) {
+ super(url, version, username, password);
+ }
+
+ public synchronized XmlRpcClient getClient() throws TracException {
+ if (xmlrpc != null) {
+ return xmlrpc;
+ }
+
+ XmlRpcClientConfigImpl config = new XmlRpcClientConfigImpl();
+ config.setEncoding(ITracClient.CHARSET);
+ config.setBasicUserName(username);
+ config.setBasicPassword(password);
+ config.setServerURL(getXmlRpcUrl());
+ config.setTimeZone(TimeZone.getTimeZone(ITracClient.TIME_ZONE));
+
+ xmlrpc = new XmlRpcClient();
+ xmlrpc.setConfig(config);
+
+ TracHttpClientTransportFactory factory = new TracHttpClientTransportFactory(xmlrpc);
+ xmlrpc.setTransportFactory(factory);
+
+ return xmlrpc;
+ }
+
+ private URL getXmlRpcUrl() throws TracException {
+ try {
+ String location = repositoryUrl.toString();
+ if (hasAuthenticationCredentials()) {
+ location += LOGIN_URL;
+ }
+ location += XMLRPC_URL;
+
+ return new URL(location);
+ } catch (Exception e) {
+ throw new TracException(e);
+ }
+ }
+
+ private Object call(String method, Object... parameters) throws TracException {
+ getClient();
+
+ try {
+ return xmlrpc.execute(method, parameters);
+ } catch (TracHttpException e) {
+ if (e.code == HttpURLConnection.HTTP_FORBIDDEN || e.code == HttpURLConnection.HTTP_UNAUTHORIZED) {
+ throw new TracLoginException();
+ } else {
+ throw new TracException(e);
+ }
+ } catch (XmlRpcException e) {
+ throw new TracRemoteException(e);
+ } catch (Exception e) {
+ throw new TracException(e);
+ }
+ }
+
+ private Object[] multicall(Map<String, Object>... calls) throws TracException {
+ Object[] result = (Object[]) call("system.multicall", new Object[] { calls });
+ for (Object item : result) {
+ try {
+ checkForException(item);
+ } catch (XmlRpcException e) {
+ throw new TracRemoteException(e);
+ } catch (Exception e) {
+ throw new TracException(e);
+ }
+ }
+ return result;
+ }
+
+ private void checkForException(Object result) throws NumberFormatException, XmlRpcException {
+ if (result instanceof Map) {
+ Map exceptionData = (Map) result;
+ if (exceptionData.containsKey("faultCode") && exceptionData.containsKey("faultString")) {
+ throw new XmlRpcException(Integer.parseInt(exceptionData.get("faultCode").toString()),
+ (String) exceptionData.get("faultString"));
+ }
+ }
+ }
+
+ private Map<String, Object> createMultiCall(String methodName, Object... parameters) throws TracException {
+ Map<String, Object> table = new HashMap<String, Object>();
+ table.put("methodName", methodName);
+ table.put("params", parameters);
+ return table;
+ }
+
+ private Object getMultiCallResult(Object item) {
+ return ((Object[]) item)[0];
+ }
+
+ public void validate() throws TracException {
+ Object[] result = (Object[]) call("system.listMethods");
+ boolean hasGetTicket = false, hasQuery = false, isRecentRevision = false;
+ for (Object methodName : result) {
+ if ("ticket.get".equals(methodName)) {
+ hasGetTicket = true;
+ }
+ if ("ticket.query".equals(methodName)) {
+ hasQuery = true;
+ }
+ if ("ticket.getRecentChanges".equals(methodName)) {
+ // this call was added in rev. 1188
+ isRecentRevision = true;
+ }
+
+ if (hasGetTicket && hasQuery && isRecentRevision) {
+ return;
+ }
+ }
+
+ throw new TracException("Required API calls are missing, please update your Trac XML-RPC Plugin to revision " + REQUIRED_REVISION + " or later");
+ }
+
+ public TracTicket getTicket(int id) throws TracException {
+ Object[] result = (Object[]) call("ticket.get", id);
+ TracTicket ticket = parseTicket(result);
+
+ result = (Object[]) call("ticket.changeLog", id, 0);
+ for (Object item : result) {
+ ticket.addComment(parseChangeLogEntry((Object[]) item));
+ }
+
+ result = (Object[]) call("ticket.listAttachments", id);
+ for (Object item : result) {
+ ticket.addAttachment(parseAttachment((Object[]) item));
+ }
+
+ String[] actions = getActions(id);
+ ticket.setActions(actions);
+
+ ticket.setResolutions(getDefaultTicketResolutions());
+
+ return ticket;
+ }
+
+ private TracAttachment parseAttachment(Object[] entry) {
+ TracAttachment attachment = new TracAttachment((String) entry[0]);
+ attachment.setDescription((String) entry[1]);
+ attachment.setSize((Integer) entry[2]);
+ attachment.setCreated(TracUtils.parseDate((Integer) entry[3]));
+ attachment.setAuthor((String) entry[4]);
+ return attachment;
+ }
+
+ private TracComment parseChangeLogEntry(Object[] entry) {
+ TracComment comment = new TracComment();
+ comment.setCreated(TracUtils.parseDate((Integer) entry[0]));
+ comment.setAuthor((String) entry[1]);
+ comment.setField((String) entry[2]);
+ comment.setOldValue((String) entry[3]);
+ comment.setNewValue((String) entry[4]);
+ return comment;
+ }
+
+ /* public for testing */
+ @SuppressWarnings("unchecked")
+ public List<TracTicket> getTickets(int[] ids) throws TracException {
+ Map<String, Object>[] calls = new Map[ids.length];
+ for (int i = 0; i < calls.length; i++) {
+ calls[i] = createMultiCall("ticket.get", ids[i]);
+ }
+
+ Object[] result = multicall(calls);
+ assert result.length == ids.length;
+
+ List<TracTicket> tickets = new ArrayList<TracTicket>(result.length);
+ for (Object item : result) {
+ Object[] ticketResult = (Object[]) getMultiCallResult(item);
+ tickets.add(parseTicket(ticketResult));
+ }
+
+ return tickets;
+ }
+
+ @SuppressWarnings("unchecked")
+ public void search(TracSearch query, List<TracTicket> tickets) throws TracException {
+ // an empty query string is not valid, therefore prepend order
+ Object[] result = (Object[]) call("ticket.query", "order=id" + query.toQuery());
+
+ Map<String, Object>[] calls = new Map[result.length];
+ for (int i = 0; i < calls.length; i++) {
+ calls[i] = createMultiCall("ticket.get", result[i]);
+ }
+ result = multicall(calls);
+
+ for (Object item : result) {
+ Object[] ticketResult = (Object[]) getMultiCallResult(item);
+ tickets.add(parseTicket(ticketResult));
+ }
+ }
+
+ private TracTicket parseTicket(Object[] ticketResult) throws InvalidTicketException {
+ TracTicket ticket = new TracTicket((Integer) ticketResult[0]);
+ ticket.setCreated((Integer) ticketResult[1]);
+ ticket.setLastChanged((Integer) ticketResult[2]);
+ Map attributes = (Map) ticketResult[3];
+ for (Object key : attributes.keySet()) {
+ ticket.putValue(key.toString(), attributes.get(key).toString());
+ }
+ return ticket;
+ }
+
+ public synchronized void updateAttributes(IProgressMonitor monitor) throws TracException {
+ monitor.beginTask("Updating attributes", 8);
+
+ Object[] result = getAttributes("ticket.component");
+ data.components = new ArrayList<TracComponent>(result.length);
+ for (Object item : result) {
+ data.components.add(parseComponent((Map) getMultiCallResult(item)));
+ }
+ monitor.worked(1);
+ if (monitor.isCanceled())
+ throw new OperationCanceledException();
+
+ result = getAttributes("ticket.milestone");
+ data.milestones = new ArrayList<TracMilestone>(result.length);
+ for (Object item : result) {
+ data.milestones.add(parseMilestone((Map) getMultiCallResult(item)));
+ }
+ monitor.worked(1);
+ if (monitor.isCanceled())
+ throw new OperationCanceledException();
+
+ List<TicketAttributeResult> attributes = getTicketAttributes("ticket.priority");
+ data.priorities = new ArrayList<TracPriority>(result.length);
+ for (TicketAttributeResult attribute : attributes) {
+ data.priorities.add(new TracPriority(attribute.name, attribute.value));
+ }
+ Collections.sort(data.priorities);
+ monitor.worked(1);
+ if (monitor.isCanceled())
+ throw new OperationCanceledException();
+
+ attributes = getTicketAttributes("ticket.resolution");
+ data.ticketResolutions = new ArrayList<TracTicketResolution>(result.length);
+ for (TicketAttributeResult attribute : attributes) {
+ data.ticketResolutions.add(new TracTicketResolution(attribute.name, attribute.value));
+ }
+ Collections.sort(data.ticketResolutions);
+ monitor.worked(1);
+ if (monitor.isCanceled())
+ throw new OperationCanceledException();
+
+ attributes = getTicketAttributes("ticket.severity");
+ data.severities = new ArrayList<TracSeverity>(result.length);
+ for (TicketAttributeResult attribute : attributes) {
+ data.severities.add(new TracSeverity(attribute.name, attribute.value));
+ }
+ Collections.sort(data.severities);
+ monitor.worked(1);
+ if (monitor.isCanceled())
+ throw new OperationCanceledException();
+
+ attributes = getTicketAttributes("ticket.status");
+ data.ticketStatus = new ArrayList<TracTicketStatus>(result.length);
+ for (TicketAttributeResult attribute : attributes) {
+ data.ticketStatus.add(new TracTicketStatus(attribute.name, attribute.value));
+ }
+ Collections.sort(data.ticketStatus);
+ monitor.worked(1);
+ if (monitor.isCanceled())
+ throw new OperationCanceledException();
+
+ attributes = getTicketAttributes("ticket.type");
+ data.ticketTypes = new ArrayList<TracTicketType>(result.length);
+ for (TicketAttributeResult attribute : attributes) {
+ data.ticketTypes.add(new TracTicketType(attribute.name, attribute.value));
+ }
+ Collections.sort(data.ticketTypes);
+ monitor.worked(1);
+ if (monitor.isCanceled())
+ throw new OperationCanceledException();
+
+ result = getAttributes("ticket.version");
+ data.versions = new ArrayList<TracVersion>(result.length);
+ for (Object item : result) {
+ data.versions.add(parseVersion((Map) getMultiCallResult(item)));
+ }
+ monitor.worked(1);
+ if (monitor.isCanceled())
+ throw new OperationCanceledException();
+ }
+
+ private TracComponent parseComponent(Map result) {
+ TracComponent component = new TracComponent((String) result.get("name"));
+ component.setOwner((String) result.get("owner"));
+ component.setDescription((String) result.get("description"));
+ return component;
+ }
+
+ private TracMilestone parseMilestone(Map result) {
+ TracMilestone milestone = new TracMilestone((String) result.get("name"));
+ milestone.setCompleted(TracUtils.parseDate((Integer) result.get("completed")));
+ milestone.setDue(TracUtils.parseDate((Integer) result.get("due")));
+ milestone.setDescription((String) result.get("description"));
+ return milestone;
+ }
+
+ private TracVersion parseVersion(Map result) {
+ TracVersion version = new TracVersion((String) result.get("name"));
+ version.setTime(TracUtils.parseDate((Integer) result.get("time")));
+ version.setDescription((String) result.get("description"));
+ return version;
+ }
+
+ @SuppressWarnings("unchecked")
+ private Object[] getAttributes(String attributeType) throws TracException {
+ Object[] ids = (Object[]) call(attributeType + ".getAll");
+ Map<String, Object>[] calls = new Map[ids.length];
+ for (int i = 0; i < calls.length; i++) {
+ calls[i] = createMultiCall(attributeType + ".get", ids[i]);
+ }
+
+ Object[] result = multicall(calls);
+ assert result.length == ids.length;
+
+ return result;
+ }
+
+ @SuppressWarnings("unchecked")
+ private List<TicketAttributeResult> getTicketAttributes(String attributeType) throws TracException {
+ Object[] ids = (Object[]) call(attributeType + ".getAll");
+ Map<String, Object>[] calls = new Map[ids.length];
+ for (int i = 0; i < calls.length; i++) {
+ calls[i] = createMultiCall(attributeType + ".get", ids[i]);
+ }
+
+ Object[] result = multicall(calls);
+ assert result.length == ids.length;
+
+ List<TicketAttributeResult> attributes = new ArrayList<TicketAttributeResult>(result.length);
+ for (int i = 0; i < calls.length; i++) {
+ try {
+ TicketAttributeResult attribute = new TicketAttributeResult();
+ attribute.name = (String) ids[i];
+ attribute.value = Integer.parseInt((String) getMultiCallResult(result[i]));
+ attributes.add(attribute);
+ } catch (NumberFormatException e) {
+ MylarStatusHandler.log(e, "Invalid response from Trac repository for attribute type: '" + attributeType
+ + "'");
+ }
+ }
+
+ return attributes;
+ }
+
+ public byte[] getAttachmentData(int ticketId, String filename) throws TracException {
+ return (byte[]) call("ticket.getAttachment", ticketId, filename);
+ }
+
+ public void putAttachmentData(int ticketId, String filename, String description, byte[] data) throws TracException {
+ call("ticket.putAttachment", ticketId, filename, description, data, true);
+ }
+
+ private class TicketAttributeResult {
+
+ String name;
+
+ int value;
+
+ }
+
+ public void createTicket(TracTicket ticket) throws TracException {
+ Map<String, String> attributes = ticket.getValues();
+ String summary = attributes.remove(Key.SUMMARY.getKey());
+ String description = attributes.remove(Key.DESCRIPTION.getKey());
+ if (summary == null || description == null) {
+ throw new InvalidTicketException();
+ }
+ call("ticket.create", summary, description, attributes);
+ }
+
+ public void updateTicket(TracTicket ticket, String comment) throws TracException {
+ Map<String, String> attributes = ticket.getValues();
+ call("ticket.update", ticket.getId(), comment, attributes);
+ }
+
+ public Set<Integer> getChangedTickets(Date since) throws TracException {
+ Object[] ids;
+ ids = (Object[]) call("ticket.getRecentChanges", since);
+ Set<Integer> result = new HashSet<Integer>();
+ for (Object id : ids) {
+ result.add((Integer) id);
+ }
+ return result;
+ }
+
+ public String[] getActions(int id) throws TracException {
+ Object[] actions = (Object[]) call("ticket.getAvailableActions", id);
+ String[] result = new String[actions.length];
+ for (int i = 0; i < result.length; i++) {
+ result[i] = (String) actions[i];
+ }
+ return result;
+ }
+
+} \ No newline at end of file
diff --git a/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/model/TracAttachment.java b/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/model/TracAttachment.java
new file mode 100644
index 000000000..6125ababb
--- /dev/null
+++ b/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/model/TracAttachment.java
@@ -0,0 +1,77 @@
+/*******************************************************************************
+ * Copyright (c) 2004 - 2006 Mylar committers and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *******************************************************************************/
+
+package org.eclipse.mylar.internal.trac.core.model;
+
+import java.util.Date;
+
+/**
+ * @author Steffen Pingel
+ */
+public class TracAttachment {
+
+ private String author;
+
+ private Date created;
+
+ private String description;
+
+ private String filename;
+
+ int size;
+
+ public TracAttachment(String filename) {
+ this.filename = filename;
+ }
+
+ public String getAuthor() {
+ return author;
+ }
+
+ public Date getCreated() {
+ return created;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public String getFilename() {
+ return filename;
+ }
+
+ public int getSize() {
+ return size;
+ }
+
+ public void setAuthor(String author) {
+ this.author = author;
+ }
+
+ public void setCreated(Date created) {
+ this.created = created;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public void setFilename(String filename) {
+ this.filename = filename;
+ }
+
+ public void setSize(int size) {
+ this.size = size;
+ }
+
+ @Override
+ public String toString() {
+ return filename;
+ }
+
+}
diff --git a/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/model/TracAttribute.java b/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/model/TracAttribute.java
new file mode 100644
index 000000000..5962eafba
--- /dev/null
+++ b/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/model/TracAttribute.java
@@ -0,0 +1,39 @@
+/*******************************************************************************
+ * Copyright (c) 2004 - 2006 Mylar committers and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *******************************************************************************/
+
+package org.eclipse.mylar.internal.trac.core.model;
+
+import java.io.Serializable;
+
+/**
+ * @author Steffen Pingel
+ */
+public class TracAttribute implements Serializable {
+
+ private static final long serialVersionUID = -4535033208999685315L;
+
+ private String name;
+
+ public TracAttribute(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+
+}
diff --git a/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/model/TracComment.java b/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/model/TracComment.java
new file mode 100644
index 000000000..d1be7585f
--- /dev/null
+++ b/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/model/TracComment.java
@@ -0,0 +1,86 @@
+/*******************************************************************************
+ * Copyright (c) 2004 - 2006 Mylar committers and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *******************************************************************************/
+
+package org.eclipse.mylar.internal.trac.core.model;
+
+import java.util.Date;
+
+/**
+ * @author Steffen Pingel
+ */
+public class TracComment {
+
+ private String author;
+
+ private Date created;
+
+ private String field;
+
+ private String newValue;
+
+ private String oldValue;
+
+ private boolean permanent;
+
+ public TracComment() {
+ }
+
+ public String getAuthor() {
+ return author;
+ }
+
+ public Date getCreated() {
+ return created;
+ }
+
+ public String getField() {
+ return field;
+ }
+
+ public String getNewValue() {
+ return newValue;
+ }
+
+ public String getOldValue() {
+ return oldValue;
+ }
+
+ public boolean isPermanent() {
+ return permanent;
+ }
+
+ public void setAuthor(String author) {
+ this.author = author;
+ }
+
+ public void setCreated(Date created) {
+ this.created = created;
+ }
+
+ public void setField(String field) {
+ this.field = field;
+ }
+
+ public void setNewValue(String newValue) {
+ this.newValue = newValue;
+ }
+
+ public void setOldValue(String oldValue) {
+ this.oldValue = oldValue;
+ }
+
+ public void setPermanent(boolean permanent) {
+ this.permanent = permanent;
+ }
+
+ @Override
+ public String toString() {
+ return "[" + field + "] " + author + ": " + oldValue + " -> " + newValue;
+ }
+
+}
diff --git a/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/model/TracComponent.java b/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/model/TracComponent.java
new file mode 100644
index 000000000..00088e9e2
--- /dev/null
+++ b/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/model/TracComponent.java
@@ -0,0 +1,42 @@
+/*******************************************************************************
+ * Copyright (c) 2004 - 2006 Mylar committers and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *******************************************************************************/
+
+package org.eclipse.mylar.internal.trac.core.model;
+
+/**
+ * @author Steffen Pingel
+ */
+public class TracComponent extends TracAttribute {
+
+ private static final long serialVersionUID = -6181067219323677076L;
+
+ private String owner;
+
+ private String description;
+
+ public TracComponent(String name) {
+ super(name);
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public String getOwner() {
+ return owner;
+ }
+
+ public void setOwner(String owner) {
+ this.owner = owner;
+ }
+
+}
diff --git a/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/model/TracMilestone.java b/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/model/TracMilestone.java
new file mode 100644
index 000000000..935689e12
--- /dev/null
+++ b/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/model/TracMilestone.java
@@ -0,0 +1,55 @@
+/*******************************************************************************
+ * Copyright (c) 2004 - 2006 Mylar committers and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *******************************************************************************/
+
+package org.eclipse.mylar.internal.trac.core.model;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * @author Steffen Pingel
+ */
+public class TracMilestone extends TracAttribute implements Serializable {
+
+ private static final long serialVersionUID = 6648558552508886484L;
+
+ private Date due;
+
+ private Date completed;
+
+ private String description;
+
+ public TracMilestone(String name) {
+ super(name);
+ }
+
+ public Date getCompleted() {
+ return completed;
+ }
+
+ public void setCompleted(Date completed) {
+ this.completed = completed;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public Date getDue() {
+ return due;
+ }
+
+ public void setDue(Date due) {
+ this.due = due;
+ }
+
+}
diff --git a/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/trac/core/internal/DELETE.java b/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/model/TracPriority.java
index edd87f71c..eed975522 100644
--- a/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/trac/core/internal/DELETE.java
+++ b/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/model/TracPriority.java
@@ -6,8 +6,17 @@
* http://www.eclipse.org/legal/epl-v10.html
*******************************************************************************/
-package org.eclipse.mylar.trac.core.internal;
+package org.eclipse.mylar.internal.trac.core.model;
-public class DELETE {
+/**
+ * @author Steffen Pingel
+ */
+public class TracPriority extends TracTicketAttribute {
+ private static final long serialVersionUID = 3617078252773178266L;
+
+ public TracPriority(String name, int value) {
+ super(name, value);
+ }
+
}
diff --git a/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/model/TracSearch.java b/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/model/TracSearch.java
new file mode 100644
index 000000000..bcc8784c8
--- /dev/null
+++ b/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/model/TracSearch.java
@@ -0,0 +1,150 @@
+/*******************************************************************************
+ * Copyright (c) 2006 - 2006 Mylar eclipse.org project and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Mylar project committers - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.mylar.internal.trac.core.model;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.mylar.context.core.MylarStatusHandler;
+import org.eclipse.mylar.internal.trac.core.ITracClient;
+import org.eclipse.mylar.internal.trac.core.model.TracSearchFilter.CompareOperator;
+
+/**
+ * Represents a Trac search. A search can have multiple {@link TracSearchFilter}s
+ * that all need to match.
+ *
+ * @author Steffen Pingel
+ */
+public class TracSearch {
+
+ /** Stores search criteria in the order entered by the user. */
+ private Map<String, TracSearchFilter> filterByFieldName = new LinkedHashMap<String, TracSearchFilter>();
+
+ /** The field the result is ordered by. */
+ private String orderBy;
+
+ private boolean ascending = true;
+
+ public TracSearch() {
+ }
+
+ public void addFilter(String key, String value) {
+ TracSearchFilter filter = filterByFieldName.get(key);
+ if (filter == null) {
+ filter = new TracSearchFilter(key);
+ CompareOperator operator = CompareOperator.fromUrl(value);
+ filter.setOperator(operator);
+ filterByFieldName.put(key, filter);
+ }
+
+ filter.addValue(value.substring(filter.getOperator().getQueryValue().length()));
+ }
+
+ public void addFilter(TracSearchFilter filter) {
+ filterByFieldName.put(filter.getFieldName(), filter);
+ }
+
+ public List<TracSearchFilter> getFilters() {
+ return new ArrayList<TracSearchFilter>(filterByFieldName.values());
+ }
+
+ public void setAscending(boolean ascending) {
+ this.ascending = ascending;
+ }
+
+ public boolean isAscending() {
+ return ascending;
+ }
+
+ public void setOrderBy(String orderBy) {
+ this.orderBy = orderBy;
+ }
+
+ public String getOrderBy() {
+ return orderBy;
+ }
+
+ /**
+ * Returns a Trac query string that conforms to the format defined at
+ * {@link http://projects.edgewall.com/trac/wiki/TracQuery#QueryLanguage}.
+ *
+ * @return the empty string, if no search order and criteria are defined; a
+ * string that starts with &amp;, otherwise
+ */
+ public String toQuery() {
+ StringBuilder sb = new StringBuilder();
+ if (orderBy != null) {
+ sb.append("&order=");
+ sb.append(orderBy);
+ if (!ascending) {
+ sb.append("&desc=1");
+ }
+ }
+ for (TracSearchFilter filter : filterByFieldName.values()) {
+ sb.append("&");
+ sb.append(filter.getFieldName());
+ sb.append(filter.getOperator().getQueryValue());
+ sb.append("=");
+ List<String> values = filter.getValues();
+ for (Iterator<String> it = values.iterator(); it.hasNext();) {
+ sb.append(it.next());
+ if (it.hasNext()) {
+ sb.append("|");
+ }
+ }
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Returns a URL encoded string that can be passed as an argument to the
+ * Trac query script.
+ *
+ * @return the empty string, if no search order and criteria are defined; a
+ * string that starts with &amp;, otherwise
+ */
+ public String toUrl() {
+ StringBuilder sb = new StringBuilder();
+ if (orderBy != null) {
+ sb.append("&order=");
+ sb.append(orderBy);
+ if (!ascending) {
+ sb.append("&desc=1");
+ }
+ } else if (filterByFieldName.isEmpty()) {
+ // TODO figure out why search must be ordered when logged in (otherwise
+ // no results will be returned)
+ sb.append("&order=id");
+ }
+
+ for (TracSearchFilter filter : filterByFieldName.values()) {
+ for (String value : filter.getValues()) {
+ sb.append("&");
+ sb.append(filter.getFieldName());
+ sb.append("=");
+ try {
+ sb.append(URLEncoder.encode(filter.getOperator().getQueryValue(), ITracClient.CHARSET));
+ sb.append(URLEncoder.encode(value, ITracClient.CHARSET));
+ } catch (UnsupportedEncodingException e) {
+ MylarStatusHandler.log(e, "Unexpected exception while decoding URL");
+ }
+ }
+ }
+ return sb.toString();
+ }
+
+}
diff --git a/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/model/TracSearchFilter.java b/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/model/TracSearchFilter.java
new file mode 100644
index 000000000..42f5c2942
--- /dev/null
+++ b/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/model/TracSearchFilter.java
@@ -0,0 +1,108 @@
+/*******************************************************************************
+ * Copyright (c) 2006 - 2006 Mylar eclipse.org project and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Mylar project committers - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.mylar.internal.trac.core.model;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Represents a search criterion. Each criterion is applied to a field such as
+ * milestone or priority. It has a compare operator and a list of values. The
+ * compare mode is <code>OR</code> for the operators <code>contains</code>,
+ * <code>starts with</code>, <code>ends with</code> and <code>is</code>.
+ * The compare mode is <code>AND</code> for all other (negated) operators.
+ *
+ * @author Steffen Pingel
+ */
+public class TracSearchFilter {
+
+ public enum CompareOperator {
+ CONTAINS("~"), CONTAINS_NOT("!~"), BEGINS_WITH("^"), NOT_BEGINS_WITH("!^"), ENDS_WITH("$"), NOT_ENDS_WITH("!$"), IS(
+ ""), IS_NOT("!");
+
+ public static CompareOperator fromUrl(String value) {
+ for (CompareOperator operator : values()) {
+ if (operator != IS && operator != IS_NOT && value.startsWith(operator.queryValue)) {
+ return operator;
+ }
+ }
+ if (value.startsWith(IS_NOT.queryValue)) {
+ return IS_NOT;
+ }
+ return IS;
+ }
+
+ /** The string that represent the operator in a Trac query. */
+ private String queryValue;
+
+ CompareOperator(String queryValue) {
+ this.queryValue = queryValue;
+ }
+
+ public String getQueryValue() {
+ return queryValue;
+ }
+
+ public String toString() {
+ switch (this) {
+ case CONTAINS:
+ return "contains";
+ case CONTAINS_NOT:
+ return "does not contain";
+ case BEGINS_WITH:
+ return "begins with";
+ case NOT_BEGINS_WITH:
+ return "does not begin with";
+ case ENDS_WITH:
+ return "ends with";
+ case NOT_ENDS_WITH:
+ return "does not end with";
+ case IS_NOT:
+ return "is not";
+ default:
+ return "is";
+ }
+ }
+
+ }
+
+ private String fieldName;
+
+ private CompareOperator operator;
+
+ private List<String> values = new ArrayList<String>();
+
+ public TracSearchFilter(String fieldName) {
+ this.fieldName = fieldName;
+ }
+
+ public void addValue(String value) {
+ values.add(value);
+ }
+
+ public String getFieldName() {
+ return fieldName;
+ }
+
+ public CompareOperator getOperator() {
+ return operator;
+ }
+
+ public List<String> getValues() {
+ return values;
+ }
+
+ public void setOperator(CompareOperator operator) {
+ this.operator = operator;
+ }
+
+}
diff --git a/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/model/TracSeverity.java b/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/model/TracSeverity.java
new file mode 100644
index 000000000..7a516f4db
--- /dev/null
+++ b/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/model/TracSeverity.java
@@ -0,0 +1,22 @@
+/*******************************************************************************
+ * Copyright (c) 2004 - 2006 Mylar committers and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *******************************************************************************/
+
+package org.eclipse.mylar.internal.trac.core.model;
+
+/**
+ * @author Steffen Pingel
+ */
+public class TracSeverity extends TracTicketAttribute {
+
+ private static final long serialVersionUID = 2173932517704827316L;
+
+ public TracSeverity(String name, int value) {
+ super(name, value);
+ }
+
+}
diff --git a/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/model/TracTicket.java b/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/model/TracTicket.java
new file mode 100644
index 000000000..9c91acd5b
--- /dev/null
+++ b/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/model/TracTicket.java
@@ -0,0 +1,225 @@
+/*******************************************************************************
+ * Copyright (c) 2006 - 2006 Mylar eclipse.org project and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Mylar project committers - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.mylar.internal.trac.core.model;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.mylar.internal.trac.core.InvalidTicketException;
+import org.eclipse.mylar.internal.trac.core.util.TracUtils;
+
+/**
+ * Represents a Trac ticket as it is retrieved from a Trac repository.
+ *
+ * @author Steffen Pingel
+ */
+public class TracTicket {
+
+ /**
+ * Represents the key of a string propertiy of a ticket.
+ *
+ * @author Steffen Pingel
+ */
+ public enum Key {
+ CC("cc"), CHANGE_TIME("changetime"), COMPONENT("component"), DESCRIPTION("description"), ID("id"), KEYWORDS(
+ "keywords"), MILESTONE("milestone"), OWNER("owner"), PRIORITY("priority"), REPORTER("reporter"), RESOLUTION(
+ "resolution"), STATUS("status"), SEVERITY("severity"), SUMMARY("summary"), TIME("time"), TYPE("type"), VERSION(
+ "version");
+
+ public static Key fromKey(String name) {
+ for (Key key : Key.values()) {
+ if (key.getKey().equals(name)) {
+ return key;
+ }
+ }
+ return null;
+ }
+
+ private String key;
+
+ Key(String key) {
+ this.key = key;
+ }
+
+ public String toString() {
+ return key;
+ }
+
+ public String getKey() {
+ return key;
+ }
+ }
+
+ public static final int INVALID_ID = -1;
+
+ private Date created;
+
+ /**
+ * User defined custom ticket fields.
+ *
+ * @see http://projects.edgewall.com/trac/wiki/TracTicketsCustomFields
+ */
+ private Map<String, String> customValueByKey;
+
+ private int id = INVALID_ID;
+
+ private Date lastChanged;
+
+ /** Trac's built-in ticket properties. */
+ private Map<Key, String> valueByKey = new HashMap<Key, String>();
+
+ private List<TracComment> comments;
+
+ private List<TracAttachment> attachments;
+
+ private String[] actions;
+
+ private String[] resolutions;
+
+ public TracTicket() {
+ }
+
+ /**
+ * Constructs a Trac ticket.
+ *
+ * @param id
+ * the nummeric Trac ticket id
+ */
+ public TracTicket(int id) {
+ this.id = id;
+ }
+
+ public Date getCreated() {
+ return created;
+ }
+
+ public int getId() {
+ return id;
+ }
+
+ public Date getLastChanged() {
+ return lastChanged;
+ }
+
+ public String getCustomValue(String key) {
+ if (customValueByKey == null) {
+ return null;
+ }
+ return customValueByKey.get(key);
+ }
+
+ public String getValue(Key key) {
+ return valueByKey.get(key);
+ }
+
+ public Map<String, String> getValues() {
+ Map<String, String> result = new HashMap<String, String>();
+ for (Key key : valueByKey.keySet()) {
+ result.put(key.getKey(), valueByKey.get(key));
+ }
+ if (customValueByKey != null) {
+ result.putAll(customValueByKey);
+ }
+ return result;
+ }
+
+ public boolean isValid() {
+ return getId() != TracTicket.INVALID_ID;
+ }
+
+ public void putBuiltinValue(Key key, String value) throws InvalidTicketException {
+ valueByKey.put(key, value);
+ }
+
+ public void putCustomValue(String key, String value) {
+ if (customValueByKey == null) {
+ customValueByKey = new HashMap<String, String>();
+ }
+ customValueByKey.put(key, value);
+ }
+
+ /**
+ * Stores a value as it is retrieved from the repository.
+ *
+ * @throws InvalidTicketException
+ * thrown if the type of <code>value</code> is not valid
+ */
+ public boolean putValue(String keyName, String value) throws InvalidTicketException {
+ Key key = Key.fromKey(keyName);
+ if (key != null) {
+ if (key == Key.ID || key == Key.TIME || key == Key.CHANGE_TIME) {
+ return false;
+ }
+ putBuiltinValue(key, value);
+ } else if (value instanceof String) {
+ putCustomValue(keyName, (String) value);
+ } else {
+ throw new InvalidTicketException("Expected string value for custom key '" + keyName + "', got '" + value
+ + "'");
+ }
+ return true;
+ }
+
+ public void setCreated(int created) {
+ this.created = TracUtils.parseDate(created);
+ }
+
+ public void setId(int id) {
+ this.id = id;
+ }
+
+ public void setLastChanged(int lastChanged) {
+ this.lastChanged = TracUtils.parseDate(lastChanged);
+ }
+
+ public void addComment(TracComment comment) {
+ if (comments == null) {
+ comments = new ArrayList<TracComment>();
+ }
+ comments.add(comment);
+ }
+
+ public void addAttachment(TracAttachment attachment) {
+ if (attachments == null) {
+ attachments = new ArrayList<TracAttachment>();
+ }
+ attachments.add(attachment);
+ }
+
+ public TracComment[] getComments() {
+ return (comments != null) ? comments.toArray(new TracComment[0]) : null;
+ }
+
+ public TracAttachment[] getAttachments() {
+ return (attachments != null) ? attachments.toArray(new TracAttachment[0]) : null;
+ }
+
+ public void setActions(String[] actions) {
+ this.actions = actions;
+ }
+
+ public String[] getActions() {
+ return actions;
+ }
+
+ public void setResolutions(String[] resolutions) {
+ this.resolutions = resolutions;
+ }
+
+ public String[] getResolutions() {
+ return resolutions;
+ }
+
+}
diff --git a/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/model/TracTicketAttribute.java b/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/model/TracTicketAttribute.java
new file mode 100644
index 000000000..01e4d5b2d
--- /dev/null
+++ b/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/model/TracTicketAttribute.java
@@ -0,0 +1,46 @@
+/*******************************************************************************
+ * Copyright (c) 2004 - 2006 Mylar committers and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *******************************************************************************/
+
+package org.eclipse.mylar.internal.trac.core.model;
+
+import java.io.Serializable;
+
+/**
+ * @author Steffen Pingel
+ */
+public class TracTicketAttribute implements Comparable<TracTicketAttribute>, Serializable {
+
+ private static final long serialVersionUID = -8611030780681519787L;
+
+ private String name;
+
+ private int value;
+
+ public TracTicketAttribute(String name, int value) {
+ this.name = name;
+ this.value = value;
+ }
+
+ public int compareTo(TracTicketAttribute o) {
+ return value - o.value;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public int getValue() {
+ return value;
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+
+}
diff --git a/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/model/TracTicketResolution.java b/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/model/TracTicketResolution.java
new file mode 100644
index 000000000..71c863c3e
--- /dev/null
+++ b/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/model/TracTicketResolution.java
@@ -0,0 +1,22 @@
+/*******************************************************************************
+ * Copyright (c) 2004 - 2006 Mylar committers and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *******************************************************************************/
+
+package org.eclipse.mylar.internal.trac.core.model;
+
+/**
+ * @author Steffen Pingel
+ */
+public class TracTicketResolution extends TracTicketAttribute {
+
+ private static final long serialVersionUID = -6933211257044813716L;
+
+ public TracTicketResolution(String name, int value) {
+ super(name, value);
+ }
+
+}
diff --git a/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/model/TracTicketStatus.java b/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/model/TracTicketStatus.java
new file mode 100644
index 000000000..d95baba3f
--- /dev/null
+++ b/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/model/TracTicketStatus.java
@@ -0,0 +1,22 @@
+/*******************************************************************************
+ * Copyright (c) 2004 - 2006 Mylar committers and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *******************************************************************************/
+
+package org.eclipse.mylar.internal.trac.core.model;
+
+/**
+ * @author Steffen Pingel
+ */
+public class TracTicketStatus extends TracTicketAttribute {
+
+ private static final long serialVersionUID = -8844909853931772506L;
+
+ public TracTicketStatus(String name, int value) {
+ super(name, value);
+ }
+
+}
diff --git a/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/model/TracTicketType.java b/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/model/TracTicketType.java
new file mode 100644
index 000000000..c87673fa0
--- /dev/null
+++ b/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/model/TracTicketType.java
@@ -0,0 +1,22 @@
+/*******************************************************************************
+ * Copyright (c) 2004 - 2006 Mylar committers and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *******************************************************************************/
+
+package org.eclipse.mylar.internal.trac.core.model;
+
+/**
+ * @author Steffen Pingel
+ */
+public class TracTicketType extends TracTicketAttribute {
+
+ private static final long serialVersionUID = -3157354751904259304L;
+
+ public TracTicketType(String name, int value) {
+ super(name, value);
+ }
+
+}
diff --git a/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/model/TracVersion.java b/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/model/TracVersion.java
new file mode 100644
index 000000000..f65a6b007
--- /dev/null
+++ b/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/model/TracVersion.java
@@ -0,0 +1,44 @@
+/*******************************************************************************
+ * Copyright (c) 2004 - 2006 Mylar committers and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *******************************************************************************/
+
+package org.eclipse.mylar.internal.trac.core.model;
+
+import java.util.Date;
+
+/**
+ * @author Steffen Pingel
+ */
+public class TracVersion extends TracAttribute {
+
+ private static final long serialVersionUID = 9018237956062697410L;
+
+ private Date time;
+
+ private String description;
+
+ public TracVersion(String name) {
+ super(name);
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public Date getTime() {
+ return time;
+ }
+
+ public void setTime(Date time) {
+ this.time = time;
+ }
+
+}
diff --git a/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/util/TracHttpClientTransportFactory.java b/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/util/TracHttpClientTransportFactory.java
new file mode 100644
index 000000000..bc94ce5d8
--- /dev/null
+++ b/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/util/TracHttpClientTransportFactory.java
@@ -0,0 +1,234 @@
+/*******************************************************************************
+ * Copyright (c) 2006 - 2006 Mylar eclipse.org project and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Mylar project committers - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.mylar.internal.trac.core.util;
+
+import java.io.BufferedOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.net.HttpURLConnection;
+import java.net.Proxy;
+import java.util.zip.GZIPInputStream;
+
+import org.apache.commons.httpclient.HttpClient;
+import org.apache.commons.httpclient.HttpVersion;
+import org.apache.commons.httpclient.methods.PostMethod;
+import org.apache.commons.httpclient.methods.RequestEntity;
+import org.apache.xmlrpc.XmlRpcException;
+import org.apache.xmlrpc.XmlRpcRequest;
+import org.apache.xmlrpc.client.XmlRpcClient;
+import org.apache.xmlrpc.client.XmlRpcCommonsTransport;
+import org.apache.xmlrpc.client.XmlRpcHttpClientConfig;
+import org.apache.xmlrpc.client.XmlRpcTransport;
+import org.apache.xmlrpc.client.XmlRpcTransportFactoryImpl;
+import org.apache.xmlrpc.util.XmlRpcIOException;
+import org.eclipse.mylar.internal.tasks.core.WebClientUtil;
+
+/**
+ * A custom transport factory used to establish XML-RPC connections. Uses the
+ * Mylar proxy settings.
+ *
+ * @author Steffen Pingel
+ */
+public class TracHttpClientTransportFactory extends XmlRpcTransportFactoryImpl {
+
+ public static class TracHttpException extends XmlRpcException {
+
+ private static final long serialVersionUID = 9032521978140685830L;
+
+ public TracHttpException(int responseCode) {
+ super(responseCode, "HTTP Error " + responseCode);
+ }
+
+ }
+
+ /**
+ * A transport that uses the Apache HttpClient library.
+ */
+ public static class TracHttpClientTransport extends XmlRpcCommonsTransport {
+
+ private int contentLength;
+ private Proxy proxy;
+
+ public TracHttpClientTransport(XmlRpcClient client) {
+ super(client);
+
+ XmlRpcHttpClientConfig config = (XmlRpcHttpClientConfig) client.getConfig();
+ // this needs to be set to avoid exceptions
+ getHttpClient().getParams().setAuthenticationPreemptive(config.getBasicUserName() != null);
+ }
+
+ public HttpClient getHttpClient() {
+ return (HttpClient) getValue("client");
+ }
+
+ @Override
+ protected InputStream getInputStream() throws XmlRpcException {
+ int responseCode = getMethod().getStatusCode();
+ if (responseCode != HttpURLConnection.HTTP_OK) {
+ throw new TracHttpException(responseCode);
+ }
+
+ return super.getInputStream();
+ }
+
+ public PostMethod getMethod() {
+ return (PostMethod) getValue("method");
+ }
+
+ public void setMethod(PostMethod method) {
+ setValue("method", method);
+ }
+
+ private Object getValue(String name) {
+ try {
+ Field field = XmlRpcCommonsTransport.class.getDeclaredField(name);
+ field.setAccessible(true);
+ return field.get(this);
+ } catch (Throwable t) {
+ throw new RuntimeException("Internal error accessing HttpClient", t);
+ }
+ }
+
+ private void setValue(String name, Object value) {
+ try {
+ Field field = XmlRpcCommonsTransport.class.getDeclaredField(name);
+ field.setAccessible(true);
+ field.set(this, value);
+ } catch (Throwable t) {
+ throw new RuntimeException("Internal error accessing HttpClient", t);
+ }
+ }
+
+ /**
+ * Based on the implementation of XmlRpcCommonsTransport and its super classes.
+ */
+ @Override
+ public Object sendRequest(XmlRpcRequest pRequest) throws XmlRpcException {
+ XmlRpcHttpClientConfig config = (XmlRpcHttpClientConfig) pRequest.getConfig();
+
+ String url = config.getServerURL().toString();
+ WebClientUtil.setupHttpClient(getHttpClient(), proxy, url);
+
+ PostMethod method = new PostMethod(WebClientUtil.getRequestPath(url));
+
+ if (config.getConnectionTimeout() != 0)
+ getHttpClient().getHttpConnectionManager().getParams().setConnectionTimeout(config.getConnectionTimeout());
+
+ if (config.getReplyTimeout() != 0)
+ getHttpClient().getHttpConnectionManager().getParams().setSoTimeout(config.getConnectionTimeout());
+
+ method.getParams().setVersion(HttpVersion.HTTP_1_1);
+
+ setMethod(method);
+
+ initHttpHeaders(pRequest);
+
+ boolean closed = false;
+ try {
+ RequestWriter writer = newRequestWriter(pRequest);
+ writeRequest(writer);
+ InputStream istream = getInputStream();
+ if (isResponseGzipCompressed(config)) {
+ istream = new GZIPInputStream(istream);
+ }
+ Object result = readResponse(config, istream);
+ closed = true;
+ close();
+ return result;
+ } catch (IOException e) {
+ throw new XmlRpcException("Failed to read servers response: "
+ + e.getMessage(), e);
+ } finally {
+ if (!closed) { try { close(); } catch (Throwable ignore) {} }
+ }
+ }
+
+ @Override
+ protected void writeRequest(final RequestWriter pWriter) throws XmlRpcException {
+ getMethod().setRequestEntity(new RequestEntity(){
+ public boolean isRepeatable() { return true; }
+ public void writeRequest(OutputStream pOut) throws IOException {
+ /* Make sure, that the socket is not closed by replacing it with our
+ * own BufferedOutputStream.
+ */
+ BufferedOutputStream bos = new BufferedOutputStream(pOut){
+ public void close() throws IOException {
+ flush();
+ }
+ };
+ try {
+ Method m = RequestWriter.class.getDeclaredMethod("write", new Class[] { OutputStream.class });
+ m.setAccessible(true);
+ m.invoke(pWriter, bos);
+ } catch (Exception e) {
+ throw new XmlRpcIOException(e);
+ }
+ }
+ public long getContentLength() { return contentLength; }
+ public String getContentType() { return "text/xml"; }
+ });
+
+ try {
+ getHttpClient().executeMethod(getMethod());
+ } catch (XmlRpcIOException e) {
+ Throwable t = e.getLinkedException();
+ if (t instanceof XmlRpcException) {
+ throw (XmlRpcException) t;
+ } else {
+ throw new XmlRpcException("Unexpected exception: " + t.getMessage(), t);
+ }
+ } catch (IOException e) {
+ throw new XmlRpcException("I/O error while communicating with HTTP server: " + e.getMessage(), e);
+ }
+ }
+
+ @Override
+ protected void setContentLength(int pLength) {
+ super.setContentLength(pLength);
+
+ this.contentLength = pLength;
+ }
+
+ public void setProxy(Proxy proxy) {
+ this.proxy = proxy;
+ }
+
+ public Proxy getProxy() {
+ return this.proxy;
+ }
+
+ }
+
+ private final TracHttpClientTransport transport;
+
+ public TracHttpClientTransportFactory(XmlRpcClient client) {
+ super(client);
+
+ transport = new TracHttpClientTransport(client);
+ }
+
+ public XmlRpcTransport getTransport() {
+ return transport;
+ }
+
+ public void setProxy(Proxy proxy) {
+ transport.setProxy(proxy);
+ }
+
+ public Proxy getProxy() {
+ return transport.getProxy();
+ }
+
+}
diff --git a/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/util/TracUtils.java b/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/util/TracUtils.java
new file mode 100644
index 000000000..be7b91c55
--- /dev/null
+++ b/org.eclipse.mylyn.trac.core/src/org/eclipse/mylyn/internal/trac/core/util/TracUtils.java
@@ -0,0 +1,38 @@
+/*******************************************************************************
+ * Copyright (c) 2004 - 2006 Mylar committers and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *******************************************************************************/
+
+package org.eclipse.mylar.internal.trac.core.util;
+
+import java.util.Calendar;
+import java.util.Date;
+import java.util.TimeZone;
+
+import org.eclipse.mylar.internal.trac.core.ITracClient;
+
+/**
+ * Provides static helper methods.
+ *
+ * @author Steffen Pingel
+ */
+public class TracUtils {
+
+ public static Date parseDate(long seconds) {
+ Calendar c = Calendar.getInstance();
+ c.setTimeZone(TimeZone.getTimeZone(ITracClient.TIME_ZONE));
+ c.setTimeInMillis(seconds * 1000l);
+ return c.getTime();
+ }
+
+ public static long toTracTime(Date date) {
+ Calendar c = Calendar.getInstance();
+ c.setTime(date);
+ c.setTimeZone(TimeZone.getTimeZone(ITracClient.TIME_ZONE));
+ return c.getTimeInMillis() / 1000l;
+ }
+
+}

Back to the top