Usando Apache HttpComponents

(sucesor de Apache Commons HttpClient)

Para los que no lo conocen, Apache Commons (http://commons.apache.org/) es una colección de bibliotecas (archivos .jar) construidas sobre la API estandar de Java que pretenden abarcar todos los algoritmos y lugares comunes que un programador utiliza habitualmente, posibilitando la reutilización del código y reduciendo notablemente la cantidad de líneas necesarias para codificar en Java.Todo programador Java medianamente serio debería chequear en Apache Commons si ya existe una solución para el problema que debe afrontar antes de ponerse a codificar.Apache Commons posee soluciones para trabajar con diversos asuntos, como “strings”, “collections”, “input/output”, “codecs” (base64, hex, etc), imagenes, “logging”, matemática, “emailing”, etc, etc, etc.

HttpComponents sea tal vez la forma más sencilla en Java de ejecutar un “GET” o un “POST” a una URL. Este podría ser uno de los modos más elementales y de bajo nivel, pero no por ello menos eficaz, de consumir datos de un “webservice”.

Aún así existe otra solución de más bajo nivel que consiste en utilizar únicamente la API de Java (nada de Apache Commons!), especificamente la clase “java.net.HttpURLConnection”, pero sería bajar demasiado para mi gusto. Además recomiendo utilizar Apache Commons siempre que se pueda (que es equivalente a decir “SIEMPRE” a secas). Se encuentran varias comparaciones en Google si uno busca “HttpURLConnection vs HttpComponents”.

A los bifes…

En los ejemplos se utiliza la clase “org.apache.commons.io.IOUtils” para copiar los bytes de un stream a otro. Esta clase es parte de “Commons IO”.

1. GET

En este código se hace un “GET” a http://www.google.com y se obtiene el html.

DefaultHttpClient httpClient = new DefaultHttpClient();

HttpResponse httpResponse = httpClient.execute(
        new HttpGet("http://www.google.com"));

System.out.println("Status Code " +
        httpResponse.getStatusLine().getStatusCode());

IOUtils.copy(httpResponse.getEntity().getContent(),
        System.out);

httpResponse.getEntity().getContent().close();

2. GET a través de un proxy (ej Squid)

Básicamente es el ejemplo anterior pero se agregaron algunas líneas para atravesar un proxy corporativo, como podría ser Squid.

DefaultHttpClient httpClient = new DefaultHttpClient();

httpClient.getCredentialsProvider().setCredentials(
        new AuthScope("proxy.miempresa.com", 3128 /* numero de puerto */),
        new UsernamePasswordCredentials("mi-usuario", "mi-password"));

httpClient.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY,
        new HttpHost("proxy.miempresa.com", 3128 /* numero de puerto */));

HttpResponse httpResponse = httpClient.execute(
        new HttpGet("http://www.google.com"));

System.out.println("Status Code " +
        httpResponse.getStatusLine().getStatusCode());

IOUtils.copy(httpResponse.getEntity().getContent(),
        System.out);

httpResponse.getEntity().getContent().close();

3. Timeouts

En determinadas situaciones es buena idea establecer un tiempo límite de respuesta en el proceso de establecer la conexión con el servidor y obtener el resultado. Sobre todo si dependemos de esto para generar otra respuesta a otro cliente (ej el caso de un servlet).

Aquí la estrategia es generar la respuesta en un tiempo razonable, y si no se puede lanzar una excepción, en cambio de dejar a todo el mundo en cola de espera.

Para esto se configuran dos parámetros en el cliente http:

  1. http.connection.timeout: tiempo máximo tolerable en milisegundos para establecer la conexión.
  2. http.socket.timeout: tiempo máximo tolerable en milisegundos que puede quedar bloqueada cualquier operación con sockets. (Es la opción SO_TIMEOUT en la API de sockets de Java).
DefaultHttpClient httpClient = new DefaultHttpClient();

httpClient.getParams().setIntParameter("http.connection.timeout",
        new Integer(1000));

httpClient.getParams().setIntParameter("http.socket.timeout",
        new Integer(1000));

HttpResponse httpResponse = httpClient.execute(
        new HttpGet("http://www.google.com"));

System.out.println("Status Code " +
        httpResponse.getStatusLine().getStatusCode());

IOUtils.copy(httpResponse.getEntity().getContent(),
        System.out);

httpResponse.getEntity().getContent().close();

Para ver otros parámetros de configuración útiles consultar la documentación de HttpComponents en http://hc.apache.org/httpcomponents-client-ga/tutorial/html/connmgmt.html

4. org.apache.http.util.EntityUtils

La clase org.apache.http.util.EntityUtils que es parte de HttpComponents nos proporciona de algunos métodos útiles para leer el input stream “content” de “httpResponse.getEntity()” y transformarlo en un “string” (método “toString”) o en un array de bytes (método “toByteArray”) por ejemplo.

Esto es recomendable para el caso de respuestas relativamente corta, o sea, no lo utilizaría si la respuesta de la request (GET o POST) es un archivo de 10 MB, por ejemplo. Para este último caso es mejor trabajar con el paradigma de streams (ej con la clase “org.apache.commons.io.IOUtils”, como hice en los ejemplos anteriores).

DefaultHttpClient httpClient = new DefaultHttpClient();

HttpResponse httpResponse = httpClient.execute(
        new HttpGet("http://www.google.com"));

System.out.println("Status Code " +
        httpResponse.getStatusLine().getStatusCode());

System.out.println(EntityUtils.toString(httpResponse.getEntity()));

Notese que no fue necesario cerrar el stream “httpResponse.getEntity().getContent()”, ya que “EntityUtils.toString” se encarga de hacer toda la magia.

5. POST

Y hemos llegado finalmente. En este ejemplo se ejecuta un comando SOAP haciendo un POST. Elegí ejecutar un comando del sistema de mails Zimbra porque estoy trabajando actualmente con esta tecnología y me quedaba más cómodo.

DefaultHttpClient httpClient = new DefaultHttpClient();

HttpPost httpPost = new HttpPost(
        "https://zimbramail.midominio.com:7071/service/admin/soap");

httpPost.setHeader("Content-Type", "application/soap+xml;charset=UTF-8");

StringBuffer postBody = new StringBuffer()
        .append("<soap:Envelope xmlns:soap=\"http://www.w3.org/2003/05/soap-envelope\">")
        .append("<soap:Header>")
        .append("<context xmlns=\"urn:zimbra\">")
        .append("<nosession/>")
        .append("</context>")
        .append("</soap:Header>")
        .append("<soap:Body>")
        .append("<AuthRequest xmlns=\"urn:zimbraAdmin\">")
        .append("<name>")
        .append("admin")
        .append("</name>")
        .append("<password>")
        .append("passwordfalso")
        .append("</password>")
        .append("</AuthRequest>")
        .append("</soap:Body>")
        .append("</soap:Envelope>");

httpPost.setEntity(new StringEntity(postBody.toString(),
        ContentType.create("text/xml", "UTF-8")));

HttpResponse httpResponse = httpClient.execute(httpPost);

System.out.println("Status Code " +
        httpResponse.getStatusLine().getStatusCode());

System.out.println(EntityUtils.toString(httpResponse.getEntity()));

Según el estandar, los comandos SOAP tiene un parámetro adicional en el header del post que especifica el nombre del comando, se llama “SOAPAction”. No es este el caso, pero si fuese requerido se podría agregar al header de la request con la siguiente línea:

httpPost.setHeader("SOAPAction", "nombreComandoSOAP");

6. Cookies

En el ejemplo anterior se ejecutaba un comando SOAP que lo que hacía era autenticarnos dentro de la aplicación Zimbra.

Esto para lo único que sirve es para ejecutar comandos subsiguientes que requieran autenticación. ¿Pero cómo sabe Zimbra si estamos autenticados cuando ejecutemos los siguientes comandos SOAP?

La respuesta es a través de las cookies. Del mismo modo que cuando ingresamos a chequear nuestros mails, Google sabe que ya estamos autenticados. Es porque nuestro navegador web tiene la capacidad de memorizar cookies y presentarlas ante Google en cada GET/POST subsiguiente.

Esta misma habilidad que tienen los navegadores web también la tiene la clase “org.apache.http.impl.client.DefaultHttpClient”, por lo que de forma automágica si ejecutamos más de un GET/POST con la misma instancia de dicha clase, las cookies serán memorizadas y presentadas en cada oportunidad.

7. Basic access authentication

Esta es una de las formas más básicas de exigir autenticación para ejecutar una request HTTP. Consiste en requerir en el header el parámetro “Authentication”. Se puede leer más sobre este sistema en http://en.wikipedia.org/wiki/Basic_access_authentication

Esto con HttpComponents lo logramos con la siguiente línea:

httpPost.setHeader("Authorization", "Basic " +
        Base64.encodeBase64String("usuario:password".getBytes()));

Cabe aclarar que la clase “org.apache.commons.codec.binary.Base64” es parte de la biblioteca “Commons Codec”.

8. Post de un archivo

En este caso deseamos hacer un upload de un archivo a Zimbra. Zimbra cuenta con una interfaz RESTful. También utilizamos Basic access authentication.

DefaultHttpClient httpClient = new DefaultHttpClient();

HttpPost httpPost = new HttpPost(
        "https://zimbramail.midominio.com:7071/home/hhernandez@midominio.com?fmt=zip");

httpPost.setHeader("Authorization", "Basic " +
        Base64.encodeBase64String("usuario:password".getBytes()));

MultipartEntity multipartEntity = new MultipartEntity();

multipartEntity.addPart("zippedMails",
        new FileBody(new File("/tmp/hhernandez.zip")));

httpPost.setEntity(multipartEntity);

HttpResponse httpResponse = httpClient.execute(httpPost);

System.out.println("Status Code " +
        httpResponse.getStatusLine().getStatusCode());

System.out.println(EntityUtils.toString(httpResponse.getEntity()));

9. Logging

Otra de las cosas que me gustan de esta biblioteca, con respecto a otras opciones, es el logging bien implementado, a través de Commons Logging.

Si configuramos el Java Logging, o Log4j, o el sistema de logging que estemos usando, para que loguee a nivel DEBUG con el logger “org.apache.http.wire” podemos ver practicamente todo lo que pasa “a través del cable”, desde que se establece la conexión con el servidor hasta que se cierra. Y si configuramos en DEBUG sólo el logger “org.apache.http.headers” restringimos la visibilidad a sólo los headers http.