3. Manejadores de contenidos. Respuestas del servidor y manejo de excepciones.

En la sesión anterior hemos hablado de cómo inyectar información contenida en las cabeceras de las peticiones HTTP, ahora nos detendremos en el cuerpo del mensaje, tanto de la petición como de la respuesta. En el caso de las peticiones, explicaremos el proceso de transformar los datos de entrada en objetos Java, para poder ser procesados por nuestros servicios. Con respecto a las respuestas proporcionadas por nuestros servicios, analizaremos tanto los códigos de respuesta por defecto, como la elaboración de respuestas complejas y manejo de excepciones.

3.1. Proveedores de entidades

JAX-RS define lo que se denominan proveedores de entidades, que son clases que proporcionan servicios de mapeado entre las representaciones del cuerpo del mensaje HTTP y los correspondientes tipos java que utilizaremos en nuestros recursos (parámetros en los métodos, o bien como tipo de la respuesta de los mismos). Las entidades también se conocen con el nombre de "message payload", o simplemente como payload, y representan el contenido del cuerpo del mensaje HTTP.

Providers

El runtime de JAX-RS puede "extenderse" (ampliarse) utilizando clases "proveedoras" (providers) suministradas por nuestra aplicación. Concretamente, JAX-RS nos proporciona un conjunto de interfaces que podemos implementar en nuestra aplicación, creando así dichas clases "proveedoras de entidades" (entity providers). La especificación de JAX-RS define un proveedor como una clase que implementa una o más interfaces JAX-RS (de entre un conjunto determinado) y que pueden anotarse con @provider para ser "descubiertas" de forma automática por el runtime de JAX-RS.

Nuestra aplicación puede proporcionar su propio mapeado entre representaciones (tipos MIME) del mensaje de entrada y tipos Java implementando las interfaces MessageBodyWriter y MessageBodyReader, convirtiéndose así en clases proveedoras de entidades (entity providers). Por ejemplo, podemos tener nuestro propio proveedor de entidades para el formato XML, o JSON, de forma que, utilizando las librerías de java para procesamiento XML o JSON (Java API for XML Processing: JAXP y Java API for JSON Processing: JSON-P), implementemos el serializado/deserializado del cuerpo del mensaje HTTP de entrada cuando éste presente los tipos MIME "application/xml" o "application_json". Las clases que realizan dichos mapeados son clases entity provider.

3.1.1. Interfaz javax.ws.rs.ext.MessageBodyReader

La interfaz MessageBodyReader define el contrato entre el runtime de JAX-RS y los componentes que proporcionan servicios de mapeado desde diferentes representaciones (indicadas como tipos mime) al tipo Java correspondiente. Cualquier clase que quiera proporcionar dicho servicio debe implementar la interfaz MessageBodyReader y debe anotarse con @Provider para poder ser "detectada" de forma automática por el runtime de JAX-RS.

La secuencia lógica de pasos seguidos por una implementación de JAX-RS cuando se mapea el cuerpo de un mensaje HTTP de entrada a un parámetro de un método Java es la siguiente:

  1. Se obtiene el media type de la petición (valor de la cabecera HTTP Content-Type). Si la petición no contiene una cabecera Content-Type se usará application/octet-stream

  2. Se identifica el tipo java del parámetro cuyo valor será mapeado desde el cuerpo del mensaje

  3. Se localiza la clase MessageBodyReader que soporta el media type de la petición y se usa su método readFrom() para mapear el contenido del cuerpo del mensaje HTTP en el tipo Java que corresponda

  4. Si no es posible encontrar el MessageBodyReader adecuado se genera la excepción NotSupportedException, con el código 405

3.1.2. Interfaz javax.ws.rs.ext.MessageBodyWriter

La interfaz MessageBodyWriter define el contrato entre el runtime de JAX-RS y los componentes que proporcionan servicios de mapeado desde un tipo Java a una representación determinada. Cualquier clase que quiera proporcionar dicho servicio debe implementar la interfaz MessageBodyWriter y debe anotarse con @Provider para poder ser "detectada" de forma automática por el runtime de JAX-RS.

La secuencia lógica de pasos seguidos por una implementación de JAX-RS cuando se mapea un valor de retorno de un método del recurso a una entidad del cuerpo de un mensaje HTTP es la siguiente:

  1. Se obtiene el objeto que será mapeado a la entidad del cuerpo del mensaje

  2. Se determina el media type de la respuesta

  3. Se localiza la clase MessageBodyWriter que soporta el objeto que será mapeado a la entidad del cuerpo del mensaje HTTP, y se utiliza su método writeTo() para realizar dicho mapeado

  4. Si no es posible encontrar el MessageBodyWriter adecuado se genera la excepción InternalServerErrorException (que es una subclase de WebApplicationException) con el código 500

3.2. Proveedores de entidad estándar incluidos en JAX-RS

Cualquier implementación de JAX-RS debe incluir un conjunto de implementaciones de MessageBodyReader y MessageBodyWriter de forma predeterminada para ciertas combinaciones de tipos Java y media types.

Table 1. Proveedores de entidades estándar de una implementación JAX-RS
Tipo Java Media Type

byte[]

*/* (Cualquier media type)

java.lang.String

*/* (Cualquier media type)

java.io.InputStream

*/* (Cualquier media type)

java.io.Reader

*/* (Cualquier media type)

java.io.File

*/* (Cualquier media type)

javax.activation.DataSource

*/* (Cualquier media type)

javax.xml.transform.Source

text/xml, application/xml, application/*+xml (tipos basados en xml)

javax.xml.bind.JAXBElement and application-supplied JAXB classes

text/xml, application/xml, application/*+xml (tipos basados en xml)

MultivaluedMap<String,String>

application/x-www-form-urlencoded (Contenido de formularios)

StreamingOutput

*/* (Cualquier media type) (Sólo MessageBodyWriter)

java.lang.Boolean, java.lang.Character, java.lang.Number

text/plain

A continuación comentaremos algunos de estos proveedores de entidades estándar, o "conversores" por defecto, que permiten convertir el cuerpo del mensaje HTTP a objetos Java de diferentes tipos y viceversa.

3.2.1. javax.ws.rs.core.StreamingOutput

StreamingOutput es una interfaz callback que implementamos cuando queremos tratar como un flujo continuo (streaming) el cuerpo de la respuesta. Constituye una alternativa "ligera" al uso de MessageBodyWriter.

public interface StreamingOutput {
  void write(OutputStream output)
     throws IOException, WebApplicationException;
}

Implementamos una instancia de esta interfaz, y la utilizamos como tipo de retorno de nuestros métodos de recursos. Cuando el runtime de JAX-RS está listo para escribir el cuerpo de respuesta del mensaje, se invoca al método write() de la instancia de StreamingOutput. Veamos un ejemplo:

@Path("/miservicio") public class MiServicio {
  @GET
  @Produces("text/plain")
  StreamingOutput get() {
    return new StreamingOutput() {
      public void write(OutputStream output)
            throws IOException, WebApplicationException {
                output.write("hello world".getBytes());
      }
  };
}

Hemos utilizado una clase interna anónima que implementa la interfaz StreamingOutput en lugar de crear una clase pública separada. La razón de utilizar una clase interna, es porque en este caso, al contener tan pocas líneas de código, resulta beneficioso mantener dicha lógica "dentro" del método del recurso JAX-RS, de forma que el código sea más fácil de "seguir". Normalmente no tendremos necesidad de reutilizar la lógica implementada en otros métodos, por lo que no tiene demasiado sentido crear otra clase específica.

¿Y por qué no inyectamos un OutputStream directamente? ¿Por qué necesitamos un objeto callback? La razón es que así dejamos que el runtime de JAX-RS maneje la salida de la manera que quiera. Por ejemplo, por razones de rendimiento, puede ser conveniente que JAX-RS utilice un thread para responder, diferente del thread de petición.

3.2.2. java.io.InputStream, java.io.Reader

Para leer el cuerpo de un mensaje de entrada, podemos utilizar las clases InputStream o Reader. Por ejemplo:

@Path("/")
public class MiServicio {
  @PUT
  @Path("/dato")
  public void modificaDato(InputStream is) {
     byte[] bytes = readFromStream(is);
     String input = new String(bytes);
     System.out.println(input);
  }

  private byte[] readFromStream(InputStream stream)
                           throws IOException {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    byte[] buffer = new byte[1000];
    int wasRead = 0;
    do {
       wasRead = stream.read(buffer);
       if (wasRead > 0) {
          baos.write(buffer, 0, wasRead);
       }
    } while (wasRead > -1);
    return baos.toByteArray();
  }
}

En este caso estamos leyendo bytes a partir de un java.io.InputStream, para convertirlo en una cadena de caracteres que mostramos por pantalla.

En el siguiente ejemplo, creamos un java.io.LineNumberReader a partir de un objeto Reader e imprimimos cada línea del cuerpo del mensaje de entrada:

@PUT
@Path("/maslineas")
public void putMasLineas(Reader reader) {
    LineNumberReader lineReader = new LineNumberReader(reader);
    do{
        String line = lineReader.readLine();
        if (line != null) System.out.println(line);
    } while (line != null);
}

No estamos limitados solamente a utilizar instancias de InputStream y/o Reader para leer el cuerpo de los mensajes de entrada. También podemos devolver dichos objetos como respuesta. Por ejemplo:

@Path("/fichero")
public class FicheroServicio {
  private static final String basePath = "...";

  @GET
  @Path("{rutafichero: .*}")
  @Produces("text/plain")
  public InputStream getFichero(@PathParam("rutafichero")
                                 String path) {
    FileInputStream is = new FileInputStream(basePath + path);
    return is;
  }
}

Aquí estamos inyectando un valor @PathParam para crear una referencia a un fichero real de nuestro disco duro. Creamos una instancia de java.io.FileInputStream a partir del valor de la ruta inyectada como parámetro y la devolvemos como cuerpo de nuestro mensaje de respuesta. La implementación de JAX-RS leerá la respuesta de este stream de entrada y la almacenará en un buffer para posteriormente "escribirla" de forma incremental en el stream de salida de la respuesta. En este caso debemos especificar la anotación @Produces para que la implementación de JAX-RS conozca el valor que debe asignar a la cabecera Content-Type en la respuesta.

3.2.3. java.io.File

Se pueden utilizar instancias de la clase java.io.File para entrada y salida de cualquier MIME-TYPE (especificado en Content-Type y/o Accept, y en las anotaciones @Produces y/o @Consumes) . El siguiente código, por ejemplo, devuelve una referencia a un fichero en nuestro disco:

@Path("/fichero")
public class FicheroServicio {
  private static final String baseRuta = "...";

  @GET
  @Path("{rutafichero: .*}")
  @Produces("text/plain")
  public File getFichero(@PathParam("rutafichero")
                                 String ruta) {
    return new File(baseRuta + ruta);
  }
}

En este caso inyectamos el valor de la ruta del fichero con la anotación @PathParam. A partir de dicha ruta creamos un objeto java.io.File y lo devolvemos como cuerpo del mensaje de respuesta. La implementación JAX-RS "leerá" la información "abriendo" un InputStream basado en esta referencia al fichero y la "escribirá" en un buffer. Posteriormente y de forma incremental, volverá a escribir el contenido del buffer en el stream de salida de la respuesta. Al igual que en el ejemplo anterior, debemos especificar la anotación @Produces para que JAX-RS sepa cómo "rellenar" la cabecera Content-Type de la respuesta.

También podemos inyectar instancias de java.io.File a partir del cuerpo del mensaje de la petición. Por ejemplo:

@POST
@Path("/masdatos")
public void post(File fichero) {
    Reader reader = new Reader(new FileInputStream(fichero));
    LineNumberReader lineReader = new LineNumberReader(reader);
    do{
      String line = lineReader.readLine();
      if (line != null) System.out.println(line);
    } while (line != null);
}

En este caso la implementación de JAX-RS crea un fichero temporal en el disco para la entrada. Lee la información desde el buffer de la red y guarda los bytes leídos en este fichero temporal. En el ejemplo, los datos leídos desde la red están representados por el el objeto File inyectado por el runtime de JAX-RS (recuerda que sólo puede haber un parámetro sin anotaciones en los métodos del recurso y que éste representa el cuerpo del mensaje de la petición HTTP). A continuación, el método post() crea un java.io.FileInputStream a partir del objeto File inyectado. Finalmente utilizamos éste stream de entrada para crear un objeto LineNumberReader y mostrar los datos por la consola.

3.2.4. byte[]

Podemos utilizar un array de bytes como entrada y salida para cualquier tipo especificado como media-type. A continuación mostramos un ejemplo:

@Path("/")
public class MiServicio {
   @GET
   @Produces("text/plain")
   public byte[] get() {
         return "hello world".getBytes();
   }

   @POST
   @Consumes("text/plain")
   public void post(byte[] bytes) {
       System.out.println(new String(bytes)); }
   }
}

Para cualquier método de recurso JAX-RS que devuelva un array de bytes, debemos especificar la anotación @Produces para que JAX-RS sepa qué valor asignar a la cabecera Content-Type.

3.2.5. String, char[]

La mayor parte de formatos en internet están basados en texto. JAX-RS puede convertir cualquier formato basado en texto a un String o a cualquier array de caracteres. Por ejemplo:

@Path("/")
public class MiServicio {
  @GET
  @Produces("application/xml")
  public String get() {
    return "<customer><name>Sergio Garcia</name></customer>";
  }

  @POST
  @Consumes("text/plain")
  public void post(String str) {
    System.out.println(str);
  }
}

Para cualquier método de recurso JAX-RS que devuelva un Sring o un array de caracteres debemos especificar la anotación @Produces para que JAX-RS sepa que valor asignar a la cabecera Content-Type.

3.2.6. MultivaluedMap<String, String> y formularios de entrada

Los formularios HTML son usados habitualmente para enviar datos a servidores web. Los datos del formulario están codificados con el media type application/x-www-form-urlencoded. Ya hemos visto como utilizar la anotación @FormParam para inyectar parámetros individuales de un formulario de las peticiones de entrada. También podremos inyectar una instancia de MultivaluedMap<String, String>, que representa todos los datos del formulario enviado en la petición. Por ejemplo:

@Path("/")
  public class MiServicio {
    @POST @Consumes("application/x-www-form-urlencoded")
    @Produces("application/x-www-form-urlencoded")
    public MultivaluedMap<String,String> post(
                       MultivaluedMap<String, String> form) {
        //el formulario tiene los campos "fieldName1" y "fieldName2"
        System.out.println(form.getFirst("fieldName1"));
        System.out.println(form.getFirst("fieldName2"));
        return form; }
}

En este código, nuestro método post() acepta peticiones POST y recibe un MultivaluedMap<String,String> que contiene todos los datos de nuestro formulario. En este caso también devolvemos una instancia de un formulario como respuesta.

Los datos del formulario pueden representarse en el cuerpo de la petción como pares nombre=valor separados por &. Por ejemplo:

fieldName1=valor%20con%20espacios&fielName2=otroValor

Los espacios en blanco se codifican como %20. No es necesario poner comillas.

3.3. Múltiples representaciones de recursos

Por defecto, un recurso RESTful se produce o consume con el tipo MIME "/". Un recurso RESTful puede restringir los media types que soporta, tanto en la petición como en la respuesta, utilizando las anotaciones @Consumes y @Produces, respectivamente. Estas anotaciones pueden especificarse, como ya hemos visto, a nivel de clase o de método de recurso. Las anotaciones especificadas sobre el método prevalecen sobre las de la clase. La ausencia de estas anotaciones es equivalente a su inclusión con el tipo MIME ("/"), es decir, su ausencia implica que se soporta cualquier tipo.

A continuación mostramos un ejemplo en el que un Pedido puede "producirse" tanto en formato xml como en formato json:

@GET
@Path("{id}")
@Produces({"application/xml", "application/json"})
public Pedido getPedido(@PathParam("id")int id) { . . . }

El método getPedido() puede generar ambas representaciones para el pedido. El tipo exacto de la respuesta viene determinado por la cabecera HTTP Accept de la petición.

Otro ejemplo, en el que pueden "consumirse" varios tipos MIME, puede ser el siguiente:

@POST
@Path("{id}")
@Consumes({"application/xml", "application/json"})
public Pedido addPedido(@PathParam("id")int id) { . . . }

En este caso el formato "consumido" vendrá dado por el valor de la cabecera HTTP Content-Type de la petición.

JAX-RS 2.0 nos permite indicar la preferencia por un media type, en el lado del servidor, utilizando el parámetro qs (quality on service). qs toma valores entre 0.000 y 1.000, e indica la calidad relativa de una representación comparado con el resto de representaciones disponibles. Una representación con un valor de qs de 0.000 nunca será elegido. Una representación sin valor para el parámetro qs se asume que dicho valor es 1.000

Ejemplo:

@POST
@Path("{id}")
@Consumes({"application/xml; qs=0.75", "application/json; qs=1"})
public Pedido addPedido(@PathParam("id")int id) { . . . }

Si un cliente realiza una petición y no manifiesta ninguna preferencia por ninguna representación en particular, o con una cabecera Accept con valor application/*, entonces el servidor seleccionará la representación con el valor de qs más alto (en este caso application/json). Los valores de qs son relativos, y como tales, solamente son comparables con otros valores qs dentro de la misma instancia de la anotación @Consumes (o @Produces).

Los clientes pueden indicar también sus preferencias utilizando otro factor relativo de calidad, en forma de parámetro denominado q. El valor del parámetro q se utiliza para ordenar el conjunto de tipos aceptados. q toma valores entre 0.000 y 1.000 (máxima preferencia). Al igual que antes, Los valores de q son relativos, y como tales, solamente son comparables con otros valores q dentro de la misma cabecera Accept o Content-type.

Las preferencias del servidor (valores de los parámetros qs) sólo se tienen en cuenta si el cliente acepta múltiples media types con el mismo valor de q

Veamos un ejemplo:

@GET
@Path("{id}")
@Produces({"application/xml"; qs=1,
           "application/json"; qs=0.75})
public Pedido getPedido(@PathParam("id")int id) { . . . }

Supongamos que un cliente lanza una petción GET con una valor para la cabecera Accept de application/*; q=0.5, text/html. En este caso, el servidor determina que los tipos MIME application/xml y application/json tienen la misma preferencia por parte del cliente (con valor de 0.5), por lo tanto, el servidor elegirá la representación application/json, ya que tiene un valor de qs mayor.

3.4. Introducción a JAXB

JAXB (Java Architecture for XML Binding) es una especificación Java antigua (JSR 222) y no está definida por JAX-RS. JAXB es un framework de anotaciones que mapea clases Java a XML y esquemas XML. Es extremadamente útil debido a que, en lugar de interactuar con una representación abstracta de un documento XML, podemos trabajar con objetos Java reales que están más cercanos al dominio que estamos modelando. JAX-RS proporciona soporte para JAXB, pero antes de revisar los manejadores de contenidos JAXB incuidos con JAX-RS, veamos una pequeña introducción al framework JAXB.

Como ya hemos dicho, si queremos mapear una clase Java existente a XML, podemos utilizar JAXB, a través de un conjunto de anotaciones. Veámoslo mejor con un ejemplo:

@XmlRootElement(name="cliente")
@XmlAccessorType(XmlAccessType.FIELD)
public class Cliente {
    @XmlAttribute
    protected int id;

    @XmlElement
    protected String nombre;

    public Customer() {}

    public int getId() { return this.id; }
    public void setId(int id) { this.id = id; }

    public String getNombre() { return this.nombre; }
    public void setNombre(String nombre} { this.nombre = nombre; }
}

La anotación @javax.xml.bind.annotation.XmlRootElement se utiliza en clases java para denotar que representan elementos XML (etiqueta XML raíz). En este caso estamos diciendo que la clase Java representa un documento XML que tiene como etiqueta raíz <cliente>. Las clases java anotadas con @XmlRootElement se denomina beans JAXB.

La anotación @javax.xml.bind.annotation.XmlAttribute la hemos asociado al campo id de nuestra clase Cliente. Esta anotación indica que el campo id de la clase debe mapearse como el atributo id del elemento raíz <cliente> del documento XML. La anotación @XmlAttribute tiene un atributo name, de forma que podemos especificar el nombre exacto del atributo XML dentro del documento. Por defecto, tiene el mismo nombre que el campo anotado.

Hemos utilizado la anotación @javax.xml.bind.annotation.XmlElement en el campo nombre de la clase Cliente. Esta anotación indica a JAXB que debe mapearse el campo nombre como el elemento <nombre> anidado en la etiqueta raíz <cliente>. Igual que antes, podemos especificar el nombre concreto del elememto XML. Por defecto toma el mismo nombre que el correspondiente campo anotado.

La anotación @javax.xml.bind.annotation.XmlAccessorType permite controlar la serialización por defecto de los atributos de la clase. Esta anotación sólo puede ser usada conjuntamente con @XmlRootElement (y alguna otra anotación que no mostramos aquí). Hemos usado como valor XmlAccessType.FIELD, lo que significa que por defecto se deben serializar todos los campos (fields) de la clase (estén anotados o no), y las propiedades (properties) de la clase que tengan anotaciones JAXB (a menos que la anotación sea @XMLTransient).

Si alguno de los campos de la clase no tiene anotaciones JAXB asociadas, por defecto se serializarán como elementos (etiquetas) en el documento XML correspondiente. Según la documentación de JAXB, un campo es una variable de instancia no estática (normalmente privada).

Las propiedades de la clase vienen dadas por las combinaciones getter/setter de los atributos de la clase. El código anterior tiene dos propiedades "nombre" (dado por el par getNombre/setNombre_) e "id" (par getId/setId). Normalmente se anotan los métodos getter. Dichas propiedades no están anotadas, por lo que JAXB no las serializará.

Al proceso de serializar (convertir) un objeto Java en un documento XML se le denomina marshalling. El proceso inverso, la conversión de XML a objetos Java se denomina unmarshalling.

Con las anotaciones anteriores, un ejemplo de una instancia de nuestra clase Cliente con un id de 42, y el valor de nombre Pablo Martinez, tendría el siguiente aspecto:

<cliente id="42">
     <nombreCompleto>Pablo Martinez</nombreCompleto>
</cliente>

Observamos que se han serializado los campos (variables de instancia de la clase).

Si no especificamos la anotación @XmlAccessorType, por defecto se utilizará:

@XmlAccessorType(XmlAccessType.PUBLIC_MEMBER)

El valor XmlAccessType.PUBLIC_MEMBER indica a JAXB que se deben serializar todos los campos públicos de la clase, todos los campos anotados, y todas las propiedades (pares getter/setter), a menos que estén anotadas con @XMLTransient.

También podríamos utilizar:

@XmlAccessorType(XmlAccessType.NONE)

En este caso, la anotación XmlAccessType.NONE indica sólo se deben serializar aquellas propiedades y/o campos de la clase que estén anotados.

A continuación indicamos, en forma de tabla, el uso de los diferentes XmlAccessType:

Table 2. Valores utilizados para @XmlAccessorType(), conjuntamente con @XmlRootElement():
Valor Significado

XmlAccessType.PUBLIC_MEMBER

Serialización por defecto si no se especifica @XmlAccessorType(). Serializa las propiedades (pares getter/setter) y campos públicos, a menos que estén anotados con @XMLTransient. Si algún campo no público está anotado, también se serializa.

XmlAccessType.FIELD

Serializa todos los campos (públicos o privados) a menos que estén anotados con @XMLTransient.

XmlAccessType.NONE

Solamente serializa aquellos campos y propiedades que estén anotadas con anotaciones JAXB

XmlAccessType.PROPERTY

Serializa cada par getter/setter, a menos que estén anotados con @XMLTransient. Si algún campo (no público) está anotado también se serializa

Podemos utilizar la anotación @XmlElement para anidar otras clases anotadas con JAXB. Por ejemplo, supongamos que queremos añadir una clase Direccion a nuestra clase Cliente:

@XmlRootElement(name="direccion")
@XmlAccessorType(XmlAccessType.FIELD)
public class Direccion {
  @XmlElement
  protected String calle;

  @XmlElement
  protected String cludad;

  @XmlElement
  protected String codPostal;

  // getters y setters
  ...
}

Simplemente tendríamos que añadir el campo de tipo Direccion a nuestra clase Cliente, de la siguiente forma:

@XmlRootElement(name="cliente")
@XmlAccessorType(XmlAccessType.FIELD)
public class Cliente {
    @XmlAttribute
    protected int id;

    @XmlElement
    protected String nombreCompleto;

    @XmlElement
    protected Direccion direccion;

    public Customer() {}

    // getters y setters
    ...
}

En este caso, una instancia de un Cliente con valores id=56, nombre="Ricardo Lopez", calle="calle del oso, 35", ciudad="Alicante", y código_postal="01010", sería serializado como:

 <cliente id="56">
    <nombre>Ricardo Lopez</nombre>
    <direccion>
       <calle>calle del oso, 35</calle>
       <ciudad>Alicante</ciudad>
       <codPostal>01010</codPostal>
    </direccion>
 </cliente>

Veamos otro ejemplo. Supongamos que tenemos el recurso EstadoResource, con métodos que responden a peticiones http GET y PUT:

@Path("/estado")
public class EstadoResource {

    private static EstadoBean estadoBean = new EstadoBean();

    @GET
    @Produces("application/xml")
    public EstadoBean getEstado() {
        return estadoBean;
    }

    @PUT
    @Consumes("application/xml")
    public void setEstado(EstadoBean estado) {
       this.estadoBean = estado;
    }
}

En este caso, la clase EstadoBean debe utilizar anotaciones JAXB para poder ser convertida automáticamente por el runtime de JAX-RS en un documento XML, y viceversa:

@XmlRootElement(name = "estadoImpresora")
public class EstadoBean {

   public String estado = "Idle";
   public int tonerRestante = 25;
   public List<TareaBean> tareas =
             new ArrayList<TareaBean>();
}

Por defecto, la anotación @XmlRootElement realiza la serialización de la clase EstadoBean en formato xml utilizando los campos públicos y propiedades definidas en la clase (estado, tonerRestante, y tareas). Vemos que el campo tareas, a su vez, es una colección de elementos de tipo TareaBean, que también necesitan ser serializados. A continuación mostramos la implementación de la clase TareaBean.java:

@XmlRootElement(name = "tarea")
public class TareaBean {
   public String nombre;
   public String estado;
   public int paginas;

   public TareaBean() {}; (1)

   public TareaBean(String nombre, String estado,
                 int paginas) {
      this.nombre = nombre;
      this.estado = estado;
      this.paginas = paginas;
   }
}
1 Para serializar la clase, es necesario que la clase tenga un constructor sin parámetros

Si accedemos al servicio anterior, nos devolverá la información sobre el estado de la siguiente forma (resultado devuelto por el método getEstado(), anotado con @GET):

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<estadoImpresora>
    <estado>Idle</estado>
    <tonerRestante>25</tonerRestante>
    <tareas>
        <nombre>texto.doc</nombre>
        <estado>imprimiendo...</estado>
        <paginas>13</paginas>
    </tareas>
    <tareas>
        <nombre>texto2.doc</nombre>
        <estado>en espera...</estado>
        <paginas>5</paginas>
    </tareas>
</estadoImpresora>

Podemos observar que se utiliza el elemento xml <tareas> para representar cada una de las instancias de TareaBean, las cuales forman parte de de la lista del campo tareas de nuestra clase EstadoBean.

Vamos a usar las anotaciones @XmlAttribute y @XmlElement de la siguiente forma:

@XmlRootElement(name="estadoImpresora")
public class EstadoBean {
 
   @XmlAttribute(name="valor")
   public String estado = "Idle";
    
   @XmlAttribute(name="toner")
   public int tonerRestante = 25;
    
   @XmlElement(name="tarea")
   public List<TareaBean> tareas =
             new ArrayList<TareaBean>();
}

En este caso, el XML resultante quedaría de la siguiente forma:

<estadoImpresora valor="Idle" toner="25">
    <tarea>
        <nombre>texto.doc</nombre>
        <estado>imprimiendo...</estado>
        <paginas>13</paginas>
    </tarea>
    <tarea>
        <nombre>texto2.doc</nombre>
        <estado>en espera...</estado>
        <paginas>5</paginas>
    </tarea>
</estadoImpresora>

Si no se indica lo contrario, por defecto se convierten los campos a elementos del XML.

En caso de que los campos no sean públicos, etiquetaremos los getters correspondiente (propiedades de la clase).

Hemos visto que para las listas el nombre que especificamos en @XmlElement se utiliza para nombrar cada elemento de la lista. Si queremos que además se incluya un elemento que envuelva a toda la lista, podemos utilizar la etiqueta @XmlElementWrapper:

@XmlRootElement(name="estadoImpresora")
public class EstadoBean {

   @XmlAttribute(name="valor")
   public String estado = "Idle";

   @XmlAttribute(name="toner")
   public int tonerRestante = 25;

   @XmlElementWrapper(name="tareas")
   @XmlElement(name="tarea")
   public List<TareaBean> tareas =
             new ArrayList<TareaBean>();
}

En este caso tendremos un XML como el que se muestra a continuación:

<estadoImpresora valor="Idle" toner="25">
    <tareas>
        <tarea>
            <nombre>texto.doc</nombre>
            <estado>imprimiendo...</estado>
            <paginas>13</paginas>
        </tarea>
        <tarea>
            <nombre>texto2.doc</nombre>
            <estado>en espera...</estado>
            <paginas>5</paginas>
        </tarea>
    </tareas>
</estadoImpresora>

Para etiquetar una lista también podemos especificar distintos tipos de elemento según el tipo de objeto contenido en la lista. Por ejemplo, supongamos que en el ejemplo anterior la clase TareaBean fuese una clase abstracta que tiene dos posible subclases: TareaSistemaBean y TareaUsuarioBean. Podríamos especificar una etiqueta distinta para cada elemento de la lista según el tipo de objeto del que se trate con la etiqueta @XmlElements, de la siguiente forma:

@XmlElementWrapper(name="tareas")
   @XmlElements({
       @XmlElement(name="usuario",type=TareaUsuarioBean.class},
       @XmlElement(name="sistema",type=TareaSistemaBean.class}})
   public List<TareaBean> tareas =
             new ArrayList<TareaBean>();

De esta forma podríamos tener un XML como el siguiente:

<estadoImpresora valor="Idle" toner="25">
    <tareas>
        <usuario>
            <nombre>texto.doc</nombre>
            <estado>imprimiendo...</estado>
            <paginas>13</paginas>
        </usuario>
        <sistema>
            <nombre>texto2.doc</nombre>
            <estado>en espera...</estado>
            <paginas>5</paginas>
        </sistema>
    </tareas>
</estadoImpresora>

Hemos visto que por defecto se serializan todos los campos. Si queremos excluir alguno de ellos de la serialización, podemos hacerlo anotándolo con @XmlTransient. Como alternativa, podemos cambiar el comportamiento por defecto de la serialización de la clase etiquetándola con @XmlAccessorType. Por ejemplo:

@XmlAccessorType(NONE)
@XmlRootElement(name="estadoImpresora")
public class EstadoBean {
    ...
}

En este último caso, especificando como tipo NONE, no se serializará por defecto ningún campo, sólo aquellos que hayamos anotado explícitamente con @XmlElement o @XmlAttribute. Los campos y propiedades (getters) anotados con estas etiquetas se serializarán siempre. Además de ellos, también podríamos especificar que se serialicen por defecto todos los campos públicos y los getters (PUBLIC_MEMBER), todos los getters (PROPERTY), o todos los campos, ya sean públicos o privados (FIELD). Todos los que se serialicen por defecto, sin especificar ninguna etiqueta, lo harán como elemento.

Por último, si nos interesa que toda la representación del objeto venga dada únicamente por el valor de uno de sus campos, podemos etiquetar dicho campo con @XmlValue.

3.4.1. Clase JAXBContext

Para serializar clases Java a y desde XML, es necesario interactuar con la clase javax.xml.bind.JAXBContext. Esta clase nos permite "inspeccionar" las clases Java para "comprender" la estructura de nuestras clases anotadas. Dichas clases se utilizan como factorías para las interfaces javax.xml.bind.Marshaller, y javax.xml.bind.Unmarshaller. Las instancias de Marshaller se utilizan para crear documentos XML a partir de objetos Java. Las instancias de Unmarshaller se utilizan para crear objetos Java a partir de documentos XML. A continuación mostramos un ejemplo de uso de JAXB para convertir una instancia de la clase Cliente, que hemos definido anteriormente, a formato XML, para posteriormente volver a crear el objeto de tipo Cliente:

Cliente cliente = new Cliente();
cliente.setId(42);
cliente.setNombre("Lucia Arg");  (1)

JAXBContext ctx = JAXBContext.newInstance(Cliente.class); (2)
StringWriter writer = new StringWriter();

ctx.createMarshaller().marshal(cliente, writer);
String modString = writer.toString();   (3)

cliente = (Cliente)ctx.createUnmarshaller().
                unmarshal(new StringReader(modString)); (4)
1 Creamos e inicializamos una instancia de tipo Cliente
2 Inicializamos JAXBContext para que pueda "analizar" la clase `Cliente
3 Utilizamos una instancia de Marshaller para escribir el objeto Cliente como un String de Java (StringWriter es un stream de caracteres que utilizaremos para construir un String)
4 Utilizamos una instancia de Unmarshaller para recrear el objeto Cliente a partir del String que hemos obtenido en el paso anterior

La clase JAXBContext constituye un punto en entrada al API JAXB para los clientes de nuestros servicios RESTful. Proporciona una abstracción para gestionar el enlazado (binding) de información XML/Java, necesaria para implementar las operaciones de marshalling y unmarshalling

  • Unmarshalling: La clase Unmarshaller proporciona a la aplicación cliente la capacidad para convertir datos XML en un "árbol" de objetos Java.

  • Marshalling: La clase Marshaller proporciona a la aplicación cliente la capacidad para convertir un "árbol" con contenidos Java, de nuevo en datos XML.

Una vez que hemos proporcionado una visión general sobre cómo funciona JAXB, vamos a ver cómo se integra con JAX-RS

3.4.2. Manejadores JAX-RS para JAXB

La especificación de JAX-RS indica que cualquier implementación debe soportar de forma automática el proceso de marshalling y unmarshalling de clases anotadas con @XmlRootElement. A continuación mostramos un ejemplo de la implementación de un servicio que hace uso de dichos manejadores. Para ello utilizamos la clase Cliente que hemos anotado previamente con @XmlRootElement y que hemos mostrado en apartados anteriores:

@Path("/clientes")
public class ClienteResource {
  @GET
  @Path("{id}")
  @Produces("application/xml")
  public Cliente getCliente(@PathParam("id") int id) {
     Cliente cli = findCliente(id);
     return cust;
  }

  @POST
  @Consumes("application/xml")
  public void crearCliente(Cliente cli) {
  ...
  }
}

Como podemos ver, una vez que aplicamos las anotaciones JAXB a nuestras clases Java ( en este caso a la clase Cliente), es muy sencillo intercambiar documentos XML entre el cliente y nuestro servicio web. Los manejadores JAXB incluidos en la implementación de JAX-RS gestionarán el marshalling/unmarshalling de cualquier clase con anotaciones JAXB, para los valores de Content-Type application/xml, text/xml, o application/*+xml. Por defecto, también se encargan de la creación e inicialización de instancias JAXBContext. Debido a que la creación de las instancias JAXBContext puede ser "cara", la implementación de JAX-RS normalmente las "guarda" después de la primera inicialización.

3.4.3. JAXB y JSON

JAXB es lo suficientemente flexible como para soportar otros formatos además de XML. Aunque la especificación de JAX-RS no lo requiere, muchas implementaciones de JAX-RS incluyen adaptadores de JAXB para soportar el formato JSON, además de XML. JSON es un formato basado en texto que puede ser interpretado directamente por Javascript. De hecho es el formato de intercambio preferido por las aplicaciones Ajax.

JSON es un formato mucho más simple que XML. Los objetos están entre llaves, {}, y contienen pares de clave/valor separados por comas. Los valores pueden ser cadenas de caracteres, booleanos (true o false), valores numéricos, o arrays de los tipos anteriores.

Supongamos que tenemos la siguiente descripción de un producto en formato XML:

<?xml version="1.0" encoding="UTF-8"?>
<producto>
   <id>1</id>
   <nombre>iPad</nombre>
   <descripcion>Dispositivo móvil</descripcion>
   <precio>500</precio>
</producto>

La representación JSON equivalente sería:

{
  "id":"1",
  "nombre":"iPad",
  "descripcion":"Dispositivo móvil",
  "precio":500
}

El formato JSON asociado, por ejemplo, al siguiente objeto Cliente :

 <cliente id="56">
    <nombre>Ricardo Lopez</nombre>
    <direccion>
       <calle>calle del oso, 35</calle>
       <ciudad>Alicante</ciudad>
       <codPostal>01010</codPostal>
    </direccion>
 </cliente>

quedaría como sigue:

{
  "nombre": "Ricardo Lopez",
  "direccion": { "calle": "calle del oso, 35",
                  "ciudad": "Alicante",
                  "codPostal": "01010"
                }
}

Como vemos en el ejemplo, el formato JSON consiste básicamente en objetos situados entre llaves, los cuales están formados por pares clave/valor, separadas por comas. Cada clave y valor está separado por ":". Los valores pueden cadenas de caracteres, booleanos (true o false), valores numéricos, o vectores de los tipos anteriores (los vectores están delimitados por corchetes).

Podemos añadir el formato application/json, o bien MediaType.APPLICATION_JSON a la anotación @Produces en nuestros métodos de recurso para generar respuestas en formato JSON:

@GET
@Path("/get")
@Produces({"application/xml","application/json"})
public Producto getProducto() { ... }

En este ejemplo, se elegirá el formato JSON en la respuesta si el cliente realiza una petición GET que incluye en la cabecera:

Accept: application/json

El tipo de respuesta es de tipo Producto. En este caso Producto debe ser un bean JAXB, es decir, una clase anotada con @XmlRootElement.

Los métodos de recurso pueden aceptar también datos JSON para clases con anotaciones JAXB:

@POST
@Path("/producto")
@Consumes({"application/xml","application/json"})
public Response crearProducto(Producto prod) { ... }

En este caso el cliente debería incluir la siguiente cabecera cuando realice la petición POST que incluya los datos JSON anteriores en el cuerpo del mensaje:

Content-Type: application/json

Hablaremos con más detalle del formato JSON en una sesión posterior.

Finalmente, y como resumen de lo anterior:

JAXB

Para dar soporte al serializado y deserializado XML se utilizan beans JAXB.Por ejemplo las clases anteriores EstadoBean, TareaBean, Cliente y Producto son ejemplos de beans JAXB. La clase que se serializará como un recurso XML o JSON se tiene que anotar con @XmlRootElement. Si en algún elemento de un recurso se retorna un elemento de esa clase y se etiqueta con los tipos @Produces({MediaType.APPLICATION_XML,Mediatype.APPLICATION_JSON}), éste se serializa automáticamente utilizando el tipo de representación aceptada por el cliente. Se puede consultar el artículo de Lars Vogel para más información.

3.5. Respuestas del servidor

Vamos a explicar cuál es el comportamiento por defecto de los métodos de recursos JAX-RS, en particular veremos cuáles son los códigos de respuesta HTTP por defecto teniendo en cuenta situaciones de éxito, así como de fallo.

Dado que, en ocasiones, vamos a tener que enviar cabeceras de respuesta específicas ante condiciones de error complejas, también vamos a explicar cómo podemos elaborar respuestas complejas utilizando el API JAX-RS.

3.5.1. Códigos de respuesta por defecto

Los códigos de respuesta por defecto se corresponden con el comportamiento indicado en la especificación de la definición de los métodos HTTP 1.1. Vamos a examinar dichos códigos de respuesta con el siguiente ejemplo de recurso JAX-RS:

@Path("/clientes")
public class ClienteResource {

  @Path("{id}")
  @GET
  @Produces("application/xml")
  public Cliente getCliente(@PathParam("id") int id) {...}

  @POST
  @Produces("application/xml")
  @Consumes("application/xml")
  public Cliente crearCliente(Cliente nuevoCli) {...}

  @PUT
  @Path("{id}")
  @Consumes("application/xml")
  public void updateCliente(@PathParam("id") int id, Cliente cli) {...}

  @Path("{id}")
  @DELETE
  public void borrarCliente(@PathParam("id") int id) {...}
}
Respuestas que indican éxito

Los números de código de respuestas HTTP con éxito se sitúan en el rango de 200 a 399:

  • Para los métodos crearCliente() y getCliente() se devolverá una respuesta HTTP con el código "200 OK", si el objeto Cliente que devuelven dichos métodos no es null.

  • Para los métodos crearCliente() y getCliente() se devolverá una respuesta HTTP con el código "204 No Content", si el objeto Cliente que devuelven dichos métodos es null. El código de respuesta 204 no indica una condición de error. Solamente avisa al cliente de que todo ha ido bien, pero que el mensaje de respuesta no contiene nada en el cuerpo de la misma. Según ésto, si un método de un recurso devuelve void, por defecto se devuelve el código de respuesta "204 No content". Este es el caso para los métodos updateCliente(), y borrarCliente() de nuestro ejemplo.

La especificación HTTP es bastante consistente para los métodos PUT, POST, GET y DELETE. Si una respuesta exitosa HTTP contiene información en el cuerpo del mensaje de respuesta, entonces el código de respuesta es "200 OK". Si el cuerpo del mensaje está vacío, entonces se debe devolver "204 No Content".

Respuestas que indican una situación de fallo

Es habitual que las respuestas "fallidas" se programen de forma que se lance una excepción. Lo veremos en un apartado posterior. Aquí comentaremos algunas condiciones de error por defecto.

Los números de código de error de respuesta estándar en HTTP se sitúan en el rango entre 400 y 599. En nuestro ejemplo, si un cliente se equivoca tecleando la URI, y ésta queda por ejemplo como http://…​/cliente, entonces el servidor no encontrará ningún método del recurso que pueda servir dicha petición (la URI correcta sería http://…​/clientes). En este caso, se enviará como respuesta el código "404 Not Found".

Para los métodos getCliente() y crearCliente() de nuestro ejemplo, si el cliente solicita una respuesta con el tipo MIME text/html, entonces la implementación de JAX-RS devolverá automáticamente "406 Not Acceptable", con un mensaje de respuesta con el cuerpo de dicho mensaje vacío. Esta respuesta indica que JAX-RS puede encontrar una ruta de URI relativa, que coincide con la petición, pero no encuentra ningún método del recurso que devuelva la respuesta con ese tipo MIME.

Si el cliente invoca una petición HTTP sobre una URI válida, para la que no se puede encontrar un método de recurso asociado, entonces el runtime de JAX-RS devolverá el código "405 Method Not Allowed". Así, en nuestro ejemplo, si el cliente solicita una operación PUT, GET, o DELETE sobre la URI /clientes, obtendrá como respuesta "405 Method Not Allowed", puesto que POST es el único método HTTP que puede dar soporte a dicha URI. La implementación de JAX-RS también devolverá una cabecera de respuesta Allow con la lista de métodos HTTP que pueden dar soporte a dicha URI. Por lo tanto, si nuestra aplicación cliente realiza la siguiente petición de entrada:

GET /clientes

el servidor devolverá la siguiente respuesta:

HTTP/1.1 405, Method Not Allowed
Allow: POST

3.5.2. Elaboración de respuestas con la clase Response

Como ya hemos visto, por ejemplo para peticiones GET, si todo va bien se estará devolviendo un código de respuesta 200 (Ok), junto con el contenido especificado en el tipo de datos utilizado en cada caso. Si devolvemos void, el código de respuesta será 204 (No Content).

Sin embargo, en ocasiones, el servicio web que estamos diseñando no puede implementarse utilizando el comportamiento por defecto de petición/respuesta inherente a JAX-RS. En estos casos necesitaremos controlar de forma explícita la respuesta que se le envía al cliente (cuerpo del mensaje de la respuesta HTTP). Por ejemplo, cuando creamos un nuevo recurso con POST deberíamos devolver 201 (Created). Para tener control sobre este código nuestros recursos JAX-RS podrán devolver instancias de javax.ws.rs.core.Response:

@GET
@Produces(MediaType.APPLICATION_XML)
public Response getClientes() {
  ClientesBean clientes = obtenerClientes();
  return Response.ok(clientes).build(); (1)
}

@POST
@Consumes(MediaType.APPLICATION_XML)
public Response addCliente(ClienteBean cliente,
                          @Context UriInfo uriInfo) {
  String id = insertarCliente(cliente); (2)
  URI uri = uriInfo
              .getAbsolutePathBuilder()
              .path("{id}")
              .build(id); (3)
  return Response.created(uri).build(); (4)
}
1 Al crear una respuesta con Response, podemos especificar una entidad, que podrá ser un objeto de cualquiera de los tipos vistos anteriormente, y que representa los datos a devolver como contenido. Por ejemplo, cuando indicamos ok(clientes), estamos creando una respuesta con código 200 (Ok) y con el contenido generado por nuestro bean JAXB clientes. Esto será equivalente a haber devuelto directamente ClientesBean como respuesta, pero con la ventaja de que en este caso podemos controlar el código de estado de la respuesta.
2 Insertamos una instancia de ClienteBean en la base de datos, y obtenemos la clave asociada a dicho cliente.
3 En la sesión anterior hemos hablado de la interfaz uriInfo. Es una interfaz inyectable que proporciona acceso a información sobre la URI de la aplicación o la URI de las peticiones recibidas. En este caso, estamos construyendo una nueva URI formada por la ruta absoluta de la petición de entrada http POST, añadiéndole la plantilla "{id}" y finalmente sustituyendo el parámetro de la plantilla por el valor id. Supongamos que la petición POST contiene la uri http://localhost:8080/recursos/clientes. Y que el valor de id es 8. El valor de la variable uri será, por tanto: http://localhost:8080/recursos/clientes/8
4 Devolvemos la respuesta incluyendo la URI anterior en la cabecera HTTP `Location

Veamos con más detalle la clase Response:

public abstract class Response {
  public abstract Object getEntity();  (1)
  public abstract int getStatus();  (2)
  public abstract MultivaluedMap<String, Object> getMetadata();  (3)
  public abstract URI getLocation();  (4)
  public abstract MediaType getMediaType();  (5)
  public abstract void close(); (6)
   ...
}
1 El método getEntity() devuelve el objeto Java correspondiente al cuerpo del mensaje HTTP.
2 El método getStatus() devuelve el código de respuesta HTTP.
3 El método getMetadata() devuelve una instancia de tipo MultivaluedMap con las cabeceras de la respuesta.
4 El método getLocation() devuelve la URI de la cabecera Location de la respuesta.
5 El método getMediaType() devuelve el mediaType del cuerpo de la respuesta
6 El método close() cierra el input stream correspondiente a la entidad asociada del cuerpo del mensaje (en el caso de que esté disponible y "abierto"). También libera cualquier otro recurso asociado con la respuesta (como por ejemplo datos posiblemente almacenados en un buffer)

Estos métodos típicamente serán invocados dese el cliente, tal y como veremos más adelante, cuando expliquemos el API cliente.

Los objetos Response no pueden crearse directamente. Tienen que crearse a partir de instancias de javax.ws.rs.core.Response.ResponseBuilder devueltas por uno de los siguientes métodos estáticos de Response:

public abstract class Response { ...
  public abstract void close() {...}
  public static ResponseBuilder status(Status status) {...}
  public static ResponseBuilder status(int status) {...}
  public static ResponseBuilder ok() {...}
  public static ResponseBuilder ok(Object entity) {...}
  public static ResponseBuilder ok(Object entity, MediaType type) {...}
  public static ResponseBuilder ok(Object entity, String type) {...}
  public static ResponseBuilder ok(Object entity, Variant var) {...}
  public static ResponseBuilder serverError() {...}
  public static ResponseBuilder created(URI location) {...}
  public static ResponseBuilder noContent() {...}
  public static ResponseBuilder notModified() {...}
  public static ResponseBuilder notModified(EntityTag tag) {...}
  public static ResponseBuilder notModified(String tag) {...}
  public static ResponseBuilder seeOther(URI location) {...}
  public static ResponseBuilder temporaryRedirect(URI location) {...}
  public static ResponseBuilder notAcceptable(List<Variant> variants) {...}
  public static ResponseBuilder fromResponse(Response response) {...}
  ...
}

Veamos por ejemplo el método ok():

ResponseBuilder ok(Object entity, MediaType type) {...}

Este método recibe como parámetros un objeto Java que queremos convertir en una respuesta HTTP y el Content-Type de dicha respuesta. Como valor de retorno se obtiene una instancia de tipo ResponseBuilder pre-inicializada con un código de estado de 200 OK.

El método created() devuelve un ResponseBuilder para un recurso creado, y asigna a la cabecera Location de la respuesta el valor de la URI proporionado como parámetro.

La clase ResponseBuilder es una factoría utilizada para crear instancias individuales de tipo Response:

public static abstract class ResponseBuilder {
  public abstract Response build();
  public abstract ResponseBuilder clone();

  public abstract ResponseBuilder status(int status);
  public ResponseBuilder status(Status status) {...}

  public abstract ResponseBuilder entity(Object entity);
  public abstract ResponseBuilder type(MediaType type);
  public abstract ResponseBuilder type(String type);

  public abstract ResponseBuilder variant(Variant variant);
  public abstract ResponseBuilder variants(List<Variant> variants);

  public abstract ResponseBuilder language(String language);
  public abstract ResponseBuilder language(Locale language);

  public abstract ResponseBuilder location(URI location);
  public abstract Response.ResponseBuilder header(String name, Object value)
  public abstract Response.ResponseBuilder link(URI uri, String rel)
  public abstract Response.ResponseBuilder link(String uri, String rel)
  ...
}

Vamos a mostrar un ejemplo sobre cómo crear respuestas utilizando un objeto Response. En este caso el método getLibro() devuelve un String que representa el libro en el que está interesado nuestro cliente:

@Path("/libro")
public class LibroServicio {
  @GET
  @Path("/restfuljava")
  @Produces("text/plain")
  public Response getLibro() {
    String libro = ...; (1)
    ResponseBuilder builder = Response.ok(libro); (2)
    builder.language("fr").header("Some-Header", "some value"); (3)
    return builder.build(); (4)
  }
}
1 Recuperamos los datos del libro solicitado. En este caso vamos a devolver una cadena de caracteres que representa el libro en el que estamos interesados.
2 Inicializamos el cuerpo de la respuesta utilizando el método Response.ok(). El código de estado de ResponseBuilder se inicializa de forma automática con 200.
3 Usamos el método ResponseBuilder.language() para asignar el valor de la cabecera Content-Languaje a francés. También usamos el método ResponseBuilder.header() para asignar un valor concreto a otra cabecera.
4 Finalmente, creamos y devolvemos el objeto Response usando el método ResponseBuilder.build().

Un detalle que es interesante destacar en este código es que no indicamos ningún valor para el Content-Type de la respuesta. Debido a que ya hemos especificado esta información en la anotación @Produces del método, el runtime de JAX-RS devolverá el valor adecuado del media type de la respuesta por nosotros.

Inclusión de cookies en la respuesta

JAX-RS proporciona la clase javax.ws.rs.core.NewCookie, que utilizaremos para crear nuevos valores de cookies y enviarlos en las respuestas.

Para poder incluir las cookies en nuestro objeto Response, primero crearemos las instancias correspondientes de tipo NewCookie las pasaremos al método ResponseBuilder.cookie(). Por ejemplo:

@Path("/myservice")
public class MyService {
  @GET
  public Response get() {
     NewCookie cookie = new NewCookie("nombre", "pepe"); (1)
     ResponseBuilder builder = Response.ok("hola", "text/plain"); (2)
     return builder.cookie(cookie).build();
}
1 Creamos una nueva cookie con el nombre de clave nombre y le asignamos el valor pepe
2 En este caso, al no indicar el tipo mime de la respuesta con la anotación @Produces, necesitamos indicarlo (en este caso como un parámetro del método ok())
El tipo enumerado de códigos de estado

JAX-RS proporciona el tipo enumerado javax.ws.rs.core.Status para representar códigos de respuesta específicos:

public enum Status {
  OK(200, "OK"),
  CREATED(201, "Created"),
  ACCEPTED(202, "Accepted"),
  NO_CONTENT(204, "No Content"),
  MOVED_PERMANENTLY(301, "Moved Permanently"),
  SEE_OTHER(303, "See Other"),
  NOT_MODIFIED(304, "Not Modified"),
  TEMPORARY_REDIRECT(307, "Temporary Redirect"),
  BAD_REQUEST(400, "Bad Request"),
  UNAUTHORIZED(401, "Unauthorized"),
  FORBIDDEN(403, "Forbidden"),
  NOT_FOUND(404, "Not Found"),
  NOT_ACCEPTABLE(406, "Not Acceptable"),
  CONFLICT(409, "Conflict"),
  GONE(410, "Gone"),
  PRECONDITION_FAILED(412, "Precondition Failed"),
  UNSUPPORTED_MEDIA_TYPE(415, "Unsupported Media Type"),
  INTERNAL_SERVER_ERROR(500, "Internal Server Error"),
  NOT_IMPLEMENTED(501, "Not Implemented"),
  SERVICE_UNAVAILABLE(503, "Service Unavailable");
  public enum Family {
     INFORMATIONAL, SUCCESSFUL, REDIRECTION,
     CLIENT_ERROR, SERVER_ERROR, OTHER
  }

  public Family getFamily()
  public int getStatusCode()
  public static Status fromStatusCode(final int statusCode)
}

Cada valor del tipo Status se asocia con una familia específica de códigos de respuesta HTTP. Estas familias se identifican por el enumerado Status.Family:

  • Los códigos en el rango del 100 se consideran informacionales

  • Los códigos en el rango del 200 se consideran exitosos

  • Los códigos en el rango del 300 son códigos con éxito pero dentro de la categoría redirección

  • Los códigos de error pertenecen a los ragos 400 y 500. En el rango de 400 se consideran errores del cliente y en el rango de 500 son errores del servidor

Tanto el método Response.status() como ResponseBuilder.status() pueden aceptar un valor enumerado de tipo Status. Por ejemplo:

@DELETE
Response delete() {
  ...
  return Response.status(Status.GONE).build();
}

En este caso, estamos indicando al cliente que lo que queremos borrar ya no existe (410).

La clase javax.ws.rs.core.GenericEntity

Cuando estamos creando objetos de tipo Response, se nos plantea un problema cuando queremos devolver tipos genéricos, ya que el manejador JAXB necesita extraer la información del tipo parametrizado de la respuesta en tiempo de ejecución. Para estos casos, JAX-RS proporciona la clase javax.ws.rs.core.GenericEntity. Veamos su uso con un ejemplo:

@GET
@Produces("application/xml")
public Response getListaClientes() {
  List<Cliente> list = new ArrayList<Cliente>();
  list.add(new Cliente(...));

  GenericEntity entity =
    new GenericEntity<List<Cliente>>(list){}; (1)

  return Response.ok(entity).build();
}
1 La clase GenericEntity es también una clase genérica. Lo que hacemos es crear una clase anónima que extiende GenericEntity, inicializando la "plantilla" de GenericEntity con el tipo genérico que estemos utilizando.

3.6. Manejadores de excepciones

Vamos a explicar cómo podemos tratar las excepciones en nuestros servicios RESTful.

Los errores pueden enviarse al cliente, bien creando y devolviendo el objeto Response adecuado, o lanzando una excepción. Podemos lanzar cualquier tipo de excepción, tanto las denominadas checked (clases que heredan de java.lang.Exception), como las excepciones unchecked (clases que extienden java.lang.RuntimeException). Las excepciones generadas son manejadas por el runtime de JAX-RS si tenemos registrado un mapper de excepciones. Dichos mappers (o mapeadores) de excepciones pueden convertir una excepción en una respuesta HTTP. Si las excepciones no están gestionadas por un mapper, éstas se propagan y se gestionan por el contenedor (de servlets) en el que se está ejecutando JAX-RS. JAX-RS proporciona también la clase javax.ws.rs.WebApplicationException. Esta excepción puede lanzarse por el código de nuestra aplicación y será procesado automáticamente por JAX-RS sin necesidad de disponer de forma explícita de ningún mapper. Vamos a ver cómo utilizar esta clase.

3.6.1. La clase javax.ws.rs.WebApplicationException

JAX-RS incluye una excepción unchecked que podemos lanzar desde nuestra aplicación RESTful (ver la documentación del http://docs.oracle.com/javaee/7/api/javax/ws/rs/WebApplicationException.html [API]). Esta excepción se puede pre-inicializar con un objeto Response, o con un código de estado particular:

Clase javax.ws.rs.WebApplicationException
public class WebApplicationException extends RuntimeException {
  //Constructores
  public WebApplicationException() {...}
  public WebApplicationException(Response response) {...} (1)
  public WebApplicationException(int status) {...}
  public WebApplicationException(Response.Status status) {...} (2)
  public WebApplicationException(String message) (3)
  public WebApplicationException(Throwable cause) {...}
  public WebApplicationException(Throwable cause, Response response) {...}
  public WebApplicationException(Throwable cause, int status) {...}
  public WebApplicationException(Throwable cause, Response.Status status) {...}

  public Response getResponse() {...]
}
1 Podemos crear una instancia a partir de un objeto Response
2 Creación de una instancia a partir de un código de estado
3 Creación de una instancia a partir de un String. Por defecto se incluye el código de estado 500. El String que se pasa como parámetro se almacena para su posterior recuperación a través del mensaje `getMessage()

Cuando JAX-RS detecta que se ha lanzado la excepción WebApplicationException, la captura y realiza una llamada al método WebApplicationException.getResponse() para obtener un objeto Response que enviará al cliente. Si la aplicación ha inicializado la excepción WebApplicationException con un código de estado, o un objeto Response, dicho código o Response se utilizarán para crear la respuesta HTTP real. En otro caso, la excepción WebApplicationException devolverá el cliente el código de respuesta "500 Internal Server Error".

Por ejemplo, supongamos que tenemos un servicio web que permite a los usuarios solicitar información de nuestros clientes, utilizando una representación XML:

@Path("/clientes")
public class ClienteResource {
  @GET
  @Path("{id}")
  @Produces("application/xml")
  public Cliente getCliente(@PathParam("id") int id) {
     Cliente cli = recuperarCliente(id);
     if (cli == null) {
          throw new WebApplicationException(Response.Status.NOT_FOUND);
     }
     return cli;
  }
}

En este ejemplo, si no encontramos una instancia de Cliente con el id proporcionado, lanzaremos una WebApplicationException que provocará que se le envíe al cliente como código de respuesta "404 Not Found".

3.6.2. Mapeado de excepciones

Normalmente, las aplicaciones tienen que "tratar" con multitud de excepciones lanzadas desde nuestro código de aplicación o por frameworks de terceros. Dejar que el servlet JAX-RS que reside en el servidor maneje la excepción no nos proporciona demasiada flexibilidad. Si capturamos y "redirigimos" todas estas excepciones a WebApplicationException podría resultar bastante tedioso. De forma alternativa, podemos implementar y registrar instancias de javax.ws.rs.ext.ExceptionMapper. Estos objetos "saben" cómo mapear una excepción lanzada por la aplicación a un objeto Response:

public interface ExceptionMapper<E extends Throwable> {
{
   Response toResponse(E exception);
}

Las clases que implementan la interfaz ExceptionMapper<T> son "proveedores de mappings de excepciones" (Exception Mapping Providers) y mapean una excepción runtime o checked a una instancia de Response.

Cuando un recurso JAX-RS lanza una excepción para la que existe un proveedor de mapping de excepciones, éste último se utiliza para devolver una instancia de Response. Esta respuesta resultante se procesa como si hubiese sido generada por el recurso.

Por ejemplo, una excepción bastante utilizada por aplicaciones de bases de datos que utilizan JPA (Java Persistence Api) es javax.persistence.EntityNotFoundException. Esta excepción se lanza cuando JPA no puede encontrar un objeto particular en la base de datos. En lugar de escribir código que maneje dicha excepción de forma explícita, podemos escribir un ExceptionMapper para que gestione dicha excepción por nosotros. Veámos cómo:

@Provider (1)
public class EntityNotFoundMapper
         implements ExceptionMapper<EntityNotFoundException> {  (2)
   public Response toResponse(EntityNotFoundException e) {  (3)
      return Response.status(Response.Status.NOT_FOUND).build();  (4)
   }
}
1 Nuestra implementación de ExceptionMapper debe anotarse con @Provider. Esto le indica al runtime de JAX-RS que esta clase es un componente REST.
2 La clase que implementa ExceptionMapper debe proporcionar el tipo que se quiere parametrizar. JAX-RS utiliza esta información para emparejar las excepciones de tipo EntityNotFoundException con nuestra clase EntityNotFoundMapper
3 El método toResponse() recibe la excepción lanzada y
4 crea un objeto Response que se utilizará para construir la respuesta HTTP

Otro ejemplo es la excepción EJBException, lanzada por aplicaciones que utilizan EJBs.

3.6.3. Jerarquía de excepciones

JAX-RS 2.0 proporciona una jerarquía de excepciones para varias condiciones de error para las peticiones HTTP. La idea es que, en lugar de crear una instancia de WebApplicationException e inicializarla con un código de estado específico, podemos uilizar en su lugar una de las excepciones de la jeraquía (clases que heredan de la clase WebApplicationException). Por ejemplo, podemos cambiar el código anterior que utilizaba WebApplicationException, y en su lugar, usar javax.ws.rs.NotFoundException:

@Path("/clientes")
public class ClienteResource {
  @GET
  @Path("{id}")
  @Produces("application/xml")
  public Cliente getCliente(@PathParam("id") int id) {
     Cliente cli = recuperarCliente(id);
     if (cli == null) {
          throw new NotFoundException();
     }
     return cli;
  }
}

Al igual que el resto de excepciones de la jerarquía, la excepción NotFoundException hereda de WebApplicationException. La siguiente tabla muestra algunas excepciones que podemos utilizar, todas ellas del paquete javax.ws.rs. Las que incluyen un código de estado en el rango de 400, son subclases de ClientErrorException, y como ya hemos indicado, representan errores en las peticiones. Las que presentan un código de estado en el rango de 500, son subclases de ServerErrorException, y representan errores del servidor.

Table 3. Jerarquía de excepciones JAX-RS:
Excepción Código de estado Descripción

BadRequestException

400

Mensaje mal formado

NotAuthorizedException

401

Fallo de autenticación

ForbiddenException

403

Acceso no permitido

NotFoundException

404

No se ha podido encontrar el recurso

NotAllowedException

405

Método HTTP no soportado

NotAcceptableException

406

Media type solicitado por el cliente no soportado (cabecera Accept de la peticion)

NotSupportedException

415

El cliente ha incluido un Media type no soportado (cabecera Content-Type de la peticion)

InternalServerErrorException

500

Error general del servidor

ServiceUnavailableException

503

El servidor está ocupado o temporalmente fuera de servicio

  • La excepción BadRequestException se utiliza cuando el cliente envía algo al servidor que éste no puede interpretar. Ejemplos de escenarios concretos que provocan que el runtime JAX-RS son: cuando una petición PUT o POST contiene un cuerpo del mensaje con un documento XML o JSON mal formado, de forma que falle el "parsing" del documento, o cuando no puede convertir un valor especificado en la cabecera o cookie al tipo deseado. Por ejemplo:

    @HeaderParam("Cabecera-Particular") int cabecera;
    @CookieParam("miCookie") int cookie;

    Si el valor de la cabecera HTTP de la petición o el valor miCookie no puede convertirse en un entero, se lanzará la excepción BadRequestException.

  • La excepción NotAuthorizedException (código 401), se usa cuando queremos escribir nuestros propios protocolos de autorización, y queremos indicar al cliente que éste necesita autenticarse con el servidor.

  • La excepción ForbiddenException (código 403),se usa generalmente cuando el cliente realiza una invocación para la que no tiene permisos de acceso. Esto ocurre normalmente debido a que el cliente no tiene el rol requerido.

  • La excepción NotFoundException (código 404), se usa cuando queremos comunicar al cliente que el recurso que está solicitando no existe. Esta excepción también se generará de forma automática por el runtime de JAX-RS cuando a éste no le sea posible inyectar un valor en un @PathParam, @QueryParam, o @MatrixParam. Al igual que hemos comentado para BadRequestException esto puede ocurrir si intentamos convertir el valor del parámetro a un tipo que no admite esta conversión.

  • La excepción NotAllowedException (código 405), se usa cuando el método HTTP que el cliente está intentando invocar no está soportado por el recurso al que el cliente está accediendo. El runtime de JAX-RS lanza automáticamente esta excepción si no encuentra ningún método que pueda "emparejar" con el método HTTP invocado.

  • La excepción NotAcceptableException (código 406), se usa cuando un cliente está solicitando un formato específico a través de la cabecera Accept. El runtime de JAX-RS lanza automáticamente esta excepción si no hay un método con una anotación @Produces que sea compatible con la cabecera Accept del cliente.

  • La excepción NotSupportedException (código 415),se usa cuando un cliente está enviando una representación que el servidor no "comprende". El runtime de JAX-RS lanza automáticamente esta excepción si no hay un método con una anotación @Consumes que coincida con el valor de la cabecera Content-Type de la petición.

  • La excepción InternalServerErrorException (código 500),es una excepción de propósito general lanzada por el servidor. Si en nuestra aplicación queremos lanzar esta excepción deberíamos hacerlo si se ha producido una condición de error que realmente no "encaja" en ninguna de las situaciones que hemos visto. El runtime de JAX-RS lanza automáticamente esta excepción si falla un MessageBodyWriter o si se lanza alguna excepción desde algún ExceptionMapper.

  • La excepción ServiceUnavailableException (código 503),se usa cuando el servidor está ocupado o temporalmente fuera de servicio. En la mayoría de los casos, es suficiente con que el cliente vuelva a intentar realizar la llamada un tiempo más tarde.

3.7. Ejercicios

3.7.1. Servicio REST ejemplo

Para familiarizarnos con las el uso de diferentes manejadores de contenidos y manejo de excepciones proporcionamos el módulo el MÓDULO s3-ejemplo-rest, con la implementación de un servicio rest sencillo que podéis probar con la herramienta postman.

En el directorio src/main/resources de dicho módulo tenéis un fichero de texto con las instrucciones (instrucciones.txt) para construir, desplegar y probar la aplicación de ejemplo.

3.7.2. Plantillas que se proporcionan

Para esta sesión proporcionamos un proyecto como plantilla con el nombre s3-filmoteca que tendrás utilizar como punto de partida para aplicar lo que hemos aprendido en esta sesión.

Se trata de una implementación parcial para gestionar una filmoteca con información de películas y actores.

La estructura lógica del proyecto proporcionado es la siguiente:

  • Paquete org.expertojava.domain: es la capa que contiene los objetos del dominio de la aplicación. Por simplicidad, no usamos una base de datos real, sino que trabajamos con datos en memoria.

  • Paquete org.expertojava.service: contiene la implementación de los servicios de nuestra aplicación, que serán accedidos desde la capa rest

  • Paquete org.expertojava.rest: constituye la capa rest de nuestra aplicación. Esta capa es cliente de la capa de servicios.

En la carpeta src/main/resources tenéis un fichero de texto (instrucciones.txt) con información detallada sobre el API rest implementado.

3.7.3. Uso de JAXB (0,5 puntos)

Utiliza las anotaciones JAXB oportunas para realizar el serializado de las entidades java a xml y json, de forma que la lista de películas de la filmoteca en formato xml sea:

Petición rest: GET /peliculas
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<peliculas>
   <pelicula duracion="120" estreno="2015-12-08T00:00:00+01:00" id="1">
     <director>Stanley Kubrick</director>
     <titulo>El resplandor</titulo>
   </pelicula>
   <pelicula duracion="155" estreno="2015-18-07T00:00:00+01:00" id="2">
     <director>Director 2</director>
     <titulo>Pelicula 2</titulo>
   </pelicula>
</peliculas>

Por defecto, JAXB serializa los tipos Date con el formato ISO 8061 para los contenidos en el cuerpo de la petión/respuesta. Dicho formato es YYYY-MM-DD (seguido de HH:MM:SS). Por otro lado, si añadimos actores a las películas, éstos deberán mostrarse bajo la etiqueta <actores>, tal y como mostramos en el siguiente ejemplo.

Los datos mostrados para una película en formato xml tienen que presentar el siguiente aspecto:

source,java] .Petición rest: GET /peliculas/1

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<pelicula duracion="120" estreno="2015-12-08T00:00:00+01:00" id="1">
    <actores>
        <actor nombre="Jack Nicholson" personaje="Jack Torrance"/>
    </actores>
    <director>Stanley Kubrick</director>
    <titulo>El resplandor</titulo>
</pelicula>

Puedes comprobar si has hecho bien el ejercicio utilizando Postman.

3.7.4. Uso de manejadores de contenidos y clase Response (0,75 puntos)

Implementa una nueva petición POST que reciba los datos de una nueva película desde un formulario. Recuerda que los datos de un formulario pueden ponerse en el cuerpo de la petición HTTP como pares nombre=valor separados por &. Los espacios en blanco se codifican como %20. No es necesario poner comillas. Por ejemplo:

nombre1=valor%20con%20espacios&nombre2=valor

Se proporciona el fichero index.html con un formulario para utilizarlo como alternativa a postman. Para acceder al formulario usaremos http://localhost:8080/s3-filmoteca/

Modifica las peticiones POST sobre películas y actores de forma que devuelvan en la cabecera Location la URI del nuevo recurso creado. Por ejemplo

http://localhost:8080/s3-filmoteca/peliculas/3

en el caso de una nueva película, y

http://localhost:8080/s3-filmoteca/peliculas/3/actores/Sigourney%20Weaver

si por ejemplo hemos añadido un nuevo actor a la película con id=3.

Modifica los métodos GET para que devuelvan el estado: 204 No Content en los casos en los que la película y/o actor consultado no exista.

Modifica los métodos GET para que devuelvan el estado: 404 Not Found, en los casos en los que las listas de películas y/o actores estén vacías.

Implementa el código para añadir un nuevo actor. Tendrás que obtener la información de la película y nombre del actor de la URI de la petición.

Puedes comprobar si has hecho bien el ejercicio utilizando Postman.

3.7.5. Manejo de excepciones (0,75 puntos)

Modifica el método addPelicula de la capa de servicio (paquete org.expertojava.service) para que lance una excepción de tipo ServiceException con el mensaje "El título de la película no puede ser nulo ni vacío" cuando se intente añadir una película con un título con valor null, o vacío.

El método addPelicula debe lanzar también una excepción de tipo ServiceException con el mensaje "La película ya existe", cuando se intente añadir una película con un título que ya existe.

Modifica el método addActor de la capa de servicio para que lance las excepciones de tipo ServiceException con los mensajes "El título de la película no puede ser nulo ni vacío" cuando se intente añadir un actor a una película cuyo título no existe, o bien el mensaje "EL actor ya existe" si intentamos añadir un actor a una película que ya habíamos añadido previamente.

Implementa un mapper para capturar las excepciones de la capa de servicio, de forma que se devuelva el estado 500, "Internal Server error", y como entidad del cuerpo de la respuesta el mensaje asociado a las excepciones generadas por el servicio ("El título de la película no puede ser nulo ni vacío", "EL actor ya existe", …​). La nueva clase pertencerá a la capa "rest". Puedes ponerle el nombre ServiceExceptionMapper.

Puedes comprobar si has hecho bien el ejercicio utilizando Postman.