

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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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())); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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&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> |
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