You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
478 lines
16 KiB
478 lines
16 KiB
11 years ago
|
package de.timroes.axmlrpc;
|
||
|
|
||
|
import java.io.IOException;
|
||
|
import java.io.InputStream;
|
||
11 years ago
|
import java.net.HttpURLConnection;
|
||
|
import java.net.SocketTimeoutException;
|
||
11 years ago
|
import java.util.Map;
|
||
|
import java.util.concurrent.ConcurrentHashMap;
|
||
11 years ago
|
|
||
|
import org.apache.http.HttpResponse;
|
||
|
import org.apache.http.client.methods.HttpPost;
|
||
|
import org.apache.http.entity.StringEntity;
|
||
|
import org.apache.http.impl.client.DefaultHttpClient;
|
||
|
import org.apache.http.protocol.HTTP;
|
||
|
|
||
|
import de.timroes.axmlrpc.serializer.SerializerHandler;
|
||
11 years ago
|
|
||
|
/**
|
||
|
* 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 {
|
||
|
|
||
|
/**
|
||
|
* Constants from the http protocol.
|
||
|
*/
|
||
|
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;
|
||
|
|
||
|
/**
|
||
|
* The client will be able to send null values. A null value will be send
|
||
|
* as <nil/>. 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, 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 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;
|
||
|
|
||
11 years ago
|
private DefaultHttpClient httpclient;
|
||
|
|
||
|
private String url;
|
||
11 years ago
|
|
||
|
private Map<Long,Caller> backgroundCalls = new ConcurrentHashMap<Long, Caller>();
|
||
|
|
||
|
private ResponseParser responseParser;
|
||
|
|
||
|
/**
|
||
|
* Create a new XMLRPC client for the given URL.
|
||
11 years ago
|
*
|
||
|
* @param httpclient The already-initialized Apache HttpClient to use for connection.
|
||
11 years ago
|
* @param url The URL to send the requests to.
|
||
|
* @param flags A combination of flags to be set.
|
||
|
*/
|
||
11 years ago
|
public XMLRPCClient(DefaultHttpClient httpclient, String url, int flags) {
|
||
11 years ago
|
|
||
|
SerializerHandler.initialize(flags);
|
||
|
|
||
11 years ago
|
this.httpclient = httpclient;
|
||
11 years ago
|
this.url = url;
|
||
|
this.flags = flags;
|
||
11 years ago
|
|
||
11 years ago
|
// Create a parser for the http responses.
|
||
|
responseParser = new ResponseParser();
|
||
|
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Create a new XMLRPC client for the given url.
|
||
|
* No flags will be used.
|
||
|
*
|
||
11 years ago
|
* @param httpclient The already-initialized Apache HttpClient to use for connection.
|
||
11 years ago
|
* @param url The url to send the requests to.
|
||
|
*/
|
||
11 years ago
|
public XMLRPCClient(DefaultHttpClient httpclient, String url) {
|
||
|
this(httpclient, url, FLAGS_NONE);
|
||
11 years ago
|
}
|
||
|
|
||
|
/**
|
||
|
* 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 {
|
||
11 years ago
|
try {
|
||
|
return new Caller().call(method, params);
|
||
|
} catch (CancelException e) {
|
||
|
// Should not happen as this is not an async call
|
||
|
throw new XMLRPCException("Background thread was explicitly cancelled, but not started asynchronously.");
|
||
|
}
|
||
11 years ago
|
}
|
||
|
|
||
|
/**
|
||
|
* 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;
|
||
|
|
||
11 years ago
|
HttpPost post = null;
|
||
11 years ago
|
private volatile boolean canceled;
|
||
|
|
||
|
/**
|
||
|
* 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(XMLRPCServerException ex) {
|
||
|
listener.onServerError(threadId, ex);
|
||
|
} catch (XMLRPCException ex) {
|
||
|
listener.onError(threadId, ex);
|
||
11 years ago
|
} catch (CancelException e) {
|
||
11 years ago
|
} 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
|
||
11 years ago
|
if (post != null)
|
||
|
post.abort();
|
||
11 years ago
|
}
|
||
|
|
||
|
/**
|
||
|
* 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.
|
||
11 years ago
|
* @throws CancelException WIll be thrown if the async execution is explicitly cancelled.
|
||
11 years ago
|
*/
|
||
11 years ago
|
public Object call(String methodName, Object[] params) throws XMLRPCException, CancelException {
|
||
11 years ago
|
|
||
|
try {
|
||
|
|
||
|
Call c = createCall(methodName, params);
|
||
11 years ago
|
|
||
|
// Prepare POST request
|
||
|
HttpPost post = new HttpPost(url);
|
||
|
post.getParams().setParameter("http.protocol.handle-redirects", false);
|
||
|
post.setHeader(CONTENT_TYPE, TYPE_XML);
|
||
|
StringEntity entity = new StringEntity(c.getXML(), HTTP.UTF_8);
|
||
|
entity.setContentType(TYPE_XML);
|
||
|
post.setEntity(entity);
|
||
|
|
||
|
HttpResponse response = httpclient.execute(post);
|
||
|
int statusCode = response.getStatusLine().getStatusCode();
|
||
|
|
||
11 years ago
|
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
|
||
11 years ago
|
istream = response.getEntity().getContent();
|
||
11 years ago
|
} else {
|
||
|
throw new XMLRPCException("Invalid status code '"
|
||
11 years ago
|
+ statusCode + "' returned from server.", new UnauthorizdException(statusCode));
|
||
11 years ago
|
}
|
||
|
|
||
|
} else {
|
||
11 years ago
|
istream = response.getEntity().getContent();
|
||
11 years ago
|
}
|
||
|
|
||
|
// 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.
|
||
11 years ago
|
String newLocation = response.getFirstHeader("Location").getValue();
|
||
11 years ago
|
// Try getting header in lower case, if no header has been found
|
||
|
if(newLocation == null || newLocation.length() <= 0)
|
||
11 years ago
|
newLocation = response.getFirstHeader("location").getValue();
|
||
11 years ago
|
|
||
|
// Set new location, disconnect current connection and request to new location.
|
||
11 years ago
|
String oldURL = url;
|
||
|
url = newLocation;
|
||
11 years ago
|
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)) {
|
||
11 years ago
|
if(!response.getFirstHeader("Content-Type").getValue().startsWith(TYPE_XML)) {
|
||
11 years ago
|
throw new XMLRPCException("The Content-Type of the response must be text/xml.");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
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();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
}
|
||
11 years ago
|
|
||
|
}
|
||
|
|
||
|
public static class CancelException extends Exception {
|
||
|
private static final long serialVersionUID = 9125122307255855136L;
|
||
11 years ago
|
}
|
||
|
|
||
11 years ago
|
public static class UnauthorizdException extends Exception {
|
||
|
private static final long serialVersionUID = -3331056540713825039L;
|
||
|
private int statusCode;
|
||
|
public UnauthorizdException(int statusCode) { this.statusCode = statusCode; }
|
||
|
public int getStatusCode() { return statusCode; }
|
||
|
}
|
||
|
|
||
11 years ago
|
}
|