viernes, 1 de marzo de 2013

Integración de Apache Tapestry con RESTEasy

RESTEasy
Ahora que hemos visto en artículos anteriores como son los servicios web REST, como realizar uno empleando la librería RESTeasy, como crear un cliente para consumirlo desde el lenguaje de programación java o mediante javascript desde un navegador web y como devolver datos en formato json, xml u otro se nos plantean algunas ideas de como definir la arquitectura de una aplicación.
Apache Tapestry 5

Las aplicaciones web normalmente se organizan en tres capas, la del cliente, la que contiene la lógica de negocio y la capa de base de datos. La capa del cliente está formada por el navegador, la de negocio donde se emplea algún framework y variará según lenguaje de programación que empleemos e incluye a menudo la responsabilidad de presentación transformando los datos de la capa de base de datos en html para el cliente. La capa de base de datos contiene los datos que trata la aplicación ya sea en forma relacional o no relacionan (no-sql).

En algunas aplicaciones es requisito ofrecer una interfaz de la aplicación para que otras aplicaciones se integren con ella, aqui es donde surgen los servicios web y se nos platea la posibilidad de extraer de la capa donde esta la lógica de negocio a una entidad independiente o integrar los servicios web como una parte más de la aplicación web.

En los ejemplos que hemos visto hasta ahora estos los he puesto como una entidad independiente, es decir, que solo ofrece los servicios web. Esta forma tiene la ventaja de que las partes de la aplicación son independientes pero tiene la desventaja de que la complejidad de la aplicación aumenta ya que los servicios web necesitarán de funcionalidades como acceder a la base de datos, inyección de dependencias o de la recarga automática de clases para ver las modificaciones en caliente mientras estamos desarrollando. Todas estas funcionalidades es probable que tengamos ya en la capa de lógica de negocio, con lo que podríamos ponerlo todo junto y aprovecharnos de las funcionadades que ya tenemos en vez de proporcionalas de nuevo en otra entidad independiente. Sin embargo, para tenerlo todo junto deberemos integrar el framework que usermos con la librería o framework que usemos para la aplicación web. En el siguiente ejemplo veremos como hacerlo para el caso de RESTEasy como librería que proporciona la funcionalidad de servicios web y Apache Tapestry como framework para la aplicación web desarrollada en Java.

En Tapestry ya hay una librería desarollada que permite integrarse con RESTEasy, sin embargo, por lo que he visto en su código fuente no permite obtener el cliente javascript como explique en el segundo ejemplo de esta serie de entradas sobre RESTEasy. Con lo que partiendo de su código fuente lo he modificado para que si se permita obtenerlo.

La parte importante de esta integración está en las clases que definene el módulo de la aplicación (AppModule.java) y el filtro de RESTEasy (ResteasyRequestFilter.java) que será el que atrape las peticiones REST. El código está comentado con notas explicativas.

package es.com.blogspot.elblogdepicodev.tapestry.resteasy.services;
import java.util.Collection;
import org.apache.tapestry5.SymbolConstants;
import org.apache.tapestry5.ioc.Configuration;
import org.apache.tapestry5.ioc.MappedConfiguration;
import org.apache.tapestry5.ioc.OrderedConfiguration;
import org.apache.tapestry5.ioc.ScopeConstants;
import org.apache.tapestry5.ioc.ServiceBinder;
import org.apache.tapestry5.ioc.annotations.InjectService;
import org.apache.tapestry5.services.HttpServletRequestFilter;
import es.com.blogspot.elblogdepicodev.resteasy.Application;
import es.com.blogspot.elblogdepicodev.resteasy.HelloWorldResource;
import es.com.blogspot.elblogdepicodev.resteasy.HelloWorldResourceImpl;
public class AppModule {
// Servicios autoconstruidos por Tapestry, Tapestry se encarga de inyectar
// las dependencias que necesiten los servicios de forma automática, ademas
// se encarga del «live class reloading» cuando se hagan modificaciones en
// la clase
public static void bind(ServiceBinder binder) {
// Servicio que inyectaremos como dependencia al servicio REST
// HelloWorldResource
binder.bind(ContadorService.class, ContadorServiceImpl.class).scope(ScopeConstants.DEFAULT);
// La dependencia en el contructor sobre ContadorService es inyectada
// automáticamente por Tapestry. El servicio inyectado podría ser
// cualquier otro que esté definido en el contenedor IoC de Tapestry
// como por ejemplo podría ser el servicio de persistencia (EntityManager).
binder.bind(HelloWorldResource.class, HelloWorldResourceImpl.class);
// Filtro de integración con RESTEasy
binder.bind(HttpServletRequestFilter.class, ResteasyRequestFilter.class).withId("ResteasyRequestFilter");
}
public static void contributeApplicationDefaults(MappedConfiguration<String, String> configuration) {
String production = "false";
configuration.add(SymbolConstants.PRODUCTION_MODE, production);
configuration.add(SymbolConstants.COMPRESS_WHITESPACE, production);
configuration.add(SymbolConstants.COMBINE_SCRIPTS, production);
configuration.add(SymbolConstants.MINIFICATION_ENABLED, production);
configuration.add(SymbolConstants.COMPACT_JSON, production);
configuration.add(SymbolConstants.SUPPORTED_LOCALES, "es");
// Contribuciones que definirán las rutas en las que el filtro de
// RESTEasy atenderá las peticiones REST
configuration.add(ResteasySymbols.MAPPING_PREFIX, "/rest");
configuration.add(ResteasySymbols.MAPPING_PREFIX_JSAPI, "/rest-jsapi");
}
// Añadir el filtro de RESTEasy al pipeline de Tapestry
public static void contributeHttpServletRequestHandler(OrderedConfiguration<HttpServletRequestFilter> configuration,
@InjectService("ResteasyRequestFilter") HttpServletRequestFilter resteasyRequestFilter) {
configuration.add("ResteasyRequestFilter", resteasyRequestFilter, "after:IgnoredPaths", "before:GZIP");
}
public static void contributeApplication(Configuration<Object> singletons, HelloWorldResource helloWorldResource) {
// Contribuir a la configuración del servicio Application con los
// servicios REST
singletons.add(helloWorldResource);
}
// Otra forma de definir un servicio, la colección de singletos proviene de
// las contribuciones hechas en contributeApplication
public static Application buildApplication(Collection<Object> singletons) {
return new Application(singletons);
}
}
view raw AppModule.java hosted with ❤ by GitHub
package es.com.blogspot.elblogdepicodev.tapestry.resteasy.services;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.core.Application;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.ext.Provider;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.ioc.annotations.Symbol;
import org.apache.tapestry5.ioc.services.SymbolSource;
import org.apache.tapestry5.services.ApplicationGlobals;
import org.apache.tapestry5.services.HttpServletRequestFilter;
import org.apache.tapestry5.services.HttpServletRequestHandler;
import org.jboss.resteasy.core.Dispatcher;
import org.jboss.resteasy.core.ResourceMethodRegistry;
import org.jboss.resteasy.core.SynchronousDispatcher;
import org.jboss.resteasy.jsapi.JSAPIWriter;
import org.jboss.resteasy.jsapi.ServiceRegistry;
import org.jboss.resteasy.plugins.server.servlet.HttpRequestFactory;
import org.jboss.resteasy.plugins.server.servlet.HttpResponseFactory;
import org.jboss.resteasy.plugins.server.servlet.HttpServletInputMessage;
import org.jboss.resteasy.plugins.server.servlet.HttpServletResponseWrapper;
import org.jboss.resteasy.plugins.server.servlet.ListenerBootstrap;
import org.jboss.resteasy.plugins.server.servlet.ServletContainerDispatcher;
import org.jboss.resteasy.specimpl.UriInfoImpl;
import org.jboss.resteasy.spi.HttpRequest;
import org.jboss.resteasy.spi.HttpResponse;
import org.jboss.resteasy.spi.Registry;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.jboss.resteasy.util.GetRestful;
public class ResteasyRequestFilter implements HttpServletRequestFilter, HttpRequestFactory, HttpResponseFactory {
private ServletContainerDispatcher servletContainerDispatcher;
private Dispatcher dispatcher;
private ResteasyProviderFactory providerFactory;
private JSAPIWriter apiWriter;
private ServiceRegistry serviceRegistry;
private Pattern restFilterPattern;
private Pattern jsapiFilterPattern;
public ResteasyRequestFilter(@Inject @Symbol(ResteasySymbols.MAPPING_PREFIX) String restFilterPath,
@Inject @Symbol(ResteasySymbols.MAPPING_PREFIX_JSAPI) String jsapiFilterPath, ApplicationGlobals globals, SymbolSource source, Application application)
throws ServletException {
// Inicializar los patrones de las rutas de las peticiones de los servicios
this.restFilterPattern = Pattern.compile(restFilterPath + ".*", Pattern.CASE_INSENSITIVE);
this.jsapiFilterPattern = Pattern.compile(jsapiFilterPath, Pattern.CASE_INSENSITIVE);
// Utilidad para obtener propiedades de configuración
ListenerBootstrap bootstrap = new TapestryResteasyBootstrap(globals.getServletContext(), source);
// Inicializar el contenedor de servicios REST
this.servletContainerDispatcher = new ServletContainerDispatcher();
this.servletContainerDispatcher.init(globals.getServletContext(), bootstrap, this, this);
this.dispatcher = servletContainerDispatcher.getDispatcher();
this.providerFactory = servletContainerDispatcher.getDispatcher().getProviderFactory();
// Añadir los servicios de la aplicación al registro de servicios de RESTeasy
processApplication(application);
ResourceMethodRegistry registry = (ResourceMethodRegistry) globals.getServletContext().getAttribute(Registry.class.getName());
this.serviceRegistry = new ServiceRegistry(null, registry, providerFactory, null);
// Utilidad que generará el javascript con los clientes de los servicios
this.apiWriter = new JSAPIWriter(restFilterPath);
}
@Override
public boolean service(HttpServletRequest request, HttpServletResponse response, HttpServletRequestHandler handler) throws IOException {
// Ruta solicitada
String path = request.getServletPath();
String pathInfo = request.getPathInfo();
if (pathInfo != null)
path += pathInfo;
// Comprobar si la ruta solicitada cumple con el patrón de RESTEasy
if (jsapiFilterPattern.matcher(path).matches()) {
// Petición de clientes javascript
String uri = request.getRequestURL().toString();
uri = uri.substring(0, uri.length() - request.getServletPath().length());
response.setContentType("text/javascript");
apiWriter.writeJavaScript(uri, request, response, serviceRegistry);
return true;
} else if (restFilterPattern.matcher(path).matches()) {
// Petición a un servicio REST
servletContainerDispatcher.service(request.getMethod(), request, response, true);
return true;
}
// La petición no es para un servicio REST, la petición es para otro filtro
return handler.service(request, response);
}
@Override
public HttpRequest createResteasyHttpRequest(String httpMethod, HttpServletRequest request, HttpHeaders headers, UriInfoImpl uriInfo, HttpResponse theResponse,
HttpServletResponse response) {
return createHttpRequest(httpMethod, request, headers, uriInfo, theResponse);
}
@Override
public HttpResponse createResteasyHttpResponse(HttpServletResponse response) {
return createServletResponse(response);
}
protected HttpRequest createHttpRequest(String httpMethod, HttpServletRequest request, HttpHeaders headers, UriInfoImpl uriInfo, HttpResponse theResponse) {
return new HttpServletInputMessage(request, theResponse, headers, uriInfo, httpMethod.toUpperCase(), (SynchronousDispatcher) dispatcher);
}
protected HttpResponse createServletResponse(HttpServletResponse response) {
return new HttpServletResponseWrapper(response, providerFactory);
}
private void processApplication(Application application) {
List<Class> actualResourceClasses = new ArrayList<Class>();
List<Class> actualProviderClasses = new ArrayList<Class>();
List resources = new ArrayList();
List providers = new ArrayList();
if (application.getClasses() != null) {
for (Class clazz : application.getClasses()) {
if (GetRestful.isRootResource(clazz)) {
actualResourceClasses.add(clazz);
} else if (clazz.isAnnotationPresent(Provider.class)) {
actualProviderClasses.add(clazz);
} else {
throw new RuntimeException("Application.getClasses() returned unknown class type: " + clazz.getName());
}
}
}
if (application.getSingletons() != null) {
for (Object obj : application.getSingletons()) {
if (GetRestful.isRootResource(obj.getClass())) {
resources.add(obj);
} else if (obj.getClass().isAnnotationPresent(Provider.class)) {
providers.add(obj);
} else {
throw new RuntimeException("Application.getSingletons() returned unknown class type: " + obj.getClass().getName());
}
}
}
for (Class clazz : actualProviderClasses)
providerFactory.registerProvider(clazz);
for (Object obj : providers)
providerFactory.registerProviderInstance(obj);
for (Class clazz : actualResourceClasses)
dispatcher.getRegistry().addPerRequestResource(clazz);
for (Object obj : resources)
dispatcher.getRegistry().addSingletonResource(obj);
}
}
Con el código de AppModule.java y ResteasyRequestFilter.java conseguimos la integración de Tapestry con RESTEasy y con ella conseguimos varias características que proporciona el contenedor IoC (Inversion of Control), el mero hecho de definir los servicios REST como servicios de Tapestry obtenemos inyección de dependencias y recarga de clases en vivo («live class reloading») que evitará que durante el desarrollo tengamos que estar constantemente parando y arrancando el servidor de aplicaciones para ver los cambios que hagamos con nuestro editor a la clase del servicio, ambas cosas sin que tengamos que hacer nada más. En cuanto a la inyección de dependencias si nos fijamos en el código de servicio REST HelloWorldResourceImpl vemos que tiene un constructor con una referencia a un objeto de la interfaz ContadorService, sin esta referencia el objeto no puede ser construido. El contenedor de dependencias de Tapestry es capaz de inyectar las dependencias necesarias en el constructor para satisfacer sus necesidades sin que tengamos que darle ninguna indicación, él se encargará de buscar en el registro un servicio que implemente esa interfaz, de construirlo (y de construir las dependencias de este servicio si a su vez las tuviese) y de pasarlo como referencia al constructor, encontrará que la clase ContadorServiceImpl lo implementa ya que así lo hemos indicado en el método AppModule.bind. De esta misma forma en los servicios REST podemos hacer uso de cualquier otro servicio, como pudiera ser el servicio persistencia proporcionado por Hibernate o JPA.

package es.com.blogspot.elblogdepicodev.resteasy;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import es.com.blogspot.elblogdepicodev.tapestry.resteasy.services.ContadorService;
public class HelloWorldResourceImpl implements HelloWorldResource {
private ContadorService contadorService;
public HelloWorldResourceImpl(ContadorService contadorService) {
this.contadorService = contadorService;
}
@Override
public String getSaluda() {
contadorService.incrementar();
return "¡Hola mundo!";
}
@Override
public String getSaludaA(String nombre) {
contadorService.incrementar();
return MessageFormat.format("¡Hola {0}!", nombre);
}
@Override
public Mensaje getMensajeJSON(String nombre) {
contadorService.incrementar();
return buildMensaje(nombre);
}
@Override
public Mensaje getMensajeXML(String nombre) {
contadorService.incrementar();
return buildMensaje(nombre);
}
private Mensaje buildMensaje(String nombre) {
return new Mensaje(nombre, "¡Hola mundo!", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
}
}
La página que genera el html (Index.tml) para el navegador con varias llamadas al servicio REST (Index.tml) es prácticamente html normal excepto por un ejemplo de como se usa un componente en Tapestry (). La etiqueta script solicita el javascript con el cliente del servicio web.

<!DOCTYPE html>
<html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd">
<head>
<meta name="author" content="pico.dev"/>
<meta name="description" content="Aplicacion de ejemplo de Apache Tapestry 5 integrado con RESTEasy"/>
<title>Aplicación de ejemplo de Apache Tapestry 5 integrado con RESTEasy</title>
<link href='http://fonts.googleapis.com/css?family=Ubuntu&amp;v1' rel='stylesheet' type='text/css'/>
<style type="text/css">
body {
font-family: 'Ubuntu', arial, serif;
font-size: 12px;
}
</style>
<script type="text/javascript" src="rest-jsapi"></script>
</head>
<body>
<img src="${context:images/tapestry.png}" alt="Apache Tapestry 5" title="Apache Tapestry 5"/><br/>
Versión: <b>${tapestryVersion}</b><br/>
<br/>
<b><t:holaMundo/></b>
<br/>
<br/>
<script type="text/javascript">
alert(HelloWorldResource.getSaluda());
alert(HelloWorldResource.getSaludaA({nombre:'picodotdev'}));
alert(HelloWorldResource.getMensajeJSON({nombre:'picodotdev'}));
alert(HelloWorldResource.getMensajeXML({nombre:'picodotdev'}));
alert(HelloWorldResource.getCuenta());
</script>
</body>
</html>
view raw Index.tml hosted with ❤ by GitHub
Y las peticiones que veríamos en el navegador serían:


Puedes ver, descargar y probar el código fuente del ejemplo desde mi repositorio de GitHub.

Referencia:
http://es.wikipedia.org/wiki/Representational_State_Transfer
Código fuente ejemplo Integración de Apache Tapestry con RESTEasy
Documentación sobre Apache Tapestry