martes, 4 de febrero de 2014

Servicios RESTFul con Jersey en App Engine


En el post anterior comentaba como hacer uso de CORS con los servicios Jersey RESTFul. Hoy entraré un poco más en detalle y explicaré como crear servicios RESTFul (para enviar y recibir JSONs) con Jersey en App Engine.

Creando el proyecto y configurando lo necesario


En mi post Google App Engine y su DataStore (NoSQL) os explicaba como montar un proyecto para usar App Engine. Así que nos saltaremos ese paso y arrancaremos desde un proyecto nuevo.


Una vez creado el proyecto DemoRESTFul incluiremos las librerías de Jersey para poder crear en el proyecto servicios RESTFul. Para ello, usaremos la versión de Jersey 1.1.5 (la que soporta por ahora App Engine) y las descargaremos de aquí. Si no os funciona, este es enlace de backup.

Copiaremos los archivos del directorio lib que hemos descargado a nuestro directorio WEB-INF/lib.



Tras esto, importamos los ficheros que hemos incluido en el Build Path del proyecto.



Estamos hablando de 7archivos JAR:

asm-3.1.jar à OJO! no se incluye en el build path
jackson-core-asl-1.1.1.jar
jersey-client-1.1.5-ea-SNAPSHOT.jar
jersey-core-1.1.5-ea-SNAPSHOT.jar
jersey-json-1.1.5-ea-SNAPSHOT.jar
jersey-server-1.1.5-ea-SNAPSHOT.jar
jettison-1.1.jar
jsr311-api-1.1.1.jar

Cuidado si vamos a usar DataStore


Un detalle importante, es que si vamos a usar el DataStore de AppEngine, la versión 2.0 de DataNucleus usa ASM 4.0 y la versión de Jersey que acabamos de importar usa la versión 3.1. La solución a esto es cambiar la configuración a del DataStore en las propiedades del proyecto > Google > App Engine para que use la versión 1.0 de DataNucleus.





De lo contrario, estaremos teniendo el siguiente problema:




Tras esto, añadiremos algunas de las librerias de App Engine en el Build Path que encontraremos en WEB-INF/lib y lo dejaremos tal que así:




NOTA: La librería GSON la incluimos en el siguiente punto del manual.

De esta forma, el mensaje que nos muestra DataNucleus por consola es el siguiente:

DataNucleus Enhancer (version 1.1.4) : Enhancement of classes
DataNucleus Enhancer completed with success for 0 classes. Timings : input=226 ms, enhance=45 ms, total=271 ms. Consult the log for full details


Librería para objetos JSON


Para trabajar con objetos JSON en App Engine, lo mejor es hacer uso de la librería GSON v2.2.2 que podemos descargar de aquí. (Aquí tenéis un enlace de backup).

La forma de proceder es la misma, incluiremos el JAR en WEB-INF/lib y lo añadiremos al Build Path.




Creando objetos JSON


Ahora jugaremos creando objetos para almacenar en el DataStore y tratarlos en los servicios RESTFul como JSON.

Crearemos el objeto ClaseA con dos atributos simples del tipo String: atrA y atrB.

package com.demo.restful.model;

public class ClaseA {
      private String atrA;
      private String atrB;
}

Siguiendo el manual de Google App Engine y su DataStore (NoSQL), extenderemos la clase de java.io.Serializable y le añadiremos su propiedad key, su constructor por defecto y sus getters y setters, así como las etiquetas @Entity y @XmlRootElement.

Nos quedará todo tal que así:

package com.demo.restful.model;

import java.io.Serializable;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.xml.bind.annotation.XmlRootElement;
import com.google.appengine.api.datastore.Key;

@Entity
@XmlRootElement
public class ClaseA implements Serializable {

      private static final long serialVersionUID = 1L;

      @Id
      private Key key;
      private String atrA;
      private String atrB;
     
      public ClaseA() {
            super();
      }
      public Key getKey() {
            return key;
      }
      public void setKey(Key key) {
            this.key = key;
      }
      public String getAtrA() {
            return atrA;
      }
      public void setAtrA(String atrA) {
            this.atrA = atrA;
      }
      public String getAtrB() {
            return atrB;
      }
      public void setAtrB(String atrB) {
            this.atrB = atrB;
      }
}


A continuación, crearemos un manejador DAO para hacer las inserciones y las consultas (todo esto ya lo expliqué en el post anterior):

package com.demo.restful.dao;

import java.security.SecureRandom;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import com.demo.restful.model.ClaseA;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;
import javax.persistence.Query;

public class ClaseADAO {
     
      private static EntityManagerFactory emf;
      //Singleton para devolver a EntityManager
      protected final EntityManager getEntityManager() {
            synchronized (ClaseADAO.class) {
                  if (emf == null) {
                        emf = Persistence.createEntityManagerFactory("transactions-optional");
                  }
            }
            return emf.createEntityManager();
      }
     
      public ClaseA create (ClaseA claseA) {
            EntityManager mgr = getEntityManager();
            try {
                  mgr.getTransaction().begin();
                  String oid = System.currentTimeMillis() + Long.toHexString((new SecureRandom()).nextInt());
                  Key key = KeyFactory.createKey(ClaseA.class.getSimpleName(),oid);
                  claseA.setKey(key);
                  mgr.persist(claseA);
                  mgr.getTransaction().commit();
             } catch (Exception e) {
                   e.printStackTrace();
            } finally {
                  mgr.close();
            }
            return claseA;
      }

      public ClaseA getByAtrA(String atrA) {
            EntityManager mgr = getEntityManager();
            ClaseA res = null;
            try {
                  String strQuery = "SELECT c FROM ClaseA AS c"
                                                + " WHERE c.atrA = :atrA";
                  Query query = mgr.createQuery(strQuery);
                  query.setParameter("atrA", atrA);
                  ClaseA result = (ClaseA)query.getSingleResult();
                  if (result != null) {
                  res = result;
                  }
            } catch (javax.persistence.NoResultException ne) {
                  return res;
            } catch (Exception e) {
                  e.printStackTrace();
            } finally {
                  mgr.close();
            }
            return res;
      }
}

Y después creamos una clase Service muy simple que haga uso de la clase DAO y será nuestro intermediario con el Servlet RESTFul. Aunque siendo sinceros, esta clase no es necesario pero deja un código limpio y ordenado.

package com.demo.restful.service;

import com.demo.restful.dao.ClaseADAO;
import com.demo.restful.model.ClaseA;

public class ClaseAService {

      public ClaseA create(ClaseA claseA){
            return (new ClaseADAO()).create(claseA);
      }
     
      public ClaseA getByAtrA(String atrA){
            return (new ClaseADAO()).getByAtrA(atrA);
      }
}

Y finalmente, crearemos el Servlet que tendrá los metodos RESTFul.

Creación del servicio RESTFul


La clase ClaseAServlet tendrá dos atributos finales que nos ayudarán a convertir objetos en JSONs (atributo gson) y a realizar las operaciones de acceso a DataStore (atributo claseAService).

Por otro lado, definirá dos métodos: uno para obtener objetos en base a busquedas por el atributo A (getByAtrA) y otra para crear objetos en el DataStore (create).

package com.demo.restful.servlet;

import com.demo.restful.service.ClaseAService;
import com.google.gson.Gson;

public class ClaseAServlet {
     
      protected final Gson gson = new Gson();
      protected final ClaseAService claseAService = new ClaseAService();
     
      public String getByAtrA(String atrA){
           
      }
     
      public String create(ClaseA claseA){
           
      }
}


Ahora, usaremos las etiquetas para indicar cual será la ruta de nuestro servicio (@Path). Esta ruta se concatenará la que definamos por defecto en los servicios Jersey (que en nuestro caso será /rest/clasea/…) y que veremos en la configuración web.xml.

@Path("/clasea")
public class ClaseAServlet {

En cada método, indicaremos también un @Path, así como el tipo de objeto que recibe (@Consumes) y produce (@Produces): ambos de tipo MediaType.APPLICATION_JSON.

Y finalmente, añadiremos el tag que nos indicará el tipo de método (@GET, POST, PUT…)

@POST
@Path("/create")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)

Como detalle, decir que podemos recibir en la URL parámetros, y esto lo indicaremos en el path entre llaves:


@GET
@Path("/get/{atrA}")
@Produces(MediaType.APPLICATION_JSON)


Ahora bien, estos métodos recibirán siempre como primer atributo el objeto @Context HttpServletRequest req y a continuación el que esperemos en la URL o como JSON.

Con esto, nuestros dos métodos quedarían tal que así:

package com.demo.restful.servlet;

import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import com.demo.restful.model.ClaseA;
import com.demo.restful.service.ClaseAService;
import com.google.gson.Gson;

@Path("/clasea")
public class ClaseAServlet {
     
      protected final Gson gson = new Gson();
      protected final ClaseAService claseAService = new ClaseAService();
     
      @GET
      @Path("/get/{atrA}")
      @Produces(MediaType.APPLICATION_JSON)
      public String getByAtrA(@Context HttpServletRequest req,
                  @PathParam("atrA") String atrA){
           
      }
     
      @POST
      @Path("/create")
      @Consumes(MediaType.APPLICATION_JSON)
      @Produces(MediaType.APPLICATION_JSON)
      public String create(@Context HttpServletRequest req,
                  ClaseA claseA){
           
      }
}

Y ahora, el código de los métodos que son muy simples: envueltos en un try/catch, se ejecutará el la operación oportuna de la clase Service, y si todo es correcto, se convertirá el objeto a JSON.

String result = "";
try {
      Objeto objeto = claseService.operacion(valores);
      if (objeto!=null){
            result = gson.toJson(objeto);
      } else {
            result = "No se ha podido realizar la operación";
      }
} catch (Exception e) {
      result = e.getMessage();
}
return result;

Quedado nuestros dos métodos así:

@GET
@Path("/get/{atrA}")
@Produces(MediaType.APPLICATION_JSON)
public String getByAtrA(@Context HttpServletRequest req,
            @PathParam("atrA") String atrA){
      String result = "";
      try {
            ClaseA claseA = claseAService.getByAtrA(atrA);
            if (claseA!=null){
                  result = gson.toJson(claseA);
            } else {
                  result = "No se han encontrado datos";
            }
      } catch (Exception e) {
            result = e.getMessage();
      }
      return result;
}

@POST
@Path("/create")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public String create(@Context HttpServletRequest req,
            ClaseA claseA){
      String result = "";
      try {
            ClaseA claseA2 = claseAService.create(claseA);
            if (claseA2!=null){
                  result = gson.toJson(claseA2);
            } else {
                  result = "Error creando el objeto";
            }
      } catch (Exception e) {
            result = e.getMessage();
      }
      return result;
}

Configuración de Jersey


Ahora, para que se arranque el servlet de Jersey y se cargue el servicio web que hemos creado, tendremos que configurar el archivo WEB-INF/web.xml con la siguiente información.

<?xml version="1.0" encoding="utf-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5">
       <security-constraint>
          <web-resource-collection>
                  <web-resource-name>aname</web-resource-name>
                  <url-pattern>/*</url-pattern>
          </web-resource-collection>
          <user-data-constraint>
                  <transport-guarantee>CONFIDENTIAL</transport-guarantee>
          </user-data-constraint>
      </security-constraint>
      <init-param>
            <param-name>com.sun.jersey.config.feature.DisableWADL</param-name>
            <param-value>true</param-value>
      </init-param>
      <init-param>
          <param-name>com.sun.jersey.api.json.POJOMappingFeature</param-name>
          <param-value>true</param-value>
      </init-param>
      <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>com.demo.restful.servlet</param-value>
            </init-param>
            <load-on-startup>1</load-on-startup>
      </servlet>
      <servlet-mapping>
          <servlet-name>Jersey Web Application</servlet-name>
          <url-pattern>/rest/*</url-pattern>
      </servlet-mapping>
      <welcome-file-list>
          <welcome-file>index.html</welcome-file>
      </welcome-file-list>
</web-app>

Como se puede apreciar, añadimos un servlet-mapping donde indicamos que los servicios web de Jersey estarán en la ruta /rest/*. Esto quiere decir que nuestros servicios tendrán la ruta http://…./rest/<path_que_hayamos_indicado>. Es decir, http://localhost:8888/rest/clasea/create y http://localhost:8888/rest/clasea/get/<valor>.

Por otro lado, en el parámetro com.sun.jersey.config.property.packages le estamos indicando donde debe buscar los servicios web, que en nuestro caso será el paquete com.demo.restful.servlet.

Pruebas de los servicios


Probar los servicios es muy sencillo: usaremos una de las aplicaciones que podemos instalar en Chrome: POSTMAN.




Y en POSTMAN simplemente deberemos indicar la URL de nuestro servicio web (por ejemplo el create sería http://localhost:8888/rest/clasea/create) el tipo de operación (POST) y el JSON a enviar.






Y fin ;-)

El ejemplo lo podéis descargar de aquí.