Proyecto de Integración
 

Servicios RESTful con Jersey

Introducción

En esta sesión de integración construiremos una interfaz web alternativa a la desarrollada en la sesión anterior. En este caso, en lugar de crear una interfaz HTML para acceder a la aplicación desde un navegador web, crearemos una serie de servicios RESTful para que otras aplicaciones puedan acceder y gestionar la información sobre los libros de la biblioteca. Este proyecto también se construirá sobre la capa de negocio desarrollada en sesiones anteriores, al igual que el proyecto web desarrollado en la sesión anterior.

Proyecto jbib-rest

Vamos a crear en primer lugar un nuevo proyecto con Maven, de nombre jbib-rest, utilizando para ello el arquetipo webapp-javaee6, al igual que el proyecto web de la sesión anterior. Por lo tanto, necesitaremos contar también con el plugin Maven integration for WTP (optional), que ya deberemos tener instalado en Eclipse (si no es así, deberá instalarse siguiendo las instrucciones indicadas en la sesión anterior). Este nuevo proyecto contendrá todos los componentes necesarios para implementar los servicios RESTful.

En el espacio de trabajo del proyecto de integración crea un nuevo proyecto jbib-rest. Escoge la opción de Eclipse New > Project... > Maven > Maven Module, sin marcar la opción de Create simple project e introduciendo:

  • Module Name: jbib-rest
  • Parent Project: proyint-jbib
  • Archetype: webapp-javaee6 (se indica en la segunda pantalla del asistente)

En el editor del POM, justo antes de las dependencias, deberemos añadir el repositorio de Maven donde se encuentra Jersey:

<repositories>
    <repository>
        <id>maven2-repository.java.net</id>
        <name>Java.net Repository for Maven</name>
        <url>http://download.java.net/maven/2</url>
        <layout>default</layout>
    </repository>
    <repository>
        <id>maven-repository.java.net</id>
        <name>Java.net Maven 1 Repository</name>
        <url>http://download.java.net/maven/1</url>
        <layout>legacy</layout>
    </repository>	
</repositories>

Tras esto añade la dependencia con el proyecto jbib-negocio, jersey-server, y jersey-json:

<dependency>
    <groupId>com.sun.jersey</groupId>
    <artifactId>jersey-server</artifactId>
    <version>1.9.1</version>
</dependency>

<dependency>
    <groupId>com.sun.jersey</groupId>
    <artifactId>jersey-json</artifactId>
    <version>1.9.1</version>
</dependency>

<dependency>
    <groupId>org.especialistajee.proyint</groupId>
    <artifactId>jbib-negocio</artifactId>
    <version>${project.parent.version}</version>
    <type>jar</type>
    <scope>compile</scope>
</dependency>

Podemos eliminar del POM el bloque <build> ... </build> ya que no es necesario. Deberemos también coger el groupId y la version del proyecto padre:

<parent>
    <artifactId>proyint-jbib</artifactId>
    <groupId>org.especialistajee.proyint</groupId>
    <version>2011</version>
</parent>

<groupId>${project.parent.groupId}</groupId>
<artifactId>jbib-rest</artifactId>
<version>${project.parent.version}</version>
<packaging>war</packaging>

<name>jbib-rest</name>

El fichero completo debe quedar tal como se indica en este pom.xml.

Ahora podemos ejecutar nuestra aplicación web directamente desde Eclipse como hemos hecho a lo largo del curso (Run As > Run on server). Tendremos que configurar Tomcat en Eclipse, si no está configurado ya en este workspace. Veremos una página de prueba creada automáticamente por Maven, que estará desplegada en la siguiente dirección:

http://localhost:8080/jbib-rest/

Configuración del acceso a datos

Vamos ahora a configurar el pool de conexiones y el realm de seguridad de la misma forma que con el proyecto de la sesión anterior.

Para ello creamos un fichero context.xml en la carpeta META-INF de nuestro proyecto REST:

<?xml version="1.0" encoding="ISO-8859-1"?>
<Context>
  <Resource
    name="jdbc/biblioteca"
    type="javax.sql.DataSource"
    auth="Container"
    username="root"
    password="especialista"
    driverClassName="com.mysql.jdbc.Driver"
    url=
      "jdbc:mysql://localhost:3306/biblioteca?autoReconnect=true"/>

  <ResourceParams name="jdbc/biblioteca">
  
    <parameter>
      <name>maxActive</name>
      <value>20</value>
    </parameter>
    
    <parameter>
      <name>maxIdle</name>
      <value>5</value>
    </parameter>

    <parameter>
      <name>maxWait</name>
      <value>10000</value>
    </parameter>
  </ResourceParams>

<Realm  className="org.apache.catalina.realm.JDBCRealm"
  driverName="com.mysql.jdbc.Driver"
  connectionURL=
      "jdbc:mysql://localhost:3306/biblioteca?autoReconnect=true"
  connectionName="root" connectionPassword="especialista"
  userTable="usuario" userNameCol="login" userCredCol="password"
  userRoleTable="usuario" roleNameCol="tipoUsuario" />
</Context>

También modificaremos el fichero web.xml de la aplicación para que referencie al recurso creado en el paso anterior. Para eso, añadimos estas líneas:

<resource-ref>
    <res-ref-name>jdbc/biblioteca</res-ref-name>
    <res-type>javax.sql.DataSource</res-type>
    <res-auth>Container</res-auth>
</resource-ref>

Si en el proyecto no se ha creado el descriptor de despliegue de forma automática, podéis utilizar este web.xml como plantilla.

La configuración de la unidad de persistencia de jbib-modelo para que utilice esta fuente de datos ya se realizó en la sesión anterior. También se copió el driver de MySql al directorio de librerías de Tomcat. Si no lo tuviésemos copiado, deberemos hacerlo para que la aplicación funcione correctamente.

Configuración de Jersey

Vamos ahora a configurar en web.xml el servlet de Jersey mediante al cual accederemos a los servicios REST. Los recursos de Jersey estarán en el paquete org.jbib.rest.resources, y accederemos a ellos a través de la URL /resources:

<servlet>
    <servlet-name>Jersey Web Application</servlet-name>
    <servlet-class>
       com.sun.jersey.spi.container.servlet.ServletContainer
    </servlet-class>
    <init-param>
        <param-name>
            com.sun.jersey.config.property.packages
        </param-name>
        <param-value>org.jbib.rest.resources</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
    <servlet-name>Jersey Web Application</servlet-name>
    <url-pattern>/resources/*</url-pattern>
</servlet-mapping>

Configuración de la seguridad

Vamos ahora a definir seguridad declarativa para nuestra aplicación. Utilizaremos seguridad básica para proteger las operaciones que modifican datos, de forma que sólo puedan acceder los usuarios bibliotecarios definidos en la base de datos.

Protección de recursos

Vamos a añadir las líneas de configuración de seguridad al fichero web.xml para proteger las operaciones que realizan modificaciones sobre los recursos (POST, PUT y DELETE). En primer lugar declaramos los roles que tendrán acceso:

<security-role>
    <role-name>ADMIN</role-name>
</security-role>
<security-role>
    <role-name>BIBLIOTECARIO</role-name>
</security-role>
<security-role>
    <role-name>PROFESOR</role-name>
</security-role>
<security-role>
    <role-name>ALUMNO</role-name>
</security-role>

Tras esto, protegeremos todos los recursos de la web para que sólo los usuarios con estos roles tengan acceso a las operaciones que alteran los datos:

<security-constraint>
    <web-resource-collection>
        <web-resource-name>Todos los recursos</web-resource-name>
        <url-pattern>/*</url-pattern>
        <http-method>DELETE</http-method>
        <http-method>POST</http-method>
        <http-method>PUT</http-method>
    </web-resource-collection>
    <auth-constraint>
        <role-name>ADMIN</role-name>
        <role-name>BIBLIOTECARIO</role-name>
        <role-name>PROFESOR</role-name>
        <role-name>ALUMNO</role-name>
    </auth-constraint>
</security-constraint>

Autentificación básica

En este caso ya no podemos utilizar seguridad basada en formulario, ya que necesitamos proporcionar una interfaz con la que otras aplicaciones puedan autentificarse de forma sencilla. Por lo tanto, utilizaremos seguridad de tipo BASIC. En el fichero web.xml, añadimos el bloque login-config:

<login-config>
    <auth-method>BASIC</auth-method>
</login-config>	

Recursos REST

Vamos a crear recursos para gestionar el conjunto de libros y para manipular cada libros individual. Estos recursos se llamarán LibrosResource y LibroResource respectivamente, siendo el primer un recurso raíz mapeado a la ruta /libros y el segundo un subrecurso de éste, al que se accede cuando se añade el ISBN de un libro a la ruta anterior. Todos los métodos soportarán las representaciones application/xml y application/json, salvo que se especifique lo contrario.

Vamos a utilizar beans JAXB para mapear los objetos Java que encapsulan la información de los libros a sus representaciones XML/JSON.

Beans JAXB

Los beans estarán en un paquete org.jbib.rest.beans. En la clase del bean añadiremos métodos para poder convertir de forma sencilla estos objetos a su correspondiente objeto de dominio:

public class LibroBean {

    String isbn;
    String titulo;
    String autor;
    int numPaginas;

    public LibroBean() {
    }

    public LibroBean(LibroDomain libro) {
        this.titulo = libro.getTitulo();
        this.autor = libro.getAutor();
        this.numPaginas = libro.getNumPaginas();
        this.isbn = libro.getIsbn();
    }

    public LibroDomain toLibroDomain() {
        LibroDomain libro = new LibroDomain();
        libro.setIsbn(isbn);
        libro.setTitulo(titulo);
        libro.setAutor(autor);
        libro.setNumPaginas(numPaginas);

        return libro;
    }
	
    // Getters y setters
    ...		
}

Añade a esta clase las anotaciones JAXB necesarias para que la etiqueta con los datos del libro tenga nombre libro y para que todos sus campos se muestren como atributos.

Tendremos también otro bean para representar el conjunto de libros:

public class LibrosBean {

    List<LibroBean> libros;
	
    public LibrosBean() {
    }
	
    public LibrosBean(List<LibroDomain> libros) {
        this.libros = new ArrayList<LibroBean>(libros.size());
        for(LibroDomain libro: libros) {
            this.libros.add(new LibroBean(libro));
        }
    }

    public List<LibroDomain> toLibroDomain() {
        List<LibroDomain> libros = 
        new ArrayList<LibroDomain>(this.libros.size());
        for(LibroBean libro: this.libros) {
            libros.add(libro.toLibroDomain());
        }
        return libros;
    }
	
    public List<LibroBean> getLibros() {
        return libros;
    }

    public void setLibros(List<LibroBean> libros) {
        this.libros = libros;
    }	
}

Añade a esta clase las etiquetas necesarias para que la lista de libros se englobe en una etiqueta de nombre libros, y dentro de ella tengamos cada libro como un elemento de nombre libro.

Listado y búsqueda

Cuando accedamos mediante GET a /libros, obtendremos la lista de todos los libros registrados en la biblioteca. Mediante una serie de parámetros podremos realizar búsquedas o paginar la lista de libros.

Si especificamos un parámetro keywork en la query, realizará una búsqueda de los libros que contengan la palabra especificada en su título o autor.

http://localhost:8080/jbib-rest/resources/libros?keyword=Pattern
<libros>
    <libro autor="Clifton Nock" isbn="0131401572" numPaginas="512" 
           titulo="Data Access Patterns"/>
    <libro autor="Martin Fowler" isbn="0321127420" numPaginas="533" 
           titulo="Patterns Of Enterprise Application Architecture"/>
</libros>

Si especificamos los parámetros firstResult y maxResults se mostrarán sólo maxResults libros en la lista empezando desde el que se encuentra en la posición firstResult en la lista completa.

http://localhost:8080/jbib-rest/resources/libros?firstResult=2&maxResults=4
<libros>
    <libro autor="Eric Newcomer and Greg Lomow" isbn="0321180860" 
           numPaginas="465" 
           titulo="Understanding SOA with Web Services"/>
    <libro autor="Kent Beck" isbn="0321278658" numPaginas="189" 
           titulo="Extreme Programming Explained - Embrace Change"/>
    <libro autor="Alistair Cockburn" isbn="0321482751" 
           numPaginas="467" titulo="Agile Software Development"/>
    <libro autor="Eric A. Marks and Michael Bell" isbn="0471768944" 
           numPaginas="276" 
           titulo="Service-Oriented Architecture (SOA)"/>
</libros>

Si no se especifica ninguno de los parámetros anteriores, se obtendrá el listado de todos los libros.

http://localhost:8080/jbib-rest/resources/libros
<libros>
    <libro autor="Clifton Nock" isbn="0131401572" numPaginas="512" 
           titulo="Data Access Patterns"/>
    <libro autor="Martin Fowler" isbn="0321127420" numPaginas="533" 
           titulo="Patterns Of Enterprise Application Architecture"/>
    <libro autor="Eric Newcomer and Greg Lomow" isbn="0321180860" 
           numPaginas="465" 
           titulo="Understanding SOA with Web Services"/>
    <libro autor="Kent Beck" isbn="0321278658" numPaginas="189" 
           titulo="Extreme Programming Explained - Embrace Change"/>
    <libro autor="Alistair Cockburn" isbn="0321482751" 
           numPaginas="467" titulo="Agile Software Development"/>
    <libro autor="Eric A. Marks and Michael Bell" isbn="0471768944" 
           numPaginas="276" 
           titulo="Service-Oriented Architecture (SOA)"/>
    <libro autor="Rod Johnson" isbn="0764558315" numPaginas="576" 
           titulo="Expert One-On-One J2EE Development Without EJB"/>
    <libro autor="Venkat Subramaniam and Andy Hunt" isbn="097451408X"
           numPaginas="208" 
           titulo="Practices of an Agile Developer"/>
    <libro autor="Esther Derby and Diana Larsen" isbn="0977616649" 
           numPaginas="200" titulo="Agile Retrospectives"/>
    <libro autor="Akkana Peck" isbn="1590595874" numPaginas="528" 
           titulo="Beginning GIMP"/>
    <libro autor="Christian Bauer and Gavin King" isbn="1932394885" 
           numPaginas="841" 
           titulo="Java Persistence with Hibernate"/>
    <libro autor="Debu Panda" isbn="1933988347" numPaginas="677" 
           titulo="EJB 3 In Action"/>
</libros>

Todas las operaciones anteriores deben implementarse en un único método de LibrosResource, que según los parámetros recibidos realizará un filtrado distinto de los libros utilizando para ello las funciones implementadas en los objetos de negocio.

Consulta de libros

Si a la ruta raíz /libros le añadimos el ISBN de un libro, y accedemos a esta nueva ruta mediante GET, veremos los datos de un libro concreto. Esto lo implementaremos en el subrecurso LibroResource. Si el ISBN especificado no existe en la base de datos, devolverá un código 404 Not found.

http://localhost:8080/jbib-rest/resources/libros/0321482751
<libro autor="Alistair Cockburn" isbn="0321482751" numPaginas="467" 
       titulo="Agile Software Development"/>

Inserción de libros

Si accedemos mediante POST a la ruta raíz /libros y proporcionamos la representación XML o JSON de un libro, dicho libro deberá ser añadido a la base de datos. Se devolverá como respuesta un código 201 Created y la URI correspondiente al nuevo libro en la cabecera Location.

Nota
Para probar esta funcionalidad ya no nos bastará con un navegador web, ya que debemos enviar como contenido la representación de un libro. Podemos utilizar el cliente genérico Swing visto en clase para realizar estas pruebas. Como ayuda, se puede obtener la representación de un libro mediante GET y utilizarla como plantilla para el nuevo libro a añadir. Se debe también introducir un login y password para poder realizar la operación, ya que está protegida mediante seguridad declarativa.

Borrado de libros

Si accedemos mediante DELETE a la ruta correspondiente a un libro concreto, podremos borrarlo. Tras borrarlo, devolveremos un código 204 No content. Si el recurso a borrar no existe, devolveremos un código 404 Not found.

Atención
Dado que casi todos los libros tienen operaciones definidas sobre ellos, ya no pueden ser borrados de la base de datos. Si intentamos borrarlos, obtendremos una excepción, y por lo tanto el servicio devolverá un código 500 Internal Server Error. Esto debe ser así. Si queremos probar esta operación deberemos crear previamente un libro nuevo que no tenga operaciones definidas sobre él.

Modificación de libros

Accediendo mediante PUT a un libro individual permitiremos modificarlo, o crearlo si no existía previamente. En caso de que se modifique un libro existente devolveremos un código 204 No content, mientras que si se ha creado un libro nuevo devolveremos 201 Created con la URI del recurso que se acaba de crear en la cabecera Location.

Imágenes de las portadas

Podremos también obtener o modificar las portadas de los libros de la base de datos. Para acceder a ellas añadiremos a la ruta de un libro individual, tras el ISBN, /imagen. Haciendo GET sobre dicha ruta obtendremos la imagen codificada mediante image/jpeg.

Para tener accesibles las imágenes primero descargaremos este pack de portadas y las copiaremos al directorio /imagenes de nuestro proyecto. Tras esto, podremos utilizar un código como el siguiente para obtener una imagen:

@GET
@Path("/imagen")
@Produces("image/jpeg")
public Response getImagen(@Context ServletContext sc) {		
    InputStream is = 
        sc.getResourceAsStream("/imagenes/" + isbn + ".jpg");
    if(is==null) {
        return Response.status(Status.NOT_FOUND).build();
    } else {
        return Response.ok(is).build();			
    }
}

Podemos ver que hemos inyectado el objeto ServletContext para poder localizar así la ruta en el disco donde están las imágenes y abrir un flujo de entrada para leerlas. Si la imagen existe devolvemos directamente el flujo de entrada para que los bytes de la imagen JPEG se envíen al cliente. Si no existe devolvemos 404 Not found.

Vamos a implementar también la operación para modificar la imagen de un libro, accediendo mediante PUT a la ruta anterior. En este caso el código a utilizar para subir la imagen y guardarla en el disco es el siguiente:

@PUT
@Path("/imagen")
@Consumes("image/jpeg")
public Response setImagen(@Context ServletContext sc, 
                          InputStream is) {
    String path = sc.getRealPath("/imagenes/");
    File file = new File(path, isbn + ".jpg");
    FileOutputStream fos = null;

    int bufferSize = 8192;
    byte [] buffer = new byte[bufferSize];
	
    try {
        fos = new FileOutputStream(file);
		
        int leidos;
		
        while( (leidos = is.read(buffer)) > 0) {
            fos.write(buffer, 0, leidos);
        }
		
    } catch (FileNotFoundException e) {
        throw new WebApplicationException(
                      Status.INTERNAL_SERVER_ERROR);
    } catch (IOException e) {
        throw new WebApplicationException(
                      Status.INTERNAL_SERVER_ERROR);
    } finally {
        if(fos!=null) {
            try {
                fos.close();
            } catch (IOException e) {}
        }
    }
	
    return Response.noContent().build();
}

Estilo HATEOAS

Vamos a añadir ahora enlaces a las representaciones anteriores siguiendo el estilo HATEOAS, con la codificación Atom. Para ello podemos utilizar una clase LinkBean como la vista en el curso, y modificar LibroBean para que incluya una lista de enlaces.

En el caso del listado de libros, cada libro deberá incluir un enlace con relación self que apuntará a la URI que da acceso al libro individual.

<libros>
  <libro autor="Clifton Nock" isbn="0131401572" numPaginas="512" 
         titulo="Data Access Patterns">
    <link rel="self" uri=
      "http://localhost:8080/jbib-rest/resources/libros/0131401572"/>
  </libro>
  <libro autor="Martin Fowler" isbn="0321127420" numPaginas="533" 
         titulo="Patterns Of Enterprise Application Architecture">
    <link rel="self" uri=
      "http://localhost:8080/jbib-rest/resources/libros/0321127420"/>
  </libro>
  <libro autor="Eric Newcomer and Greg Lomow" isbn="0321180860" 
         numPaginas="465" 
         titulo="Understanding SOA with Web Services">
    <link rel="self" uri=
      "http://localhost:8080/jbib-rest/resources/libros/0321180860"/>
  </libro>
  ...
</libros>

En el caso de la consulta de un libro individual, deberemos obtener los siguientes enlaces:

  • self: Apunta a la misma URI del libro actual.
  • libro/borra: Apunta a la misma URI del libro actual para indicar que se puede utilizar para borrarlo.
  • libro/modifica: Apunta a la misma URI del libro actual para indicar que se puede utilizar para modificarlo.
  • libro/imagen: Apunta a la URI en la que se encuentra la imagen del libro actual.
<libro autor="Alistair Cockburn" isbn="0321482751" numPaginas="467" 
       titulo="Agile Software Development">
 <link rel="self" uri=
   "http://localhost:8080/jbib-rest/resources/libros/0321482751"/>
 <link rel="libro/imagen" uri=
   "http://localhost:8080/jbib-rest/resources/libros/0321482751/imagen"/>
 <link rel="libro/borra" uri=
   "http://localhost:8080/jbib-rest/resources/libros/0321482751"/>
 <link rel="libro/modifica" uri=
   "http://localhost:8080/jbib-rest/resources/libros/0321482751"/>
</libro>

Control de acceso

Vamos ahora a comprobar en los métodos que realizan modificaciones (POST, PUT y DELETE) que el usuario que accede a ellos tiene permisos para realizar la operación correspondiente. Todas las operaciones implementadas anteriormente sólo pueden ser realizadas por bibliotecarios, por lo que si el usuario no tiene este rol deberemos devolver como respuesta un código 403 Forbidden.

Para realizar esta comprobación, inyectaremos en todos los métodos que modifiquen datos un objeto de tipo SecurityContext.

Realización de reservas

Vamos también a permitir reservar libros mediante REST. Para ello añadiremos a la ruta de un libro individual, tras el ISBN, /reserva, y realizaremos POST sobre esta ruta. Esta operación sólo la podrán realizar los usuarios de tipo alumno o profesor, si cualquier otro usuario intentase realizar una reserva obtendrá un código 403 Forbidden.

La reserva se realizará para el usuario autentificado actualmente. Podemos obtenerlo mediante el método getUserPrincipal().getName() del contexto de seguridad.

Además, haremos que los libros que estén disponibles para reserva (es decir, que no tengan ninguna operación activa actualmente) tengan un enlace con relación libro/reserva a la URI correspondiente a la reserva. En caso de que el libro no esté disponible, dicho enlace no debe aparecer.

<libro autor="Alistair Cockburn" isbn="0321482751" numPaginas="467" 
       titulo="Agile Software Development">
 <link rel="self" uri=
   "http://localhost:8080/jbib-rest/resources/libros/0321482751"/>
 <link rel="libro/imagen" uri=
   "http://localhost:8080/jbib-rest/resources/libros/0321482751/imagen"/>
 <link rel="libro/borra" uri=
   "http://localhost:8080/jbib-rest/resources/libros/0321482751"/>
 <link rel="libro/modifica" uri=
   "http://localhost:8080/jbib-rest/resources/libros/0321482751"/>
 <link rel="libro/reserva" uri=
   "http://localhost:8080/jbib-rest/resources/libros/0321482751/reserva"/>
</libro>

Entrega

Para la entrega se deberá etiquetar el estado del proyecto con el tag entrega-proyint-cw-rest. Recordamos que la dirección del proyecto padre proyint-jbib en el servidor SVN debe ser:

svn+ssh://server.jtech.ua.es/home/svn/<login>/proyint/trunk/proyint-jbib

Como ayuda, proporcionamos la siguiente figura con un ejemplo de estructura de paquetes y clases resultantes de esta sesión.