viernes, 22 de febrero de 2013

Devolver xml, json o html con RESTEasy

RESTEasy
En esta serie de artículos sobre los servicios web REST hemos visto hasta el momento como hacer un programa sencillo Hola Mundo con RESTEasy y como crear un cliente Java y Javascript de ese servicio web para consumirlo desde javascript en una aplicación web y desde un programa java usando la librería RESTEasy. En un servicio web REST podemos devolver datos en varios formatos, ya sea texto plano, html, json o xml, en esta entrada veremos como devolver datos en los dos últimos y que anotaciones usar para que las cabeceras devueltas en la petición sean las correctas, que ventajas nos puede aporta y que problemas nos puede resolver.

Antes unos pequeños apuntes de porque nos puede interesar devolver json o xml. Unos de los motivos es que a medida que las aplicaciones web están ganando en complejidad estas están tendiendo a delegar en el cliente la parte de visualización en vez de ser el servidor el que devuelva html formateado directamente. Además de evitar cierta carga de proceso en el servidor y delegarla en los clientes ofreciendo una interfaz REST de la apliccación permitimos que terceros desarrollen sus propias aplicaciones y se integren consumiendo los servicios que ofrecemos.

Una vez explicadas algunas ventajas, veamos el ejemplo de como devolver json o xml en el servicio REST, en el siguiente código el servicio HelloWorldResource devolverá los datos de un objeto a través de los métodos getMensajeJSON y getMensajeXML. Los métodos del servicio puede anotarse con @Produces indicando el formato de los datos que se devuelve, en la clase MediaType hay constantes para muchos formatos. Si el formato en el que devolvemos los datos no está definido en la clase MediaType podemos definir uno específico con @Produces("text/html"):

package es.com.blogspot.elblogdepicodev.resteasy;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
@Path("/helloworld")
public interface HelloWorldResource {
@GET
@Path("/saluda")
public String getSaluda();
@GET
@Path("/saluda/{nombre}")
public String getSaludaA(@PathParam("nombre") String nombre);
@GET
@Path("/mensaje/{nombre}")
@Produces(MediaType.APPLICATION_JSON)
public Mensaje getMensajeJSON(@PathParam("nombre") String nombre);
@GET
@Path("/mensaje/{nombre}")
@Produces(MediaType.APPLICATION_XML)
public Mensaje getMensajeXML(@PathParam("nombre") String nombre);
}
package es.com.blogspot.elblogdepicodev.resteasy;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
public class HelloWorldResourceImpl implements HelloWorldResource {
@Override
public String getSaluda() {
return "¡Hola mundo!";
}
@Override
public String getSaludaA(String nombre) {
return MessageFormat.format("¡Hola {0}!", nombre);
}
@Override
public Mensaje getMensajeJSON(String nombre) {
return buildMensaje(nombre);
}
@Override
public Mensaje getMensajeXML(String nombre) {
return buildMensaje(nombre);
}
private Mensaje buildMensaje(String nombre) {
return new Mensaje(nombre, "¡Hola mundo!", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
}
}
La anotación @Produces es muy importante en el servicio REST ya que los métodos getMensajeJSON y getMensajeXML tienen especificada la misma ruta de petición en la etiqueta @Path, ¿como sabe entonces el servicio REST en que formato se han devolver los datos si la URL solicitada es la misma para ambos casos? La respuesta es empleando la información enviada en las cabeceras del protocolo http, más concretamente empleando la cabecera Accept que en este protocolo indica en que formato quiere el cliente los datos. En muchas aplicaciones es habitual encontrarse en que se realizan peticiones al servidor especificando como Accept text/html y resulta que el servidor devuelve los datos en formato json, xml u otro, la aplicación funcionará bien pero lo hace empleando el protocolo http de forma incorrecta, esto no ocurre con los servicios REST que en función de lo que se solicita en la petición así es el formato de los datos devueltos. En la siguiente imagen pude verse la cabecera Accept de la petición enviada por el cliente javascript.


En el ejemplo se devuelve una clase POJO con varios atributos que usa las anotaciones @XmlRootElement, @XmlElement, @XmlAttribute para indicar los datos y la forma de los datos a generar como resultado:

package es.com.blogspot.elblogdepicodev.resteasy;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
@XmlRootElement(name = "mensaje")
public class Mensaje {
private String nombre;
private String mensaje;
private String fecha;
public Mensaje() {
}
public Mensaje(String nombre, String mensaje, String fecha) {
this.nombre = nombre;
this.mensaje = mensaje;
this.fecha = fecha;
}
@XmlElement
public String getNombre() {
return nombre;
}
public void setNombre(String nombre) {
this.nombre = nombre;
}
@XmlElement
public String getMensaje() {
return mensaje;
}
public void setMensaje(String mensaje) {
this.mensaje = mensaje;
}
@XmlAttribute
public String getFecha() {
return fecha;
}
public void setFecha(String fecha) {
this.fecha = fecha;
}
}
view raw Mensaje.java hosted with ❤ by GitHub
Para el cliente java desde el punto de vista del que lo usa la comunicación con el servidor y el formato en el que se transmiten los mensajes es transparente.

package es.com.blogspot.elblogdepicodev.resteasy;
import org.jboss.resteasy.client.ProxyFactory;
import org.jboss.resteasy.plugins.providers.RegisterBuiltin;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
public class HelloWorldResourceClient implements HelloWorldResource {
private HelloWorldResource client;
public HelloWorldResourceClient() {
// Obtener el cliente a partir de la interfaz y de donde está localizado
client = ProxyFactory.create(HelloWorldResource.class, "http://localhost:8080/helloworld-resteasy/rest");
}
@Override
public String getSaluda() {
return client.getSaluda();
}
@Override
public String getSaludaA(String nombre) {
return client.getSaludaA(nombre);
}
@Override
public Mensaje getMensajeJSON(String nombre) {
return client.getMensajeJSON(nombre);
}
@Override
public Mensaje getMensajeXML(String nombre) {
return client.getMensajeXML(nombre);
}
public static void main(String[] args) {
// Inicializacion a realizar una vez por máquina virtual
RegisterBuiltin.register(ResteasyProviderFactory.getInstance());
HelloWorldResourceClient client = new HelloWorldResourceClient();
System.out.println(client.getSaluda());
System.out.println(client.getSaludaA("picodotdev"));
System.out.println(client.getMensajeJSON("picodotdev"));
System.out.println(client.getMensajeXML("picodotdev"));
}
}

En el cliente javascript si usamos el formato json nos será más cómodo tratarlo en el navegador:
<html>
<head>
<title>Ejemplo sencillo de web service con RESTEasy</title>
<script type="text/javascript" src="http://localhost:8080/helloworld-resteasy/rest-jsapi"></script>
</head>
<body>
<script type="text/javascript">
alert(HelloWorldResource.getSaluda());
alert(HelloWorldResource.getSaludaA({nombre:'picodotdev'}));
alert(HelloWorldResource.getMensajeJSON({nombre:'picodotdev'}));
alert(HelloWorldResource.getMensajeXML({nombre:'picodotdev'}));
</script>
</body>
</html>
En las siguientes imágenes las peticiones que se realizan desde el navegador con el cliente javascript, en la columna type se puede ver el tipo de datos devuelto en las peticiones:


Para que el ejemplo funcione deberemos incluir las dependencias org.jboss.resteasy:resteasy-jaxb-provider:2.3.5.Final y org.jboss.resteasy:resteasy-jettison-provider:2.3.5.Final.

...
dependencies {
compile 'org.jboss.resteasy:jaxrs-api:2.3.5.Final'
compile 'org.jboss.resteasy:resteasy-jaxrs:2.3.5.Final'
compile 'org.jboss.resteasy:resteasy-jsapi:2.3.5.Final'
compile 'org.jboss.resteasy:resteasy-jaxb-provider:2.3.5.Final'
compile 'org.jboss.resteasy:resteasy-jettison-provider:2.3.5.Final'
providedCompile 'javax.servlet:servlet-api:2.5'
}
...
view raw build.gradle hosted with ❤ by GitHub
Para finalizar, comentar que la combinación de ofrecer en la aplicación una API REST junto con librerías como Backbone.js que proporcionan un Modelo-Vista-Controlador (MVC) en el navegador accediendo al servidor para consumir el servicio puede dar lugar a aplicaciones más flexibles y fáciles de desarrollar entre otras cosas. Y esa es la tendencia hacia la que están evolucionado las aplicaciones web a medida que el propio lenguaje javascript y los navegadores aumentan sus capacidades.

En el siguiente enlace puedes encontrar el código fuente completo de este ejemplo y probarlo tu mismo en un servidor de aplicaciones como Tomcat. La siguiente entrada será como integrar RESTEasy con un framework de desarrollo web como Apache Tapestry.

Referencia:
http://www.jboss.org/resteasy
http://backbonejs.org
Código fuente completo del ejemplo Hola Mundo con RESTEasy