com.mdt.rtm.Invoker.java Source code

Java tutorial

Introduction

Here is the source code for com.mdt.rtm.Invoker.java

Source

/*
 * 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 <http://www.gnu.org/licenses/>.
 */
package com.mdt.rtm;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.apache.commons.codec.binary.Hex;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.NameValuePair;
import org.apache.commons.httpclient.URI;
import org.apache.commons.httpclient.URIException;
import org.apache.commons.httpclient.UsernamePasswordCredentials;
import org.apache.commons.httpclient.auth.AuthScope;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.params.HttpMethodParams;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.xml.sax.SAXException;

/**
 * Handles the details of invoking a method on the RTM REST API.
 * 
 * @author Will Ross Jun 21, 2007
 */
public class Invoker {

    private static final Log log = LogFactory.getLog("Invoker");

    private static final DocumentBuilder builder;
    static {
        DocumentBuilder b;
        try {
            DocumentBuilderFactory fact = DocumentBuilderFactory.newInstance();
            b = fact.newDocumentBuilder();
        } catch (Exception e) {
            log.error("Unable to construct document builder.", e);
            b = null;
        }
        builder = b;
    }

    private final String serviceBaseUrl;

    public static final String REST_SERVICE_URL_POSTFIX = "/services/rest/";

    public static final String ENC = "UTF-8";

    public static String API_SIG_PARAM = "api_sig";

    public static final long INVOCATION_INTERVAL = 2000;

    private long lastInvocation;

    private final ApplicationInfo applicationInfo;

    private final MessageDigest digest;

    private String proxyHostName;

    private int proxyPortNumber;

    private String proxyLogin;

    private String proxyPassword;

    public Invoker(String serviceBaseUrl, ApplicationInfo applicationInfo) throws ServiceInternalException {
        this.serviceBaseUrl = serviceBaseUrl;
        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);
        }
    }

    public void setHttpProxySettings(String proxyHostName, int proxyPortNumber, String proxyLogin,
            String proxyPassword) {
        this.proxyHostName = proxyHostName;
        this.proxyPortNumber = proxyPortNumber;
        this.proxyLogin = proxyLogin;
        this.proxyPassword = proxyPassword;
    }

    public Element invoke(Param... params) throws ServiceException {
        Element result;

        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) {
                throw new ServiceInternalException(
                        "Unexpected interruption while attempting to pause for some time before invoking the RTM service back",
                        e);
            }
        }

        log.debug("Invoker running at " + new Date());

        HttpClient client = new HttpClient();
        if (proxyHostName != null) {
            // Sets an HTTP proxy and the credentials for authentication
            client.getHostConfiguration().setProxy(proxyHostName, proxyPortNumber);
            if (proxyLogin != null) {
                client.getState().setProxyCredentials(AuthScope.ANY,
                        new UsernamePasswordCredentials(proxyLogin, proxyPassword));
            }
        }
        GetMethod method = new GetMethod(serviceBaseUrl + REST_SERVICE_URL_POSTFIX);
        method.setRequestHeader(HttpMethodParams.HTTP_URI_CHARSET, "UTF-8");
        NameValuePair[] pairs = new NameValuePair[params.length + 1];
        int i = 0;
        for (Param param : params) {
            log.debug("  setting " + param.getName() + "=" + param.getValue());
            pairs[i++] = param.toNameValuePair();
        }
        pairs[i++] = new NameValuePair(API_SIG_PARAM, calcApiSig(params));
        method.setQueryString(pairs);

        try {
            URI methodUri;
            try {
                methodUri = method.getURI();
                log.info("Executing the method:" + methodUri);
            } catch (URIException exception) {
                String message = "Cannot determine the URI of the web method";
                log.error(message);
                throw new ServiceInternalException(message, exception);
            }
            int statusCode = client.executeMethod(method);

            if (statusCode != HttpStatus.SC_OK) {
                log.error("Method failed: " + method.getStatusLine());
                throw new ServiceInternalException("method failed: " + method.getStatusLine());
            }

            // THINK: this method is deprecated, but the only way to get the body as a string, without consuming
            // the body input stream: the HttpMethodBase issues a warning but does not let you call the "setResponseStream()" method!
            String responseBodyAsString = method.getResponseBodyAsString();
            log.info("  Invocation response:\r\n" + responseBodyAsString);
            Document responseDoc = builder.parse(method.getResponseBodyAsStream());
            Element wrapperElt = responseDoc.getDocumentElement();
            if (!wrapperElt.getNodeName().equals("rsp")) {
                throw new ServiceInternalException(
                        "unexpected response returned by RTM service: " + responseBodyAsString);
            } 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: " + responseBodyAsString);
                    } 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: " + responseBodyAsString);
                    } else {
                        result = (Element) dataElt;
                    }
                }
            }

        } catch (HttpException e) {
            throw new ServiceInternalException("", e);
        } catch (IOException e) {
            throw new ServiceInternalException("", e);
        } catch (SAXException e) {
            throw new ServiceInternalException("", e);
        } finally {
            // Release the connection.
            method.releaseConnection();
        }

        lastInvocation = System.currentTimeMillis();
        return result;
    }

    final String calcApiSig(Param... params) throws ServiceInternalException {
        try {
            digest.reset();
            digest.update(applicationInfo.getSharedSecret().getBytes(ENC));
            List<Param> sorted = Arrays.asList(params);
            Collections.sort(sorted);
            for (Param param : sorted) {
                digest.update(param.getName().getBytes(ENC));
                digest.update(param.getValue().getBytes(ENC));
            }
            return new String(Hex.encodeHex(digest.digest()));
            // return new String(digest.digest(), ENC);
        } catch (UnsupportedEncodingException e) {
            throw new ServiceInternalException("cannot hahdle properly the encoding", e);
        }
    }

}