Java tutorial
/* XMLRPC client for GWT Copyright (C) 2006 This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ package com.fredhat.gwt.xmlrpc.client; import com.google.gwt.user.client.rpc.AsyncCallback; import com.google.gwt.xml.client.*; import com.google.gwt.xml.client.impl.DOMParseException; import com.google.gwt.http.client.*; import java.util.*; /** * This class collects information that is specific to the XML-RPC server. It is * then used in an {@link XmlRpcRequest} object to invoke your XML-RPC methods. * * Quick example: * <pre class="code"> * ... * XmlRpcClient client = new XmlRpcClient("http://www.foo.com/xmlrpcserver.php"); * String methodName = "example.addTwoIntegers"; * Object[] params = new Object[]{3, 4}; * * XmlRpcRequest<Integer> request = new XmlRpcRequest<Integer>( * client, methodName, params, new AsyncCallback<Integer>() { * public void onSuccess(Integer response) { * // Handle integer response logic here * } * * public void onFailure(Throwable response) { * String failedMsg = response.getMessage(); * // Put other failed response handling logic here * } * }); * request.execute(); * ... * </pre> * * * * @author Fred Drake * @see XmlRpcRequest */ public class XmlRpcClient implements Cloneable { /** * The default timeout of a XMLRPC request, in milliseconds. */ public static final int DEFAULT_TIMEOUT = 10000; private String serverURL; private boolean debugMessages; private int timeout; private String username; private String password; /** * Creates an XMLRPC client with the server URL * @param serverURL the full URL of the XMLRPC server of which you wish to connect */ public XmlRpcClient(String serverURL) { this.serverURL = serverURL; this.debugMessages = false; this.timeout = DEFAULT_TIMEOUT; this.username = null; this.password = null; } /** * Gets the XMLRPC client debug mode. When in debug mode, the raw request and * response messages will be sent to standard output. Note that standard output * is only displayed when the application is running under hosted mode. * @return true if the client is in debug mode, false otherwise. */ public boolean getDebugMode() { return debugMessages; } /** * Sets the XMLRPC client debug mode. When in debug mode, the raw request and * response messages will be sent to standard output. Note that standard output * is only displayed when the application is running under hosted mode. * @param debugMode Set true if you want debug mode, false otherwise. */ public void setDebugMode(boolean debugMode) { this.debugMessages = debugMode; } /** * Gets the XML-RPC server URL. Note that this needs to be a full URL (including * the preceding "http://"). * @return The XML-RPC server URL */ public String getServerURL() { return serverURL; } /** * Sets the XML-RPC server URL. Note that this needs to be a full URL (including * the preceding "http://"). * @param serverURL The XML-RPC server URL */ public void setServerURL(String serverURL) { this.serverURL = serverURL; } /** * Gets the timeout for a method. If this amount of time has elapsed without a * return call from an XML-RPC method, the associated {@link AsyncCallback#onFailure(Throwable)} * will be invoked. * @return The timeout value in milliseconds */ public int getTimeoutMillis() { return timeout; } /** * Gets the username (if one exists) associated with this client. Only for clients that * require authentication. * @return The associated username, or null if one does not exist. */ public String getUsername() { return username; } /** * Sets the username to be associated with this client. This is for clients that * require authentication. * @param username The username for server authentication */ public void setUsername(String username) { this.username = username; } /** * Gets the password (if one exists) associated with this client. Only for clients that * require authentication. * @return The associated password, or null if one does not exist. */ public String getPassword() { return password; } /** * Sets the password to be associated with this client. This is for clients that * require authentication. * @param password The password for server authentication */ public void setPassword(String password) { this.password = password; } /** * Sets the timeout of the XMLRPC request, overiding the * {@link XmlRpcClient#DEFAULT_TIMEOUT} value. * @param timeout The timeout of the request, in milliseconds. */ public void setTimeoutMillis(int timeout) { this.timeout = timeout; } /** * Executes an asynchronous XMLRPC call to the server with a specified username * and password. If the execution was successful, the callback's {@link AsyncCallback#onSuccess(Object)} * method will be invoked with the return value as the argument. If the * execution failed for any reason, the callback's {@link AsyncCallback#onFailure(Throwable)} method will * be invoked with an instance of {@link XmlRpcException} instance as it's argument. * @param username the username for authentication * @param password the password for authentication * @param methodName the name of the XMLRPC method * @param params the parameters for the XMLRPC method * @param callback the logic implementation for handling the XMLRPC responses. * @deprecated As of XMLRPC-GWT v1.1, * build an {@link XmlRpcRequest} then call {@link XmlRpcRequest#execute()} */ @SuppressWarnings("unchecked") @Deprecated public void execute(String username, String password, String methodName, Object[] params, final AsyncCallback callback) { if (methodName == null || methodName.equals("")) { callback.onFailure(new XmlRpcException("The method name parameter cannot be null")); return; } if (params == null) params = new Object[0]; Document request = buildRequest(methodName, params); if (debugMessages) System.out.println("** Request **\n" + request + "\n*************"); RequestBuilder requestBuilder = new RequestBuilder(RequestBuilder.POST, serverURL); requestBuilder.setHeader("Content-Type", "text/xml"); requestBuilder.setTimeoutMillis(timeout); if (username != null) requestBuilder.setUser(username); if (password != null) requestBuilder.setPassword(password); try { requestBuilder.sendRequest(request.toString(), new RequestCallback() { public void onResponseReceived(Request req, Response res) { if (res.getStatusCode() != 200) { callback.onFailure(new XmlRpcException("Server returned " + "response code " + res.getStatusCode() + " - " + res.getStatusText())); return; } Object responseObj = buildResponse(res.getText()); if (responseObj instanceof XmlRpcException) callback.onFailure((XmlRpcException) responseObj); else callback.onSuccess(responseObj); } public void onError(Request req, Throwable t) { callback.onFailure(t); } }); } catch (RequestException e) { callback.onFailure(new XmlRpcException("Couldn't make server request", e)); } } /** * Executes an asynchronous XMLRPC call to the server. If the execution was * successful, the callback's {@link AsyncCallback#onSuccess(Object)} method will be invoked with the return * value as the argument. If the execution failed for any reason, the callback's * {@link AsyncCallback#onFailure(Throwable)} method will be invoked with an instance of {@link XmlRpcException} * as it's argument. * @param methodName the name of the XMLRPC method * @param params the parameters for the XMLRPC method * @param callback the logic implementation for handling the XMLRPC responses. * @deprecated As of XMLRPC-GWT v1.1, * build an {@link XmlRpcRequest} then call {@link XmlRpcRequest#execute()} */ @SuppressWarnings("unchecked") @Deprecated public void execute(String methodName, Object[] params, AsyncCallback callback) { this.execute(null, null, methodName, params, callback); } /** * Returns a copy of the {@link XmlRpcClient} object */ public Object clone() { XmlRpcClient clientClone = new XmlRpcClient(this.serverURL); clientClone.debugMessages = this.debugMessages; clientClone.timeout = this.timeout; return clientClone; } // Builds the XML request structure // This is legacy logic used in the deprecated execute methods. private Document buildRequest(String methodName, Object[] params) { Document request = XMLParser.createDocument(); Element methodCall = request.createElement("methodCall"); { Element methodNameElem = request.createElement("methodName"); methodNameElem.appendChild(request.createTextNode(methodName)); methodCall.appendChild(methodNameElem); } { Element paramsElem = request.createElement("params"); for (int i = 0; i < params.length; i++) { Element paramElem = request.createElement("param"); paramElem.appendChild(buildValue(params[i])); paramsElem.appendChild(paramElem); } methodCall.appendChild(paramsElem); } request.appendChild(methodCall); return request; } // Builds an individual parameter and sends it back through the StringBuffer // This is legacy logic used in the deprecated execute methods. private Element buildValue(Object param) { Document doc = XMLParser.createDocument(); Element valueElem = doc.createElement("value"); if (param == null) { Element nilElem = doc.createElement("nil"); valueElem.appendChild(nilElem); } else if (param instanceof Integer) { Element intElem = doc.createElement("int"); intElem.appendChild(doc.createTextNode(param.toString())); valueElem.appendChild(intElem); } else if (param instanceof Boolean) { Element boolElem = doc.createElement("boolean"); boolean b = ((Boolean) param).booleanValue(); if (b) boolElem.appendChild(doc.createTextNode("1")); else boolElem.appendChild(doc.createTextNode("0")); valueElem.appendChild(boolElem); } else if (param instanceof String) { Element stringElem = doc.createElement("string"); stringElem.appendChild(doc.createTextNode(param.toString())); valueElem.appendChild(stringElem); } else if (param instanceof Double) { Element doubleElem = doc.createElement("double"); doubleElem.appendChild(doc.createTextNode(param.toString())); valueElem.appendChild(doubleElem); } else if (param instanceof com.fredhat.gwt.xmlrpc.client.Date) { com.fredhat.gwt.xmlrpc.client.Date date = (com.fredhat.gwt.xmlrpc.client.Date) param; Element dateElem = doc.createElement("dateTime.iso8601"); dateElem.appendChild(doc.createTextNode(dateToISO8601(date))); valueElem.appendChild(dateElem); } else if (param instanceof Base64) { Element base64Elem = doc.createElement("base64"); base64Elem.appendChild(doc.createTextNode(param.toString())); valueElem.appendChild(base64Elem); } else if (param instanceof Map) { @SuppressWarnings("unchecked") Map<String, Object> map = (Map<String, Object>) param; Element mapElem = doc.createElement("struct"); for (Iterator<String> iter = map.keySet().iterator(); iter.hasNext();) { Object key = iter.next(); Element memberElem = doc.createElement("member"); { Element nameElem = doc.createElement("name"); nameElem.appendChild(doc.createTextNode(key.toString())); memberElem.appendChild(nameElem); } { Element innerValueElem = buildValue(map.get(key)); memberElem.appendChild(innerValueElem); } mapElem.appendChild(memberElem); } valueElem.appendChild(mapElem); } else if (param instanceof List) { @SuppressWarnings("unchecked") List<Object> list = (List<Object>) param; Element listElem = doc.createElement("array"); { Element dataElem = doc.createElement("data"); for (Iterator<Object> iter = list.iterator(); iter.hasNext();) { Element innerValueElem = buildValue(iter.next()); dataElem.appendChild(innerValueElem); } listElem.appendChild(dataElem); } valueElem.appendChild(listElem); } return valueElem; } // Converts a com.gwt.components.client.xmlrpc.Date to the // pseudo ISO8601 date format // This is legacy logic used in the deprecated execute methods. @SuppressWarnings("deprecation") private String dateToISO8601(com.fredhat.gwt.xmlrpc.client.Date date) { // The GWT Date object is just a wrapper, // so these deprecated methods are okay. int year = date.getYear() + 1900; int month = date.getMonth() + 1; int day = date.getDate(); int hour = date.getHours(); int minute = date.getMinutes(); int second = date.getSeconds(); StringBuffer sb = new StringBuffer(); sb.append(dateString(year, 4)).append(dateString(month, 2)); sb.append(dateString(day, 2)).append("T"); sb.append(dateString(hour, 2)).append(":"); sb.append(dateString(minute, 2)).append(":"); sb.append(dateString(second, 2)); return sb.toString(); } // Converts a pseudo ISO8601 date format to a // com.gwt.components.client.xmlrpc.Date object // This is legacy logic used in the deprecated execute methods. @SuppressWarnings("deprecation") private Date iso8601ToDate(String dateString) throws XmlRpcException { // We need the format 19980717T14:08:55 int year; int month; int day; int hour; int minute; int second; com.fredhat.gwt.xmlrpc.client.Date dateObj = new Date(); if (dateString.length() != 17) throw new XmlRpcException("Datetime value " + dateString + " is not in XMLRPC ISO8601 Format"); try { year = Integer.parseInt(dateString.substring(0, 4)); month = Integer.parseInt(dateString.substring(4, 6)); day = Integer.parseInt(dateString.substring(6, 8)); hour = Integer.parseInt(dateString.substring(9, 11)); minute = Integer.parseInt(dateString.substring(12, 14)); second = Integer.parseInt(dateString.substring(15)); } catch (NumberFormatException e) { throw new XmlRpcException("Datetime value " + dateString + " is not in XMLRPC ISO8601 Format"); } // The GWT Date object is just a wrapper, // so these deprecated methods are okay. dateObj.setYear(year - 1900); dateObj.setMonth(month - 1); dateObj.setDate(day); dateObj.setHours(hour); dateObj.setMinutes(minute); dateObj.setSeconds(second); return dateObj; } // Parses the servers XML response and constructs the proper return object. // This is legacy logic used in the deprecated execute methods. private Object buildResponse(String responseText) { if (debugMessages) System.out.println("** Response **\n" + responseText + "\n**************"); Document doc = null; try { doc = XMLParser.parse(responseText); } catch (DOMParseException e) { return new XmlRpcException("Unparsable response", e); } Element methodResponse = doc.getDocumentElement(); if (!methodResponse.getNodeName().equals("methodResponse")) return new XmlRpcException("The document element must be named \"methodResponse\" " + "(this is " + methodResponse.getNodeName() + ")"); if (getElementNodeCount(methodResponse.getChildNodes()) != 1) return new XmlRpcException("There may be only one element under <methodResponse> " + "(this has " + getElementNodeCount(methodResponse.getChildNodes()) + ")"); Element forkNode = getFirstElementChild(methodResponse); if (forkNode.getNodeName().equals("fault")) { if (getElementNodeCount(forkNode.getChildNodes()) != 1) return new XmlRpcException("The <fault> element must have " + "exactly one child"); Element valueNode = getFirstElementChild(forkNode); Object faultDetailsObj = getValueNode(valueNode); if (!(faultDetailsObj instanceof Map)) return new XmlRpcException("The <fault> element must be a <struct>"); @SuppressWarnings("unchecked") Map<String, Object> faultDetails = (Map<String, Object>) faultDetailsObj; if (faultDetails.get("faultCode") == null || faultDetails.get("faultString") == null || !(faultDetails.get("faultCode") instanceof Integer)) return new XmlRpcException("The <fault> element must contain " + "exactly one <struct> with an integer \"faultCode\" and " + "a string \"faultString\" value"); int faultCode = ((Integer) faultDetails.get("faultCode")).intValue(); return new XmlRpcException(faultCode, faultDetails.get("faultString").toString(), null); } else if (!forkNode.getNodeName().equals("params")) return new XmlRpcException( "The <methodResponse> must contain either " + "a <params> or <fault> element."); // No return object is allowed Element paramNode = getFirstElementChild(forkNode); if (paramNode == null) return null; if (!paramNode.getNodeName().equals("param") || getElementNodeCount(paramNode.getChildNodes()) < 1) return new XmlRpcException("The <params> element must contain " + "one <param> element"); Node valueNode = getFirstElementChild(paramNode); return getValueNode(valueNode); } // Fetches the proper java object from an XML node // This is legacy logic used in the deprecated execute methods. private Object getValueNode(Node node) { if (!node.getNodeName().equals("value")) return new XmlRpcException("Value node must be named <value>, not " + "<" + node.getNodeName() + ">"); if (getElementNodeCount(node.getChildNodes()) == 0) { // If no type is indicated, the type is string. String strValue = getNodeTextValue(node); return strValue == null ? "" : strValue; } if (getElementNodeCount(node.getChildNodes()) != 1) return new XmlRpcException("A <value> node must have exactly one child"); Node valueType = getFirstElementChild(node); if (valueType.getNodeName().equals("i4") || valueType.getNodeName().equals("int")) { String intValueString = getNodeTextValue(valueType); if (intValueString == null) return new XmlRpcException("Integer child is not a text node"); try { return new Integer(intValueString); } catch (NumberFormatException e) { return new XmlRpcException("Value \"" + intValueString + "\" is not" + " an integer"); } } else if (valueType.getNodeName().equals("boolean")) { String boolValue = getNodeTextValue(valueType); if (boolValue == null) return new XmlRpcException("Child of <boolean> is not a text node"); if (boolValue.equals("0")) return new Boolean(false); else if (boolValue.equals("1")) return new Boolean(true); else return new XmlRpcException("Value \"" + boolValue + "\" must be 0 or 1"); } else if (valueType.getNodeName().equals("string")) { String strValue = getNodeTextValue(valueType); if (strValue == null) strValue = ""; // Make sure <string/> responses will exist as empty return strValue; } else if (valueType.getNodeName().equals("double")) { String doubleValueString = getNodeTextValue(valueType); if (doubleValueString == null) return new XmlRpcException("Child of <double> is not a text node"); try { return new Double(doubleValueString); } catch (NumberFormatException e) { return new XmlRpcException("Value \"" + doubleValueString + "\" is not a double"); } } else if (valueType.getNodeName().equals("dateTime.iso8601")) { String dateValue = getNodeTextValue(valueType); if (dateValue == null) return new XmlRpcException("Child of <dateTime> is not a text node"); try { return iso8601ToDate(dateValue); } catch (XmlRpcException e) { return new XmlRpcException(e.getMessage()); } } else if (valueType.getNodeName().equals("base64")) { String baseValue = getNodeTextValue(valueType); if (baseValue == null) return new XmlRpcException("Improper XML-RPC response format"); return new Base64(baseValue); } else if (valueType.getNodeName().equals("struct")) { @SuppressWarnings("unchecked") Map<String, Object> map = new HashMap<String, Object>(getElementNodeCount(valueType.getChildNodes())); for (int i = 0; i < valueType.getChildNodes().getLength(); i++) { if (valueType.getChildNodes().item(i).getNodeType() != Node.ELEMENT_NODE) continue; Element memberNode = (Element) valueType.getChildNodes().item(i); String name = null; Object value = null; if (!memberNode.getNodeName().equals("member")) return new XmlRpcException("Children of <struct> may only be " + "named <member>"); // NodeList.getElementsByTagName(String) does a deep search, so we // can legally get more than one back. Therefore, if the response // has more than one <name/> and <value/> at it's highest level, // we will only process the first one it comes across. if (memberNode.getElementsByTagName("name").getLength() < 1) return new XmlRpcException("A <struct> element must contain " + "at least one <name> child"); if (memberNode.getElementsByTagName("value").getLength() < 1) return new XmlRpcException("A <struct> element must contain " + "at least one <value> child"); name = getNodeTextValue(memberNode.getElementsByTagName("name").item(0)); value = getValueNode(memberNode.getElementsByTagName("value").item(0)); if (name == null) return new XmlRpcException("The <name> element must " + "contain a string value"); map.put(name, value); } return map; } else if (valueType.getNodeName().equals("array")) { if (getElementNodeCount(valueType.getChildNodes()) != 1) return new XmlRpcException("An <array> element must contain " + "a single <data> element"); Node dataNode = getFirstElementChild(valueType); if (!dataNode.getNodeName().equals("data")) return new XmlRpcException("An <array> element must contain " + "a single <data> element"); List<Object> list = new ArrayList<Object>(); for (int i = 0; i < dataNode.getChildNodes().getLength(); i++) { Node valueNode = dataNode.getChildNodes().item(i); if (valueNode.getNodeType() != Node.ELEMENT_NODE) continue; if (!valueNode.getNodeName().equals("value")) return new XmlRpcException("Children of <data> may only be " + "<value> elements"); list.add(getValueNode(valueNode)); } return list; } else if (valueType.getNodeName().equals("nil")) { return null; } // Not sure what it was supposed to be return new XmlRpcException( "Improper XML-RPC response format:" + " Unknown node name \"" + valueType.getNodeName() + "\""); } // Quick utility method to help properly format the pseudo ISO8601 date. // This is legacy logic used in the deprecated execute methods. private String dateString(int number, int places) { String numStr = new Integer(number).toString(); StringBuffer sb = new StringBuffer(); places -= numStr.length(); for (int i = 0; i < places; i++) { sb.append("0"); } sb.append(numStr); return sb.toString(); } // Quick utility method to achieve the text value of a node without having to // traverse one more level deep each time. Similar in feel to JDOM's // convenience method. // This is legacy logic used in the deprecated execute methods. private String getNodeTextValue(Node node) { if (node.getChildNodes().getLength() != 1) return null; Node textNode = (Node) node.getFirstChild(); if (textNode.getNodeType() != Node.TEXT_NODE) return null; return textNode.getNodeValue(); } // Fetches the number of element nodes in a node list. // This is legacy logic used in the deprecated execute methods. private int getElementNodeCount(NodeList nodeList) { int elements = 0; for (int i = 0; i < nodeList.getLength(); i++) { Node node = nodeList.item(i); if (node.getNodeType() == Node.ELEMENT_NODE) elements++; } return elements; } // Acts just like Node.getFirstChild() except this returns the first // child that is an element node. // This is legacy logic used in the deprecated execute methods. private Element getFirstElementChild(Node parentNode) { NodeList children = parentNode.getChildNodes(); for (int i = 0; i < children.getLength(); i++) { Node node = children.item(i); if (node.getNodeType() == Node.ELEMENT_NODE) return (Element) node; } return null; } }