/******************************************************************************* * Copyright (c) 2006, 2010 Steffen Pingel 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: * 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$ } }