package org.xmlrpc.android;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileWriter;
import java.io.InputStream;
import java.io.SequenceInputStream;
import java.io.StringWriter;
import java.net.URI;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import android.util.Xml;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.FileEntity;
import org.apache.http.entity.StringEntity;
import org.apache.http.params.CoreConnectionPNames;
import org.apache.http.params.HttpParams;
import org.apache.http.params.HttpProtocolParams;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserFactory;
import org.xmlpull.v1.XmlSerializer;
import org.wordpress.android.WordPress;
import org.wordpress.android.util.DeviceUtils;
/**
* A WordPress XMLRPC Client. Based on android-xmlrpc:
* code.google.com/p/android-xmlrpc/ Async support based on aXMLRPC:
* https://github.com/timroes/aXMLRPC
*/
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 Map<Long, Caller> backgroundCalls = new HashMap<Long, Caller>();
private ConnectionClient 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(URI uri, String httpuser, String httppasswd) {
postMethod = new HttpPost(uri);
postMethod.addHeader("Content-Type", "text/xml");
postMethod.addHeader("charset", "UTF-8");
if (DeviceUtils.isBlackBerry()) {
postMethod.addHeader("User-Agent",
DeviceUtils.getBlackBerryUserAgent());
} else {
postMethod.addHeader("User-Agent", "wp-android/"
+ WordPress.versionName);
}
httpParams = postMethod.getParams();
HttpProtocolParams.setUseExpectContinue(httpParams, false);
//username & password not needed
UsernamePasswordCredentials creds = new UsernamePasswordCredentials(
httpuser, httppasswd);
//this gets connections working over https
if (uri.getScheme() != null) {
if (uri.getScheme().equals("https")) {
if (uri.getPort() == -1)
try {
client = new ConnectionClient(creds, 443);
} catch (KeyManagementException e) {
client = new ConnectionClient(creds);
} catch (NoSuchAlgorithmException e) {
client = new ConnectionClient(creds);
} catch (KeyStoreException e) {
client = new ConnectionClient(creds);
} catch (UnrecoverableKeyException e) {
client = new ConnectionClient(creds);
}
else
try {
client = new ConnectionClient(creds, uri.getPort());
} catch (KeyManagementException e) {
client = new ConnectionClient(creds);
} catch (NoSuchAlgorithmException e) {
client = new ConnectionClient(creds);
} catch (KeyStoreException e) {
client = new ConnectionClient(creds);
} catch (UnrecoverableKeyException e) {
client = new ConnectionClient(creds);
}
} else {
client = new ConnectionClient(creds);
}
} else {
client = new ConnectionClient(creds);
}
serializer = Xml.newSerializer();
}
public void addQuickPostHeader(String type) {
postMethod.addHeader("WP-QUICK-POST", type);
}
/**
* Convenience constructor. Creates new instance based on server String
* address
*
* @param XMLRPC
* server address
*/
public XMLRPCClient(String url, String httpuser, String httppasswd) {
this(URI.create(url), httpuser, httppasswd);
}
/**
* Convenience XMLRPCClient constructor. Creates new instance based on
* server URL
*
* @param XMLRPC
* server URL
*/
public XMLRPCClient(URL url, String httpuser, String httppasswd) {
this(URI.create(url.toExternalForm()), httpuser, httppasswd);
}
/**
* Set WP.com auth header
*
* @param String
* authorization token
*/
public void setAuthorizationHeader(String authToken) {
if (authToken != null)
postMethod.addHeader("Authorization",
String.format("Bearer %s", authToken));
else
postMethod.removeHeaders("Authorization");
}
/**
* 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 call(method, params, null);
}
/**
* 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 call(method, null, null);
}
public Object call(String method, Object[] params, File tempFile)
throws XMLRPCException {
return new Caller().callXMLRPC(method, params, tempFile);
}
/**
* Convenience call for callAsync with two paramaters
*
* @param XMLRPCCallback
* listener, XMLRPC methodName, XMLRPC parameters
* @return unique id of this async call
* @throws XMLRPCException
*/
public long callAsync(XMLRPCCallback listener, String methodName,
Object[] params) {
return callAsync(listener, methodName, params, null);
}
/**
* Asynchronous XMLRPC call
*
* @param XMLRPCCallback
* listener, XMLRPC methodName, XMLRPC parameters, File for large
* uploads
* @return unique id of this async call
* @throws XMLRPCException
*/
public long callAsync(XMLRPCCallback listener, String methodName,
Object[] params, File tempFile) {
long id = System.currentTimeMillis();
new Caller(listener, id, methodName, params, tempFile).start();
return id;
}
/**
* 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 File tempFile;
private volatile boolean canceled;
private ConnectionClient 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, File tempFile) {
this.listener = listener;
this.threadId = threadId;
this.methodName = methodName;
this.params = params;
this.tempFile = tempFile;
}
/**
* 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.callXMLRPC(methodName, params, tempFile);
listener.onSuccess(threadId, o);
} catch (CancelException ex) {
// Don't notify the listener, if the call has been canceled.
} catch (XMLRPCException ex) {
listener.onFailure(threadId, ex);
} finally {
backgroundCalls.remove(threadId);
}
}
/**
* Cancel this call. This will abort the network communication.
*/
public void cancel() {
// TODO this doesn't work
// Set the flag, that this thread has been canceled
canceled = true;
// Disconnect the connection to the server
http.getHttpRequestRetryHandler();
}
/**
* 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")
private Object callXMLRPC(String method, Object[] params, File tempFile)
throws XMLRPCException {
try {
// prepare POST body
if (method.equals("wp.uploadFile")) {
if (!tempFile.exists() && !tempFile.mkdirs()) {
throw new XMLRPCException(
"Path to file could not be created.");
}
FileWriter fileWriter = new FileWriter(tempFile);
serializer.setOutput(fileWriter);
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 < params.length; i++) {
serializer.startTag(null, TAG_PARAM).startTag(null,
XMLRPCSerializer.TAG_VALUE);
XMLRPCSerializer.serialize(serializer, params[i]);
serializer.endTag(null, XMLRPCSerializer.TAG_VALUE)
.endTag(null, TAG_PARAM);
}
serializer.endTag(null, TAG_PARAMS);
}
serializer.endTag(null, TAG_METHOD_CALL);
serializer.endDocument();
fileWriter.flush();
fileWriter.close();
FileEntity fEntity = new FileEntity(tempFile,
"text/xml; charset=\"UTF-8\"");
fEntity.setContentType("text/xml");
//fEntity.setChunked(true);
postMethod.setEntity(fEntity);
} else {
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 < params.length; i++) {
serializer.startTag(null, TAG_PARAM).startTag(null,
XMLRPCSerializer.TAG_VALUE);
if (method.equals("metaWeblog.editPost")
|| method.equals("metaWeblog.newPost")) {
XMLRPCSerializer.serialize(serializer,
params[i]);
} else {
XMLRPCSerializer.serialize(serializer,
params[i]);
}
serializer.endTag(null, XMLRPCSerializer.TAG_VALUE)
.endTag(null, TAG_PARAM);
}
serializer.endTag(null, TAG_PARAMS);
}
serializer.endTag(null, TAG_METHOD_CALL);
serializer.endDocument();
HttpEntity entity = new StringEntity(bodyWriter.toString());
//Log.i("WordPress", bodyWriter.toString());
postMethod.setEntity(entity);
}
//set timeout to 40 seconds, does it need to be set for both client and method?
client.getParams().setParameter(
CoreConnectionPNames.CONNECTION_TIMEOUT, 40000);
client.getParams().setParameter(
CoreConnectionPNames.SO_TIMEOUT, 40000);
postMethod.getParams().setParameter(
CoreConnectionPNames.CONNECTION_TIMEOUT, 40000);
postMethod.getParams().setParameter(
CoreConnectionPNames.SO_TIMEOUT, 40000);
// execute HTTP POST request
HttpResponse response = client.execute(postMethod);
//Log.i("WordPress", "response = " + response.getStatusLine());
// check status code
int statusCode = response.getStatusLine().getStatusCode();
deleteTempFile(method, tempFile);
if (statusCode != HttpStatus.SC_OK) {
throw new XMLRPCException("HTTP status code: " + statusCode
+ " was returned. "
+ response.getStatusLine().getReasonPhrase());
}
// setup pull parser
XmlPullParser pullParser = XmlPullParserFactory.newInstance()
.newPullParser();
HttpEntity entity = response.getEntity();
InputStream is = entity.getContent();
// Many WordPress configs can output junk before the xml response (php warnings for example), this cleans it.
int bomCheck = -1;
int stopper = 0;
while ((bomCheck = is.read()) != -1 && stopper <= 5000) {
stopper++;
String snippet = "";
//60 == '<' character
if (bomCheck == 60) {
for (int i = 0; i < 4; i++) {
byte[] chunk = new byte[1];
is.read(chunk);
snippet += new String(chunk, "UTF-8");
}
if (snippet.equals("?xml")) {
//it's all good, add xml tag back and start parsing
String start = "<" + snippet;
List<InputStream> streams = Arrays.asList(
new ByteArrayInputStream(start.getBytes()),
is);
is = new SequenceInputStream(
Collections.enumeration(streams));
break;
} else {
//keep searching...
List<InputStream> streams = Arrays
.asList(new ByteArrayInputStream(snippet
.getBytes()), is);
is = new SequenceInputStream(
Collections.enumeration(streams));
}
}
}
pullParser.setInput(is, "UTF-8");
// lets start pulling...
pullParser.nextTag();
pullParser.require(XmlPullParser.START_TAG, null,
TAG_METHOD_RESPONSE);
pullParser.nextTag(); // either TAG_PARAMS (<params>) or TAG_FAULT (<fault>)
String tag = pullParser.getName();
if (tag.equals(TAG_PARAMS)) {
// normal response
pullParser.nextTag(); // TAG_PARAM (<param>)
pullParser
.require(XmlPullParser.START_TAG, null, TAG_PARAM);
pullParser.nextTag(); // TAG_VALUE (<value>)
// no parser.require() here since its called in XMLRPCSerializer.deserialize() below
// deserialize result
Object obj = XMLRPCSerializer.deserialize(pullParser);
entity.consumeContent();
return obj;
} else if (tag.equals(TAG_FAULT)) {
// fault response
pullParser.nextTag(); // TAG_VALUE (<value>)
// no parser.require() here since its called in XMLRPCSerializer.deserialize() below
// deserialize fault result
Map<String, Object> map = (Map<String, Object>) XMLRPCSerializer
.deserialize(pullParser);
String faultString = (String) map.get(TAG_FAULT_STRING);
int faultCode = (Integer) map.get(TAG_FAULT_CODE);
entity.consumeContent();
throw new XMLRPCFault(faultString, faultCode);
} else {
entity.consumeContent();
throw new XMLRPCException(
"Bad tag <"
+ tag
+ "> in XMLRPC response - neither <params> nor <fault>");
}
} catch (XMLRPCException e) {
// catch & propagate XMLRPCException/XMLRPCFault
deleteTempFile(method, tempFile);
throw e;
} catch (Exception e) {
// wrap any other Exception(s) around XMLRPCException
deleteTempFile(method, tempFile);
throw new XMLRPCException(e);
}
}
}
private void deleteTempFile(String method, File tempFile) {
if (tempFile != null) {
if ((method.equals("wp.uploadFile"))) { //get rid of the temp file
tempFile.delete();
}
}
}
private class CancelException extends RuntimeException {
}
}