Java tutorial
/* * 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); } } }