2. Anotaciones básicas JAX-RS. El modelo de despliegue.
Ya hemos visto como crear un servicio REST básico. Ahora se trata de analizar con más detalle aspectos fundamentales sobre la implementación de los servicios. Comenzaremos por detallar los usos de la anotación @Path, que es la que nos permite "etiquetar" una clase Java como un recurso REST sobre el que podremos realizar las operaciones que hemos identificado en la sesión anterior. También hablaremos algo más sobre las anotaciones @Produces y @Consumes que ya hemos utilizado para implementar nuestro primer servicio.
En segundo lugar hablaremos sobre la extracción de información de las peticiones HTTP, y cómo podemos inyectar esa información en nuestro código java. Esto nos permitirá servir las peticiones sin tener que escribir demasiado código adicional.
Finalmente, explicaremos más detenidamente cómo configurar el despliegue de nuestra aplicación REST, de forma que sea portable.
2.1. ¿Cómo funciona el enlazado de métodos HTTP?
JAX-RS define cinco anotaciones que se corresponden con operaciones HTTP específicas:
-
@javax.ws.rs.GET
-
@javax.ws.rs.PUT
-
@javax.ws.rs.POST
-
@javax.ws.rs.DELETE
-
@javax.ws.rs.HEAD
En la sesión anterior ya hemos utilizado estas anotaciones para hacer corresponder (enlazar) peticiones HTTP GET con un método Java concreto:
Por ejemplo:
@Path("/clientes")
public class ServicioCliente {
@GET @Produces("application/xml")
public String getTodosLosClientes() { }
}
En este código, la anotación @GET indica al runtime JAX-RS que el método java getTodosLosClientes() atiende peticiones HTTP GET dirigidas a la URI /clientes
Sólamente se puede utilizar una de las anotaciones anteriores para un mismo método. Si se aplica más de uno, se produce un error durante el despliegue de la aplicación |
Es interesante conocer que cada una de estas anotaciones, a su vez, está anotada con otras anotaciones (podríamos llamarlas meta anotaciones). Por ejemplo, la implementación de la anotación @GET tiene este aspecto:
package javax.ws.rs;
import ...;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@HttpMethod(HttpMethod.GET)
public @interface GET {
}
@GET, en sí mismo, no tiene ningún significado especial para el proveedor JAX-RS (runtime
de JAX-RS). Lo que hace que la anotación @GET sea significativo para el runtime de JAX-RS
es el valor de la meta anotación @javax.ws.rs.HttpMethod (en este caso HttpMethod.GET
).
Este valor es el que realmente "decide" que un determinado método Java se "enlace" con
un determinado método HTTP.
¿Cuáles son las implicaciones de ésto? Pues que podemos crear nuevas anotaciones que podemos enlazar a otros métodos HTTP que no sean GET, POST, PUT, DELETE, o HEAD. De esta forma podríamos permitir que diferentes tipos de clientes que hacen uso de la operación HTTP LOCK, puedan ser "atendidos" por nuestro servicio REST (como por ejemplo un cliente WebDAV ).
2.2. La anotación @Path
La anotación @Path identifica la "plantilla" de path para la URI del recurso al que se accede y se puede especificar a nivel de clase o a nivel de método de dicho recurso.
El valor de una anotación @Path es una expresión que denota una URI relativa a la URI base del servidor en el que se despliega el recurso, a la raiz del contexto de la aplicación, y al patrón URL al que responde el runtime de JAX-RS.
Un segmento de la URI es cada una de las subcadenas delimitadas por /
que aparecen
en dicha URI. Por ejemplo, la URI http://ejemplo.clientes.com/clientes/vip/recientes contiene 4
segmentos de ruta: ejemplo.clientes.com, clientes, vip y recientes.
La anotación @Path no es necesario que contenga una ruta que empiece o termine con el carácter |
Para que una clase Java sea identificada como una clase que puede atender peticiones HTTP,
ésta tiene que estar anotada con al menos la expresión: @Path("/")
. Este tipo
de clases se denominan recursos JAX-RS raíz.
Para recibir una petición, un método Java debe tener al menos una anotación de método HTTP, como por ejemplo @javax.ws.rs.GET. Este método no requiere tener ninguna anotación @Path adicional. Por ejemplo:
@Path("/pedidos")
public class PedidoResource {
@GET
public String getTodosLosPedidos() {
...
}
}
Una petición HTTP GET /pedidos se delegará en el método getTodosLosPedidos().
Podemos aplicar también @Path a un método Java. Si hacemos esto, la expresión de la anotación @Path de la clase, se concatenará con la expresión de la anotación @Path del método. Por ejemplo:
@Path("/pedidos")
public class PedidoResource {
@GET
@Path("noPagados")
public String getPedidosNoPagados() {
...
}
}
De esta forma, una petición GET /pedidos/noPagados se delegará en el método getPedidosNoPagados().
Podemos tener anotaciones @Path para cada método, que serán relativos a la ruta indicada en la anotación @Path de la definición de la clase. Por ejemplo, la siguiente clase de recurso sirve peticiones a la URI /pedidos:
@Path("/pedidos")
public class PedidoResource {
@GET
public String getPedidos() {
...
}
}
Si quisiéramos proporcionar el servicio en la URI pedidos/incidencias, por ejemplo, no necesitamos una nueva definición de clase, y podríamos anotar un nuevo método getIncidenciasPedidos() de la siguiente forma:
@Path("/pedidos")
public class PedidoResource {
@GET
public String getPedidos() {...}
@GET
@Path("/incidencias")
public String getIncidenciasPedidos() {...}
}
Ahora tenemos una clase de recurso que gestiona peticiones para /pedidos, y para /pedidos/incidencias/.
2.2.1. Expresiones @Path
El valor de una anotación @Path puede ser una cadena de caracteres, o también puede contener expresiones más complejas si es necesario, nos referiremos a ellas como expresiones @Path
Una expresión @Path puede incluir variables, que se indican entre llaves, que serán sustituidas en tiempo de ejecución dependiendo del valor que se indique en la llamada al recurso. Así, por ejemplo, si tenemos la siguiente anotación:
@GET
@Path("/clientes/{id}")
y el usuario realiza la llamada:
GET http://org.expertojava/contexto/rest/clientes/Pedro
la petición se delegará en el método que esté anotado con las anotaciones anteriores y el valor de {id} será instanciado en tiempo de ejecución a "Pedro".
Para obtener el valor del nombre del cliente, utilizaremos la anotación @PathParam
en los parámetros del método, de la siguiente forma:
@GET
@Path("/clientes/{nombre}")
public String getClientePorNombre(@PathParam("nombre") String nombre) {
...
}
Una expresión @Path puede tener más de una variable, cada una figurará entre llaves. Por ejemplo, si utilizamos la siguiente expresión @Path:
@Path("/{nombre1}/{nombre2}/")
public class MiResource {
...
}
podremos atender peticiones dirigidas a URIs que respondan a la plantilla:
http://org.expertojava/contexto/recursos/{nombre1}/{nombre2}
como por ejemplo:
http://org.expertojava/contexto/recursos/Pedro/Lopez
Las expresiones @Path pueden incluir más de una variable para referenciar un segmento de ruta. Por ejemplo:
@Path("/")
public class ClienteResource {
@GET
@Path("clientes/{apellido1}-{apellido2}")
public String getCliente(@PathParam("apellido1") String ape1,
@PathParam("apellido2") String ape2) {
...
}
}
Una petición del tipo:
GET http://org.expertojava/contexto/clientes/Pedro-Lopez
será procesada por el método getCliente()
Expresiones regulares
Las anotaciones @Path pueden contener expresiones regulares (asociadas a las variables). Por ejemplo, si nuestro método getClienteId() tiene un parámetro de tipo entero, podemos restringir las peticiones para tratar solamente aquellas URIs que contengan dígitos en el segmento de ruta que nos interese:
@Path("/clientes")
public class ClienteResource {
@GET
@Path("{id : \\d+}") //solo soporta dígitos
public String getClienteId(@PathParam("id") int id) {
...
}
}
Si la URI de la petición de entrada no satisface ninguna expresión regular de ninguno de los metodos del recurso, entonces se devolverá el código de error: 404 Not Found
El formato para especificar expresiones regulares para las variables del path es:
{" nombre-variable [ ":" expresion-regular ] "}
El uso de expresiones regulares es opcional. Si no se proporciona una expresión regular, por defecto se admite cualquier carácter. En términos de una expresión regular, la expresión regular por defecto sería:
"[^/]+?"
Por ejemplo, si queremos aceptar solamente nombres que comiencen por una letra, y a continuación puedan contener una letra o un dígito, lo expresaríamos como:
@Path("/clientes")
public class ClienteResource {
@GET
@Path("{nombre : [a-zA-Z][a-zA-Z_0-9]}")
public String getClienteNombre(@PathParam("nombre") string nom) {
...
}
}
De esta forma, la URI /clientes/aaa
no sería válida, la URI /clientes/a9
activaría el método getClienteNombre(), y la URI /clientes/89
activaría
el método getClienteId().
Las expresiones regulares no se limitan a un sólo segmento de la URI. Por ejemplo:
@Path("/clientes")
public class ClienteResource {
@GET
@Path("{id : .+}")
public String getCliente(@PathParam("id") String id) {
...
}
@GET
@Path("{id : .+}/direccion")
public String getDireccion(@PathParam("id") String id) {
...
}
}
La expresión regular .+
indica que están permitidos cualquier número de caracteres.
Así, por ejemplo, la petición GET /clientes/pedro/lopez
podría delegarse en el método
getClientes()
El método getDireccion() tiene asociada una expresión más específica, la cual puede
mapearse con cualquier cadena de caracteres que termine con /direccion. Según ésto,
la petición GET /clientes/pedro/lopez/direccion
podría delegarse en el método
getDireccion().
Reglas de precedencia
En el ejemplo anterior, acabamos de ver que las expresiones @Path para getCliente() y
getDireccion() son ambiguas. Una petición GET /clientes/pedro/lopez/direccion
podría
mapearse con cualquiera de los dos métodos. La especificación JAX-RS define las siguientes
reglas para priorizar el mapeado de expresiones regulares:
-
El primer criterio para ordenar las acciones de mapeado es el número de caracteres literales que contiene la expresión @Path, teniendo prioridad aquellas con un mayor número de caracteres literales. El patrón de la URI para el método getCliente() tiene 10 carácteres literales:
/clientes
. El patrón para el método getDireccion() tiene 19:clientes/direccion
. Por lo tanto se elegiría primero el método getDireccion() -
El segundo criterio es el número de variables en expresiones @Path (por ejemplo {id}, o {id: .+}). Teniendo precedencia las patrones con un mayor número de variables
-
El tercer criterio es el número de variables que tienen asociadas expresiones regulares (también en orden descendente)
A continuación mostramos una lista de expresiones @Path, ordenadas en orden descendente de prioridad:
-
/clientes/{id}/{nombre}/direccion
-
/clientes/{id : .+}/direccion
-
/clientes/{id}/direccion
-
/clientes/{id : .+}
Las expresiones 1..3 se analizarían primero ya que tienen más caracteres literales que la expresión número 4. Si bien las expresiones 1..3 tienen el mismo número de caracteres literales. La expresión 1 se analizaría antes que las otras dos debido a la segunda regla (tiene más variables). Las expresiones 2 y 3 tienen el mismo número de caracteres literales y el mismo número de variables, pero la expresión 2 tiene una variable con una expresión regular asociada.
Estas reglas de ordenación no son perfectas. Es posible que siga habiendo ambigüedades, pero cubren el 90% de los casos. Si el diseño de nuestra aplicación presenta ambigüedades aplicando estas reglas, es bastante probable que hayamos complicado dicho diseño y sería conveniente revisarlo y refactorizar nuestro esquema de URIs.
2.2.2. Parámetros matrix (Matrix parameters)
Los parámetros matrix con pares nombre-valor incluidos como parte de la URI.
Aparecen al final de un segmento de la URI (segmento de ruta) y están delimitados por el
carácter ;
. Por ejemplo:
http://ejemplo.coches.com/seat/ibiza;color=black/2006
En la ruta anterior el parámetro matrix aparece después del segmento de ruta ibiza.
Su nombre es color
y el valor
asociado es black
.
Un parámetro matrix es diferente de lo que denominamos parámetro de consulta (query parameter), ya que los parámetros matrix representan atributos de ciertos segmentos de la URI y se utilizan para propósitos de identificación. Pensemos en ellos como adjetivos. Los parámetros de consulta, por otro lado, siempre aparecen al final de la URI, y siempre pertenecen al recurso "completo" que estemos referenciando.
Los parámetros matrix son ignorados cuando el runtime de JAX-RS realiza el matching de las peticiones de entrada a métodos de recursos REST. De hecho, es "ilegal" incluir parámetros matrix en las expresiones @Path. Por ejemplo:
@Path("/seat")
public class SeatService {
@GET
@Path("/ibiza/{anyo}")
@Produces("image/jpeg")
public Response getIbizaImagen(@PathParam("anyo") String anyo) {
... }
}
Si la petición de entrada es: GET /seat/ibiza;color=black/2009, el método getIbizaImagen() sería elegido por el proveedor de JAX-RS para servir la petición de entrada, y sería invocado. Los parámetros matrix NO se consideran parte del proceso de matching debido a que normalmente son atributos variables de la petición.
2.2.3. Subrecursos (Subresource Locators)
Acabamos de ver la capacidad de JAX-RS para hacer corresponder, de forma estática a través de la anotación @Path, URIs especificadas en la entrada de la petición con métodos Java específicos. JAX-RS también nos permitirá, de forma dinámica servir nosotros mismos las peticiones a través de los denominados subresource locators (localizadores de subrecursos).
Los subresource locators son métodos Java anotados con @Path, pero sin anotaciones @GET, @PUT, … Este tipo de métodos devuelven un objeto, que es, en sí mismo, un servicio JAX-RS que "sabe" cómo servir el resto de la petición. Vamos a describir mejor este concepto con un ejemplo.
Supongamos que queremos extender nuestro servicio que proporciona información sobre los clientes. Disponemos de diferentes bases de datos de clientes según regiones geográficas. Queremos añadir esta información en nuestro esquema de URIs pero desacoplando la búsqueda del servidor de base de datos, de la consulta particular de un cliente en concreto. Añadiremos la información de la zona geográfica en la siguiente expresión @Path:
/clientes/{zona}-db/{clienteId}
A continuación definimos la clase ZonasClienteResource, que delegará en la clase ClienteResource, que ya teníamos definida.
@Path("/clientes")
public class ZonasClienteResource {
@Path("{zona}-db")
public ClienteResource getBaseDeDatos(@PathParam("zona") String db) {
// devuelve una instancia dependiendo del parámetro db
ClienteResource resource = localizaClienteResource(db);
return resource;
}
protected ClienteResource localizaClienteResource(String db) {
...
}
}
La clase ZonasClienteResource es nuestro recurso raíz. Dicha clase no atiende ninguna petición HTTP directamente. Nuestro recurso raíz procesa el segmento de URI que hace referencia a la base de datos en donde buscar a nuestro cliente y devuelve una instancia de dicha base de datos (o más propiamente dicho, del objeto con en que accederemos a dicha base de datos). El "proveedor" de JAX-RS utiliza dicha instancia para "servir" el resto de la petición:
public class ClienteResource {
private Map<Integer, Cliente> clienteDB =
new ConcurrentHashMap<Integer, Cliente>();
private AtomicInteger idContador = new AtomicInteger();
public ClienteResource(Map<Integer, Cliente> clienteDB) {
this.clienteDB = clienteDB;
}
@POST
@Consumes("application/xml")
public Response crearCliente(InputStream is) { ... }
@GET
@Path("{id}")
@Produces("application/xml")
public Cliente recuperarClienteId(@PathParam("id") int id) { ... }
@PUT
@Path("{id}")
@Consumes("application/xml")
public void modificarCliente(@PathParam("id") int id, Cliente cli) { ... }
}
Si un usuario envía la petición GET /clientes/norteamerica-db/333, el proveedor JAX-RS primero realizará un matching de la expresión sobre el método ZonasClienteResource.getBaseDeDatos(). A continuación procesará el resto de la petición ("/333") a través del método ClienteResource.recuperarClienteId().
Podemos observar que la nueva clase ClienteResource, además de tener un nuevo constructor, ya no está anotada con @Path. Esto implica que ya no es un recurso de nuestro sistema; es un subrecurso y no debe ser registrada en el runtime de JAX-RS a través de la clase Application (como veremos más adelante).
Veamos otro ejemplo. Supongamos que tenemos un conjunto de alumnos, del que podemos obtener el listado completo de alumnos y añadir nuevos alumnos, pero además queremos que cada alumno individual pueda consultarse, modificarse o borrarse. Una forma sencilla de tratar esto es dividir el código en un recurso (lista de alumnos) y un subrecurso (alumno individual) de la siguiente forma:
@Path("/alumnos")
public class AlumnosResource {
@Context
UriInfo uriInfo;
@GET
@Produces({MediaType.APPLICATION_XML,
MediaType.APPLICATION_JSON})
public List<AlumnoBean> getAlumnos() {
return FactoriaDaos.getAlumnoDao().getAlumnos();
}
@POST
@Consumes(MediaType.APPLICATION_JSON)
public void addAlumno(AlumnoBean alumno) throws IOException {
String dni = FactoriaDaos.getAlumnoDao().addAlumno(alumno);
URI uri =
uriInfo.getAbsolutePathBuilder().path("{dni}").build(dni);
Response.created(uri).build();
}
@Path("{alumno}")
public AlumnoResource getAlumno(
@PathParam("alumno") String dni) {
return new AlumnoResource(uriInfo, dni);
}
}
Vemos que en este recurso inyectamos información sobre la URI solicitada como
variable de instancia (utilizando la anotación @Context
, de la que hablaremos
más adelante). Para el conjunto de alumnos ofrecemos dos operaciones:
obtener la lista de alumnos, y añadir un nuevo alumno a la lista, la cual devuelve
como respuesta la URI que nos da acceso al recurso que acabamos de añadir.
Sin embargo, lo más destacable es el último método. Éste se ejecutará cuando
añadamos a la ruta el identificador de un alumno (por ejemplo /alumnos/15
).
En este caso lo que hace es devolver un subrecurso (AlumnoResource),
para así tratar un alumno individual (destacamos que el nombre está en singular,
para distinguirlo del recurso anterior que representa el conjunto).
Cuando hacemos esto estamos delegando en el nuevo Recurso para tratar la petición.
public class AlumnoResource {
UriInfo uriInfo;
String dni;
public AlumnoResource(UriInfo uriInfo, String dni) {
this.uriInfo = uriInfo;
this.dni = dni;
}
@GET
@Produces({MediaType.APPLICATION_XML,MediaType.APPLICATION_JSON})
public AlumnoBean getAlumno() {
AlumnoBean alumno =
FactoriaDaos.getAlumnoDao().getAlumno(dni);
if(alumno==null)
throw new WebApplicationException(Status.NOT_FOUND);
return alumno;
}
@PUT
@Consumes(MediaType.APPLICATION_XML)
public Response setAlumno(AlumnoBean alumno) {
// El DNI del alumno debe coincidir con el de la URI
alumno.setDni(dni);
if(FactoriaDaos.getAlumnoDao().getAlumno(dni) != null) {
FactoriaDaos.getAlumnoDao().updateAlumno(alumno);
return Response.noContent().build();
} else {
FactoriaDaos.getAlumnoDao().addAlumno(alumno);
return
Response.created(uriInfo.getAbsolutePath()).build();
}
}
@DELETE
public void deleteAlumno() {
FactoriaDaos.getAlumnoDao().deleteAlumno(dni);
}
}
Este recurso ya no es un recurso raíz mapeado a una ruta determinada (podemos ver que la clase no lleva la anotación @Path), sino que es creado desde otro recurso. Es, por lo tanto, un subrecurso.
Como ya hemos visto, los subrecursos nos permiten simplificar la forma de trabajar con conjuntos de recursos, definiendo en un único método la ruta de acceso a un recurso individual, en lugar de tenerlo que hacer de forma independiente para cada operación.
Además, este diseño modular de los recursos nos va a permitir reutilizar determinados recursos dentro de otros. Por ejemplo, dentro del recurso de un alumno podríamos ver la lista de asignaturas en las que se ha matriculado, y reutilizar el subrecurso encargado de acceder a las asignaturas para poder acceder a sus datos a partir del recurso del alumno. No deberemos abusar de esta característica, ya que si creamos relaciones cíclicas perdemos la característica deseable de los servicios REST de que cada recurso está asignado a una única URI.
En un subrecurso NO podemos inyectar objetos de contexto mediante la anotación
|
Carácter dinámico del "dispatching" de peticiones
En los ejemplos anteriores hemos ilustrado el concepto de subresource locator, aunque no hemos mostrado completamente su carácter dinámico. Así, si volvemos al primero de ellos, el método ZonasClienteResource.getBaseDeDatos() puede devolver cualquier instancia de cualquier clase. En tiempo de ejecución, el proveedor JAX-RS "buscará el interior" de esta instancia métodos de recurso que puedan gestionar la petición.
Supongamos que tenemos dos bases de datos de clientes con diferentes tipos de identificadores. Una de ellas utiliza una clave numérica. La otra utiliza una clave formada por el nombre y apellidos. Necesitamos tener dos clases diferentes para extraer la información adecuada de la URI de la petición. Cambiaremos la implementación de la siguiente forma:
@Path("/clientes")
public class ZonasClienteResourceResource {
protected ClienteResource europa = new ClienteResource();
protected OtraClaveClienteResource norteamerica =
new OtraClaveClienteResource();
@Path("{zona}-db")
public Object getBaseDeDatos(@PathParam("zona") String db) {
if (db.equals("europa")) {
return europa;
}
else if (db.equals("norteamerica")) {
return northamerica; }
else return null; }
}
En lugar de devolver una instancia de ClienteResource, el método getBaseDeDatos()
devuelve una instancia de java.lang.Object
. JAX-RS analizará la instancia
devuelta para ver cómo procesar el resto de la petición.
Ahora, si un usuario envía la petición GET /clientes/europa-db/333, se utilizará la clase ClienteResource para servir el resto de la petición. Si la petición es GET /clientes/norteamerica-db/john-smith utilizaremos el nuevo subrecurso OtraClaveClienteResource:
public class OtraClaveClienteResource {
private Map<String, Cliente> clienteDB =
new ConcurrentHashMap<String, Cliente>();
@GET
@Path("{nombre}-{apellidos}")
@Produces("application/xml")
public Cliente getCliente(@PathParam("nombre") String nombre,
@PathParam("apellidos") String apelllidos) {
...
}
@PUT
@Path("{nombre}-{apellidos}")
@Consumes("application/xml")
public void actualizaCliente()@PathParam("nombre") String nombre,
@PathParam("apellidos") String apelllidos,
Cliente cli) {
...
}
}
2.3. Usos de las anotaciones @Produces y @Consumes
La información enviada a un recurso y posteriormente devuelta al cliente que realizó
la petición se especifica con la cabecera HTTP Media-Type
, tanto en la petición
como en la respuesta. Como ya hemos visto, podemos especificar que representaciones de
los recursos (valor de Media_Type) son capaces de aceptar y/o producir nuestros
servicios mediante las siguientes anotaciones:
-
javax.ws.rs.Consumes
-
javax.ws.rs.Produces
La ausencia de dichas anotaciones es equivalente a incluirlas con el valor de media type
/
, es decir, su ausencia implica que se soporta (acepta) cualquier tipo de representación.
2.3.1. Anotación @Consumes
Esta anotación funciona conjuntamente con @POST y @PUT. Le indica al framework (librerías JAX-RS)
a qué método se debe delegar la petición de entrada. Específicamente, el cliente
fija la cabecera HTTP Content-Type
y el framework delega la petición al correspondiente
método capaz de manejar dicho contenido. Un ejemplo de anotación con @PUT es la
siguiente:
@Path("/pedidos")
public class PedidoResource {
@PUT
@Consumes("application/xml")
public void modificarPedido(Pedido representation) { }
}
Si @Consumes se aplica a la clase, por defecto los métodos correspondientes aceptan los tipos especificados de tipo MIME. Si se aplica a nivel de método, se ignora cualquier anotación @Consumes a nivel de clase para dicho método.
En este ejemplo, le estamos indicando al framework que el método modificarPedido()
acepta un recurso cuya representación (tipo MIME) es "application/xml"
(y que se almacenará
en la variable representation, hablaremos de ello en la siguiente sesión). Por lo tanto, un cliente que se conecte al
servicio web a través de la URI /pedidos debe enviar una petición HTTP PUT
conteniendo el valor de application/xml
como tipo MIME de la cabecera HTTP Content-Type
,
y el cuerpo (body) del mensaje HTTP debe ser, por tanto, un documento xml válido.
Si no hay métodos de recurso que puedan responder al tipo MIME solicitado (tipo MIME especificado en la anotación @Consumes del servicio), se le devolverá al cliente un código HTTP 415 ("Unsupported Media Type"). Si el método que consume la representación indicada como tipo MIME no devuelve ninguna representación, se enviará un el código HTTP 204 ("No content"). A continuación mostramos un ejemplo en el que sucede ésto:
@POST
@Consumes("application/xml")
public void creaPedido(Pedido pedido) {
// Crea y almacena un nuevo _Pedido_
}
Podemos ver que el método "consume" una representación en texto plano, pero devuelve void, es decir, no devuelve ninguna representación. En este caso, se envía el código de estado HTTP 204 No content en la respuesta.
Un recurso puede aceptar diferentes tipos de "entradas". Así, podemos utilizar la anotación @PUT con más de un método para gestionar las repuestas con tipos MIME diferentes. Por ejemplo, podríamos tener un método para aceptar estructuras XML, y otro para aceptar estructuras JSON.
@Path("/pedidos")
public class PedidoResource {
@PUT
@Consumes("application/xml")
public void modificarPedidoXML(InputStream pedido) { }
@PUT
@Consumes("application/json")
public void modificarPedidoJson(InputStream pedido) { }
}
2.3.2. Anotación @Produces
Esta anotación funciona conjuntamente con @GET, @POST y @PUT. Indica al framework qué tipo de representación se envía de vuelta al cliente.
De forma más específica, el cliente envía una petición HTTP junto con una cabecera HTTP Accept que se mapea directamente con el Content-Type que el método produce. Por lo tanto, si el valor de la cabecera Accept HTTP es application/xml, el método que gestiona la petición devuelve un stream de tipo MIME application/xml. Esta anotación también puede utilizarse en más de un método en la misma clase de recurso. Un ejemplo que devuelve representaciones XML y JSON sería el siguiente:
@Path("/pedidos")
public class PedidoResource {
@GET
@Produces("application/xml")
public String getPedidoXml() { }
@GET
@Produces("application/json")
public String getPedidoJson() { }
}
Si un cliente solicita una petición a una URI con un tipo MIME no soportado por el recurso, el framework JAX-RS lanza la excepción adecuada, concretamente el runtime de JAX-RS envía de vuelta un error HTTP 406 Not acceptable |
Se puede declarar más de un tipo en la misma declaración @Produces, como por ejemplo:
@Produces({"application/xml", "application/json"})
public String getPedidosXmlOJson() {
...
}
El método getPedidosXmlOJson() será invocado si cualquiera de los dos tipos MIME especificados en la anotación @Produces son aceptables (la cabecera Accept de la petición HTTP indica qué representación es aceptable). Si ambas representaciones son igualmente aceptables, se elegirá la primera.
En lugar de especificar los tipos MIME como cadenas de texto en @Consumes y @Produces,
podemos utilizar las constantes definidas en la clase javax.ws.rs.core.MediaType,
como por ejemplo |
2.4. Inyección de parámetros JAX-RS
Buena parte del "trabajo" de JAX-RS es el "extraer" información de una petición HTTP e inyectarla en un método Java. Podemos estar interesados en un fragmento de la URI de entrada, en los parámetros de petición,… El cliente también podría enviar información en las cabeceras de la petición. A continuación indicamos una lista con algunas de las anotaciones que podemos utilizar para inyectar información de las peticiones HTTP.
-
@javax.ws.rs.PathParam
-
@javax.ws.rs.MatrixParam
-
@javax.ws.rs.QueryParam
-
@javax.ws.rs.FormParam
-
@javax.ws.rs.HeaderParam
-
@javax.ws.rs.Context
-
@javax.ws.rs.BeanParam
Habitualmente, estas anotaciones se utilizan en los parámetros de un método de recurso JAX-RX. Cuando el proveedor de JAX-RS recibe una petición HTTP, busca un método Java que pueda servir dicha petición. Si el método Java tiene parámetros anotados con alguna de estas anotaciones, extraerá la información de la petición HTTP y la "pasará" como un parámetro cuando se invoque el método.
2.4.1. @javax.ws.rs.PathParam
Ya la hemos utilizado en la sesión anterior. @PathParam nos permite inyectar el valor de los parámetros de la URI definidos en expresiones @Path. Recordemos el ejemplo:
@Path("/clientes")
public class ClienteResource {
...
@GET
@Path("{id}")
@Produces("application/xml")
public Cliente recuperarClienteId(@PathParam("id") int id) {
...
}
}
Podemos referenciar más de un parámetro en el path de la URI en nuestros método java. Por ejemplo, supongamos que estamos utilizando el nombre y apellidos para identificar a un cliente en nuestra clase de recurso:
@Path("/clientes")
public class ClienteResource {
...
@GET
@Path("{nombre}-{apellidos}")
@Produces("application/xml")
public Cliente recuperarClienteId(@PathParam("nombre") String nom,
@PathParam("apellidos") String ape) {
...
}
}
En ocasiones, un paramétro de path de la URI puede repetirse en diferentes expresiones @Path que conforman el patrón de matching completo para un método de un recurso (por ejemplo puede repetirse en la expresión @Path de la clase y de un método). En estos casos, la anotación @PathParam siempre referencia el parámetro path final. Así, en el siguiente código:
@Path("/clientes/{id}")
public class ClienteResource {
...
@GET
@Path("/direccion/{id}")
@Produces("text/plain")
public String getDireccion(@PathParam("id") String direccionId) {
...
}
}
Si nuestra petición HTTP es: GET /clientes/123/direccion/456, el parámetro direccionId
del
método getDireccion() tendría el valor inyectado de "456".
2.4.2. Interfaz UriInfo
Podemos disponer, además, de un API más general para consultar y extraer información sobre las
peticiones URI de entrada. Se trata de la interfaz javax.ws.rs.core.UriInfo
:
public interface UriInfo {
public java.net.URI getAbsolutePath();
public UriBuilder getAbsolutePathBuilder();
public java.net.URI getBaseUri();
public UriBuilder getBaseUriBuilder();
public String getPath();
public List<PathSegment> getPathSegments();
public MultivaluedMap<String, String> getPathParameters();
...
}
Los métodos getAbsolutePathBuilder() y getAbsolutePath() devuelven la ruta absoluta de la petición HTTP en forma de UriBuilder y URI respectivamente.
Los métodos getBaseUri() y getBaseUriBuilder() devuelven la ruta "base" de la aplicación (ruta raiz de nuestros servicios rest) en forma de UriBuilder y URI respectivamente.
El método UriInfo.getPath() permite obtener la ruta relativa de nuestros servicios REST utilizada para realizar el matching con nuestra petición de entrada (es la ruta de la petición actual relativa a la ruta base de la petición rest)
El método UriInfo.getPathSegments() "divide" la
ruta relativa de nuestro servicio REST en una serie de objetos PathSegment (segmentos de ruta,
delimitados por /
).
El método UriInfo.getPathParameters() devuelve un objeto de tipo MultivaluedMap con todos los parámetros del path definidos en todas las expresiones @Path de nuestra petición rest.
Por ejemplo, si la ruta de nuestra petción http es: http://localhost:8080/contexto/rest/clientes/2 (siendo "contexto" la ruta raíz del war desplegado, y "rest" la ruta de servicio de jax-rs):
-
la ruta absoluta (método getAbsolutePath()) sería http://localhost:8080/contexto/rest/clientes/2
-
la ruta base (método getBaseUri) sería http://localhost:8080/contexto/rest/
-
la ruta relativa a la ruta base (método getPath()) sería /clientes/2
-
el número de segmentos de la petición rest (método getPathSegments()) serían 2: "clientes" y "2"
Podemos inyectar una instancia de la interfaz UriInfo
utilizando la anotación @javax.ws.rs.core.Context
.
A continuación mostramos un ejemplo:
@Path("/coches/{marca}")
public class CarResource {
@GET
@Path("/{modelo}/{anyo}")
@Produces("image/jpeg")
public Response getImagen(@Context UriInfo info) {
String fabricado = info.getPathParameters().getFirst("marca");
PathSegment modelo = info.getPathSegments().get(2);
String color = modelo.getMatrixParameteres().getFirst("color");
...
}
}
En este ejemplo, inyectamos una instancia de UriInfo como parámetro del método getImagen(). A continuación hacemos uso de dicha instancia para extraer información de la URI.
Recuerda que también podríamos inyectar una instancia de UriInfo en una variable de instancia de la clase raíz de nuestro recurso. |
El método CarResource.getImagen() utiliza la interfaz javax.ws.rs.core.PathSegment
que,
como ya hemos indicado, representa un segmento de ruta.
package javax.ws.rs.core;
public interface PathSegment {
String getPath();
MultivaluedMap<String, String> getMatrixParameters();
}
El método PathSegment.getPath() devuelve el valor de la cadena de caracteres del segmento de ruta actual, sin considerar nigún parámetro matrix que pudiese contener.
El método PathSegment.getMatrixParameters() devuelve un "mapa" con todos los parámetros matrix aplicados a un segmento de ruta.
Supongamos que realizamos la siguiente petición http para el código anterior (clase CarResource):
GET /coches/seat/leon;color=rojo/2015
Esta petición es delegada en el método ClarResource.getImagen(). La ruta contiene 4 segmentos: coches, seat, leon y 2015. La variable _modelo tomará el valor leon y la variable color se instanciará con el valor rojo.
2.4.3. @javax.ws.rs.MatrixParam
La especificación JAX-RS nos permite inyectar una matriz de valores de parámetros
a través de la anotación javax.ws.rs.MatrixParam
:
@Path("/coches/{marca}")
public class CarResource {
@GET
@Path("/{modelo}/{anyo}")
@Produces("image/jpeg")
public Response getImagen(@PathParam("marca") String marca
@PathParam("modelo") String modelo
@MatrixParam("color") String color) {
... }
}
El uso de la anotación @MatrixParam
simplifica nuestro código y lo hace algo más
legible. Si, por ejemplo, la petición de entrada es:
GET /coches/seat/ibiza;color=black/2009
entonces el parámetro color del método CarResource.getImagen() tomaría el valor black.
2.4.4. @javax.ws.rs.QueryParam
La anotación @javax.ws.rs.QueryParam
nos permite inyectar parámetros de consulta
(query parameters) de la
URI en los valores de los parámetros de los métodos java de nuestros recursos. Por ejemplo,
supongamos que queremos consultar información de nuestros clientes y queremos recuperar
un subconjunto de clientes de nuestra base de datos. Nuestra URI de petición podría ser
algo así:
GET /clientes?inicio=0&total=10
El parámetro de consulta inicio
representa el índice (o posición) del primer cliente que
queremos consultar, y el parámetro total
representa cuántos clientes en total queremos
obtener como respuesta. Una implementación del servicio RESTful podría contener el
siguiente código:
@Path("/clientes")
public class ClienteResource {
@GET
@Produces("application/xml")
public String getClientes(@QueryParam("inicio") int inicio,
@QueryParam("total") int total)
... }
}
En este ejemplo, el parámetro inicio
tomaría el valor 0
, y el parámetro total
tomaría el valor 10
(JAX-RS convierte automáticamente las cadenas de caracteres
de los parámetros de consulta en enteros).
2.4.5. @javax.ws.rs.FormParam
La anotación @javax.ws.rs.FormParam
se utiliza para acceder al cuerpo del mensaje
de la petición HTTP de entrada, cuyo valor de Content-Type es application/x-www-form-urlencoded.
Es decir, se utiliza para acceder a entradas individuales de un formulario HTML. Por ejemplo,
supongamos que para registrar a nuevos clientes en el sistema tenemos que rellenar el
siguiente formulario:
<FORM action="http://ejemplo.com/clientes" method="post">
<P>
Nombre: <INPUT type="text" name="nombre"><BR>
Apellido: <INPUT type="text" name="apellido"><BR>
<INPUT type="submit" value="Send">
</P>
</FORM>
La ejecución de este código, inyectará los valores del formulario como parámetros de nuestro método Java que representa el servicio, de la siguiente forma:
@Path("/clientes")
public class ClienteResource {
@POST
public void crearCliente(@FormParam("nombre") String nom,
@FormParam("apellido") String ape) {
... }
}
Aquí estamos inyectando los valores de nombre
y apellidos
del formulario HTML en
los parámetors nom
y ape
del método java crearCliente(). Los datos del formulario
"viajan" a través de la red codificados como URL-encoded. Cuando se utiliza
la anotación @FormParam, JAX-RS decodifica de forma automática las entradas del fomulario
antes de inyectar sus valores.
Así, por ejemplo, si tecleamos los valores Maria Luisa y_Perlado_, como valores en los campos de texto nombre y apellido del formulario, el cuerpo de nuestro mensaje HTTP será nombre=Maria%20Luisa;apellido=Perlado. Este mensaje será recibido por nuestro método, que extraerá los valores correspondientes y los instanciará en los parámetros nom, y ape del método _ClienteResource.crearCliente().
2.4.6. @javax.ws.rs.HeaderParam
La anotación @javax.ws.rs.HeaderParam
se utiliza para inyectar valores de las cabeceras de
las peticiones HTTP. Por ejemplo, si estamos interesados en la página web que nos ha
referenciado o enlazado con nuestro servicio web, podríamos acceder a la cabecera HTTP
Referer utilizando la anotación @HeaderParam, de la siguiente forma:
@Path("/miservicio")
public class MiServicio {
@GET
@Produces("text/html")
public String get(@HeaderParam("Referer") String referer) {
... }
}
De forma alternativa, podemos acceder de forma programativa a todas las cabeceras de
la petición de entrada, utilizando la interfaz javax.ws.rs.core.HttpHeaders
.
public interface HttpHeaders {
public List<String> getRequestHeader(String name);
public MultivaluedMap<String, String> getRequestHeaders();
...
}
El método getRequestHeader()
permite acceder a una cabecera en concreto, y el método
getRequestHeaders()
nos proporciona un objeto de tipo Map que representa
todas las cabeceras. A continuación mostramos un ejemplo que accede a todas
las cabeceras de la petición HTTP de entrada.
@Path("/miservicio")
public class MiServicio {
@GET
@Produces("text/html")
public String get(@Context HttpHeaders cabeceras) {
String referer = headers.getRequestHeader("Referer").get(0);
for (String header : headers.getRequestHeaders().keySet()) {
System.out.println("Se ha utilizado esta cabecera : " + header);
}
...
}
}
2.4.7. @javax.ws.rs.core.Context
Dentro de nuestros recursos JAX-RS podemos inyectar determinados objetos con información sobre
el contexto de JAX-RS, sobre el contexto de servlets, o sobre elementos de la petición recibida
desde el cliente. Para ello utilizaremos la anotación @javax.ws.rs.core.Context
.
En los ejemplos de esta sesión, ya hemos visto como utilizarla para inyectar objetos de tipo UriInfo y HttpHeaders.
A continuación mostramos un ejemplo en el que podos obtener detalles sobre el contexto del despliegue de la aplicacion, asi como del contexto de peticiones individuales utilizando la anotacion @Context:
@Path("orders")
public class PedidoResource {
@Context Application app; (1)
@Context UriInfo uri; (2)
@Context HttpHeaders headers; (3)
@Context Request request; (4)
@Context SecurityContext security; (5)
@Context Providers providers; (6)
@GET
@Produces("application/xml")
public List<Order> getAll(@QueryParam("start")int from,
@QueryParam("end")int to) { //. . .(app.getClasses());
//. . .(uri.getPath());
//. . .(headers.getRequestHeader(HttpHeaders.ACCEPT));
//. . .(headers.getCookies());
//. . .(request.getMethod());
//. . .(security.isSecure());
//. . .
}
}
1 | Application proporciona acceso a la información de la configuración de la aplicación (clase Application) |
2 | UriInfo proporciona acceso a la URI de la petición |
3 | HttpHeaders proporciona acceso a las cabeceras de la petición HTTP La anotación @HeaderParam puede también utilizarse para enlazar una cabecera HTTP a un parámetro de un método de nuestro recurso, a un campo del mismo, o a una propiedad de un bean |
4 | Request se utiliza para procesar la respuestas, típicamente se usa juntamente con la clase Response para construir la respuesta de forma dinámica |
5 | SecurityContext proporciona acceso a la información de la petición actual relacionada con la seguridad |
6 | Providers proporciona información sobre la búsqueda del runtime de las instancias de proveedores utilizando un conjunto de criterios de búsqueda |
Con respecto a contexto de servlets, podremos inyectar información de ServletContext, ServletConfig, HttpServletRequest, y HttpServletResponse. Debemos recordar que los recursos JAX-RS son invocados por un servlet dentro de una aplicación web, por lo que podemos necesitar tener acceso a la información del contexto de servlets. Por ejemplo, si necesitamos acceder a la ruta en disco donde tenemos los datos de nuestra aplicación web tendremos que inyectar el objeto @ServletContext:
@GET
@Produces("image/jpeg")
public InputStream getImagen(@Context ServletContext sc) {
return sc.getResourceAsStream("/fotos/" + nif + ".jpg");
}
2.4.8. @javax.ws.rs.BeanParam
La anotación @javax.ws.rs.BeanParam
nos permite inyectar una clase específica cuyos
métodos o atributos estén anotados con alguna de las anotaciones de inyección de parámetros
@xxxParam que hemos visto en esta sesión. Por ejemplo, supongamos esta clase:
public class ClienteInput {
@FormParam("nombre")
String nombre;
@FormParam("apellido")
String apellido;
@HeaderParam("Content-Type")
String contentType;
public String getFirstName() {...}
...
}
La clase ClienteInput
es un simple POJO (Plain Old Java Object) que contiene el nombre y
apellidos de un cliente, así como el tipo de contenido del mismo. Podemos dejar que JAX-RS
cree, inicialice, e inyecte esta clase usando la anotación @BeanParam de la siguiente forma:
@Path("/clientes")
public class ClienteResource {
@POST
public void crearCliente(@BeanParam ClienteInput newCust) {
...}
}
El runtime de JAX-RS "analizará" los parámetros anotados con @BeanParam para inyectar las anotaciones correspondientes y asignar el valor que corresponda. En este ejemplo, la clase ClienteInput contendrá dos valores de un formulario de entrada, y uno de los valores de la cabecera de la petición. De esta forma, nos podemos evitar una larga lista de parámetros en el método crearCliente() (en este caso son sólo tres pero podrían ser muchos más).
2.4.9. Conversión automática de tipos
Todas las anotaciones que hemos visto referencian varias partes de la petición HTTP. Todas ellas se representan como una cadena de caracteres en dicha petición HTTP. JAX-RS puede convertir esta cadena de caracteres en cualquier tipo Java, siempre y cuando se cumpla al menos uno de estos casos:
-
Se trata de un tipo primitivo. Los tipos int, short, float, double, byte, char, y boolean, pertenecen a esta categoría.
-
Se trata de una clase Java que tiene un constructor con un único parámetro de tipo String
-
Se trata de una clase Java que tiene un método estático denominado valueOf(), que toma un único String como argumento, y devuelve una instancia de la clase.
-
Es una clase de tipo java.util.List<T>, java.util.Set<T>, o java.util.SortedSet<T>, en donde T es un tipo que satisface los criterios 2 ó 3, o es un String. Por ejemplo, List<Double>, Set<String>, o SortedSet<Integer>.
Si el runtime JAX-RS falla al convertir una cadena de caracteres en el tipo Java especificado, se considera un error del cliente. Si se produce este fallo durante el procesamiento de una inyección de tipo @MatrixParam, @QueryParam, o @PathParam, se devuelve al cliente un error "404 Not found". Si el fallo tiene lugar con el procesamiento de las inyecciones @HeaderParam o @CookieParam (esta última no la hemos visto), entonces se envía al cliente el eror "400 Bad Request".
2.4.10. Valores por defecto (@DefaultValue)
Suele ser habitual que algunos de los parámetros proporcionados en las peticiones a servicios RESTful sean opcionales. Cuando un cliente no proporciona esta información opcional en la petición, JAX-RS inyectará por defecto un valor null si se trata de un objeto, o un valor cero en el caso de tipos primitivos.
Estos valores por defecto no siempre son los que necesitamos para nuestro servicio.
Para solucionar este problema, podemos definir nuestro propio valor por defecto para
los parámetros que sean opcionales, utilizando la anotación @javax.ws.rs.DefaultValue
.
Consideremos el ejemplo anterior relativo a la recuperación de la información de un subconjunto de clientes de nuestra base de datos. Para ello utilizábamos dos parámetros de consulta para indicar el índice del primer elemento, así como el número total de elementos que estamos interesados en recuperar. En este caso, no queremos que el cliente tenga que especificar siempre estos parámetros al realizar la peticion. Usaremos la anotación @DefaultValue para indicar los valores por defecto que nos interese.
@Path("/clientes")
public class ClienteResource {
@GET
@Produces("application/xml")
public String getClientes(
@DefaultValue("0") @QueryParam("inicio") int inicio,
@DefaultValue("10") @QueryParam("total") int total)
... }
}
Hemos usado @DefaultValue para especificar un índice de comienzo con valor cero, y un tamaño del subconjunto de los datos de la respuesta. JAX-RS utilizará las reglas de conversión de cadenas de caracteres que acabamos de indicar para convertir el valor del parámetro en el tipo Java que especifiquemos.
2.5. Configuración y despliegue de aplicaciones JAX-RS
Como ya hemos visto en la sesión anterior, implementamos nuestros servicios REST utilizando el API de Java JAX-RS (especificación JSR-339). Una aplicación JAX-RS consiste en uno o más recursos y cero o más proveedores. En este apartado vamos a describir ciertos aspectos aplicados a las aplicaciones JAX-RS como un todo, concretamente a la configuración y también a la publicación de las mismas cuando utilizamos un servidor de aplicaciones JavaEE 7 o bien un contenedor de servlets 3.0, que incluyan una implementación del API JAX-RS. También indicaremos cómo configurar el despliegue en el caso de no disponer como mínimo de un contenedor de servlets 3.0.
2.5.1. Configuración mediante la clase Application
Tanto los recursos (clases anotadas con @Path) como los proveedores que conforman nuestra aplicación JAX-RS pueden configurarse utilizando una subclase de Application. Cuando hablamos de configuración nos estamos refiriendo, en este caso, a definir los mecanismos para localizar las clases que representan los recursos, así como a los proveedores.
Un proveedor es una clase que implementa una o alguna de las siguientes interfaces JAX-RS: MesssageBodyReader, MessageBodyWriter, ContextResolver<T>, y ExceptionMapper<T>. Las dos primeras permiten crear proveedores de entidades (entity providers), la tercera es un proveedor de contexto (context provider), y la última un proveedor de mapeado de excepciones (exception mapping provider). Las clases que actúan como "proveedores" están anotadas con @Provider, para que puedan ser identificadas automáticamente por el runtime JAX-RS. |
El uso de una subclase de Application para configurar nuestros servicios REST constituye la forma más sencilla de desplegar los servicios JAX-RS en un servidor de aplicaciones certificado como Java EE (en este caso, Wildfly cumple con este requisito), o un contenedor standalone de Servlet 3 (como por ejemplo Tomcat).
Pasemos a conocer la clase javax.ws.rs.core.Application
. El uso de la clase
Application es la única forma portable de "decirle" a
JAX-RS qué servicios web (clases anotadas con @Path), así como qué otros
elementos, como filtros, interceptores,…, queremos publicar (desplegar).
La clase Application se define como:
package javax.ws.rs.core;
import java.util.Collections;
import java.util.Set;
public abstract class Application {
private static final Set<Object> emptySet =
Collections.emptySet();
public abstract Set<Class<?>> getClasses();
public Set<Object> getSingletons() {
return emptySet;
}
}
La clase Application es muy simple. Como ya hemos indicado, su propósito es proporcionar una lista de clases y objetos que "queremos" desplegar.
El método getClasses()
devuelve una lista de clases de servicios web y
proveedores JAX-RS. Cualquier servicio JAX-RS devuelto por este método sigue
el modelo per-request, que ya hemos introducido en la sesión anterior.
Cuando la implementación de JAX-RS determina que una petición HTTP necesita ser
procesada por un método de una de estas clases, se creará una instancia de dicha
clase durante la petición, y se "destruirá" al finalizar la misma. En este caso
estamos delegando en el runtime JAX-RS la creación de los objetos.
Las clases "proveedoras" son instanciadas por el contenedor JAX-RS y registradas
una única vez por aplicación.
El método getSingletons()
devuelve una lista de servicios y proveedores web JAX-RS
"ya instanciados". Nosotros, como programadores de las aplicaciones, somos responsables
de crear estos objetos. El runtime JAX-RS iterará a través de la lista de objetos
y los registrará internamente.
Un ejemplo de uso de una subclase de Application podría ser éste:
package org.expertojava;
import javax.ws.rs.core.Application;
import javax.ws.rs.ApplicationPath;
@ApplicationPath("/rest")
public class ComercioApplication extends Application {
public Set<Class<?>> getClasses() {
HashSet<Class<?>> set = new HashSet<Class<?>>();
set.add(ClienteResource.class);
set.add(PedidoResource.class);
return set;
}
public Set<Object> getSingletons() {
JsonWriter json = new JsonWriter();
TarjetaCreditoResource servicio = new TarjetaCreditoResource();
HashSet<Object> set = new HashSet();
set.add(json);
set.add(servicio);
return set;
}
}
La anotación @ApplicationPath
define la base URL de la ruta para todos nuestros
servicios JAX-RS desplegados. Así, por ejemplo, accederemos a todos nuestros servicios JAX-RS
serán desde la ruta "/rest" cuando los ejecutemos. En el ejemplo anterior
estamos indicando que ClienteResource y PedidoResource son servicios per-request.
El método getSingletons() devuelve el servicio de tipo TarjetaCreditoResource, así como
el proveedor JsonWriter (que implementa la interfaz MessageBodyWriter).
Si tenemos al menos una implementación de la clase Application anotada con @ApplicationPath, esta será "detectada" y desplegada automáticamente por el servidor de aplicaciones.
Podemos aprovechar completamente esta capacidad para "escanear" y detectar automáticamente nuestros servicios si tenemos implementada una subclase de Application, pero dejamos que getSingletons() devuelva el conjunto vacío, y no indicamos nada en el método getClasses(), de esta forma:
package org.expertojava;
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;
@ApplicationPath("/rest")
public class ComercioApplication extends Application {
}
En este caso, el servidor de aplicaciones se encargará de buscar en el directorio WEB-INF/classes y en cualquier fichero jar dentro del directorio WEB-INF/lib. A continuación añadirá cualquier clase anotada con @Path o @Provider a la lista de "cosas" que necesitan ser desplegadas y registradas en el runtime JAX-RS.
Los servicios REST son "atendidos" por un servlet, que es específico de la implementación JAX-RS utilizada por el servidor de aplicaciones. El servidor wildfly utiliza la implementación de JAX-RS 2.0 denomindada resteasy (otra implementación muy utilizada es jersey, por ejemplo con el servidor de aplicaciones Glassfish). El runtime de JAX-RS contiene un servlet inicializado con un parámetro de inicialización de tipo javax.ws.rs.Application, cuyo valor será instanciado "automáticamente" por el servidor de aplicaciones con el nombre de la subclase de Application que sea detectada en el war de nuestra aplicación.
2.5.2. Configuración mediante un fichero web.xml
En la sesión anterior, no hemos utilizado de forma explícita la clase Application para configurar el despliegue. En su lugar, hemos indicado esta información en el fichero "web.xml":
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
<!-- Con estas líneas, el servidor es el responsable de
añadir el servlet correspondiente de forma automática.
Si en nuestro war, tenemos clases anotadas con anotaciones JAX-RS
para recibir invocaciones REST, éstas serán detectadas y registradas-->
<servlet-mapping>
<servlet-name>javax.ws.rs.core.Application</servlet-name>
<url-pattern>/rest/*</url-pattern>
</servlet-mapping>
</web-app>
Esta configuración es equivalente a incluir una subclase de Application sin sobreescribir los métodos correspondientes. En este caso, se añade de forma dinámica el servlet que sirve las peticiones REST, con el nombre javax.ws.rs.core.Application, de forma que se detecten automáticamente todas las clases de recursos, y clases proveedoras empaquetadas en el war de la aplicación.
2.5.3. Configuración en un contenedor que no disponga de una implementación JAX-RS
Si queremos hacer el despliegue sobre servidores de aplicaciones o servidores web que den soporte a una especificación de servlets con una versión inferior a la 3.0, tendremos que configurar MANUALMENTE el fichero web.xml para que "cargue" el servlet de nuestra implementación propietaria de JAX-RS (cuyos ficheros jar deberemos incluir en el directorio WEB-INF/lib de nuestro war). Un ejemplo de configuración podría ser éste:
<?xml version="1.0"?>
<web-app>
<servlet>
<servlet-name>JAXRS</servlet-name>
<servlet-class>
org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher
</servlet-class>
<init-param>
<param-name>
javax.ws.rs.Application
</param-name>
<param-value>
org.expertoJava.ComercioApplication
</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>JAXRS</servlet-name>
<url-pattern>/rest/*</url-pattern>
</servlet-mapping>
</web-app>
En la configuración anterior estamos indicando de forma explícita el servlet JAX-RS que recibe las peticiones REST, que a su vez, utilizará la clase Application para detectar qué servicios y proveedores REST serán desplegados en el servidor.
También será necesario incluir la librería con la implementación JAX-RS 2.0 de forma explícita en el war generado (recordemos que para ello, tendremos que utilizar la etiqueta <scope>compile<scope>, para que se añadan los jar correspondientes).
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-web-api</artifactId>
<version>7.0</version>
<scope>compile</scope>
</dependency>
2.6. Ejercicios
Para esta sesión añadiremos un nuevo módulo en el que implementaremos un servicio rest incorporando los conceptos que hemos explicado durante la sesión. En concreto:
-
Creamos un módulo Maven con IntelliJ (desde el directorio
ejercicios-rest-expertojava
) con el arquetipowebapp-javaee7
, tal y como hemos visto en los apuntes de la sesión. Las coordenadas del artefacto Maven serán:-
GroupId: org.expertojava
-
ArtifactId: s2-foro-nuevo
-
version: 1.0-SNAPSHOT
-
-
Configuramos el pom.mxl del proyecto para poder compilar, empaquetar y desplegar nuestro servicio en el servidor de aplicaciones Wildfly. Consulta los apuntes para ver cuál debe ser el contenido de las etiquetas <properties>, <dependencies> y <build>
-
Vamos a estructurar los fuentes (directorio src/main/java) de nuestro proyecto en los siguientes paquetes:
-
org.expertojava.datos: contendrá clases relacionadas con los datos a los que accede nuestra aplicación rest. Por simplicidad, almacenaremos en memoria los datos de nuestra aplicación.
-
org.expertojava.modelo: contiene las clases de nuestro modelo de objetos, que serán clases java con atributos y sus correspondientes getters y setters
-
org.expertojava.rest: contiene los recursos JAX-RS, que implementan nuestros servicios rest, así como las clases necesarias para automatizar el despliegue de dichos recursos
-
2.6.1. Creación de un recurso: creación y consulta de temas en el foro (0,5 puntos)
Vamos a crear un recurso JAX-RS al que denominaremos TemasResource
(en el paquete org.expertojava.rest ). En el siguiente ejercicio
, al configurar la aplicación, haremos que este recurso sea un singleton.
Nuestro recurso gestionará sus propios datos en memoria. Por ejemplo
podemos utilizar un atributo private de tipo HashMap en el que almacenaremos los temas, cada uno
con un identificador numérico como clave. También necesitaremos un atributo para
generar las claves para cada uno de los temas. Por ejemplo:
private Map<Integer, Tema> temasDB = new HashMap<Integer, Tema>(); private int contadorTemas = 0;
Fíjate que si utilizamos los tipos HashMap e int podemos tener problemas de concurrencia si múltiples usuarios están realizando peticiones para crear y/o consultar los temas del foro. En una situación real deberíamos utilizar en su lugar los tipos ConcurrentHasMap y AtomicInteger, para evitar el que dos usuarios intentaran crear un nuevo tema con la misma clave, perdiéndose así uno de los dos temas creados. Al tratarse de un ejercicio en el que solamente tendremos un cliente, no nos planteará ningún problema el trabajar con HashMap e int, por lo que podéis elegir cualquiera de las dos opciones para realizar el ejercicio |
-
Nuestro recurso estará accesible en el servidor en la ruta
/temas
(relativa a la raíz del contexto de nuestra aplicación, y a la ruta de nuestro servlet JAX-RS, que determinaremos con la anotación @ApplicationPath de nuestra clase Application). -
En el paquete org.expertojava.modelo crearemos la clase
Tema
, con los atributos privados:int id; String nombre;
y sus correspondientes getters y setters:
setId(), getId() setNombre(), getNombre()
-
Implementamos un primer método en el recurso
TemasResource
, denominadocreaTema()
, para poder crear un nuevo tema en el foro. Dicho método atenderá peticiones POST a nuestro servicio. Los datos de entrada (cadena de caracteres que respresenta el nombre del tema) se pasan a través de un formulario html, en el que tenemos una única entrada denominada "nombre".
Puedes incluir el siguiente contenido en el fichero index.html
para introducir los
datos desde el navegador:
<!DOCTYPE html>
<html>
<head>
<title>Start Page</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
</head>
<body>
<h1>Alta de temas en el foro: </h1>
<form action="/s2-foro-nuevo/rest/temas" method="post">
Nombre del tema: <input type="text" name="nombre" /><br />
<input type="submit" value="Enviar" />
</form>
</body>
</html>
Cada nuevo Tema
creado se añadirá a nuestra base de datos en memoria temasDB
junto con un identificador numérico (que se irá incrementando para cada nueva
instancia creada).
-
Implementamos un segundo método para consultar los temas creados en el foro. El método se denominará
verTemasTodos()
, y devuelve (en formato texto) todos los temas actualmente creados. Dado que puede haber un gran número de ellos, vamos a permitir que el usuario decida cuántos elementos como máximo quiere consultar a partir de una posición determinada. Por defecto, si no se indica esta información, se mostrarán como máximo los primeros 8 temas registrados en el foro. Si el identificador a partir del cual queremos iniciar la consulta es mayor que el número de temas almacenados, entonces devolveremos la cadena: "No es posible atender la consulta". Ejemplos de URIs que acepta dicho método son:-
/temas
en este caso, y suponiendo que hayamos creado solamente los tres temas del apartado anterior, el resultado sería:
"Listado de temas del 1 al 8: 1. animales 2. plantas 3. ab"
-
/temas?inicio=2&total=2
el resultado sería:
"Listado de temas del 2 al 3: 2. plantas 3. ab"
-
/temas?inicio=7&total=1
el resultado sería:
"No es posible atender la consulta"
-
Como ya hemos comentado, las URIs indicadas en este ejercicio son relativas a la
raíz del contexto de nuestra aplicación y a la ruta especificada para nuestros servicios rest.
Recuerda que si has configurado el pom.xml como en la sesión anterior, la raíz
del contexto de la aplicación vendrá dada por el valor de la etiqueta <finalName>,
anidada en <build>. En nuestro caso debería ser "/s2-foro-nuevo". Más adelante,
fijaremos la ruta de nuestros servicios rest como |
2.6.2. Despliegue y pruebas del recurso (0,5 puntos)
Vamos a construir y desplegar nuestro servicio en el servidor de aplicaciones.
Para ello vamos a utilizar una subclase de Application que añadiremos en el
paquete org.expertojava.rest. La ruta en la que se van a servir nuestras peticiones rest
será rest
. Fíjate que el recurso que hemos creado es el
encargado de gestionar (crear, modificar,…) sus propios datos. Por lo tanto
necesitamos que nuestro recurso REST sea un singleton. Implementa la clase
ForoApplication
y realiza la construcción y despliegue del proyecto. A continuación
prueba el servicio utilizando postman. Puedes probar la inserción de temas utilizando
también el formulario a través de la URI: http://localhost:8080/s2-foro-nuevo.
Podemos utilizar las entradas del apartado anterior, de forma que comprobemos que se
crean correctamente los temas "animales", "plantas", y "ab", y que
obtenemos los listados correctos tanto si no indicamos el inicio y total de elementos,
como si decidimos mostrar los temas desde el 2 hasta el 3.
Cuando utilices el cliente IntelliJ para probar métodos POST, debes proporcionar un Request Body no vacío. En este caso, como en la propia URI incluimos el contenido del mensaje, que es el nombre del tema que queremos añadir al foro tendrás que seleccionar Text aunque no rellenemos el campo correspondiente. De no hacerlo así, obtendremos como respuesta un cuerpo de mensaje vacío, y la cabecera de respuesta HTTP/1.1 415 Unsupported Media Type |
2.6.3. Múltiples consultas de los temas del foro (0,5 puntos)
Implementa tres nuevas consultas de los temas del foro, de forma que:
-
Se pueda realizar una consulta de un tema concreto a partir de su identificador numérico (el método solamente debe admitir identificadores formados por uno o más dígitos). Si el tema consultado no existe se debe devolver una excepción con la cabecera de respuesta HTTP/1.1 404 Not Found. Por ejemplo:
-
/temas/2
Debe devolver lo siguiente:
"Ver el tema 2: plantas"
-
/temas/4
Obtenemos como respuesta un cuerpo de mensaje vacío, y la cabecera de respuesta: HTTP/1.1 404 Not Found
-
-
Se pueda realizar una consulta de los temas que comiencen por uno de los siguientes caracteres: a, b, c, ó d. Por ejemplo, teniendo en cuenta que hemos introducido los temas anteriores:
-
/temas/a
Debe devolver lo siguiente:
"Listado de temas que comienzan por a: animales"
-
/temas/d
Debe devolver: "Listado de temas que comienzan por d:"
-
-
Se pueda realizar una consulta de los temas que contengan una subcadena de caracteres. Por ejemplo, teniendo en cuenta que hemos introducido los temas anteriores:
-
/temas/ma + Debe devolver lo siguiente:
"Listado de temas que contienen la subcadena : ma animales"
-
2.6.4. Creación de subrecursos (0,5 puntos)
Vamos a crear el subrecurso MensajesResource
(en el paquete org.expertojava.rest),
de forma que este recurso gestione la creación y consulta de mensajes para cada uno
de los temas del foro. Este subrecurso debe atender peticiones desde rutas del tipo:
/temas/identificadorTema/mensajes
, siendo identificadorTema la clave
numérica asociada a uno de los temas almacenados.
-
En este caso, nuestro subrecurso no será un singleton, por lo que necesitaremos almacenar los mensajes en otra clase diferente (ya que crearemos una nueva instancia del recurso para cada petición). La clase
DatosEnMemoria
(en el paquete org.expertojava.datos) será la encargada de almacenar en memoria la información de los mensajes publicados para cada tema. Por ejemplo puedes utilizar los siguientes campos estáticos para gestionar los mensajes:public static Map<Mensaje, String> mensajesDB = new HashMap<Mensaje, String>();
La clave será el propio mensaje (objeto Mensaje, que se asociará al tema correspondiente)
public static int contadorMen = 0;
Como ya hemos comentado puedes usar ConcurrentHashMap y AtomicInteger en lugar de los tipos anteriores, para evitar problemas de concurrencia. |
-
En el paquete org.expertojava.datos crearemos la clase
Mensaje
, con los atributos privados:int id; String texto; String autor="anonimo";
y sus correspondientes getters y setters:
setId(), getId() setTexto(), getTexto() setAutor(), getAutor()
-
Vamos a crear un método para poder realizar la publicación de un mensaje de texto en el foro, en uno de los temas ya creados. Independientemente del tipo de petición realizada sobre los mensajes, si el tema indicado en la URI no existe, lanzaremos la excepción WebApplicationException(Response.Status.NOT_FOUND). Veamos algún ejemplo:
-
Deberemos poder realizar una petición POST a /temas/1/mensajes, con el cuerpo de mensaje = "Mensaje numero 1". El mensaje creado, por defecto tendrá asociado el autor "anonimo"
-
Si realizamos una petición para añadir un mensaje a la URI: /temas/9/mensajes, deberíamos obtener como cabecera de respuesta: HTTP/1.1 404 Not Found, independientemente del cuerpo del mensaje
-
-
Vamos a crear un método para realizar una consulta de todos los mensajes publicados en un tema concreto. Por ejemplo:
-
Una petición GET a /temas/1/mensajes debería dar como resultado:
"Lista de mensajes para el tema: animales 1. Mensaje anonimo"
-
Si realizamos una petición GET a la URI: /temas/9/mensajes, deberíamos obtener como cabecera de respuesta: HTTP/1.1 404 Not Found, independientemente del cuerpo del mensaje
-
-
Finalmente vamos a añadir dos nuevos métodos para: (a) añadir un nuevo mensaje en un tema concreto, indicando el autor del mensaje. Como restricción, el nombre del autor deberá estar formado solamente por caracteres alfabéticos, utilizando mayúsculas o minúsculas, y como mínimo tiene que tener un caracter; y (b) consultar todos los mensajes que un determinado autor ha publicado en el foro en un tema determinado
-
Una petición POST a la URI: /temas/1/mensajes/pepe, con el cuerpo de mensaje con valor "mensaje de pepe" debería crear un nuevo mensaje para el tema con identificador 2, y devolver como resultado el nuevo id (y/o la URI del nuevo recurso en la cabecera de respuesta Location, si seguimos la ortodoxia REST). En caso de que devolvamos la URI del nuevo recurso podemos utilizar la orden:
-
return Response.created(uriInfo.getAbsolutePathBuilder() (1)
.segment(String.valueOf(id)) (2)
.build()) (3)
.build(); (4)
1 | Obtenemos el path absoluto de la uri que nos ha invocado |
2 | Añadimos el identificador id del nuevo recurso creado |
3 | Construimos la nueva URI |
4 | Construimos el objeto Response. |
Veremos cómo manipular objetos de tipo Response en sesiones posteriores.
Recuerda que para acceder al cuerpo de la petición basta con definir un parámetro de tipo String. JAX-RS automáticamente lo instanciará con el cuerpo de la petición como una cadena. |
-
Una petición GET a la URI: /temas/1/mensajes/anonimo, daría como resultado:
"Lista de mensajes tema= animales ,y autor= anonimo
-
Mensaje anonimo"
-
-
Una petición GET a la URI: /temas/1/mensajes/, daría como resultado:
"Lista de mensajes para el tema: animales 1. Mensaje anonimo 2. mensaje de pepe"
-
Una petición GET a la URI: /temas/1/mensajes/roberto, daría como resultado:
"Lista de mensajes tema= animales ,y autor= roberto"