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... 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 createMultiCall(String methodName, Object... parameters) throws TracException { Map table = new HashMap(); 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 getTickets(int[] ids) throws TracException { Map[] 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 tickets = new ArrayList(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 tickets) throws TracException { // an empty query string is not valid, therefore prepend order Object[] result = (Object[]) call("ticket.query", "order=id" + query.toQuery()); Map[] 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(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(result.length); for (Object item : result) { data.milestones.add(parseMilestone((Map) getMultiCallResult(item))); } monitor.worked(1); if (monitor.isCanceled()) throw new OperationCanceledException(); List attributes = getTicketAttributes("ticket.priority"); data.priorities = new ArrayList(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(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(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(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(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(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[] 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 getTicketAttributes(String attributeType) throws TracException { Object[] ids = (Object[]) call(attributeType + ".getAll"); Map[] 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 attributes = new ArrayList(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 int createTicket(TracTicket ticket) throws TracException { Map 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(); } return (Integer) call("ticket.create", summary, description, attributes); } public void updateTicket(TracTicket ticket, String comment) throws TracException { Map attributes = ticket.getValues(); call("ticket.update", ticket.getId(), comment, attributes); } public Set getChangedTickets(Date since) throws TracException { Object[] ids; ids = (Object[]) call("ticket.getRecentChanges", since); Set result = new HashSet(); 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; } }