====== Paginación en Servicios Rest ====== 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. ===== Configuración ===== 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//: com.sun.jersey.spi.container.ContainerResponseFilters org.glassfish.jersey.pagination.PaginationContainerResponseFilter 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//: ... ... ... Si utilizamos el gestor de problemas, //paginationContainerResponseFilter// tiene que estar declarado antes que //problemContainerResponseFilter//. Aunque no sea estrictamente necesario, os recomendamos [[fdw:fundeweb:gt:rest:json_provider| configurar Jackson como proveeder de JSON]]. Aunque tambien se puede con Jettison. ==== Ejemplos ==== === Ejemplo cuando solo devolvemos JSON y sin ordenación ni filtrado === 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> pe = new PaginatedEntity>(page, limit); log.info( "Entra en obtenerUsuariosDtoPaginados [page='#0', limit='#1']", page, limit ); ResultQuery 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: * La petición tiene que ser //GET// como suele ser habitual en las APIs Rest para realizar consultas. * La petición devuelve JSON. * Tiene como parámetros: page (el número de página) y limit (el número de elementos a devolver en la página). * La petición devuelve un objeto //Response// con la entidad //PaginatedEntity//. Este objeto se crea pasando el número de pagina, el limite de elementos por página. * Antes de devolver el objeto //Response//, tenemos que establecer en la instancia de la clase //PaginatedEntity//, los elementos que devuelve la consulta y el número total de elementos. * En la clase //PaginatedEntity//, el parámetro de clase es un objeto de //List//, donde //RolDto// es un POJO con la información del rol. El parámetro de clase también podría ser del tipo //RolDto[]//. * Tiene dos constructores, que tienen como parámetros: el indice de la página, el número de elementos para cada página o la ordenación de la consulta. Veamos ahora la llamada al servicio de roles: public ResultQuery obtenerRolDtoPaginados(Map 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 { static final String NAME = "rolDtoDataAccessService"; ResultQuery obtenerRolDtoPaginados(Map 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 implements RolDtoDataAccessService { private static final String[] RESTRICTIONS_ROL_DTO = { UtilString.cmpTextFilterEjbql("rol.idRol", ":idRol"), }; @Override public ResultQuery obtenerRolDtoPaginados( Map 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: * //X-Page-Count// con valor 1 (el número total de páginas). * //X-Total-Count// con valor 3 (el número total de elementos de la consulta). * //Link// con valor (HATEOAS para la navegación entre páginas):; rel="first", ; 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. \\ === Ejemplo cuando devolvemos JSON o XML y sin ordenación ni filtrado === \\ @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 pe = new PaginatedEntity(page, limit); log.info( "Entra en obtenerUsuariosDtoPaginados [page='#0', limit='#1']", page, limit ); ResultQuery 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: * La petición tiene que ser //GET// como suele ser habitual en las APIs Rest para realizar consultas. * La petición devuelve JSON o XML. * Tiene como parámetros: page (el número de página) y limit (el número de elementos a devolver en cada página). * La petición devuelve un objeto //Response// con la entidad //PaginatedEntity//. Este objeto se crea pasando el número de pagina, el limite de elementos por página. * El parámetro de clase en la clase //PaginatedEntity//, es un objeto de la clase //RolDtoList//, que es un objeto JAXB que contiene internamente un objeto //List// que es una lista de pojos con la información del rol. La lista de roles, también podría ser del tipo //RolDto[]// * Antes de devolver el objeto //Response//, tenemos que establecer en la instancia de la clase //PaginatedEntity//, los elementos que devuelve la consulta y el número total de elementos. \\ 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 roles = null; public RolDtoList() {} public RolDtoList(List roles) { this.roles = roles; } /** * @return the usuarios */ @JsonUnwrapped @XmlElement(name="rol") public List getRoles() { return roles; } /** * @param usuarios the usuarios to set */ public void setRoles( List roles ) { this.roles = roles; } } Las características de la clase: * Es un POJO JAXB, tiene la anotación //@XmlRootElement//, que con la propiedad //name// establece el nombre del elemento XML. * Tiene una propiedad //roles// de tipo //List//, que es la lista de roles. * Tiene el método //getRoles()// con la anotación //@XmlElement(name="rol")// para establecer el nombre del elemento XML y la anotación //@JsonUnwrapped// que la utilizamos para que el JSON salga de forma igual a la configuración del ejemplo anterior. 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: GESTOR GEST Custom CUS Role ADMIN ADM Las cabeceras de la respuesta HTTP tienen la siguiente información relacionada con la paginación: * //X-Page-Count// con valor 1 (el número total de páginas). * //X-Total-Count// con valor 3 (el número total de elementos de la consulta). * //Link// con valor (HATEOAS para la navegación entre páginas):; rel="first", ; 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. \\ === Ejemplo cuando devolvemos JSON o XML, con ordenación y sin filtrado === \\ @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 pe = new PaginatedEntity(page, limit, sort); log.info( "Entra en obtenerUsuariosDtoPaginados [page='#0', limit='#1', sort='#2']", page, limit, sort ); ResultQuery 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 tiene que ser //GET// como suele ser habitual en las APIs Rest para realizar consultas. * La petición devuelve JSON o XML. * Tiene como parámetros: page (el número de página), limit (el número de elementos a devolver en la página), sort (la propiedad para la ordenación). * La petición devuelve un objeto //Response// con la entidad //PaginatedEntity//. Este objeto se crea pasando el número de pagina, el limite de elementos por página. * Antes de devolver el objeto //Response//, tenemos que establecer en la instancia de la clase //PaginatedEntity//, los elementos que devuelve la consulta y el número total de elementos. * En la llamada al servicio, el campo de ordenación se debe obtener llamando al método //getProcessedSort()// de la clase //PaginatedEntity//. 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 //:(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: * //X-Page-Count// con valor 1 (el número total de páginas). * //X-Total-Count// con valor 3 (el número total de elementos de la consulta). * //Link// con valor (HATEOAS para la navegación entre páginas):; rel="first", ; 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. \\ === Ejemplo cuando devolvemos JSON o XML, con ordenación y filtrado === 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 pe = new PaginatedEntity(page, limit, sort); log.info( "Entra en obtenerUsuariosDtoPaginados [page='#0', limit='#1', sort='#2', idRol='#3']", page, limit, sort, idRol ); Map parameters = new HashMap(); if (!Strings.isEmpty( idRol )) { parameters.put( "idRol", idRol ); } ResultQuery 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 tiene que ser //GET// como suele ser habitual en las APIs Rest para realizar consultas. * La petición devuelve JSON o XML. * Tiene como parámetros: page (el número de página), limit (el número de elementos a devolver en la página), sort (la propiedad para la ordenación). * La petición devuelve un objeto //Response// con la entidad //PaginatedEntity//. Este objeto se crea pasando el número de pagina, el limite de elementos por página. * Antes de devolver el objeto //Response//, tenemos que establecer en la instancia de la clase //PaginatedEntity//, los elementos que devuelve la consulta y el número total de elementos. * En la llamada al servicio, el campo de ordenación se debe obtener llamando al método //getProcessedSort()// de la clase //PaginatedEntity//. * Pasamos los parámetros para filtrar la consulta en un objeto //Map//, como es habitual. 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: * //X-Page-Count// con valor 1 (el número total de páginas). * //X-Total-Count// con valor 1 (el número total de elementos de la consulta). * //Link// con valor (HATEOAS para la navegación entre páginas):; rel="first", ; 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. \\ === Ejemplo con varias páginas === 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: * //X-Page-Count// con valor 3 (el número total de páginas). * //X-Total-Count// con valor 3 (el número total de elementos de la consulta). * //Link// con valor (HATEOAS para la navegación entre páginas):; rel="first", ; rel="next", ; 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: * //X-Page-Count// con valor 3 (el número total de páginas). * //X-Total-Count// con valor 3 (el número total de elementos de la consulta). * //Link// con valor (HATEOAS para la navegación entre páginas):; rel="first", ; rel="prev", ; rel="next", ; 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: * //X-Page-Count// con valor 3 (el número total de páginas). * //X-Total-Count// con valor 3 (el número total de elementos de la consulta). * //Link// con valor (HATEOAS para la navegación entre páginas):; rel="first", ; rel="prev", ; 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. \\ ==== Resúmen ==== Los parámetros que tiene que tener la petición, son opcionales, no son obligatorios. Los parámetros principales son: * //page//: es el numero de página, sino se pasa o es menor a 1, se le asigna valor 1. Si es mayor al número total de páginas, devuelve una página en blanco, junto a los //links// de la primera y última página. * //limit//: es el numero de elementos por página, sino se pasa o es menor a 1, se le asigna el valor //LinkPagination.MAX_RESULTS// (25). * //sort//: es el parámetro de ordenación. Debe estar codificado y se valida si cumple la expresión regular: //^\w+(\.\w+)*(:(asc|ASC|desc|DESC))?(;\w+(\.\w+)*(:(asc|ASC|desc|DESC))?)*$//. 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. ---- --- //[[juanmiguel.bernal@ticarum.es|JUAN MIGUEL BERNAL GONZALEZ]] 25/06/2020 08:40//