blob: a86c52ce20af41f04c04da7b8893587498b91ca5 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2006, 2010 Steffen Pingel and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* https://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Steffen Pingel - initial API and implementation
* Xiaoyang Guan - improvements
*******************************************************************************/
package org.eclipse.mylyn.internal.trac.core.client;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
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 java.util.regex.Pattern;
import org.apache.commons.httpclient.Credentials;
import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HostConfiguration;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.HttpState;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.auth.AuthScheme;
import org.apache.commons.httpclient.auth.AuthScope;
import org.apache.commons.httpclient.auth.BasicScheme;
import org.apache.commons.httpclient.auth.DigestScheme;
import org.apache.commons.httpclient.auth.NTLMScheme;
import org.apache.commons.httpclient.methods.HeadMethod;
import org.apache.xmlrpc.XmlRpcException;
import org.apache.xmlrpc.client.XmlRpcClient;
import org.apache.xmlrpc.client.XmlRpcClientConfigImpl;
import org.apache.xmlrpc.serializer.CharSetXmlWriterFactory;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.mylyn.commons.core.CoreUtil;
import org.eclipse.mylyn.commons.core.StatusHandler;
import org.eclipse.mylyn.commons.net.AbstractWebLocation;
import org.eclipse.mylyn.commons.net.AuthenticationCredentials;
import org.eclipse.mylyn.commons.net.AuthenticationType;
import org.eclipse.mylyn.commons.net.Policy;
import org.eclipse.mylyn.commons.net.UnsupportedRequestException;
import org.eclipse.mylyn.commons.net.WebUtil;
import org.eclipse.mylyn.internal.trac.core.TracCorePlugin;
import org.eclipse.mylyn.internal.trac.core.model.TracAction;
import org.eclipse.mylyn.internal.trac.core.model.TracAttachment;
import org.eclipse.mylyn.internal.trac.core.model.TracComment;
import org.eclipse.mylyn.internal.trac.core.model.TracComponent;
import org.eclipse.mylyn.internal.trac.core.model.TracMilestone;
import org.eclipse.mylyn.internal.trac.core.model.TracPriority;
import org.eclipse.mylyn.internal.trac.core.model.TracRepositoryInfo;
import org.eclipse.mylyn.internal.trac.core.model.TracSearch;
import org.eclipse.mylyn.internal.trac.core.model.TracSeverity;
import org.eclipse.mylyn.internal.trac.core.model.TracTicket;
import org.eclipse.mylyn.internal.trac.core.model.TracTicket.Key;
import org.eclipse.mylyn.internal.trac.core.model.TracTicketField;
import org.eclipse.mylyn.internal.trac.core.model.TracTicketField.Type;
import org.eclipse.mylyn.internal.trac.core.model.TracTicketResolution;
import org.eclipse.mylyn.internal.trac.core.model.TracTicketStatus;
import org.eclipse.mylyn.internal.trac.core.model.TracTicketType;
import org.eclipse.mylyn.internal.trac.core.model.TracVersion;
import org.eclipse.mylyn.internal.trac.core.model.TracWikiPage;
import org.eclipse.mylyn.internal.trac.core.model.TracWikiPageInfo;
import org.eclipse.mylyn.internal.trac.core.util.HttpMethodInterceptor;
import org.eclipse.mylyn.internal.trac.core.util.TracHttpClientTransportFactory;
import org.eclipse.mylyn.internal.trac.core.util.TracHttpClientTransportFactory.TracHttpException;
import org.eclipse.mylyn.internal.trac.core.util.TracUtil;
import org.eclipse.mylyn.internal.trac.core.util.TracXmlRpcClientRequest;
import org.eclipse.osgi.util.NLS;
/**
* Represents a Trac repository that is accessed through the Trac XmlRpcPlugin.
*
* @author Steffen Pingel
* @author Xiaoyang Guan
*/
public class TracXmlRpcClient extends AbstractTracClient implements ITracWikiClient {
private static final Pattern ERROR_PATTERN_RPC_METHOD_NOT_FOUND = Pattern.compile("RPC method \".*\" not found"); //$NON-NLS-1$
private static final Pattern ERROR_PATTERN_MID_AIR_COLLISION = Pattern.compile("Sorry, can not save your changes.*This ticket has been modified by someone else since you started"); //$NON-NLS-1$
private static final String ERROR_XML_RPC_PRIVILEGES_REQUIRED = "XML_RPC privileges are required to perform this operation"; //$NON-NLS-1$
private class XmlRpcRequest {
private final String method;
private final Object[] parameters;
public XmlRpcRequest(String method, Object[] parameters) {
this.method = method;
this.parameters = parameters;
}
public Object execute(IProgressMonitor monitor) throws TracException {
try {
// first attempt
return executeCallInternal(monitor);
} catch (TracPermissionDeniedException e) {
if (accountMangerAuthenticationFailed) {
// do not try again if this has failed in the past since it
// is more likely that XML_RPC permissions have not been set
throw e;
}
AuthenticationCredentials credentials = location.getCredentials(AuthenticationType.REPOSITORY);
if (!credentialsValid(credentials)) {
throw e;
}
// try form-based authentication via AccountManagerPlugin as a
// fall-back
HostConfiguration hostConfiguration = WebUtil.createHostConfiguration(httpClient, location, monitor);
try {
authenticateAccountManager(httpClient, hostConfiguration, credentials, monitor);
} catch (TracLoginException loginException) {
// caused by wrong user name or password
throw loginException;
} catch (IOException ignore) {
accountMangerAuthenticationFailed = true;
throw e;
}
try {
validateAuthenticationState(httpClient);
} catch (TracLoginException ignore) {
// most likely form based authentication is not supported by
// repository
accountMangerAuthenticationFailed = true;
throw e;
}
// the authentication information is available through the shared state in httpClient
}
// second attempt
return executeCallInternal(monitor);
}
private Object executeCallInternal(IProgressMonitor monitor) throws TracException {
try {
if (isTracd && digestScheme != null) {
probeAuthenticationScheme(monitor);
}
if (DEBUG_XMLRPC) {
System.err.println("Calling " + location.getUrl() + ": " + method + " " + CoreUtil.toString(parameters)); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
}
TracXmlRpcClientRequest request = new TracXmlRpcClientRequest(xmlrpc.getClientConfig(), method,
parameters, monitor);
return xmlrpc.execute(request);
} catch (TracHttpException e) {
handleAuthenticationException(e.code, e.getAuthScheme());
// if not handled, throw generic exception
throw new TracException(e);
} catch (XmlRpcException e) {
// XXX work-around for http://trac-hacks.org/ticket/5848
if (ERROR_XML_RPC_PRIVILEGES_REQUIRED.equals(e.getMessage()) || e.code == XML_FAULT_PERMISSION_DENIED) {
handleAuthenticationException(HttpStatus.SC_FORBIDDEN, null);
// should never happen as call above should always throw an exception
throw new TracRemoteException(e);
} else if (isNoSuchMethodException(e)) {
throw new TracNoSuchMethodException(e);
} else if (isMidAirCollision(e)) {
throw new TracMidAirCollisionException(e);
} else {
throw new TracRemoteException(e);
}
} catch (OperationCanceledException e) {
throw e;
} catch (Exception e) {
throw new TracException(e);
}
}
private boolean isMidAirCollision(XmlRpcException e) {
if (e.code == XML_FAULT_GENERAL_ERROR && e.getMessage() != null
&& ERROR_PATTERN_MID_AIR_COLLISION.matcher(e.getMessage()).find()) {
return true;
}
return false;
}
private boolean isNoSuchMethodException(XmlRpcException e) {
// the fault code is used for various errors, therefore detection is based on the message
// message format by XML-RPC Plugin version:
// 1.0.1: XML-RPC method "ticket.ge1t" not found
// 1.0.6: RPC method "ticket.ge1t" not found
// 1.10: RPC method "ticket.ge1t" not found' while executing 'ticket.ge1t()
if (e.code == XML_FAULT_GENERAL_ERROR && e.getMessage() != null
&& ERROR_PATTERN_RPC_METHOD_NOT_FOUND.matcher(e.getMessage()).find()) {
return true;
}
return false;
}
protected boolean handleAuthenticationException(int code, AuthScheme authScheme) throws TracException {
if (code == HttpStatus.SC_UNAUTHORIZED) {
if (DEBUG_AUTH) {
System.err.println(location.getUrl() + ": Unauthorized (" + code + ")"); //$NON-NLS-1$ //$NON-NLS-2$
}
digestScheme = null;
TracLoginException exception = new TracLoginException();
exception.setNtlmAuthRequested(authScheme instanceof NTLMScheme);
throw exception;
} else if (code == HttpStatus.SC_FORBIDDEN) {
if (DEBUG_AUTH) {
System.err.println(location.getUrl() + ": Forbidden (" + code + ")"); //$NON-NLS-1$ //$NON-NLS-2$
}
digestScheme = null;
throw new TracPermissionDeniedException();
} else if (code == HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED) {
if (DEBUG_AUTH) {
System.err.println(location.getUrl() + ": Proxy authentication required (" + code + ")"); //$NON-NLS-1$ //$NON-NLS-2$
}
throw new TracProxyAuthenticationException();
} else if (code == SC_CERT_AUTH_FAILED) {
if (DEBUG_AUTH) {
System.err.println(location.getUrl() + ": Certificate authentication failed (" + code + ")"); //$NON-NLS-1$ //$NON-NLS-2$
}
throw new TracSslCertificateException();
}
return false;
}
}
private static final boolean DEBUG_XMLRPC = Boolean.valueOf(Platform.getDebugOption("org.eclipse.mylyn.trac.core/debug/xmlrpc")); //$NON-NLS-1$
public static final String XMLRPC_URL = "/xmlrpc"; //$NON-NLS-1$
public static final String REQUIRED_REVISION = "1950"; //$NON-NLS-1$
public static final int REQUIRED_EPOCH = 0;
public static final int REQUIRED_MAJOR = 0;
public static final int REQUIRED_MINOR = 1;
// XML-RPC Plugin <1.10
private static final int XML_FAULT_GENERAL_ERROR = 1;
// since XML-RPC Plugin 1.10
@SuppressWarnings("unused")
private static final int XML_FAULT_RESOURCE_NOT_FOUND = 404;
// since XML-RPC Plugin 1.10
private static final int XML_FAULT_PERMISSION_DENIED = 403;
private static final int LATEST_VERSION = -1;
public static final int REQUIRED_WIKI_RPC_VERSION = 2;
private XmlRpcClient xmlrpc;
private TracHttpClientTransportFactory factory;
private boolean accountMangerAuthenticationFailed;
private XmlRpcClientConfigImpl config;
private final HttpClient httpClient;
private boolean probed;
private volatile DigestScheme digestScheme;
private final AuthScope authScope;
private boolean isTracd;
private TracRepositoryInfo info = new TracRepositoryInfo();
public TracXmlRpcClient(AbstractWebLocation location, Version version) {
super(location, version);
this.httpClient = createHttpClient();
this.authScope = new AuthScope(WebUtil.getHost(repositoryUrl), WebUtil.getPort(repositoryUrl), null,
AuthScope.ANY_SCHEME);
}
public synchronized XmlRpcClient getClient() throws TracException {
if (xmlrpc == null) {
config = new XmlRpcClientConfigImpl();
config.setEncoding(ITracClient.CHARSET);
config.setTimeZone(TimeZone.getTimeZone(ITracClient.TIME_ZONE));
config.setContentLengthOptional(false);
config.setConnectionTimeout(WebUtil.getConnectionTimeout());
config.setReplyTimeout(WebUtil.getSocketTimeout());
xmlrpc = new XmlRpcClient();
xmlrpc.setConfig(config);
// bug 307200: force factory that supports proper UTF-8 encoding
xmlrpc.setXmlWriterFactory(new CharSetXmlWriterFactory());
factory = new TracHttpClientTransportFactory(xmlrpc, httpClient);
factory.setLocation(location);
factory.setInterceptor(new HttpMethodInterceptor() {
public void processRequest(HttpMethod method) {
DigestScheme scheme = digestScheme;
if (scheme != null) {
if (DEBUG_AUTH) {
System.err.println(location.getUrl() + ": Digest scheme is present"); //$NON-NLS-1$
}
Credentials creds = httpClient.getState().getCredentials(authScope);
if (creds != null) {
if (DEBUG_AUTH) {
System.err.println(location.getUrl() + ": Setting digest scheme for request"); //$NON-NLS-1$
}
method.getHostAuthState().setAuthScheme(digestScheme);
method.getHostAuthState().setAuthRequested(true);
}
}
}
public void processResponse(HttpMethod method) {
AuthScheme authScheme = method.getHostAuthState().getAuthScheme();
if (authScheme instanceof DigestScheme) {
digestScheme = (DigestScheme) authScheme;
if (DEBUG_AUTH) {
System.err.println(location.getUrl() + ": Received digest scheme"); //$NON-NLS-1$
}
}
}
});
xmlrpc.setTransportFactory(factory);
// update configuration with latest values
AuthenticationCredentials credentials = location.getCredentials(AuthenticationType.REPOSITORY);
config.setServerURL(getXmlRpcUrl(credentials));
if (credentialsValid(credentials)) {
Credentials httpCredentials = WebUtil.getHttpClientCredentials(credentials,
WebUtil.getHost(location.getUrl()));
httpClient.getState().setCredentials(authScope, httpCredentials);
// if (CoreUtil.TEST_MODE) {
// System.err.println(" Setting credentials: " + httpCredentials); //$NON-NLS-1$
// }
httpClient.getState().setCredentials(authScope, httpCredentials);
} else {
httpClient.getState().clearCredentials();
}
}
return xmlrpc;
}
private URL getXmlRpcUrl(AuthenticationCredentials credentials) throws TracException {
try {
String location = repositoryUrl.toString();
if (credentialsValid(credentials)) {
location += LOGIN_URL;
}
location += XMLRPC_URL;
return new URL(location);
} catch (Exception e) {
throw new TracException(e);
}
}
private void probeAuthenticationScheme(IProgressMonitor monitor) throws TracException {
AuthenticationCredentials credentials = location.getCredentials(AuthenticationType.REPOSITORY);
if (!credentialsValid(credentials)) {
return;
}
if (DEBUG_AUTH) {
System.err.println(location.getUrl() + ": Probing authentication"); //$NON-NLS-1$
}
HostConfiguration hostConfiguration = WebUtil.createHostConfiguration(httpClient, location, monitor);
HeadMethod method = new HeadMethod(getXmlRpcUrl(credentials).toString());
try {
// execute without any credentials set
int result = WebUtil.execute(httpClient, hostConfiguration, method, new HttpState(), monitor);
if (DEBUG_AUTH) {
System.err.println(location.getUrl() + ": Received authentication response (" + result + ")"); //$NON-NLS-1$ //$NON-NLS-2$
}
if (result == HttpStatus.SC_UNAUTHORIZED || result == HttpStatus.SC_FORBIDDEN) {
AuthScheme authScheme = method.getHostAuthState().getAuthScheme();
if (authScheme instanceof DigestScheme) {
this.digestScheme = (DigestScheme) authScheme;
if (DEBUG_AUTH) {
System.err.println(location.getUrl() + ": Received digest scheme"); //$NON-NLS-1$
}
} else if (authScheme instanceof BasicScheme) {
httpClient.getParams().setAuthenticationPreemptive(true);
if (DEBUG_AUTH) {
System.err.println(location.getUrl() + ": Received basic scheme"); //$NON-NLS-1$
}
} else if (authScheme != null) {
if (DEBUG_AUTH) {
System.err.println(location.getUrl() + ": Received scheme (" + authScheme.getClass() + ")"); //$NON-NLS-1$ //$NON-NLS-2$
}
} else {
if (DEBUG_AUTH) {
System.err.println(location.getUrl() + ": No authentication scheme received"); //$NON-NLS-1$
}
}
Header header = method.getResponseHeader("Server"); //$NON-NLS-1$
isTracd = (header != null && header.getValue().startsWith("tracd")); //$NON-NLS-1$
if (DEBUG_AUTH && isTracd) {
System.err.println(location.getUrl() + ": Tracd detected"); //$NON-NLS-1$
}
// Header header = method.getResponseHeader("WWW-Authenticate");
// if (header != null) {
// if (header.getValue().startsWith("Basic")) {
// httpClient.getParams().setAuthenticationPreemptive(true);
// } else if (header.getValue().startsWith("Digest")) {
// DigestScheme scheme = new DigestScheme();
// try {
// scheme.processChallenge(header.getValue());
// this.digestScheme = scheme;
// } catch (MalformedChallengeException e) {
// // ignore
// }
// }
// }
}
} catch (IOException e) {
// ignore
} finally {
WebUtil.releaseConnection(method, monitor);
}
}
private Object call(IProgressMonitor monitor, String method, Object... parameters) throws TracException {
monitor = Policy.monitorFor(monitor);
TracException lastException = null;
for (int attempt = 0; attempt < 3; attempt++) {
if (!probed) {
try {
probeAuthenticationScheme(monitor);
} finally {
probed = true;
}
}
getClient();
try {
XmlRpcRequest request = new XmlRpcRequest(method, parameters);
return request.execute(monitor);
} catch (TracLoginException e) {
try {
location.requestCredentials(AuthenticationType.REPOSITORY, null, monitor);
} catch (UnsupportedRequestException ignored) {
throw e;
}
lastException = e;
} catch (TracPermissionDeniedException e) {
try {
location.requestCredentials(AuthenticationType.REPOSITORY, null, monitor);
} catch (UnsupportedRequestException ignored) {
throw e;
}
lastException = e;
} catch (TracProxyAuthenticationException e) {
try {
location.requestCredentials(AuthenticationType.PROXY, null, monitor);
} catch (UnsupportedRequestException ignored) {
throw e;
}
lastException = e;
} catch (TracSslCertificateException e) {
try {
location.requestCredentials(AuthenticationType.CERTIFICATE, null, monitor);
} catch (UnsupportedRequestException ignored) {
throw e;
}
lastException = e;
}
}
if (lastException != null) {
throw lastException;
} else {
// this path should never be reached
throw new IllegalStateException();
}
}
private Object[] multicall(IProgressMonitor monitor, Map<String, Object>... calls) throws TracException {
Object[] result = (Object[]) call(monitor, "system.multicall", new Object[] { calls }); //$NON-NLS-1$
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")) { //$NON-NLS-1$ //$NON-NLS-2$
throw new XmlRpcException(Integer.parseInt(exceptionData.get("faultCode").toString()), //$NON-NLS-1$
(String) exceptionData.get("faultString")); //$NON-NLS-1$
} else if (exceptionData.containsKey("title")) { //$NON-NLS-1$
String message = (String) exceptionData.get("title"); //$NON-NLS-1$
String detail = (String) exceptionData.get("_message"); //$NON-NLS-1$
if (detail != null) {
message += ": " + detail; //$NON-NLS-1$
}
throw new XmlRpcException(XML_FAULT_GENERAL_ERROR, message);
}
}
}
private Map<String, Object> createMultiCall(String methodName, Object... parameters) throws TracException {
Map<String, Object> table = new HashMap<String, Object>();
table.put("methodName", methodName); //$NON-NLS-1$
table.put("params", parameters); //$NON-NLS-1$
return table;
}
private Object getMultiCallResult(Object item) {
return ((Object[]) item)[0];
}
public TracRepositoryInfo validate(IProgressMonitor monitor) throws TracException {
Integer epochAPIVersion;
Integer majorAPIVersion;
Integer minorAPIVersion;
try {
Object[] result = (Object[]) call(monitor, "system.getAPIVersion"); //$NON-NLS-1$
if (result.length >= 3) {
epochAPIVersion = (Integer) result[0];
majorAPIVersion = (Integer) result[1];
minorAPIVersion = (Integer) result[2];
} else if (result.length >= 2) {
epochAPIVersion = 0;
majorAPIVersion = (Integer) result[0];
minorAPIVersion = (Integer) result[1];
} else {
throw new TracException(NLS.bind(Messages.TracXmlRpcClient_API_version_unsupported_Error,
REQUIRED_REVISION));
}
} catch (TracNoSuchMethodException e) {
throw new TracException(NLS.bind(Messages.TracXmlRpcClient_Required_API_calls_missing_Error,
REQUIRED_REVISION));
}
info = new TracRepositoryInfo(epochAPIVersion, majorAPIVersion, minorAPIVersion);
if (!info.isApiVersionOrHigher(REQUIRED_EPOCH, REQUIRED_MAJOR, REQUIRED_MINOR)) {
throw new TracException(NLS.bind(Messages.TracXmlRpcClient_API_version_X_unsupported_Error,
info.toString(), REQUIRED_REVISION));
}
return info;
}
private void updateAPIVersion(IProgressMonitor monitor) throws TracException {
if (info.isStale()) {
validate(monitor);
}
}
private boolean isApiVersionOrHigher(int epoch, int major, int minor, IProgressMonitor monitor)
throws TracException {
updateAPIVersion(monitor);
return info.isApiVersionOrHigher(epoch, major, minor);
}
public List<TracComment> getComments(int id, IProgressMonitor monitor) throws TracException {
Object[] result = (Object[]) call(monitor, "ticket.changeLog", id, 0); //$NON-NLS-1$
List<TracComment> comments = new ArrayList<TracComment>(result.length);
for (Object item : result) {
comments.add(parseChangeLogEntry((Object[]) item));
}
return comments;
}
public TracTicket getTicket(int id, IProgressMonitor monitor) throws TracException {
Object[] result = (Object[]) call(monitor, "ticket.get", id); //$NON-NLS-1$
TracTicket ticket = parseTicket(result);
result = (Object[]) call(monitor, "ticket.changeLog", id, 0); //$NON-NLS-1$
for (Object item : result) {
ticket.addComment(parseChangeLogEntry((Object[]) item));
}
result = (Object[]) call(monitor, "ticket.listAttachments", id); //$NON-NLS-1$
for (Object item : result) {
ticket.addAttachment(parseAttachment((Object[]) item));
}
TracAction[] actions = getActions(id, monitor);
ticket.setActions(actions);
updateAttributes(new NullProgressMonitor(), false);
TracTicketResolution[] resolutions = getTicketResolutions();
if (resolutions != null) {
String[] resolutionStrings = new String[resolutions.length];
for (int i = 0; i < resolutions.length; i++) {
resolutionStrings[i] = resolutions[i].getName();
}
ticket.setResolutions(resolutionStrings);
} else {
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(parseDate(entry[3]));
attachment.setAuthor((String) entry[4]);
return attachment;
}
private TracComment parseChangeLogEntry(Object[] entry) {
TracComment comment = new TracComment();
comment.setCreated(parseDate(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, IProgressMonitor monitor) 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]); //$NON-NLS-1$
}
Object[] result = multicall(monitor, 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;
}
public void searchForTicketIds(TracSearch query, List<Integer> tickets, IProgressMonitor monitor)
throws TracException {
// an empty query string is not valid, therefore prepend order
Object[] result = (Object[]) call(monitor,
"ticket.query", "order=id" + query.toQuery(supportsMaxSearchResults(monitor))); //$NON-NLS-1$ //$NON-NLS-2$
for (Object item : result) {
tickets.add((Integer) item);
}
}
@SuppressWarnings("unchecked")
public void search(TracSearch query, List<TracTicket> tickets, IProgressMonitor monitor) throws TracException {
// an empty query string is not valid, therefore prepend order
Object[] result = (Object[]) call(monitor,
"ticket.query", "order=id" + query.toQuery(supportsMaxSearchResults(monitor))); //$NON-NLS-1$ //$NON-NLS-2$
Map<String, Object>[] calls = new Map[result.length];
for (int i = 0; i < calls.length; i++) {
calls[i] = createMultiCall("ticket.get", result[i]); //$NON-NLS-1$
}
result = multicall(monitor, calls);
for (Object item : result) {
Object[] ticketResult = (Object[]) getMultiCallResult(item);
tickets.add(parseTicket(ticketResult));
}
}
private boolean supportsWorkFlow(IProgressMonitor monitor) throws TracException {
return isApiVersionOrHigher(1, 0, 1, monitor);
}
private boolean supportsMaxSearchResults(IProgressMonitor monitor) throws TracException {
return isApiVersionOrHigher(1, 0, 0, monitor);
}
private TracTicket parseTicket(Object[] ticketResult) throws InvalidTicketException {
TracTicket ticket = new TracTicket((Integer) ticketResult[0]);
ticket.setCreated(parseDate(ticketResult[1]));
ticket.setLastChanged(parseDate(ticketResult[2]));
Map<?, ?> attributes = (Map<?, ?>) ticketResult[3];
for (Object key : attributes.keySet()) {
ticket.putValue(key.toString(), attributes.get(key).toString());
}
return ticket;
}
private Date parseDate(Object object) {
if (object instanceof Date) {
return (Date) object;
} else if (object instanceof Integer) {
return TracUtil.parseDate((Integer) object);
}
throw new ClassCastException("Unexpected object type for date: " + object.getClass()); //$NON-NLS-1$
}
@Override
public synchronized void updateAttributes(IProgressMonitor monitor) throws TracException {
monitor.beginTask("Updating attributes", 9); //$NON-NLS-1$
Object[] result = getAttributes("ticket.component", monitor); //$NON-NLS-1$
data.components = new ArrayList<TracComponent>(result.length);
for (Object item : result) {
data.components.add(parseComponent((Map<?, ?>) getMultiCallResult(item)));
}
advance(monitor, 1);
result = getAttributes("ticket.milestone", monitor); //$NON-NLS-1$
data.milestones = new ArrayList<TracMilestone>(result.length);
for (Object item : result) {
data.milestones.add(parseMilestone((Map<?, ?>) getMultiCallResult(item)));
}
advance(monitor, 1);
List<TicketAttributeResult> attributes = getTicketAttributes("ticket.priority", monitor); //$NON-NLS-1$
data.priorities = new ArrayList<TracPriority>(result.length);
for (TicketAttributeResult attribute : attributes) {
data.priorities.add(new TracPriority(attribute.name, attribute.value));
}
Collections.sort(data.priorities);
advance(monitor, 1);
attributes = getTicketAttributes("ticket.resolution", monitor); //$NON-NLS-1$
data.ticketResolutions = new ArrayList<TracTicketResolution>(result.length);
for (TicketAttributeResult attribute : attributes) {
data.ticketResolutions.add(new TracTicketResolution(attribute.name, attribute.value));
}
Collections.sort(data.ticketResolutions);
advance(monitor, 1);
attributes = getTicketAttributes("ticket.severity", monitor); //$NON-NLS-1$
data.severities = new ArrayList<TracSeverity>(result.length);
for (TicketAttributeResult attribute : attributes) {
data.severities.add(new TracSeverity(attribute.name, attribute.value));
}
Collections.sort(data.severities);
advance(monitor, 1);
boolean assignValues = isApiVersionOrHigher(1, 0, 0, monitor);
attributes = getTicketAttributes("ticket.status", assignValues, monitor); //$NON-NLS-1$
data.ticketStatus = new ArrayList<TracTicketStatus>(result.length);
for (TicketAttributeResult attribute : attributes) {
data.ticketStatus.add(new TracTicketStatus(attribute.name, attribute.value));
}
Collections.sort(data.ticketStatus);
advance(monitor, 1);
attributes = getTicketAttributes("ticket.type", monitor); //$NON-NLS-1$
data.ticketTypes = new ArrayList<TracTicketType>(result.length);
for (TicketAttributeResult attribute : attributes) {
data.ticketTypes.add(new TracTicketType(attribute.name, attribute.value));
}
Collections.sort(data.ticketTypes);
advance(monitor, 1);
result = getAttributes("ticket.version", monitor); //$NON-NLS-1$
data.versions = new ArrayList<TracVersion>(result.length);
for (Object item : result) {
data.versions.add(parseVersion((Map<?, ?>) getMultiCallResult(item)));
}
advance(monitor, 1);
result = (Object[]) call(monitor, "ticket.getTicketFields"); //$NON-NLS-1$
data.ticketFields = new ArrayList<TracTicketField>(result.length);
data.ticketFieldByName = null;
for (Object item : result) {
data.ticketFields.add(parseTicketField((Map<?, ?>) item));
}
advance(monitor, 1);
}
private void advance(IProgressMonitor monitor, int worked) {
monitor.worked(worked);
if (monitor.isCanceled()) {
throw new OperationCanceledException();
}
}
private TracComponent parseComponent(Map<?, ?> result) {
TracComponent component = new TracComponent((String) result.get("name")); //$NON-NLS-1$
component.setOwner((String) result.get("owner")); //$NON-NLS-1$
component.setDescription((String) result.get("description")); //$NON-NLS-1$
return component;
}
private TracMilestone parseMilestone(Map<?, ?> result) {
TracMilestone milestone = new TracMilestone((String) result.get("name")); //$NON-NLS-1$
milestone.setCompleted(parseDate(result.get("completed"))); //$NON-NLS-1$
milestone.setDue(parseDate(result.get("due"))); //$NON-NLS-1$
milestone.setDescription((String) result.get("description")); //$NON-NLS-1$
return milestone;
}
private TracVersion parseVersion(Map<?, ?> result) {
TracVersion version = new TracVersion((String) result.get("name")); //$NON-NLS-1$
version.setTime(parseDate(result.get("time"))); //$NON-NLS-1$
version.setDescription((String) result.get("description")); //$NON-NLS-1$
return version;
}
private TracTicketField parseTicketField(Map<?, ?> result) {
TracTicketField field = new TracTicketField((String) result.get("name")); //$NON-NLS-1$
field.setType(TracTicketField.Type.fromString((String) result.get("type"))); //$NON-NLS-1$
field.setLabel((String) result.get("label")); //$NON-NLS-1$
field.setDefaultValue((String) result.get("value")); //$NON-NLS-1$
Object[] items = (Object[]) result.get("options"); //$NON-NLS-1$
if (items != null) {
String[] options = new String[items.length];
for (int i = 0; i < items.length; i++) {
options[i] = (String) items[i];
}
field.setOptions(options);
}
if (result.get("custom") != null) { //$NON-NLS-1$
field.setCustom((Boolean) result.get("custom")); //$NON-NLS-1$
}
if (result.get("order") != null) { //$NON-NLS-1$
field.setOrder((Integer) result.get("order")); //$NON-NLS-1$
}
if (result.get("optional") != null) { //$NON-NLS-1$
field.setOptional((Boolean) result.get("optional")); //$NON-NLS-1$
}
if (result.get("width") != null) { //$NON-NLS-1$
field.setWidth((Integer) result.get("width")); //$NON-NLS-1$
}
if (result.get("height") != null) { //$NON-NLS-1$
field.setHeight((Integer) result.get("height")); //$NON-NLS-1$
}
return field;
}
@SuppressWarnings("unchecked")
private Object[] getAttributes(String attributeType, IProgressMonitor monitor) throws TracException {
Object[] ids = (Object[]) call(monitor, attributeType + ".getAll"); //$NON-NLS-1$
Map<String, Object>[] calls = new Map[ids.length];
for (int i = 0; i < calls.length; i++) {
calls[i] = createMultiCall(attributeType + ".get", ids[i]); //$NON-NLS-1$
}
Object[] result = multicall(monitor, calls);
assert result.length == ids.length;
return result;
}
private List<TicketAttributeResult> getTicketAttributes(String attributeType, IProgressMonitor monitor)
throws TracException {
return getTicketAttributes(attributeType, false, monitor);
}
@SuppressWarnings("unchecked")
private List<TicketAttributeResult> getTicketAttributes(String attributeType, boolean assignValues,
IProgressMonitor monitor) throws TracException {
// get list of attribute ids first
Object[] ids = (Object[]) call(monitor, attributeType + ".getAll"); //$NON-NLS-1$
// fetch all attributes in a single call
Map<String, Object>[] calls = new Map[ids.length];
for (int i = 0; i < calls.length; i++) {
calls[i] = createMultiCall(attributeType + ".get", ids[i]); //$NON-NLS-1$
}
Object[] result = multicall(monitor, 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];
Object value = getMultiCallResult(result[i]);
if (assignValues) {
attribute.value = i;
} else {
attribute.value = (value instanceof Integer) ? (Integer) value : Integer.parseInt((String) value);
}
attributes.add(attribute);
} catch (ClassCastException e) {
StatusHandler.log(new Status(IStatus.WARNING, TracCorePlugin.ID_PLUGIN,
"Invalid response from Trac repository for attribute type: '" + attributeType + "'", e)); //$NON-NLS-1$ //$NON-NLS-2$
} catch (NumberFormatException e) {
StatusHandler.log(new Status(IStatus.WARNING, TracCorePlugin.ID_PLUGIN,
"Invalid response from Trac repository for attribute type: '" + attributeType + "'", e)); //$NON-NLS-1$ //$NON-NLS-2$
}
}
return attributes;
}
public InputStream getAttachmentData(int ticketId, String filename, IProgressMonitor monitor) throws TracException {
byte[] data = (byte[]) call(monitor, "ticket.getAttachment", ticketId, filename); //$NON-NLS-1$
return new ByteArrayInputStream(data);
}
public void putAttachmentData(int ticketId, String filename, String description, InputStream in,
IProgressMonitor monitor, boolean replace) throws TracException {
byte[] data;
try {
data = readData(in, new NullProgressMonitor());
} catch (IOException e) {
throw new TracException(e);
}
call(monitor, "ticket.putAttachment", ticketId, filename, description, data, replace); //$NON-NLS-1$
}
private byte[] readData(InputStream in, IProgressMonitor monitor) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
byte[] buffer = new byte[512];
while (true) {
int count = in.read(buffer);
if (count == -1) {
return out.toByteArray();
}
if (monitor.isCanceled()) {
throw new OperationCanceledException();
}
out.write(buffer, 0, count);
if (monitor.isCanceled()) {
throw new OperationCanceledException();
}
}
} finally {
try {
in.close();
} catch (IOException e) {
StatusHandler.log(new Status(IStatus.ERROR, TracCorePlugin.ID_PLUGIN,
"Error closing attachment stream", e)); //$NON-NLS-1$
}
}
}
public void deleteAttachment(int ticketId, String filename, IProgressMonitor monitor) throws TracException {
call(monitor, "ticket.deleteAttachment", ticketId, filename); //$NON-NLS-1$
}
private class TicketAttributeResult {
String name;
int value;
}
public int createTicket(TracTicket ticket, IProgressMonitor monitor) 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();
}
if (supportsNotifications(monitor)) {
return (Integer) call(monitor, "ticket.create", summary, description, attributes, true); //$NON-NLS-1$
} else {
return (Integer) call(monitor, "ticket.create", summary, description, attributes); //$NON-NLS-1$
}
}
private boolean supportsNotifications(IProgressMonitor monitor) throws TracException {
return isApiVersionOrHigher(0, 0, 2, monitor);
}
public void updateTicket(TracTicket ticket, String comment, IProgressMonitor monitor) throws TracException {
updateAPIVersion(monitor);
Map<String, String> attributes = ticket.getValues();
if (!supportsWorkFlow(monitor)) {
// submitted as part of status and resolution updates for Trac < 0.11
attributes.remove("action"); //$NON-NLS-1$
// avoid confusing older XML-RPC plugin versions
attributes.remove(Key.TOKEN.getKey());
}
if (supportsNotifications(monitor)) {
call(monitor, "ticket.update", ticket.getId(), comment, attributes, true); //$NON-NLS-1$
} else {
call(monitor, "ticket.update", ticket.getId(), comment, attributes); //$NON-NLS-1$
}
}
public Set<Integer> getChangedTickets(Date since, IProgressMonitor monitor) throws TracException {
Object[] ids;
ids = (Object[]) call(monitor, "ticket.getRecentChanges", since); //$NON-NLS-1$
Set<Integer> result = new HashSet<Integer>();
for (Object id : ids) {
result.add((Integer) id);
}
return result;
}
public TracAction[] getActions(int id, IProgressMonitor monitor) throws TracException {
if (supportsWorkFlow(monitor)) {
Object[] actions = (Object[]) call(monitor, "ticket.getActions", id); //$NON-NLS-1$
TracAction[] result = new TracAction[actions.length];
for (int i = 0; i < result.length; i++) {
Object[] entry = (Object[]) actions[i];
TracAction action = new TracAction((String) entry[0]);
action.setLabel((String) entry[1]);
action.setHint((String) entry[2]);
Object[] inputs = (Object[]) entry[3];
// each action can be associated with fields
for (Object inputArray : inputs) {
Object[] inputEntry = (Object[]) inputArray;
TracTicketField field = new TracTicketField((String) inputEntry[0]);
field.setDefaultValue((String) inputEntry[1]);
Object[] optionEntry = (Object[]) inputEntry[2];
if (optionEntry.length == 0) {
field.setType(Type.TEXT);
} else {
field.setType(Type.SELECT);
String[] options = new String[optionEntry.length];
for (int j = 0; j < options.length; j++) {
options[j] = (String) optionEntry[j];
}
field.setOptions(options);
}
action.addField(field);
}
result[i] = action;
}
return result;
} else {
Object[] actions = (Object[]) call(monitor, "ticket.getAvailableActions", id); //$NON-NLS-1$
TracAction[] result = new TracAction[actions.length];
for (int i = 0; i < result.length; i++) {
result[i] = new TracAction((String) actions[i]);
}
return result;
}
}
public Date getTicketLastChanged(Integer id, IProgressMonitor monitor) throws TracException {
Object[] result = (Object[]) call(monitor, "ticket.get", id); //$NON-NLS-1$
return parseDate(result[2]);
}
public void validateWikiRpcApi(IProgressMonitor monitor) throws TracException {
if (((Integer) call(monitor, "wiki.getRPCVersionSupported")) < 2) { //$NON-NLS-1$
validate(monitor);
}
}
public String wikiToHtml(String sourceText, IProgressMonitor monitor) throws TracException {
return (String) call(monitor, "wiki.wikiToHtml", sourceText); //$NON-NLS-1$
}
public String[] getAllWikiPageNames(IProgressMonitor monitor) throws TracException {
Object[] result = (Object[]) call(monitor, "wiki.getAllPages"); //$NON-NLS-1$
String[] wikiPageNames = new String[result.length];
for (int i = 0; i < wikiPageNames.length; i++) {
wikiPageNames[i] = (String) result[i];
}
return wikiPageNames;
}
public TracWikiPageInfo getWikiPageInfo(String pageName, IProgressMonitor monitor) throws TracException {
return getWikiPageInfo(pageName, LATEST_VERSION, null);
}
public TracWikiPageInfo getWikiPageInfo(String pageName, int version, IProgressMonitor monitor)
throws TracException {
// Note: if an unexpected null value is passed to XmlRpcPlugin, XmlRpcClient will throw a TracRemoteException.
// So, this null-parameter checking may be omitted if resorting to XmlRpcClient is more appropriate.
if (pageName == null) {
throw new IllegalArgumentException("Wiki page name cannot be null"); //$NON-NLS-1$
}
Object result = (version == LATEST_VERSION) ? call(monitor, "wiki.getPageInfo", pageName) // //$NON-NLS-1$
: call(monitor, "wiki.getPageInfoVersion", pageName, version); //$NON-NLS-1$
return parseWikiPageInfo(result);
}
@SuppressWarnings("unchecked")
public TracWikiPageInfo[] getWikiPageInfoAllVersions(String pageName, IProgressMonitor monitor)
throws TracException {
TracWikiPageInfo latestVersion = getWikiPageInfo(pageName, null);
Map<String, Object>[] calls = new Map[latestVersion.getVersion() - 1];
for (int i = 0; i < calls.length; i++) {
calls[i] = createMultiCall("wiki.getPageInfoVersion", pageName, i + 1); //$NON-NLS-1$
}
Object[] result = multicall(monitor, calls);
TracWikiPageInfo[] versions = new TracWikiPageInfo[result.length + 1];
for (int i = 0; i < result.length; i++) {
Object pageInfoResult = getMultiCallResult(result[i]);
versions[i] = parseWikiPageInfo(pageInfoResult);
}
versions[result.length] = latestVersion;
return versions;
}
private TracWikiPageInfo parseWikiPageInfo(Object pageInfoResult) throws InvalidWikiPageException {
// Note: Trac XML-RPC Plugin returns 0 (as Integer) if pageName or version doesn't exist,
// and XmlRpcClient doesn't throw an Exception in this case
if (pageInfoResult instanceof Map<?, ?>) {
TracWikiPageInfo pageInfo = new TracWikiPageInfo();
Map<?, ?> infoMap = (Map<?, ?>) pageInfoResult;
pageInfo.setPageName((String) infoMap.get("name")); //$NON-NLS-1$
pageInfo.setAuthor((String) infoMap.get("author")); //$NON-NLS-1$
pageInfo.setLastModified(parseDate(infoMap.get("lastModified"))); //$NON-NLS-1$
pageInfo.setVersion((Integer) infoMap.get("version")); //$NON-NLS-1$
return pageInfo;
} else {
throw new InvalidWikiPageException("Wiki page name or version does not exist"); //$NON-NLS-1$
}
}
public String getWikiPageContent(String pageName, IProgressMonitor monitor) throws TracException {
return getWikiPageContent(pageName, LATEST_VERSION, null);
}
public String getWikiPageContent(String pageName, int version, IProgressMonitor monitor) throws TracException {
// Note: if an unexpected null value is passed to XmlRpcPlugin, XmlRpcClient will throw a TracRemoteException.
// So, this null-parameter checking may be omitted if resorting to XmlRpcClient is more appropriate.
if (pageName == null) {
throw new IllegalArgumentException("Wiki page name cannot be null"); //$NON-NLS-1$
}
if (version == LATEST_VERSION) {
// XmlRpcClient throws a TracRemoteException if pageName or version doesn't exist
return (String) call(monitor, "wiki.getPage", pageName); //$NON-NLS-1$
} else {
return (String) call(monitor, "wiki.getPageVersion", pageName, version); //$NON-NLS-1$
}
}
public String getWikiPageHtml(String pageName, IProgressMonitor monitor) throws TracException {
return getWikiPageHtml(pageName, LATEST_VERSION, null);
}
public String getWikiPageHtml(String pageName, int version, IProgressMonitor monitor) throws TracException {
if (pageName == null) {
throw new IllegalArgumentException("Wiki page name cannot be null"); //$NON-NLS-1$
}
if (version == LATEST_VERSION) {
// XmlRpcClient throws a TracRemoteException if pageName or version doesn't exist
return (String) call(monitor, "wiki.getPageHTML", pageName); //$NON-NLS-1$
} else {
return (String) call(monitor, "wiki.getPageHTMLVersion", pageName, version); //$NON-NLS-1$
}
}
public TracWikiPageInfo[] getRecentWikiChanges(Date since, IProgressMonitor monitor) throws TracException {
if (since == null) {
throw new IllegalArgumentException("Date parameter cannot be null"); //$NON-NLS-1$
}
Object[] result = (Object[]) call(monitor, "wiki.getRecentChanges", since); //$NON-NLS-1$
TracWikiPageInfo[] changes = new TracWikiPageInfo[result.length];
for (int i = 0; i < result.length; i++) {
changes[i] = parseWikiPageInfo(result[i]);
}
return changes;
}
public TracWikiPage getWikiPage(String pageName, IProgressMonitor monitor) throws TracException {
return getWikiPage(pageName, LATEST_VERSION, null);
}
public TracWikiPage getWikiPage(String pageName, int version, IProgressMonitor monitor) throws TracException {
TracWikiPage page = new TracWikiPage();
page.setPageInfo(getWikiPageInfo(pageName, version, null));
page.setContent(getWikiPageContent(pageName, version, null));
page.setPageHTML(getWikiPageHtml(pageName, version, null));
return page;
}
public boolean putWikipage(String pageName, String content, Map<String, Object> attributes, IProgressMonitor monitor)
throws TracException {
Boolean result = (Boolean) call(monitor, "wiki.putPage", pageName, content, attributes); //$NON-NLS-1$
return result.booleanValue();
}
public boolean deleteWikipage(String pageName, IProgressMonitor monitor) throws TracException {
Boolean result = (Boolean) call(monitor, "wiki.deletePage", pageName); //$NON-NLS-1$
return result.booleanValue();
}
public String[] listWikiPageAttachments(String pageName, IProgressMonitor monitor) throws TracException {
Object[] result = (Object[]) call(monitor, "wiki.listAttachments", pageName); //$NON-NLS-1$
String[] attachments = new String[result.length];
for (int i = 0; i < attachments.length; i++) {
attachments[i] = (String) result[i];
}
return attachments;
}
public InputStream getWikiPageAttachmentData(String pageName, String fileName, IProgressMonitor monitor)
throws TracException {
String attachmentName = pageName + "/" + fileName; //$NON-NLS-1$
byte[] data = (byte[]) call(monitor, "wiki.getAttachment", attachmentName); //$NON-NLS-1$
return new ByteArrayInputStream(data);
}
/**
* Attach a file to a Wiki page on the repository.
* <p>
* This implementation uses the wiki.putAttachmentEx() call, which provides a richer functionality specific to Trac.
*
* @param pageName
* the name of the Wiki page
* @param fileName
* the name of the file to be attached
* @param description
* the description of the attachment
* @param in
* An InputStream of the content of the attachment
* @param replace
* whether to overwrite an existing attachment with the same filename
* @return The (possibly transformed) filename of the attachment. If <code>replace</code> is <code>true</code>, the
* returned name is always the same as the argument <code>fileName</code>; if <code>replace</code> is
* <code>false</code> and an attachment with name <code>fileName</code> already exists, a number is appended
* to the file name (before suffix) and the generated filename of the attachment is returned.
* @throws TracException
*/
public String putWikiPageAttachmentData(String pageName, String fileName, String description, InputStream in,
boolean replace, IProgressMonitor monitor) throws TracException {
byte[] data;
try {
data = readData(in, new NullProgressMonitor());
} catch (IOException e) {
throw new TracException(e);
}
return (String) call(monitor, "wiki.putAttachmentEx", pageName, fileName, description, data, replace); //$NON-NLS-1$
}
public void deleteTicket(int ticketId, IProgressMonitor monitor) throws TracException {
call(monitor, "ticket.delete", ticketId); //$NON-NLS-1$
}
}