/*
* Copyright 2007, MetaDimensional Technologies Inc.
*
*
* This file is part of the RememberTheMilk Java API.
*
* The RememberTheMilk Java API is free software; you can redistribute it
* and/or modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* The RememberTheMilk Java API is distributed in the hope that it will be
* useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
* General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see .
*/
package com.mdt.rtm;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.xml.sax.SAXException;
import android.util.Log;
/**
* Handles the details of invoking a method on the RTM REST API.
*
* @author Will Ross Jun 21, 2007
*/
public class Invoker {
private static final String TAG = "rtm-invoker";
private static final DocumentBuilder builder;
static
{
// Done this way because the builder is marked "final"
DocumentBuilder aBuilder;
try
{
final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(false);
factory.setValidating(false);
aBuilder = factory.newDocumentBuilder();
}
catch (Exception exception)
{
Log.e(TAG, "Unable to construct a document builder", exception);
aBuilder = null;
}
builder = aBuilder;
}
private static final String ENCODING = "UTF-8";
private static final String API_SIG_PARAM = "api_sig";
private static final long INVOCATION_INTERVAL = 400;
private long lastInvocation;
private final ApplicationInfo applicationInfo;
private final MessageDigest digest;
private String serviceRelativeUri;
private HttpClient httpClient;
public Invoker(String serverHostName, int serverPortNumber,
String serviceRelativeUri, ApplicationInfo applicationInfo)
throws ServiceInternalException {
this.serviceRelativeUri = serviceRelativeUri;
httpClient = new DefaultHttpClient();
lastInvocation = System.currentTimeMillis();
this.applicationInfo = applicationInfo;
try {
digest = MessageDigest.getInstance("md5");
} catch (NoSuchAlgorithmException e) {
throw new ServiceInternalException(
"Could not create properly the MD5 digest", e);
}
}
private StringBuffer computeRequestUri(Param... params)
throws ServiceInternalException {
final StringBuffer requestUri = new StringBuffer(serviceRelativeUri);
if (params.length > 0) {
requestUri.append("?");
}
for (Param param : params) {
try {
requestUri.append(param.getName()).append("=").append(
URLEncoder.encode(param.getValue(), ENCODING)).append(
"&");
} catch (Exception exception) {
final StringBuffer message = new StringBuffer(
"Cannot encode properly the HTTP GET request URI: cannot execute query");
Log.e(TAG, message.toString(), exception);
throw new ServiceInternalException(message.toString());
}
}
requestUri.append(API_SIG_PARAM).append("=").append(calcApiSig(params));
return requestUri;
}
/** Call invoke with a false repeat */
public Element invoke(Param... params) throws ServiceException {
return invoke(false, params);
}
public Element invoke(boolean repeat, Param... params)
throws ServiceException {
long timeSinceLastInvocation = System.currentTimeMillis() -
lastInvocation;
if (timeSinceLastInvocation < INVOCATION_INTERVAL) {
// In order not to invoke the RTM service too often
try {
Thread.sleep(INVOCATION_INTERVAL - timeSinceLastInvocation);
} catch (InterruptedException e) {
return null;
}
}
// We compute the URI
final StringBuffer requestUri = computeRequestUri(params);
HttpResponse response = null;
final HttpGet request = new HttpGet("http://"
+ ServiceImpl.SERVER_HOST_NAME + requestUri.toString());
final String methodUri = request.getRequestLine().getUri();
Element result;
try {
Log.i(TAG, "Executing the method:" + methodUri);
response = httpClient.execute(request);
lastInvocation = System.currentTimeMillis();
final int statusCode = response.getStatusLine().getStatusCode();
if (statusCode != HttpStatus.SC_OK) {
Log.e(TAG, "Method failed: " + response.getStatusLine());
// Tim: HTTP error. Let's wait a little bit
if (!repeat) {
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
// ignore
}
return invoke(true, params);
}
throw new ServiceInternalException("method failed: "
+ response.getStatusLine());
}
final Document responseDoc = builder.parse(response.getEntity()
.getContent());
final Element wrapperElt = responseDoc.getDocumentElement();
if (!wrapperElt.getNodeName().equals("rsp")) {
throw new ServiceInternalException(
"unexpected response returned by RTM service: "
+ wrapperElt.getNodeName());
} else {
String stat = wrapperElt.getAttribute("stat");
if (stat.equals("fail")) {
Node errElt = wrapperElt.getFirstChild();
while (errElt != null
&& (errElt.getNodeType() != Node.ELEMENT_NODE || !errElt
.getNodeName().equals("err"))) {
errElt = errElt.getNextSibling();
}
if (errElt == null) {
throw new ServiceInternalException(
"unexpected response returned by RTM service: "
+ wrapperElt.getNodeValue());
} else {
throw new ServiceException(Integer
.parseInt(((Element) errElt)
.getAttribute("code")),
((Element) errElt).getAttribute("msg"));
}
} else {
Node dataElt = wrapperElt.getFirstChild();
while (dataElt != null
&& (dataElt.getNodeType() != Node.ELEMENT_NODE || dataElt
.getNodeName().equals("transaction") == true)) {
try {
Node nextSibling = dataElt.getNextSibling();
if (nextSibling == null) {
break;
} else {
dataElt = nextSibling;
}
} catch (IndexOutOfBoundsException exception) {
// Some implementation may throw this exception,
// instead of returning a null sibling
break;
}
}
if (dataElt == null) {
throw new ServiceInternalException(
"unexpected response returned by RTM service: "
+ wrapperElt.getNodeValue());
} else {
result = (Element) dataElt;
}
}
}
} catch (IOException e) {
throw new ServiceInternalException("Error making connection: " +
e.getMessage(), e);
} catch (SAXException e) {
// repeat call if possible.
if(!repeat)
return invoke(true, params);
else
throw new ServiceInternalException("Error parsing response. " +
"Please try sync again!", e);
} finally {
httpClient.getConnectionManager().closeExpiredConnections();
}
return result;
}
final String calcApiSig(Param... params) throws ServiceInternalException {
try {
digest.reset();
digest.update(applicationInfo.getSharedSecret().getBytes(ENCODING));
List sorted = Arrays.asList(params);
Collections.sort(sorted);
for (Param param : sorted) {
digest.update(param.getName().getBytes(ENCODING));
digest.update(param.getValue().getBytes(ENCODING));
}
return convertToHex(digest.digest());
} catch (UnsupportedEncodingException e) {
throw new ServiceInternalException(
"cannot hahdle properly the encoding", e);
}
}
private static String convertToHex(byte[] data) {
StringBuffer buf = new StringBuffer();
for (int i = 0; i < data.length; i++) {
int halfbyte = (data[i] >>> 4) & 0x0F;
int two_halfs = 0;
do {
if ((0 <= halfbyte) && (halfbyte <= 9))
buf.append((char) ('0' + halfbyte));
else
buf.append((char) ('a' + (halfbyte - 10)));
halfbyte = data[i] & 0x0F;
} while (two_halfs++ < 1);
}
return buf.toString();
}
}