From ffdf8bfb127f1349253fe66571ca1027480e9c14 Mon Sep 17 00:00:00 2001 From: Eric Kok Date: Tue, 22 Oct 2013 15:12:16 +0200 Subject: [PATCH] Replaced android-xmlrpc library for aXMLRPC to get a more reliable rTorrent connection (speed could be better, but I'm working on that). --- core/libs/transdroid-connect.jar | Bin 258592 -> 286245 bytes .../axmlrpc/AuthenticationManager.java | 55 ++ lib/src/de/timroes/axmlrpc/Call.java | 86 ++ lib/src/de/timroes/axmlrpc/CookieManager.java | 95 ++ .../de/timroes/axmlrpc/ResponseParser.java | 96 +++ .../de/timroes/axmlrpc/XMLRPCCallback.java | 36 + lib/src/de/timroes/axmlrpc/XMLRPCClient.java | 812 ++++++++++++++++++ .../de/timroes/axmlrpc/XMLRPCException.java | 26 + .../axmlrpc/XMLRPCRuntimeException.java | 17 + .../axmlrpc/XMLRPCServerException.java | 38 + .../axmlrpc/XMLRPCTimeoutException.java | 15 + lib/src/de/timroes/axmlrpc/XMLUtil.java | 124 +++ .../axmlrpc/serializer/ArraySerializer.java | 78 ++ .../axmlrpc/serializer/Base64Serializer.java | 24 + .../axmlrpc/serializer/BooleanSerializer.java | 24 + .../serializer/DateTimeSerializer.java | 32 + .../axmlrpc/serializer/DoubleSerializer.java | 27 + .../axmlrpc/serializer/IntSerializer.java | 23 + .../axmlrpc/serializer/LongSerializer.java | 23 + .../axmlrpc/serializer/NullSerializer.java | 21 + .../axmlrpc/serializer/Serializer.java | 34 + .../axmlrpc/serializer/SerializerHandler.java | 228 +++++ .../axmlrpc/serializer/StringSerializer.java | 38 + .../axmlrpc/serializer/StructSerializer.java | 112 +++ .../axmlrpc/xmlcreator/SimpleXMLCreator.java | 31 + .../axmlrpc/xmlcreator/XmlElement.java | 73 ++ lib/src/de/timroes/base64/Base64.java | 161 ++++ .../daemon/Rtorrent/RtorrentAdapter.java | 30 +- .../daemon/util/FakeTrustManager.java | 2 +- lib/src/org/xmlrpc/android/Base64Coder.java | 199 ----- lib/src/org/xmlrpc/android/XMLRPCClient.java | 379 -------- .../org/xmlrpc/android/XMLRPCException.java | 17 - lib/src/org/xmlrpc/android/XMLRPCFault.java | 24 - .../org/xmlrpc/android/XMLRPCSerializer.java | 205 ----- 34 files changed, 2351 insertions(+), 834 deletions(-) create mode 100644 lib/src/de/timroes/axmlrpc/AuthenticationManager.java create mode 100644 lib/src/de/timroes/axmlrpc/Call.java create mode 100644 lib/src/de/timroes/axmlrpc/CookieManager.java create mode 100644 lib/src/de/timroes/axmlrpc/ResponseParser.java create mode 100644 lib/src/de/timroes/axmlrpc/XMLRPCCallback.java create mode 100644 lib/src/de/timroes/axmlrpc/XMLRPCClient.java create mode 100644 lib/src/de/timroes/axmlrpc/XMLRPCException.java create mode 100644 lib/src/de/timroes/axmlrpc/XMLRPCRuntimeException.java create mode 100644 lib/src/de/timroes/axmlrpc/XMLRPCServerException.java create mode 100644 lib/src/de/timroes/axmlrpc/XMLRPCTimeoutException.java create mode 100644 lib/src/de/timroes/axmlrpc/XMLUtil.java create mode 100644 lib/src/de/timroes/axmlrpc/serializer/ArraySerializer.java create mode 100644 lib/src/de/timroes/axmlrpc/serializer/Base64Serializer.java create mode 100644 lib/src/de/timroes/axmlrpc/serializer/BooleanSerializer.java create mode 100644 lib/src/de/timroes/axmlrpc/serializer/DateTimeSerializer.java create mode 100644 lib/src/de/timroes/axmlrpc/serializer/DoubleSerializer.java create mode 100644 lib/src/de/timroes/axmlrpc/serializer/IntSerializer.java create mode 100644 lib/src/de/timroes/axmlrpc/serializer/LongSerializer.java create mode 100644 lib/src/de/timroes/axmlrpc/serializer/NullSerializer.java create mode 100644 lib/src/de/timroes/axmlrpc/serializer/Serializer.java create mode 100644 lib/src/de/timroes/axmlrpc/serializer/SerializerHandler.java create mode 100644 lib/src/de/timroes/axmlrpc/serializer/StringSerializer.java create mode 100644 lib/src/de/timroes/axmlrpc/serializer/StructSerializer.java create mode 100644 lib/src/de/timroes/axmlrpc/xmlcreator/SimpleXMLCreator.java create mode 100644 lib/src/de/timroes/axmlrpc/xmlcreator/XmlElement.java create mode 100644 lib/src/de/timroes/base64/Base64.java delete mode 100644 lib/src/org/xmlrpc/android/Base64Coder.java delete mode 100644 lib/src/org/xmlrpc/android/XMLRPCClient.java delete mode 100644 lib/src/org/xmlrpc/android/XMLRPCException.java delete mode 100644 lib/src/org/xmlrpc/android/XMLRPCFault.java delete mode 100644 lib/src/org/xmlrpc/android/XMLRPCSerializer.java diff --git a/core/libs/transdroid-connect.jar b/core/libs/transdroid-connect.jar index 1fdbd664da6b8b4d288164dde0e21042872462e4..868d4aa15400d8fe0fe2127929b2633fa38d2f9f 100644 GIT binary patch delta 54556 zcma%?V~}KBx1h_mZQHKuve9MRwvlDCOI^0zW!tuG+nRpwor#&b@!fAG^T*0K5qW;( zUis{Op0!r)0b)@HGTcuYP%tzgsK0Mi_XIe7P-|DUIy)p_5F6Kee_#!ef8rYGD(FA4 z5podfpXh^>1NKkEM@NMDCo&MSLjH}3YoI)-J42vo^>q}9|9mmg2Z^WNpBna`n1nn5_9ns0I;HI`N`>(_xqOfbnE8Zd$&FS z)B!t+2M5e6iUpIa=no?;w{`tN%JRj=M-n$KS2coMuJ7R6)>vywb5eVI3(2Km?rGaT zcduN4sF{PjAnl7-nlQ*D1kNnkj2TaMN4=g_(jm6>Y%&ObvAyo30PqL@Jk;mD>YMu?xKOWZp59 zj-UzqWLPr*L#-r*Roj~7z*#Fx3fBV?6hzzxkX66on%>S5WF{ALntS0P^NKdL9~kS7 zNQJ%%*HMyhp_7!fo*O_HTAD-KS!BRH!I?H77Qh<0+Yo&s?JcV*rCoh)Ke8tYLz$pF zrl({|0M5jfI$#>$%)EMPTJekF)iezug`V-!o5hy)bZicH_7)htsvv&vEi#}2Mwc}T zz+vp4E>jFHzlfsHM zsq0`$L)M`NVa%)Ld_∓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 M-L!EWP?2c=2Z9lvyZ`_I delta 26867 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 hO8C-cUk?{2f8#UwU 0) { + XmlElement params = new XmlElement(XMLRPCClient.PARAMS); + methodCall.addChildren(params); + + for(Object o : this.params) { + params.addChildren(getXMLParam(o)); + } + } + + return creator.toString(); + } + + /** + * Generates the param xml tag for a specific parameter object. + * + * @param o The parameter object. + * @return The object serialized into an xml tag. + * @throws XMLRPCException Will be thrown if the serialization failed. + */ + private XmlElement getXMLParam(Object o) throws XMLRPCException { + XmlElement param = new XmlElement(XMLRPCClient.PARAM); + XmlElement value = new XmlElement(XMLRPCClient.VALUE); + param.addChildren(value); + value.addChildren(SerializerHandler.getDefault().serialize(o)); + return param; + } + +} diff --git a/lib/src/de/timroes/axmlrpc/CookieManager.java b/lib/src/de/timroes/axmlrpc/CookieManager.java new file mode 100644 index 00000000..8f6ae475 --- /dev/null +++ b/lib/src/de/timroes/axmlrpc/CookieManager.java @@ -0,0 +1,95 @@ +package de.timroes.axmlrpc; + +import java.net.HttpURLConnection; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * The CookieManager handles cookies for the http requests. + * If the FLAGS_ENABLE_COOKIES has been set, it will save cookies + * and send it with every request. + * + * @author Tim Roes + */ +class CookieManager { + + private static final String SET_COOKIE = "Set-Cookie"; + private static final String COOKIE = "Cookie"; + + private int flags; + private Map cookies = new ConcurrentHashMap(); + + /** + * Create a new CookieManager with the given flags. + * + * @param flags A combination of flags to be set. + */ + public CookieManager(int flags) { + this.flags = flags; + } + + /** + * Delete all cookies. + */ + public void clearCookies() { + cookies.clear(); + } + + /** + * Returns a {@link Map} of all cookies. + * + * @return All cookies + */ + public Map getCookies() { + return cookies; + } + + /** + * Read the cookies from an http response. It will look at every Set-Cookie + * header and put the cookie to the map of cookies. + * + * @param http A http connection. + */ + public void readCookies(HttpURLConnection http) { + + // Only save cookies if FLAGS_ENABLE_COOKIES has been set. + if((flags & XMLRPCClient.FLAGS_ENABLE_COOKIES) == 0) + return; + + String cookie, key; + String[] split; + + // Extract every Set-Cookie field and put the cookie to the cookies map. + for(int i = 0; i < http.getHeaderFields().size(); i++) { + key = http.getHeaderFieldKey(i); + if(key != null && SET_COOKIE.toLowerCase().equals(key.toLowerCase())) { + cookie = http.getHeaderField(i).split(";")[0]; + split = cookie.split("="); + if(split.length >= 2) + cookies.put(split[0], split[1]); + } + } + + } + + /** + * Write the cookies to a http connection. It will set the Cookie field + * to all currently set cookies in the map. + * + * @param http A http connection. + */ + public void setCookies(HttpURLConnection http) { + + // Only save cookies if FLAGS_ENABLE_COOKIES has been set. + if((flags & XMLRPCClient.FLAGS_ENABLE_COOKIES) == 0) + return; + + String concat = ""; + for(Map.Entry cookie : cookies.entrySet()) { + concat += cookie.getKey() + "=" + cookie.getValue() + "; "; + } + http.setRequestProperty(COOKIE, concat); + + } + +} \ No newline at end of file diff --git a/lib/src/de/timroes/axmlrpc/ResponseParser.java b/lib/src/de/timroes/axmlrpc/ResponseParser.java new file mode 100644 index 00000000..b0968963 --- /dev/null +++ b/lib/src/de/timroes/axmlrpc/ResponseParser.java @@ -0,0 +1,96 @@ +package de.timroes.axmlrpc; + +import de.timroes.axmlrpc.serializer.SerializerHandler; +import java.io.InputStream; +import java.util.Map; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +/** + * The ResponseParser parses the response of an XMLRPC server to an object. + * + * @author Tim Roes + */ +class ResponseParser { + + private static final String FAULT_CODE = "faultCode"; + private static final String FAULT_STRING = "faultString"; + + /** + * The given InputStream must contain the xml response from an xmlrpc server. + * This method extract the content of it as an object. + * + * @param response The InputStream of the server response. + * @return The returned object. + * @throws XMLRPCException Will be thrown whenever something fails. + * @throws XMLRPCServerException Will be thrown, if the server returns an error. + */ + public Object parse(InputStream response) throws XMLRPCException { + + try { + + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document dom = builder.parse(response); + Element e = dom.getDocumentElement(); + + // Check for root tag + if(!e.getNodeName().equals(XMLRPCClient.METHOD_RESPONSE)) { + throw new XMLRPCException("MethodResponse root tag is missing."); + } + + e = XMLUtil.getOnlyChildElement(e.getChildNodes()); + + if(e.getNodeName().equals(XMLRPCClient.PARAMS)) { + + e = XMLUtil.getOnlyChildElement(e.getChildNodes()); + + if(!e.getNodeName().equals(XMLRPCClient.PARAM)) { + throw new XMLRPCException("The params tag must contain a param tag."); + } + + return getReturnValueFromElement(e); + + } else if(e.getNodeName().equals(XMLRPCClient.FAULT)) { + + @SuppressWarnings("unchecked") + Map o = (Map)getReturnValueFromElement(e); + + throw new XMLRPCServerException((String)o.get(FAULT_STRING), (Integer)o.get(FAULT_CODE)); + + } + + throw new XMLRPCException("The methodResponse tag must contain a fault or params tag."); + + } catch (Exception ex) { + + if(ex instanceof XMLRPCServerException) + throw (XMLRPCServerException)ex; + else + throw new XMLRPCException("Error getting result from server.", ex); + + } + + } + + /** + * This method takes an element (must be a param or fault element) and + * returns the deserialized object of this param tag. + * + * @param element An param element. + * @return The deserialized object within the given param element. + * @throws XMLRPCException Will be thrown when the structure of the document + * doesn't match the XML-RPC specification. + */ + private Object getReturnValueFromElement(Element element) throws XMLRPCException { + + element = XMLUtil.getOnlyChildElement(element.getChildNodes()); + + return SerializerHandler.getDefault().deserialize(element); + + } + +} \ No newline at end of file diff --git a/lib/src/de/timroes/axmlrpc/XMLRPCCallback.java b/lib/src/de/timroes/axmlrpc/XMLRPCCallback.java new file mode 100644 index 00000000..9e4c0497 --- /dev/null +++ b/lib/src/de/timroes/axmlrpc/XMLRPCCallback.java @@ -0,0 +1,36 @@ +package de.timroes.axmlrpc; + +/** + * The XMLRPCCallback interface must be implemented by a listener for an + * asynchronous call to a server method. + * When the server responds, the corresponding method on the listener is called. + * + * @author Tim Roes + */ +public interface XMLRPCCallback { + + /** + * This callback is called whenever the server successfully responds. + * + * @param id The id as returned by the XMLRPCClient.asyncCall(..) method for this request. + * @param result The Object returned from the server. + */ + public void onResponse(long id, Object result); + + /** + * This callback is called whenever an error occurs during the method call. + * + * @param id The id as returned by the XMLRPCClient.asyncCall(..) method for this request. + * @param error The error occured. + */ + public void onError(long id, XMLRPCException error); + + /** + * This callback is called whenever the server returns an error. + * + * @param id The id as returned by the XMLRPCClient.asyncCall(..) method for this request. + * @param error The error returned from the server. + */ + public void onServerError(long id, XMLRPCServerException error); + +} diff --git a/lib/src/de/timroes/axmlrpc/XMLRPCClient.java b/lib/src/de/timroes/axmlrpc/XMLRPCClient.java new file mode 100644 index 00000000..d07332a9 --- /dev/null +++ b/lib/src/de/timroes/axmlrpc/XMLRPCClient.java @@ -0,0 +1,812 @@ +package de.timroes.axmlrpc; + +import de.timroes.axmlrpc.serializer.SerializerHandler; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStreamWriter; +import java.net.*; +import java.security.SecureRandom; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.ConcurrentHashMap; +import javax.net.ssl.*; + +/** + * An XMLRPCClient is a client used to make XML-RPC (Extensible Markup Language + * Remote Procedure Calls). + * The specification of XMLRPC can be found at http://www.xmlrpc.com/spec. + * You can use flags to extend the functionality of the client to some extras. + * Further information on the flags can be found in the documentation of these. + * For a documentation on how to use this class see also the README file delivered + * with the source of this library. + * + * @author Tim Roes + */ +public class XMLRPCClient { + + private static final String DEFAULT_USER_AGENT = "aXMLRPC"; + + /** + * Constants from the http protocol. + */ + static final String USER_AGENT = "User-Agent"; + static final String CONTENT_TYPE = "Content-Type"; + static final String TYPE_XML = "text/xml; charset=utf-8"; + static final String HOST = "Host"; + static final String CONTENT_LENGTH = "Content-Length"; + static final String HTTP_POST = "POST"; + + /** + * XML elements to be used. + */ + static final String METHOD_RESPONSE = "methodResponse"; + static final String PARAMS = "params"; + static final String PARAM = "param"; + public static final String VALUE = "value"; + static final String FAULT = "fault"; + static final String METHOD_CALL = "methodCall"; + static final String METHOD_NAME = "methodName"; + static final String STRUCT_MEMBER = "member"; + + /** + * No flags should be set. + */ + public static final int FLAGS_NONE = 0x0; + + /** + * The client should parse responses strict to specification. + * It will check if the given content-type is right. + * The method name in a call must only contain of A-Z, a-z, 0-9, _, ., :, / + * Normally this is not needed. + */ + public static final int FLAGS_STRICT = 0x01; + + /** + * The client will be able to handle 8 byte integer values (longs). + * The xml type tag <i8> will be used. This is not in the specification + * but some libraries and servers support this behaviour. + * If this isn't enabled you cannot recieve 8 byte integers and if you try to + * send a long the value must be within the 4byte integer range. + */ + public static final int FLAGS_8BYTE_INT = 0x02; + + /** + * With this flag, the client will be able to handle cookies, meaning saving cookies + * from the server and sending it with every other request again. This is needed + * for some XML-RPC interfaces that support login. + */ + public static final int FLAGS_ENABLE_COOKIES = 0x04; + + /** + * The client will be able to send null values. A null value will be send + * as . This extension is described under: http://ontosys.com/xml-rpc/extensions.php + */ + public static final int FLAGS_NIL = 0x08; + + /** + * With this flag enabled, the XML-RPC client will ignore the HTTP status + * code of the response from the server. According to specification the + * status code must be 200. This flag is only needed for the use with + * not standard compliant servers. + */ + public static final int FLAGS_IGNORE_STATUSCODE = 0x10; + + /** + * With this flag enabled, the client will forward the request, if + * the 301 or 302 HTTP status code has been received. If this flag has not + * been set, the client will throw an exception on these HTTP status codes. + */ + public static final int FLAGS_FORWARD = 0x20; + + /** + * With this flag enabled, the client will ignore, if the URL doesn't match + * the SSL Certificate. This should be used with caution. Normally the URL + * should always match the URL in the SSL certificate, even with self signed + * certificates. + */ + public static final int FLAGS_SSL_IGNORE_INVALID_HOST = 0x40; + + /** + * With this flag enabled, the client will ignore all unverified SSL/TLS + * certificates. This must be used, if you use self-signed certificates + * or certificated from unknown (or untrusted) authorities. If this flag is + * used, calls to {@link #installCustomTrustManager(javax.net.ssl.TrustManager)} + * won't have any effect. + */ + public static final int FLAGS_SSL_IGNORE_INVALID_CERT = 0x80; + + /** + * With this flag enabled, a value with a missing type tag, will be parsed + * as a string element. This is just for incoming messages. Outgoing messages + * will still be generated according to specification. + */ + public static final int FLAGS_DEFAULT_TYPE_STRING = 0x100; + + /** + * With this flag enabled, the {@link XMLRPCClient} ignores all namespaces + * used within the response from the server. + */ + public static final int FLAGS_IGNORE_NAMESPACES = 0x200; + + /** + * With this flag enabled, the {@link XMLRPCClient} will use the system http + * proxy to connect to the XML-RPC server. + */ + public static final int FLAGS_USE_SYSTEM_PROXY = 0x400; + + /** + * This prevents the decoding of incoming strings, meaning & and < + * won't be decoded to the & sign and the "less then" sign. See + * {@link #FLAGS_NO_STRING_ENCODE} for the counterpart. + */ + public static final int FLAGS_NO_STRING_DECODE = 0x800; + + /** + * By default outgoing string values will be encoded according to specification. + * Meaning the & sign will be encoded to & and the "less then" sign to <. + * If you set this flag, the encoding won't be done for outgoing string values. + * See {@link #FLAGS_NO_STRING_ENCODE} for the counterpart. + */ + public static final int FLAGS_NO_STRING_ENCODE = 0x1000; + + /** + * This flag disables all SSL warnings. It is an alternative to use + * FLAGS_SSL_IGNORE_INVALID_CERT | FLAGS_SSL_IGNORE_INVALID_HOST. There + * is no functional difference. + */ + public static final int FLAGS_SSL_IGNORE_ERRORS = + FLAGS_SSL_IGNORE_INVALID_CERT | FLAGS_SSL_IGNORE_INVALID_HOST; + + /** + * This flag should be used if the server is an apache ws xmlrpc server. + * This will set some flags, so that the not standard conform behavior + * of the server will be ignored. + * This will enable the following flags: FLAGS_IGNORE_NAMESPACES, FLAGS_NIL, + * FLAGS_DEFAULT_TYPE_STRING + */ + public static final int FLAGS_APACHE_WS = FLAGS_IGNORE_NAMESPACES | FLAGS_NIL + | FLAGS_DEFAULT_TYPE_STRING; + + private final int flags; + + private URL url; + private Map httpParameters = new ConcurrentHashMap(); + + private Map backgroundCalls = new ConcurrentHashMap(); + + private ResponseParser responseParser; + private CookieManager cookieManager; + private AuthenticationManager authManager; + + private TrustManager[] trustManagers; + private KeyManager[] keyManagers; + + private Proxy proxy; + + private int timeout; + + /** + * Create a new XMLRPC client for the given URL. + * + * @param url The URL to send the requests to. + * @param userAgent A user agent string to use in the HTTP requests. + * @param flags A combination of flags to be set. + */ + public XMLRPCClient(URL url, String userAgent, int flags) { + + SerializerHandler.initialize(flags); + + this.url = url; + + this.flags = flags; + // Create a parser for the http responses. + responseParser = new ResponseParser(); + + cookieManager = new CookieManager(flags); + authManager = new AuthenticationManager(); + + httpParameters.put(CONTENT_TYPE, TYPE_XML); + httpParameters.put(USER_AGENT, userAgent); + + // If invalid ssl certs are ignored, instantiate an all trusting TrustManager + if(isFlagSet(FLAGS_SSL_IGNORE_INVALID_CERT)) { + trustManagers = new TrustManager[] { + new X509TrustManager() { + public void checkClientTrusted(X509Certificate[] xcs, String string) + throws CertificateException { } + + public void checkServerTrusted(X509Certificate[] xcs, String string) + throws CertificateException { } + + public X509Certificate[] getAcceptedIssuers() { + return null; + } + } + }; + } + + if(isFlagSet(FLAGS_USE_SYSTEM_PROXY)) { + // Read system proxy settings and generate a proxy from that + Properties prop = System.getProperties(); + String proxyHost = prop.getProperty("http.proxyHost"); + int proxyPort = Integer.parseInt(prop.getProperty("http.proxyPort", "0")); + if(proxyPort > 0 && proxyHost.length() > 0 && !proxyHost.equals("null")) { + proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, proxyPort)); + } + } + + } + + /** + * Create a new XMLRPC client for the given URL. + * The default user agent string will be used. + * + * @param url The URL to send the requests to. + * @param flags A combination of flags to be set. + */ + public XMLRPCClient(URL url, int flags) { + this(url, DEFAULT_USER_AGENT, flags); + } + + /** + * Create a new XMLRPC client for the given url. + * No flags will be set. + * + * @param url The url to send the requests to. + * @param userAgent A user agent string to use in the http request. + */ + public XMLRPCClient(URL url, String userAgent) { + this(url, userAgent, FLAGS_NONE); + } + + /** + * Create a new XMLRPC client for the given url. + * No flags will be used. + * The default user agent string will be used. + * + * @param url The url to send the requests to. + */ + public XMLRPCClient(URL url) { + this(url, DEFAULT_USER_AGENT, FLAGS_NONE); + } + + /** + * Returns the URL this XMLRPCClient is connected to. If that URL permanently forwards + * to another URL, this method will return the forwarded URL, as soon as + * the first call has been made. + * + * @return Returns the URL for this XMLRPCClient. + */ + public URL getURL() { + return url; + } + + /** + * Sets the time in seconds after which a call should timeout. + * If {@code timeout} will be zero or less the connection will never timeout. + * In case the connection times out and {@link XMLRPCTimeoutException} will + * be thrown for calls made by {@link #call(java.lang.String, java.lang.Object[])}. + * For calls made by {@link #callAsync(de.timroes.axmlrpc.XMLRPCCallback, java.lang.String, java.lang.Object[])} + * the {@link XMLRPCCallback#onError(long, de.timroes.axmlrpc.XMLRPCException)} method + * of the callback will be called. By default connections won't timeout. + * + * @param timeout The timeout for connections in seconds. + */ + public void setTimeout(int timeout) { + this.timeout = timeout; + } + + /** + * Sets the user agent string. + * If this method is never called the default + * user agent 'aXMLRPC' will be used. + * + * @param userAgent The new user agent string. + */ + public void setUserAgentString(String userAgent) { + httpParameters.put(USER_AGENT, userAgent); + } + + /** + * Sets a proxy to use for this client. If you want to use the system proxy, + * use {@link #FLAGS_adbUSE_SYSTEM_PROXY} instead. If combined with + * {@code FLAGS_USE_SYSTEM_PROXY}, this proxy will be used instead of the + * system proxy. + * + * @param proxy A proxy to use for the connection. + */ + public void setProxy(Proxy proxy) { + this.proxy = proxy; + } + + /** + * Set a HTTP header field to a custom value. + * You cannot modify the Host or Content-Type field that way. + * If the field already exists, the old value is overwritten. + * + * @param headerName The name of the header field. + * @param headerValue The new value of the header field. + */ + public void setCustomHttpHeader(String headerName, String headerValue) { + if(CONTENT_TYPE.equals(headerName) || HOST.equals(headerName) + || CONTENT_LENGTH.equals(headerName)) { + throw new XMLRPCRuntimeException("You cannot modify the Host, Content-Type or Content-Length header."); + } + httpParameters.put(headerName, headerValue); + } + + /** + * Set the username and password that should be used to perform basic + * http authentication. + * + * @param user Username + * @param pass Password + */ + public void setLoginData(String user, String pass) { + authManager.setAuthData(user, pass); + } + + /** + * Clear the username and password. No basic HTTP authentication will be used + * in the next calls. + */ + public void clearLoginData() { + authManager.clearAuthData(); + } + + /** + * Returns a {@link Map} of all cookies. It contains each cookie key as a map + * key and its value as a map value. Cookies will only be used if {@link #FLAGS_ENABLE_COOKIES} + * has been set for the client. This map will also be available (and empty) + * when this flag hasn't been said, but has no effect on the HTTP connection. + * + * @return A {@code Map} of all cookies. + */ + public Map getCookies() { + return cookieManager.getCookies(); + } + + /** + * Delete all cookies currently used by the client. + * This method has only an effect, as long as the FLAGS_ENABLE_COOKIES has + * been set on this client. + */ + public void clearCookies() { + cookieManager.clearCookies(); + } + + /** + * Installs a custom {@link TrustManager} to handle SSL/TLS certificate verification. + * This will replace any previously installed {@code TrustManager}s. + * If {@link #FLAGS_SSL_IGNORE_INVALID_CERT} is set, this won't do anything. + * + * @param trustManager {@link TrustManager} to install. + * + * @see #installCustomTrustManagers(javax.net.ssl.TrustManager[]) + */ + public void installCustomTrustManager(TrustManager trustManager) { + if(!isFlagSet(FLAGS_SSL_IGNORE_INVALID_CERT)) { + trustManagers = new TrustManager[] { trustManager }; + } + } + + /** + * Installs custom {@link TrustManager TrustManagers} to handle SSL/TLS certificate + * verification. This will replace any previously installed {@code TrustManagers}s. + * If {@link #FLAGS_SSL_IGNORE_INVALID_CERT} is set, this won't do anything. + * + * @param trustManagers {@link TrustManager TrustManagers} to install. + * + * @see #installCustomTrustManager(javax.net.ssl.TrustManager) + */ + public void installCustomTrustManagers(TrustManager[] trustManagers) { + if(!isFlagSet(FLAGS_SSL_IGNORE_INVALID_CERT)) { + this.trustManagers = trustManagers.clone(); + } + } + + /** + * Installs a custom {@link KeyManager} to handle SSL/TLS certificate verification. + * This will replace any previously installed {@code KeyManager}s. + * If {@link #FLAGS_SSL_IGNORE_INVALID_CERT} is set, this won't do anything. + * + * @param keyManager {@link KeyManager} to install. + * + * @see #installCustomKeyManagers(javax.net.ssl.KeyManager[]) + */ + public void installCustomKeyManager(KeyManager keyManager) { + if(!isFlagSet(FLAGS_SSL_IGNORE_INVALID_CERT)) { + keyManagers = new KeyManager[] { keyManager }; + } + } + + /** + * Installs custom {@link KeyManager KeyManagers} to handle SSL/TLS certificate + * verification. This will replace any previously installed {@code KeyManagers}s. + * If {@link #FLAGS_SSL_IGNORE_INVALID_CERT} is set, this won't do anything. + * + * @param keyManagers {@link KeyManager KeyManagers} to install. + * + * @see #installCustomKeyManager(javax.net.ssl.KeyManager) + */ + public void installCustomKeyManagers(KeyManager[] keyManagers) { + if(!isFlagSet(FLAGS_SSL_IGNORE_INVALID_CERT)) { + this.keyManagers = keyManagers.clone(); + } + } + + /** + * Call a remote procedure on the server. The method must be described by + * a method name. If the method requires parameters, this must be set. + * The type of the return object depends on the server. You should consult + * the server documentation and then cast the return value according to that. + * This method will block until the server returned a result (or an error occurred). + * Read the README file delivered with the source code of this library for more + * information. + * + * @param method A method name to call. + * @param params An array of parameters for the method. + * @return The result of the server. + * @throws XMLRPCException Will be thrown if an error occurred during the call. + */ + public Object call(String method, Object... params) throws XMLRPCException { + return new Caller().call(method, params); + } + + /** + * Asynchronously call a remote procedure on the server. The method must be + * described by a method name. If the method requires parameters, this must + * be set. When the server returns a response the onResponse method is called + * on the listener. If the server returns an error the onServerError method + * is called on the listener. The onError method is called whenever something + * fails. This method returns immediately and returns an identifier for the + * request. All listener methods get this id as a parameter to distinguish between + * multiple requests. + * + * @param listener A listener, which will be notified about the server response or errors. + * @param methodName A method name to call on the server. + * @param params An array of parameters for the method. + * @return The id of the current request. + */ + public long callAsync(XMLRPCCallback listener, String methodName, Object... params) { + long id = System.currentTimeMillis(); + new Caller(listener, id, methodName, params).start(); + return id; + } + + /** + * Cancel a specific asynchronous call. + * + * @param id The id of the call as returned by the callAsync method. + */ + public void cancel(long id) { + + // Lookup the background call for the given id. + Caller cancel = backgroundCalls.get(id); + if(cancel == null) { + return; + } + + // Cancel the thread + cancel.cancel(); + + try { + // Wait for the thread + cancel.join(); + } catch (InterruptedException ex) { + // Ignore this + } + + } + + /** + * Create a call object from a given method string and parameters. + * + * @param method The method that should be called. + * @param params An array of parameters or null if no parameters needed. + * @return A call object. + */ + private Call createCall(String method, Object[] params) { + + if(isFlagSet(FLAGS_STRICT) && !method.matches("^[A-Za-z0-9\\._:/]*$")) { + throw new XMLRPCRuntimeException("Method name must only contain A-Z a-z . : _ / "); + } + + return new Call(method, params); + + } + + /** + * Checks whether a specific flag has been set. + * + * @param flag The flag to check for. + * @return Whether the flag has been set. + */ + private boolean isFlagSet(int flag) { + return (this.flags & flag) != 0; + } + + /** + * The Caller class is used to make asynchronous calls to the server. + * For synchronous calls the Thread function of this class isn't used. + */ + private class Caller extends Thread { + + private XMLRPCCallback listener; + private long threadId; + private String methodName; + private Object[] params; + + private volatile boolean canceled; + private HttpURLConnection http; + + /** + * Create a new Caller for asynchronous use. + * + * @param listener The listener to notice about the response or an error. + * @param threadId An id that will be send to the listener. + * @param methodName The method name to call. + * @param params The parameters of the call or null. + */ + public Caller(XMLRPCCallback listener, long threadId, String methodName, Object[] params) { + this.listener = listener; + this.threadId = threadId; + this.methodName = methodName; + this.params = params; + } + + /** + * Create a new Caller for synchronous use. + * If the caller has been created with this constructor you cannot use the + * start method to start it as a thread. But you can call the call method + * on it for synchronous use. + */ + public Caller() { } + + /** + * The run method is invoked when the thread gets started. + * This will only work, if the Caller has been created with parameters. + * It execute the call method and notify the listener about the result. + */ + @Override + public void run() { + + if(listener == null) + return; + + try { + backgroundCalls.put(threadId, this); + Object o = this.call(methodName, params); + listener.onResponse(threadId, o); + } catch(CancelException ex) { + // Don't notify the listener, if the call has been canceled. + } catch(XMLRPCServerException ex) { + listener.onServerError(threadId, ex); + } catch (XMLRPCException ex) { + listener.onError(threadId, ex); + } finally { + backgroundCalls.remove(threadId); + } + + } + + /** + * Cancel this call. This will abort the network communication. + */ + public void cancel() { + // Set the flag, that this thread has been canceled + canceled = true; + // Disconnect the connection to the server + http.disconnect(); + } + + /** + * Call a remote procedure on the server. The method must be described by + * a method name. If the method requires parameters, this must be set. + * The type of the return object depends on the server. You should consult + * the server documentation and then cast the return value according to that. + * This method will block until the server returned a result (or an error occurred). + * Read the README file delivered with the source code of this library for more + * information. + * + * @param method A method name to call. + * @param params An array of parameters for the method. + * @return The result of the server. + * @throws XMLRPCException Will be thrown if an error occurred during the call. + */ + public Object call(String methodName, Object[] params) throws XMLRPCException { + + try { + + Call c = createCall(methodName, params); + + // If proxy is available, use it + URLConnection conn; + if(proxy != null) + conn = url.openConnection(proxy); + else + conn = url.openConnection(); + + http = verifyConnection(conn); + http.setInstanceFollowRedirects(false); + http.setRequestMethod(HTTP_POST); + http.setDoOutput(true); + http.setDoInput(true); + + // Set timeout + if(timeout > 0) { + http.setConnectTimeout(timeout * 1000); + http.setReadTimeout(timeout * 1000); + } + + // Set the request parameters + for(Map.Entry param : httpParameters.entrySet()) { + http.setRequestProperty(param.getKey(), param.getValue()); + } + + authManager.setAuthentication(http); + cookieManager.setCookies(http); + + OutputStreamWriter stream = new OutputStreamWriter(http.getOutputStream()); + stream.write(c.getXML()); + stream.flush(); + stream.close(); + + // Try to get the status code from the connection + int statusCode; + try { + statusCode = http.getResponseCode(); + } catch(IOException ex) { + // Due to a bug on android, the getResponseCode()-method will + // fail the first time, with a IOException, when 401 or 403 has been returned. + // The second time it should success. If it fail the second time again + // the normal exceptipon handling can take care of this, since + // it is a real error. + statusCode = http.getResponseCode(); + } + + InputStream istream; + + // If status code was 401 or 403 throw exception or if appropriate + // flag is set, ignore error code. + if(statusCode == HttpURLConnection.HTTP_FORBIDDEN + || statusCode == HttpURLConnection.HTTP_UNAUTHORIZED) { + + if(isFlagSet(FLAGS_IGNORE_STATUSCODE)) { + // getInputStream will fail if server returned above + // error code, use getErrorStream instead + istream = http.getErrorStream(); + } else { + throw new XMLRPCException("Invalid status code '" + + statusCode + "' returned from server."); + } + + } else { + istream = http.getInputStream(); + } + + // If status code is 301 Moved Permanently or 302 Found ... + if(statusCode == HttpURLConnection.HTTP_MOVED_PERM + || statusCode == HttpURLConnection.HTTP_MOVED_TEMP) { + // ... do either a foward + if(isFlagSet(FLAGS_FORWARD)) { + boolean temporaryForward = (statusCode == HttpURLConnection.HTTP_MOVED_TEMP); + + // Get new location from header field. + String newLocation = http.getHeaderField("Location"); + // Try getting header in lower case, if no header has been found + if(newLocation == null || newLocation.length() <= 0) + newLocation = http.getHeaderField("location"); + + // Set new location, disconnect current connection and request to new location. + URL oldURL = url; + url = new URL(newLocation); + http.disconnect(); + Object forwardedResult = call(methodName, params); + + // In case of temporary forward, restore original URL again for next call. + if(temporaryForward) { + url = oldURL; + } + + return forwardedResult; + + } else { + // ... or throw an exception + throw new XMLRPCException("The server responded with a http 301 or 302 status " + + "code, but forwarding has not been enabled (FLAGS_FORWARD)."); + + } + } + + if(!isFlagSet(FLAGS_IGNORE_STATUSCODE) + && statusCode != HttpURLConnection.HTTP_OK) { + throw new XMLRPCException("The status code of the http response must be 200."); + } + + // Check for strict parameters + if(isFlagSet(FLAGS_STRICT)) { + if(!http.getContentType().startsWith(TYPE_XML)) { + throw new XMLRPCException("The Content-Type of the response must be text/xml."); + } + } + + cookieManager.readCookies(http); + + return responseParser.parse(istream); + + } catch(SocketTimeoutException ex) { + throw new XMLRPCTimeoutException("The XMLRPC call timed out."); + } catch (IOException ex) { + // If the thread has been canceled this exception will be thrown. + // So only throw an exception if the thread hasnt been canceled + // or if the thred has not been started in background. + if(!canceled || threadId <= 0) { + throw new XMLRPCException(ex); + } else { + throw new CancelException(); + } + } + + } + + /** + * Verifies the given URLConnection to be a valid HTTP or HTTPS connection. + * If the SSL ignoring flags are set, the method will ignore SSL warnings. + * + * @param conn The URLConnection to validate. + * @return The verified HttpURLConnection. + * @throws XMLRPCException Will be thrown if an error occurred. + */ + private HttpURLConnection verifyConnection(URLConnection conn) throws XMLRPCException { + + if(!(conn instanceof HttpURLConnection)) { + throw new IllegalArgumentException("The URL is not valid for a http connection."); + } + + // Validate the connection if its an SSL connection + if(conn instanceof HttpsURLConnection) { + + HttpsURLConnection h = (HttpsURLConnection)conn; + + // Don't check, that URL matches the certificate. + if(isFlagSet(FLAGS_SSL_IGNORE_INVALID_HOST)) { + h.setHostnameVerifier(new HostnameVerifier() { + public boolean verify(String host, SSLSession ssl) { + return true; + } + }); + } + + // Associate the TrustManager with TLS and SSL connections, if present. + if(trustManagers != null) { + try { + String[] sslContexts = new String[]{ "TLS", "SSL" }; + + for(String ctx : sslContexts) { + SSLContext sc = SSLContext.getInstance(ctx); + sc.init(keyManagers, trustManagers, new SecureRandom()); + h.setSSLSocketFactory(sc.getSocketFactory()); + } + + } catch(Exception ex) { + throw new XMLRPCException(ex); + } + + } + + return h; + + } + + return (HttpURLConnection)conn; + + } + + } + + private class CancelException extends RuntimeException { } + +} diff --git a/lib/src/de/timroes/axmlrpc/XMLRPCException.java b/lib/src/de/timroes/axmlrpc/XMLRPCException.java new file mode 100644 index 00000000..96c8572d --- /dev/null +++ b/lib/src/de/timroes/axmlrpc/XMLRPCException.java @@ -0,0 +1,26 @@ +package de.timroes.axmlrpc; + +/** + * The exception is thrown whenever the remote procedure call fails in some point. + * + * @author Tim Roes + */ +public class XMLRPCException extends Exception { + + public XMLRPCException() { + super(); + } + + public XMLRPCException(Exception ex) { + super(ex); + } + + public XMLRPCException(String ex) { + super(ex); + } + + public XMLRPCException(String msg, Exception ex) { + super(msg, ex); + } + +} diff --git a/lib/src/de/timroes/axmlrpc/XMLRPCRuntimeException.java b/lib/src/de/timroes/axmlrpc/XMLRPCRuntimeException.java new file mode 100644 index 00000000..f2dfcebb --- /dev/null +++ b/lib/src/de/timroes/axmlrpc/XMLRPCRuntimeException.java @@ -0,0 +1,17 @@ +package de.timroes.axmlrpc; + +/** + * + * @author Tim Roes + */ +public class XMLRPCRuntimeException extends RuntimeException { + + public XMLRPCRuntimeException(String ex) { + super(ex); + } + + public XMLRPCRuntimeException(Exception ex) { + super(ex); + } + +} \ No newline at end of file diff --git a/lib/src/de/timroes/axmlrpc/XMLRPCServerException.java b/lib/src/de/timroes/axmlrpc/XMLRPCServerException.java new file mode 100644 index 00000000..0b403e93 --- /dev/null +++ b/lib/src/de/timroes/axmlrpc/XMLRPCServerException.java @@ -0,0 +1,38 @@ +package de.timroes.axmlrpc; + +/** + * This exception will be thrown if the server returns an error. It contains the + * message and the error number returned from the server. + * + * @author Tim Roes + */ +public class XMLRPCServerException extends XMLRPCException { + + private int errornr; + + public XMLRPCServerException(String ex, int errnr) { + super(ex); + this.errornr = errnr; + } + + /** + * Returns the detail message string of this throwable. + * It will have the server error number at the end. + * + * @return The detail message string of this error. + */ + @Override + public String getMessage() { + return super.getMessage() + " [" + errornr + "]"; + } + + /** + * Return the error number. + * + * @return The error number. + */ + public int getErrorNr() { + return errornr; + } + +} diff --git a/lib/src/de/timroes/axmlrpc/XMLRPCTimeoutException.java b/lib/src/de/timroes/axmlrpc/XMLRPCTimeoutException.java new file mode 100644 index 00000000..6c24ff94 --- /dev/null +++ b/lib/src/de/timroes/axmlrpc/XMLRPCTimeoutException.java @@ -0,0 +1,15 @@ +package de.timroes.axmlrpc; + +/** + * Will be thrown when a call to the server times out. The timeout can be + * set via {@link XMLRPCClient#setTimeout(int)}. + * + * @author Tim Roes + */ +public class XMLRPCTimeoutException extends XMLRPCException { + + XMLRPCTimeoutException(String ex) { + super(ex); + } + +} \ No newline at end of file diff --git a/lib/src/de/timroes/axmlrpc/XMLUtil.java b/lib/src/de/timroes/axmlrpc/XMLUtil.java new file mode 100644 index 00000000..fb3b2cc6 --- /dev/null +++ b/lib/src/de/timroes/axmlrpc/XMLUtil.java @@ -0,0 +1,124 @@ +package de.timroes.axmlrpc; + +import de.timroes.axmlrpc.xmlcreator.XmlElement; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +/** + * This class provides some utility methods for the use with the Java DOM parser. + * + * @author Tim Roes + */ +public class XMLUtil { + + /** + * Returns the only child element in a given NodeList. + * Will throw an error if there is more then one child element or any other + * child that is not an element or an empty text string (whitespace are normal). + * + * @param list A NodeList of children nodes. + * @return The only child element in the given node list. + * @throws XMLRPCException Will be thrown if there is more then one child element + * except empty text nodes. + */ + public static Element getOnlyChildElement(NodeList list) throws XMLRPCException { + + Element e = null; + Node n; + for(int i = 0; i < list.getLength(); i++) { + n = list.item(i); + // Strip only whitespace text elements and comments + if((n.getNodeType() == Node.TEXT_NODE + && n.getNodeValue().trim().length() <= 0) + || n.getNodeType() == Node.COMMENT_NODE) + continue; + + // Check if there is anything else than an element node. + if(n.getNodeType() != Node.ELEMENT_NODE) { + throw new XMLRPCException("Only element nodes allowed."); + } + + // If there was already an element, throw exception. + if(e != null) { + throw new XMLRPCException("Element has more than one children."); + } + + e = (Element)n; + + } + + return e; + + } + + /** + * Returns the text node from a given NodeList. If the list contains + * more then just text nodes, an exception will be thrown. + * + * @param list The given list of nodes. + * @return The text of the given node list. + * @throws XMLRPCException Will be thrown if there is more than just one + * text node within the list. + */ + public static String getOnlyTextContent(NodeList list) throws XMLRPCException { + + StringBuilder builder = new StringBuilder(); + Node n; + + for(int i = 0; i < list.getLength(); i++) { + n = list.item(i); + + // Skip comments inside text tag. + if(n.getNodeType() == Node.COMMENT_NODE) { + continue; + } + + if(n.getNodeType() != Node.TEXT_NODE) { + throw new XMLRPCException("Element must contain only text elements."); + } + + builder.append(n.getNodeValue()); + + } + + return builder.toString(); + + } + + /** + * Checks if the given {@link NodeList} contains a child element. + * + * @param list The {@link NodeList} to check. + * @return Whether the {@link NodeList} contains children. + */ + public static boolean hasChildElement(NodeList list) { + + Node n; + + for(int i = 0; i < list.getLength(); i++) { + n = list.item(i); + + if(n.getNodeType() == Node.ELEMENT_NODE) { + return true; + } + } + + return false; + + } + + /** + * Creates an xml tag with a given type and content. + * + * @param type The type of the xml tag. What will be filled in the <..>. + * @param content The content of the tag. + * @return The xml tag with its content as a string. + */ + public static XmlElement makeXmlTag(String type, String content) { + XmlElement xml = new XmlElement(type); + xml.setContent(content); + return xml; + } + +} \ No newline at end of file diff --git a/lib/src/de/timroes/axmlrpc/serializer/ArraySerializer.java b/lib/src/de/timroes/axmlrpc/serializer/ArraySerializer.java new file mode 100644 index 00000000..5850f806 --- /dev/null +++ b/lib/src/de/timroes/axmlrpc/serializer/ArraySerializer.java @@ -0,0 +1,78 @@ +package de.timroes.axmlrpc.serializer; + +import de.timroes.axmlrpc.XMLRPCException; +import de.timroes.axmlrpc.XMLRPCRuntimeException; +import de.timroes.axmlrpc.XMLUtil; +import de.timroes.axmlrpc.xmlcreator.XmlElement; +import java.util.ArrayList; +import java.util.List; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +/** + * + * @author Tim Roes + */ +public class ArraySerializer implements Serializer { + + private static final String ARRAY_DATA = "data"; + private static final String ARRAY_VALUE = "value"; + + public Object deserialize(Element content) throws XMLRPCException { + + List list = new ArrayList(); + + Element data = XMLUtil.getOnlyChildElement(content.getChildNodes()); + + if(!ARRAY_DATA.equals(data.getNodeName())) { + throw new XMLRPCException("The array must contain one data tag."); + } + + // Deserialize every array element + Node value; + for(int i = 0; i < data.getChildNodes().getLength(); i++) { + + value = data.getChildNodes().item(i); + + // Strip only whitespace text elements and comments + if(value == null || (value.getNodeType() == Node.TEXT_NODE + && value.getNodeValue().trim().length() <= 0) + || value.getNodeType() == Node.COMMENT_NODE) + continue; + + if(value.getNodeType() != Node.ELEMENT_NODE) { + throw new XMLRPCException("Wrong element inside of array."); + } + + list.add(SerializerHandler.getDefault().deserialize((Element)value)); + + } + + return list.toArray(); + } + + public XmlElement serialize(Object object) { + + Iterable iter = (Iterable)object; + XmlElement array = new XmlElement(SerializerHandler.TYPE_ARRAY); + XmlElement data = new XmlElement(ARRAY_DATA); + array.addChildren(data); + + try { + + XmlElement e; + for(Object obj : iter) { + e = new XmlElement(ARRAY_VALUE); + e.addChildren(SerializerHandler.getDefault().serialize(obj)); + data.addChildren(e); + } + + } catch(XMLRPCException ex) { + throw new XMLRPCRuntimeException(ex); + } + + return array; + + } + +} \ No newline at end of file diff --git a/lib/src/de/timroes/axmlrpc/serializer/Base64Serializer.java b/lib/src/de/timroes/axmlrpc/serializer/Base64Serializer.java new file mode 100644 index 00000000..d8e468e9 --- /dev/null +++ b/lib/src/de/timroes/axmlrpc/serializer/Base64Serializer.java @@ -0,0 +1,24 @@ +package de.timroes.axmlrpc.serializer; + +import de.timroes.axmlrpc.XMLRPCException; +import de.timroes.axmlrpc.XMLUtil; +import de.timroes.axmlrpc.xmlcreator.XmlElement; +import de.timroes.base64.Base64; +import org.w3c.dom.Element; + +/** + * + * @author Tim Roes + */ +public class Base64Serializer implements Serializer { + + public Object deserialize(Element content) throws XMLRPCException { + return Base64.decode(XMLUtil.getOnlyTextContent(content.getChildNodes())); + } + + public XmlElement serialize(Object object) { + return XMLUtil.makeXmlTag(SerializerHandler.TYPE_BASE64, + Base64.encode((Byte[])object)); + } + +} \ No newline at end of file diff --git a/lib/src/de/timroes/axmlrpc/serializer/BooleanSerializer.java b/lib/src/de/timroes/axmlrpc/serializer/BooleanSerializer.java new file mode 100644 index 00000000..4267b092 --- /dev/null +++ b/lib/src/de/timroes/axmlrpc/serializer/BooleanSerializer.java @@ -0,0 +1,24 @@ +package de.timroes.axmlrpc.serializer; + +import de.timroes.axmlrpc.XMLRPCException; +import de.timroes.axmlrpc.XMLUtil; +import de.timroes.axmlrpc.xmlcreator.XmlElement; +import org.w3c.dom.Element; + +/** + * + * @author Tim Roes + */ +public class BooleanSerializer implements Serializer { + + public Object deserialize(Element content) throws XMLRPCException { + return (XMLUtil.getOnlyTextContent(content.getChildNodes()).equals("1")) + ? Boolean.TRUE : Boolean.FALSE; + } + + public XmlElement serialize(Object object) { + return XMLUtil.makeXmlTag(SerializerHandler.TYPE_BOOLEAN, + ((Boolean)object == true) ? "1" : "0"); + } + +} \ No newline at end of file diff --git a/lib/src/de/timroes/axmlrpc/serializer/DateTimeSerializer.java b/lib/src/de/timroes/axmlrpc/serializer/DateTimeSerializer.java new file mode 100644 index 00000000..a95219a5 --- /dev/null +++ b/lib/src/de/timroes/axmlrpc/serializer/DateTimeSerializer.java @@ -0,0 +1,32 @@ +package de.timroes.axmlrpc.serializer; + +import de.timroes.axmlrpc.XMLRPCException; +import de.timroes.axmlrpc.XMLUtil; +import de.timroes.axmlrpc.xmlcreator.XmlElement; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import org.w3c.dom.Element; + +/** + * + * @author timroes + */ +public class DateTimeSerializer implements Serializer { + + private static final String DATETIME_FORMAT = "yyyyMMdd'T'HH:mm:ss"; + private static final SimpleDateFormat DATE_FORMATER = new SimpleDateFormat(DATETIME_FORMAT); + + public Object deserialize(Element content) throws XMLRPCException { + try { + return DATE_FORMATER.parse(XMLUtil.getOnlyTextContent(content.getChildNodes())); + } catch (ParseException ex) { + throw new XMLRPCException("Unable to parse given date.", ex); + } + } + + public XmlElement serialize(Object object) { + return XMLUtil.makeXmlTag(SerializerHandler.TYPE_DATETIME, + DATE_FORMATER.format(object)); + } + +} diff --git a/lib/src/de/timroes/axmlrpc/serializer/DoubleSerializer.java b/lib/src/de/timroes/axmlrpc/serializer/DoubleSerializer.java new file mode 100644 index 00000000..361ca2d1 --- /dev/null +++ b/lib/src/de/timroes/axmlrpc/serializer/DoubleSerializer.java @@ -0,0 +1,27 @@ +package de.timroes.axmlrpc.serializer; + +import de.timroes.axmlrpc.XMLRPCException; +import de.timroes.axmlrpc.XMLUtil; +import de.timroes.axmlrpc.xmlcreator.XmlElement; +import java.math.BigDecimal; +import org.w3c.dom.Element; + +/** + * This serializer is responsible for floating point numbers. + * + * @author Tim Roes + */ +public class DoubleSerializer implements Serializer { + + public Object deserialize(Element content) throws XMLRPCException { + return Double.valueOf(XMLUtil.getOnlyTextContent(content.getChildNodes())); + } + + public XmlElement serialize(Object object) { + // Turn double value of object into a BigDecimal to get the + // right decimal point format. + BigDecimal bd = BigDecimal.valueOf(((Number)object).doubleValue()); + return XMLUtil.makeXmlTag(SerializerHandler.TYPE_DOUBLE, bd.toPlainString()); + } + +} diff --git a/lib/src/de/timroes/axmlrpc/serializer/IntSerializer.java b/lib/src/de/timroes/axmlrpc/serializer/IntSerializer.java new file mode 100644 index 00000000..5d7b23db --- /dev/null +++ b/lib/src/de/timroes/axmlrpc/serializer/IntSerializer.java @@ -0,0 +1,23 @@ +package de.timroes.axmlrpc.serializer; + +import de.timroes.axmlrpc.XMLRPCException; +import de.timroes.axmlrpc.XMLUtil; +import de.timroes.axmlrpc.xmlcreator.XmlElement; +import org.w3c.dom.Element; + +/** + * + * @author timroes + */ +public class IntSerializer implements Serializer { + + public Object deserialize(Element content) throws XMLRPCException { + return Integer.parseInt(XMLUtil.getOnlyTextContent(content.getChildNodes())); + } + + public XmlElement serialize(Object object) { + return XMLUtil.makeXmlTag(SerializerHandler.TYPE_INT, + object.toString()); + } + +} diff --git a/lib/src/de/timroes/axmlrpc/serializer/LongSerializer.java b/lib/src/de/timroes/axmlrpc/serializer/LongSerializer.java new file mode 100644 index 00000000..15ba9f65 --- /dev/null +++ b/lib/src/de/timroes/axmlrpc/serializer/LongSerializer.java @@ -0,0 +1,23 @@ +package de.timroes.axmlrpc.serializer; + +import de.timroes.axmlrpc.XMLRPCException; +import de.timroes.axmlrpc.XMLUtil; +import de.timroes.axmlrpc.xmlcreator.XmlElement; +import org.w3c.dom.Element; + +/** + * + * @author Tim Roes + */ +class LongSerializer implements Serializer { + + public Object deserialize(Element content) throws XMLRPCException { + return Long.parseLong(XMLUtil.getOnlyTextContent(content.getChildNodes())); + } + + public XmlElement serialize(Object object) { + return XMLUtil.makeXmlTag(SerializerHandler.TYPE_LONG, + ((Long)object).toString()); + } + +} diff --git a/lib/src/de/timroes/axmlrpc/serializer/NullSerializer.java b/lib/src/de/timroes/axmlrpc/serializer/NullSerializer.java new file mode 100644 index 00000000..e1619b61 --- /dev/null +++ b/lib/src/de/timroes/axmlrpc/serializer/NullSerializer.java @@ -0,0 +1,21 @@ +package de.timroes.axmlrpc.serializer; + +import de.timroes.axmlrpc.XMLRPCException; +import de.timroes.axmlrpc.xmlcreator.XmlElement; +import org.w3c.dom.Element; + +/** + * + * @author Tim Roes + */ +public class NullSerializer implements Serializer { + + public Object deserialize(Element content) throws XMLRPCException { + return null; + } + + public XmlElement serialize(Object object) { + return new XmlElement(SerializerHandler.TYPE_NULL); + } + +} \ No newline at end of file diff --git a/lib/src/de/timroes/axmlrpc/serializer/Serializer.java b/lib/src/de/timroes/axmlrpc/serializer/Serializer.java new file mode 100644 index 00000000..c139ebce --- /dev/null +++ b/lib/src/de/timroes/axmlrpc/serializer/Serializer.java @@ -0,0 +1,34 @@ +package de.timroes.axmlrpc.serializer; + +import de.timroes.axmlrpc.XMLRPCException; +import de.timroes.axmlrpc.xmlcreator.XmlElement; +import org.w3c.dom.Element; + +/** + * A Serializer is responsible to serialize a specific type of data to + * an xml tag and deserialize the content of this xml tag back to an object. + * + * @author Tim Roes + */ +public interface Serializer { + + /** + * This method takes an xml type element and deserialize it to an object. + * + * @param content Must be an xml element of a specific type. + * @return The deserialized content. + * @throws XMLRPCException Will be thrown whenervt the deserialization fails. + */ + public Object deserialize(Element content) throws XMLRPCException; + + /** + * This method takes an object and returns a representation as a string + * containing the right xml type tag. The returning string must be useable + * within a value tag. + * + * @param object The object that should be serialized. + * @return An XmlElement representation of the object. + */ + public XmlElement serialize(Object object); + +} \ No newline at end of file diff --git a/lib/src/de/timroes/axmlrpc/serializer/SerializerHandler.java b/lib/src/de/timroes/axmlrpc/serializer/SerializerHandler.java new file mode 100644 index 00000000..02f25d40 --- /dev/null +++ b/lib/src/de/timroes/axmlrpc/serializer/SerializerHandler.java @@ -0,0 +1,228 @@ +package de.timroes.axmlrpc.serializer; + +import de.timroes.axmlrpc.XMLRPCClient; +import de.timroes.axmlrpc.XMLRPCException; +import de.timroes.axmlrpc.XMLRPCRuntimeException; +import de.timroes.axmlrpc.XMLUtil; +import de.timroes.axmlrpc.xmlcreator.XmlElement; +import java.math.BigDecimal; +import java.util.Calendar; +import java.util.Date; +import java.util.Map; +import org.w3c.dom.Element; + +/** + * The serializer handler serializes and deserialized objects. + * It takes an object, determine its type and let the responsible handler serialize it. + * For deserialization it looks at the xml tag around the element. + * The class is designed as a kind of singleton, so it can be accessed from anywhere in + * the library. + * + * @author Tim Roes + */ +public class SerializerHandler { + + public static final String TYPE_STRING = "string"; + public static final String TYPE_BOOLEAN = "boolean"; + public static final String TYPE_INT = "int"; + public static final String TYPE_INT2 = "i4"; + public static final String TYPE_LONG = "i8"; + public static final String TYPE_DOUBLE = "double"; + public static final String TYPE_DATETIME = "dateTime.iso8601"; + public static final String TYPE_STRUCT = "struct"; + public static final String TYPE_ARRAY = "array"; + public static final String TYPE_BASE64 = "base64"; + public static final String TYPE_NULL = "nil"; + + private static SerializerHandler instance; + + /** + * Initialize the serialization handler. This method must be called before + * the get method returns any object. + * + * @param flags The flags that has been set in the XMLRPCClient. + * @see XMLRPCClient + */ + public static void initialize(int flags) { + instance = new SerializerHandler(flags); + } + + /** + * Return the instance of the SerializerHandler. + * It must have been initialized with initialize() before. + * + * @return The instance of the SerializerHandler. + */ + public static SerializerHandler getDefault() { + if(instance == null) { + throw new XMLRPCRuntimeException("The SerializerHandler has not been initialized."); + } + return instance; + } + + private StringSerializer string; + private BooleanSerializer bool = new BooleanSerializer(); + private IntSerializer integer = new IntSerializer(); + private LongSerializer long8 = new LongSerializer(); + private StructSerializer struct = new StructSerializer(); + private DoubleSerializer floating = new DoubleSerializer(); + private DateTimeSerializer datetime = new DateTimeSerializer(); + private ArraySerializer array = new ArraySerializer(); + private Base64Serializer base64 = new Base64Serializer(); + private NullSerializer nil = new NullSerializer(); + + private int flags; + + /** + * Generates the SerializerHandler. + * This method can only called from within the class (the initialize method). + * + * @param flags The flags to use. + */ + private SerializerHandler(int flags) { + this.flags = flags; + string = new StringSerializer( + (flags & XMLRPCClient.FLAGS_NO_STRING_ENCODE) == 0, + (flags & XMLRPCClient.FLAGS_NO_STRING_DECODE) == 0 + ); + } + + /** + * Deserializes an incoming xml element to an java object. + * The xml element must be the value element around the type element. + * The type of the returning object depends on the type tag. + * + * @param element An type element from within a value tag. + * @return The deserialized object. + * @throws XMLRPCException Will be thrown whenever an error occurs. + */ + public Object deserialize(Element element) throws XMLRPCException { + + if(!XMLRPCClient.VALUE.equals(element.getNodeName())) { + throw new XMLRPCException("Value tag is missing around value."); + } + + if(!XMLUtil.hasChildElement(element.getChildNodes())) { + // Value element doesn't contain a child element + if((flags & XMLRPCClient.FLAGS_DEFAULT_TYPE_STRING) != 0) { + return string.deserialize(element); + } else { + throw new XMLRPCException("Missing type element inside of value element."); + } + } + + // Grep type element from inside value element + element = XMLUtil.getOnlyChildElement(element.getChildNodes()); + + Serializer s = null; + + String type; + + // If FLAGS_IGNORE_NAMESPACE has been set, only use local name. + if((flags & XMLRPCClient.FLAGS_IGNORE_NAMESPACES) != 0) { + type = element.getLocalName() == null ? element.getNodeName() : element.getLocalName(); + } else { + type = element.getNodeName(); + } + + if((flags & XMLRPCClient.FLAGS_NIL) != 0 && TYPE_NULL.equals(type)) { + s = nil; + } else if(TYPE_STRING.equals(type)) { + s = string; + } else if(TYPE_BOOLEAN.equals(type)) { + s = bool; + } else if(TYPE_DOUBLE.equals(type)) { + s = floating; + } else if (TYPE_INT.equals(type) || TYPE_INT2.equals(type)) { + s = integer; + } else if(TYPE_DATETIME.equals(type)) { + s = datetime; + } else if (TYPE_LONG.equals(type)) { + if((flags & XMLRPCClient.FLAGS_8BYTE_INT) != 0) { + s = long8; + } else { + throw new XMLRPCException("8 byte integer is not in the specification. " + + "You must use FLAGS_8BYTE_INT to enable the i8 tag."); + } + } else if(TYPE_STRUCT.equals(type)) { + s = struct; + } else if(TYPE_ARRAY.equals(type)) { + s = array; + } else if(TYPE_BASE64.equals(type)) { + s = base64; + } else { + throw new XMLRPCException("No deserializer found for type '" + type + "'."); + } + + return s.deserialize(element); + + } + + /** + * Serialize an object to its representation as an xml element. + * The xml element will be the type element for the use within a value tag. + * + * @param object The object that should be serialized. + * @return The xml representation of this object. + * @throws XMLRPCException Will be thrown, if an error occurs (e.g. the object + * cannot be serialized to an xml element. + */ + public XmlElement serialize(Object object) throws XMLRPCException { + + Serializer s = null; + + if((flags & XMLRPCClient.FLAGS_NIL) != 0 && object == null) { + s = nil; + } else if(object instanceof String) { + s = string; + } else if(object instanceof Boolean) { + s = bool; + } else if(object instanceof Double || object instanceof Float + || object instanceof BigDecimal) { + s = floating; + } else if (object instanceof Integer || object instanceof Short + || object instanceof Byte) { + s = integer; + } else if(object instanceof Long) { + // Check whether the 8 byte integer flag was set. + if((flags & XMLRPCClient.FLAGS_8BYTE_INT) != 0) { + s = long8; + } else { + // Allow long values as long as their fit within the 4 byte integer range. + long l = (Long)object; + if(l > Integer.MAX_VALUE || l < Integer.MIN_VALUE) { + throw new XMLRPCException("FLAGS_8BYTE_INT must be set, if values " + + "outside the 4 byte integer range should be transfered."); + } else { + s = integer; + } + } + } else if(object instanceof Date) { + s = datetime; + } else if(object instanceof Calendar) { + object = ((Calendar)object).getTime(); + s = datetime; + } else if (object instanceof Map) { + s = struct; + } else if(object instanceof byte[]) { + byte[] old = (byte[])object; + Byte[] boxed = new Byte[old.length]; + for(int i = 0; i < boxed.length; i++) { + boxed[i] = new Byte(old[i]); + } + object = boxed; + s = base64; + } else if(object instanceof Byte[]) { + s = base64; + } else if(object instanceof Iterable) { + s = array; + } else { + throw new XMLRPCException("No serializer found for type '" + + object.getClass().getName() + "'."); + } + + return s.serialize(object); + + } + +} \ No newline at end of file diff --git a/lib/src/de/timroes/axmlrpc/serializer/StringSerializer.java b/lib/src/de/timroes/axmlrpc/serializer/StringSerializer.java new file mode 100644 index 00000000..1ef1ee9e --- /dev/null +++ b/lib/src/de/timroes/axmlrpc/serializer/StringSerializer.java @@ -0,0 +1,38 @@ +package de.timroes.axmlrpc.serializer; + +import de.timroes.axmlrpc.XMLRPCException; +import de.timroes.axmlrpc.XMLUtil; +import de.timroes.axmlrpc.xmlcreator.XmlElement; +import org.w3c.dom.Element; + +/** + * + * @author Tim Roes + */ +public class StringSerializer implements Serializer { + + private boolean decodeStrings; + private boolean encodeStrings; + + public StringSerializer(boolean encodeStrings, boolean decodeStrings) { + this.decodeStrings = decodeStrings; + this.encodeStrings = encodeStrings; + } + + public Object deserialize(Element content) throws XMLRPCException { + String text = XMLUtil.getOnlyTextContent(content.getChildNodes()); + if(decodeStrings) { + text = text.replaceAll("<", "<").replaceAll("&", "&"); + } + return text; + } + + public XmlElement serialize(Object object) { + String content = object.toString(); + if(encodeStrings) { + content = content.replaceAll("&", "&").replaceAll("<", "<"); + } + return XMLUtil.makeXmlTag(SerializerHandler.TYPE_STRING, content); + } + +} \ No newline at end of file diff --git a/lib/src/de/timroes/axmlrpc/serializer/StructSerializer.java b/lib/src/de/timroes/axmlrpc/serializer/StructSerializer.java new file mode 100644 index 00000000..87fa9615 --- /dev/null +++ b/lib/src/de/timroes/axmlrpc/serializer/StructSerializer.java @@ -0,0 +1,112 @@ +package de.timroes.axmlrpc.serializer; + +import de.timroes.axmlrpc.XMLRPCException; +import de.timroes.axmlrpc.XMLRPCRuntimeException; +import de.timroes.axmlrpc.XMLUtil; +import de.timroes.axmlrpc.xmlcreator.XmlElement; +import java.util.HashMap; +import java.util.Map; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +/** + * + * @author Tim Roes + */ +public class StructSerializer implements Serializer { + + private static final String STRUCT_MEMBER = "member"; + private static final String STRUCT_NAME = "name"; + private static final String STRUCT_VALUE = "value"; + + public Object deserialize(Element content) throws XMLRPCException { + + Map map = new HashMap(); + + Node n, m; + String s; + Object o; + for(int i = 0; i < content.getChildNodes().getLength(); i++) { + + n = content.getChildNodes().item(i); + + // Strip only whitespace text elements and comments + if((n.getNodeType() == Node.TEXT_NODE + && n.getNodeValue().trim().length() <= 0) + || n.getNodeType() == Node.COMMENT_NODE) + continue; + + if(n.getNodeType() != Node.ELEMENT_NODE + || !STRUCT_MEMBER.equals(n.getNodeName())) { + throw new XMLRPCException("Only struct members allowed within a struct."); + } + + // Grep name and value from member + s = null; o = null; + for(int j = 0; j < n.getChildNodes().getLength(); j++) { + m = n.getChildNodes().item(j); + + // Strip only whitespace text elements and comments + if((m.getNodeType() == Node.TEXT_NODE + && m.getNodeValue().trim().length() <= 0) + || m.getNodeType() == Node.COMMENT_NODE) + continue; + + if(STRUCT_NAME.equals(m.getNodeName())) { + if(s != null) { + throw new XMLRPCException("Name of a struct member cannot be set twice."); + } else { + s = XMLUtil.getOnlyTextContent(m.getChildNodes()); + } + } else if(m.getNodeType() == Node.ELEMENT_NODE && STRUCT_VALUE.equals(m.getNodeName())) { + if(o != null) { + throw new XMLRPCException("Value of a struct member cannot be set twice."); + } else { + o = SerializerHandler.getDefault().deserialize((Element)m); + } + } else { + throw new XMLRPCException("A struct member must only contain one name and one value."); + } + + } + + map.put(s, o); + + } + + return map; + + } + + public XmlElement serialize(Object object) { + + XmlElement struct = new XmlElement(SerializerHandler.TYPE_STRUCT); + + try { + + XmlElement entry, name, value; + + // We can safely cast here, this Serializer should only be called when + // the parameter is a map. + @SuppressWarnings("unchecked") + Map map = (Map)object; + + for(Map.Entry member : map.entrySet()) { + entry = new XmlElement(STRUCT_MEMBER); + name = new XmlElement(STRUCT_NAME); + value = new XmlElement(STRUCT_VALUE); + name.setContent(member.getKey()); + value.addChildren(SerializerHandler.getDefault().serialize(member.getValue())); + entry.addChildren(name); + entry.addChildren(value); + struct.addChildren(entry); + } + + } catch(XMLRPCException ex) { + throw new XMLRPCRuntimeException(ex); + } + + return struct; + } + +} \ No newline at end of file diff --git a/lib/src/de/timroes/axmlrpc/xmlcreator/SimpleXMLCreator.java b/lib/src/de/timroes/axmlrpc/xmlcreator/SimpleXMLCreator.java new file mode 100644 index 00000000..d43349ef --- /dev/null +++ b/lib/src/de/timroes/axmlrpc/xmlcreator/SimpleXMLCreator.java @@ -0,0 +1,31 @@ +package de.timroes.axmlrpc.xmlcreator; + +/** + * This is a very simple xml creator. It allows creating an xml document + * containing multiple xml tags. No attributes are supported. + * + * @author Tim Roes + */ +public class SimpleXMLCreator { + + private XmlElement root; + + /** + * Set the root element of the xml tree. + * + * @param element The element to use as root element in this tree. + */ + public void setRootElement(XmlElement element) { + this.root = element; + } + + /** + * Return the string representation of the xml tree. + * @return String representation of the xml tree. + */ + @Override + public String toString() { + return "\n" + root.toString(); + } + +} diff --git a/lib/src/de/timroes/axmlrpc/xmlcreator/XmlElement.java b/lib/src/de/timroes/axmlrpc/xmlcreator/XmlElement.java new file mode 100644 index 00000000..7af25e4c --- /dev/null +++ b/lib/src/de/timroes/axmlrpc/xmlcreator/XmlElement.java @@ -0,0 +1,73 @@ +package de.timroes.axmlrpc.xmlcreator; + +import java.util.ArrayList; +import java.util.List; + +/** + * An xml element within an xml tree. + * In this case an xml element can have a text content OR a multiple amount + * of children. The xml element itself has a name. + * + * @author Tim Roes + */ +public class XmlElement { + + private List children = new ArrayList(); + private String name; + private String content; + + /** + * Create a new xml element with the given name. + * + * @param name The name of the xml element. + */ + public XmlElement(String name) { + this.name = name; + } + + /** + * Add a child to this xml element. + * + * @param element The child to add. + */ + public void addChildren(XmlElement element) { + children.add(element); + } + + /** + * Set the content of this xml tag. If the content is set the children + * won't be used in a string representation. + * + * @param content Content of the xml element. + */ + public void setContent(String content) { + this.content = content; + } + + /** + * Return a string representation of this xml element. + * + * @return String representation of xml element. + */ + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + if(content != null && content.length() > 0) { + builder.append("\n<").append(name).append(">") + .append(content) + .append("\n"); + return builder.toString(); + } else if(children.size() > 0) { + builder.append("\n<").append(name).append(">"); + for(XmlElement x : children) { + builder.append(x.toString()); + } + builder.append("\n"); + return builder.toString(); + } else { + builder.append("\n<").append(name).append("/>\n"); + return builder.toString(); + } + } + +} diff --git a/lib/src/de/timroes/base64/Base64.java b/lib/src/de/timroes/base64/Base64.java new file mode 100644 index 00000000..24612791 --- /dev/null +++ b/lib/src/de/timroes/base64/Base64.java @@ -0,0 +1,161 @@ +package de.timroes.base64; + +import java.util.HashMap; + +/** + * A Base64 en/decoder. You can use it to encode and decode strings and byte arrays. + * + * @author Tim Roes + */ +public class Base64 { + + private static final char[] code = ("=ABCDEFGHIJKLMNOPQRSTUVWXYZ" + + "abcdefghijklmnopqrstuvwxyz0123456789+/").toCharArray(); + + private static final HashMap map = new HashMap(); + + static { + for(int i = 0; i < code.length; i++) { + map.put(code[i], (byte)i); + } + } + + /** + * Decode a base64 encoded string to a byte array. + * + * @param in A string representing a base64 encoding. + * @return The decoded byte array. + */ + public static byte[] decode(String in) { + + in = in.replaceAll("\\r|\\n",""); + if(in.length() % 4 != 0) { + throw new IllegalArgumentException("The length of the input string must be a multiple of four."); + } + + if(!in.matches("^[A-Za-z0-9+/]*[=]{0,3}$")) { + throw new IllegalArgumentException("The argument contains illegal characters."); + } + + byte[] out = new byte[(in.length()*3)/4]; + + char[] input = in.toCharArray(); + + int outi = 0; + int b1, b2, b3, b4; + for(int i = 0; i < input.length; i+=4) { + b1 = (map.get(input[i]) - 1); + b2 = (map.get(input[i+1]) - 1); + b3 = (map.get(input[i+2]) - 1); + b4 = (map.get(input[i+3]) - 1); + out[outi++] = (byte)(b1 << 2 | b2 >>> 4); + out[outi++] = (byte)((b2 & 0x0F) << 4 | b3 >>> 2); + out[outi++] = (byte)((b3 & 0x03) << 6 | (b4 & 0x3F)); + } + + if(in.endsWith("=")) { + byte[] trimmed = new byte[out.length - (in.length() - in.indexOf("="))]; + System.arraycopy(out, 0, trimmed, 0, trimmed.length); + return trimmed; + } + + return out; + + } + + /** + * Decode a base64 encoded string to a string. + * + * @param in The string representation of the base64 encoding. + * @return The decoded string in the default charset of the JVM. + */ + public static String decodeAsString(String in) { + return new String(decode(in)); + } + + /** + * Encode a String and return the encoded string. + * + * @param in A string to encode. + * @return The encoded byte array. + */ + public static String encode(String in) { + return encode(in.getBytes()); + } + + /** + * Encode a Byte array and return the encoded string. + * + * @param in A string to encode. + * @return The encoded byte array. + */ + public static String encode(Byte[] in) { + byte[] tmp = new byte[in.length]; + for(int i = 0; i < tmp.length; i++) { + tmp[i] = in[i]; + } + return encode(tmp); + } + + /** + * Encode a byte array and return the encoded string. + * + * @param in A string to encode. + * @return The encoded byte array. + */ + public static String encode(byte[] in) { + StringBuilder builder = new StringBuilder(4 * ((in.length+2)/3)); + byte[] encoded = encodeAsBytes(in); + for(int i = 0; i < encoded.length; i++) { + builder.append(code[encoded[i]+1]); + if(i % 72 == 71) + builder.append("\n"); + } + return builder.toString(); + } + + /** + * Encode a String and return the encoded byte array. Bytes that has been + * appended to pad the string to a multiple of four are set to -1 in the array. + * + * @param in A string to encode. + * @return The encoded byte array. + */ + public static byte[] encodeAsBytes(String in) { + return encodeAsBytes(in.getBytes()); + } + + /** + * Encode a byte array and return the encoded byte array. Bytes that has been + * appended to pad the string to a multiple of four are set to -1 in the array. + * + * @param inArray A string to encode. + * @return The encoded byte array. + */ + public static byte[] encodeAsBytes(byte[] inArray) { + + // Output string must be 4 * floor((n+2)/3) large + byte[] out = new byte[4 * ((inArray.length+2)/3)]; + // Create padded input array with the next largest length that is a multiple of 3 + byte[] in = new byte[(inArray.length+2)/3*3]; + // Copy content form unpadded to padded array + System.arraycopy(inArray, 0, in, 0, inArray.length); + + int outi = 0; + for(int i = 0; i < in.length; i+=3) { + + out[outi++] = (byte)((in[i] & 0xFF) >>> 2); + out[outi++] = (byte)(((in[i] & 0x03) << 4) | ((in[i+1] & 0xFF) >>> 4)); + out[outi++] = (byte)(((in[i+1] & 0x0F) << 2) | ((in[i+2] & 0xFF) >>> 6)); + out[outi++] = (byte)(in[i+2] & 0x3F); + + } + + for(int i = in.length - inArray.length; i > 0; i--) { + out[out.length - i] = -1; + } + + return out; + } + +} \ No newline at end of file diff --git a/lib/src/org/transdroid/daemon/Rtorrent/RtorrentAdapter.java b/lib/src/org/transdroid/daemon/Rtorrent/RtorrentAdapter.java index 21b4760d..574e8eca 100644 --- a/lib/src/org/transdroid/daemon/Rtorrent/RtorrentAdapter.java +++ b/lib/src/org/transdroid/daemon/Rtorrent/RtorrentAdapter.java @@ -23,7 +23,9 @@ import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.UnsupportedEncodingException; +import java.net.MalformedURLException; import java.net.URI; +import java.net.URL; import java.net.URLDecoder; import java.util.ArrayList; import java.util.Date; @@ -35,6 +37,7 @@ import java.util.Map.Entry; import org.base64.android.Base64; import org.transdroid.daemon.Daemon; import org.transdroid.daemon.DaemonException; +import org.transdroid.daemon.DaemonException.ExceptionType; import org.transdroid.daemon.DaemonSettings; import org.transdroid.daemon.IDaemonAdapter; import org.transdroid.daemon.Label; @@ -43,7 +46,6 @@ import org.transdroid.daemon.Torrent; import org.transdroid.daemon.TorrentDetails; import org.transdroid.daemon.TorrentFile; import org.transdroid.daemon.TorrentStatus; -import org.transdroid.daemon.DaemonException.ExceptionType; import org.transdroid.daemon.task.AddByFileTask; import org.transdroid.daemon.task.AddByMagnetUrlTask; import org.transdroid.daemon.task.AddByUrlTask; @@ -62,13 +64,14 @@ import org.transdroid.daemon.task.SetFilePriorityTask; import org.transdroid.daemon.task.SetLabelTask; import org.transdroid.daemon.task.SetTransferRatesTask; import org.transdroid.daemon.util.DLog; -import org.transdroid.daemon.util.HttpHelper; -import org.xmlrpc.android.XMLRPCClient; -import org.xmlrpc.android.XMLRPCException; +import org.transdroid.daemon.util.FakeTrustManager; + +import de.timroes.axmlrpc.XMLRPCClient; +import de.timroes.axmlrpc.XMLRPCException; /** * An adapter that allows for easy access to rTorrent torrent data. Communication - * is handled via the XML-RPC protocol. + * is handled via the XML-RPC protocol as implemented by the aXMLRPC library. * * @author erickok * @@ -234,7 +237,7 @@ public class RtorrentAdapter implements IDaemonAdapter { } } - private Object makeRtorrentCall(String serverMethod, Object[] arguments) throws DaemonException { + private Object makeRtorrentCall(String serverMethod, Object[] arguments) throws DaemonException, MalformedURLException { // Initialise the HTTP client if (rpcclient == null) { @@ -258,10 +261,19 @@ public class RtorrentAdapter implements IDaemonAdapter { /** * Instantiates a XML-RPC client with proper credentials. * @throws DaemonException On conflicting settings (i.e. user authentication but no password or username provided) + * @throws MalformedURLException Thrown when the URL could not be properly constructed */ - private void initialise() throws DaemonException { - - this.rpcclient = new XMLRPCClient(HttpHelper.createStandardHttpClient(settings, true), buildWebUIUrl().trim()); + private void initialise() throws DaemonException, MalformedURLException { + + //this.rpcclient = new XMLRPCClient(HttpHelper.createStandardHttpClient(settings, true), buildWebUIUrl().trim()); + int flags = XMLRPCClient.FLAGS_8BYTE_INT | XMLRPCClient.FLAGS_ENABLE_COOKIES; + if (settings.getSsl() && settings.getSslTrustAll()) + flags = XMLRPCClient.FLAGS_8BYTE_INT | XMLRPCClient.FLAGS_ENABLE_COOKIES | XMLRPCClient.FLAGS_SSL_IGNORE_INVALID_CERT; + this.rpcclient = new XMLRPCClient(new URL(buildWebUIUrl().trim()), flags); + if (settings.getSsl() && settings.getSslTrustKey() != null && !settings.getSslTrustKey().isEmpty()) + this.rpcclient.installCustomTrustManager(new FakeTrustManager(settings.getSslTrustKey())); + this.rpcclient.setTimeout(settings.getTimeoutInMilliseconds() / 1000); + this.rpcclient.setLoginData(settings.getUsername(), settings.getPassword()); } diff --git a/lib/src/org/transdroid/daemon/util/FakeTrustManager.java b/lib/src/org/transdroid/daemon/util/FakeTrustManager.java index bbb7bffc..6b82eade 100644 --- a/lib/src/org/transdroid/daemon/util/FakeTrustManager.java +++ b/lib/src/org/transdroid/daemon/util/FakeTrustManager.java @@ -13,7 +13,7 @@ public class FakeTrustManager implements X509TrustManager { private static final X509Certificate[] _AcceptedIssuers = new X509Certificate[] {}; private static final String LOG_NAME = "TrustManager"; - FakeTrustManager(String certKey){ + public FakeTrustManager(String certKey){ super(); this.certKey = certKey; } diff --git a/lib/src/org/xmlrpc/android/Base64Coder.java b/lib/src/org/xmlrpc/android/Base64Coder.java deleted file mode 100644 index f9a48075..00000000 --- a/lib/src/org/xmlrpc/android/Base64Coder.java +++ /dev/null @@ -1,199 +0,0 @@ -package org.xmlrpc.android; - -/** - * A Base64 Encoder/Decoder. - * - *

- * This class is used to encode and decode data in Base64 format as described in - * RFC 1521. - * - *

- * This is "Open Source" software and released under the GNU/LGPL license.
- * It is provided "as is" without warranty of any kind.
- * Copyright 2003: Christian d'Heureuse, Inventec Informatik AG, Switzerland.
- * Home page: www.source-code.biz
- * - *

- * Version history:
- * 2003-07-22 Christian d'Heureuse (chdh): Module created.
- * 2005-08-11 chdh: Lincense changed from GPL to LGPL.
- * 2006-11-21 chdh:
- *   Method encode(String) renamed to encodeString(String).
- *   Method decode(String) renamed to decodeString(String).
- *   New method encode(byte[],int) added.
- *   New method decode(String) added.
- */ - -class Base64Coder { - - // Mapping table from 6-bit nibbles to Base64 characters. - private static char[] map1 = new char[64]; - static { - int i = 0; - for (char c = 'A'; c <= 'Z'; c++) { - map1[i++] = c; - } - for (char c = 'a'; c <= 'z'; c++) { - map1[i++] = c; - } - for (char c = '0'; c <= '9'; c++) { - map1[i++] = c; - } - map1[i++] = '+'; - map1[i++] = '/'; - } - - // Mapping table from Base64 characters to 6-bit nibbles. - private static byte[] map2 = new byte[128]; - static { - for (int i = 0; i < map2.length; i++) { - map2[i] = -1; - } - for (int i = 0; i < 64; i++) { - map2[map1[i]] = (byte) i; - } - } - - /** - * Encodes a string into Base64 format. No blanks or line breaks are - * inserted. - * - * @param s - * a String to be encoded. - * @return A String with the Base64 encoded data. - */ - static String encodeString(String s) { - return new String(encode(s.getBytes())); - } - - /** - * Encodes a byte array into Base64 format. No blanks or line breaks are - * inserted. - * - * @param in - * an array containing the data bytes to be encoded. - * @return A character array with the Base64 encoded data. - */ - static char[] encode(byte[] in) { - return encode(in, in.length); - } - - /** - * Encodes a byte array into Base64 format. No blanks or line breaks are - * inserted. - * - * @param in - * an array containing the data bytes to be encoded. - * @param iLen - * number of bytes to process in in. - * @return A character array with the Base64 encoded data. - */ - static char[] encode(byte[] in, int iLen) { - int oDataLen = (iLen * 4 + 2) / 3; // output length without padding - int oLen = ((iLen + 2) / 3) * 4; // output length including padding - char[] out = new char[oLen]; - int ip = 0; - int op = 0; - while (ip < iLen) { - int i0 = in[ip++] & 0xff; - int i1 = ip < iLen ? in[ip++] & 0xff : 0; - int i2 = ip < iLen ? in[ip++] & 0xff : 0; - int o0 = i0 >>> 2; - int o1 = ((i0 & 3) << 4) | (i1 >>> 4); - int o2 = ((i1 & 0xf) << 2) | (i2 >>> 6); - int o3 = i2 & 0x3F; - out[op++] = map1[o0]; - out[op++] = map1[o1]; - out[op] = op < oDataLen ? map1[o2] : '='; - op++; - out[op] = op < oDataLen ? map1[o3] : '='; - op++; - } - return out; - } - - /** - * Decodes a string from Base64 format. - * - * @param s - * a Base64 String to be decoded. - * @return A String containing the decoded data. - * @throws IllegalArgumentException - * if the input is not valid Base64 encoded data. - */ - static String decodeString(String s) { - return new String(decode(s)); - } - - /** - * Decodes a byte array from Base64 format. - * - * @param s - * a Base64 String to be decoded. - * @return An array containing the decoded data bytes. - * @throws IllegalArgumentException - * if the input is not valid Base64 encoded data. - */ - static byte[] decode(String s) { - return decode(s.toCharArray()); - } - - /** - * Decodes a byte array from Base64 format. No blanks or line breaks are - * allowed within the Base64 encoded data. - * - * @param in - * a character array containing the Base64 encoded data. - * @return An array containing the decoded data bytes. - * @throws IllegalArgumentException - * if the input is not valid Base64 encoded data. - */ - static byte[] decode(char[] in) { - int iLen = in.length; - if (iLen % 4 != 0) { - throw new IllegalArgumentException( - "Length of Base64 encoded input string is not a multiple of 4."); - } - while (iLen > 0 && in[iLen - 1] == '=') { - iLen--; - } - int oLen = (iLen * 3) / 4; - byte[] out = new byte[oLen]; - int ip = 0; - int op = 0; - while (ip < iLen) { - int i0 = in[ip++]; - int i1 = in[ip++]; - int i2 = ip < iLen ? in[ip++] : 'A'; - int i3 = ip < iLen ? in[ip++] : 'A'; - if (i0 > 127 || i1 > 127 || i2 > 127 || i3 > 127) { - throw new IllegalArgumentException( - "Illegal character in Base64 encoded data."); - } - int b0 = map2[i0]; - int b1 = map2[i1]; - int b2 = map2[i2]; - int b3 = map2[i3]; - if (b0 < 0 || b1 < 0 || b2 < 0 || b3 < 0) { - throw new IllegalArgumentException( - "Illegal character in Base64 encoded data."); - } - int o0 = (b0 << 2) | (b1 >>> 4); - int o1 = ((b1 & 0xf) << 4) | (b2 >>> 2); - int o2 = ((b2 & 3) << 6) | b3; - out[op++] = (byte) o0; - if (op < oLen) { - out[op++] = (byte) o1; - } - if (op < oLen) { - out[op++] = (byte) o2; - } - } - return out; - } - - // Dummy constructor. - private Base64Coder() { - } -} diff --git a/lib/src/org/xmlrpc/android/XMLRPCClient.java b/lib/src/org/xmlrpc/android/XMLRPCClient.java deleted file mode 100644 index 95a037d4..00000000 --- a/lib/src/org/xmlrpc/android/XMLRPCClient.java +++ /dev/null @@ -1,379 +0,0 @@ -package org.xmlrpc.android; - -import java.io.InputStreamReader; -import java.io.Reader; -import java.io.StringWriter; -import java.net.URI; -import java.util.Map; - -import org.apache.http.HttpEntity; -import org.apache.http.HttpResponse; -import org.apache.http.HttpStatus; -import org.apache.http.client.HttpClient; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.entity.StringEntity; -import org.apache.http.params.HttpParams; -import org.apache.http.params.HttpProtocolParams; -import org.transdroid.daemon.DaemonException; -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserFactory; -import org.xmlpull.v1.XmlSerializer; - -import android.util.Xml; - - -/** - * XMLRPCClient allows to call remote XMLRPC method. - * - *

- * The following table shows how XML-RPC types are mapped to java call parameters/response values. - *

- * - *

- * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - *
XML-RPC TypeCall ParametersCall Response
int, i4byte
Byte
short
Short
int
Integer
int
Integer
i8long
Long
long
Long
doublefloat
Float
double
Double
double
Double
stringStringString
booleanboolean
Boolean
boolean
Boolean
dateTime.iso8601java.util.Date
java.util.Calendar
java.util.Date
base64byte[]byte[]
arrayjava.util.List<Object>
Object[]
Object[]
structjava.util.Map<String, Object>java.util.Map<String, Object>
- *

- */ - -public class XMLRPCClient { - private static final String TAG_METHOD_CALL = "methodCall"; - private static final String TAG_METHOD_NAME = "methodName"; - private static final String TAG_METHOD_RESPONSE = "methodResponse"; - private static final String TAG_PARAMS = "params"; - private static final String TAG_PARAM = "param"; - private static final String TAG_FAULT = "fault"; - private static final String TAG_FAULT_CODE = "faultCode"; - private static final String TAG_FAULT_STRING = "faultString"; - - private HttpClient client; - private HttpPost postMethod; - private XmlSerializer serializer; - private HttpParams httpParams; - - /** - * XMLRPCClient constructor. Creates new instance based on server URI - * @param XMLRPC server URI - */ - public XMLRPCClient(HttpClient client, URI uri) { - postMethod = new HttpPost(uri); - postMethod.addHeader("Content-Type", "text/xml"); - - // WARNING - // I had to disable "Expect: 100-Continue" header since I had - // two second delay between sending http POST request and POST body - httpParams = postMethod.getParams(); - HttpProtocolParams.setUseExpectContinue(httpParams, false); - - this.client = client; - serializer = Xml.newSerializer(); - } - - /** - * Convenience constructor. Creates new instance based on server String address - * @param XMLRPC server address - */ - public XMLRPCClient(HttpClient client, String url) { - this(client, URI.create(url)); - } - - /** - * Call method with optional parameters. This is general method. - * If you want to call your method with 0-8 parameters, you can use more - * convenience call methods - * - * @param method name of method to call - * @param params parameters to pass to method (may be null if method has no parameters) - * @return deserialized method return value - * @throws XMLRPCException - */ - public Object call(String method, Object[] params) throws XMLRPCException { - return callXMLRPC(method, params); - } - - /** - * Convenience method call with no parameters - * - * @param method name of method to call - * @return deserialized method return value - * @throws XMLRPCException - */ - public Object call(String method) throws XMLRPCException { - return callXMLRPC(method, null); - } - - /** - * Convenience method call with one parameter - * - * @param method name of method to call - * @param p0 method's parameter - * @return deserialized method return value - * @throws XMLRPCException - */ - public Object call(String method, Object p0) throws XMLRPCException { - Object[] params = { - p0, - }; - return callXMLRPC(method, params); - } - - /** - * Convenience method call with two parameters - * - * @param method name of method to call - * @param p0 method's 1st parameter - * @param p1 method's 2nd parameter - * @return deserialized method return value - * @throws XMLRPCException - */ - public Object call(String method, Object p0, Object p1) throws XMLRPCException { - Object[] params = { - p0, p1, - }; - return callXMLRPC(method, params); - } - - /** - * Convenience method call with three parameters - * - * @param method name of method to call - * @param p0 method's 1st parameter - * @param p1 method's 2nd parameter - * @param p2 method's 3rd parameter - * @return deserialized method return value - * @throws XMLRPCException - */ - public Object call(String method, Object p0, Object p1, Object p2) throws XMLRPCException { - Object[] params = { - p0, p1, p2, - }; - return callXMLRPC(method, params); - } - - /** - * Convenience method call with four parameters - * - * @param method name of method to call - * @param p0 method's 1st parameter - * @param p1 method's 2nd parameter - * @param p2 method's 3rd parameter - * @param p3 method's 4th parameter - * @return deserialized method return value - * @throws XMLRPCException - */ - public Object call(String method, Object p0, Object p1, Object p2, Object p3) throws XMLRPCException { - Object[] params = { - p0, p1, p2, p3, - }; - return callXMLRPC(method, params); - } - - /** - * Convenience method call with five parameters - * - * @param method name of method to call - * @param p0 method's 1st parameter - * @param p1 method's 2nd parameter - * @param p2 method's 3rd parameter - * @param p3 method's 4th parameter - * @param p4 method's 5th parameter - * @return deserialized method return value - * @throws XMLRPCException - */ - public Object call(String method, Object p0, Object p1, Object p2, Object p3, Object p4) throws XMLRPCException { - Object[] params = { - p0, p1, p2, p3, p4, - }; - return callXMLRPC(method, params); - } - - /** - * Convenience method call with six parameters - * - * @param method name of method to call - * @param p0 method's 1st parameter - * @param p1 method's 2nd parameter - * @param p2 method's 3rd parameter - * @param p3 method's 4th parameter - * @param p4 method's 5th parameter - * @param p5 method's 6th parameter - * @return deserialized method return value - * @throws XMLRPCException - */ - public Object call(String method, Object p0, Object p1, Object p2, Object p3, Object p4, Object p5) throws XMLRPCException { - Object[] params = { - p0, p1, p2, p3, p4, p5, - }; - return callXMLRPC(method, params); - } - - /** - * Convenience method call with seven parameters - * - * @param method name of method to call - * @param p0 method's 1st parameter - * @param p1 method's 2nd parameter - * @param p2 method's 3rd parameter - * @param p3 method's 4th parameter - * @param p4 method's 5th parameter - * @param p5 method's 6th parameter - * @param p6 method's 7th parameter - * @return deserialized method return value - * @throws XMLRPCException - */ - public Object call(String method, Object p0, Object p1, Object p2, Object p3, Object p4, Object p5, Object p6) throws XMLRPCException { - Object[] params = { - p0, p1, p2, p3, p4, p5, p6, - }; - return callXMLRPC(method, params); - } - - /** - * Convenience method call with eight parameters - * - * @param method name of method to call - * @param p0 method's 1st parameter - * @param p1 method's 2nd parameter - * @param p2 method's 3rd parameter - * @param p3 method's 4th parameter - * @param p4 method's 5th parameter - * @param p5 method's 6th parameter - * @param p6 method's 7th parameter - * @param p7 method's 8th parameter - * @return deserialized method return value - * @throws XMLRPCException - */ - public Object call(String method, Object p0, Object p1, Object p2, Object p3, Object p4, Object p5, Object p6, Object p7) throws XMLRPCException { - Object[] params = { - p0, p1, p2, p3, p4, p5, p6, p7, - }; - return callXMLRPC(method, params); - } - - /** - * Call method with optional parameters - * - * @param method name of method to call - * @param params parameters to pass to method (may be null if method has no parameters) - * @return deserialized method return value - * @throws XMLRPCException - */ - @SuppressWarnings("unchecked") - protected Object callXMLRPC(String method, Object[] params) throws XMLRPCException { - try { - // prepare POST body - StringWriter bodyWriter = new StringWriter(); - serializer.setOutput(bodyWriter); - serializer.startDocument(null, null); - serializer.startTag(null, TAG_METHOD_CALL); - // set method name - serializer.startTag(null, TAG_METHOD_NAME).text(method).endTag(null, TAG_METHOD_NAME); - if (params != null && params.length != 0) { - // set method params - serializer.startTag(null, TAG_PARAMS); - for (int i=0; i in XMLRPC response - neither nor "); - } - } catch (XMLRPCException e) { - // catch & propagate XMLRPCException/XMLRPCFault - throw e; - } catch (Exception e) { - // wrap any other Exception(s) around XMLRPCException - throw new XMLRPCException(e); - } - } -} diff --git a/lib/src/org/xmlrpc/android/XMLRPCException.java b/lib/src/org/xmlrpc/android/XMLRPCException.java deleted file mode 100644 index 0bfd4e50..00000000 --- a/lib/src/org/xmlrpc/android/XMLRPCException.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.xmlrpc.android; - -public class XMLRPCException extends Exception { - - /** - * - */ - private static final long serialVersionUID = 7499675036625522379L; - - public XMLRPCException(Exception e) { - super(e); - } - - public XMLRPCException(String string) { - super(string); - } -} diff --git a/lib/src/org/xmlrpc/android/XMLRPCFault.java b/lib/src/org/xmlrpc/android/XMLRPCFault.java deleted file mode 100644 index 9f371d07..00000000 --- a/lib/src/org/xmlrpc/android/XMLRPCFault.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.xmlrpc.android; - -public class XMLRPCFault extends XMLRPCException { - /** - * - */ - private static final long serialVersionUID = 5676562456612956519L; - private String faultString; - private int faultCode; - - public XMLRPCFault(String faultString, int faultCode) { - super("XMLRPC Fault: " + faultString + " [code " + faultCode + "]"); - this.faultString = faultString; - this.faultCode = faultCode; - } - - public String getFaultString() { - return faultString; - } - - public int getFaultCode() { - return faultCode; - } -} diff --git a/lib/src/org/xmlrpc/android/XMLRPCSerializer.java b/lib/src/org/xmlrpc/android/XMLRPCSerializer.java deleted file mode 100644 index 4f488516..00000000 --- a/lib/src/org/xmlrpc/android/XMLRPCSerializer.java +++ /dev/null @@ -1,205 +0,0 @@ -package org.xmlrpc.android; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.StringReader; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Date; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; - -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; -import org.xmlpull.v1.XmlSerializer; - -class XMLRPCSerializer { - static final String TAG_NAME = "name"; - static final String TAG_MEMBER = "member"; - static final String TAG_VALUE = "value"; - static final String TAG_DATA = "data"; - - static final String TYPE_INT = "int"; - static final String TYPE_I4 = "i4"; - static final String TYPE_I8 = "i8"; - static final String TYPE_DOUBLE = "double"; - static final String TYPE_BOOLEAN = "boolean"; - static final String TYPE_STRING = "string"; - static final String TYPE_DATE_TIME_ISO8601 = "dateTime.iso8601"; - static final String TYPE_BASE64 = "base64"; - static final String TYPE_ARRAY = "array"; - static final String TYPE_STRUCT = "struct"; - - static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd'T'HH:mm:ss"); - - @SuppressWarnings("unchecked") - static void serialize(XmlSerializer serializer, Object object ) throws IOException { - // check for scalar types: - if (object instanceof Integer || object instanceof Short || object instanceof Byte) { - serializer.startTag(null, TYPE_I4).text(object.toString()).endTag(null, TYPE_I4); - } else - if (object instanceof Long) { - serializer.startTag(null, TYPE_I8).text(object.toString()).endTag(null, TYPE_I8); - } else - if (object instanceof Double || object instanceof Float) { - serializer.startTag(null, TYPE_DOUBLE).text(object.toString()).endTag(null, TYPE_DOUBLE); - } else - if (object instanceof Boolean) { - Boolean bool = (Boolean) object; - String boolStr = bool.booleanValue() ? "1" : "0"; - serializer.startTag(null, TYPE_BOOLEAN).text(boolStr).endTag(null, TYPE_BOOLEAN); - } else - if (object instanceof String) { - serializer.startTag(null, TYPE_STRING).text(object.toString()).endTag(null, TYPE_STRING); - } else - if (object instanceof Date || object instanceof Calendar) { - String dateStr = dateFormat.format(object); - serializer.startTag(null, TYPE_DATE_TIME_ISO8601).text(dateStr).endTag(null, TYPE_DATE_TIME_ISO8601); - } else - if (object instanceof byte[] ){ - String value = new String(Base64Coder.encode((byte[])object)); - serializer.startTag(null, TYPE_BASE64).text(value).endTag(null, TYPE_BASE64); - } else - if (object instanceof List) { - serializer.startTag(null, TYPE_ARRAY).startTag(null, TAG_DATA); - List list = (List) object; - Iterator iter = list.iterator(); - while (iter.hasNext()) { - Object o = iter.next(); - serializer.startTag(null, TAG_VALUE); - serialize(serializer, o); - serializer.endTag(null, TAG_VALUE); - } - serializer.endTag(null, TAG_DATA).endTag(null, TYPE_ARRAY); - } else - if (object instanceof Object[]) { - serializer.startTag(null, TYPE_ARRAY).startTag(null, TAG_DATA); - Object[] objects = (Object[]) object; - for (int i=0; i map = (Map) object; - Iterator> iter = map.entrySet().iterator(); - while (iter.hasNext()) { - Entry entry = iter.next(); - String key = entry.getKey(); - Object value = entry.getValue(); - - serializer.startTag(null, TAG_MEMBER); - serializer.startTag(null, TAG_NAME).text(key).endTag(null, TAG_NAME); - serializer.startTag(null, TAG_VALUE); - serialize(serializer, value); - serializer.endTag(null, TAG_VALUE); - serializer.endTag(null, TAG_MEMBER); - } - serializer.endTag(null, TYPE_STRUCT); - } else { - throw new IOException("Cannot serialize " + object); - } - } - - static Object deserialize(XmlPullParser parser) throws XmlPullParserException, IOException { - parser.require(XmlPullParser.START_TAG, null, TAG_VALUE); - - parser.nextTag(); - String typeNodeName = parser.getName(); - - Object obj; - if (typeNodeName.equals(TYPE_INT) || typeNodeName.equals(TYPE_I4)) { - String value = parser.nextText(); - obj = Integer.parseInt(value); - } else - if (typeNodeName.equals(TYPE_I8)) { - String value = parser.nextText(); - obj = Long.parseLong(value); - } else - if (typeNodeName.equals(TYPE_DOUBLE)) { - String value = parser.nextText(); - obj = Double.parseDouble(value); - } else - if (typeNodeName.equals(TYPE_BOOLEAN)) { - String value = parser.nextText(); - obj = value.equals("1") ? Boolean.TRUE : Boolean.FALSE; - } else - if (typeNodeName.equals(TYPE_STRING)) { - obj = parser.nextText(); - } else - if (typeNodeName.equals(TYPE_DATE_TIME_ISO8601)) { - String value = parser.nextText(); - try { - obj = dateFormat.parseObject(value); - } catch (ParseException e) { - throw new IOException("Cannot deserialize dateTime " + value); - } - } else - if (typeNodeName.equals(TYPE_BASE64)) { - String value = parser.nextText(); - BufferedReader reader = new BufferedReader(new StringReader(value)); - String line; - StringBuffer sb = new StringBuffer(); - while ((line = reader.readLine()) != null) { - sb.append(line); - } - obj = Base64Coder.decode(sb.toString()); - } else - if (typeNodeName.equals(TYPE_ARRAY)) { - parser.nextTag(); // TAG_DATA () - parser.require(XmlPullParser.START_TAG, null, TAG_DATA); - - parser.nextTag(); - List list = new ArrayList(); - while (parser.getName().equals(TAG_VALUE)) { - list.add(deserialize(parser)); - parser.nextTag(); - } - parser.require(XmlPullParser.END_TAG, null, TAG_DATA); - parser.nextTag(); // TAG_ARRAY () - parser.require(XmlPullParser.END_TAG, null, TYPE_ARRAY); - obj = list.toArray(); - } else - if (typeNodeName.equals(TYPE_STRUCT)) { - parser.nextTag(); - Map map = new HashMap(); - while (parser.getName().equals(TAG_MEMBER)) { - String memberName = null; - Object memberValue = null; - while (true) { - parser.nextTag(); - String name = parser.getName(); - if (name.equals(TAG_NAME)) { - memberName = parser.nextText(); - } else - if (name.equals(TAG_VALUE)) { - memberValue = deserialize(parser); - } else { - break; - } - } - if (memberName != null && memberValue != null) { - map.put(memberName, memberValue); - } - parser.require(XmlPullParser.END_TAG, null, TAG_MEMBER); - parser.nextTag(); - } - parser.require(XmlPullParser.END_TAG, null, TYPE_STRUCT); - obj = map; - } else { - throw new IOException("Cannot deserialize " + parser.getName()); - } - parser.nextTag(); // TAG_VALUE () - parser.require(XmlPullParser.END_TAG, null, TAG_VALUE); - return obj; - } -}