API REST en Liferay 7 con JAX-RS

4
13632

En este tutorial veremos cómo montar encima de Liferay 7 una API REST con JAX-RS y los servicios locales de Liferay.



Índice

1. Entorno

  • Hardware: Portátil MacBook Pro 17' (2.66 Ghz Intel Core I7, 8GB DDR3).
  • Sistema Operativo: Mac OS Sierra 10.12.4
  • Entorno de desarrollo: iTerm2 y IntelliJ IDEA 2017.1.3
  • Liferay 7.0 CE GA 3
  • Java JRE 1.8.0.131

2. Introducción

Una API de tipo REST nos aporta muchas ventajas, como simplicidad, fácil exploración y facilidad de uso para los clientes debido a su estandarización.

Muy a nuestro pesar, Liferay no expone directamente una API REST, pero sí unos Web Services en JSON (localhost:8080/api/jsonws) y otros en XML (http://localhost:8080/api/axis) (SOAP). Liferay nos da la posibilidad de crear nuestros propios Web Services usando blade y JAX-RS (Implementación de Apache CXF).

Si queréis ver el código fuente aquí tenéis el link al repositorio de Github

He aquí las posibilidades con las que contamos según los diferentes tipos de servicios:

  • Liferay JSON Web Services
    • Autorización
    • Autenticación
    • Configuración automática
    • Explorador de endpoints
    • Local
      • Cualquier lenguaje
      • Servicios propios
    • Remoto
  • Liferay Axis Web Services
    • Autorización
    • Autenticación
    • Configuración automática
    • Explorador de endpoints
    • Local
      • Cualquier lenguaje
      • Servicios propios
    • Remoto
  • Liferay Web Services propios
    • Autorización
    • Autenticación
    • Configuración automática
    • Local
      • Cualquier lenguaje
      • Servicios propios
    • Remoto

En cuanto a los JSON Web Services y a los de Axis podemos consumirlos dentro de un portlet o aplicación local. Si quisiésemos exponer algún servicio a clientes remotos tendríamos que crear Web Services propios que hagan llamadas a los Web Services de JSON o XML. La otra posibilidad es crear un Web Service que interactúe directamente con los localService de Liferay.

También tener en cuenta que, si desplegamos un Web Service propio, perderemos la autorización y autenticación que disponemos con los otros Web Services, es decir que por ejemplo tendremos que comprobar que el usuario X tiene permiso o no para acceder al recurso Y.

Referencias servicios:

  • Liferay Web Services
  • Seguridad Web Services
  • Generación del WSDD para Web Services Axis
  • Registrando JSON Web Services
  • Creando servicios remotos

3. Generación del módulo

Vamos a proceder a generar un Workspace de Liferay, lo que nos dará muchas facilidades a la hora de desplegar, crear y desarrollar módulos OSGi. Si quieres, puedes ver el tutorial sobre la creación de proyectos, módulos y portlets de Liferay 7 con Blade CLI, donde Javier explica más detalladamente Blade CLI y el Liferay Workspace. Comenzamos a crear el módulo.

Iniciamos con blade el Workspace en el directorio que queramos:

blade init liferay-workspace

Descargamos el bundle de Liferay:

blade gw initBundle

Una vez hecho esto pasamos a crear nuestro módulo. Usaremos una plantilla predefinida para evitarnos el tener que configurarlo todo desde cero. Para ello:

blade create -t rest -p <nombre_paquete> <nombre_proyecto>

Donde nuestro <nombre_paquete> será com.autentia.liferay.rest y nuestro proyecto <nombre_proyecto> será rest-users.

Blade infiere de este comando que nuestro endpoint a la hora de registrarlo en la instancia de Liferay será /rest-users. Si queremos cambiar este endpoint tendremos que modificar los dos archivos de configuración dentro de la carpeta $LIFERAY_WORKSPACE/modules/rest-users/src/main/resources/configuration. Tanto el archivo com.liferay.portal.remote.cxf.common.configuration.CXFEndpointPublisherConfiguration-cxf como com.liferay.portal.remote.rest.extender.configuration.RestExtenderConfiguration-rest. Y modificar el /contextPath a lo que queramos. En mi caso será /rest

Nota: Solamente deja modificar desde código la configuración la primera vez, así que si queremos volver a cambiar este valor, tendremos que hacerlo desde la interfaz de Liferay. (Control Panel / Configuration / System Settings / Foundation / REST Extender y CXF Endpoints)

Una vez hecho esto pasamos a iniciar Liferay:

blade server start

Desde otro terminal y en el directorio de nuestro módulo:

blade deploy

Si introducimos el siguiente comando, veremos el nombre de nuestro módulo en el terminal:

blade sh ss

Liferay REST SS Con Gogo shell

Como se puede observar, el módulo con id 486 es el módulo que acabamos de desplegar.

Nota blade sh hace que nos conectemos a través de Apache Felix a la Gogo Shell, que es la que nos da toda la información de los módulos y nos permite interactuar con ellos.

Ahora, si accedemos a http://localhost:8080/o/rest/greetings en nuestro navegador, podremos ver la respuesta It works!.

Nota: Por defecto todos los módulos OSGi que creamos colgarán del endpoint o (o de OSGi).

4. Arquitectura API REST

Según la arquitectura REST el enfoque cae en los recursos, que son sustantivos en plural (usuarios, roles, artículos, etc.) y sobre estos recursos, dependiendo del método http que usemos se tomarán unas acciones u otras. En el protocolo HTTP tenemos 4 verbos principales: GET, POST, PUT y DELETE. Con estos verbos podemos hacer operaciones CRUD sobre un recurso.

Haremos las peticiones con una extensión de Chrome llamada Postman para comprobar que estas se realizan correctamente.

5. JAX-RS

JAX-RS (Java API for RESTful Web Services) es una especificación de Java que da soporte a la creación de servicios web basados en la arquitectura Representational State Transfer (REST). Hace uso de anotaciones y tiene varias implementaciones, como: Jersey, Apache CXF, RESTeasy, RESTlet, etc.

Liferay usa la implementación de Apache CXF, aunque al ser una especificación al aprender una aprendemos todas. Actualmente JAX-RS va por la versión 2.0 que fue sacada en 2013.

En nuestra clase RestUsersApplication pondremos el siguiente código:

package com.autentia.liferay.rest.application;

import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;

import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

@ApplicationPath("/users") // Indica a JAX-RS el endpoint de nuestro recurso
@Component(immediate = true, service = Application.class)
public class RestUsersApplication extends Application {

    @Reference // Inyectamos nuestra clase recurso mediante una anotación de OSGi
    private RestUserResource restUserResource;

    // Registramos nuestro servicio en OSGi
    @Override
    public Set<Object> getSingletons() {
        super.getSingletons();
        return new HashSet<>(Collections.singletonList(restUserResource));
    }

}

Ahora nos creamos una clase RestUserResource que es donde estará la implementación con los métodos GET, POST, PUT y DELETE, aunque de momento probaremos con el método GET nada más. Además, aquí haremos las llamadas al servicio de Liferay para recoger los datos. Una posible mejora de este programa sería crear una clase de servicio para nuestro RestUserResource, aunque esto lo dejamos a manos del lector.

Dado que usamos el servicio UserLocalService debemos añadir este a gradle. Con lo que en el archivo build.gradle de nuestro módulo incluimos lo siguiente:

dependencies {
    compileOnly "com.liferay.portal:com.liferay.portal.kernel:2.0.0"
    compileOnly group: "javax.ws.rs", name: "javax.ws.rs-api", version: "2.0.1"
    compileOnly group: "org.osgi", name: "org.osgi.service.component.annotations", version: "1.3.0"
}

Nota: compileOnly es parte del plugin de gradle de Liferay, e indica que las dependencias serán recogidas en caliente de la instancia de Liferay, ya que tiene esas librerías expuestas por defecto. Si quisiésemos incluir nosotros una librería externa, debemos usar compileInclude, que hace que compile la librería y además añade la entrada al Manifest.mf. Más información aquí.

Un punto importante a tener en cuenta es que los servicios de Liferay devuelven entidades User mientras que nosotros queremos que devuelva una entidad creada por nosotros, ¿por qué es esto?

Esto es debido a que JAX-RS no tiene conocimientos sobre cómo parsear nuestro POJO a JSON o XML, con lo que hay que anotar nuestra entidad con la etiqueta @XmlRootElement.

Aquí tenemos un problema, ya que nosotros no podemos modificar directamente (y no deberíamos) la entidad User del kernel de Liferay, con lo que nos crearemos un POJO intermedio (RestUser) que mapeará aquellos campos que queremos de User a RestUser.

Por lo que cuando hacemos una llamada a userLocalService tenemos que convertir User a RestUser. En el caso de getUsers() uso un Stream de Java 8 para recorrer el ArrayList de User y por cada uno hago una instancia de RestUser y devuelvo por tanto una lista de RestUser.

He aquí el código de nuestro RestUser:

package com.autentia.liferay.rest.application;

import com.liferay.portal.kernel.model.User;

import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement // Permite parsear nuestra entidad a XML o JSON según el Accept de la petición
public class RestUser {

    private long id;
    private String firstname;
    private String lastname;

    // Importante tener un constructor público vacío para que CXF Apache pueda hacer las instancias correctamente
    public RestUser() {
    }

    RestUser(User user) {
        id = user.getUserId();
        firstname = user.getFirstName();
        lastname = user.getLastName();
    }

    // Los getters y setters deberán ser públicos
    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getFirstname() {
        return firstname;
    }

    public void setFirstname(String firstname) {
        this.firstname = firstname;
    }

    public String getLastname() {
        return lastname;
    }

    public void setLastname(String lastname) {
        this.lastname = lastname;
    }

}

En versiones de JAX-RS anteriores había que crear una clase MessageBodyWriter que se encargaba de parsear nuestra entidad, pero en la nueva versión no hace falta, lo que nos quita mucho boilerplate.

6. GET

En nuestra recién creada clase RestUserResource incluimos el siguiente código:

package com.autentia.liferay.rest.application;

import com.liferay.portal.kernel.model.User;
import com.liferay.portal.kernel.service.UserLocalService;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;

import javax.ws.rs.GET;
import java.util.List;
import java.util.stream.Collectors;

@Component(immediate = true, service = RestUserResource.class) // Nuestro recurso es a su vez un componente de OSGi
public class RestUserResource {

    // Instancia del logger de Liferay para poder loggear información
    private static final Log log = LogFactoryUtil.getLog(RestUserResource.class);

    @Reference // Referencia al servicio de Liferay para usuarios
    private UserLocalService userLocalService;

    @GET // Anotación de JAX-RS que hace que cuando nos conectemos a /users por GET ejecutará este método
    public List<RestUser> getUsers() {
        final List<User> users = userLocalService.getUsers(-1, -1);
        return users.stream().map(RestUser::new).collect(Collectors.toList());
    }

}

Hacemos blade deploy y probamos a hacer una petición desde Postman a http://localhost:8080/o/rest/users (cada vez que modifiquemos nuestro módulo haremos un blade deploy).

Liferay API REST - Petición GET Usuarios

Importante añadir en la sección Headers el campo Accept y poner como valor application/json ya que si no nos dará el siguiente error: No message body writer has been found for class java.util.ArrayList, ContentType: text/html (en Postman no dará error ya que Postman todas las peticiones que hace pone por defecto una cabecera de application/json).

Si cambiamos el valor del campo Accept a application/xml, veremos que devuelve la respuesta en formato XML. Esto es gracias a que tenemos anotado nuestro RestUser con XmlRootElement, de no ser así nos tocaría a nosotros programar la funcionalidad de parseo a XML o JSON.

Vamos a implementar un endpoint que devuelva un único usuario por id. Para ello en la clase RestUserResource añadimos el siguiente método:

@GET
@Path("{id}")
public RestUser getUser(@PathParam("id") long id) throws PortalException {
    return new RestUser(userLocalService.getUserById(id));
}

Nota: La anotación @PathParam recoge de la url el parámetro id. (ej: el 20164 de la url «http://localhost:8080/o/rest/users/20164» y lo mapea a nuestra variable.

Probamos ahora con el Postman:

Liferay API REST - Petición GET Usuario

Sin embargo, si introducimos el id de un usuario que no existe no recibimos un error 404, si no un error 500, lo que indica fallo interno del servidor. Esto es debido a que hacemos un throw en nuestro método.

Hay una forma mejor de manejar las excepciones con JAX-RS y es la siguiente:

@GET
@Path("{id}")
public RestUser getUser(@PathParam("id") long id) {
    try {
        return new RestUser(userLocalService.getUserById(id));
    } catch (PortalException e) {
        log.info(e);
        throw new NotFoundException(e);
    }
}

Nota: Dado que estamos en OSGi depurar a base de prueba y error no es especialmente óptimo, usamos log para ver por consola los errores.

De esta forma, si hacemos una petición sobre un usuario que no existe, recibiremos un error 404.

Aquí tienes un listado de las excepciones de JAX-RS:

Excepción Código de estado Descripción
BadRequestException 400 Mensaje malformado
NotAuthorizedException 401 Fallo en la autenticación
ForbiddenException 403 No hay suficientes permisos
NotFoundException 404 No se ha podido encontrar el recurso
NotAllowedException 405 Método de HTTP no válido
NotAcceptableException 406 Media Type no válido
NotSupportedException 415 Media Type de POST no válido
InternalServerErrorException 500 Error interno del servidor
ServiceUnavailableException 503 Servidor no disponible

Referencia manejo errores JAX-RS

7. POST

El código para crear un User (ojo cuidado, que tenemos que crear un User y no un RestUser) es el siguiente:

@POST
public RestUser postUser(RestUser restUser) {
    try {
        final User user = userLocalService.addUser(
                0,
                PortalUtil.getDefaultCompanyId(),
                true,
                null,
                null,
                true,
                null,
                restUser.getFirstname() + restUser.getLastname() + "@autentia.com",
                0,
                null,
                Locale.ENGLISH,
                restUser.getFirstname(),
                null,
                restUser.getLastname(),
                0,
                0,
                true,
                0,
                1,
                0,
                "",
                null,
                null,
                null,
                null,
                false,
                null
        );
        return new RestUser(user);
    } catch (PortalException e) {
        log.info(e);
        throw new InternalServerErrorException(e);
    }
}

Liferay API REST - addUser

Cosas a tener en cuenta:

  • emailAddress es un campo obligatorio y no puede estar duplicado
  • locale no puede ser nulo
  • birthdayDay empieza en 1, no como birthdayMonth que empieza en 0

Hacemos una petición POST sobre http://localhost:8080/o/rest/users de la siguiente forma:

Liferay API REST - Postman POST User

Por convención una vez se crea o modifica un recurso es conveniente devolver el recurso recién creado o modificado.

8. PUT

@PUT
@Path("{id}")
public RestUser putUser(@PathParam("id") long id, RestUser restUser) {
    try {
        final User user = userLocalService.getUserById(id);
        user.setFirstName(restUser.getFirstname());
        user.setLastName(restUser.getLastname());
        return new RestUser(userLocalService.updateUser(user));
    } catch (PortalException e) {
        log.info(e);
        throw new NotFoundException(e);
    }
}

Nota: Según el método PUT hemos de pasar el recurso entero en el body con todos los campos, aunque estos no sean los que queramos modificar. Si quieres otra opción puedes mirar el método PATCH.

Liferay API REST - Petición PUT Usuario

9. DELETE

@DELETE
@Path("{id}")
public RestUser deleteUser(@PathParam("id") long id) {
    try {
        return new RestUser(userLocalService.deleteUser(id));
    } catch (PortalException e) {
        log.info(e);
        throw new NotFoundException(e);
    }
}

Liferay API REST - Petición DELETE Usuario

10. Conclusión

La inclusión de la arquitectura OSGi en Liferay 7 ha posibilitado la creación de módulos como el que acabamos de crear con mucha facilidad. Además, hemos visto cómo montar una API REST con JAX-RS.

11. Referencias

César Alberca
Nómada Digital y Artesano del Software de España | JavaScript es mi pasaporte al mundo. En Autentia, me centro en crear aplicaciones web robustas usando React, Angular y Vue, con un énfasis especial en pruebas y TypeScript para asegurar la calidad y mantenibilidad. Como ponente internacional y miembro y embajador del comité de Codemotion, disfruto compartiendo mis conocimientos y aprendiendo de la comunidad tecnológica global. Cuando no estoy programando, puedes encontrarme explorando el mundo, presentando Colivers Club; un podcast para nómadas digitales, escalando, jugando juegos de mesa o aprendiendo un hobby nuevo.

4 COMENTARIOS

  1. Excelente articulo. Gracias por el aporte.
    Estoy tratando de consumir un servicio rest usando JAX-RS y he tenido inconvenientes. Tienes algún ejemplo que pueda compartir sobre consumir servicios rest en liferay 7?

  2. Buenas tardes Cesar
    Sin duda alguna es un excelente articulo, pero me surgen un par de preguntas, espero por favor me las pueda solucionar:
    1. Para que hago esto: blade gw initBundle??
    2. Al momento de crear el proyecto con el comando blade init liferay-workspace, me sale este error
    Error: The command init is missing required parameters.
    The following option is required: [–liferay-version | -v] The Liferay product to target. Typing «more» will show all product versions.
    1: dxp-7.2-sp1
    2: dxp-7.1-sp4
    3: dxp-7.0-sp13
    4: portal-7.3-ga3
    5: portal-7.2-ga2
    6: portal-7.1-ga4
    7: portal-7.0-ga7
    8: commerce-2.0.7-7.2
    Cual debo seleccionar. La 1 o la 5 ??

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad