Construyendo APIs modernas en Liferay DXP con Swagger y JAX-RS

Con la llegada de Liferay DXP y los servicios JAX-RS descubrimos que había mundo más allá de JSON-WS y Service Builder. En este artículo os convenceremos para dar el salto a esta nueva tecnología, demostrando cómo su combinación con Swagger y la especificación OpenAPI nos puede ayudar a construir rápidamente APIs REST robustas.

¿JAX-RS? ¿Me he perdido algo?

JAX-RS es una especificación de JEE basada en anotaciones como: @Path, @GET o @POST; que simplifica el desarrollo de APIs REST. Esta especificación tiene varias implementaciones entre las que se encuentra Apache CXF, que es la que vamos a encontrar en Liferay.

La aparición de JAX-RS en Liferay DXP, no es casual, está íntimamente ligada a los nuevos frameworks JS que inundan la web moderna estos días: una especificación aceptada como ésta agiliza el desarrollo de las APIs que consumen últimamente las aplicaciones desarrolladas sobre Angular, React o incluso MetalJS.

Vale, ¿y qué es eso de Swagger?

Cuando hablamos de Swagger nos referimos a un framework sobre el que se desarrollan todo un conjunto de herramientas destinadas a diseñar, documentar y construir APIs REST.

Con Swagger Editor y la especificación OpenAPI podemos diseñar y describir la API que vamos a construir, con Swagger Codegen podemos generar el código tanto de cliente como de servidor en más de 50 lenguajes de programación, y con SwaggerUI podemos probar y ver la documentación de la API que hemos diseñado.

 

Swagger Workflow

Ya podéis ver por dónde van los tiros: Con Swagger abstraemos la lógica de implementación de la propia API a la definición del documento OpenAPI. Con este fichero generamos tanto la documentación como el código que servirá nuestra API REST y proporcionará los DTOs que serán usados en nuestro backend Liferay.

La mejor forma de ver cómo usar swagger en nuestro módulo JAX-RS es con un ejemplo, así que… ¡al lío!

Definiendo una API de ejemplo

Para empezar, vamos a definir la API que vamos a usar en nuestro ejemplo. Crearemos una interfaz para un pequeño gestor de notas que constará de las operaciones clásicas de lectura y escritura. Esta tarea la realizaremos desde el Swagger Editor, en el que podremos describir nuestra API en formato YAML, a la vez que obtenemos la pre-visualización de la documentación generada a partir de esta.

Swagger Editor

La definición de nuestra API incluirá un único endpoint /notes sobre el que podremos ejecutar peticiones GET, POST, y DELETE. Dispondremos también de un modelo Note que incluirá las propiedades id, content y date, además de un modelo NotePayload, usado en la operación de creación y con una única propiedad content.

Dado que el objetivo de este artículo no es profundizar en la especificación OpenAPI, os incluimos a continuación el YAML resultante que podéis importar directamente en Swagger Editor.

swagger: "2.0"
info:
  description: "This is a demo API for managing Notes. The purpose of this API is to show the capabilities of embedding Swagger into Liferay."
  version: "1.0.0"
  title: "Notes API"
host: "localhost:8080"
basePath: "/o/api/v1"
tags:
- name: "notes"
  description: "Everything about your notes"
schemes:
- "http"
paths:
  /notes:
    get:
      tags:
      - "notes"
      summary: "Get all existing notes"
      operationId: "getNotes"
      produces:
      - "application/json"
      responses:
        200:
          description: "Request succesful"
          schema:
            type: "array"
            items:
              $ref: "#/definitions/Note"
    post:
      tags:
      - "notes"
      summary: "Add a new note to our repository"
      operationId: "addNote"
      consumes:
      - "application/json"
      produces:
      - "application/json"
      parameters:
      - in: "body"
        name: "body"
        description: "Note object that needs to be added to to our repository"
        required: true
        schema:
          $ref: "#/definitions/NotePayload"
      responses:
        200:
          description: "Request succesful"
          schema:
            $ref: "#/definitions/Note"
  /notes/{noteId}:
    delete:
      tags:
      - "notes"
      summary: "Removes note from our repository"
      operationId: "removeNote"
      parameters: 
      - name: "noteId"
        in: "path"
        description: "ID of note to delete"
        required: true
        type: "integer"
        format: "int64"
      responses:
        200:
          description: "Request succesful"
        404:
          description: "Note not found"
definitions:
  Note: 
    allOf:
      - $ref: "#/definitions/NotePayload"
      - type: object
        properties:
          id:
            type: "integer"
            format: "int64"
          date:
            type: "string"
            format: "date-time"
            description: "Date when the note was submitted"
  NotePayload:
    type: "object"
    properties:
      content:
        type: "string"
        description: "Note text"

En internet podemos encontrar múltiples alternativas a Swagger Editor que permiten diseñar y documentar APIs basándonos en la especificación OpenAPI, algunas de ellas bastante completas como OpenAPI-GUI

Para obtener el fichero JSON, que usará Swagger Codegen para generar nuestra API, usaremos el menú File del Swagger Editor y seleccionaremos la opción Convert and save as JSON.

Creando nuestro endpoint JAX-RS

El endpoint será el módulo que servirá nuestra API JAX-RS en Liferay. Empezaremos creándolo en un workspace de Liferay, ejecutando el siguiente comando:

blade create -t rest -p com.sample.api -c Rest notes-api

Recordad que gracias a blade disponemos de numerosas templates que nos ayudarán a generar una base código de la que partir en nuestros proyectos.

Si observamos nuestra carpeta modules dentro del workspace de Liferay, veremos que ahora tenemos un nuevo módulo de nombre notes-api. Dentro de la carpeta resources añadiremos el JSON que generamos con Swagger Editor en el punto anterior, quedando la estructura de carpetas como sigue:

Módulo notes-api

Integrando Swagger Codegen en Liferay

Para completar nuestro endpoint necesitamos integrar dos piezas que hasta ahora trabajaban por separado: Swagger y Liferay.

Con la especificación de la API dentro de nuestro módulo, añadiremos una nueva tarea en Gradle que nos permita generar automáticamente los DTOs y las interfaces de nuestra API,  al compilar el proyecto.

Para ello empezaremos añadiendo al script build.gradle las nuevas dependencias que necesitará tanto Swagger Codegen para ser ejecutado dentro de Gradle como el código generado.

dependencies {
	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"
	compileOnly group: 'org.apache.cxf', name: 'cxf-bundle-jaxrs', version: "2.2.9"
	compileOnly group: 'io.swagger', name: 'swagger-jaxrs', version: "1.5.17"
	
	compileInclude group: 'io.swagger', name: 'swagger-annotations', version: "1.5.17"
	compileInclude group: 'com.fasterxml.jackson.jaxrs', name: 'jackson-jaxrs-json-provider', version: "2.7.4"
}

import io.swagger.codegen.DefaultGenerator
import io.swagger.codegen.config.CodegenConfigurator

// Add swagger codegen to our buildscript dependencies
buildscript {
	repositories {
		mavenCentral()
	}
	dependencies {
		classpath("io.swagger:swagger-codegen:2.2.2")
	}
}

A continuación, definiremos una nueva tarea dentro del mismo script llamada generateApi que nos permitirá generar las interfaces y los modelos de nuestra API a través de Swagger Codegen.


// Define task for generating the API models and interfaces
task generateApi {
	doLast {
		def config = new CodegenConfigurator()
		config.setInputSpec("file:///${projectDir}/src/main/resources/swagger.json")
		config.setOutputDir("${projectDir}")
		config.setLang('jaxrs-cxf')
		config.setAdditionalProperties([
				'interfaceOnly': 'true',
				'apiPackage'   : 'com.sample.api',
				'modelPackage' : 'com.sample.api.model',
				'sourceFolder' : 'src/main/java',
				'skipOverwrite': true
		])
		new DefaultGenerator().opts(config.toClientOptInput()).generate()
	}
}

Dentro de esta tarea y a través del objeto CodegenConfigurator podremos definir ciertos aspectos del código generado como: la ubicación del fichero que contiene la especificación OpenAPI, el directorio en el que se escribirán los fuentes generados, o incluso los paquetes a los que pertenecerán esas clases que se generen.

Por último, antes de generar nuestra API deberemos crear un fichero de nombre .swagger-codegen-ignore en el directorio padre de este proyecto. Este fichero evitará que se generen ficheros de maven y otros artefactos fuera del alcance de este artículo. Su contenido será el siguiente:

# Swagger Codegen Ignore
# Use this file to prevent files from being overwritten by the generator.
# The patterns follow closely to .gitignore or .dockerignore.

**/impl/*
**/test/**
pom.xml
README.md

Tras ejecutar la tarea generateApi la estructura de ficheros de nuestro módulo debe quedar como se puede ver en la siguiente imagen:

Módulo notes-api después de generar la API

Los dos modelos Note y NotePayload son simples clases POJO, con anotaciones de Swagger. La interfaz NotesApi representa el endpoint que hemos definido para crear / editar y borrar Notas. Si alguna vez trabajaste con RestControllers de Spring MVC,  te va resultar conocida su estructura puesto que el concepto tiene bastante en común.

Implementando la API con JAX-RS

Con las interfaces y los modelos generados en base a nuestra especificación ya solo nos queda pasar a la implementación de nuestra API.

Empezaremos con la implementación de la clase NotesApi creando la clase NotesApiImpl dentro del paquete com.sample.api.impl. Su implementación a modo de ejemplo será la siguiente:

package com.sample.api.impl;

import com.sample.api.NotesApi;
import com.sample.api.model.Note;
import com.sample.api.model.NotePayload;

import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import javax.xml.datatype.DatatypeConfigurationException;
import javax.xml.datatype.DatatypeFactory;
import java.util.*;

public class NotesApiImpl implements NotesApi {

    private long idCounter = 0;
    private List notes = new ArrayList();

    @Override
    public Note addNote(NotePayload body) {
        // Create note object from payload
        Note note = new Note();
        note.setId(idCounter++);

        try {
            GregorianCalendar gregorianCalendar = new GregorianCalendar();
            gregorianCalendar.setTime(new Date());
            note.setDate(DatatypeFactory.newInstance().newXMLGregorianCalendar(gregorianCalendar));
        } catch (DatatypeConfigurationException e) {
            // Unexpected error. Answering our request with Internal Server Error (500)
            throw new WebApplicationException();
        }
        note.setContent(body.getContent());

        // Add note to our internal list
        notes.add(note);

        return note;
    }

    @Override
    public List getNotes() {

        return notes;
    }

    @Override
    public void removeNote(Long noteId) {
        Optional noteToDelete = notes.stream().filter(note -> note.getId().equals(noteId)).findFirst();

        if (!noteToDelete.isPresent())
        {
            // If note doesn't exist answer the request with Not Found (404)
            throw new WebApplicationException(Response.Status.NOT_FOUND);
        }

        // Otherwise remove note
        notes.remove(noteToDelete.get());
    }
}

Solo nos queda modificar la clase RestApplication que generó Liferay para que use el endpoint que acabamos de generar. Para ello borraremos la implementación de la API por defecto que incluía Liferay y retornaremos en el método getSingletons,  una instancia de la clase NotesApiImpl que acabamos de crear junto a un serializador por defecto para las respuestas que origine la API.


package com.sample.api.application;

import com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider;
import com.sample.api.impl.NotesApiImpl;
import org.osgi.service.component.annotations.Component;

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

@ApplicationPath("/v1")
@Component(immediate = true, service = Application.class)
public class RestApplication extends Application {

	public Set getSingletons() {
		Set singletons = new HashSet();
		
		// Register Jackson JSON serializer and our notes endpoint
		singletons.add(new JacksonJsonProvider());
		singletons.add(new NotesApiImpl());
		
		return singletons;
	}

}

 

Desplegando y probando la solución

Si has llegado hasta aquí estarás deseando probar la API que hemos generado. Lo primero que tenemos que hacer es desplegar el módulo y comprobar que se ha iniciado correctamente.

Para ello usaremos en nuestro módulo la propia tarea deploy de Gradle con blade gw deploy. Si todo ha ido bien, con nuestro bundle arrancado deberíamos ver la siguiente traza de log:

00:00:00,000 INFO [fileinstall-/bundles/osgi/modules][BundleStartStopLogger:35] STARTED com.sample.api_1.0.0 [522]

El siguiente paso será probar la API. Con la configuración por defecto nuestra API se despliega en http://localhost:8080/o/notes-api/v1/, por lo que solo hace falta coger nuestra especificación, abrir Postman y lanzarse a jugar con el endpoint que hemos generado 😉

Probando nuestra API con Postman

 

Deja un comentario

Menú de cierre