Con esta guía vamos a presentar como realizar la paginación en APIs de servicios Rest. ¿Que es la paginación? La paginación es realizar una consulta sobre una colección de entidades (Tablas de BBDD, etc.), donde la consulta solo devuelve un número determinado de elementos y no todos los elementos, a la que llamaremos página. También se proporcionaba los medios para poder obtener: la primera página, la página anterior, la página posterior, la última página, el número total de páginas y el número total de elementos que obtiene la consulta. Esta consulta se puede ordenar y filtrar.
Para poder trabajar con la paginación en los servicios Rest en aplicaciones FundeWeb 2.0, tenemos que añadir la siguiente configuración en el fichero web.xml:
<context-param> <param-name>com.sun.jersey.spi.container.ContainerResponseFilters</param-name> <param-value> org.glassfish.jersey.pagination.PaginationContainerResponseFilter </param-value> </context-param>
Recordemos que el parámetro de contexto com.sun.jersey.spi.container.ContainerResponseFilters se puede indicar el valor como una lista separada por ','. Por lo que si ya existe dicha declaración solo hay que añadir el org.glassfish.jersey.pagination.PaginationContainerResponseFilter.
Para poder trabajar con la paginación en los servicios Rest en aplicaciones FundeWeb 1.5 migradas a Weblogic 12.2, tenemos que añadir la siguiente configuración en el fichero cxf-beans.xml:
<beans ...> <jaxrs:server ...> ... <jaxrs:providers> <ref bean="paginationContainerResponseFilter"/> ... </jaxrs:providers> </jaxrs:server> <bean id="paginationContainerResponseFilter" class="org.umu.atica.jaxrs.pagination.PaginationContainerResponseFilter"/> ... </beans>
Si utilizamos el gestor de problemas, paginationContainerResponseFilter tiene que estar declarado antes que problemContainerResponseFilter.
Aunque no sea estrictamente necesario, os recomendamos configurar Jackson como proveeder de JSON. Aunque tambien se puede con Jettison.
Veamos un ejemplo para obtener los roles de una aplicación donde el resultado solo produce JSON:
@GET @Path( "/roles" ) @Produces( MediaType.APPLICATION_JSON ) public Response obtenerRolesPaginados(@QueryParam(LinkPagination.PAGE_QUERY_PARAM) Integer page, @QueryParam(LinkPagination.LIMIT_QUERY_PARAM) Integer limit) { PaginatedEntity<List<RolDto>> pe = new PaginatedEntity<List<RolDto>>(page, limit); log.info( "Entra en obtenerUsuariosDtoPaginados [page='#0', limit='#1']", page, limit ); ResultQuery<RolDto> datos = servicioRoles.obtenerRolDtoPaginados(null, pe.getFirstResult(), pe.getLimit(), null); pe.setCurrentPage( datos.getResultList() ); pe.setTotalCount( datos.getResultCount() ); return Response.ok(pe).build(); }
Como podéis ver, la petición tiene las siguientes características:
Veamos ahora la llamada al servicio de roles:
public ResultQuery<RolDto> obtenerRolDtoPaginados(Map<String, Object> parameters, int firstResult, int resultLimit, String order) { return rolDtoDataAccessService.obtenerRolDtoPaginados(parameters, firstResult, resultLimit, order); }
El trabajo real se hace en el DAS para el RolDto:
package es.um.atica.prueba.security.authorization.das.interfaces; import java.util.Map; import es.um.atica.jpa.das.DataAccessService; import es.um.atica.jpa.das.ResultQuery; import es.um.atica.prueba.services.rest.pojo.RolDto; public interface RolDtoDataAccessService extends DataAccessService<RolDto> { static final String NAME = "rolDtoDataAccessService"; ResultQuery<RolDto> obtenerRolDtoPaginados(Map<String, Object> parameters, int firstResult, int resultLimit, String order); }
La implementación:
package es.um.atica.prueba.security.authorization.das.impl; import java.util.Arrays; import java.util.Map; import javax.ejb.Local; import javax.ejb.Stateless; import org.jboss.seam.annotations.Name; import es.um.atica.commons.utils.UtilString; import es.um.atica.jpa.das.DataAccessServiceImpl; import es.um.atica.jpa.das.ResultQuery; import es.um.atica.prueba.security.authorization.das.interfaces.RolDtoDataAccessService; import es.um.atica.prueba.security.authorization.entities.Roles; import es.um.atica.prueba.services.rest.pojo.RolDto; @Local( RolDtoDataAccessService.class ) @Stateless @Name( RolDtoDataAccessService.NAME ) public class RolDtoDataAccessServiceImpl extends DataAccessServiceImpl<RolDto> implements RolDtoDataAccessService { private static final String[] RESTRICTIONS_ROL_DTO = { UtilString.cmpTextFilterEjbql("rol.idRol", ":idRol"), }; @Override public ResultQuery<RolDto> obtenerRolDtoPaginados( Map<String, Object> parameters, int firstResult, int resultLimit, String order ) { return resultsByDtoNamedQueryWithDinamicFilter( Roles.ROLE_DTO_ALL, Arrays.asList(RESTRICTIONS_ROL_DTO), parameters, firstResult, resultLimit, order, null, null ); } }
Con la llamada al método resultsByDtoNamedQueryWithDinamicFilter(), mapeamos los datos de la tabla en un DTO. La consulta la tenemos definida en la entidad Roles. Las propiedades del DTO tienen que tener el mismo nombre del alias que tiene en la consulta JPQL.
En estos ejemplos hacemos un mapeo a un DTO partiendo de una consulta JPQL, pero este mapeo también se puede realizar con consultas nativas. Como por ejemplo resultsByDtoNamedNativeQueryWithDinamicFilter().
@NamedQuery(name = Roles.ROLE_DTO_ALL, query = "select rol.idRol as idRol, rol.rolPadre.idRol as rolPadre, rol.descripcion as descripcion from Roles rol") ... public static final String ROLE_DTO_ALL = "Roles.obtenerTodosRolDto";
En la clase RolDto tenemos que tener definidas las propiedades: idRol, rolPadre y descripcion; con sus respectivos métodos de acceso (get/set).
La petición seria la siguiente:
localhost:8001/prueba/rest/roles?page=1&limit=5
Resultado:
[ { "idRol": "GEST", "rolPadre": null, "descripcion": "GESTOR" }, { "idRol": "CUS", "rolPadre": null, "descripcion": "Custom" }, { "idRol": "ADM", "rolPadre": null, "descripcion": "Role ADMIN " } ]
Las cabeceras de la respuesta HTTP tienen la siguiente información relacionada con la paginación:
<localhost:8001/prueba/rest/roles?page=1&limit=5>; rel="first", <localhost:8001/prueba/rest/roles?page=1&limit=5>; rel="last"
Estos datos son así, porque la consulta solo devuelve 3 elementos, y el tamaño de la página (limit) es de 5 elementos.
@GET @Path( "/roles" ) @Produces( { MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML } ) public Response obtenerRolesPaginados(@QueryParam(LinkPagination.PAGE_QUERY_PARAM) Integer page, @QueryParam(LinkPagination.LIMIT_QUERY_PARAM) Integer limit) { PaginatedEntity<RolDtoList> pe = new PaginatedEntity<RolDtoList>(page, limit); log.info( "Entra en obtenerUsuariosDtoPaginados [page='#0', limit='#1']", page, limit ); ResultQuery<RolDto> datos = servicioRoles.obtenerRolDtoPaginados(null, pe.getFirstResult(), pe.getLimit(), null); pe.setCurrentPage( new RolDtoList( datos.getResultList() ) ); pe.setTotalCount( datos.getResultCount() ); return Response.ok(pe).build(); }
Como podéis ver, la petición tiene las siguientes características:
Veamos el código de la clase RolDtoList:
package es.um.atica.prueba.services.rest.pojo; import java.util.List; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; import org.codehaus.jackson.annotate.JsonUnwrapped; @XmlRootElement(name = "roles") public class RolDtoList { private List<RolDto> roles = null; public RolDtoList() {} public RolDtoList(List<RolDto> roles) { this.roles = roles; } /** * @return the usuarios */ @JsonUnwrapped @XmlElement(name="rol") public List<RolDto> getRoles() { return roles; } /** * @param usuarios the usuarios to set */ public void setRoles( List<RolDto> roles ) { this.roles = roles; } }
Las características de la clase:
La petición seria la siguiente:
localhost:8001/prueba/rest/roles?page=1&limit=5
Para obtener el resultado en JSON tenemos que añadir en la petición, la cabecera HTTP Accept con valor application/json. Resultado:
[ { "idRol": "GEST", "rolPadre": null, "descripcion": "GESTOR" }, { "idRol": "CUS", "rolPadre": null, "descripcion": "Custom" }, { "idRol": "ADM", "rolPadre": null, "descripcion": "Role ADMIN " } ]
Si queremos obtener el resultado en XML, tenemos que añadir en la petición, la cabecera HTTP Accept con valor application/xml. Resultado:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <roles> <rol> <descripcion>GESTOR</descripcion> <idRol>GEST</idRol> </rol> <rol> <descripcion>Custom</descripcion> <idRol>CUS</idRol> </rol> <rol> <descripcion>Role ADMIN </descripcion> <idRol>ADM</idRol> </rol> </roles>
Las cabeceras de la respuesta HTTP tienen la siguiente información relacionada con la paginación:
<localhost:8001/prueba/rest/roles?page=1&limit=5>; rel="first", <localhost:8001/prueba/rest/roles?page=1&limit=5>; rel="last"
Estos datos son asi, porque la consulta solo devuelve 3 elementos, y el tamaño de la página (limit) es de 5 elementos.
@GET @Path( "/roles" ) @Produces( { MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML } ) public Response obtenerRolesPaginados(@QueryParam(LinkPagination.PAGE_QUERY_PARAM) Integer page, @QueryParam(LinkPagination.LIMIT_QUERY_PARAM) Integer limit, @QueryParam(LinkPagination.SORT_QUERY_PARAM) String sort) { PaginatedEntity<RolDtoList> pe = new PaginatedEntity<RolDtoList>(page, limit, sort); log.info( "Entra en obtenerUsuariosDtoPaginados [page='#0', limit='#1', sort='#2']", page, limit, sort ); ResultQuery<RolDto> datos = servicioRoles.obtenerRolDtoPaginados(null, pe.getFirstResult(), pe.getLimit(), pe.getProcessedSort(), null); pe.setCurrentPage( new RolDtoList( datos.getResultList() ) ); pe.setTotalCount( datos.getResultCount() ); return Response.ok(pe).build(); }
Como podéis ver, la petición tiene las siguientes características:
La petición seria la siguiente:
localhost:8001/prueba/rest/roles?page=1&limit=5&sort=idRol%3ADESC
El parámetro sort, indica la ordenación en la consulta, el formato es <nombre_propiedad>:(ASC|asc|DESC|desc). La ordenación puede ser opcional, en ese caso no hace falta el separador usar :. Si queremos tener ordenación múltiple, solo tenemos que separar los pares con ;. El valor del parámetro sort tiene que estar codificado, ya que usa los caracteres especiales : (%3A) y ; (%3B). Resultado:
[ { "idRol": "GEST", "rolPadre": null, "descripcion": "GESTOR" }, { "idRol": "CUS", "rolPadre": null, "descripcion": "Custom" }, { "idRol": "ADM", "rolPadre": null, "descripcion": "Role ADMIN " } ]
Las cabeceras de la respuesta HTTP tienen la siguiente información relacionada con la paginación:
<localhost:8001/prueba/rest/roles?page=1&limit=5&sort=idRol%3ADESC>; rel="first", <localhost:8001/prueba/rest/roles?page=1&limit=5&sort=idRol%3ADESC>; rel="last"
Estos datos son asi, porque la consulta solo devuelve 3 elementos, y el tamaño de la página (limit) es de 5 elementos.
Los parámetros para el filtrado, se añaden como parámetros de la petición.
@GET @Path( "/roles" ) @Produces( { MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML } ) public Response obtenerRolesPaginados(@QueryParam(LinkPagination.PAGE_QUERY_PARAM) Integer page, @QueryParam(LinkPagination.LIMIT_QUERY_PARAM) Integer limit, @QueryParam(LinkPagination.SORT_QUERY_PARAM) String sort, @QueryParam("idRol") String idRol ) { PaginatedEntity<RolDtoList> pe = new PaginatedEntity<RolDtoList>(page, limit, sort); log.info( "Entra en obtenerUsuariosDtoPaginados [page='#0', limit='#1', sort='#2', idRol='#3']", page, limit, sort, idRol ); Map<String, Object> parameters = new HashMap<String,Object>(); if (!Strings.isEmpty( idRol )) { parameters.put( "idRol", idRol ); } ResultQuery<RolDto> datos = servicioRoles.obtenerRolDtoPaginados(parameters, pe.getFirstResult(), pe.getLimit(), pe.getProcessedSort()); pe.setCurrentPage( new RolDtoList( datos.getResultList() ) ); pe.setTotalCount( datos.getResultCount() ); return Response.ok(pe).build(); }
Como podéis ver, la petición tiene las siguientes características:
La petición seria la siguiente:
localhost:8001/prueba/rest/roles?page=1&limit=5&idRol%3ADESC&idRol=A
El parámetro idRol contiene el valor para el filtrado de la consulta. Resultado:
[ { "idRol": "ADM", "rolPadre": null, "descripcion": "Role ADMIN " } ]
Las cabeceras de la respuesta HTTP tienen la siguiente información relacionada con la paginación:
<localhost:8001/prueba/rest/roles?page=1&limit=5&idRol%3ADESC&idRol=A>; rel="first", <localhost:8001/prueba/rest/roles?page=1&limit=5&idRol%3ADESC&idRol=A>; rel="last"
Estos datos son asi, porque la consulta solo devuelve 1 elementos, y el tamaño de la página (limit) es de 5 elementos.
La petición seria la siguiente:
localhost:8001/prueba/rest/roles?page=1&limit=1
Queremos la página 1 con páginas de un elemento. Resultado:
[ { "idRol": "ADM", "rolPadre": null, "descripcion": "Role ADMIN " } ]
Las cabeceras de la respuesta HTTP tienen la siguiente información relacionada con la paginación:
<localhost:8001/prueba/rest/roles?page=1&limit=1>; rel="first", <localhost:8001/prueba/rest/roles?page=2&limit=1>; rel="next", <localhost:8001/prueba/rest/roles?page=3&limit=1>; rel="last"
Estos datos son asi, porque la consulta solo devuelve 1 elementos, y el tamaño de la página (limit) es de 1 elementos.
Si la petición es la siguiente:
localhost:8001/prueba/rest/roles?page=2&limit=1
Queremos la página 2 con páginas de un elemento. Resultado:
[ { "idRol": "GEST", "rolPadre": null, "descripcion": "GESTOR" } ]
Las cabeceras de la respuesta HTTP tienen la siguiente información relacionada con la paginación:
<localhost:8001/prueba/rest/roles?page=1&limit=1>; rel="first", <localhost:8001/prueba/rest/roles?page=1&limit=1>; rel="prev", <localhost:8001/prueba/rest/roles?page=3&limit=1>; rel="next", <localhost:8001/prueba/rest/roles?page=3&limit=1>; rel="last"
Estos datos son asi, porque la consulta solo devuelve 1 elementos, y el tamaño de la página (limit) es de 1 elementos.
Si la petición es la siguiente:
localhost:8001/prueba/rest/roles?page=3&limit=1
Queremos la página 3 con páginas de un elemento. Resultado:
[ { "idRol": "CUS", "rolPadre": null, "descripcion": "Custom" } ]
Las cabeceras de la respuesta HTTP tienen la siguiente información relacionada con la paginación:
<localhost:8001/prueba/rest/roles?page=1&limit=1>; rel="first", <localhost:8001/prueba/rest/roles?page=2&limit=1>; rel="prev", <localhost:8001/prueba/rest/roles?page=3&limit=1>; rel="last"
Estos datos son asi, porque la consulta solo devuelve 1 elementos, y el tamaño de la página (limit) es de 1 elementos.
Los parámetros que tiene que tener la petición, son opcionales, no son obligatorios. Los parámetros principales son:
Los parámetros de filtrado son independientes a estos parámetros.
Siempre tenemos que devolver un objeto Response que debe tener como entidad una instancia de PaginatedEntity. Cuando se devuelve SOLO JSON, se puede asignar directamente la lista o array de DTOs a la instancia de PaginatedEntity. Si se devuelve también XML, hay que utilizar POJOs JAXB con la configuración indicada en los ejemplos.
— JUAN MIGUEL BERNAL GONZALEZ 25/06/2020 08:40