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
- 2. Introducción
- 3. Generación del módulo
- 4. Arquitectura API REST
- 5. JAX-RS
- 6. GET
- 7. POST
- 8. PUT
- 9. DELETE
- 10. Conclusión
- 11. Referencias
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áticaExplorador de endpoints- Local
- Cualquier lenguaje
- Servicios propios
Remoto
- Liferay Web Services propios
AutorizaciónAutenticaciónConfiguració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
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
).
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:
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);
}
}
Cosas a tener en cuenta:
emailAddress
es un campo obligatorio y no puede estar duplicadolocale
no puede ser nulobirthdayDay
empieza en 1, no comobirthdayMonth
que empieza en 0
Hacemos una petición POST sobre http://localhost:8080/o/rest/users
de la siguiente forma:
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.
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);
}
}
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
- https://dennis-xlc.gitbooks.io/restful-java-with-jax-rs-2-0-2rd-edition/en/
- https://www.youtube.com/watch?v=xkKcdK1u95s
- https://www.youtube.com/watch?v=y6rrP6zbfLk
- http://www.xtivia.com/creating-jax-rs-rest-services-liferay-dxp/
- https://en.wikipedia.org/wiki/Representational_state_transfer
- http://www.restapitutorial.com/
- https://en.wikipedia.org/wiki/OSGi
- http://www.javaworld.com/article/2077837/application-development/java-se-hello-osgi-part-1-bundles-for-beginners.html
- https://developer.mozilla.org
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?
Excelente articulo, pero veo que se pierde la seguridad nativa de Liferay no?
¡Muchas gracias por tu comentario! ¿Qué problemas has tenido? ¿La raíz del problema es JAX-RS o de Liferay? Si el problema es debido a JAX-RS te recomiendo esta serie de videos muy bien logrados: https://www.youtube.com/watch?v=xkKcdK1u95s ¡Un saludo!
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 ??