Quantcast
Channel: Adictos al trabajo
Viewing all 996 articles
Browse latest View live

Introducción a GraphQL

$
0
0

Tutorial para empezar a construir un API con GraphQL desde cero.

Índice de contenidos

1. Introducción

En este tutorial vamos a aprender cómo desarrollar una API con GraphQL, una herramienta desarrollada por Facebook que pretende sustituir a la archiconocida RESTfull, aunque en mi opinión es un “tipo de” y no un “ó”.

Estamos ante un nuevo concepto a la hora de desarrollar APIs, ya que GraphQL no es solo una herramienta sino también un lenguaje que busca ser lo más natural posible cuando pedimos un recurso.

Esta herramienta lleva tiempo entre nosotros, pero ha sido hace unas semanas cuando GitHub ha empezado a exponer en producción su API con GraphQL, lo que hará que empiece a ganar popularidad y se extienda entre la comunidad de desarrolladores.

En este tutorial vamos a realizar un pequeño desarrollo con Node.js, ya que creo que es la forma más rápida y simple de aprender los conceptos de GraphQL sin entrar en la complejidad de un lenguaje o preocuparnos demasiado por el servidor. Tampoco veremos cómo consumir la API ya que con las herramientas de desarrollo que nos provee GraphQL es suficiente.

Como he dicho, vamos a utilizar JavaScript, pero GraphQL está disponible para TypeScript, Ruby, Python, Java, Scala, C# (entre otros) y para clientes como React, React Native, Angular 2, Android e iOS. Para consultar todos los lenguajes y herramientas soportadas, podéis dirigiros la web de GraphQL.

El objetivo del tutorial será crear una API cuyo dominio de negocio son las pizzas :). Se podrán consultar las pizzas disponibles, ver sus ingredientes y añadir nuevas pizzas.

2. Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro 15′ (2.5 GHz Intel Core i7, 16GB DDR3, 500GB Flash Storage).
  • Sistema Operativo: Mac OS Sierra 10.12.5
  • Entorno de desarrollo: Visual Studio Code 1.12.2
  • Node.js 7.9.0 y npm 4.2.0
  • Express 4.15.3
  • GraphQL 0.9.6, GraphQL Server for Express 0.7.2 y GraphQL-tools 0.11.0

3. Principales características de GraphQL

Estas son los pilares fundamentales de GraphQL:

  • En ocasiones solo se necesita un único campo de un recurso, pero el servidor devuelve todos, sin embargo, con GraphQL es la aplicación la que tiene el control del recurso, reduciendo el ancho de banda y los tiempos.
  • Con GraphQL se puede acceder a distintos recursos en una única llamada.
  • Se elimina el concepto de endpoints, sustituyéndolo por tipos con los indicar solo lo que se necesita.
  • Se intenta eliminar el versionado de las APIs pudiendo deprecar el uso de atributos.
  • GraphQL desacopla la dependencia con el acceso a las bases de datos.

4. Definir el Schema del API

Esta será probablemente la parte más difícil de un proyecto con GraphQL ya que el Schema definirá las operaciones y tipos que expondrá nuestra API. Como vemos a continuación, cambia la filosofía de RESTful y nos olvidamos de endpoints y verbos.

Primero se definen las posibles operaciones de nuestra API de pizzas. En este proyecto se usa el plugin “babel-plugin-inline-import” para leer ficheros *.graphql y modularizar las definiciones. En caso de no usar este plugin habría que definir el Schema en template strings:

schema {
    query: Query
    mutation: Mutation
}

type Query {
    pizzas (name: [String]): [Pizza]
    ingredients: [Ingredient]
}

type Mutation {
    createPizzas (pizzas: [PizzaInput]): [Pizza]
}

Pasemos a analizar el Schema:

  • schema: sección donde se definen los tipos de operaciones de nuestra API. Las Queries serán las consultas que se podrán realizar y las Mutations la forma en la que realizamos modificaciones en el servidor.
  • Query: se definen dos consultas: pizzas e ingredients:
    • La consulta pizzas puede recibir como parámetros un array de string (name: [String]) y devolverá un array con objetos de tipo Pizza ([Pizza]).
    • La consulta ingredients no recibe parámetros y devolverá un array con objetos de tipo Ingredient ([Ingredient]).
  • Mutation: se define la modificación createPizzas que creará pizzas y que puede recibir un array de objetos de tipo PizzaInput (pizzas: [PizzaInput]) y que devolverá un array de objetos de tipo Pizza ([Pizza]) con las pizzas creadas.

Una vez definidas las operaciones, definiremos los tipos que pueden devolver y recibir:

type Pizza {
    id: Int!
    name: String!
    origin: String
    ingredients: [Ingredient]
}

type Ingredient {
    id: Int!
    name: String!
    calories: String
}

input PizzaInput {
    name: String!
    origin: String
    ingredientIds: [Int]
}
  • Se definen dos tipos Pizza e Ingredient con la palabra reservaba “type”, los cuales vimos que usarán las operaciones anteriores pizzas, ingredients y createPizzas. Viendo el tipo Pizza más a fondo se define:
    • id: se define el atributo id de tipo integer que no puede ser null (nótese el carácter !).
    • origin: se define el atributo origin de tipo string que puede ser null.
    • ingredients: se define el atributo ingredients que será un array de objetos de tipo Ingredient ([Ingredient]).
  • Se define un tipo de entrada PizzaInput con la palabra reservada “input” que será usado por las operaciones de tipo Mutation para recibir parámetros.

Con esto hemos terminado de definir el Schema de nuestra API. Si queréis ver más tipos disponibles podéis dirigiros a la sección Schema de GraphQL.

5. Resolviendo las operaciones del API

Ahora necesitamos decirle a GraphQL cómo resolver las operaciones cuando se reciban peticiones de recurros. Recurriremos a funciones comúnmente llamadas resolvers:

const pizzaResolver = {
    Query: {
        pizzas(root, { name }) {
            // Business logic to get Pizza Object
            return requestedPizzas;
        }
    },

    Pizza: {
        ingredients(pizza) {
            // Business logic to resolve the field "ingredients" when "Pizza" object is requested
            return requestedIngredients;
        }
    },

    Mutation: {
        createPizzas(root, { pizzas }) {
            // Business logic to add new pizza to database
            // The "ingredients" field is autoresolved by GraphQL
            return newPizzas;
        }
    }
};

Pasemos a analizar el resolver

  • Para resolver la Query pizzas de nuestro Schema, tenemos la función pizzas(root, { name }), la cual será llamada cuando se realice dicha Query.
  • Como hemos visto, la Query pizzas puede recibir un parámetro name: [String] y será en el segundo parámetro de la función pizzas(root, { name }) donde lo reciba.
  • Uno de los atributos del tipo Pizza es ingredients y para que GraphQL pueda devolver el array de objetos Ingredient habrá que indicarle cómo resolverlo. Por lo tanto, cuando se pida el atributo ingredients, el resolver Pizza.ingredients(pizza) será invocado, recibiendo cada objeto Pizza encontrado y devolviendo un array de Ingredient.
  • Para resolver la Mutation createPizzas, se invocará a la función createPizzas(root, { pizzas }), la cual recibirá un objeto input PizzaInput y devolverá cada Pizza creada.

Con esto tendríamos definida y resuelta nuestra API y podríamos empezar a usarlo. En el punto 7 os dejo el código completo del proyecto para ver la implementación con más detalle.

Podríamos haber definido nuestro Schema y resolver sin usar el lenguaje GraphQL de la siguiente manera:

import graphql from 'graphql';

let pizzaType = new graphql.GraphQLObjectType({
    name: 'Pizza',
    fields: {
        id: { type: graphql.GraphQLInt },
        name: { type: graphql.GraphQLString },
    }
});

let queryType = new graphql.GraphQLObjectType({
    name: 'Query',
    fields: {
        user: {
            type: pizzaType,
            args: {
                name: { type: graphql.GraphQLList(String) }
            },
            resolve: function (root, { name }) {
                return pizzas;
            }
        }
    }
});

var schema = new graphql.GraphQLSchema({ query: queryType });

6. Probar la API

Ahora que ya tenemos construida nuestra API vamos a ver los resultados obtenidos con unas cuantas líneas de código. No veremos como consumir la API desde clientes ya que GraphQL nos da una herramienta para probar peticiones, GraphiQL.


Esta es la interfaz de GraphiQL y en ella tenemos los siguientes componentes:

  • Una primera sección donde escribir Queries y Mutations. Esta sección se irá autocompletando con lo que vayamos escribiendo e irá haciendo sugerencias de las operaciones y tipos disponibles. Además indicará errores de sintaxis.
  • Si lo deseamos, podemos escribir Queries y Mutations como funciones a las que pasar variables desde la sección “QUERY VARIABLES”. En este tutorial tenemos consultas simples y no usaremos dicha sección pero nos ayudará a realizar consultas más legibles cuando tengan más complejidad.
  • Tenemos una tercera sección en la que poder visualizar los resultados devueltos por el servidor en formato JSON.
  • Por último, tendremos la documentación de nuestra API totalmente autogenerada donde podremos ver todas las posibles operaciones y tipos expuestos. Esto es muy importante ya que nos ahorrará horas de documentación.
  • Añado las herramientas de desarrollo donde podemos observar la URL de la petición (siempre será la misma) y el Payload con las claves “operationName”, “query” y “variables”.

Vamos a ver algunas de las consultas que podemos realizar:






Como se puede observar, se pueden pedir todas o solo determinadas pizzas y en la respuesta podemos indicar solo los campos que necesitemos. También se pueden consultar los ingredientes de las pizzas o incluso acceder a varios recursos a la vez (pizzas e ingredients).

A la hora de añadir pizzas, podremos hacer lo mismo con los resultados obtenidos, podemos filtrarlos e incluso ver como se resuelve el campo ingredients. Además, si uno de los parámetros obligatorios no es enviado, GraphiQL lo indicará como un error y el servidor nos devolverá que la operación ha fallado sin que tengamos que reflejarlo en el código (tan solo indicar en el input del Mutation que el campo no puede ser null).



Os dejo alguna operación más en el README del código fuente del tutorial.

7. Código fuente del tutorial

En mi repositorio de GitHub podéis encontrar todo el código del proyecto del tutorial. Para ejecutarlo solo hay que tener instalado Node.js 7.9.0 y ejecutar los siguientes comandos en un terminal desde el directorio del proyecto: “npm install” y “npm start”.

Tras ejecutar los comandos se podrán hacer peticiones a la API en la URL “http://localhost:3000/pizza_api” o acceder a GraphiQL a través de la URl http://localhost:3000/graphiql.

8. Conclusiones

Como hemos podido ver, GraphQL nos da herramientas para desarrollar un API de forma rápida, natural e independiente del acceso a la base de datos. Además soporta una gran cantidad de lenguajes y clientes.

Durante el desarrollo he podido darme cuenta de que los tipos de schema son limitados ya que el hecho de solo disponer de Query y Mutation limita el tipo de API que podemos construir conceptualmente hablando. Por ejemplo, si quisiéramos implementar un servicio que enviara un email, en mi opinión debería existir un tercer tipo denominado “Service”, ya que no es ni una Query ni un Mutation.

Por otro lado, el hecho de que GraphQL no devuelva todos los campos no implica que la consulta si se los traiga de la base de datos.

Un saludo.

Alejandro

aalmazan@autentia.com


Autodespliegue con jenkins

$
0
0

En este tutorial veremos como configurar jenkins para realizar auto-despliegues.

Índice de contenidos


1. Introducción

Me surgía la necesidad de contaros cómo se realiza la autoinstalación de una aplicación java a través de la ejecución de un script, dentro de un paso de Jenkins. Existe un plugin (Deploy Plugin) que realiza el despliegue de las aplicaciones, pero hemos tenido la necesidad de hacerlo manualmente y aquí os muestro cómo.

Lo encuentro muy útil, muy sencillo y muy cómodo.


2. Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro Retina 15′ (2.2 Ghz Intel Core I7, 16GB DDR3).
  • Sistema Operativo: Mac OS Yosemite 10.10
  • Entorno de desarrollo: jenkins ver.2.7.4

3. Instalación plugin

El primer paso será la instalación del plugin necesario para poder configurar el auto-despliegue en jenkins.

– Jenkins Post build task


4. Configurar plugin

Para configurar este plugin, simplemente, tendremos que ir al final de la configuración de jenkins de nuestro proyecto, sección “Acciones para ejecutar después”. Abrir el desplegable “Añadir una Acción” y seleccionar “Post Build Task”

Ahora configuramos este plugin, para ello, añadimos en el apartado “script”, el path del script que realiza el despliegue.


5. El script

Generamos un script start.sh, en el directorio que hemos indicado en el paso anterior.

start.sh
#!/bin/bash
  echo ".-Deployment Process-."

  numProcess=`ps -ef | grep services-$1.jar | grep -v grep | wc -l`

  # solo ejecuta kill si el proceso esta arrancado
  if [ $numProcess -ne 0 ]; then
     echo "Stopping Beauty..."
     ps -ef | grep nombreApp-$1.jar | grep -v grep | awk '{print $2}' | xargs kill -9
  fi

  if [ $? == 0 ]; then
     echo "Beauty stopped."
     if [ -f services-$1.jar ]; then
        nohup java -jar services-$1.jar --spring.config.location=core/application.yml,service/application.yml > service.log &
        if [ $? == 0 ]; then
           echo "Installing jar..."
           sleep 45
           cat service.log
       	 echo "-------------------------------------------------------------------------------------------------------------"
           echo "DEPLOYMENT SUCCESS"
           echo "-------------------------------------------------------------------------------------------------------------"
           exit 0

        else
           echo "ERROR while trying to deploy service-$1.jar"

        fi
     else
       echo "services-$1.jar don't exist."
     fi
  else
    echo "ERROR while trying to stop the application."
  fi
  exit 1

En este directorio tenemos el archivo services-.XXXX.jar y el script start.sh, en la carpeta “core” están los archivos de configuración de core y en la carpeta “services” los de services. El script start.sh ejecuta los siguientes pasos:

  • Detiene la aplicación services-XXXX.jar
  • Comprueba la existencia de los ficheros de configuración y el fichero jar
  • Despliega el fichero jar
  • Espera 45 segundos para darle tiempo a que se despliegue, que genere el log y así poder mostrarlo en la consola de jenkins.
  • Si todo va bien, muestra el mensaje DEPLOYMENT SUCCESS, si no, muestra el error correspondiente.

Nota: XXXX es la versión de la aplicación a desplegar.


6. Referencias

  • Post Build Task plugin: https://wiki.jenkins-ci.org/display/JENKINS/Post+build+task

7. Conclusiones

Hemos conseguido configurar el despliegue automático de nuestra aplicación, desde ahora, una vez actualizado el código en el repositorio, se construirá la build y al terminar se lanzará la instalación de la aplicación. ¿No me digas que no es cómodo?

¡¡¡Me encanta!!!

Un saludo.

Kike

Test A/B con Optimizely

$
0
0

Índice de contenidos

  1. Introducción
  2. La hipótesis
  3. El test
  4. Resultados
  5. Conclusión
  6. Referencias

1. Introducción

El test A/B es un experimento a través del cual se puede medir el comportamiento de los usuarios sobre dos versiones de una misma web. Es una herramienta sencilla y económica, y permite mejorar la tasa de conversión, ahorrando tiempo y dinero.


2. La hipótesis

La hipótesis es una suposición que permite definir la variación (el cambio que vamos a efectuar). Cuanto mejor formulada esté la hipótesis, más interesantes serán los resultados recogidos por el test. Hay que tener en cuenta el cambio que se quiere realizar y el comportamiento que se pretende obtener.

Pongamos como ejemplo una landing que permite descargar una nueva aplicación móvil. En la versión actual de la web, el call to action que lleva a la página de descargas es un botón del mismo color que el fondo, lo que reduce su visibilidad. Una hipótesis sería:

Si cambio el color de fondo del botón de descargas, es muy probable que aumente el número de personas que haga click en el botón.


3. El test

Es la hora de comprobar si la hipótesis que he formulado antes es cierta. Para ello me registro en https://www.optimizely.com/ con la versión de prueba gratuita e inicio sesión. Optimizely es una herramienta que permite hacer experimentos muy interesantes, pero el objeto de este tutorial es únicamente el Test A/B.


Creo un nuevo proyecto web y le doy un nombre.


Antes de crear el Test debo implementar un fragmento de código en el index de mi aplicación. Puedo obtenerlo en settings>implementation.


También voy a añadir la página sobre la que quiero hacer el experimento. Puedo añadirla desde el menú lateral, en implementation. Le doy un nombre y escribo la url:


Ahora edito la página que acabo de crear para añadir un evento al botón que quiero modificar y trackear.


Selecciono el botón que quiero trackear y le doy a crear evento. Le doy un nombre y una categoría, y guardo el evento.

Ya estamos listos para crear el test a/b. Para ello accedo a Experimentos en el menú lateral.

Y en el desplegable selecciono la opción de crear un nuevo experimento.


Para crear el test necesitamos indicarle una serie de parámetros. Primero le damos un nombre a nuestro test y, a continuación, formulamos la hipótesis.


En Pages añadimos la página que hemos creado previamente:


Metrics el evento que definimos con anterioridad para ser trackeado.

Por último, personalizo el nombre de las variaciones y no efectúo más cambios, ya que solo quiero introducir una diferencia y que se muestre el mismo número de veces que la original. A pesar de que este tipo de aplicaciones permiten introducir más de una variación en una misma página, no es recomendable pues se corre el riesgo de perder el foco y disminuye la fiabilidad de los resultados.

Creo el experimento.


Selecciono la “Variación Botón verde” y realizo el cambio que planteaba en la hipótesis.


Comienzo el experimento pulsando Start Experiment. Esto significa que desde este momento mis usuarios verán uno u otro botón, indistintamente.


4. Resultados

Desde el momento en que comience mi experimento, podré ver los resultados y establecer filtros. En este caso, puedo ver cómo la variación que he hecho supone una mejora del 125 %. No obstante, debo destacar que deberíamos tener un 95 % de confianza para que estos resultados sean válidos a la hora de tomar una decisión definitiva, es decir, que los resultados no se deban al azar.



5. Conclusión

Como habéis visto, en poco más de 5 minutos podemos tener un test a/b funcionando y con resultados en tiempo real. Si investigáis por vuestra cuenta, veréis que existen muchos parámetros con los que jugar y experimentos que realizar. Os invito a realizar un tutorial sobre alguno de ellos.

Por otro lado, sería interesante que contrastaseis con test en laboratorio los resultados obtenidos para que estéis seguros de los cambios que vais a efectuar.


6. Referencias

Tests unitarios, de integración y de aceptación en Angular con Jasmine, Karma y Protractor

$
0
0

En este tutorial vamos a hablar de un tema que como desarrolladores deberíamos tener presente en cualquier tecnología que estemos utilizando para implementar y validar nuestras soluciones: los tests.

Índice de contenidos


1. Entorno

Este tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil Mac Book Pro 15″ (2,3 Ghz Intel Core i7, 16 GB DDR3)
  • Sistema Operativo: Mac OS Sierra
  • VSCode 1.12.2
  • @angular/cli 1.0.6
  • jasmine 2.5.2
  • protrator 5.1.0
  • karma 1.4.1

2. Introducción

Este tutorial parte como explicación en texto de lo que se vio en el taller de testing de NgLabs en el marco del Meetup de Angular Madrid y donde Jorge Baumann (@baumannzone) grabó un screencast con los pasos que dimos en el taller para la implementación de los tests. Que podéis seguir en este enlace: Taller de testing con Angular.

Cuando hablas con desarrolladores te das cuenta de que muy pocos saben y hacen tests de sus implementaciones. Entonces, les pregunto ¿cómo me demuestras que lo que has hecho está bien? Siempre me suelen contestar “pues mira abro la aplicación, pincho en el botón y sale lo que tiene que salir”, me dicen orgullosos. Entonces es cuando les digo y si te pido que esto me lo demuestres cada vez que hagas un cambio en el código junto con las otras mil historias que has implementado para saber que esto se puede poner en producción… entonces es cuando algunos cambian el rictus y otros, los más atrevidos, dicen “bueno pero esto ya lo validará el equipo de QA que para eso está”.

Y ese, amigos, es uno de los mayores problemas de las grandes compañías que tienen un equipo de QA, que encima no automatiza las pruebas con lo que cual el tiempo desde que una persona de negocio tiene una superidea, que va a reportar millones a su empresa, es directamente proporcional al tiempo que el equipo de QA, con sus pruebas en Excel supercurradas, va a tardar en validar ese desarrollo para su puesta en producción; perdiendo de esta forma la ventana de oportunidad y por tanto la ganancia de la idea.

¿Cómo podemos los desarrolladores minimizar este periodo al máximo? La respuesta es sencilla… estando seguros en todo momento que lo que se va a subir a producción es funcional y técnicamente correcto desde la fase de desarrollo; y esto solo se puede conseguir con tests y procesos automáticos que podamos repetir una y otra vez. De esta forma podríamos hacer subidas a producción con total confianza varias veces al día si fuera necesario.

En este tutorial voy a aportar mi granito de arena para todos los que desarrollan con el framework JavaScript de moda, por su sencillez, flexibilidad y productividad: Angular.


3. Preparación del entorno

Uno de los puntos clave a la hora de implementar tests con Angular es la configuración adecuada del entorno con Jasmine, Karma y Protractor; que gracias al maravilloso @angular/cli ya tenemos de serie; así que simplemente tenemos que tener una instancia de NodeJS con npm y ejecutar:

$> npm install -g @angular/cli
Y creamos un proyecto, en el taller creamos el siguiente:
$> ng new nglabs

Ahora abrimos el proyecto con un editor de textos, a mí el que más me gusta es Visual Studio Code porque tiene una integración perfecta con TypeScript y gracias a estos plugins favorece nuestra productividad: (sé de más de uno que en el taller empezó con otro y se pasó rápido a VSCode)

  • Auto Import: nos quita de lo más tedioso de trabajar con TypeScript que es hacer los imports necesarios.
  • Angular Language Service: nos permite el autocompletado en los ficheros en los templates de los componentes y marca error cuando interpolamos una variable que no está definida como atributo.
  • TSLint: en el propio editor nos marca los errores de estilo y como se integra con codelyzer nos aplica las reglas de estilo definidas por el equipo de Angular.
  • Sort TypeScript Imports: nos da un atajo de teclado (o al salvar el fichero) para organizar los imports que utilicemos.
  • TypeScript Hero: nos facilita un atajo de teclado para eliminar todos los imports que no se estén utilizando en el fichero.
  • vscode-icons: muestra un icono distinto en función de la naturaleza del fichero, ayuda a identificar más rápidamente los ficheros.
  • bma-coverage: esta es una extensión especifica de testing que se integra con el fichero lcov generado al ejecutar los tests con el flag –code-coverage para marcar en el código si esa línea tiene cobertura o no.

Después de importar estas extensiones algunas de ellas llevan una configuración adicional dentro del fichero Preferences –> Settings que tiene que quedar de esta forma:

{
    "window.zoomLevel": 3,
    "workbench.iconTheme": "vscode-icons",
    "typescript.extension.sortImports.sortOnSave": true,
    "tslint.autoFixOnSave": true,
    "files.autoSave": "off",
    "bma-coverage":{
        "lcovs":[
            "./coverage/lcov.info"
        ]
    }
}

A destacar la propiedad “tslint.autoFixOnSave” que aplica todas las normas del fichero tslint automáticamente al guardar el fichero.

Probamos los tests que vienen de serie cuando creamos el proyecto con angular-cli con el comando:

$> npm run test -- --code-coverage

Nota que no he usado directamente el comando ng de angular-cli sino con npm ejecutando la tarea que viene definida en la sección “scripts” del package.json; de esta forma nos aseguramos de estar usando la versión de angular-cli local y no la que tengamos instalada de forma global para generarlos. Además incluyo la opción –code-coverage con lo que verás que genera la carpeta coverage con los informes en HTML y el fichero lcov.info.

Como ves todos los tests están en verde así que tenemos un buen punto de partida.


4. Vamos al lío

Vamos a desarrollar una aplicación que recupere los primeros usuarios de GitHub atacando al API (https://api.github.com/users) y por cada uno muestre por pantalla los campos: login, avatar, url y admin. Para ello lo primero que vamos a hacer es crear el componente que se encargará de mostrarlos por pantalla, gracias al angular-cli esto es tan sencillo como ejecutar:

$> npm run ng -- generate component list-users

Esto nos va a crear una carpeta con los ficheros del componente, entre ellos un .spec que como la mayoría no sabe lo que es, tiende a borrarse de forma inmediata, pero para eso está este tutorial. 😉

Ahora pensamos en la solución y, por favor, que a nadie se le ocurra utilizar el servicio Http directamente en el componente. Yo, este tipo de componentes los estructuro en tres capas: un servicio de proxy que solo tiene como misión conectar con el API y devolver la respuesta, un servicio de adaptación de la respuesta que viene del servidor al modelo de mi aplicación y el componente que se encarga de visualizar esta información por pantalla.

Por tanto creamos el servicio de proxy que inicialmente, como no va a ser utilizado por nadie más, lo vamos a incluir dentro de la carpeta list-users. Para ello ejecutamos:

$> npm run ng -- generate service list-users/list-users-proxy

Y lo implementamos de esta forma:

import { environment } from '../../environments/environment';
import { Http, Response } from '@angular/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';


@Injectable()
export class ListUsersProxyService {

  constructor(private http: Http) { }

  getUsers(): Observable {
    return this.http.get(`${environment.api}/users`);
  }

}

Fíjate que el método devuelve un Observable con la Response de Angular y no hace ni debe hacer más lógica que esta. Ahora lo único que queremos verificar es que la llamada física se está haciendo correctamente, por lo tanto, tenemos que implementar un test de integración que lo verifique y no tiene sentido que hagamos un test unitario de esta parte. Así que en el fichero .spec asociado a este servicio de proxy vamos a realizar y verificar la llamada de esta forma:

import { async, inject, TestBed } from '@angular/core/testing';
import { HttpModule } from '@angular/http';
import { ListUsersProxyService } from './list-users-proxy.service';


describe('ListUsersProxyServiceIT', () => {
  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpModule],
      providers: [ListUsersProxyService]
    });
  });

  it('should be created', inject([ListUsersProxyService], (service: ListUsersProxyService) => {
    expect(service).toBeTruthy();
  }));

  it ('should get users', async(() => {
    const service: ListUsersProxyService = TestBed.get(ListUsersProxyService);
    service.getUsers().subscribe(
      (response) => expect(response.json()).not.toBeNull(),
      (error) => fail(error)
    );
  }));
});

Te habrás dado cuenta de que el 80% de este código ya nos lo había proporcionado Angular y que nuestra labor como desarrolladores “solo” se limita a configurar la clase TestBed con todas las dependencias necesarias, en este caso, la importación de HttpModule porque estamos usando el servicio Http y subscribirnos a la llamada para verificar el resultado. En este tipo de tests no hay que ser muy específico en los expects ya que los datos de la llamada pueden variar con frecuencia.

Podemos ejecutar el comando de test para verificar que efectivamente el test pasa; pero cuidado porque si olvidas el “async” que envuelve la función puedes estar incurriendo en un falso positivo dado que la parte asíncrona del test no se estará ejecutando. Para evitar esto te aconsejo que pongas un console.log inicialmente y veas que realmente se muestra en la consola.

Listo nuestro primer test de integración y no ha dolido mucho a que no. 😉

Tienes que tener en cuenta que una de las cosas que más complica este tipo de tests es la asincronía así que el truco está en eliminar esta asincronía en el resto de tests para lo cual vamos a crear un fake del servicio de proxy. Para crear el fake tenemos que tener en cuenta que cumpla con la misma signatura de la función que estamos utilizando, esto es, que devuelva una Observable de tipo Response, pero lo que no hacemos es inyectar el servicio Http sino que creamos un Observable síncrono con los datos de la respuesta real, la cual establecemos como constante en un fichero llamado “list-users.fake.spec.ts” (dejamos la extensión .spec para que no se incluya en el código de producción)

export const LIST_USERS_FAKE = [
  {
    'login': 'mojombo',
    'id': 1,
    'avatar_url': 'https://avatars3.githubusercontent.com/u/1?v=3',
    'gravatar_id': '',
    'url': 'https://api.github.com/users/mojombo',
    'html_url': 'https://github.com/mojombo',
    'followers_url': 'https://api.github.com/users/mojombo/followers',
    'following_url': 'https://api.github.com/users/mojombo/following{/other_user}',
    'gists_url': 'https://api.github.com/users/mojombo/gists{/gist_id}',
    'starred_url': 'https://api.github.com/users/mojombo/starred{/owner}{/repo}',
    'subscriptions_url': 'https://api.github.com/users/mojombo/subscriptions',
    'organizations_url': 'https://api.github.com/users/mojombo/orgs',
    'repos_url': 'https://api.github.com/users/mojombo/repos',
    'events_url': 'https://api.github.com/users/mojombo/events{/privacy}',
    'received_events_url': 'https://api.github.com/users/mojombo/received_events',
    'type': 'User',
    'site_admin': false
  },
  {
    'login': 'defunkt',
    'id': 2,
    'avatar_url': 'https://avatars3.githubusercontent.com/u/2?v=3',
    'gravatar_id': '',
    'url': 'https://api.github.com/users/defunkt',
    'html_url': 'https://github.com/defunkt',
    'followers_url': 'https://api.github.com/users/defunkt/followers',
    'following_url': 'https://api.github.com/users/defunkt/following{/other_user}',
    'gists_url': 'https://api.github.com/users/defunkt/gists{/gist_id}',
    'starred_url': 'https://api.github.com/users/defunkt/starred{/owner}{/repo}',
    'subscriptions_url': 'https://api.github.com/users/defunkt/subscriptions',
    'organizations_url': 'https://api.github.com/users/defunkt/orgs',
    'repos_url': 'https://api.github.com/users/defunkt/repos',
    'events_url': 'https://api.github.com/users/defunkt/events{/privacy}',
    'received_events_url': 'https://api.github.com/users/defunkt/received_events',
    'type': 'User',
    'site_admin': true
  }
]`

Y ahora lo usamos en el fake “list-users-proxy.service.fake.spec.ts” con el siguiente contenido:

import 'rxjs/add/observable/of';
import { LIST_USERS_FAKE } from './list-users.fake.spec';
import { Response, ResponseOptions } from '@angular/http';
import { Observable } from 'rxjs/Observable';


export class ListUsersProxyServiceFake {

  getUsers(): Observable {
    const responseOptions: ResponseOptions = new ResponseOptions({
      body: LIST_USERS_FAKE
    });
    const response: Response = new Response(responseOptions);
    return Observable.of(response);
  }

}

De este modo cuando invoquemos al método usando esta implementación no se llamará al servicio real pero la respuesta será la misma a todos los efectos.

Es el momento de crear nuestro modelo que como he comentado anteriormente, tiene 4 campos. En este caso nos podemos plantear si construir el modelo con una interfaz o con una clase. La diferencia reside en que con la interfaz no añades más código a la aplicación, es simplemente para que el IDE te pueda autocompletar este tipo de datos; mientras que con la clase sí estás añadiendo código real y no es tan flexible a la hora de inicializar los datos a través del constructor.

A mí últimamente me gusta más hacerlo con interfaces así que la podemos crear con el siguiente comando:

$> npm run ng -- generate interface list-users/user

Con el siguiente contenido:

export interface User {
    login: string;
    avatar: string;
    url: string;
    admin: boolean;
}

El siguiente paso es crear el servicio que adapta los datos de la respuesta al modelo. Para ello ejecutamos:

$> npm run ng -- generate service list-users/list-users

Este servicio va a inyectar el proxy y gracias a la programación reactiva que ofrece la librería rxjs podemos fácilmente hacer el mapeo entre los campos de la respuesta y el modelo de nuestro negocio que, como es nuestro caso, no tienen por qué coincidir y nos permite desacoplarnos de la respuesta y dar un sentido semántico al desarrollo que facilita la legibilidad y el mantenimiento de la aplicación. El contenido de este servicio es el siguiente:

import 'rxjs/add/operator/map';
import { ListUsersProxyService } from './list-users-proxy.service';
import { User } from './user';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';


@Injectable()
export class ListUsersService {

  constructor(private proxy: ListUsersProxyService) { }

  getUsers(): Observable {
    return this.proxy.getUsers().map(
      (response) => {
        const listUsers: User[] = [];
        const data = response.json();
        data.forEach(d => {
          const user: User = {
            login: d.login,
            avatar: d.avatar_url,
            url: d.url,
            admin: d.site_admin
          };
          listUsers.push(user);
        });
        return listUsers;
      }
    );
  }

}

Ahora vamos a configurar e implementar el test asociado que como vamos a utilizar el fake será un test unitario no haciendo falta la implementación de un test de integración. Este es el contenido del test:

import { ListUsersProxyService } from './list-users-proxy.service';
import { ListUsersProxyServiceFake } from './list-users-proxy.service.fake.spec';
import { ListUsersService } from './list-users.service';
import { inject, TestBed } from '@angular/core/testing';


describe('ListUsersService', () => {
  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        ListUsersService,
        {provide: ListUsersProxyService, useClass: ListUsersProxyServiceFake}
      ]
    });
  });

  it('should be created', inject([ListUsersService], (service: ListUsersService) => {
    expect(service).toBeTruthy();
  }));

  it('should get users', () => {
    const service: ListUsersService = TestBed.get(ListUsersService);
    service.getUsers().subscribe(
      (users) => {
        expect(users[0].login).toEqual('mojombo');
        expect(users[0].avatar).toBeDefined();
      }
    );
  });
});

Fíjate que la clave está en la definición del provider donde establecemos que la implementación la proporcione el fake creado, de esta forma no necesitamos la función async y podemos ser más específicos en los expects dado que esta respuesta sí la estamos controlando, y solo queremos verificar que el mapeo de campos se está haciendo de forma adecuada. Ejecutamos el comando de test y vemos que todos los tests van pasando y que tenemos un buen grado de cobertura.

Teniendo ya los servicios implementados y probados, es el momento de implementar nuestro componente. El cual dentro del método ngOnInit va a establecer el valor del atributo “users” que será un array de tipo User. No olvidéis establecer la suscripción para poder desubscribir y así evitar posibles “memory leaks”.

import { ListUsersService } from './list-users.service';
import { User } from './user';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Subscription } from 'rxjs/Subscription';

@Component({
  selector: 'app-list-users',
  templateUrl: './list-users.component.html',
  styleUrls: ['./list-users.component.css']
})
export class ListUsersComponent implements OnInit, OnDestroy {

  users: User[];

  subs: Subscription;

  constructor(private service: ListUsersService) { }

  ngOnInit() {
    this.subs = this.service.getUsers().subscribe(
      users => this.users = users
    );
  }

  ngOnDestroy() {
    this.subs.unsubscribe();
  }

}

Y en el template podemos establecer el siguiente contenido atendiendo a poner los ids adecuados que faciliten los tests de aceptación. Un posible contenido (sin mucho estilo, aquí es donde digo que entrarían los diseñadores con sus componentes de Polymer supercurrados donde el desarrollador solo tiene que pasarle una estructura de datos definida para que los datos se pinten de forma corporativa y mucho más bonita) podría ser este:

{{user.login}}

{{user.url}}

{{user.admin | admin}}

avatar

Ahora implementamos el test asociado donde es muy importante que lo limitemos a verificar que los atributos del componente se establecen adecuadamente y no empecemos a liarnos a verificar elementos del DOM que van a hacer que nuestro tests sea mucho más frágil. El contenido del test unitario sería este:

import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ListUsersComponent } from './list-users.component';
import { ListUsersProxyService } from './list-users-proxy.service';
import { ListUsersProxyServiceFake } from './list-users-proxy.service.fake.spec';
import { ListUsersService } from './list-users.service';
import { NO_ERRORS_SCHEMA } from '@angular/core';


describe('ListUsersComponent', () => {
  let component: ListUsersComponent;
  let fixture: ComponentFixture;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ ListUsersComponent ],
      providers: [
        ListUsersService,
        {provide: ListUsersProxyService, useClass: ListUsersProxyServiceFake}
      ],
      schemas: [NO_ERRORS_SCHEMA]
    })
    .compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(ListUsersComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  afterEach(() => {
    component.ngOnDestroy();
  });

  it('should be created', () => {
    expect(component).toBeTruthy();
  });

  it('should set users', () => {
    component.ngOnInit();
    expect(component.users[0].login).toEqual('mojombo');
  });

});

La propia implementación por defecto ya nos ofrece las instancias de fixture (para comprobar el DOM) y component que no es más que la instancia del componente que nos permite llamar a los métodos y verificar los atributos.

¡Pues ya está! Nuestra aplicación implementada y probada. Ahora cualquier cambio no nos dará pánico porque habrá un test que nos dirá si estamos rompiendo funcionalidad y estaremos mucho más confiados a la hora de poner nuestros desarrollos en producción. Recuerda más vale 10 subidas pequeñas al día controlados que una cada 3 meses con un montón de funcionalidad que no da tiempo a verificar en el momento de pasar a producción.

Ahora podemos configurar apropiadamente nuestro fichero “app.module.ts” con el siguiente contenido:

import { AppComponent } from './app.component';
import { BrowserModule } from '@angular/platform-browser';
import { HttpModule } from '@angular/http';
import { ListUsersComponent } from './list-users/list-users.component';
import { ListUsersProxyService } from './list-users/list-users-proxy.service';
import { ListUsersService } from './list-users/list-users.service';
import { NgModule } from '@angular/core';


@NgModule({
  declarations: [
    AppComponent,
    ListUsersComponent
  ],
  imports: [
    BrowserModule,
    HttpModule
  ],
  providers: [ListUsersService, ListUsersProxyService],
  bootstrap: [AppComponent]
})
export class AppModule { }

Por lo que ya podemos arrancar nuestra aplicación con el comando:

$> npm run start

Y verificar que la funcionalidad es correcta. Este es un punto fundamental en los tests de aceptación que necesitan que la aplicación esté desarrollada y corriendo.

Angular almacena los tests de aceptación en la carpeta e2e y maneja el patrón Page Object donde tenemos un fichero .po que almacena las funciones de acceso al DOM y otros .spec que implementan los tests haciendo uso de los .po.

De este modo lo primero es crear el fichero list-users.po.ts dentro de la carpeta e2e con el siguiente contenido, donde a través de los elementos de protractor nos quedamos con la instancia del DOM del primer usuario a través del id que le hemos puesto.

import { browser, by, element } from 'protractor';

export class ListUsersPage {
  navigateTo() {
    return browser.get('/');
  }

  getFirstUser() {
    return element(by.id('user-0'));
  }

}

Ahora creamos el fichero “list-users.spec.ts” donde hacemos el flujo de cargar la aplicación y verificar que el primer usuario es ‘mojombo’

import { browser } from 'protractor';
import { ListUsersPage } from '../pos/list-users.po';

describe('nglabs List Users', () => {
  let page: ListUsersPage;

  beforeEach(() => {
    page = new ListUsersPage();
  });

  it('should check first user is mojombo', () => {
    page.navigateTo();
    page.getFirstUser().getText().then(
      text => {
        expect(text).toContain('mojombo');
      }
    );
    expect(page.getFirstUser().getText()).toContain('mojombo');
  });

  afterEach(() => {
    browser.driver.sleep(3000); //Esto es solo para que nuestro ojo humano pueda ver el resultado en el navegador
  });

});

Ahora podemos ejecutar estos tests con el comando:

$> npm run e2e

Obviamente este tipo de tests son los más frágiles pero sí que nos valen para registrar los flujos más críticos de nuestra aplicación y lanzarlos como “smoke tests” en cualquier entorno para verificar que una subida a producción se ha hecho de forma satisfactoria, por ejemplo. Esto es mucho más rápido y efectivo que 10 equipos quedando a las 4.00 am para subir a producción y de 4.05 am a varias horas después están verificando manualmente que no han roto nada (esto lo he vivido en cliente); cuando con este tipo de tests en cuestión de minutos y de forma automática está verificado y si fallan se puede configurar para hacer un rollback a la versión anterior.

Nota: es posible que tengáis que adaptar los ficheros de tests que vienen con la configuración inicial, simplemente borrad todos aquellos casos de tests que ya no tengan validez.

5. Conclusiones

Como ves no es tan complicado hacer las cosas bien y vivir mucho más tranquilos dejando el temor de pasar a producción y ayudando a que el mantenimiento sea mucho menos costoso. Pudiendo hacer realidad las fantasías de cualquier persona de negocio de ver su idea en producción, realmente, lo antes posible.

Si te quedan dudas de cómo hacer esto o quieres que te ayudemos, contáctanos y hablamos.

Cualquier duda o sugerencia en la zona de comentarios.

Saludos.

Una aproximación a la usabilidad en web y móviles

$
0
0

Resumen de No me hagas pensar (Don’t make me think) para iniciarse en la usabilidad en web y móviles.

Índice de contenidos

1. Introducción

Una buena forma de iniciarse en usabilidad es a través del libro Don’t make me think o lo que es lo mismo, No me hagas pensar de Steve Krug. Un pequeño manual muy fácil de leer, lleno de obviedades en las que, tal vez, no te has parado a pensar. Este es un resumen de la última versión actualizada que incluye referencias a la usabilidad aplicada en móviles.

2. Resumen

1. Introducción

La usabilidad no va de tecnología, va de cómo la gente comprende y usa las cosas. Mientras la tecnología cambia a menudo con rapidez, la gente cambia muy despacio.
Si algo es usable significa que cualquier persona de habilidad y experiencia media o media-baja, puede imaginar cómo usar cualquier cosa (sitio web, App…) para realizar algo sin que sea más difícil de lo que se merece.
Principio de usabilidad: si algo requiere una gran inversión de tiempo, o lo parece, es menos probable que sea usado.

2. Tener en cuenta

¡No me hagas pensar!

  • Nada importante a más de dos clics. Tres, cuatro o cinco serían aceptables. Sin embargo, Steve Krug indica que “no importa el número de veces que haya que hacer clic en algo, si la opción es mecánica e inequívoca”. Tres clics inconscientes podrían equivaler a uno que requiere reflexión. Cuando no podamos evitar una elección difícil, hay que orientar al usuario de forma breve, oportuna (fácil de encontrar cuando se necesite) e ineludible (no debe pasar desapercibida).
  • Hablar el lenguaje de usuario.
  • Ser coherente.
  • Evitar interrogantes.

Cosas que hacen pensar:

  • Terminología: palabras generadas por el departamento de marketing, tecnicismos y nombres específicos de la empresa. Ejemplo: Trabajo/Ofertas de trabajo/Rama de trabajo.
  • Vínculos y botones mal definidos y/o indicados.
  • Interrogantes.

3. Uso de la web por los usuarios

En términos generales, el usuario escanea la página buscando un link que se parezca a lo que tiene en mente. No lee toda la página (normalmente porque tiene prisa, sabe que no todo el contenido es relevante y porque está entrenado), no sigue un orden y gran parte del total de la página ni lo mira. Hay excepciones en páginas de noticias, reportajes o descripciones pero el usuario aunque lee, también escanea.

Centran su atención en palabras y frases que encajan mejor con:

  • La tarea que tenemos en mente.
  • Nuestros intereses personales en ese momento.
  • Palabras que causan una reacción repentina (Ejemplo: gratis, oferta, sexo, nuestro nombre).

Por norma general el usuario no ve las opciones que se le ofrecen y elige, si no que opta por la primera opción razonable ya que:

  • Tiene prisa.
  • El fallo no se paga caro: el botón “atrás” es el más utilizado en web.
  • El éxito nunca está asegurado.

Utilizamos las cosas sin libro de instrucciones.

4. Claves para que los usuarios vean y entiendan la mayor parte de un site

Aprovechamiento y uso de convencionalismos: patrones de diseño más usados y estandarizados.

  • Colocación de elementos en una página: logotipo, navegación primaria…
  • Funcionamiento: iconos, formularios…
  • Apariencia de los elementos: link, botones de redes sociales.

Creación de una jerarquía clara en cada página. Si somos conscientes de que estamos escaneando es porque probablemente haya una mala jerarquización.

  • Los elementos más importantes deben ser los más destacados.
  • Lo relacionado por lógica, también se relaciona visualmente.
  • Los elementos se agrupan visualmente para mostrar que son parte de algo.

Dividir las páginas en zonas claramente definidas, así el usuario se centrará en las zonas donde cree que estará la información que busca.

Mostrar claramente dónde se puede hacer clic ya que es lo que más hace el usuario en Web. Busca claves visuales Cook formas (botones, enlaces), lugares (barra de menú) y formatos (color, subrayado, cambio del cursor). Ejemplo: vínculos y encabezamientos donde no se puede hacer clic NO llevarán el mismo color.

Eliminar el ruido visual:

  • Ruido: cuando todo parece importante o tiene mucha animación, color, etc.
  • Desorganizado: usar rejillas.
  • Desorden: evitar páginas cargadas de contenido.

Formatear el contenido para facilitar el escaneo:

  • Usa muchos encabezados y bien escritos: deben definir el contenido al que preceden y estar más cerca de este que del contenido anterior. Si hay varios niveles, hacer bien visible cada uno.
  • Párrafos cortos, incluso de una frase.
  • Uso de bullets.
  • Destacar palabras claves en negrita, sobretodo cuando aparecen por primera vez.

5. Cómo escribir en web

Omitir palabras innecesarias: “Elimine la mitad de las palabras de cada página; luego deshágase de la mitad restante” Krug.

  • Suprimir lo que no va a leerse reduce el ruido, destaca el contenido principal y se acortan las páginas.
  • Ofrecer información práctica evitando elogios en textos publicitarios y/o introductorios.
  • Las primeras páginas de las secciones del sitio y las instrucciones suelen tener siempre exceso de texto.

6. Diseño de navegación

“Un usuario no utiliza tu site si no está cómodo al hacerlo” Krug.

Proceso general de navegación del usuario:

  • Intenta encontrar algo.
  • Pregunta o busca:
    • Usuarios que dominan la búsqueda: van al buscador.
    • Usuarios con dominio de los vínculos: navegan por los menús.
    • Otros usuarios: estarán condicionados por el esquema de su mente, el momento, las prisas y de la facilidad de uso de la navegación del site.
  • O encuentra o abandona.
  • Carencias de la web en relación a usabilidad:
    • Ignorancia del tamaño de la página: dificulta saber si se ha visto todo lo que interesa de un sitio, así como cuando parar de buscar. Que los links clicados cambien de color acota el tamaño.
    • Ausencia de la sensación de ubicación: de ahí la importancia de una home (lugar fijo), el botón “atrás” o los marcadores de libros.

Funciones de una buena navegación:

  • Ayudar a encontrar lo que busca el usuario.
  • Informa al usuario de dónde se encuentra. Las “migas de pan” o ruta que ha seguido el usuario:
    • Situadas en la postre superior.
    • Utilizar > para separar los niveles.
    • Ejemplo: negrita el último término
  • Crea una jerarquía visible de los contenidos guiando correctamente al usuario.
  • Enseña a usar el sitio.
  • Da confianza al usuario sobre el dueño del site.

Tener el cuenta las convenciones (cambiantes) de la navegación web:

  • Logotipo en la parte superior izquierda que lleva la home.
  • Menú con indicador de estilo (puntero, cambio del color del texto, negrita, invirtiendo el color, cambio de color del botón) para señalar dónde estamos.
  • Log in/out, registro en la parte superior derecha.
  • Pie de página en la parte inferior.
  • Cuadro de búsqueda obligatorio.
  • Hamburguesas como icono de menú en móvil…

Crear una navegación coherente que englobe al site, así el usuario aprende una sola vez cómo usarlo y no se perderá.
Esta coherencia debe reducirse a la mínima expresión en páginas donde hay que rellenar un formulario ya que el usuario no necesita distracciones. Ejemplo: formulario de pago.

Tener muy en cuenta la arquitectura de contenidos antes de diseñar. Intentar hacer sites con no más de tres niveles de profundidad.

Poner nombre a las páginas:

  • Ubica.
  • Enmarca el contenido.
  • Destaca (tamaño, color, fuente…) marcando jerarquía.
  • Coherencia con el nombre de su link. En la medida de lo posible serán iguales. Ejemplo: botón “contacto” lleva a la página “contacto”.

Las pestañas son una herramienta de navegación excelentes porque:

  • Son muy claras y fáciles de entender.
  • Difíciles de perder.
  • Están muy logradas.

7. Página principal

Elementos:

  • Identidad y misión del sitio: si además justifica por qué será perfecto. OBLIGATORIO.
  • Jerarquía del sitio: qué hay y qué puedo hacer. OBLIGATORIO.
  • Búsqueda.
  • Sugerencias.
  • Promoción del contenido: lo mejor, lo último, lo popular.
  • Presenta promociones: invitan a explorar nuevas secciones.
  • Contenido actualizado.
  • Transacciones: espacio para publicidad, promos cruzadas y potenciados de marca.
  • Accesos directos.
  • Registro y logística.
  • Mostrar por dónde empezar.
  • Credibilidad y confianza.

Restricciones:

  • Espacio limitado en el que no cabe todo el que quiera poner publicidad.
  • Todo el mundo opina porque es la página más importante.
  • Llamar la atención de cualquiera (sea público objetivo o no).

Transmitir el mensaje:

Hay lugares importantes donde esperamos encontrar frases que explican cómo funciona el site:

  • Línea de etiquetas (tagline): frase cardinal que caracteriza a toda la empresa. Aparece debajo, encima o al lado del logotipo. Son muy eficaces.
    • Claras e informativas.
    • Extensión justa (seis a ocho palabras).
    • Expresan diferenciación y un beneficio claro.
    • No son slogans.
    • Gratas, enérgicas y a veces ingeniosas.
  • Mensaje de bienvenida: descripción breve del site generalmente está en la parte superior derecha o en el centro de contenidos
  • Video corto explicativo.

El usuario intentará por su cuenta ubicarse y si no lo consigue querrá un lugar al que acudir.

Pautas para dar a entender el mensaje:

  • Utiliza tanto espacio como sea necesario pero no más: hay que ir al grano y explicar como mucho cuatro características.
  • No uses una frase relativa a la misión de la empresa como mensaje de bienvenida.

Debe ser autoexplicativa: debe decirnos dónde empezar a buscar, a navegar y a probar el mejor material.

La página principal debe estar libre de una carga promocional excesiva.

8. Pruebas de usabilidad

El usuario medio no existe. Cada individuo, según sus circunstancias, empleo, gustos, etc., verá una misma página como ideal, de diferente manera. Sin embargo, en discusiones entre desarrolladores y diseñadores (habituales en el día a día), se tiende a argumentar que la mayoría de los usuarios se comportan como uno mismo. Un test de usabilidad determina qué funciona y qué no, dejando de lado si al usuario le gusta o le disgusta y ayudando a la toma de decisiones entre ambos equipos. Estas pruebas sirven para determinar cómo una persona usa algo en un tiempo determinado.

Por el contrario, con un grupo de discusión se obtiene con rapidez una muestra de las opiniones y los sentimientos de los usuarios sobre los productos. Pueden ser apropiados para determinar, de forma rápida, lo que la audiencia quiere, necesita y le gusta. Son más rentables en la fase de planificación de un proyecto.

Los tests de usabilidad deben hacerse desde el principio del proyecto.

  • Si quieres un gran sitio, tienes que probarlo.
  • Hacer una prueba con un usuario es mejor que nada.
  • Probar con un usuario al principio es mejor que con 50 al final.
  • Para ahorrar costes las puedes hacer tu mismo.
  • Una mañana al mes para una prueba de entre 35 minutos y una hora, con tres usuarios diferentes, más una reunión para decidir qué corregir, está bien.
  • Tres participantes (no todos especializados en el ámbito del producto) son suficientes para detectar problemas importantes que den trabajo para el siguiente mes.
  • Haced la prueba en un lugar donde no nos interrumpan.
  • Haz tests de las páginas de la competencia antes de empezar.

Prueba típica, intentando que el participante piense en voz alta:

  • Bienvenida: 4 minutos. Explicación del funcionamiento de la prueba.
  • Preguntas personales: 2 minutos.
  • Tour: 3 minutos. Explicación por parte de los participantes de qué entienden en la página principal.
  • Tareas: 35 minutos. Observar al participante tratando de realizar tareas a veces, largas. Dejar libertad al participante, no influenciarle, ni ayudarle.
  • Explotación: 5 minutos. Ronda de preguntas.
  • Conclusión: 5 minutos. Despedida.

9. Móvil

Un sitio para móviles es un subconjunto del sitio completo. Por tanto, ha partes que se dejan a un lado. Si se incluye todo hay que establecer prioridades. Las cosas que quiero usar con prisa y/o con frecuencia deben estar muy a mano. Todo lo demás puede estar a unos clics pero en un camino fácil de seguir. Para ver la misma cantidad de información que en la versión de escritorio, el usuario tendrá que hacer más clics y desplazarte más, pero esto es inevitable, solo hay que hacer que se sienta cómodo haciéndolo.

Debe ser responsive. Como esto aún da problemas hay que:
Permitir zoom.
Poner un link al Sitio completo.

En el móvil no hay cursor por tanto los elementos no cambian de estado al pasar sobre ellos.

Las páginas deben ser:

  • Rápidas de cargar.
  • Comprensibles. Deja las instrucciones a mano.
  • Memorizables: debes recordar cómo se usan la segunda vez que accedes a ellas.

Test de usuario en móvil:

El proceso para hacer tests de usuario en móvil es igual al de escritorio. Lo que cambia es la logística.
Graba la pantalla y por tanto los gestos del usuario aunque repliques la imagen en un monitor.
Adjunta la cámara al dispositivo para que el usuario pueda mantenerla con naturalidad y pueda moverse libremente manteniendo la pantalla visible y enfocada.

10. Sites bien hechos

Hay que tener en cuenta que los que visitan nuestro site son personas que tienen un “depósito de buena voluntad” o una determinada paciencia, que no es igual en todas ellas ni en todas las situaciones.

Cosas que acaban con la paciencia del usuario porque siente que el autor no le tiene en cuenta:

  • Ocultar la información deseada: como el teléfono de atención al cliente, precios o costes de envío por ejemplo.
  • Castigar al usuario por no hacer las cosas a su manera: introducción de los números de la tarjeta de crédito sin espacios.
  • Pedir información innecesaria.
  • Mentir.
  • Colocar obstáculos/publicidad en el camino.
  • Mala apariencia y diseño.

Cosas que aumentan nuestra paciencia:

  • Conoce qué quiere el usuario y dáselo fácil.
  • Sé honesto y muestra la información que no quieres mostrar.
  • Ahorra pasos siempre que puedas.
  • Anticípate a las posibles preguntas y contéstalas: FAQS.
  • Proporciona comodidades: botón para imprimir la página por ejemplo.
  • Facilita la recuperación ante los errores.
  • Si tienes limitaciones para hacer algo de forma óptima para el usuario, dilo.

11. Accesibilidad

Cosas a implementar desde el comienzo para hacer más accesible un site a todo tipo de personas:

  • Añadir alt a las imágenes.
  • Usa los encabezados h1,h2 y h3 correctamente.
  • Haz que los formularios funcionen con lectores de pantalla.
  • Crea un vínculo “saltar al contenido principal” al principio de cada página.
  • Permite el uso del teclado.
  • Crea gran contraste entre fondo y texto.
  • Elude una plantilla accesible.

3. Bibliografía recomendada

  • Orígenes del poder: cómo las personas toman decisiones. Gary Klein.
  • Letting go of the words. Janice (Ginny) Redish.
  • Formularios que funcionan: diseñar impuestos web para la usabilidad. Carolina Jarrett.

Persistencia en Liferay 7 con Service Builder

$
0
0

Aprende a generar tu código de persistencia con la herramienta que los propios chicos de Liferay utilizan: Service Builder.

Índice de contenidos


1. Introducción

Imagina que quieres tener, por ejemplo, una colección de libros y escritores para poder listarla en uno de tus portlets de Liferay Portal. Además, quieres permitir añadir nuevos libros y escritores, modificar los ya existentes e incluso borrarlos. Para ello, por una parte, tienes que guardalos en base de datos; por otra, tienes que tener una serie de clases que te permitan manejar esta persistencia. Escribir el código para montar esto puede ser aburrido, pero no hay de qué preocuparse, pues Liferay nos proporciona una herramienta que nos lo autogenera: Service Builder. Básicamente, nosotros definimos nuestras entidades en un archivo XML (service.xml) y Service Builder se encargará de generar automáticamente las capas de modelo, persistencia y servicio a partir de él.

El uso de Service Builder nos ahorrará gran tiempo de desarrollo y dejará el código separado por capas. Además, nos permitirá definir nuestro propio código de persistencia si lo deseamos.

Los de Liferay aseguran que es una herramienta bastante robusta y fiable, y se basan en que ellos la utilizan para generar su código de persistencia y sus módulos de servicio. No obstante, indican que podemos prescindir de ella para el desarrollo de aplicaciones Liferay, aunque hacen hincapié en que la usemos por el tiempo que nos ahorra.

En este tutorial aprenderemos a utilizarla y veremos cómo solventar los problemas que, desafortunadamente, nos encontramos cuando intentamos seguir la documentación oficial a causa de la falta de completitud de la misma.

Puedes encontrar el código de este tutorial en este repositorio. He ido separando en diferentes commits los pasos que se dan en el tutorial para así facilitar el seguimiento del mismo.


2. Entorno

Este tutorial se ha desarrollado en el siguiente entorno:

  • Portátil MacBook Pro (Retina, 15′, mediados 2015), macOS Sierra 10.12.5
  • Liferay Community Edition Portal 7.0.2 GA3 (Wilberforce / Build 7002 / August 5, 2016)
  • Java 1.8.0_131
  • PostgreSQL 9.6.2
  • IntelliJ IDEA Ultimate 2017.1.3

3. Preparar el proyecto

Vamos a construir un proyecto Liferay 7 de cero, para lo cual utilizaremos Blade CLI. En este otro tutorial se explica cómo crear el proyecto y cómo integrarlo con tu IDE favorito. Aquí, aunque no nos saltaremos ningún paso, no nos pararemos a volver a describir detalladamente qué hace cada uno; por tanto, recomiendo leerlo antes de empezar con este tutorial.

3.1. Usar Blade CLI para generar el proyecto

Vamos a utilizar ya Blade CLI para crear nuestro proyecto, así que abre la terminal y sigue los siguientes pasos:

  1. Creamos el proyecto Liferay:
    ~/workspaces/pruebas
    $ blade init tutorial-liferay7-servicebuilder
  2. Añadimos el paquete Liferay Portal + Tomcat:
    ~/workspaces/pruebas/tutorial-liferay7-servicebuilder
    $ ./gradlew initBundle
  3. Arrancamos el servidor local:
    ~/workspaces/pruebas/tutorial-liferay7-servicebuilder
    $ blade server start
  4. Una vez se haya levantado, accedemos a
    http://localhost:8080/
    y vemos el asistente de configuración de Liferay Portal. Como vamos a persistir en base de datos, no usaremos Hypersonic, sino que emplearemos otra base de datos (puedes seguir el tutorial Configurar Liferay 7 con PostgreSQL para ello).
  5. Tras haber configurado Liferay Portal y accedido a él (nos habrá pedido reiniciarlo al elegir otra base de datos como PostgreSQL), utilizamos la plantilla service-builder de Blade CLI:
    ~/workspaces/pruebas/tutorial-liferay7-servicebuilder
    $ blade create -t service-builder -p tutoriales.liferay.servicebuilder.libro libro

    Este comando crea el módulo libro y, dentro de él, los módulos libro-api y libro-service.

  6. Creamos, desde el directorio libro, el módulo libro-web, donde tendremos nuestro portlet MVC:
    ~/workspaces/pruebas/tutorial-liferay7-servicebuilder/modules/libro
    $ blade create -t mvc-portlet -p tutoriales.liferay.servicebuilder.libro -c MyMvcPortlet libro-web

  7. Abrimos el archivo bnd.bnd del módulo libro-web y cambiamos la línea:
    Bundle-SymbolicName: tutoriales.liferay.servicebuilder.libro
    por:
    Bundle-SymbolicName: tutoriales.liferay.servicebuilder.libro.web

3.2. Desplegar los módulos

Llegados a este punto, tenemos tres módulos autogenerados —dos de ellos vacíos— que no hacen gran cosa, pero podemos probar a desplegarlos para ver que hicimos bien todos los pasos de la sección anterior. Para ello, con el servidor iniciado, ejecutamos:

~/workspaces/pruebas/tutorial-liferay7-servicebuilder
$ blade deploy

En el archivo de log tutorial-liferay7-servicebuilder/bundles/logs/liferay.yyyy-MM-dd.log, o en terminal si arrancamos el servidor con

blade server start
—sin la opción
-b
—, veremos lo siguiente:
hh:mm:ss.milliseconds INFO  [Thread-68][BundleStartStopLogger:35] STARTED tutoriales.liferay.servicebuilder.libro.web_1.0.0 [486]
hh:mm:ss.milliseconds INFO  [Thread-71][BundleStartStopLogger:35] STARTED tutoriales.liferay.servicebuilder.libro.service_1.0.0 [487]
hh:mm:ss.milliseconds INFO  [Thread-74][BundleStartStopLogger:35] STARTED tutoriales.liferay.servicebuilder.libro.api_1.0.0 [488]

Esto indica que los módulos se han instalado y activado correctamente. Si tuviésemos algún problema, podríamos probar a ejecutar

gradle clean && gradle build && gradle deploy
desde el directorio de cada módulo o a desinstalar los módulos antes de volver a desplegarlos (ver siguiente sección).

3.3. Ver módulos instalados y activos

Es posible ver en cualquier momento —con el servidor levantado, eso sí— qué módulos hay instalados y cuáles están activos. Para ello, ejecutamos en terminal lo siguiente para iniciar una consola de Apache Felix Gogo desde una sesión local telnet:

$ telnet localhost 11311

y nos aparecerá:

Trying ::1...
telnet: connect to address ::1: Connection refused
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
____________________________
Welcome to Apache Felix Gogo

g!

Desde aquí podemos ejecutar el comando

lb
, que lista todos los módulos instalados, incluidos nuestros tres módulos:
g! lb
START LEVEL 20
     ID|State      |Level|Name
      0|Active     |    0|OSGi System Bundle (3.10.200.v20150831-0856)
      1|Active     |    6|Liferay Util Taglib (2.4.1)
    ...|...        |  ...|...
    485|Active     |   10|product-app-theme (1.0.7)
    486|Active     |    1|libro-web (1.0.0)
    487|Active     |    1|libro-service (1.0.0)
    488|Active     |    1|libro-api (1.0.0)

Este comando puede ir acompañado de las opciones

-s
y
-l
:
  • -s
    para que liste los módulos mostrando el nombre simbólico en lugar del nombre. En nuestros módulos, el nombre simbólico lo definimos en el archivo bnd.bnd de cada uno de ellos. Es por esto por lo que lo cambiamos antes en el módulo libro-web.
  • -l
    para ver la ubicación de cada módulo. En los que hemos creado, nos mostrará que tenemos los archivos JAR en cada carpeta build de cada módulo.

Es importante que los módulos que hemos creado aparezcan como activos (estado Active) y no como únicamente instalados (estado Installed). Ante un módulo inactivo, podemos emplear el comando

start <id_del_módulo>
para intentar activarlo.

Para desinstalar un módulo, haremos

uninstall <id_del_módulo>
. Pero ¡atención!, parece que a veces la desinstalación no se lleva a cabo correctamente —aunque
lb
no nos muestre ya los módulos— y los JAR se mantienen en la carpeta bundles/osgi/modules (donde bundles es el directorio donde tenemos el paquete Liferay Portal + Tomcat). En ese caso, deberíamos eliminar los JAR a mano.

¡Importante! Para salir de la consola, emplearemos

disconnect
. Más comandos útiles en la documentación de Liferay sobre Felix Gogo Shell.

Por último, cabe destacar que podemos emplear Blade CLI para realizar las anteriores acciones sin tener que entrar a la consola Gogo. Para ello, ejecutamos desde terminal:

blade sh <comando_de_la_consola_gogo>

Por ejemplo:

blade sh lb -s
.

4. Definir el modelo en el service.xml

En la introducción, comentamos que sería en el service.xml donde definiríamos nuestro modelo. Si nos fijamos, la plantilla service-builder de Blade CLI nos generó este archivo en el módulo libro-service. Además, lo hizo con datos de ejemplo, con una entidad Foo, así que lo modificamos y lo dejamos así:

<?xml version="1.0"?>
<!DOCTYPE service-builder PUBLIC "-//Liferay//DTD Service Builder 7.0.0//EN" "http://www.liferay.com/dtd/liferay-service-builder_7_0_0.dtd">

<service-builder package-path="tutoriales.liferay.servicebuilder.libro">
    <namespace>LIBRO</namespace>

    <entity name="Libro" uuid="true" local-service="true" remote-service="false">
        <!-- PK fields -->
        <column name="libroId" primary="true" type="long"/>

		<!-- Group instance -->
		<column name="groupId" type="long"/>

		<!-- Audit fields -->
		<column name="companyId" type="long"/>
		<column name="userId" type="long"/>
		<column name="userName" type="String"/>
		<column name="createDate" type="Date"/>
		<column name="modifiedDate" type="Date"/>

        <!-- Other fields -->
        <column name="titulo" type="String"/>
        <column name="escritor" type="String"/>
        <column name="publicacion" type="Date"/>

        <!-- Order -->
        <order by="asc">
            <order-column name="titulo"/>
            <order-column name="escritor"/>
        </order>

        <!-- Finder methods -->
        <finder name="Titulo" return-type="Collection">
            <finder-column name="titulo"/>
        </finder>
    </entity>
</service-builder>

4.1. Sintaxis del service.xml

La sintaxis del service.xml viene definida por el DTD Service Builder 7.0.0, así que, ante cualquier duda, viene bien echarle un vistazo. No obstante, describimos aquí las etiquetas y atributos empleados:

  • namespace
    . Este nombre se prefijará al nombre de las entidades cuando se creen las tablas en base de datos. Ejemplo, si
    namespace
    es LIBRO y una entidad (
    entity
    ) tiene nombre (
    name
    ) Escritor, entonces la tabla generada en base de datos será LIBRO_Escritor. No debemos utilizar namespaces ya usados por Liferay como groups, quartz o users; para ver cuáles son, podemos abrir la base de datos generada por Liferay Portal y ver los prefijos del nombre de sus tablas.
  • entity
    . Entidad a partir de la cual se generará la tabla en base de datos y el código relativo al modelo, la persistencia y el servicio. Los atributos que tenemos son:
    • name
      . Nombre de la entidad.
    • uuid
      . Si su valor es
      true
      , se generará una columna
      UUID
      que se rellenará automáticamente.
    • local-service
      y
      remote-service
      . Si su valor es
      true
      , se generarán interfaces locales y remotas, respectivamente, para la capa de servicio. Como este código es de prueba y trabajamos en un servidor local, nos evitamos generar las remotas dando valor
      false
      a este último atributo.
  • column
    . Atributo de la entidad y, por tanto, columna de la tabla. Su nombre viene dado por
    name
    y su tipo por
    type
    . Para especificar que forma parte de la clave primaria, incluimos
    primary=true
    ; podemos, por tanto, indicar que varias columnas forman la clave primaria a través de este atributo. En cuanto al tipo, parece que los únicos disponibles son los tipos primitivos
    boolean
    ,
    double
    ,
    int
    y
    long
    y las clases
    Blob
    ,
    Collection
    ,
    Date
    y
    String
    . Y digo «parece» porque no veo que Liferay indique en ningún sitio cuáles son los tipos soportados. De hecho, el propio archivo DTD solamente especifica lo siguiente: «The type value specifies whether the column is a String, Boolean, or int, etc.». Así que lo que he hecho ha sido ver qué tipos nos deja elegir el asistente de Service Builder de Liferay IDE.
  • order
    . A través de su atributo
    by
    —que admite los valores
    asc
    y
    desc
    —, nos permite obtener las entidades ordenadas cuando las recuperamos de base de datos, en función de las columnas que le indiquemos con
    order-column
    . Si proporcionamos más de una columna, entonces ordenará en base a la primera y luego a la segunda. En nuestro caso, que hemos indicado que ordene por
    titulo
    y por
    escritor
    ascendentemente, si tuviéramos dos libros con el mismo título, entonces nos devolvería primero aquel cuyo escritor fuese alfabéticamente anterior al otro.
  • finder
    . Esta etiqueta hará que Service Builder genere métodos para recuperar, eliminar y contar la entidad en función de las columnas que indiquemos con
    finder-column
    . Lo nombraremos (atributo
    name
    ) con la convención CamelCase, ya que Service Builder tomará esta palabra para dar nombre a los métodos que generará. Con
    return-type
    indicamos si queremos que devuelva una colección de entidades (
    Collection
    ) o una única entidad (
    Libro
    , por ejemplo) si buscásemos por clave única.

4.2. Modelo definido en el service.xml

Nuestro modelo va a consistir, de momento, en una única entidad Libro. Los libros tendrán un título, el nombre de su escritor y una fecha de publicación. Se podrán buscar por título y se devolverán ordenados por título y nombre de escritor. Además, tendrán una clave primaria sintética: libroId. ¿Y el resto de campos? Si nos fijamos, los campos bajo los comentarios «Group instance» y «Audit fields» se generaron automáticamente cuando ejecutamos Service Builder. Unos sirven para guardar los identificadores del sitio y de la instancia del portal y así poder soportar multitenencia: cada instancia del portal y cada sitio dentro de cada instancia pueden tener conjuntos de datos independientes. Otros sirven para poder registrar quién creó las entidades y cuándo. ¿Qué guarda cada columna?

  • groupId. Identificador del sitio.
  • companyId. Identificador de la instancia del portal.
  • userId y userName. Identificador y nombre del usuario que posee la instancia de la entidad.
  • createDate y modifiedDate. Fecha en la que se creó la instancia de la entidad y fecha en la que se modificó por última vez.

No es obligatorio tener estos campos, pero sí conveniente por los propósitos expuestos. Al menos, deberíamos tener la columna companyId si queremos crear relaciones M-N (ya veremos por qué).


5. Generar el código con Service Builder

Ya tenemos preparado el service.xml, asi que procedemos a generar el código con Service Builder:

~/workspaces/pruebas/tutorial-liferay7-servicebuilder
$ ./gradlew buildService

Vemos que ahora tenemos carpetas src en los módulos libro-api y libro-service —antes vacíos— pobladas de paquetes y clases. Antes de pararnos a analizarlas, recordemos que dijimos que Service Builder separaba el código en capas: modelo, persistencia y servicio. La capa de modelo es responsable de definir los objetos que representan nuestras entidades; la de persistencia, de tratar con la base de datos; y la de servicio, de exponer una API para realizar operaciones CRUD. Si nos fijamos, ambos módulos contienen paquetes model, persistence y service.

5.1. Análisis del código generado

En el service.xml, pusimos el atributo

local-service="true"
en la
entity
Libro. Esto supuso que Service Builder generase los siguientes archivos: la interfaz LibroLocalService y las clases LibroLocalServiceBaseImpl, LibroLocalServiceImpl, LibroLocalServiceUtil y LibroLocalServiceWrapper. Con
remote-service="true"
(nosotros le dimos valor
false
) se hubiesen generado las mismas pero con nombre LibroServiceXXX en lugar de LibroLocalServiceXXX. Los servicios locales contienen la lógica de negocio y el acceso a la capa de persistencia, y pueden ser invocados únicamente desde el servidor Liferay en el que son desplegados. Los remotos son accesibles desde cualquier lado, por lo que normalmente tienen código adicional para comprobar permisos.

Vamos ahora a comprender una parte importante del código autogenerado, pero antes entendamos que el módulo libro-api contiene la API y el módulo libro-service la implementa. Nuestro portlet, en el módulo libro-web, empleará la API y ni siquiera tendrá como dependencia al módulo libro-service.

  • En cuanto a la persistencia, LibroPersistence es la interfaz que define los métodos CRUD y es implementada por LibroPersistenceImpl. Ambas indican que no deben ser referenciadas directamente, sino que hay que utilizar el envoltorio LibroUtil; sin embargo, éste solamente debe ser usado por la capa de servicio. Esto quiere decir que, para acceder a las operaciones CRUD desde nuestro portlet, no emplearemos LibroUtil; en su lugar, usaremos la clase LibroLocalServiceUtil.
  • En cuanto al servicio local, LibroLocalService es la interfaz y es implementada por LibroLocalServiceImpl. Ambas indican que no deben ser referenciadas directamente, sino que hay que utilizar el envoltorio LibroLocalServiceUtil, el cual dijimos que sería utilizado por el portlet como punto de entrada a la capa de servicio.
  • En cuanto al modelo, LibroModel es la interfaz base y es implementada por LibroModelImpl, clase que sirve únicamente como contenedor de las propiedades de acceso por defecto generadas por Service Builder. No deberíamos referenciar LibroModel, sino Libro, interfaz que la extiende y que es implementada por LibroImpl.

No debemos modificar ninguna clase generada a excepción de LibroImpl, LibroLocalServiceImpl y, si hubiésemos generado el servicio remoto (

remote-service="true"
), LibroServiceImpl. Es en estas tres clases —todas ellas del módulo libro-service— en donde realizaremos cambios para que, al ejecutar Service Builder, se vean plasmados en el resto de clases generadas.

Pero ¿por qué querríamos realizar cambios? Imaginemos, por ejemplo, que desde nuestro portlet quisiésemos añadir un libro. Hemos dicho que debemos utilizar la clase LibroLocalServiceUtil para ello. Ésta tiene un método

addLibro(Libro libro)
, pero nosotros queremos añadir a base de datos un libro a partir de su título, nombre de escritor y fecha de publicación, es decir, nos gustaría tener un método
addLibro(String titulo, String escritor, LocalDate publicacion)
. Pues bien, lo que tenemos que hacer es definir este método en LibroLocalServiceImpl e implementarlo. Pero, claro, esta clase es del paquete libro-service, del cual libro-web —donde está nuestro portlet— ni siquiera depende, y es la implementación de la interfaz LibroLocalService (módulo libro-api). Si el portlet usa la API (el envoltorio LibroLocalServiceUtil), ¿cómo tiene acceso a este nuevo método? Pues básicamente gracias a Service Builder: creamos el nuevo método en LibroLocalServiceImpl y, al ejecutar Service Builder, se añadirá a la interfaz y estará disponible a través de LibroLocalServiceUtil.

Todavía no haremos ningún cambio, no añadiremos métodos propios. Además, antes de actualizar nuestros JAR con el código generado y desplegarlos, cambiaremos la longitud máxima que admite el título de un libro.

5.2. Modificar el límite de caracteres de las columnas String

Las columnas titulo y escritor, que definimos como

String
, son cadenas de caracteres de longitud máxima 75. Este es el valor por defecto que asigna Service Builder, y lo podemos comprobar si abrimos el archivo libro-service/src/main/resources/META-INF/sql/tables.sql (veremos
titulo VARCHAR(75) null
).

Si queremos aumentar el máximo número de caracteres, podemos pensar que nos basta con modificar el archivo tables.sql y poner, por ejemplo,

titulo VARCHAR(200) null
. Esto sería incorrecto, pues los archivos de los directorios spring y sql se sobreescriben cada vez que ejecutamos
./gradlew buildService
. Entonces, ¿cómo hacemos para modificar ese valor por defecto? Pues a través del archivo portlet-model-hints.xml del módulo libro-service. Si lo abrimos, veremos la línea:
<field name="titulo" type="String"/>

La sustituimos por:

<field name="titulo" type="String">
    <hint name="max-length">200</hint>
</field>

Ahora, si ejecutamos

./gradlew buildService
desde la raíz del proyecto, veremos que ha sobreescrito tables.sql y que tiene
titulo VARCHAR(200) null
. En cambio, portlet-model-hints.xml permanece intacto, con el cambio que habíamos realizado.

Tienes más información sobre model hints en la documentación oficial.

Ahora es momento de actualizar los JAR de nuestros módulos y desplegarlos. Si te falla en este punto, puedes probar a ejecutar

gradle clean && gradle build && gradle deploy
en libro-service, luego en libro-api y, por último, hacer
blade deploy
de nuevo. En los logs aparecerá la siguiente traza:
hh:mm:ss.milliseconds INFO  [Thread-70][ServiceComponentLocalServiceImpl:317] Running LIBRO SQL scripts

Podemos acceder a nuestra base de datos (con pgAdmin, por ejemplo, si empleaste PostgreSQL) y ver que se ha creado la tabla LIBRO_Libro con los campos que definimos, incluido el campo titulo de longitud máxima 200.


6. Modificar base de datos ya creada

Si editásemos el service.xml para modificar nuestra entidad, ejecutásemos

./gradlew buildService
y desplegásemos, Service Builder regeneraría el código pero, atención, no aplicaría los cambios en base de datos, como indica la documentación de Liferay en la última nota de esta página. Según ella, lo que tendríamos que hacer es, en resumen, eliminar la tabla y volver a crearla. Para ello, ejecutaríamos las siguientes sentencias:
DROP TABLE IF EXISTS LIBRO_Libro;
DELETE FROM servicecomponent WHERE buildnamespace = 'LIBRO';
DELETE FROM release_ WHERE servletcontextname = 'tutoriales.liferay.servicebuilder.libro.service';

Por supuesto, esta medida solamente sirve en desarrollo; en producción, tendremos que conservar los datos que tenemos y aplicar a la base de datos únicamente los cambios que queremos hacer. Como ejemplo, añadiremos el género a nuestros libros. Antes de nada, comprobamos que, en efecto, Service Builder no altera la base de datos:

  • Modificamos el archivo service.xml para añadir
    <column name="genero" type="String"/>
    y el archivo portlet-model-hints.xml para indicar la longitud máxima:
    <field name="genero" type="String">
        <hint name="max-length">60</hint>
    </field>
  • Ejecutamos
    ./gradlew buildService
    para regenerar las clases y que así tengan en cuenta la nueva columna.
  • Desplegamos de nuevo con
    blade deploy
    . Veremos en los logs que aparece la siguiente traza:
    hh:mm:ss.milliseconds INFO  [Thread-76][ServiceComponentLocalServiceImpl:326] Upgrading LIBRO database to build number X
    Aunque ponga que se ha actualizado la base de datos, si accedemos a ella veremos que no lo ha hecho: nuestra tabla LIBRO_Libro no tiene la nueva columna genero.

Entonces, ¿qué tenemos que hacer para que cambie la base de datos? Pues básicamente crear una nueva versión del módulo libro-service y definir los cambios en una serie de clases:

  • UpgradeProcess. Crearemos clases que extiendan esta clase de Liferay y en ellas pondremos el código SQL que refleja los cambios que queremos hacer (en nuestro caso, añadir una nueva columna genero).
  • UpgradeStepRegistrator. Tendremos una única clase que extienda ésta. En ella indicaremos qué cambios —es decir, qué clases UpgradeProcess— se aplican entre versiones.

Vamos a ello:

  • Añadimos la siguiente dependencia a nuestro archivo build.gradle del módulo libro-service:
    compile group: "com.liferay", name: "com.liferay.portal.upgrade", version: "2.0.0"
  • Modificamos el archivo bnd.bnd del módulo libro-service para aumentar la versión del módulo: subimos el valor de los atributos
    Bundle-Version
    y
    Liferay-Require-SchemaVersion
    de
    1.0.0
    a
    1.0.1
    .
  • Seguimos la estructura propuesta por Liferay en su documentación: creamos un paquete upgrade con la clase LibroUpgradeStepRegistrator y con un subpaquete v1_0_1 con una clase UpgradeLibro.

  • Añadimos el código correspondiente a nuestra clase LibroUpgradeStepRegistrator:
    package tutoriales.liferay.servicebuilder.libro.upgrade;
    
    import com.liferay.portal.upgrade.registry.UpgradeStepRegistrator;
    import org.osgi.service.component.annotations.Component;
    import tutoriales.liferay.servicebuilder.libro.upgrade.v1_0_1.UpgradeLibro;
    
    @Component(immediate = true, service = UpgradeStepRegistrator.class)
    public class LibroUpgradeStepRegistrator implements UpgradeStepRegistrator {
    
        private static final String BUNDLE_SYMBOLIC_NAME = "tutoriales.liferay.servicebuilder.libro.service";
    
        @Override
        public void register(Registry registry) {
            registry.register(BUNDLE_SYMBOLIC_NAME, "1.0.0", "1.0.1",
                    new UpgradeLibro());
        }
    
    }
    Básicamente estamos diciendo que cuando se pase de la versión
    1.0.0
    a la versión
    1.0.1
    se aplique el paso UpgradeLibro. Con el atributo
    immediate = true
    indicamos que el módulo se active inmediatamente después de ser instalado.
  • Creamos nuestra clase UpgradeLibro, en la que ejecutamos el código SQL que necesitamos para añadir la columna genero:
    package tutoriales.liferay.servicebuilder.libro.upgrade.v1_0_1;
    
    import com.liferay.portal.kernel.upgrade.UpgradeProcess;
    
    public class UpgradeLibro extends UpgradeProcess {
    
        @Override
        protected void doUpgrade() throws Exception {
            runSQL("ALTER TABLE LIBRO_Libro ADD COLUMN genero VARCHAR(60) NULL");
        }
    
    }
    Definimos la columna de la misma manera en la que está definida en el archivo tables.sql (archivo autogenerado a partir del contenido de service.xml y de portlet-model-hints.xml).
  • Desplegamos con
    build.deploy
    . Veremos en los logs las siguientes trazas:
    hh:mm:ss.milliseconds INFO  [Thread-91][BundleStartStopLogger:38] STOPPED tutoriales.liferay.servicebuilder.libro.service_1.0.0 [492]
    hh:mm:ss.milliseconds INFO  [Thread-91][BundleStartStopLogger:35] STARTED tutoriales.liferay.servicebuilder.libro.service_1.0.1 [492]
    hh:mm:ss.milliseconds INFO  [Thread-103][UpgradeProcess:82] Upgrading tutoriales.liferay.servicebuilder.libro.upgrade.v1_0_1.UpgradeLibro
    hh:mm:ss.milliseconds INFO  [Thread-103][UpgradeProcess:97] Completed upgrade process tutoriales.liferay.servicebuilder.libro.upgrade.v1_0_1.UpgradeLibro in 5ms
    Si desplegásemos de nuevo, no aparecerían las dos últimas trazas, pues el módulo ya está en su versión
    1.0.1
    y el proceso de actualización de base de datos se ejecuta cuando pasamos de la
    1.0.0
    a la
    1.0.1
    .
  • Comprobamos que, ahora sí, tenemos la nueva columna en nuestra tabla.
  • Como última medida, listamos los módulos y comprobamos que no tenemos la versión
    1.0.0
    de libro-service. Si así fuese, la desinstalaríamos.

6.1. Relaciones entre entidades

En el ejemplo dado, únicamente tenemos una entidad, pero lo normal es que nuestro modelo tenga varias y estén relacionadas. Vamos a ver cómo debemos proceder para tener tanto relaciones uno a varios como relaciones varios a varios.

6.1.1. Relaciones uno a varios (1-N)

Cada uno de nuestros libros estará escrito por un único escritor (1), y cada escritor podrá escribir múltiples libros (N).

Empezamos reescribiendo el service.xml. Añadimos la entidad Escritor, eliminamos la columna escritor de Libro y le añadimos la columna escritorId, que será nuestra clave externa, pues hace referencia a la clave primaria de Escritor. Eliminamos del orden la columna escritor y, por último, añadimos un método de búsqueda para poder recoger todos los libros que pertenezcan a cierto escritor.

<?xml version="1.0"?>
<!DOCTYPE service-builder PUBLIC "-//Liferay//DTD Service Builder 7.0.0//EN" "http://www.liferay.com/dtd/liferay-service-builder_7_0_0.dtd">

<service-builder package-path="tutoriales.liferay.servicebuilder.libro">
    <namespace>LIBRO</namespace>

    <entity name="Libro" uuid="true" local-service="true" remote-service="false">
        <!-- PK fields -->
        <column name="libroId" primary="true" type="long"/>

        <!-- Group instance -->
        <column name="groupId" type="long"/>

        <!-- Audit fields -->
        <column name="companyId" type="long"/>
        <column name="userId" type="long"/>
        <column name="userName" type="String"/>
        <column name="createDate" type="Date"/>
        <column name="modifiedDate" type="Date"/>

        <!-- Other fields -->
        <column name="titulo" type="String"/>
        <column name="publicacion" type="Date"/>
        <column name="genero" type="String"/>
        <column name="escritorId" type="long"/>

        <!-- Order -->
        <order by="asc">
            <order-column name="titulo"/>
        </order>

        <!-- Finder methods -->
        <finder name="Titulo" return-type="Collection">
            <finder-column name="titulo"/>
        </finder>
        <finder name="EscritorId" return-type="Collection">
            <finder-column name="escritorId"/>
        </finder>
    </entity>

    <entity name="Escritor" uuid="true" local-service="true" remote-service="false">
        <!-- PK fields -->
        <column name="escritorId" primary="true" type="long"/>

        <!-- Group instance -->
        <column name="groupId" type="long"/>

        <!-- Audit fields -->
        <column name="companyId" type="long"/>
        <column name="userId" type="long"/>
        <column name="userName" type="String"/>
        <column name="createDate" type="Date"/>
        <column name="modifiedDate" type="Date"/>

        <!-- Other fields -->
        <column name="nombre" type="String"/>

        <!-- Order -->
        <order by="asc">
            <order-column name="nombre"/>
        </order>

        <!-- Finder methods -->
        <finder name="Nombre" return-type="Collection">
            <finder-column name="nombre"/>
        </finder>
    </entity>
</service-builder>

Ejecutamos

./gradlew buildService
y vemos que se han creado nuevas clases para la entidad Escritor y que se ha modificado el código existente referente a la entidad Libro. Por otra parte, nos fijamos en el detalle de que el archivo portlet-model-hints.xml ha sido modificado para registrar estos cambios pero que no ha sido regenerado totalmente, ya que ha mantenido los límites de longitud que definimos anteriormente.

Como ya sabemos, para que estos cambios se den en nuestra base de datos, no basta con desplegar los módulos. Debemos aumentar la versión en el bnd.bnd de libro-service a la

1.1.0
, crear el subpaquete upgrade/v1_1_0, en él dos nuevas clases AddEscritor y UpgradeLibro y registrar los cambios en LibroUpgradeStepRegistrator:
package tutoriales.liferay.servicebuilder.libro.upgrade.v1_1_0;

import com.liferay.portal.kernel.upgrade.UpgradeProcess;

public class AddEscritor extends UpgradeProcess {

    @Override
    protected void doUpgrade() throws Exception {
        runSQL("CREATE TABLE LIBRO_Escritor (" +
                "uuid_ VARCHAR(75) NULL," +
                "escritorId LONG NOT NULL PRIMARY KEY," +
                "groupId LONG," +
                "companyId LONG," +
                "userId LONG," +
                "userName VARCHAR(75) NULL," +
                "createDate DATE NULL," +
                "modifiedDate DATE NULL," +
                "nombre VARCHAR(75) NULL" +
                ");");
    }

}
package tutoriales.liferay.servicebuilder.libro.upgrade.v1_1_0;

import com.liferay.portal.kernel.upgrade.UpgradeProcess;

public class UpgradeLibro extends UpgradeProcess {

    @Override
    protected void doUpgrade() throws Exception {
        runSQL("ALTER TABLE LIBRO_Libro DROP COLUMN escritor");
        runSQL("ALTER TABLE LIBRO_Libro ADD COLUMN escritorId LONG");
    }

}
package tutoriales.liferay.servicebuilder.libro.upgrade;

import com.liferay.portal.upgrade.registry.UpgradeStepRegistrator;
import org.osgi.service.component.annotations.Component;
import tutoriales.liferay.servicebuilder.libro.upgrade.v1_1_0.AddEscritor;

@Component(immediate = true, service = UpgradeStepRegistrator.class)
public class LibroUpgradeStepRegistrator implements UpgradeStepRegistrator {

    private static final String BUNDLE_SYMBOLIC_NAME = "tutoriales.liferay.servicebuilder.libro.service";

    @Override
    public void register(Registry registry) {
        registry.register(BUNDLE_SYMBOLIC_NAME, "1.0.0", "1.0.1",
                new tutoriales.liferay.servicebuilder.libro.upgrade.v1_0_1.UpgradeLibro());

        registry.register(BUNDLE_SYMBOLIC_NAME, "1.0.1", "1.1.0",
                new AddEscritor(),
                new tutoriales.liferay.servicebuilder.libro.upgrade.v1_1_0.UpgradeLibro());
    }

}

El método Registry#register acepta una serie de UpgradeStep —interfaz que implementa UpgradeProcess— como último parámetro. Nosotros le enviamos AddEscritor y la versión

1.1.0
de UpgradeLibro. Cuántos UpgradeProcess crees y en qué paquete los coloques depende de cómo te guste a ti organizarlos; en nuestro caso, estamos siguiendo la recomendación de Liferay, como ya dijimos. De esta manera, el código es legible y podemos leerlo así: «cuando pasamos de la versión 1.0.1 a la 1.1.0, añadimos la entidad Escritor y actualizamos Libro».

Ahora ya podemos desplegar los módulos y ver que nuestra base de datos tiene una nueva tabla LIBRO_Escritor y que LIBRO_Libro se ha modificado.

6.1.2. Relaciones varios a varios (M-N)

Ahora vamos a cambiar las reglas y vamos a establecer que un libro pueda ser escrito por múltiples escritores (M) en lugar de por uno solo.

Vimos que en la relación 1-N bastaba con definir en el service.xml la columna

<column name="escritorId" type="long"/>
en la entidad Libro. Pues bien, sabemos que para tener una relación M-N entre Libro y Escritor no podemos tener en Libro la referencia al escritor, pues un libro puede estar escrito por varios escritores, sino que tenemos que tener una tercera tabla que relacione la clave primaria de Libro (libroId) y la de Escritor (escritorId), así que empezamos deshaciéndonos, en el service.xml, de la columna escritorId y del método de búsqueda por id de escritor. Añadimos, en su lugar:
<column name="escritores" type="Collection" entity="Escritor" mapping-table="Libros_Escritores"/>

De igual forma, en Escritor pondremos:

<column name="libros" type="Collection" entity="Libro" mapping-table="Libros_Escritores"/>

Nuestro service.xml quedaría así:

<?xml version="1.0"?>
<!DOCTYPE service-builder PUBLIC "-//Liferay//DTD Service Builder 7.0.0//EN" "http://www.liferay.com/dtd/liferay-service-builder_7_0_0.dtd">

<service-builder package-path="tutoriales.liferay.servicebuilder.libro">
    <namespace>LIBRO</namespace>

    <entity name="Libro" uuid="true" local-service="true" remote-service="false">
        <!-- PK fields -->
        <column name="libroId" primary="true" type="long"/>

        <!-- Group instance -->
        <column name="groupId" type="long"/>

        <!-- Audit fields -->
        <column name="companyId" type="long"/>
        <column name="userId" type="long"/>
        <column name="userName" type="String"/>
        <column name="createDate" type="Date"/>
        <column name="modifiedDate" type="Date"/>

        <!-- Other fields -->
        <column name="titulo" type="String"/>
        <column name="publicacion" type="Date"/>
        <column name="genero" type="String"/>
        <column name="escritores" type="Collection" entity="Escritor" mapping-table="Libros_Escritores"/>

        <!-- Order -->
        <order by="asc">
            <order-column name="titulo"/>
        </order>

        <!-- Finder methods -->
        <finder name="Titulo" return-type="Collection">
            <finder-column name="titulo"/>
        </finder>
    </entity>

    <entity name="Escritor" uuid="true" local-service="true" remote-service="false">
        <!-- PK fields -->
        <column name="escritorId" primary="true" type="long"/>

        <!-- Group instance -->
        <column name="groupId" type="long"/>

        <!-- Audit fields -->
        <column name="companyId" type="long"/>
        <column name="userId" type="long"/>
        <column name="userName" type="String"/>
        <column name="createDate" type="Date"/>
        <column name="modifiedDate" type="Date"/>
        <column name="libros" type="Collection" entity="Libro" mapping-table="Libros_Escritores"/>

        <!-- Other fields -->
        <column name="nombre" type="String"/>

        <!-- Order -->
        <order by="asc">
            <order-column name="nombre"/>
        </order>

        <!-- Finder methods -->
        <finder name="Nombre" return-type="Collection">
            <finder-column name="nombre"/>
        </finder>
    </entity>
</service-builder>

Esto no quiere decir que vayamos a tener estas dos columnas en nuestras tablas de base de datos, sino que lo que estamos haciendo es indicar a Service Builder que se trata de una relación M-N.

Ejecutamos Service Builder con

./gradlew buildService
y vemos que, aunque ha generado código, nos da un error:
java.lang.IllegalArgumentException: No entity column exist with column database name escritorId for entity Libro
. Esto se debe a que el archivo indexes.sql tiene la línea
create index IX_B94CE263 on LIBRO_Libro (escritorId);
incluida. Cuando eliminemos un método de búsqueda (finder) del service.xml y nos dé este error, simplemente borramos el archivo indexes.sql. No pasa nada por ello, pues se generará de nuevo a partir de nuestro service.xml, así que lo borramos y ejecutamos
./gradlew buildService
otra vez.

De nuevo, aumentamos el bnd.bnd a la versión

1.2.0
y creamos las clases para llevar a cabo el proceso de actualización:
package tutoriales.liferay.servicebuilder.libro.upgrade.v1_2_0;

import com.liferay.portal.kernel.upgrade.UpgradeProcess;

public class UpgradeLibro extends UpgradeProcess {

    @Override
    protected void doUpgrade() throws Exception {
        runSQL("ALTER TABLE LIBRO_Libro DROP COLUMN escritorId");
    }

}
package tutoriales.liferay.servicebuilder.libro.upgrade.v1_2_0;

import com.liferay.portal.kernel.upgrade.UpgradeProcess;

public class AddLibrosEscritores extends UpgradeProcess {

    @Override
    protected void doUpgrade() throws Exception {
        runSQL("CREATE TABLE LIBRO_Libros_Escritores (" +
                "companyId LONG NOT NULL," +
                "escritorId LONG NOT NULL," +
                "libroId LONG NOT NULL," +
                "PRIMARY KEY (escritorId, libroId)" +
                ");");
    }

}
package tutoriales.liferay.servicebuilder.libro.upgrade;

import com.liferay.portal.upgrade.registry.UpgradeStepRegistrator;
import org.osgi.service.component.annotations.Component;
import tutoriales.liferay.servicebuilder.libro.upgrade.v1_1_0.AddEscritor;
import tutoriales.liferay.servicebuilder.libro.upgrade.v1_2_0.AddLibrosEscritores;

@Component(immediate = true, service = UpgradeStepRegistrator.class)
public class LibroUpgradeStepRegistrator implements UpgradeStepRegistrator {

    private static final String BUNDLE_SYMBOLIC_NAME = "tutoriales.liferay.servicebuilder.libro.service";

    @Override
    public void register(Registry registry) {
        registry.register(BUNDLE_SYMBOLIC_NAME, "1.0.0", "1.0.1",
                new tutoriales.liferay.servicebuilder.libro.upgrade.v1_0_1.UpgradeLibro());

        registry.register(BUNDLE_SYMBOLIC_NAME, "1.0.1", "1.1.0",
                new AddEscritor(),
                new tutoriales.liferay.servicebuilder.libro.upgrade.v1_1_0.UpgradeLibro());

        registry.register(BUNDLE_SYMBOLIC_NAME, "1.1.0", "1.2.0",
                new tutoriales.liferay.servicebuilder.libro.upgrade.v1_2_0.UpgradeLibro(),
                new AddLibrosEscritores());
    }

}

Desplegamos y comprobamos que en nuestra base de datos existe una nueva tabla: LIBRO_Libros_Escritores. Esta tabla tiene el id del escritor y el id del libro, y ambos forman la clave primaria; sin embargo, no son claves externas, no hacen referencia a las columnas escritorId ni a libroId de las tablas Escritor y Libro, respectivamente.

La tabla LIBRO_Libros_Escritores tiene, además, la columna companyId. Ésta fue definida automáticamente por Service Builder cuando generamos el código a partir del service.xml. Por esta razón dijimos en la sección 4.2. Modelo definido en el service.xml que, al menos, necesitábamos tener la columna companyId en nuestra entidad Libro (y, posteriormente, en Escritor).

Como vemos que la tabla tiene, además de los ids de las tablas de la relación, otro campo —companyId—, podríamos pensar que podemos añadir las columnas que quisiésemos a esta tabla; por ejemplo, saber cuántos días dedicó cada escritor a cada libro. Por desgracia, parece que el soporte de Service Builder a las relaciones M-N no va más allá de tener los ids de las tablas que relaciona —además del companyId— y, por tanto, no lo contempla.


7. Operaciones CRUD con el portlet

En este punto, tenemos preparadas la base de datos y las capas autogeneradas por el Service Builder para operar con ella. Ahora nos gustaría tener un portlet que mostrase nuestra colección de libros y escritores existentes y que nos permitiese, además, crear, modificar y eliminar elementos, es decir, realizar las funciones básicas CRUD. Para ello, publicaré un tutorial en el que explicaré cómo hacerlo con portlets MVC y vista en JSP.


8. Conclusiones

Tras usar Service Builder nos damos cuenta de la gran cantidad de código que esta herramienta nos ahorra. Básicamente definimos en un XML nuestro modelo y éste es autogenerado. No obstante, no todo es tan sencillo, pues en cuanto queremos realizar cambios ya tenemos que definir procesos de actualización aparte del XML. Además, es necesario saber cómo lidiar con ciertos errores y cómo realizar ciertas acciones que, o bien se encuentran disgregadas en la documentación oficial, o bien no aparecen siquiera.


9. Referencias

Crea tu servidor RTMP para streaming con nginx

$
0
0

En este tutorial vamos a ver cómo puedes crear tu propio servidor RTMP. Con él podrás realizar streaming de vídeo con tu propio servicio sin ser limitado por las especificaciones que te imponen servicios como Youtube o Twitch.

Índice de contenidos

1. Introducción

La mayoría nos hemos topado ya con algún streaming en directo en servicios como Youtube o Twitch. En este caso veremos cómo hacer lo mismo sin depender de alguna de estas plataformas. Para ello utilizaremos nginx.

Nginx es un servidor web/proxy multiplataforma que trabaja con los protocolos HTTP, HTTPS, SMTP, IMAP, POP3. En este caso le añadiremos un módulo para poder tener nuestro servidor RTMP (Real-Time Messaging Protocol). Gracias a este complemento podremos empezar a emitir en nuestra pantalla, juegos o incluso lo que obtengamos desde un dispositivo externo como una cámara de vídeo.

2. Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro Retina 15′ (2.5 Ghz Intel Core I7, 8GB DDR3).
  • Sistema Operativo: macOS Sierra 10.12.4
  • Entorno de desarrollo: NetBeans IDE 8.0.2
  • Servidor: Nginx 1.12.0
  • Streaming Software: OBS 18.0.1

3. Dónde alojarlo

Una de las ventajas de nginx es lo realmente ligero que puede llegar a ser y su capacidad de balancear la carga. Actualmente lo estoy utilizando desde una VPN que corre Ubuntu y he comprobado que una Raspberry Pi 3 Model B con Raspbian puede soportar varias peticiones al servidor sin problemas. En este caso, alojaré el servidor en el ordenador descrito en el punto anterior.

Como vemos, el hardware no es un punto muy importante. Lo que sí debemos tener en cuenta es la conexión a internet que tendrá la máquina que aloje el servidor RTMP y la máquina que se utilizara para realizar el streaming. Se debe tener una buena conexión tanto de subida como de bajada. Pero la velocidad mínima que debes de tener dependerá de variables como la resolución de imagen, calidad de imagen/audio y el número de peticiones simultáneas máximas que deseas tener.

4. NGINX

En los siguientes puntos aprenderemos cómo instalar nginx junto con su módulo rtmp. Además, veremos cómo realizar una configuración básica para poder empezar a testear el servicio en unos minutos.

4.1. Instalación de nginx con el módulo RTMP

Para su descarga podemos acceder al directorio de nginx desde aquí.Y el módulo rtmp desde tu Github aquí.

En este caso utilizaremos Homebrew.

$ brew tap homebrew/nginx

Agregamos el repositorio nginx a nuestra lista de fórmulas.

$ brew options nginx-full

Podemos visualizar la lista de complementos de los que disponemos en el repositorio agregado anteriormente. En este caso solo utilizaremos rtmp-module.

$ brew install nginx-full --with-rtmp-module

Después de instalar nginx deberíamos ver la siguiente salida en la consola.

instalación nginx

Entre otros, nos encontramos la siguiente información:

  • Origen de nuestro http: /usr/local/var/www
  • Puerto de nginx: 80
  • Localización de nuestro archivo nginx.conf, el cual configuraremos en el siguiente paso: /usr/local/etc/nginx/nginx.conf
  • Comando para iniciar nginx: nginx
  • Comando para detener nginx: nginx -s quit

4.2. Configuración de nuestro RTMP

Lo que haremos en este paso será agregar nuestro servidor RTMP dentro del archivo de nginx.conf. Agregaremos nuestro rtmp justo antes del http. Debería quedarnos algo como esto.

events {
    ...
}
rtmp {
    server {
        listen 1935;
        application adictos {
            live on;
            record off;
        }
    }
}

http {
    ...
}

Con esta configuración tan simple ya podemos empezar a poner a prueba nuestro servidor. Vamos a explicar qué significan estas líneas. Dentro de elemento raíz describiremos el server. Con “listen 1935” agregaremos el socket de escucha para las conexiones RTMP. Después, con las siguientes líneas describimos una aplicación dentro de nuestro servidor RTMP.

application adictos {
    live on;
    record off;
}

En este caso solo tenemos una application pero podríamos tener tantos como queramos. Creando varios podemos configurar cada uno con unas características específicas dentro del mismo servidor.

Con live on; dejamos activado el stream. Y con record off; desactivamos que nuestro directo quede grabado. Para este caso tenemos las siguientes opciones [off|all|audio|vídeo|keyframes|manual].

Como podéis ver, hacer una setup básica para empezar a emitir en unos minutos es muy fácil. Pero si queréis aprovechar toda la funcionalidad del módulo RTMP de nginx os recomiendo visitar este enlace con las directivas de configuración.

5. Empezar a emitir

Una vez establecido nuestro servidor, toca ponerse a stremear, ¿no? Para ello existen muchos programas como XSplit, OBS, Evolve, Kazam… Por experiencia propia recomiendo OBS (Open Broadcaster Software) debido a lo ligero y fácil que resulta configurarlo.

En este caso me saltaré la explicación de este software ya que mi compañera Leti hizo un tutorial explicando cómo Capturar pantalla y retransmitir en directo con OBS.

5.1. Configurar la ip de nuestro servidor

Nos disponemos a configurar el servidor que utilizaremos. Normalmente, los programas de streaming traen pre-configurados algunas plataformas como Twitch o Youtube. Pero en este caso seleccionaremos la opción de utilizar un servicio personalizado.

configuración observar

Como vemos, la URL sigue este formato:

rtmp://[IP o dominio del servidor]/[Nombre del application]

En mi caso, al utilizar un servidor alojado en mi propia máquina, utilizaré localhost. El nombre del application debe ser el que hemos configurado en nginx.conf. La clave de retransmisión es una cadena de caracteres de control. Esto se utilizará más adelante para poder ver el streaming que quieras. Los servicios mencionados antes te proveen de dicha clave.

En este punto, deberíamos poder empezar a transmitir sin problemas.

obs emitiendo

6. Visualiza tu emisión

Entraremos en cualquiera de nuestros reproductores de vídeo, en mi caso VLC. Y agregamos la dirección de red de nuestro servidor.

Esta dirección consta del siguiente formato:

rtmp://[IP o dominio del servidor]/[Nombre del application]/[Clave de retransmisión] configuración vlc

Una vez agregada la dirección podremos observar nuestro directo.

vlc recibiendo

7. Conclusiones

Hemos visto cómo en unos minutos hemos sido capaces de levantar un servidor que será capaz de retransmitir la señal de vídeo que queramos. Las plataformas que he mencionado antes son buenas, pero desde un principio nos imponen diferentes limitaciones. De esta forma, seremos capaces de transmitir cuando y lo que queramos, restringir el acceso, y mucho más.

8. Referencias

Balanceando Apps de Spring Boot con NGINX en Docker

$
0
0

En este artículo se hará uso de un NGINX para balancear 3 nodos desplegando una App de Spring-Boot en cada nodo.

Índice de contenidos

1. Introducción

La motivación de este tutorial surgió porque queríamos hacer una maqueta de la arquitectura de un cliente con docker, lo más real posible. La arquitectura a maquetar con docker era la siguiente:

¡Vamos allá!

2. Entorno

El tutorial está escrito usando el siguiente entorno:
  • Hardware: Portátil MacBook Pro Retina 15′ (2,5 Ghz Intel Core i7, 16GB DDR3)
  • Sistema Operativo: Mac OS Sierra 10.12.5
  • Entorno de desarrollo: Eclipse Neon.2 Release (4.6.2)
  • Docker version 17.03.1-ce, build c6d412e
  • Docker Machine version 0.10.0, build 76ed2a6
  • Docker Compose version 1.11.1, build 7c5d5e4

3. Levantando un solo nodo

Como una primera aproximación, vamos a levantar un contenedor (solo un nodo) que despliegue nuestra app, la cual se conecta a una BBDD que esta en otro contenedor.

Nuestra app simplemente hará una select y mostrará un mensaje diferente dependiendo del nodo que realice la select. El código de la app lo tenéis en mi repositorio de github.

Para levantar la BBDD bastaría con ejecutar el siguiente comando desde un terminal (posicionados en el directorio padre en ./src/main/resources/DockerNginx3Nodos/ del directorio /data/):

$> docker run --name mysql -p 3306:3306 -v ${Ruta_Padre_Data}/data:/docker-entrypoint-initdb.d -e MYSQL_ROOT_PASSWORD=root -d mysql:5.6

En el contenedor de BBDD tendremos una tabla que será de donde sacará el mensaje a mostrar cada nodo.

Una vez creado el contenedor de BBDD, necesitamos que pertenezca a una red. Los futuros nodos podrán hacer uso de la BBDD si también pertenecen a esta red. Los comando necesarios para crear la red y añadir el contenedor de BBDD son los siguientes:

$> docker network create dbnet
$> docker network connect dbnet mysql

Para esta primera aproximación y ver que nuestra app conecta con la BBDD, bastará con ejecutar el siguiente comando desde el directorio target de nuestro proyecto.

$> java -jar sampleApp-0.0.1-SNAPSHOT.jar --nodo.numero=001

Si todo ha ido bien, al acceder a esta url nos debería mostrar el siguiente mensaje:

4. Balanceando 3 nodos con NGINX

El objetivo de un balanceador es ofrecer un punto de entrada y distribuir la carga entre los diferentes nodos a balancear. Como estamos trabajando con docker y para no tener que crear una máquina docker por cada nodo, vamos a desplegar cada contenedor en una única máquina docker (IP 192.168.99.100) desplegándose cada App en diferentes puertos, tal como se puede ver en la siguiente imagen.

4.1. Dockerfile de cada nodo

Para nuestro ejemplo, nuestros nodos van a tener las siguientes características:

  • CentOS release 6.8 (Final)
  • OpenJDK version “1.7.0_131” 64-Bit

Para levantar cada contenedor en el puerto indicado, bastaría con modificar el Dockerfile que se muestra a continuación, poniendo el puerto en las partes marcadas entre asteriscos y establecer el ID_NODO equivalente en la propiedad ‘nodo.numero’.

FROM centos:6.8

MAINTAINER ddelcastillo <ddelcastillo@autentia.com>

# Install packages
RUN yum install -y unzip wget curl git

# install Java 7
RUN su -c "yum --assumeyes install java-1.7.0-openjdk-devel"

# create Jar Folder
RUN su -c "mkdir -p /tmp/jar"

# Environment variables
ENV HOME /root/tmp
ENV JAVA_HOME /usr/lib/jvm/java-1.7.0-openjdk.x86_64
ENV PATH $JAVA_HOME/bin:$PATH

VOLUME /tmp/jar
COPY ./jar/ /tmp/jar

VOLUME /tmp/jar/config
WORKDIR /tmp/jar

EXPOSE **10001**

ENTRYPOINT ["java","-jar","-Dserver.port=**10001**","/tmp/jar/app.jar","--nodo.numero=**001**"]

Haciendo estos cambios, tendremos 3 ficheros Dockerfile (uno por cada nodo).

4.2. Un vistazo al nginx.conf

Nuestro fichero de configuración de NGINX es el siguiente:

worker_processes auto;

events { worker_connections 1024; }

http {
    upstream node-app {
              least_conn;
              server 192.168.99.100:10001 weight=10 max_fails=3 fail_timeout=30s;
              server 192.168.99.100:10002 weight=10 max_fails=3 fail_timeout=30s;
              server 192.168.99.100:10003 weight=10 max_fails=3 fail_timeout=30s;
    }

    server {
        listen 80;
        server_name     test.com;
        location / {
            resolver           8.8.8.8 valid=300s;
            resolver_timeout   10s;
            proxy_pass         http://node-app;
            proxy_redirect     off;
            proxy_set_header   Host $host;
            proxy_set_header   X-Real-IP $remote_addr;
            proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header   X-Forwarded-Host $server_name;
        }
    }
}

A continuación, comentaremos alguna de las propiedades de aparecen en el fichero:

  • Upstream: Con la directiva upstream definimos un pool de servidores. En cada servidor podemos definir propiedades como timeout para el fallo, número de intentos, peso (capacidad), etc. También podemos definir el mecanismo de balanceo (round-robin, least-connected, ip-hash). En nuestro caso, hemos elegido least-connected, donde la petición se asignará al servidor con el menor número de conexiones activas.
  • Con respecto a las propiedades del servidor de NGINX destacamos:
    • Listen: Puerto en el que escucha el servidor.
    • Resolver: Se le fija en servidor DNS.
    • Pass_proxy: Indicamos que todas las peticiones hechas a / serán gestionadas por el pool de servidores.
    • Proxy_redirect: Al indicarle off, estamos indicando que no utilizaremos un proxy inverso.
    • Proxy_set_header: Con esta directiva podemos establecer diferentes cabeceras con información del proxy.

4.3. Docker-compose final

Nuestro docker-compose.yml quedaría de la siguiente forma:

version: "2"
networks:
  dbnet:
    external:
      name: dbnet

services:
    node1:
        build:
          context: .
          dockerfile: ./Dockerfile1
        networks:
          - dbnet
        ports:
          - 10001:10001
        volumes:
          - /Users/ddelcastillo/Downloads/sampleApp/src/main/resources/DockerNginx3Nodos/jar/:/tmp/jar/
          - /Users/ddelcastillo/Downloads/sampleApp/src/main/resources/DockerNginx3Nodos/jar/config/:/tmp/jar/config/
    node2:
      build:
        context: .
        dockerfile: ./Dockerfile2
      networks:
         - dbnet
      ports:
         - 10002:10002
      volumes:
         - /Users/ddelcastillo/Downloads/sampleApp/src/main/resources/DockerNginx3Nodos/jar/:/tmp/jar/
         - /Users/ddelcastillo/Downloads/sampleApp/src/main/resources/DockerNginx3Nodos/jar/config/:/tmp/jar/config/
    node3:
      build:
        context: .
        dockerfile: ./Dockerfile3
      networks:
        - dbnet
      ports:
        - 10003:10003
      volumes:
        - /Users/ddelcastillo/Downloads/sampleApp/src/main/resources/DockerNginx3Nodos/jar/:/tmp/jar/
        - /Users/ddelcastillo/Downloads/sampleApp/src/main/resources/DockerNginx3Nodos/jar/config/:/tmp/jar/config/
    proxy:
      build:
        context:  ./nginx
        dockerfile: Dockerfile
      ports:
        - "80:80"
      links:
          - node1:node1
          - node2:node2
          - node3:node3

Lo más destacable de este docker-compose.yml podría ser lo siguiente:

  • Como se ha declarado la BBDD e incluido a cada nodo dentro de su red. No hemos incluido la BBDD en el compose (aunque se podría), puesto que ya la había creado el contenedor e incluido a la red en el apartado 3.
  • El mapeo de puertos, tanto para la parte de docker (docker se expone la app) como para la parte host (que tiene que cuadrar con los puertos puestos en el pool se servidores en el nginx.conf)
  • Cada nodo, como se ha dicho en el apartado 4.1, tiene su propio Dockerfile
  • Como enlazamos los cada nodo en el balanceador.
  • Los volumenes compartidos, que corresponder al jar de la app de Spring Boot a desplegar y el application.yml respectivamente.

4.4. Un balanceador dockerizado funcionando

Ya solo nos queda levantar nuestro contenedores y ver que nuestro nginx balancea correctamente con el siguiente comando (desde src/resources/DockerNginx3Nodos):

$> docker-compose up -d

Si accedemos a esta url podemos ver que nos responde:

Y si realizamos muchas peticiones, vemos que cambia a:

Vemos que nuestro balanceador funciona correctamente si miramos las trazas en el terminal (ejecutando el comando anterior sin -d)

5. Conclusiones

Con este tutorial me he dado cuenta de la cantidad de posibilidades que tiene docker, dándonos la posibilidad de maquetar casi cualquier arquitectura (teniendo en cuenta la penalización en el rendimiento).

También me ha servido para conocer un poco más de cerca un balanceador como NGINX.

6. Referencias


Operaciones CRUD en Liferay 7 con MVCPortlet y JSP

$
0
0

Aprende a realizar operaciones CRUD en Liferay 7 accediendo desde un MVCPortlet a la capa de servicio generada por Service Builder y creando la vista con JSP.

Índice de contenidos


1. Introducción

En el tutorial Persistencia en Liferay 7 con Service Builder, vimos cómo definir nuestro modelo, crear en base de datos las tablas correspondientes a él y generar automáticamente la capa de modelo, persistencia y servicio. Ahora vamos a hacer uso de la capa de servicio desde un portlet para poder realizar las operaciones CRUD básicas sobre nuestro modelo compuesto por libros y escritores: crearlos, listarlos, modificarlos y eliminarlos.

Este tutorial es una continuación del antes mencionado, siendo su lectura obligatoria. No obstante, no es necesario realizar los pasos llevados a cabo en él, pues daremos las indicaciones para crear un proyecto de cero.

Puedes encontrar el código de este tutorial en este repositorio. He ido separando en diferentes commits los pasos que se dan en el tutorial para así facilitar el seguimiento del mismo.


2. Entorno

Este tutorial se ha desarrollado en el siguiente entorno:

  • Portátil MacBook Pro (Retina, 15′, mediados 2015), macOS Sierra 10.12.5
  • Liferay Community Edition Portal 7.0.2 GA3 (Wilberforce / Build 7002 / August 5, 2016)
  • Java 1.8.0_131
  • PostgreSQL 9.6.2
  • IntelliJ IDEA Ultimate 2017.1.3

3. Preparar el proyecto

Si seguiste el tutorial de Service Builder, entonces ya tendrás un proyecto preparado con entidades Libro y Escritor relacionadas de forma M-N: un libro puede ser escrito por varios escritores y un escritor puede escribir diferentes libros. Si no lo tienes, sigue una serie de pasos para generar un proyecto similar al del tutorial de Service Builder, con la salvedad de que no tendremos clases de actualización de base de datos (UpgradeProcess y UpgradeStepRegistrator), ya que generaremos el modelo final de primeras y no haremos cambios sobre él, es decir, nuestro service.xml no cambiará.

Para preparar el proyecto, empieza abriendo la terminal y sigue los siguientes pasos:

  1. Creamos el proyecto Liferay:
    ~/workspaces/pruebas
    $ blade init tutorial-liferay7-crud
  2. Añadimos el paquete Liferay Portal + Tomcat:
    ~/workspaces/pruebas/tutorial-liferay7-crud
    $ ./gradlew initBundle
  3. Arrancamos el servidor local:
    ~/workspaces/pruebas/tutorial-liferay7-crud
    $ blade server start
  4. Una vez se haya levantado, accedemos a
    http://localhost:8080/
    y vemos el asistente de configuración de Liferay Portal. Como vamos a persistir en base de datos, no usaremos Hypersonic, sino que emplearemos otra base de datos (puedes seguir el tutorial Configurar Liferay 7 con PostgreSQL para ello).
  5. Tras haber configurado Liferay Portal y accedido a él (nos habrá pedido reiniciarlo al elegir otra base de datos como PostgreSQL), utilizamos la plantilla service-builder de Blade CLI para generar los módulos libro-api y libro-service:
    ~/workspaces/pruebas/tutorial-liferay7-crud
    $ blade create -t service-builder -p tutoriales.liferay.crud.libro libro
  6. Creamos, desde el directorio libro, el módulo libro-web, donde tendremos nuestro portlet MVC:
    ~/workspaces/pruebas/tutorial-liferay7-crud/modules/libro
    $ blade create -t mvc-portlet -p tutoriales.liferay.crud.libro -c MyMvcPortlet libro-web
  7. Abrimos el archivo bnd.bnd del módulo libro-web y cambiamos la línea:
    Bundle-SymbolicName: tutoriales.liferay.crud.libro
    por:
    Bundle-SymbolicName: tutoriales.liferay.crud.libro.web
  8. Modificamos el archivo service.xml:
    <?xml version="1.0"?>
    <!DOCTYPE service-builder PUBLIC "-//Liferay//DTD Service Builder 7.0.0//EN" "http://www.liferay.com/dtd/liferay-service-builder_7_0_0.dtd">
    
    <service-builder package-path="tutoriales.liferay.crud.libro">
        <namespace>LIBRO</namespace>
    
        <entity name="Libro" uuid="true" local-service="true" remote-service="false">
            <!-- PK fields -->
            <column name="libroId" primary="true" type="long"/>
    
            <!-- Group instance -->
            <column name="groupId" type="long"/>
    
            <!-- Audit fields -->
            <column name="companyId" type="long"/>
            <column name="userId" type="long"/>
            <column name="userName" type="String"/>
            <column name="createDate" type="Date"/>
            <column name="modifiedDate" type="Date"/>
    
            <!-- Other fields -->
            <column name="titulo" type="String"/>
            <column name="publicacion" type="Date"/>
            <column name="genero" type="String"/>
            <column name="escritores" type="Collection" entity="Escritor" mapping-table="Libros_Escritores"/>
    
            <!-- Order -->
            <order by="asc">
                <order-column name="titulo"/>
            </order>
    
            <!-- Finder methods -->
            <finder name="Titulo" return-type="Collection">
                <finder-column name="titulo"/>
            </finder>
        </entity>
    
        <entity name="Escritor" uuid="true" local-service="true" remote-service="false">
            <!-- PK fields -->
            <column name="escritorId" primary="true" type="long"/>
    
            <!-- Group instance -->
            <column name="groupId" type="long"/>
    
            <!-- Audit fields -->
            <column name="companyId" type="long"/>
            <column name="userId" type="long"/>
            <column name="userName" type="String"/>
            <column name="createDate" type="Date"/>
            <column name="modifiedDate" type="Date"/>
            <column name="libros" type="Collection" entity="Libro" mapping-table="Libros_Escritores"/>
    
            <!-- Other fields -->
            <column name="nombre" type="String"/>
    
            <!-- Order -->
            <order by="asc">
                <order-column name="nombre"/>
            </order>
    
            <!-- Finder methods -->
            <finder name="Nombre" return-type="Collection">
                <finder-column name="nombre"/>
            </finder>
        </entity>
    </service-builder>
  9. Generamos el código con Service Builder:
    ~/workspaces/pruebas/tutorial-liferay7-crud
    $ ./gradlew buildService
  10. Añadimos el módulo libro-api como dependencia de libro-web, para lo cual incluimos la línea
    compileOnly project(":modules:libro:libro-api")
    en el archivo build.gradle del módulo libro-web.
  11. Desplegamos los módulos:
    ~/workspaces/pruebas/tutorial-liferay7-crud
    $ blade deploy
    Si nos dio algún error del tipo «Unresolved requirement: Import-Package: tutoriales.liferay.crud.libro.exception», volvemos a ejecutar
    blade deploy
    . Este error es debido a que
    blade deploy
    despliega, en orden, libro-service, libro-web y libro-api y, como libro-service depende de libro-api, no puede desplegarlo. La segunda vez que ejecutamos el comando, como ya está desplegado libro-api, no tenemos problema. Si esto no funcionase, podemos ejecutar
    gradle clean && gradle build && gradle deploy
    en cada módulo y volver a probar.

Si accedemos a Liferay Portal, podemos buscar el portlet por el nombre «libro-web Portlet» y añadirlo a nuestro portal (puedes ver cómo hacerlo en el tutorial sobre Blade CLI). En este momento, el portlet simplemente mostrará los textos «libro-web Portlet» y «Hello from libro-web JSP!».


4. Analizar el módulo libro-web

Tras usar Blade CLI para crear este módulo, en el paquete java se generó únicamente la clase MyMvcPortlet. En resources, el archivo Language.properties y los JSP init.jsp y view.jsp. Muy pocos archivos en comparación con todos los que generó Service Builder en los módulos libro-api y libro-service.

Si seguimos el patrón de diseño MVC (Modelo-Vista-Controlador), nuestro modelo fue el que generamos con Service Builder; la vista, los JSP; y el controlador, el portlet MyMvcPortlet. Nuestro portlet recibirá peticiones de la vista, operará con el modelo y responderá a la vista.

4.1. MVCPortlet

MyMvcPortlet extiende MVCPortlet, que es la clase que Liferay nos insta a emplear, y está anotada con

@Component
para indicar que es un servicio declarativo de OSGi. Sus elementos son:
  • immediate = true
    . Indica que el componente se active inmediatamente después de ser instalado.
  • service = Portlet.class
    . Especifica el tipo bajo el cual registrar este componente como servicio. En nuestro caso será
    javax.portlet.Portlet
    .
  • property = {...}
    . Conjunto de propiedades del componente. Explicaremos algunas más adelante.

En principio, la lógica del controlador será sencilla, así que tendremos todo nuestro código en la clase MyMvcPortlet. Posteriormente lo separaremos en comandos MVC de Liferay.

4.2. JSP

Los archivos JSP formarán nuestra vista. Liferay nos dice que es una buena práctica que el archivo init.jsp contenga todos nuestros

import
de Java, declaraciones
taglib
e inicialización de variables.

init.jsp es incluido por view.jsp, que es el JSP que representa la vista del portlet. Pero ¿dónde se establece que esto sea así? En la clase MyMvcPortlet. Si nos fijamos en la propiedad

javax.portlet.init-param.view-template
, veremos que su valor es
/view.jsp
.

4.3. Language.properties

El archivo Language.properties está compuesto de pares

nombre=valor
y sirve para definir los textos que aparecen en nuestro portlet. Gracias a él podemos internacionalizar nuestro portlet, pues podemos crear archivos Language_en.properties, Language_es.properties, Language_fr.properties, etc. en los que incluir los textos en diferentes idiomas.

5. Realizar operaciones CRUD

Vamos a dar funcionalidad a nuestro portlet para que sea capaz de realizar operaciones CRUD sobre el modelo de libros y escritores definido. De esta manera, aprenderemos cómo desarrollar con portlets MVC y vista en JSP y cómo acceder desde el portlet al código generado por Service Builder.

5.1. [C] Crear

Queremos que nuestro portlet permita guardar escritores en base de datos. Tendrá un campo para escribir el nombre y un botón para guardar. La clase MyMvcPortlet recibirá el nombre del escritor y empleará la capa de servicio generada por Service Builder para añadir un nuevo escritor a base de datos.

En el tutorial Persistencia en Liferay 7 con Service Builder, vimos que las clases LibroLocalServiceUtil y EscritorLocalServiceUtil son nuestro punto de entrada a la capa de servicio. Para añadir un escritor desde MyMvcPortlet, tendríamos que usar EscritorLocalServiceUtil, pero vemos que su método

addEscritor
pide por parámetro un objeto Escritor. Escritor es una interfaz implementada por EscritorImpl, pero esta implementación es del módulo libro-service, del cual libro-web no depende —y queremos mantener esto así—, así que no podemos crear un Escritor desde MyMvcPortlet. ¿Qué hacemos entonces? Pues, básicamente, añadir un método a EscritorLocalServiceUtil que nos permita añadir escritores a partir de su nombre —y de un par de campos de multitenencia y auditoría—.

Vale, tenemos que añadir el nuevo método a EscritorLocalServiceUtil, pero esta clase es de libro-api, así que no debemos escribir nuestros propios métodos ahí. Lo que hay que hacer es desarrollar nuestro método en EscritorLocalServiceImpl (una de las poquitas clases que podemos editar) y, con Service Builder, regenerar EscritorLocalServiceUtil para que lo añada a ella y así podamos utilizarlo desde nuestro portlet. Vamos a ello.

Empezamos añadiendo el código a nuestra clase EscritorLocalServiceImpl:

package tutoriales.liferay.crud.libro.service.impl;

import aQute.bnd.annotation.ProviderType;
import tutoriales.liferay.crud.libro.model.Escritor;
import tutoriales.liferay.crud.libro.model.impl.EscritorImpl;
import tutoriales.liferay.crud.libro.service.base.EscritorLocalServiceBaseImpl;

@ProviderType
public class EscritorLocalServiceImpl extends EscritorLocalServiceBaseImpl {

    public void addEscritor(long groupId, long companyId, long userId, String userName, String nombre) {
        final Escritor escritor = new EscritorImpl();
        escritor.setEscritorId(counterLocalService.increment());
        escritor.setGroupId(groupId);
        escritor.setCompanyId(companyId);
        escritor.setUserId(userId);
        escritor.setUserName(userName);
        escritor.setNombre(nombre);

        addEscritor(escritor);
    }

}

Detalles a tener en cuenta:

  • Parámetros del método. Recordemos que nuestro Escritor tenía una serie de campos además del nombre, por lo que tendremos que especificarlos, aunque no todos: no hace falta preocuparse por los atributos createDate y modifiedDate (cuándo se creo y cuándo se modificó el escritor), pues estos se asignan automáticamente; sin embargo, no nos libramos de establecer cuál es el groupId y el companyId (identificadores del sitio y de la instancia del portal, respectivamente) y el userId y userName (identificador y nombre del usuario que crea el escritor).
  • Creación de Escritor. Aquí ya podemos importar EscritorImpl para crear una instancia de Escritor.
  • Adición de Escritor. Una vez construimos el escritor, lo añadimos a base de datos con el método
    addEscritor
    que ya tenía la clase.

Ahora ejecutamos Service Builder para generar el método en EscritorLocalServiceUtil:

~/workspaces/pruebas/tutorial-liferay7-crud
$ ./gradlew buildService

Ya podemos usar el método desde nuestro portlet. Veamos su código primero:

package tutoriales.liferay.crud.libro.portlet;

import com.liferay.portal.kernel.portlet.bridges.mvc.MVCPortlet;
import com.liferay.portal.kernel.theme.ThemeDisplay;
import com.liferay.portal.kernel.util.ParamUtil;
import com.liferay.portal.kernel.util.WebKeys;
import org.osgi.service.component.annotations.Component;
import tutoriales.liferay.crud.libro.service.EscritorLocalServiceUtil;

import javax.portlet.ActionRequest;
import javax.portlet.ActionResponse;
import javax.portlet.Portlet;
import javax.portlet.ProcessAction;

@Component(
        immediate = true,
        property = {
                "com.liferay.portlet.display-category=category.sample",
                "com.liferay.portlet.instanceable=true",
                "javax.portlet.display-name=Libros y escritores",
                "javax.portlet.init-param.template-path=/",
                "javax.portlet.init-param.view-template=/view.jsp",
                "javax.portlet.resource-bundle=content.Language",
                "javax.portlet.security-role-ref=power-user,user"
        },
        service = Portlet.class
)
public class MyMvcPortlet extends MVCPortlet {

    @ProcessAction(name = "addEscritor")
    public void addEscritor(ActionRequest request, ActionResponse response) {
        final ThemeDisplay td = (ThemeDisplay) request.getAttribute(WebKeys.THEME_DISPLAY);
        final String nombre = ParamUtil.getString(request, "nombreEscritor");

        EscritorLocalServiceUtil.addEscritor(td.getSiteGroupId(), td.getCompanyId(), td.getUser().getUserId(), td.getUser().getFullName(), nombre);
    }

}

Hemos creado un método

addEscritor
anotado con
@ProcessAction
y un atributo
name
con valor
"addEscritor"
. Esta anotación sirve para indicar que el método
addEscritor
será el que se ejecute cuando desde la vista (desde JSP) se realice una petición al servidor para procesar la acción “addEscritor”. Por cierto, el nombre de la acción y el nombre del método no tienen por qué ser iguales.

Cuando el portlet reciba la petición de ejecutar esta acción, creará un escritor. Para ello necesita su nombre, y este viene dado por la vista en la petición, en el parámetro cuyo nombre es

"nombreEscritor"
. Además del nombre, el método
EscritorLocalServiceUtil.addEscritor
necesita otra serie de campos. Afortunadamente, estos los podemos obtener de la petición creando un objeto
ThemeDisplay
.

Ahora nos queda definir la vista en el archivo view.jsp. Vamos a tener un formulario con un campo de texto en el que introducir el nombre del escritor y un botón para crear dicho escritor.

<%@ page language="java" contentType="text/html; charset=UTF-8" %>

<%@ include file="./init.jsp" %>

<portlet:actionURL name="addEscritor" var="addEscritorUrl"/>

<aui:form action="${addEscritorUrl}">
    <aui:input name="nombreEscritor" type="textarea" label="Escribe aquí el nombre del escritor:"/>
    <aui:button name="addEscritorButton" type="submit" value="Crear escritor"/>
</aui:form>

Analicemos el código. Por una parte, creamos una variable

actionURL
, llamada
addEscritorUrl
, que apunta a
"addEscritor"
, el nombre de la acción para indicar al portlet que cree un escritor (recordemos la anotación
@ProcessAction(name = "addEscritor")
). Después, creamos un formulario al que le pasamos la variable
addEscritorUrl
para que sepa dónde mandar los datos. Dentro del formulario tenemos un botón y un campo de texto en el que escribir el nombre del autor. El nombre de este campo (
"nombreEscritor"
) es el que usamos en el portlet, el que vimos que utilizábamos para recoger su valor de la petición (
ParamUtil.getString(request, "nombreEscritor");
).

Con el código listo, desplegamos con

blade deploy
y accedemos a Liferay Portal para ver nuestro portlet con los nuevos cambios. Seguramente te pase como a mí y veas el portlet, aparentemente, correcto:

Sin embargo, si intentas añadir un escritor, obtendrás una excepción

java.lang.NoSuchMethodError
y el portlet fallará:

Para solucionarlo, ejecuta

gradle clean && gradle build && gradle deploy
en cada módulo (al menos en libro-api), recarga la página y vuelve a probar:

5.2. [R] Leer

El siguiente paso tras crear escritores es poder listarlos: realizar una operación de lectura de base de datos y pintar la colección de escritores recibida en la vista.

Para la creación de escritores, el flujo consistía en que la vista mandaba al portlet el nombre de un escritor y éste hacía uso de la capa de servicio para crear uno nuevo y guardarlo en base de datos. Ahora el sentido será el contrario: cuando se vaya a pintar el portlet, éste hará una petición a base de datos para recuperar todos los escritores y se los mandará a la vista para que los pinte.

Empezamos añadiendo a nuestra clase MyMvcPortlet el siguiente método:

@Override
public void render(RenderRequest renderRequest, RenderResponse renderResponse) throws IOException, PortletException {
    final List<Escritor> escritores = EscritorLocalServiceUtil.getEscritors(0, Integer.MAX_VALUE);

    renderRequest.setAttribute("escritores", escritores);

    super.render(renderRequest, renderResponse);
}

¿Para qué sirve el método

render
que sobreescribimos? Antes de nada, recordemos que las propiedades
"javax.portlet.init-param.template-path=/"
y
"javax.portlet.init-param.view-template=/view.jsp"
de nuestro portlet sirven para saber con qué vista se pinta el portlet: indican, respectivamente, que los JSP se encuentran en la raíz del directorio resources y que aquel que hará de vista será view.jsp. Pues bien, en nuestro caso no nos basta con que se pinte directamente el portlet a partir del archivo view.jsp; antes debemos pasarle la lista de escritores. Ahí es cuando entra en juego el método
render
.

En el método

render
, usamos
EscritorLocalServiceUtil.getEscritors
para recuperar todos los escritores y los añadimos al objeto
RenderRequest
como atributo de nombre
"escritores"
.

Ahora modificamos el archivo view.jsp para añadir lo siguiente:

<jsp:useBean id="escritores" type="java.util.List<tutoriales.liferay.crud.libro.model.Escritor>" scope="request"/>

<liferay-ui:search-container emptyResultsMessage="No has creado todavía ningún escritor.">
    <liferay-ui:search-container-results results="${escritores}"/>

    <liferay-ui:search-container-row className="tutoriales.liferay.crud.libro.model.Escritor" modelVar="escritor">
        <liferay-ui:search-container-column-text name="Nombre" property="nombre"/>
    </liferay-ui:search-container-row>

    <liferay-ui:search-iterator/>
</liferay-ui:search-container>

En la primera línea, recogemos la lista de escritores que mandamos desde el portlet. A continuación, empleamos el Liferay’s Search Container (

search-container
) para crear una tabla en la que listar los escritores, aunque de momento solo va a tener una columna para mostrar sus nombres. Analicemos su código:
  • El atributo
    emptyResultsMessage
    nos permite definir el texto que se pintará si la lista de escritores es vacía.
  • En
    search-container-results
    indicamos la lista que vamos a pintar:
    results="${escritores}"
    .
  • Con
    search-container-row
    aclaramos cuál es la clase de los elementos de nuestra lista:
    tutoriales.liferay.crud.libro.model.Escritor
    .
  • Definimos la columna con
    search-container-column-text
    . El atributo
    name
    es el nombre de la columna y el atributo
    property
    es el campo de la clase Escritor que va en ella.

Por cierto, el componente

search-container
proporciona funcionalidad extra, como paginación, que queda fuera de este tutorial y no usaremos, pero está bien saberlo.

Desplegamos y vemos nuestra lista de escritores:

5.3. [U] Actualizar

Para poder actualizar el nombre de un escritor, vamos a añadir a la tabla una nueva columna que tenga un botón de edición. Al pulsarlo, éste repintará el portlet —cambiará el JSP— para mostrar un campo de texto en el que poner el nuevo nombre y un botón para confirmar los cambios. Cuando se confirme, el portlet pintará su vista inicial, la del listado de escritores —volverá al JSP inicial—. De esta manera, aprenderemos además lo que hay que hacer para que el portlet cambie entre diferentes JSP.

5.3.1. Redirigir al formulario de edición

Empezamos añadiendo una nueva columna a nuestro

search-container
del archivo view.jsp:
<liferay-ui:search-container-row className="tutoriales.liferay.crud.libro.model.Escritor" modelVar="escritor">
    <liferay-ui:search-container-column-text name="Nombre" property="nombre"/>
    <liferay-ui:search-container-column-jsp name="Editar" path="/escritorActionButtons.jsp"/>
</liferay-ui:search-container-row>

Esta columna es de tipo

search-container-column-jsp
, lo que quiere decir que incluirá el JSP que indiquemos en su atributo
path
. El nuevo archivo escritorActionButtons.jsp es el siguiente:
<%@ include file="./init.jsp" %>

<%@ page import="com.liferay.portal.kernel.util.WebKeys" %>
<%@ page import="com.liferay.taglib.search.ResultRow" %>
<%@ page import="tutoriales.liferay.crud.libro.model.Escritor" %>

<%
    final ResultRow row = (ResultRow) request.getAttribute(WebKeys.SEARCH_CONTAINER_RESULT_ROW);
    final Escritor escritor = (Escritor) row.getObject();
%>

<portlet:actionURL name="displayEscritorEdition" var="displayEscritorEditionUrl">
    <portlet:param name="idEscritor" value="<%=String.valueOf(escritor.getEscritorId())%>"/>
</portlet:actionURL>

<liferay-ui:icon-menu>
    <liferay-ui:icon image="edit" message="Editar" url="<%=displayEscritorEditionUrl%>"/>
</liferay-ui:icon-menu>

Este JSP pinta un icono con imagen

"edit"
que, al ser pulsado, realiza la acción
"displayEscritorEdition"
guardada en la variable
"displayEscritorEditionUrl"
. Esta acción, que ahora añadiremos a MyMvcPortlet para que la procese, consistirá en que el portlet cambie su vista view.jsp por una nueva vista escritorEdit.jsp en la que haya un campo para poder editar el nombre del escritor. Como tenemos que saber qué escritor queremos editar, habrá que mandar el identificador del mismo. Esto lo hacemos con la línea
<portlet:param name="idEscritor" value="<%=String.valueOf(escritor.getEscritorId())%>"/>
. El escritor lo sacamos de la petición, de su atributo
WebKeys.SEARCH_CONTAINER_RESULT_ROW
.

Añadimos la acción

"displayEscritorEdition"
a MyMvcPortlet:
@ProcessAction(name = "displayEscritorEdition")
public void displayEscritorEdition(ActionRequest request, ActionResponse response) throws IOException, PortletException, PortalException {
    final String id = request.getParameter("idEscritor");
    final Escritor escritor = EscritorLocalServiceUtil.getEscritor(Long.valueOf(id));

    request.setAttribute("escritor", escritor);
    response.setRenderParameter("mvcPath", "/escritorEdit.jsp");
}

El método recupera el escritor a partir de su identificador, lo añade a la request y redirige a escritorEdit.jsp. Eso último se hace añadiendo a la response el parámetro

"mvcPath"
—convención de Liferay— con valor la ruta del JSP.

5.3.2. Crear formulario de edición

Creamos la vista escritorEdit.jsp:

<%@ page contentType="text/html; charset=UTF-8" %>

<%@ include file="./init.jsp" %>

<jsp:useBean id="escritor" type="tutoriales.liferay.crud.libro.model.Escritor" scope="request"/>

<portlet:actionURL name="editEscritor" var="editEscritorUrl"/>

<aui:form action="${editEscritorUrl}">
    <aui:input name="nombreEscritor" label="Modifica aquí el nombre del escritor:" value="<%=escritor.getNombre() %>"/>
    <aui:input name="idEscritor" type="hidden" value="<%=String.valueOf(escritor.getEscritorId()) %>"/>
    <aui:button name="editEscritorButton" type="submit" value="Editar escritor"/>
</aui:form>

Este JSP recibe el escritor (

useBean
), crea una variable en la que guarda una acción para indicar al portlet que debe editar un escritor (
actionURL
) y pinta un formulario (
form
) con un campo de texto en el que escribir el nombre (
"nombreEscritor"
), un botón para realizar la acción y un campo de texto oculto (
"idEscritor"
) en el que guarda el identificador del escritor para así mandárselo al portlet y que éste sepa qué escritor modificar.

Ahora nos queda añadir a MyMvcPortlet el método para procesar la acción de edición. Este método va a llamar a la capa de servicio para actualizar base de datos, y aquí nos pasa algo parecido a lo que nos ocurría con la creación: el método

updateEscritor
de EscritorLocalServiceUtil recibe por parámetro un objeto Escritor que no podemos construir desde el portlet. Procedemos, por tanto, de la misma manera que antes: creamos en EscritorLocalServiceImpl un método de actualización a partir del id y nombre del escritor, generamos el código con
./gradlew buildService
y usamos el método generado desde nuestro portlet.

El método de actualización en EscritorLocalServiceImpl es:

public void updateEscritor(long id, String nombre) throws PortalException {
    final Escritor escritor = getEscritor(id);
    escritor.setNombre(nombre);

    updateEscritor(escritor);
}

Tras generar el código con Service Builder, creamos en nuestra clase MyMvcPortlet un método que responderá a la petición de ejecución de la acción

"editEscritor"
:
@ProcessAction(name = "editEscritor")
public void editEscritor(ActionRequest request, ActionResponse response) throws IOException, PortletException, PortalException {
    final String id = request.getParameter("idEscritor");
    final String nombre = request.getParameter("nombreEscritor");

    EscritorLocalServiceUtil.updateEscritor(Long.valueOf(id), nombre);

    response.setRenderParameter("mvcPath", "/view.jsp");
}

La acción recupera el identificador y el nuevo nombre del escritor, se los envía al método que acabamos de crear para que actualice y, por último, redirige a la vista inicial (

view.jsp
) utilizando el, ya visto, parámetro
"mvcPath"
.

Desplegamos (recuerda hacer

gradle clean && gradle build && gradle deploy
en, al menos, libro-api si te falla) y así quedaría nuestro portlet:

5.4. [D] Eliminar

La última operación es la de borrado y, con todo lo que llevamos ya montado, implementarla será fácil.

Vamos a aprovechar la columna en la que pusimos el botón de edición para poner también el de borrado, por lo que podemos empezar cambiándole el nombre, pues la llamamos «Editar». Ahora modificamos nuestro archivo escritorActionButtons.jsp para añadirle la URL a la acción de borrado (

"deleteEscritor"
) y el nuevo botón:
<portlet:actionURL name="deleteEscritor" var="deleteEscritorUrl">
    <portlet:param name="idEscritor" value="<%=String.valueOf(escritor.getEscritorId())%>"/>
</portlet:actionURL>

<liferay-ui:icon-menu>
    <liferay-ui:icon image="edit" message="Editar" url="<%=displayEscritorEditionUrl%>"/>
    <liferay-ui:icon image="delete" message="Eliminar" url="<%=deleteEscritorUrl%>"/>
</liferay-ui:icon-menu>

Escribimos el método de borrado en nuestra clase MyMvcPortlet:

@ProcessAction(name = "deleteEscritor")
public void deleteEscritor(ActionRequest request, ActionResponse response) throws IOException, PortletException, PortalException {
    final String id = request.getParameter("idEscritor");

    EscritorLocalServiceUtil.deleteEscritor(Long.valueOf(id));
}

Desplegamos y ya podemos borrar escritores:


6. Simplificar el controlador con comandos MVC

Veamos cómo ha quedado nuestro controlador:

public class MyMvcPortlet extends MVCPortlet {

    @Override
    public void render(RenderRequest renderRequest, RenderResponse renderResponse) throws IOException, PortletException {
        final List<Escritor> escritores = EscritorLocalServiceUtil.getEscritors(0, Integer.MAX_VALUE);

        renderRequest.setAttribute("escritores", escritores);

        super.render(renderRequest, renderResponse);
    }

    @ProcessAction(name = "addEscritor")
    public void addEscritor(ActionRequest request, ActionResponse response) {
        final ThemeDisplay td = (ThemeDisplay) request.getAttribute(WebKeys.THEME_DISPLAY);
        final String nombre = ParamUtil.getString(request, "nombreEscritor");

        EscritorLocalServiceUtil.addEscritor(td.getSiteGroupId(), td.getCompanyId(), td.getUser().getUserId(), td.getUser().getFullName(), nombre);
    }

    @ProcessAction(name = "displayEscritorEdition")
    public void displayEscritorEdition(ActionRequest request, ActionResponse response) throws IOException, PortletException, PortalException {
        final String id = request.getParameter("idEscritor");
        final Escritor escritor = EscritorLocalServiceUtil.getEscritor(Long.valueOf(id));

        request.setAttribute("escritor", escritor);
        response.setRenderParameter("mvcPath", "/escritorEdit.jsp");
    }

    @ProcessAction(name = "editEscritor")
    public void editEscritor(ActionRequest request, ActionResponse response) throws IOException, PortletException, PortalException {
        final String id = request.getParameter("idEscritor");
        final String nombre = request.getParameter("nombreEscritor");

        EscritorLocalServiceUtil.updateEscritor(Long.valueOf(id), nombre);

        response.setRenderParameter("mvcPath", "/view.jsp");
    }

    @ProcessAction(name = "deleteEscritor")
    public void deleteEscritor(ActionRequest request, ActionResponse response) throws IOException, PortletException, PortalException {
        final String id = request.getParameter("idEscritor");

        EscritorLocalServiceUtil.deleteEscritor(Long.valueOf(id));
    }

}

Es una clase que realiza pocas operaciones y no es muy larga. Pero ¿qué ocurriría si nuestro portlet soportase más funcionalidad? Seguramente necesitaríamos más métodos anotados con

@ProcessAction(name = "nueva_acción")
y estos serían más complejos y tendrían más líneas de código. La clase iría creciendo, empeorando así la legibilidad. Para solucionar esto, Liferay ha creado comandos MVC que nos permiten aliviar el código de MVCPortlet: MVC Action Command, MVC Render Command y MVC Resource Command.

6.1. MVC Action Command

Los métodos de nuestra clase MyMvcPortlet podemos transformarlos en MVC Action Commands. Liferay proporciona la interfaz MVCActionCommand y una clase BaseMVCActionCommand que la implementa, por lo que nosotros deberemos extender BaseMVCActionCommand en lugar de implementar MVCActionCommand.

Por supuesto, puedes nombrar como desess a las clases que creemos, pero es buena práctica llamarlas «XXXMvcActionCommand», donde «XXX» es el nombre de la acción. Por ejemplo, si transformamos el método encargado de añadir escritores, como lo nombramos «addEscritor», tiene sentido que creemos una clase llamada «AddEscritorMvcActionCommand»:

package tutoriales.liferay.crud.libro.portlet;

import com.liferay.portal.kernel.portlet.bridges.mvc.BaseMVCActionCommand;
import com.liferay.portal.kernel.portlet.bridges.mvc.MVCActionCommand;
import com.liferay.portal.kernel.theme.ThemeDisplay;
import com.liferay.portal.kernel.util.ParamUtil;
import com.liferay.portal.kernel.util.WebKeys;
import org.osgi.service.component.annotations.Component;
import tutoriales.liferay.crud.libro.service.EscritorLocalServiceUtil;

import javax.portlet.ActionRequest;
import javax.portlet.ActionResponse;

@Component(
        immediate = true,
        property = {
                "javax.portlet.name=tutoriales_liferay_crud_libro_portlet_MyMvcPortlet",
                "mvc.command.name=addEscritor"
        },
        service = MVCActionCommand.class
)
public class AddEscritorMvcActionCommand extends BaseMVCActionCommand {

    @Override
    protected void doProcessAction(ActionRequest request, ActionResponse response) throws Exception {
        final ThemeDisplay td = (ThemeDisplay) request.getAttribute(WebKeys.THEME_DISPLAY);
        final String nombre = ParamUtil.getString(request, "nombreEscritor");

        EscritorLocalServiceUtil.addEscritor(td.getSiteGroupId(), td.getCompanyId(), td.getUser().getUserId(), td.getUser().getFullName(), nombre);
    }

}

El método

addEscritor
de MyMvcPortlet lo eliminamos y ponemos su contenido en nuestra nueva clase, en el método
doProcessAction
. Esta clase está anotada como componente de OSGi y sus propiedades son esenciales:
  • javax.portlet.name
    . Aquí tendremos que poner el nombre de nuestro portlet, que será aquel definido en MyMvcPortlet, en la propiedad homónima. Es decir, la propiedad
    "javax.portlet.name=tutoriales_liferay_crud_libro_portlet_MyMvcPortlet"
    deberá estar tanto en MyMvcPortlet como en AddEscritorMvcActionCommand: la primera identifica al portlet, la segunda lo referencia.
  • mvc.command.name
    . Esta propiedad define el nombre de la acción a procesar. Debe ser el que teníamos en la ya borrada anotación
    @ProcessAction
    .

Haremos lo mismo con las acciones «editEscritor» y «deleteEscritor».

6.2. MVC Render Command

En nuestra clase MyMvcPortlet ya solamente quedan dos métodos:

render
y
displayEscritorEdition
. Recordemos que este último, anotado con
@ProcessAction
, recibía la petición
"displayEscritorEdition"
que venía del
actionURL
de escritorActionButtons.jsp y su propósito era redirigir a escritorEdit.jsp. Vamos a editar el archivo JSP para transformar la
actionURL
en una
renderURL
:
<portlet:renderURL var="displayEscritorEditionUrl">
    <portlet:param name="mvcRenderCommandName" value="displayEscritorEdition"/>
    <portlet:param name="idEscritor" value="<%= String.valueOf(escritor.getEscritorId()) %>"/>
</portlet:renderURL>

Como vemos, es muy parecida a la antigua

actionURL
, solo que contiene un parámetro nombrado
"mvcRenderCommandName"
que indica cuál va a ser el componente
MVCRenderCommand
que procese la petición.
MVCRenderCommand
es una interfaz que nosotros tenemos que implementar:
package tutoriales.liferay.crud.libro.portlet;

import com.liferay.portal.kernel.exception.PortalException;
import com.liferay.portal.kernel.portlet.bridges.mvc.MVCRenderCommand;
import org.osgi.service.component.annotations.Component;
import tutoriales.liferay.crud.libro.model.Escritor;
import tutoriales.liferay.crud.libro.service.EscritorLocalServiceUtil;

import javax.portlet.PortletException;
import javax.portlet.RenderRequest;
import javax.portlet.RenderResponse;

@Component(
        immediate = true,
        property = {
                "javax.portlet.name=tutoriales_liferay_crud_libro_portlet_MyMvcPortlet",
                "mvc.command.name=displayEscritorEdition"
        },
        service = MVCRenderCommand.class
)
public class EditEscritorMvcRenderCommand implements MVCRenderCommand {

    @Override
    public String render(RenderRequest request, RenderResponse response) throws PortletException {
        final String id = request.getParameter("idEscritor");

        try {
            final Escritor escritor = EscritorLocalServiceUtil.getEscritor(Long.valueOf(id));
            request.setAttribute("escritor", escritor);

        } catch (PortalException e) {
            throw new RuntimeException(e);
        }

        return "/escritorEdit.jsp";
    }

}

Como vemos, de nuevo indicamos el nombre del portlet a través de la propiedad

javax.portlet.name
y el nombre del comando con
mvc.command.name
, nombre que indicamos en
renderURL
.

Por último, borramos el método

displayEscritorEdition
de MyMvcPortlet, clase que ya quedará así de simple:
public class MyMvcPortlet extends MVCPortlet {

    @Override
    public void render(RenderRequest renderRequest, RenderResponse renderResponse) throws IOException, PortletException {
        final List<Escritor> escritores = EscritorLocalServiceUtil.getEscritors(0, Integer.MAX_VALUE);

        renderRequest.setAttribute("escritores", escritores);

        super.render(renderRequest, renderResponse);
    }

}

6.3. MVC Resource Command

El último de los comandos, cuya interfaz es

MVCResourceCommand
y está implementado por la clase abstracta
BaseMVCResourceCommand
, es utilizado para procesar peticiones a recursos.

Vamos a dar la opción de descargar los escritores existentes. El usuario pulsará un botón y el navegador le ofrecerá la posibilidad de abrir o descargar un archivo de texto plano con los datos de los escritores que estén guardados en base de datos. Para ello, empezamos creando nuestro comando:

package tutoriales.liferay.crud.libro.portlet;

import com.liferay.portal.kernel.portlet.PortletResponseUtil;
import com.liferay.portal.kernel.portlet.bridges.mvc.BaseMVCResourceCommand;
import com.liferay.portal.kernel.portlet.bridges.mvc.MVCResourceCommand;
import com.liferay.portal.kernel.util.ContentTypes;
import org.osgi.service.component.annotations.Component;
import tutoriales.liferay.crud.libro.service.EscritorLocalServiceUtil;

import javax.portlet.ResourceRequest;
import javax.portlet.ResourceResponse;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;

@Component(
        property = {
                "javax.portlet.name=tutoriales_liferay_crud_libro_portlet_MyMvcPortlet",
                "mvc.command.name=downloadEscritores"
        },
        service = MVCResourceCommand.class
)
public class DownloadEscritoresMvcResourceCommand extends BaseMVCResourceCommand {

    @Override
    protected void doServeResource(ResourceRequest request, ResourceResponse response) throws Exception {
        final String escritores = EscritorLocalServiceUtil.getEscritors(0, Integer.MAX_VALUE).toString();

        final InputStream stream = new ByteArrayInputStream(escritores.getBytes(StandardCharsets.UTF_8));

        PortletResponseUtil.sendFile(request, response, "escritores.txt", stream, ContentTypes.APPLICATION_TEXT);
    }

}

El método recoge los escritores de base de datos, hace un simple

toString
de ellos (nada más sofisticado para no meter ruido en lo que de verdad nos interesa en este momento) y crea un
InputStream
a partir de él. Con el método de utilidad
PortletResponseUtil.sendFile
somos capaces de hacer que el navegador nos permita descargar un archivo de texto con esta información. Por cierto, vemos que las propiedades de este comando —en la anotación
@Component
— son las mismas que debíamos definir en los otros dos comandos.

Al archivo view.jsp le añadimos el siguiente código:

<portlet:resourceURL id="downloadEscritores" var="downloadEscritoresUrl"/>

<aui:button name="downloadEscritoresButton" type="submit" value="Descargar lista de escritores" onClick="<%=downloadEscritoresUrl%>"/>

Lo que hemos hecho ha sido definir una

resourceURL
cuyo identificador es el nombre del comando que hemos creado y un botón que nos permita ejecutar la petición.

Si desplegamos, podemos ver en nuestro portlet el nuevo botón de descarga y comprobar que todas las operaciones CRUD que realizábamos antes de crear los comandos siguen funcionando.


7. Trabajar con relaciones M-N

Ya hemos visto cómo hacer para comunicar la vista y el controlador, es decir, cómo se hablan los JSP con el MVCPortlet y sus comandos. También hemos visto cómo emplear la capa de servicio generada por Service Builder y añadir métodos personalizados. Sin embargo, todo esto lo hemos hecho únicamente con la entidad Escritor, ignorando completamente a Libro. Esto ha sido así porque estábamos centrados en aprender los conceptos mencionados, y meter más código hubiese sido añadir complejidad que no tenía que ver con la lección. Ahora, ya aprendido el tema, vamos a ver cómo guardar libros y escritores y que estos estén relacionados.

La funcionalidad a implementar que tendrá nuestro portlet será:

  • [C] Crear libros teniendo que elegir los escritores de cada uno.
  • [R] Listar libros junto a sus escritores.
  • [U] Editar los escritores de un libro.
  • [D] Eliminar libros y la relación con sus escritores y eliminar escritores y la relación con sus libros.

Los escritores con los que estábamos operando se persisten en la tabla LIBRO_Escritor. Además de esta tabla, recordemos que teníamos otras dos: LIBRO_Libro y LIBRO_Libros_Escritores. En la primera se guardan los libros y en la segunda los identificadores de los libros y de los escritores para saber quiénes son los autores de cada libro y qué libros ha escrito cada uno.

Para manejar la persistencia de escritores, implementábamos nuestros métodos personalizados en EscritorLocalServiceImpl y los usábamos, junto a los ya existentes, a través de la clase EscritorLocalServiceUtil desde MyMvcPortlet y sus comandos. Haremos lo propio para los libros con las clases LibroLocalServiceImpl y LibroLocalServiceUtil. ¿Y qué pasa con la relación? No existe un LibrosEscritoresLocalServiceUtil, sino que, al definir la relación M-N en el service.xml y generar el código con Service Builder, se generaron métodos en EscritorLocalServiceUtil y en LibroLocalServiceUtil que nos permiten operar con los identificadores de los libros y de los escritores. En concreto, emplearemos el método

public static void setLibroEscritors(long libroId, long[] escritorIds)
de la clase EscritorLocalServiceUtil:
package tutoriales.liferay.crud.libro.service.impl;

import aQute.bnd.annotation.ProviderType;
import com.liferay.portal.kernel.exception.PortalException;
import tutoriales.liferay.crud.libro.model.Escritor;
import tutoriales.liferay.crud.libro.model.Libro;
import tutoriales.liferay.crud.libro.model.impl.LibroImpl;
import tutoriales.liferay.crud.libro.service.EscritorLocalServiceUtil;
import tutoriales.liferay.crud.libro.service.base.LibroLocalServiceBaseImpl;

import java.time.LocalDate;
import java.time.ZoneId;
import java.util.Collection;
import java.util.Date;

@ProviderType
public class LibroLocalServiceImpl extends LibroLocalServiceBaseImpl {

    public void addLibro(long groupId, long companyId, long userId, String userName, String titulo, LocalDate publicacion, String genero, Collection<Escritor> escritores) {
        final Libro libro = new LibroImpl();

        libro.setLibroId(counterLocalService.increment());
        libro.setGroupId(groupId);
        libro.setCompanyId(companyId);
        libro.setUserId(userId);
        libro.setUserName(userName);

        libro.setTitulo(titulo);
        libro.setPublicacion(localDateToDate(publicacion));
        libro.setGenero(genero);

        addLibro(libro);

        EscritorLocalServiceUtil.setLibroEscritors(libro.getLibroId(), getEscritorIds(escritores));
    }

    private Date localDateToDate(LocalDate localDate) {
        return Date.from(localDate.atStartOfDay(ZoneId.systemDefault()).toInstant());
    }

    public void updateLibro(long id, Collection<Escritor> escritores) throws PortalException {
        EscritorLocalServiceUtil.setLibroEscritors(id, getEscritorIds(escritores));
    }

    private long[] getEscritorIds(Collection<Escritor> escritores) {
        return escritores.stream().mapToLong(Escritor::getEscritorId).toArray();
    }

}

En el método

addLibro
, primero creamos y añadimos el nuevo libro. Después empleamos el método de EscritorLocalServiceUtil antes mencionado para pasarle el identificador del libro y de los escritores que lo han compuesto; con esto, se añadirán nuevas filas a nuestra tabla LIBRO_Libros_Escritores.

Si queremos modificar quiénes son los escritores de un libro, volveremos a utilizar el mismo método, que se encargará de dejar en LIBRO_Libros_Escritores, para el libro dado, únicamente los identificadores de los escritores que pasamos al método.

Para listar los libros y los escritores de cada uno, utilizamos el siguiente código (que usaríamos en el método

render
de nuestra clase MyMvcPortlet):
  • Para recoger todos los libros:
    final List<Libro> libros = LibroLocalServiceUtil.getLibros(0, Integer.MAX_VALUE);
  • Para obtener los escritores de un libro dado:
    final List<Escritor> escritores = EscritorLocalServiceUtil.getLibroEscritors(libro.getLibroId());

Ahora nos queda eliminar filas de LIBRO_Libros_Escritores, y esto no puede ser más fácil: no hay que hacer nada. Basta con que borremos un escritor o un libro para que se borren las filas de la relación que hacen referencia a él, es decir, emplearíamos el método

EscritorLocalServiceUtil.deleteEscritor(long escritorId)
o
LibroLocalServiceUtil.deleteLibro(long libroId)
y fin.

El código JSP y los comandos del controlador los omitimos para no hacer el tutorial más largo innecesariamente, pues no aportan nada nuevo a lo ya visto.


8. Conclusiones

En este tutorial hemos visto cómo la capa de servicio generada por Service Builder nos facilita la vida: da acceso a nuestro portlet MVCPortlet para realizar operaciones de persistencia como las CRUD y nos permite generar nuestros propios métodos, todo ello de manera simple cuando sabemos qué clases emplear.

Por otra parte, nos damos cuenta del interés de Liferay por modularizar su framework, llevándolo al desarrollo de portlets a través del cumplimiento del principio de responsabilidad única con los comandos MVC, que nos permiten aligerar nuestro MVCPortlet y definir en cada uno de ellos una acción del usuario.

Por último, hemos sido capaces de emplear la ya veterana tecnología JSP para crear nuestra vista y realizar la comunicación entre ella y los comandos MVC.


9. Referencias

Aframe: bienvenido a WebVR

$
0
0

Tutorial sobre aframe, nueva tecnología para uso de VR en web.

Índice de contenidos

1. Introducción

Aframe es un framework en javascript para generar web en un entorno virtual.

Este framework usa la arquitectura ECS (Entity Component System), usada en el desarrollo de juegos donde cada objeto es una entidad.

La ventaja de este framework es que los objetos ya no están fijos en una jerarquía, por lo que ahora las posibilidades son inmensas, los objetos pueden tener un comportamiento sin límites.

En ECS tenemos:

  • Entidades: Son objetos contenedores donde los componentes son ligados para otorgarles propiedades.
  • Componentes: Son las propiedades que hacen que una entidad sea diferente a otra. Los componentes donan a la entidades de comportamiento, apariencia y funcionalidad.
  • Sistemas: provienen el entorno donde manejar y desarrollar los componentes. Podemos usar los sistemas para separar la funcionalidad de la información, donde los componentes son los contenedores de la información y el sistema se ocupa de la lógica del uso de estos.

2. Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro 17′ (3 Ghz Intel Core 2 Duo, 8GB DDR3).
  • Sistema Operativo: Mac OS El Capitán 10.11
  • Entorno de desarrollo: Atom IDE 1.16.0 x64
  • npm 3.3.6
  • node 5.0.0

3. ECS in AFrame

Las entidades son representadas con <a-entity>, los componentes son representados mediante las propiedades html en <a-entity>. Estos tienen schema, manejadores del ciclo de vida y métodos. Las entidades se declaran en una escena (<a-scene>).

A-Frame permite llevar a ECS a otro nivel, A-frame es declarativo, tienen HTML y está basado en el DOM, lo que permite solucionar muchas de las debilidades de ECS. A continuación se muestran las capacidades que el DOM proporciona para ECS:

  • Referencia a otras entidades con selectores de consulta: El DOM proporciona un potente sistema selector de consultas que nos permite seleccionar una entidad o entidades que coincidan con una condición. Podemos obtener referencias a entidades por ID, clases o atributos de datos. Debido a que A-Frame está basado en HTML, podemos usar selectores de consulta fuera de la caja. Document.querySelector (‘# player’).
  • Comunicación cruzada entre entidades con desacoplamiento de eventos: El DOM proporciona la capacidad de escuchar y emitir eventos. Esto proporciona un sistema de comunicación entre entidades. Los componentes no tienen conocimiento de otros componentes, estos solo emiten eventos y otros componentes pueden escuchar esos eventos.
  • APIs para la gestión del ciclo de vida con las API de DOM: El DOM proporciona API para actualizar los elementos HTML y el árbol. Tales como .setAttribute, .removeAttribute, .createElement y .removeChild se pueden utilizar tal y como lo usamos en el desarrollo web normal.
  • Filtro de entidad con selectores de atributos: El DOM proporciona selectores de atributos que nos permiten consultar una entidad o entidades que tienen o no ciertos atributos HTML. Esto significa que podemos solicitar entidades que tengan o no un determinado conjunto de componentes. Document.querySelector (‘[enemigo]: no ([vivo]’).
  • Declarativo: Por último, el DOM proporciona HTML. A-Frame es el puente entre ECS y HTML haciendo un patrón ya limpio declarativo, legible y extensible.

3.1 Entidades

Como ya hemos dicho podemos crear entidades y añadirles propiedades para diseñar el objeto que deseemos.

La sintaxis es la siguiente:

<a-entity ${componentName}="${propertyName1}: ${propertyValue1}; ${propertyName2:}: ${propertyValue2}">

Mejor lo vemos con un ejemplo, se verá más claro:

<a-entity geometry="primitive: sphere; radius: 1.5"
         light="type: point; color: white; intensity: 2"
         material="color: white; shader: flat; src: glow.jpg"
         position="0 0 -5"></a-entity>

Aquí tenemos un objeto primitivo esfera, con un radio de 1.5, el cual tiene un punto de luz blanco con intensidad 2, sin sombras. La esfera tendrá una textura que se carga mediante la propiedad src de material.

3.2. Componentes

Los componentes tienen la capacidad de modificar las entidades a través de sus propiedades. Los componentes pueden tener propiedades simples o compuestas.

  • Simples
<a-entity position="0 0 5"></a-entity>
  • Compuestas
<a-entity light="type: point; color: white; intensity: 2"></a-entity>

3.2.1. Registrar un componente

El schema es donde definimos las propiedades, en el siguiente ejemplo, el componente car tiene dos propiedades, “power” que es numérico y “colour” que es un string.

Cuando se utilice el componente en el DOM, podremos usar esas propiedades

<a-entity coche="power: 230; colour: blue"></a-entity>

4. Proyecto de ejemplo

Para comenzar crearemos un fichero index.html donde incluiremos la siguiente librería.

<script src=”https://aframe.io/releases/0.5.0/aframe.min.js”</script>

4.1. Crear index.html

<!DOCTYPE html>
<html>
  <head>
    <title>Hello, WebVR! - A-Frame</title>
    <meta name="description" content="Hello, WebVR! - A-Frame">
    <script src="https://aframe.io/releases/0.5.0/aframe.min.js">
    </script>
  </head>
  <body>
    <a-scene>
      <a-entity light="color: #ccccff; intensity: 1;
        type: ambient;"></a-entity>
      <a-entity geometry="primitive: plane; width: 10000;
        height: 10000;" rotation="-90 0 0"
        material="src: #grid; repeat: 10000 10000;
        transparent: true; metalness:0.6;
        roughness: 0.4; sphericalEnvMap: #sky;">
      </a-entity>

      <a-box position="-1 0.5 -3" rotation="0 45 0"
        color="#4CC3D9"></a-box>
      <a-sphere position="0 1.25 -5" radius="1.25"
        color="#EF2D5E"></a-sphere>
      <a-cylinder position="1 0.75 -3" radius="0.5"
        height="1.5" color="#FFC65D"></a-plane>
      <a-sky color="#ECECEC">
    </a-scene>
  </body>
</html>

Aquí tenemos una escena con varias entidades. Si os fijáis, se han declarado entidades con etiquetas diferentes a entity. Esto está permitido para las entidades primitivas. Las siguientes declaraciones serían idénticas

<a-entity geometry="primitive: box; position: 1 0 3"></a-entity>
<a-box position="1 0 3"></a-box>

4.2. Ejecución

Para poder levantar la aplicación debemos tener instalado node y npm.

Una vez hecho esto, creamos en la carpeta del proyecto un fichero package.json con el siguiente contenido:

{
"name": "aframeVR",
"description": "Ejemplo aframeVR",
"version": "1.0.0",
"license": "MIT",
"scripts": {
"start": "live-server --port=7000",
"deploy": "ghpages"
},
"devDependencies": {
"ghpages": "0.0.3",
"budo": "^7.0.0",
"ghpages": "0.0.3",
"nodemon": "^1.11.0",
"live-server": "^1.2.0"
}
}

Seguidamente, ejecutamos:

$> npm install

Esto nos instalara todas las dependencias declaradas en el fichero package.json

Una vez termine de descargar e instalar las dependencias, solo nos queda ejecutar para poder levantar la aplicación, para ello ejecutamos el comando:

$> npm start

La aplicación se ejecutara en el puerto 7000

5. Conclusiones

Creo que mozilla ha hecho un gran trabajo con este framework, aún está verde, pero preveo que llegará lejos, por su facilidad de uso y su baja curva de aprendizaje. Lo que está claro, que el futuro web será VR.

No os perdáis los siguientes tutoriales de este fantástico framework.

Saludos

6. Referencias

Manejo de Excepciones en SpringMVC (II)

$
0
0
En este tutorial vamos a ampliar la información que detallábamos en Manejo de excepciones en SpringMVC con el uso de @ControllerAdvice y @RestControllerAdvice.

Índice de contenidos

1. Introducción

Las anotaciones @ControllerAdvice y @RestControllerAdvice permiten utilizar las mismas técnicas de manejo de excepciones que veíamos en Manejo de excepciones en SpringMVC pero a nivel de toda la aplicación.

La anotación @ControllerAdvice aparece en la versión 3.2 de Spring y consiste en una especialización de la anotación @Component que permite declarar métodos relacionados con el manejo de excepciones que serán compartidos entre múltiples controladores, evitando así la duplicidad de código o la generación de jerarquías para que los controladores traten de manera homogénea las excepciones.

Por otro lado, la anotación @RestControllerAdvice aparece por primera vez en la versión 4.3 de Spring y se trata de una anotación que aúna @ControllerAdvice y @ResponseBody. Su funcionamiento es prácticamente idéntico a @ControllerAdvice, aunque su uso está enfocado a APIs REST, con el agregado de permitirnos establecer un contenido para el cuerpo de las respuestas los casos de error contemplados.

2. Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: MacBook Pro 17′ (2.66 GHz Intel Core i7, 8GB DDR3 SDRAM).
  • Sistema Operativo: Mac OS Sierra 10.12.5
  • Entorno de desarrollo: IntelliJ 2017.1
  • Apache Maven 3.3.9

3. Uso de @ControllerAdvice y @RestControllerAdvice

3.1. @ControllerAdvice

En nuestros proyectos SpringMVC, tan solo será necesario declarar la clase encargada de gestionar las incidencias y anotarla con una de las dos anotaciones, dependiendo del caso, @CotrollerAdvice o @RetControllerAdvice.

Si no se establece lo contrario, la lógica definida para el manejo de una excepción mediante esta herramienta se aplicará de manera global.

Como es de esperar, no siempre querremos aplicar esta solución a la totalidad de los controladores sino a un subconjunto concreto de los mismos. Esta situación tiene fácil solución puesto que podemos acotar su ámbito de aplicación de diferentes maneras:

  • Seleccionando la paquetería base sobre la que queremos que aplique:

    @ControllerAdvice(basePackages={"com.autentia.biblioteca.ui.libros","com.autentia.biblioteca.ui.usuarios"})
    public class GlobalExceptionHandler {
        ...
    }
  • Seleccionando el conjunto de clases que extiendan una clase o implementen una interfaz:

    @ControllerAdvice(assignableTypes = {ThisInterface.class, ThatInterface.class})
  • Seleccionando el conjunto de clases anotadas de una manera específica:

    @ControllerAdvice(annotations= MyAnnotation.class)

Esta anotación da soporte, a su vez, a 3 anotaciones distintas:

  • @ExceptionHandler: Los métodos anotados de esta manera se encargarán de manejar las excepciones que se hayan detallado en la propia anotación.
  • @ModelAttribute: Esta anotación permite completar la información de un modelo expuesto vía web view.
  • @InitBinder: Permite inicializar el WebDataBinder que se utilizará para inicializar los formularios asociados al controlador.

3.2. @RestControllerAdvice

La forma de trabajar es similar a la utilizada con la @ControllerAdvice:

  • Anotamos nuestra clase de manejo general con @RestControllerAdvice.
  • Definimos el método con la lógica de control adecuada y lo anotamos con @ExceptionHandler estableciendo la excepción de la que nos vamos a encargar.
  • Establecemos el código de estado en la respuesta mediante @ResponseStatus.

El método encargado de manejar la excepción debe devolver el objeto con la información relativa a la excepción.

4. Ejemplo de aplicación

Para ilustrar el uso de estas anotaciones vamos a proceder con la creación de un proyecto web con SpringBoot, SpringMVC y Thymeleaf.

Nos ayudaremos de IntelliJ como IDE para realizar el arranque de proyecto, en concreto utilizando el asistente SpringInitialzr.

La imagen anterior muestra la estructura de carpetas resultante tras hacer uso del asistente.

A continuación, como en otras ocasiones, estableceremos las dependencias de nuestro proyecto.

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>com.autentia.exceptionhandling</groupId>
	<artifactId>demo</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>jar</packaging>

	<name>demo</name>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>1.5.3.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
		<java.version>1.8</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-thymeleaf</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<optional>true</optional>
		</dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-test</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.11</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

El asistente habrá creado una clase Application por defecto que se ajusta a nuestras necesidades.

DemoApplication.java
package com.autentia.exceptionhandling;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DemoApplication {

	public static void main(String[] args) {
		SpringApplication.run(DemoApplication.class, args);
	}

}

Y a continuación detallamos el controlador encargado de manejar las peticiones y la plantilla html para dar forma a nuestra página.

DemoController.java
package com.autentia.exceptionhandling;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

@Controller
public class DemoController {
    @RequestMapping("/greeting")
    public String greeting(@RequestParam(value = "name", required = false, defaultValue = "World")String name, Model model){
        model.addAttribute("name", name);
        return "greeting";
    }
}
greeting.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title>Getting Started: Serving Web Content</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <p th:text="'Hello, ' + ${name} + '!'" />
    </body>
</html>

Con esto ya tenemos la aplicación base sobre la que vamos a establecer nuestro @ControllerAdvice.

Si procedemos con el arranque de la aplicación y consultamos con nuestro navegador la URL adecuada, en nuestro caso http://localhost:8081/greeting podremos ver el sistema funcionando correctamente.

A continuación pasamos a configurar nuestro ControllerAdvice delcarando la clase y su método encargado de manejarlo.

DemoExceptionHandler.javas
package com.autentia.exceptionhandling;

import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

@ControllerAdvice
public class DemoExceptionHandler {

    @ExceptionHandler(Exception.class)
    public String exceptionHandler(){
        return "error";
    }
}

Hemos establecido la clase anterior como controladora de excepciones a nivel global mediante @ControllerAdvice, y hemos especificado a través de la anotación @ExceptionHandler(Exception.class) que el método exceptionHandler se encargue de manejar las excepciones de tipo Exception de manera que se muestre la template error.

A continuación definimos la template error.html.

error.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title>ERROR</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <p>You shouldn't be here </p>
    </body>
</html>

Por último crearemos un endpoint que tan solo lance la excepción para poder ver que el ControllerAdvice funciona correctamente:

ExceptionGeneratorController.java
package com.autentia.exceptionhandling;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class ExceptionGeneratorController {
    @RequestMapping("/anotherService")
    public String generator() throws Exception {
        throw new Exception("excepcion");
    }
}

5. Conclusiones

Como hemos visto, mediante las anotaciones @ControllerAdvice y @RestControllerAdvice podemos declarar, de una manera rápida y elegante, el comportamiento que debe tener nuestra aplicación cuando se producen excepciones.

6. Referencias

Aframe: eventos

$
0
0
Manejo de eventos en aframe.

Índice de contenidos

1. Introducción

Hola a todos, seguimos con la saga Aframe, en este capítulo veremos los eventos. Primero veremos como declararlos dentro de un componente, programaremos acciones a realizar cuando se cumplan ciertos eventos y finalmente los asociaremos a una entidad.

2. Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: MacBook Pro 2,5 GHz Intel Core i7. 16gb DDR3
  • Sistema Operativo: Mac OS Sierra 10.12
  • Entorno de desarrollo: Visual studio Code
  • node 8.0.0
  • npm 5.0.3

3. Eventos

Al igual que en la web en 2D, tenemos eventos. Los componentes usan los eventos para comunicarse, pueden lanzar eventos o estar a la escucha.

// `collide` evento emitido por un componente de una entidad al colisionar con otra.
document.querySelector('a-entity').addEventListener('collide', function (evt) {
 console.log('This A-Frame entity collided with another entity!');
});

4. Proyecto de Ejemplo

Generamos un nuevo proyecto. Con un index.html, añadimos la librería de aframe y dibujamos una caja.

index.html
<!DOCTYPE html>
<html>
 <head>
   <meta charset="utf-8">
   <title>Cursor Handler - A-Frame School</title>
   <meta name="description" content="Cursor Handler - A-Frame School">
   <script src="https://aframe.io/releases/0.5.0/aframe.min.js"></script>
   <script src="handle-events.js"></script>
   </script>
 </head>
 <body>
   <a-scene>
     <a-box color="#EF2D5E" position="0 1 -4" handle-events ></a-box>

     <a-camera><a-cursor></a-cursor></a-camera>
     <a-sky color="#333"></a-sky>
     <a-plane color="#000" rotation="-90 0 0" width="500" height="500"></a-plane>
   </a-scene>
 </body>

4.1. Componente con eventos

Ahora crearemos un componente donde añadiremos eventos. Este componente lo podremos añadir a entidades para que usen sus eventos. Generamos un fichero llamado handleEvents.js. Y añadimos el siguiente código:

AFRAME.registerComponent('handle-events', {
       init: function () {
         var el = this.el;  //
         el.addEventListener('mouseenter', function () {
           el.setAttribute('color', 'red');
           console.log("mouseenter");
         });
         el.addEventListener('mouseleave', function () {
           el.setAttribute('color', 'grey');
             console.log("mouseleave");
         });
         el.addEventListener('click', function () {
           el.setAttribute('scale', {x: numAleatorio(1,5), y: numAleatorio(1,3), z: numAleatorio(1,3)});
             console.log("click");
         });

         function numAleatorio(min, max) {
           return Math.round(Math.random() * (max - min) + min);
           }
       }
     });

Aquí podemos ver como la entidad que use este componente estará a la escucha de tres eventos: mouseenter, mouseleave y click. Para cada evento hemos programado un cambio en una de las propiedades de la entidad.

Cuando nos situemos encima de la entidad el atributo color se actualizará a rojo y la entidad reflejará este cambio. Al dejar la entidad su color pasará a coger el valor ‘gris’. Y si hacemos click sobre ella aumentará su tamaño aleatoriamente.

4.2. Asociar eventos a entidades

Para asociar este componente de eventos a una entidad solo tendremos que declararlo en la etiqueta de la entidad.

Añadimos el fichero handleEvents.js al fichero index.html y modificamos la entidad a-box para añadirle los eventos.

5. Conclusiones

Después de este tutorial, espero que vayáis viendo las posibilidades que se nos van presentando. En el siguiente tutorial veremos las animaciones.

No me faltéis.

Saludos.

6. Referencias

Kotlin, primeros pasos

$
0
0

En este tutorial vamos a dar nuestros primeros pasos con el lenguaje de programación Kotlin, para ello veremos qué necesitamos para poder empezar a hacer nuestros primeros programas mediante ejemplos sencillos.

1. Introducción

Kotlin es un lenguaje de programación fuertemente tipado que se ejecuta principalmente sobre la JVM (Java Virtual Machine), y digo “principalmente” porque también es posible compilarlo a JavaScript o incluso a código nativo, pero por ahora nos vamos a centrar en la JVM y sus capacidades de interoperabilidad con librerías o cualquier código escrito en Java.

Es un lenguaje creado por JetBrains (sí, sí, los creadores de los conocidos IDE de desarrollo IntelliJ, PyCharm, WebStorm, AppCode…​) hace algún tiempo, pero parece que ha saltado de nuevo a la palestra con el anuncio por parte de Google de considerarlo lenguaje de primer nivel para el desarrollo de Android y, por tanto, darle soporte directo.

Esto es muy buena noticia y va a ayudar a garantizar la vida y evolución del lenguaje, por lo que puede ser un buen momento para echarle a un vistazo al lenguaje, ya que este de verdad tiene cosas muy interesantes, especialmente para los que venimos de Java o ya usamos la JVM.

  • Es un lenguaje fuertemente tipado. Llamadme viejuno y para gustos los colores, pero para mí esto es fundamental en un lenguaje. El hecho de que el propio compilador nos detecte errores de escritura en un tiempo anterior al de ejecución me parece maravilloso, esto sin contar con que las herramientas disponibles son mucho más potentes que en un lenguaje no tipado.

  • Interoperabilidad con Java y la JVM en general. Una cosa que han hecho muy bien es cómo puedes combinar Kotlin con tus programas o librerías ya escritos en Java. Desde tu nuevo código escrito en Kotlin puedes llamar directamente a cualquier clase de Java, y al revés también, desde Java puedes llamar a código escrito en Kotlin. Esto nos permite reutilizar el gran ecosistema al que ya estamos acostumbrados en Java.

  • ¡Compila directamente a bytecode! No penséis cosas raras de que transforma el código a Java y luego lo compila con javac. No, no, no, tiene compilador propio y genera directamente bytecode de la JVM, sin pasos intermedios.

A nivel de lenguaje nos vamos a encontrar con todo lo que tiene Java y muchas características más que van a facilitarnos mucho la vida a la hora de hacer nuestras aplicaciones. No voy a contarlas todas, ya que la idea de este tutorial no es enseñar Kotlin, y para ello os recomiendo la propia guía de referencia de Kotlin, muy completa, clara y con ejemplos. Pero podemos destacar:

  • Funciones de extensión (mixins). Esto no existe en Java y para hacer algo parecido hay que montar un arco de iglesia. Muy interesante para no abusar de la herencia, que es una relación que acopla mucho a los participantes.

  • Las funciones son elementos de primer nivel. Sí, tendremos lambdas por todos lados, y sin llegar a ser un lenguaje funcional completo como Clojure o Scala, vamos a poder disfrutar de muchas cositas.

  • Todo es una expresión, esto quiere decir que podemos hacer cosas como poner un if a la derecha de un = (asignación).

  • Y muchas otras cosas como:

    • Control de nulos en tiempo de compilación (The Billion Dollar Mistake).

    • Inferencia de tipos.

    • Sobrecarga de operadores (+, -, [], ==, etc.).

    • Rangos.

    • Patrones implementados directamente en el lenguaje como el Builder o el Delegate.

    • Azúcar sintáctico que nos va a permitir la construcción de DSLs.

    • Coroutines, todavía en experimental pero una buena ayuda para la programación concurrente. Según los autores: “Coroutines provide a way to avoid blocking a thread and replace it with a cheaper and more controllable operation: suspension of a coroutine.”

    • …​

Sólo con esta lista parcial espero haber picado tu curiosidad para, por lo menos, echar un ojo a la guía de referencia de Kotlin.

Además, si queréis hacer pruebas o ver ejemplos, lo podéis hacer directamente desde el navegador en https://try.kotlinlang.org.

2. Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro 15” (2.5 GHz Intel i7, 16GB 1600 Mhz DDR3, 500GB Flash Storage).

  • AMD Radeon R9 M370X

  • Sistema Operativo: macOS Sierra 10.12.5

  • JVM 1.8.0_121 (Oracle Corporation 25.121-b13)

  • Kotlin 1.1.3

  • Gradle 4.0

3. Instalación de Kotlin

Lo mejor de todo es que Kotlin no necesita instalación.

Por supuesto Kotlin tiene un compilador de línea de comandos, pero no va a ser necesaria su instalación ya que normalmente vamos a trabajar con Maven o con Gradle, en concreto en este tutorial usaremos Gradle. En cualquier caso, si tenéis curiosidad podéis encontrar más información en https://kotlinlang.org/docs/tutorials/command-line.html

Así, para preparar la estructura de nuestro primer proyecto, ejecutaremos:

$ mkdir kotlin-tutorial ; cd kotlin-tutorial
$ gradle init
$ mkdir -p src/main/kotlin
$ mkdir -p src/test/kotlin

Para que Gradle sea capaz de compilar Kotlin sobreescribimos todo el contenido del fichero build.gradle (veréis que el contenido está totalmente comentado y es sólo un ejemplo que podéis borrar con tranquilidad) con:

plugins {
    id 'org.jetbrains.kotlin.jvm' version '1.1.3'
}

tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
    sourceCompatibility = JavaVersion.VERSION_1_8
    targetCompatibility = JavaVersion.VERSION_1_8

    kotlinOptions {
        jvmTarget = '1.8'
    }
}

repositories {
    mavenCentral()
}

dependencies {
    compile 'org.jetbrains.kotlin:kotlin-stdlib-jre8'
}

Aquí destacamos:

  • línea 2 – declaramos el plugin de Kotlin. Esta es la nueva forma de declarar los plugins en Gradle y Kotlin ya la soporta.

  • líneas 5 a 12 – estamos indicando la configuración para todas las tareas de tipo compilación de Kotlin. Esto es conveniente hacerlo así ya que realmente puede haber varias tareas de este tipo, por ejemplo para compilar el código, para compilar el código de los tests, para compilar el código de producción, el de desarrollo…​

  • línea 10 – vemos cómo le estamos indicando al compilador de Kotlin que vamos a usar una JVM 1.8. Esto sirve para que el compilador haga ciertas optimizaciones y le saque más partido a la JVM. A día de hoy, si no especificamos nada, el valor por defecto será 1.6.

Ahora vamos a crear nuestro primer código Kotlin y, como no podía ser de otra forma, empezaremos por nuestro querido “Hola, mundo”. Para ello creamos el fichero src/main/kotlin/App.kt con el siguiente contenido:

fun main(args: Array<String>) {
    println("Hello, world!")
}

Se ve cómo es simplemente el punto de entrada de la aplicación, donde nos entra un array con los argumentos de la línea de comandos y simplemente escribimos un mensaje por consola. Cabe destacar cómo hemos escrito la función sin necesidad de que esta esté dentro de una clase, y no como en Java, donde sí tenemos esa restricción.

Para ver que todo funciona correctamente, ejecutamos en la línea de comandos:

$ gradle build

Donde deberíamos ver algo similar a:

Gradle Kotlin build success

4. Añadiendo soporte para tests

Sólo con el punto anterior ya podríamos empezar a hacer aplicaciones con Kotlin, pero ya sabéis que soy muy fan de los tests y que no entiendo el desarrollo sin ellos, así que vamos a modificar un poco el fichero build.gradle para añadir soporte para JUnit 5.

Añadir soporte para JUnit 4 hubiera sido cuestión de añadir una dependencia, mientras que el soporte para JUnit 5 nos va a costar un poquito más (no es culpa de Kotlin, sino del propio soporte de JUnit 5), pero quiero hacer este ejemplo para que veáis cómo podemos usar Kotlin con las últimas versiones de nuestras librerías Java sin ningún problema.

Primero vamos a añadir al principio del fichero las siguientes líneas para dar de alta el plugin de Gradle de JUnit 5 (como vemos, todavía no soporta el nuevo DSL de Gradle para la definición de plugins):

buildscript {
    ext {
        junitPlatformVersion = '1.0.0-M4'
        junitJupiterVersion = '5.0.0-M4'
        junitVintageVersion = '4.12.0-M4'
    }

    repositories {
        mavenCentral()
    }
    dependencies {
        classpath "org.junit.platform:junit-platform-gradle-plugin:${junitPlatformVersion}"
    }
}

Ahora aplicamos el plugin, para ello justo después de la sección de plugins { …​ } que tenemos de antes, añadimos la línea:

apply plugin: 'org.junit.platform.gradle.plugin'

Y por último, dentro de la sección de dependencias, añadimos las necesarias para poder escribir los tests:

dependencies {
    ...
    testCompile 'org.jetbrains.kotlin:kotlin-test'
    testCompile 'org.jetbrains.kotlin:kotlin-test-junit'

    testCompile "org.junit.jupiter:junit-jupiter-api:${junitJupiterVersion}"
    testRuntime "org.junit.jupiter:junit-jupiter-engine:${junitJupiterVersion}"
    testRuntime "org.junit.vintage:junit-vintage-engine:${junitVintageVersion}"
}

Aquí podemos destacar:

  • línea 3 – Añadimos una librería de Kotlin que nos proporciona funciones de aserción (assertEquals…​) más al estilo de Kotlin. Esto no es estrictamente necesario y podemos usar directamente las de JUnit, pero nos va a facilitar la escritura de los tests.

  • línea 4 – Es otra librería de Kotlin que da soporte a la librería anterior para integrarla con el ciclo de JUnit, de forma que todo el reporte de errores, etc., sea a través de JUnit.

  • líneas de la 6 a la 8 – Son simplemente las dependencias de JUnit 5 que usaríamos en cualquier caso (sea Kotlin o Java).

Podemos mencionar también cómo en el caso de las dependencias de las líneas 3 y 4 no hemos puesto versión, esto es posible gracias a que el propio plugin de Kotlin las va a proporcionar en función de la versión de Kotlin que estamos usando.

Os pongo el fichero completo para que sea más fácil verlo:

buildscript {
    ext {
        junitPlatformVersion = '1.0.0-M4'
        junitJupiterVersion = '5.0.0-M4'
        junitVintageVersion = '4.12.0-M4'
    }

    repositories {
        mavenCentral()
    }
    dependencies {
        classpath "org.junit.platform:junit-platform-gradle-plugin:${junitPlatformVersion}"
    }
}

plugins {
    id 'org.jetbrains.kotlin.jvm' version '1.1.3'
}

apply plugin: 'org.junit.platform.gradle.plugin'

tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
    sourceCompatibility = JavaVersion.VERSION_1_8
    targetCompatibility = JavaVersion.VERSION_1_8

    kotlinOptions {
        jvmTarget = '1.8'
    }
}

repositories {
    mavenCentral()
}

dependencies {
    compile 'org.jetbrains.kotlin:kotlin-stdlib-jre8'

    testCompile 'org.jetbrains.kotlin:kotlin-test'
    testCompile 'org.jetbrains.kotlin:kotlin-test-junit'

    testCompile "org.junit.jupiter:junit-jupiter-api:${junitJupiterVersion}"
    testRuntime "org.junit.jupiter:junit-jupiter-engine:${junitJupiterVersion}"
    testRuntime "org.junit.vintage:junit-vintage-engine:${junitVintageVersion}"
}

Ya podemos escribir tests en Kotlin usando JUnit 5. Podemos hacer la prueba creando el fichero src/test/kotlin/AppTest.kt con el contenido:

import org.junit.jupiter.api.Test
import kotlin.test.assertEquals

class AppTest {

    @Test
    fun `addition is correct`() {
        assertEquals(4, 2 + 2)
    }
}

Podemos destacar:

  • línea 1 – estamos usando la anotación de JUnit 5.

  • línea 2 – estamos usando las aserciones de la librería de Kotlin.

  • línea 7 – vemos cómo el acento inverso ` es un carácter válido para el nombre de un método y esto nos permite poner espacios, con lo que conseguimos métodos de tests más legibles (no sería recomendable para los métodos de las clases de producción pero sí muy conveniente para los métodos de test).

Si ahora ejecutamos (el clean no es necesario pero lo ponemos para forzar una compilación completa):

$ gradle clean build

deberíamos ver algo similar a:

Gradle Kotlin build tests success

Si ejecutamos los tests desde el IntelliJ, veremos algo como:

Kotlin IntelliJ tests

Donde podemos apreciar la ventaja de los nombres.

5. Conclusiones

Si solemos trabajar con Java, o en general con la JVM, empezar con Kotlin es muy sencillo, ya que la barrera de entrada, por lo menos para jugar un poco, es prácticamente inexistente.

Además, ya tenemos cantidad de herramientas que lo soportan, incluyendo los tres grandes IDEs a día de hoy: Eclipse, Netbeans y, por supuesto, IntelliJ y Android Studio.

Otra buena referencia es este site donde podréis encontrar cantidad de información y recursos: Kotlin is awesome!

¿Kotlin va a sustituir a Java? Sólo el tiempo lo dirá, pero lo que está claro es que trae cantidad de cosas útiles, que está evolucionando muy rápido, y que ahora también tiene el apoyo de Google. Todo esto sumado a la facilidad para mezclarlo con nuestro código y librerías existentes lo convierte en una opción más que viable y atractiva. Más vale que Java se ande con ojo y se dé un poco de vidilla si no quiere quedarse en la cuenta.

Eso sí, ¡larga vida a la JVM!

6. Sobre el autor

Alejandro Pérez García (@alejandropgarci)
Ingeniero en Informática (especialidad de Ingeniería del Software) y Certified ScrumMaster

Socio fundador de Autentia Real Business Solutions S.L. – “Soporte a Desarrollo”

Socio fundador de ThE Audience Megaphone System, S.L. – TEAMS – “Todo el potencial de tus grupos de influencia a tu alcance”

Incluyendo Tests de SOAP UI dentro ciclo de vida de Maven

$
0
0

En este tutorial vamos a ver cómo podemos ejecutar los tests de SoapUI dentro del ciclo de vida de Maven.

Índice de contenidos

1. Introducción

En este tutorial vamos a incluir tests de SOAP UI punto a punto dentro del ciclo de vida de Maven.

Para escribir este tutorial nos estamos basando en uno hecho por Alberto Moratilla donde hacía tests unitarios atacando a servicios REST.

En este tutorial intentamos aportar un ejemplo similar al de Alberto, atacando a servicios SOAP desarrollados con Spring Boot, además de la incorporación de casos de uso en nuestro proyecto de SOAP UI utilizando el TestCaseEditor que nos posibilita validar los diferentes casos de uso de nuestro desarrollo.

La motivación de este tutorial viene dada porque nosotros trabajamos con metodologías Agiles (Scrum). En Scrum intentas entregar valor al cliente en pequeños plazos de tiempo. Esos plazos de tiempo se llaman Sprint. En cada final de Sprint se realiza una demo al cliente donde se verifica el correcto funcionamiento del software entregado validando diferentes casos de uso. Para los desarrollos de WS solemos utilizar SOAP UI para demostrarle al cliente que el proyecto se comporta conforme a lo esperado. Pensamos que seria interesante utilizar esos proyectos de SOAP UI no sólo en la demo, e intentar integrarlos dentro del ciclo de vida de Maven para nuestro proyecto.

2. Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: MacBook Pro Retina 15′ (2,5 Ghz Intel Core i7, 16GB DDR3).
  • Sistema Operativo: Mac OS Sierra 10.12.5.
  • Entorno de desarrollo: Eclipse Neon.2 Release (4.6.2).
  • Java 1.8
  • Apache Maven 3.3.9
  • SOAP UI 5.3.0
  • Docker version 17.03.1-ce, build c6d412e
  • Docker Machine version 0.10.0, build 76ed2a6

3. Preparando el proyecto de SOAP UI

Un proyecto de SOAP UI se puede descomponer en “test suite”. En nuestro caso, había un “test suite” por cada demostración o Sprint. Un “test suite” contiene varios “test cases”. Un caso de uso podría ser la cancelación de una reserva de hotel, lo que implica dos operaciones al menos (alta y cancelación de la reserva). Y a su vez, un test case se compone de uno o varios “test steps”. En nuestro ejemplo anterior habría un paso por cada operación.

Si no estas muy familiarizado con SOAP UI, te recomiendo que le eches un vistazo al siguiente tutorial antes.

3.1. El servidor

Como servidor utilizaremos una modificación del WS propuesto por Christoph Burmeister en su blog, incluyendo la operación de añadir ciudades.Nuestro WS tendrá dos operaciones:

  • getCityByCode: Devuelve el nombre de la ciudad, país, año en el que se fundo, código postal y población de un código de postal dado.
  • addCity: Dados el nombre de la ciudad, país, año en el que se fundó, código postal y población. Se habrá insertado correctamente si devuelve el código postal

El código del servidor lo podéis encontrar en mi repositorio de github.

Antes de desplegar el servicio SOAP es necesario levantar la BBDD de la que depende. Para ello, nosotros hemos optado por levantar un contenedor de docker de mysql.Para ello, abrimos un terminal, posicionándonos en /src/main/resources/ del proyecto ejecutamos el siguiente comando (cambiando la ruta a la que se referencia en el volumen):

$> docker run --name mysql -p 3306:3306 -v /Users/ddelcastillo/Downloads/CitiesWS/src/main/resources/data:/docker-entrypoint-initdb.d -e MYSQL_ROOT_PASSWORD=root -d mysql:5.6

Este comando nos precarga las siguientes ciudades en nuestro servicio:

3.2. Preparando los tests de SOAP UI

Nuestro proyecto de SOAP UI tendrá los siguientes casos de uso dentro del TestSuite “Tuto_CitiesWS” :

  • Casos de Uso OK:
    • getCityByCode: Consultar por una ciudad que exista.
    • addCity: Añadir una ciudad que no exista.
  • Caso de Uso KO:
    • getCityByCodeResponse: Consultar por una ciudad que no exista.
    • addCity: Intentar añadir una ciudad que exista.
  • Casos de Uso con TestCase Editor:
    • Inserta y Consultar (TestCase IC): Insertar una ciudad que no existe y consultar por la ciudad recién insertada.
    • Consultar, Insertar y Consultar (TestCase CIC): Consultar por una ciudad que no existe, insertarla y volver a consultar.

4. Creando el proyecto de Maven

Como podéis ver en el proyecto Maven de ejemplo, dentro de src/test/resources tenemos el proyecto de SOAP UI al que haremos referencia en los tests de JUnit.

4.1. Incluyendo dependencias

Las dependencias necesaria para utilizar tests de SOAP UI es la siguiente, tal y como podéis ver en el pom.xml

<dependency>
	<groupId>com.smartbear.soapui</groupId>
	<artifactId>soapui</artifactId>
	<version>5.2.1</version>
</dependency>

4.2. Creando perfil para los tests de SOAP UI

Debido a que los tests pueden penalizar por su tiempo de ejecución y por unos errores en tiempo de ejecución que comentaremos en el apartado 5.3. Hemos decido crear un perfil que únicamente ejecute los tests SOAP UI. Este perfil se podría ejecutar “bajo demanda” o incluir dentro del proceso de “liberación de Release”.

Los cambios necesarios en el pom.xml para excluir estos test a únicamente el uso de este perfil de Maven son los siguientes:

<profiles>
	...
	<profile>
		<id>soapui</id>
		<properties>
			<environment.profile>soapui integration</environment.profile>
			<environment.name>Integration environment for soapui</environment.name>
			<skip.integration.tests>false</skip.integration.tests>
			<skip.unit.tests>true</skip.unit.tests>
		</properties>
		...
		<build>
				<plugin>
					<groupId>org.apache.maven.plugins</groupId>
					<artifactId>maven-failsafe-plugin</artifactId>
					<executions>
						<execution>
							<id>integration-tests</id>
							<goals>
								<goal>integration-test</goal>
								<goal>verify</goal>
							</goals>
						</execution>
					</executions>

					<configuration>
						<argLine>-Dfile.encoding=${project.build.sourceEncoding}
							${failsafe.argLine}</argLine>
						<skipITs>${skip.integration.tests}</skipITs>
						<includes>
							<include>**/*SoapUITest.java</include>
						</includes>
						<excludes>
							<exclude>**/*IT.java</exclude>
							<exclude>**/*IntegrationTest.java</exclude>
						</excludes>
					</configuration>
				</plugin>
			</plugins>
		</build>
	</profile>
	...
</profiles>

5. Creando los tests de JUnit

En este punto podemos diferenciar dos métodos para ejecutar nuestros tests:

  • Ejecutar todos los casos de uso del proyecto de SOAP UI.
  • Ejecutar un caso de uso específico.
Como peculiaridad, en el apartado de caso de uso específico, crearemos un caso de uso que enlace dos pasos (operaciones) utilizando TestCase Editor de SOAP UI.

5.1. Ejecución de todo el proyecto de SOAP UI

Tal y como comenta Alberto en su tutorial, lo malo de ejecutar todos los casos de uso de un proyecto es que no puedes hacer asserts dentro del test de JUnit. El código para ejecutar todos los casos de uso sería el siguiente:

@Test
public void shouldExecuteAllTestCases() throws Exception {
	cityService.deleteCityByCode(1);
	SoapUITestCaseRunner runner = new SoapUITestCaseRunner();

	runner.setProjectFile(TEST_FILE);
	runner.setEndpoint("http://localhost:".concat(port + "").concat("/ws/cities"));
	runner.setPrintReport(true);
	runner.setJUnitReport(true);
	runner.setExportAll(true);
	runner.setOutputFolder("./target/surefire-reports");
	runner.setIgnoreError(true);
	runner.run();
}

Como podéis ver en el test, establecemos una serie de propiedades de configuración y para la generación de informes, de la misma manera que podríamos hacerlo en la opción “Launch TestRunner” de la parte gráfica, tal y como se puede ver en las siguientes imágenes:

5.2. Ejecución de un Test Case específico

La única diferencia entre este test y el anterior es que aquí le especificamos en las propiedades el caso de uso a ejecutar y podemos evaluar el test con asserts, tal y como se puede ver:

@Test
public void shouldExecuteICTestCase() throws Exception{
	long insertCode = 1;

	SoapUITestCaseRunner runner = new SoapUITestCaseRunner();

	cityService.deleteCityByCode(1);
	runner.setProjectFile(TEST_FILE);
	runner.setEndpoint("http://localhost:".concat(port + "").concat("/ws/cities"));
	runner.setPrintReport(true);
	runner.setJUnitReport(true);
	runner.setExportAll(true);
	runner.setOutputFolder("./target/surefire-reports");
	runner.setIgnoreError(true);
	runner.setTestSuite("Tuto_CitiesWS");
	runner.setTestCase("TestCase IC");
	runner.run();
	City city = cityService.getCityByCode(insertCode);
	assertNotNull(city);
	assertEquals(insertCode,city.getCode());
	assertEquals("TomorrowLand",city.getName());
	assertEquals("Chiquitistan",city.getCountry());
	assertEquals(1,city.getFounded());
	assertEquals(1,city.getPopulation());
}

El caso de uso que ejecutamos en este test conlleva la llamada de dos operaciones del WS (de Inserción y Consulta). Insertamos una nueva ciudad y consultamos por ella misma. Para realizar todos estos pasos y relacionar las operaciones en el caso de uso, utilizamos TestCase Editor, tal y como se puede ver en la siguiente imagen:

5.3. Errores al ejecutar los tests

Si habéis ejecutado el perfil de SOAP UI con Maven con el siguiente comando:

$> mvn clean install -P soapui

Os habrán salido un montón de errores aunque los tests se ejecuten satisfactoriamente como se puede ver:

Buscando por internet el error, vimos que nos faltaban unas clases que encontramos en este repositorio. Al empaquetarlas vimos que los errores dejaban de salir pero al ver que se hacia referencia a la version de pago de SOAP UI y viendo que los errores no eran bloqueantes, ya que los tests pueden ejecutarse. Optamos por la opción de crear un perfil solo para estos test. Aun así, tal y como dice Alberto en su tutorial, no se recomienda el uso de estos tests en entornos productivos.

6. Conclusiones

Hemos visto como incorporar tests de SOAP UI a algo relativamente nuevo (SpringBoot) desarrollando algo relativamente viejo (WS SOAP). Se que son pocas diferencias con respecto al Tutorial de Moratilla pero como diría Newton:

Si he visto un poco más lejos es porque me he elevado a hombros de gigantes.

Y si necesitáis una justificación un poco mas contemporánea:

No he copiado el libro, seguro que ha sido un virus o algún error informático (Ana Rosa).

Espero haberos aportado algo más ;).

7. Referencias

Generación dinámica de tests con JUnit 5

$
0
0

En esta entrada vamos a conocer una de las funcionalidades que incluye JUnit 5: los tests dinámicos. Contaremos qué son, cómo se utilizan y los compararemos con las opciones que teníamos antes de su aparición para resolver el mismo problema.

Índice de contenidos

1. Introducción

¿Cuántas veces nos ha pasado que tenemos una pieza de código que debería funcionar igual para un conjunto de entradas distintas?, ¿y cuántas veces hemos testeado que el comportamiento es igual para todas ellas? Es cierto que la mayoría de las ocasiones es imposible probar todos los casos porque hay infinitos valores de entrada (por ejemplo si trabajamos con un String), pero siempre podemos identificar los más problemáticos. Sin embargo, es muy pesado crearnos todos los tests y pocas veces lo hacemos como deberíamos.

Por suerte hay diversas técnicas para ayudarnos, si no a testear absolutamente todos los casos, al menos a aumentar el número de pruebas con el menor esfuerzo posible. Yo me centraré en la generación de tests dinámicos que ofrece JUnit 5, pero también veremos algunas alternativas usando JUnit 4.

2. Entorno

He escrito y desarrollado el tutorial usando este entorno:

  • Hardware: Portátil Mac Book Pro 15″ (2,4 Ghz Intel Core i5, 8 GB DDR3)
  • Sistema Operativo: Mac OS Sierra
  • Entorno de desarrollo: Eclipse Java EE IDE, Mars Release (4.5.2)
  • Java 8
  • Librerías: JUnit 5 y Hamcrest 1.3

3. ¿Qué son los tests dinámicos y qué ventajas tienen?

La generación dinámica de tests nos permite crear, en tiempo de ejecución, un número variable de tests. Cada uno de ellos se lanza independientemente, por lo que ni su ejecución ni el resultado dependen del resto. El uso más habitual es tener un número variable de repeticiones de un mismo test para que se ejecuten de forma individual sobre cada uno de los estados de entrada de un conjunto (por estado de entrada nos referimos al dato o conjunto de datos que definen la entrada de la funcionalidad que vamos a probar).

La ventaja más clara con los test tradicionales es que podemos probar nuestro código frente a una cantidad mayor de casos de entrada sin tener que escribir cada caso explícitamente. Esto es especialmente útil para probar los extremos de un conjunto de valores, listas vacías, strings con caracteres extraños, nulos, etc; que no requieren un tratamiento especial dentro de nuestro código (entonces habría que ir pensando en tener un test propio para ese caso) pero aún así queremos asegurarnos que no provocan un comportamiento inesperado.

Además, aunque de primeras no llame tanto la atención, la independencia entre los tests es una característica igual de interesante. Gracias a ella podemos crear todos los tests que necesitemos sin temor de que al fallar uno se aborte la ejecución de los que faltan.

4. Usando @TestFactory con Junit 5

Para implementar los tests dinámicos, JUnit 5 nos ofrece nuevos elementos. Por un lado tenemos la clase DynamicTest, que no es otra cosa que el objeto que define el test que vamos a ejecutar. Se crea a partir de dos componentes: el nombre que se mostrará en el árbol al ejecutarlo y una instancia de la interfaz Executable con el propio código que se ejecutará.

Executable es, básicamente, una interfaz funcional igual que Runnable pero que puede llegar a lanzar una instancia de Throwable (todas las excepciones de Java, así como los errores y los fallos en las aserciones de JUnit extienden de esta clase). Y precisamente por ser interfaz funcional podemos expresarla también en forma de lambda. Además de para la creación de tests dinámicos también sirve como parámetro de algunas de las nuevas aserciones, pero eso no es objeto de esta entrada.

El último de los nuevos elementos que vamos a ver por ahora es la anotación @TestFactory, que utilizamos con métodos que devuelven un conjunto iterable de DynamicTest (este conjunto es cualquier instancia de Iterator, Iterable o Stream). Al ejecutar la clase, JUnit encontrará la nueva etiqueta, creará todos los tests dinámicos que devuelve y los tratará como si se tratasen de métodos anotados con @Test (aunque con un ciclo de vida ligeramente distinto).

Como vemos a continuación, es muy sencillo crear una lista de tests dinámicos que aparecerán por separado en los resultados:

@TestFactory
Collection listOfDynamicTests() {

    DynamicTest testKO = DynamicTest.dynamicTest("Should fail", new Executable() {
        @Override
        public void execute() throws Throwable {
            assertTrue(false);
        }
    });

    DynamicTest testOK = DynamicTest.dynamicTest("Should pass", new Executable() {
        @Override
        public void execute() throws Throwable {
            assertTrue(true);
        }
    });

    return Arrays.asList(testKO, testOK);
}

Ejecución de tests dinámicos con fallos

Este código solo muestra cómo se crean los tests y deja ver que el fallo de uno no afecta al otro. Ayudándonos de imports estáticos, lambdas y refactorizando un poco podemos llegar a esto:

@TestFactory
Collection minimizedListOfDynamicTests() {
    return Arrays.asList(
            dynamicTest("Should fail", () -> assertTrue(false)),
            dynamicTest("Should pass", () -> assertTrue(true))
    );
}

Sin embargo, la verdad es que así no tiene demasiados casos de uso. Es al trabajar con un conjunto de valores cuando podemos empezar a ver su potencial. Pongámonos en el caso de que estamos intentando probar la función contains que trae la clase String (ya sabemos que debería funcionar, es solo para tener un ejemplo). Uno de los tests que haríamos es comprobar que devuelve true cuando nuestra cadena contiene la cadena objetivo, da igual su posición. En caso de usar tests dinámicos quedaría algo similar a esto:

@TestFactory
Stream containsShouldWorkWhenTargetIsWithinTheStringNoMatterItsPosition() {
    final String myName = "Alex";
    final List stringsWithMyName = Arrays.asList("Alex", "I'm Alex", "Alex Acebes", "I'm Alex, hi!");

    return stringsWithMyName.stream().map(
        stringUnderTest -> dynamicTest("\"" + stringUnderTest + "\" should contain \"" + myName + "\"", new Executable() {
            @Override
            public void execute() throws Throwable {
                assertThat(stringUnderTest.contains(myName), is(true));
            }
        }
    ));
}

Hemos creado un stream a partir de la lista y mapeado cada elemento a un test dinámico que comprueba que contiene la cadena “Alex”. Obviamente, no es la única manera de lograrlo. Recordad que podemos devolver colecciones o iteradores además de un stream.

Existe una segunda manera de implementar tests dinámicos. La clase DynamicTest ofrece un método estático que nos crea un stream de tests dinámicos a partir de tres elementos: un generador de datos de entrada, un generador de nombres y el código de los tests que vamos a crear. Los datos de entrada se definen con una instancia de la interfaz Iterator, que podemos definir nosotros mismos u obtener a partir de una colección. Los nombres se generan usando una función que recibe un elemento del tipo generado (o que extienda de él) y devuelve un String. Por último, el código del test se especifica instanciando la interfaz ThrowingConsumer, otro de los elementos que nos ofrece JUnit 5. Se trata de una interfaz funcional con un método que consume un dato y no devuelve nada, pero que es susceptible de lanzar una instancia de Throwable. Es decir, ThrowingConsumer es a la interfaz Consumer lo que Executable es a Runnable.

Para demostrar cómo creamos nuestro propio Iterator, vamos a comprobar que la suma de la clase BigInteger funciona igual que el operador de suma de los enteros. Generamos 1000 tests dinámicos que reciben dos enteros aleatorios y los suman usando las dos formas que vamos a comparar. (Disclaimer: usar valores aleatorios para testear no es buena idea porque los tests no serán repetibles a no ser que usemos la misma semilla. En estos ejemplos solo lo estamos usando para poner de manifiesto que los datos también pueden ser dinámicos).

@TestFactory
Stream addOfBigIntegerShouldWorkLikePlusOperatorOfIntegers() {

    Iterator inputGenerator = new Iterator() {

        int createdElements = 0;
        final Random random = new Random();

        @Override
        public boolean hasNext() {
            return createdElements < 1000;
        }

        @Override
        public Integer[] next() {
            createdElements++;
            return new Integer[] {random.nextInt(), random.nextInt()};
        }
    };

    Function displayNameGenerator = (numbers) -> "Add " + numbers[0] + " to" + numbers[1];

    ThrowingConsumer testExecutor = new ThrowingConsumer() {

        @Override
        public void accept(Integer[] numbers) throws Throwable {
            final BigInteger bigZero = new BigInteger(numbers[0].toString());
            final BigInteger bigOne = new BigInteger(numbers[1].toString());

            final BigInteger bigAddition = bigZero.add(bigOne);

            assertThat(bigAddition.intValue(), is(numbers[0] + numbers[1]));
        }
    };

    return DynamicTest.stream(inputGenerator, displayNameGenerator, testExecutor);
}

De esta forma también podemos representar cualquiera de los tests que hubiésemos creado con el método que vimos antes. Como prueba, nuestro ejemplo del contains quedaría así (tras dividirlo en generadores, aplicar lambdas y ponerlos directamente como parámetros):

@TestFactory
Stream containsShouldWorkWhenTargetIsWithinTheStringNoMatterItsPositionV2() {
    final String myName = "Alex";
    final List stringsWithMyName = Arrays.asList("Alex", "I'm Alex", "Alex Acebes", "I'm Alex, hi!");

    return DynamicTest.stream(stringsWithMyName.iterator(),
            (input) -> "\"" + input + "\" should contain \"" + myName + "\"",
            (input) -> assertThat(input.contains(myName), is(true)));
}

4.1. Tengamos en cuenta antes utilizarlos…

Antes de que creáis que los tests dinámicos son lo mejor del mundo y que debemos usarlos siempre a partir de ahora (que nunca deberíamos pensar así de ninguna herramienta) hay unas limitaciones que quiero comentar.

  1. Debuguear se vuelve un poco complicado porque tanto al implementar clases anónimas como lambdas se pìerde el contexto de algunas variables y es más difícil saber qué tiene cada campo.
  2. Cuando falla un assert, el stacktrace que se muestra es un poco menos descriptivo de lo habitual. El error aparecerá justo después del fallo, pero la línea no tendrá el nombre de nuestro método sino accept o execute (esto depende de cómo los estemos creando). No es un problema demasiado grave, pero hay que saberlo para no volverse loco.
  3. Las anotaciones @BeforeEach y @AfterEach no funcionan correctamente. Si los utilizamos veremos cómo se ejecutan antes y después del método anotado con @TestFactory pero no de los tests que creamos dentro. Solo nos sirven para hacer inicializaciones sobre valores que no tengan que resetearse entre ellos.
  4. Por último, las interfaces que implementamos están limitadas en el número de parámetros (Executable no admite ninguno y ThrowingConsumer solo uno). Se puede solucionar pasando clases complejas, conjuntos de datos, teniendo colecciones externas del mismo tamaño o algún truco similar. Pero aun así supone una desventaja con las alternativas que veremos a continuación.

4.2. Buenas prácticas

Aunque cada uno es libre de utilizar como quiera las herramientas a su disposición, voy a daros un par de consejos para que no se nos vaya de las manos.

  1. Como regla general, solo utilizaremos las factorías para crear varias instancias del mismo test. Ni siquiera para pasar un conjunto completo de valores de entrada si nuestro SUT (System Under Test) no responde de la misma forma ante ellos. No deberíamos usarlos para agrupar tests que tienen las mismas preparaciones. Si lo hacemos perderemos legibilidad y mantenibilidad y a la larga nos acabará pasando factura. Refactorizando siempre podemos sacar el código común fuera y no tener que recurrir a esto. Además JUnit 5 también pone a nuestra disposición las Nested Classes precisamente para los agrupamientos.
  2. Con tantas interfaces funcionales susceptibles de instanciar con lambdas es fácil crear un código que con el tiempo no entendamos ni nosotros. Si es corto y simple no pasa nada, pero a medida que se complica nos pasará factura. En estos casos es mejor utilizar clases anónimas que, aunque más largas, se pueden entender mejor.
  3. A partir de una cierta complejidad es conveniente utilizar la generación con tres elementos porque nos permite modularizar más el código, entenderlo mejor e incluso reutilizar los generadores de valores de entrada.

5. Alternativas a la generación dinámica sin JUnit 5

Lo que hemos visto parece curioso cuanto menos, pero tiene la pega de que solo se puede usar con JUnit 5 y es posible que por restricciones del proyecto no tengamos acceso a él. Sin embargo, antes hemos dicho que hay más técnicas para lograrlo. Ahora vamos a ver cuáles.

5.1. Usando un bucle for

Si queremos hacer las mismas aserciones sobre una cantidad significativa de datos de entrada, lo primero que se nos viene a la mente es usar un bucle for de la siguiente manera:

@Test
void containsShouldWorkWhenTargetIsWithinTheStringNoMatterItsPositionWithForLoop() {
    final String myName = "Alex";
    final List stringsWithMyName = Arrays.asList("Alex", "I'm Alex", "I'm Alejandro, don't shorten it", "Alex Acebes", "Nobody", "I'm Alex, hi!");

    for (String string : stringsWithMyName) {
        assertThat(string.contains(myName), is(true));
    }
}

De esta forma ejecutamos el mismo test sobre varios datos de prueba, pero no logramos el mismo resultado. Si el assert que ejecutamos dentro falla en alguna iteración (el código de arriba falla en la tercera y quinta) nos daría algo como esto:

Ejecución de tests en un for con fallos

En el detalle del error podemos ver qué ha fallado y, dependiendo del caso concreto, en qué iteración. Pero con el primer error saldremos del bucle y no ejecutaremos el resto. Es decir, si el test pasa es que todos los casos contemplados funcionan pero si falla para uno de ellos no tendremos información de qué hubiera pasado en los que no se han lanzado. Esta es la principal desventaja frente a los tests dinámicos, que si nos dan información de todos los casos de prueba que hemos definido porque se ejecutan de forma independiente.

Podemos arreglar esto si metemos el bloque de código del test dentro de un try/catch que capture un AssertionError. De esta forma, aunque alguno de los tests falle el resto se seguirá ejecutando. Pero como solo tenemos una salida para los tests, habrá que sacar los errores por la consola. Además luego no debemos olvidar lanzar la excepción para que nuestro test salga en rojo:

@Test
void containsShouldWorkWhenTargetIsWithinTheStringNoMatterItsPositionWithTryCatch() {
    final String myName = "Alex";
    final List stringsWithMyName = Arrays.asList("Alex", "I'm Alex", "I'm Alejandro, don't shorten it", "Alex Acebes", "Nobody", "I'm Alex, hi!");
    AssertionError failure = null;

    System.err.println("Test: containsShouldWorkWhenTargetIsWithinTheStringNoMatterItsPositionWithTryCatch");
    for (String string : stringsWithMyName) {
        try{
            assertThat("\"" + string + "\" doesn't contain \"" + myName + "\"", string.contains(myName), is(true));
        } catch(AssertionError error){
            failure = error;
            System.err.println(error.getMessage() + "\n");
        }
    }

    if(failure != null) {
        throw failure;
    }
}

Salida de resultados con try en un for

Salida por consola con try en un for

Así sí que podemos lograr un efecto parecido a lo que buscábamos, aunque deja un poco que desear tanto el código como el resultado. Vamos a ver dos alternativas más usando JUnit.

5.2. Parametrización de tests

Los tests parametrizados permiten, al igual que los dinámicos, añadir tests en tiempo de ejecución. Se definen los atributos de la clase en los que se aparecerán los valores de los distintos casos de entrada. Posteriormente se creará una ejecución del test con cada uno de estos estados.

Para utilizarlos disponemos de la anotación @Parameters para definir el método generador de los estados de entrada y @Parameter para marcar las variables donde se guardarán. En el repositorio que indico al final de la entrada también podéis ver cómo inyectar los valores mediante constructor en lugar de con anotaciones.

Volvamos al ejemplo de comprobar que las operaciones en BigInteger son iguales que en los Integer. Pero esta vez, además de la suma, también usaremos la resta para ilustrar la principal diferencia de la parametrización con respecto al resto de opciones.

@RunWith(Parameterized.class)
public class ParameterizedTesting {

    @Parameter // First parameter doesn't need number
    public Integer firstNumber;

    @Parameter(1) // Starting with second parameter, number is needed
    public Integer secondNumber;

    @Parameters(name = "Test with {0} and {1}")
    public static List generator() {
	final Random random = new Random();

	final List testCases = new LinkedList();
	for (int i = 0; i < 1000; i++) {
		testCases.add(new Integer[] { random.nextInt(), random.nextInt() });
	}

	return testCases;
    }

    @Test
    public void addOfBigIntegerShouldWorkLikePlusOperatorOfIntegers() {
     final BigInteger bigFirst = new BigInteger(firstNumber.toString());
     final BigInteger bigSecond = new BigInteger(secondNumber.toString());

     final BigInteger bigAddition = bigFirst.add(bigSecond);

     assertThat(bigAddition.intValue(), is(firstNumber + secondNumber));
    }

    @Test
    public void substractOfBigIntegerShouldWorkLikeMinusOperatorOfIntegers() {
     final BigInteger bigFirst = new BigInteger(firstNumber.toString());
     final BigInteger bigSecond = new BigInteger(secondNumber.toString());

     final BigInteger bigAddition = bigFirst.subtract(bigSecond);

     assertThat(bigAddition.intValue(), is(firstNumber - secondNumber));
    }
}

Ejecución de tests parametrizados

Como vemos, en este caso también poblamos nuestro árbol de tests con cada uno de los casos particulares en tiempo de ejecución. La diferencia fundamental en cómo ocurre. Antes se miraba para cada test qué estados de entrada íbamos a tener. Ahora miraremos cada estado de entrada y lanzaremos todos los tests de la clase con él, por lo que se agrupan bajo casos de entrada. Esto implica que todas las pruebas que definimos en la clase tendrán el mismo número de ejecuciones y con los mismos datos. En ocasiones como la que acabamos de ver puede ser útil, pero normalmente nos limita bastante y nos obliga a cambiar la mentalidad a la hora de elegir nuestro SUT.

5.3. Testing con teorías

Las teorías son otra utilidad de JUnit que permiten definir un número variable de valores para cada tipo de dato. Esto se puede hacer definiendo los DataPoints, que pueden ser valores constantes, colecciones o métodos generadores. Posteriormente en cada test se define qué parámetros va a recibir y se ocupará de probar todas las posibles combinaciones con los valores correspondientes a su tipo. Esta es la principal diferencia con las opciones que hemos visto hasta ahora y que había que hacer explícitamente. Además se pueden filtrar los valores que queremos usar eligiendo el DataPoint concreto si tenemos varios para un mismo tipo.

Su uso es tan sencillo como definir las entradas con @DataPoints si anotamos un método generador o @DataPoint si se trata de una variable y luego hacer que el test reciba los parámetros que necesitemos.

Para poner de manifiesto la selección de DataPoints vamos a cambiar de ejemplo y probar que la multiplicación de un número positivo y uno negativo da como resultado uno negativo y que elevar al cuadrado da siempre un resultado positivo.

@RunWith(Theories.class)
public class TheoriesTesting {

	@DataPoints("positive values")
	public static int[] positiveCreation() {
		return new int[]{ 1, 2, 3};
	}

	@DataPoints("negative values")
	public static int[] negativeCreation() {
		return new int[]{ -1, -2, -3};
	}

	@Theory
	public void multiplicationShouldGiveNegativeValueWhenNumbersHasDifferentSigne(@FromDataPoints("positive values") final int a, @FromDataPoints("negative values") final int b) {
		System.out.println("Multiply: " + a + " * " + b);
		assertThat(a*b, lessThan(0));
	}

	@Theory
	public void squareShouldGivePositiveResult(final int number) {
		System.out.println("Square: " + number + "^2");
		assertThat(number*number, greaterThan(0));
	}
}

Gracias a lo que hemos sacado por pantalla podemos ver que efectivamente para el primer test solo se han cogido los valores que queríamos para cada parámetro mientras que para el segundo se han usado todos los DataPoints disponibles.

Salida por consola de las combinaciones de valores con teorías

Resultados de los tests con teorías

Sin embargo, esta vez no se están creando varios tests en el árbol sino uno solo. En realidad está funcionando de forma muy similar a nuestra primera aproximación con un for. Esto implica que tenemos las mismas dificultades a la hora de trabajar con las aserciones fallidas, aunque la solución no es tan sencilla. Aparte de esto, la principal diferencia es que nos abstrae de la generación y combinación de valores de entrada, pero no es nada que no pudiésemos simular nosotros sin demasiadas dificultades.

5.4. Comparativa de todas las alternativas

Aunque solo hemos dado una visión general de cómo funcionan los tests parametrizados y las teorías, es importante hacer una comparación. A priori la generación dinámica de tests es la herramienta más potente porque permite, con mayor o menor esfuerzo, simular el comportamiento de cualquiera de las demás. Pero en general todas las opciones son muy similares y, si se utilizan algunos trucos o se combinan con otras técnicas, pueden dar los mismos resultados. Vamos a compararlas en base a tres puntos: la visualización de los resultados, la sintaxis y la dificultad de adaptarse a algunos casos concretos.

  1. Con respecto a la visualización, a mi parecer los tests dinámicos son los que organizan mejor la información para casos normales porque puedes ver fácilmente todas las instancias de un test concreto. Además ofrecen una armonía entre centrarnos en el comportamiento (teorías y bucles for) y en que todo un conjunto de datos valide contra las operaciones sean cuales sean (parametrización).
  2. A la hora de escribir y leer creo que las teorías son las más intuitivas, mientras que la parametrización es algo más pesada y tienes que involucrar a la clase entera para utilizarla. Por su parte, los tests dinámicos son un poco complejos al principio pero según vas haciéndote a ellos se vuelven muy sencillos y se puede reutilizar bastante código.
  3. Por último, en flexibilidad la generación dinámica tiene las de ganar porque tiene una dificultad similar para implementar casi cualquier caso. En cambio el resto de soluciones son mucho más sencillas bajo las condiciones correctas pero también más complejas en situaciones adversas.

Para concluir, si tuviese que quedarme con una opción para usar a diario en cualquier situación me quedaría con la protagonista de esta entrada. Pero aún así, cada una tiene sus puntos fuertes y convendría analizar primero a qué nos vamos a enfrentar para poder elegir con fundamento.

5.5. Property-based testing

Antes de terminar quería hacer un último apunte sobre la similitud de los tests dinámicos con el property-based testing. Si bien es cierto que en ambos casos tenemos como entrada un conjunto de estados y probamos cada uno de ellos por separado, no hay que confundirlos porque están en dos niveles claramente diferenciables. La creación de tests dinámicos de la que hemos hablado es simplemente una herramienta que nos ofrece la posibilidad de lanzar un test todas las veces que queramos. Por su parte, property-based testing es una técnica que parte de la filosofía de que cada test comprueba que una propiedad concreta de nuestro código (ya sea un método, una secuencia de ellos o una funcionalidad aún mayor) se cumple siempre para todas las entradas del dominio válido. Quizá las teorías, por tener un enfoque original más matemático, son las que más se asemejan a esto.

No obstante, sí que es cierto que los tests dinámicos y el resto de alternativas que hemos comentado son un buen punto de partida para comenzar a usarlo y crear frameworks específicos.

5.6. Otras herramientas

En esta entrada me he limitado a ver la alternativas con las que contamos si utilizamos únicamente JUnit, pero por supuesto hay más opciones si nos vamos a herramientas externas.

En lo que a tests parametrizados se refiere hay algunas muy interesantes, pero quizá la más usada sea JUnitParams. Permite personalizar la obtención de parámetros y evitar la restricción de que todos los tests tienen que ejecutarse con todos los valores. Ahora podremos especificar para cada uno si los queremos de uno o varios métodos concretos, una lista, un fichero…

Además también tenemos a nuestra disposición herramientas para property-based testing, que nos servirían para lo que estamos buscando. Las hay para casi todos los lenguajes, pero para Java la más conocida es junit-quickcheck. Estos frameworks tienen más utilidades que simplemente dar valores de entrada a los tests, pero puede servirnos perfectamente solo para esto.

6. Conclusiones

En esta entrada hemos visto qué es la generación dinámica de tests, cuándo nos puede venir bien y sus limitaciones. Pero aunque pueda llamar la atención, no ha venido ni mucho menos para destronar a los tests tradicionales. Simplemente son una herramienta más para facilitarnos el trabajo en casos concretos. Es importante ver primero si nuestra situación es adecuada y, en ese caso, cuál de las alternativas que hemos comentado se adapta mejor a las circunstancias.

No obstante, os animo a experimentar un poco por vuestra cuenta fuera de proyectos reales. Si es para divertirse un rato no es tan importante guardar las formas y cualquiera de las opciones que hemos tratado se pueden potenciar y combinar para dar resultados muy interesantes.

Por último, aquí tenéis el enlace al repositorio de GitHub donde está subido el código del tutorial y algunas pruebas más, listo para importar y ejecutar.

7. Referencias


Cómo sacar partido a los monitores 4K trabajando desde Eclipse en macOS

$
0
0

En el tutorial de hoy vamos a explicar una manera muy sencilla de aprovechar la capacidad de los nuevos monitores 4K cuando estamos trabajando con Eclipse en Mac, uno de los IDE’s de desarrollo más habituales dentro del mundo JAVA.

Índice de contenidos

1. Introducción

Parece que últimamente la palabra de moda en cuanto a dispositivos/pantallas se refiere, es 4K. Sin entrar, ni mucho menos en los detalles técnicos, lo que nos permite esta tecnología, entre otras cosas, es disponer de una mayor resolución de pantalla, lo que sin duda, puede resultar una gran ventaja para todos aquellos que, como nosotros, sois desarrolladores y pasáis muchas horas al día frente a la pantalla de vuestro Mac.

Sin embargo, a pesar de lo cómodo que puede suponer para un desarrollador disponer de un mayor espacio en su pantalla dada por una mayor resolución, es posible que, si os estáis haciendo “viejunos” como yo, no veáis con facilidad los textos, ya sea en Eclipse o en cualquier otra aplicación.

Inicialmente el sentido común nos indica que la solución es sencilla, aumentar la fuente por defecto del sistema o aumentar la fuente por defecto a nivel de preferencias de Eclipse. Sin embargo, ninguna de estas soluciones por si sola sirve para conseguir aprovechar la máxima resolución de nuestra pantalla 4K sin dejarnos la vista mientras desarrollamos en Eclipse.

2. Entorno

El tutorial está escrito usando el siguiente entorno:
  • Hardware: Portátil MacBook Pro Retina, 15′ (2,5 GHz Intel Core i7, 16 GB 1600 MHz DDR3).
  • Sistema Operativo: macOS Sierra 10.12.5
  • Entorno de desarrollo: Eclipse Java EE IDE for Web Developers. Version: Neon.2 Release (4.6.2)
  • Pantalla: LG Ultra HD 4K 27′
  • Herramientas: TinkerTool 6.1

*** Está solución es válida solo para macOS.

3. El problema

El problema es simplemente que, si a nivel de preferencias de pantalla en mi Mac, establezco la máxima resolución que ofrece mi pantalla 4K no veo un “pimiento”. Sin embargo, si hago uso de una resolución menor que me permita leer correctamente el texto pierdo muchísimo espacio en la pantalla y se hace mucho más incomodo leer y escribir el código de nuestras aplicaciones.

A continuación se muestran un par de capturas de pantalla, donde espero que podáis apreciar la diferencia entre el espacio de pantalla y el tamaño de la fuente entre la resolución por omisión y la máxima resolución que nos proporciona nuestra pantalla 4K:

Como podéis apreciar, en la primera imagen (resolución por omisión 1920×1080) la fuente/texto es mayor y es perfectamente legible aunque el espacio es menor que la segunda imagen (resolución máxima 3840×2161) lo que hace más incomodo el trabajo diario. Sin duda, como desarrollador prefiero disponer de más espacio de trabajo para, por ejemplo, ver una clase completa de un solo vistazo pero lo principal es ser capaz de leer el código que estamos escribiendo y no se vosotros pero, a máxima resolución, a mí se me hace casi imposible.

Sólo recordar que para cambiar la configuración de nuestras pantallas en Mac solo es necesario ir a Preferencias del Sistema –> Pantallas.

4. La solución

Bueno, como indicaba en la introducción una posible solución pasaría por aumentar la fuente por defecto en las Preferencias de Eclipse

El inconveniente de esta solución es que solo podemos modificar las fuentes de los distintos editores que nos proporciona Eclipse, lo que no impide que el resto de vistas de las perspectivas correspondientes se sigan visualizando con un tamaño tan reducido que sigue siendo demasiado incomodo trabajar.

La otra posible solución pasa por aumentar la fuente por defecto de nuestro macOS pero, ¡¡oh no!! como muchos de vosotros ya sabréis, por defecto en macOS no puedo configurar algunas preferencias de sistema tan básicas como el tamaño de la fuente. Es posible realizar estas modificaciones en algunas de las aplicaciones nativas del sistema desde las preferencias de cada aplicación, pero Eclipse no es una de estas aplicaciones y como acabamos de ver desde sus Preferencias solo se puede modificar el tamaño de las fuentes de los editores.

A continuación una captura donde se puede apreciar como, tras modificar la fuente del editor de Java, el código es perfectamente legible mientras que el resto de los textos de las vistas no lo son.

4.1.TinkerTool

En este punto es donde os presentamos una herramienta verdaderamente util para los usuarios de Mac, sobre todo en el caso que nos ocupa. Está herramienta es TinkerTool, una aplicación que nos permite personalizar las preferencias de nuestro Mac de manera más que interesante.

El primer paso es realizar la instalación de la aplicación tal y como lo haríamos con cualquier otra. Una vez arrancamos la aplicación vemos algo como lo que se muestra en la siguiente captura de pantalla:

Os recomiendo que echéis un vistazo en general a la cantidad de características de sistema que nos permite modificar la aplicación aunque las características que nos interesan se encuentran en el menú Fonts.

De manera muy sencilla podemos aumentar cada una de las fuentes del sistema a nuestro gusto. La próxima vez que arranquemos Eclipse observaremos que aumentado también de manera significativa el tamaño de las fuentes de las vistas de cada una de las perspectivas.

Aun así y aunque creo que con esto sería suficiente para trabajar de una manera bastante más cómoda, para mí, todavía el tamaño de las fuentes de las vistas es demasiado reducida si estamos haciendo uso de la máxima resolución de pantalla (3840 x 2161), por no mencionar que todos los iconos siguen viéndose realmente pequeños. Además este cambio en las características del sistema no solo afecta a Eclipse sino a cualquier aplicación que haga uso de las fuentes del sistema, lo que no tiene porque ser lo que andamos buscando, es decir, puede que no queramos modificar las fuentes del resto de aplicaciones sino solo las fuentes de Eclipse, pero ¿realmente podemos hacer algo al respecto?

Por desgracia la respuesta a la pregunta anterior es NO, al menos en nuestros entornos macOS. Es por eso que la solución propuesta en este tutorial es lo que más se acerca a la solución adecuada y proporcionada por Eclipse para solucionar este problema desde la release Eclipse Neon (4.6) M6.

Como se puede apreciar en la documentación “Auto-scaling cannot be disabled on the Mac as it is provided by the OS.” y por ello esta solución no es válida en entorno macOS. Mi recomendación sin duda y dado que no podemos resolver esté problema al 100% es la de hacer uso de TinkerTool para aumentar las fuentes del sistema de tal modo que, tanto los editores de Eclipse como las vistas de cada una de las perspectivas dispondrán de un tamaño aceptable. Para los iconos solo podemos renunciar a algo del espacio que nos proporciona la máxima resolución y hacer uso de una resolución menor.

5. Conclusiones

La conclusión es que siempre que podamos hay que aprovechar al máximo las ventajas del hardware que utilizamos en nuestro trabajo diario a pesar de que, en ocasiones no sea sencillo e incluso imposible dadas las limitaciones que nos impone el software que utilizamos.

Espero que os sirva de utilidad y que pronto os pueda aportar una mejor solución que nos permita hacer más cómodo nuestro trabajo diario como desarrolladores.

Un cordial saludo.

Implementación de un sistema de notificaciones en Angular

$
0
0

En este tutorial vamos a resolver un caso de uso muy común en cualquier aplicación front a la hora de querer mostrar al usuario un mensaje de feedback de sus operaciones, y lo vamos a hacer de una forma muy sencilla y reactiva con Angular, RxJS y PrimeNG.

Índice de contenidos


1. Entorno

Este tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil Mac Book Pro 15″ (2,3 Ghz Intel Core i7, 16 GB DDR3)
  • Sistema Operativo: macOS Sierra
  • @angular/cli 1.06

2. Introducción

A poco que hayas desarrollado alguna aplicación de front te habrás encontrado con el caso de uso de tener que mostrar una notificación al usuario ya sea por un error o para informar de que todo ha ido bien.

Si la aplicación la has desarrollado en Angular te habrás visto en la necesidad de tener que poner en cada componente la lógica de notificaciones al usuario, o habrás dejado el típico console.log(error) 😉

Pero, amigo, la programación reactiva ha llegado para solucionar muchos de estos casos de usos comunes de una forma elegante, efectiva y sobre todo, que te evita duplicar código en componentes.


3. Vamos al lío

Lo primero que vamos a hacer es definir nuestro modelo creando una interfaz llamada “Notificacion” con el siguiente contenido:

export interface Notificacion {
    severity: string;
    summary: string;
    detail: string;
};

Como ves simplemente definimos que nuestra notificación va a definir una severidad, un texto de resumen y un detalle todos ellos de tipo string.

Ahora creamos un servicio de bus de notificaciones donde vamos a tener una variable de tipo Subject que nos va a permitir enviar y recibir las notificaciones a través de este bus. Además nos vamos a definir una serie de métodos específicos para cada una de las severidades de la notificación.

El contenido del servicio podría ser parecido a este:

import { Notificacion } from './notificacion';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';

@Injectable()
export class NotificacionesBusService {

  showNotificacionSource: Subject = new Subject();

  getNotificacion(): Observable {
    return this.showNotificacionSource.asObservable();
  }

  showError(msg: string, summary?: string) {
    this.show('error', summary, msg);
  }

  showSuccess(msg: string, summary?: string) {
    this.show('success', summary, msg);
  }

  showInfo(msg: string, summary?: string) {
    this.show('info', summary, msg);
  }

  showWarn(msg: string, summary?: string) {
    this.show('warn', summary, msg);
  }

  private show(severity: string, summary: string, msg: string) {
    const notificacion: Notificacion = {
      severity: severity,
      summary: summary,
      detail: msg
    };

    this.notify(notificacion);

  }

  private notify(notificacion: Notificacion): void {
    this.showNotificacionSource.next(notificacion);
  }

}

Los puntos clave de esta implementación son por un lado el envío de la notificación al bus con el método “notify” y por otro el método “getNotificacion” que permite devolver el observable al que se van a subscribir los escuchantes del bus.

A fin de probar el comportamiento podemos implementar un test con el siguiente contenido:

import { TestBed } from '@angular/core/testing';
import { async } from '@angular/core/testing';
import { NotificacionesBusService } from './notificaciones-bus.service';
import { Notificacion } from './notificacion';

describe('Notificaciones bus service', () => {

    beforeEach(async(() => {
        TestBed.configureTestingModule({
            providers: [NotificacionesBusService]
        });
    }));

    it('should enviar y obtener una notificacion', () => {
        const msg = 'Prueba';
        const service: NotificacionesBusService = TestBed.get(NotificacionesBusService);
        service.getNotificacion().subscribe(
            noti => {
                console.log(noti.detail);
                expect(noti.detail).toEqual(msg);
            },
            error => {
                fail(error);
            }
        );
        service.showError(msg);
        service.showSuccess(msg);
        service.showWarn(msg);
        service.showInfo(msg);
    });
});

En el resultado del test tienes que ver que se muestra cuatro veces el contenido del detalle. Fíjate que estamos usando Subject en la implementación del servicio, porque estamos seguros de que primero se va a subscribir antes de empezar a enviar nada.

Nota: Te aconsejo que modifiques el test y pongas el subscribe después de las llamadas a los métodos. Ahora verás que no muestras los mensajes por consola. Esto es debido a que con el Subject solo recibes lo que envías después de la subscripción. En caso de no estar seguros de que la subscripción se realiza antes del envío del primer mensaje, cambia Subject por ReplaySubject en la implementación del servicio.

De esta forma podemos incluir la lógica de recibir la notificación en el componente principal de la aplicación a fin de que esté disponible desde cualquier punto de la aplicación; y hacer uso de una librería de componentes como PrimeNG con un componente Growl para pintar de forma “bonita” la notificación por pantalla.

Este sería el contenido del fichero app.component.html:

<p-growl [(value)]="msgs" id="errorMessages"></p-growl>
<router-outlet></router-outlet>

Dentro del fichero app.component.ts tenemos que inyectar a través del constructor el servicio “NotificacionesBusService” antes creado y establecer en el método ngOnInit() la lógica de subscripción al bus para atender a las notificaciones que se envíen:

constructor(private notificacionesBus: NotificacionesBusService) { }

ngOnInit() {
     this.notificacionesSub =
     this.notificacionesBus.getNotificacion().subscribe(
        (notificacion: Notificacion) => {
            this.msgs = [];
            this.msgs.push(notificacion);
         }
      );
}

No olvidéis hacer la desubscripción en el método ngOnDestroy() de todas las subscripciones que hagáis, evitando “memory leaks”

ngOnDestroy() {
        if (this.notificacionesSub) {
            this.notificacionesSub.unsubscribe();
        }
    }

Ahora desde cualquier parte de la aplicación, ya sea otro componente o servicio, podemos inyectar el servicio NotificacionesBusService y llamar a cualquiera de sus métodos con la información de la notificación que queramos mostrar sin preocuparnos por nada más, ya que este envío será atendido por la lógica del componente principal y el mensaje se mostrará por pantalla gracias al componente de PrimeNG, el cual previamente tendréis que instalar como dependencia en vuestro proyecto Angular. El componente que utilicéis es independiente de la lógica de envío de notificaciones.

Este podría ser un ejemplo de uso desde otro componente:

constructor( private notificacionBus: NotificacionesBusService) { }

this.service.hagoAlgo(info)
        .then(res => {
          this.notificacionBus.showSuccess('Proceso con éxito');
        })
        .catch(err => {
          this.notificacionBus.showError('Ha ocurrido un error');
        });

4. Conclusiones

Haz el cálculo de la cantidad de código duplicado que te ahorras con esta técnica y lo que ganas en mantenibilidad y legibilidad y como yo gritarás bien alto: !Viva la programación reactiva con RxJS¡

Cualquier duda o sugerencia en la zona de comentarios.

Saludos.

Manejo de documentos PDF en Java con iText

$
0
0

En este tutorial se mostrará la creación de un documento PDF de forma sencilla y rápida empleando iText y Maven.

Índice de contenidos


1. Introducción

Si por el motivo que fuera se necesita manejar un documento PDF, aunque ya sea para agregar unas líneas o una imagen, una forma sencilla y sin complicaciones de realizar este cometido es emplear iText.

iText nos permite, usando Java, editar o transformar contenido a PDF. Aunque en este tutorial se trate de forma muy tangente no quiero dar a entender que deba ser un “engine”, como lo llaman sus desarrolladores, a tener en cuenta a la ligera ni mucho menos. De hecho es una herramienta muy potente capaz de crear PDFs de formas muy variopintas, mezclarlos y hasta convertir código HTML a PDF.

Antes de comenzar a “trastear” con iText debo aclarar que es gratuito siempre y cuando no se emplee en proyectos cerrados, en dicho caso se requerirá una licencia.


2. Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro 15′ (2 Ghz Intel Core I7, 8GB DDR3).
  • Sistema Operativo: Mac OS Sierra 10.12.5
  • Entorno de desarrollo: Eclipse Neon 3
  • Apache Maven 3.5.0

3. Añadir las dependencias en Maven

Una vez creamos nuestro proyecto maven en Eclipse toca añadir las dependencias.

En nuestro caso es tan sencillo como añadir iText como dependencia en el pom.xml de nuestro proyecto:

<dependency>
    <groupId>com.itextpdf</groupId>
    <artifactId>itextpdf</artifactId>
    <version>5.5.10</version>
</dependency>

4. Manejo del documento en Java

Aprovechando que Maven nos ha generado un App.java vamos a usarlo para nuestro pequeño ejemplo:

public class App
{

	public static void main(String[] args)
    {
        writePDF();
    }

    private static void writePDF() {

        Document document = new Document();

        try {
        	String path = new File(".").getCanonicalPath();
        	String FILE_NAME = path + "/itext-test-file.pdf";

            PdfWriter.getInstance(document, new FileOutputStream(new File(FILE_NAME)));

            document.open();

            Paragraph paragraphHello = new Paragraph();
            paragraphHello.add("Hello iText paragraph!");
            paragraphHello.setAlignment(Element.ALIGN_JUSTIFIED);

            document.add(paragraphHello);

            Paragraph paragraphLorem = new Paragraph();
            paragraphLorem.add("Lorem ipsum dolor sit amet, consectetur adipiscing elit."
            		+ "Maecenas finibus fringilla turpis, vitae fringilla justo."
            		+ "Sed imperdiet purus quis tellus molestie, et finibus risus placerat."
            		+ "Donec convallis eget felis vitae interdum. Praesent varius risus et dictum hendrerit."
            		+ "Aenean eu semper nunc. Aenean posuere viverra orci in hendrerit. Aenean dui purus, eleifend nec tellus vitae,"
            		+ " pretium dignissim ex. Aliquam erat volutpat. ");

            java.util.List paragraphList = new ArrayList<>();

            paragraphList = paragraphLorem.breakUp();

            Font f = new Font();
            f.setFamily(FontFamily.COURIER.name());
            f.setStyle(Font.BOLDITALIC);
            f.setSize(8);

            Paragraph p3 = new Paragraph();
            p3.setFont(f);
            p3.addAll(paragraphList);
            p3.add("TEST LOREM IPSUM DOLOR SIT AMET CONSECTETUR ADIPISCING ELIT!");

            document.add(paragraphLorem);
            document.add(p3);
            document.close();

        } catch (FileNotFoundException | DocumentException e) {
            e.printStackTrace();
        } catch (IOException e) {
			e.printStackTrace();
		}

    }
}

Mediante este código obtenemos el siguiente resultado:

Empleamos principalmente párrafos, aunque cada uno tiene sus peculiaridades. Existen más elementos como imágenes, pero los fundamentos son los mismos.

Podemos modificar el alineamiento del texto añadido ya que los párrafos tienen atributos para tal fin, además de poder cambiar la fuente en tamaño, forma e incluso que se vea en “negrita” o cursiva.

Una función que puede llamar la atención es “addAll” la cual añade todos los elementos de una colección de tipo Element, en este caso, al párrafo, lo que puede dinamizar bastante el crear estos documentos. Llama la atención el hecho de que añade “tal cual” los elementos de la colección, es decir, como puede observarse en la imagen del resultado, el estilo sigue siendo el original y no el que tiene el párrafo.

Cabe destacar que el documento PDF debe abrirse antes de operar con él y debe cerrarse tras las últimas modificaciones.


5. Conclusiones

Como hemos visto anteriormente, el manejo de un documento es muy sencillo, aunque no olvidar que puede complicarse todo lo que queramos ya que nos permite hacer muchas cosas más, pero para ello os invito a probar iText y a revisar los ejemplos que muestran, que podréis encontrar más abajo.


6. Referencias

Google Analytics I: iniciación

$
0
0

Índice de contenidos

1. Introducción

Vamos a aprender en una serie de tutoriales qué es Google Analytics, para qué sirve, cómo instalarlo y cómo interpretar los datos de los informes que nos ofrece. Esta herramienta cuenta, además, con otras extensiones o complementos que también veremos sobre la marcha en más o menos profundidad, como Campaign URL Builder, Page Analytics o Search Console.

2. Preparando el terreno

  • Crear una cuenta.

  • cuenta de google analytics
  • Localizar el identificador: administrador (la rueda del menú) / Seleccionar nuestra cuenta en CUENTA / Seleccionar una propiedad en PROPIEDAD y dentro de ese mismo menú Información de seguimiento / Código de seguimiento

  • ID seguimiento
  • Incluirlo en WP: inicia sesión en WordPress e instala el plugin ‘Google Analytics for WordPress by MonsterInsights’ (si tenías instalado Google Analytics by Yoast, sustitúyelo). Sigue las instrucciones para vincular tu cuenta con el plugin desde la pestaña ‘General’ que es la que se abre por defecto. En la pestaña ‘Traking’ configura el plugin:
    • Engagement: añade al editor y al colaborador si lo deseas.
    • Atribución de enlace mejorada.
    • Ajustes plugin
    • Affliate links: Establece la dirección /externo/ y utiliza para el label la misma palabra sin barras.
    • Affiliate links

    El resto de menús los dejamos como están por defecto.

3. Conozcamos Analytics

Lo primero que hay que saber es que ha habido un rediseño de la interfaz de Google Analytics:

  • Desaparece el menú superior.
  • Aparece un menú que permite navegar entre cuentas y vistas, y que permite establecer favoritas.
  • En la parte superior derecha (menú de tres puntos verticales) podemos configurar el periodo de tiempo por defecto que queremos ver de nuestros informes.
  • Se ha actualizado la navegación y el diseño del menú lateral:
    • El menú Personalización agrupa todas los paneles, informes, accesos directos y alertas que creemos o queramos crear.
    • Al final se incorpora el menú de Configuración de administrador.

4. Menús laterales: Audiencia

Desde este menú tenemos acceso a todo lo referente a nuestras sesiones (antes llamadas visitas) y a nuestros usuarios (antes visitantes). Dos sesiones pueden ser de un mismo usuario. Nos informa sobre quiénes son nuestros usuarios.

En el primer submenú, ‘Overview’, veremos una vista general de todos los datos que nos informan de cómo es nuestra audiencia: número de usuarios totales, usuarios nuevos, páginas visitadas, tasa de rebote… Además, podremos elegir el periodo de tiempo que queremos analizar. Si hacemos clic sobre cada uno, la gráfica superior cambia.

Audiencia Overview

Otros datos generales que nos muestra esta primera pantalla son los relativos al lugar de donde proceden nuestros usuarios, su idioma, el dispositivo de acceso o el sistema operativo de dicho dispositivo.

De todos los datos podemos extraer informes más detallados haciendo clic en el link inferior: ‘view full report’. Si os fijais, cada vez que hacemos clic en estos detalles se abre/resalta un submenú a la izquierda dentro de ‘Audience’. Por tanto, podremos ir directamente a cada informe que nos interese sin pasar por el general.

NOTA: el indicador de idioma ‘es’ y ‘es-es’ se puede agrupar en uno solo ya que es lo mismo pero se divide por motivos de configuración de cada usuario.

Language

El el submenú ‘Active Users’, se nos muestra el informe relativo a usuarios activos, es decir, usuarios que usan el servicio estén o no registrados.

Los submenús Demographics, Interest, Geo, Behavior, Technology y Mobile se corresponden prácticamente con los vistos en ‘Overview’.

En ‘Benchmarking’ podemos ver cómo está nuestro proyecto comparado con el sector que hemos concretado al crear la cuenta. Si quieres cambiar el sector, en cualquiera de los submenús puedes modificarlo. Este menú nos indica en qué debemos mejorar, marcándolo en rojo.

Sector

‘Users Flow’ indica cómo los usuarios se mueven por tu página. En mi opinión es uno de los menús más importantes ya que puedes ver fácilmente qué páginas de tu sitio están funcionando o por el contrario, analizar por qué no llega tráfico a una sección determinada o dónde y por qué se caen tus usuarios. Desde aquí podremos filtrar el parámetro de medida y ver todos los flujos que mide Analytics.

Filtros Flujos

5. Menús laterales: Adquisición

Este menú centra el análisis en indicarnos cómo llegan los usuarios a nuestro site, es decir, qué redes sociales atraen más visitas, si hay terceros que nos enlazan, los anuncios más óptimos, etc. Por tanto, nos da información sobre de dónde vienen nuestras visitas. Igual que en el menú ‘Audiencia’ podremos establecer la franja temporal que queremos analizar.

Selección Fechas

En ‘Overview’, lo primero que nos destaca analytics son los canales desde donde entra el mayor número de usuarios o Top Channels. También el número de sesiones y las conversiones.

NOTA: una conversión es un cambio de estado del usuario al realizar una acción definida por marketing y por la que pasa a ser un cliente, suscriptor, lead…

Channels Canales

Debajo de este gráfico general podemos ver datos más detallados de cada uno de los canales: nuevas sesiones, nuevos usuarios, tasa de rebote…

NOTA: m.facebook.com es la versión móvil de Facebook

Para entrar más en el detalle de los canales navegaremos por el submenú de la izquierda ‘All Traffic’. En ‘Channels’ veremos la información de ‘Overview’ más especificada y con opciones de filtrado más precisas. Podemos ver los datos agrupados por canales o podemos identificar la fuente y/o el medio.

Filtros Channels

Además, podremos ir profundizando en cada canal hasta llegar al detalle igual que hemos hecho en ‘Audiencia’.

En ‘Treemaps’ (seguimos dentro de All Traffic) analytics utiliza un método para mostrar información jerárquica usando rectángulos anidados (treemaping). Estos rectángulos se configuran en función de dos métricas:

  • Primaria: establece el tamaño del rectángulo.
  • Secundaria: marca el color que varía de rojo (valores más bajos aunque no necesariamente malos) a verde (valores más altos).

De esta manera en cada rectángulo podremos ver de un vistazo ambas métricas, pudiendo, además, ver el valor de la métrica secundaria en el globo ubicado junto al canal.

treemap

En la parte inferior tendremos el desglose habitual, sin embargo, en este menú al hacer clic sobre cada canal nos mostrará más información específica:

  • Organic Search: muestra las keywords.
  • Social: muestra las diferentes redes sociales.
  • Direct: muestra las páginas de destino.
  • Referral: muestra las fuentes de tráfico.

Los submenús ‘Source/Medium’ y ‘Referrals’ dentro de ‘All Traffic’ son accesos directos a lo visto anteriormente.

Todos los informes del submenú ‘AdWords’ aparecerán, lógicamente, sólo si lo tenemos activo. No entramos en este tutorial.

El submenú ‘Search Console’, o lo que antes llamábamos Web Master Tools, muestra los mismos informes que la propia herramienta pero en analytics. Es decir, podemos saber cómo ve el buscador nuestro site (palabras clave, información demográfica y links de procedencia) y mejorar el rendimiento del mismo. Para ello, debemos crear una cuenta en Search Console, añadir una propiedad y validarla.

WMTools

‘Social’ nos da informes sobre las redes sociales desde las que nos llega más tráfico al compartir contenido de nuestro site en ellas. En ‘Network Referrals’ vemos una comparativa entre las sesiones abiertas desde redes sociales y las sesiones globale. Deberán ir más o menos a la par. Como siempre, podemos entrar en cada red social y profundizar más.

Social Referrals

En ‘Landing Pages’ (social) nos especifica a través de qué páginas concretas han accedido nuestros usuarios. Son interesantes porque vemos sus intereses y si nuestras campañas en redes funcionan y en cuáles lo hacen mejor.

‘Conversions’ nos marca las conversiones que vienen de redes sociales. Este informe nos dice en qué red social conseguimos más conversiones ya sean económicas, de suscripción, contacto…

Al igual que en ‘Audiencia’ veíamos el flujo de los usuarios por nuestro site, dentro de ‘Social’ podremos ver el flujo de los usuarios por nuestras redes sociales en el submenú ‘Users Flow’.

Flujo RRSS

En el submenú ‘Campaigns’ volvemos a encontrar varias opciones. ‘All Campaigns’ muestra toda la información global relativa a las campañas, ya sean de AdWords o no.

NOTA: Google Analytics define que “las campañas publicitarias, motores de búsqueda, redes sociales y otras fuentes que envían usuarios a tu propiedad se conocen en general como campañas y fuentes de tráfico”.

Para crear una campaña:

  • Abrimos Campaign URL Builder.
  • Rellenamos los campos:
    • Website URL: URL a la que quiero mandar a los usuarios.
    • Campaign Source: fuente de la campaña. Identificamos un motor de búsqueda, boletín, página web…
    • Campaign Medium (opcional): medio de la campaña. Identificamos si es un correo electrónico, un banner, el coste por clic (CPC)…
    • Campaign Name (opcional): nos sirve para identificar un producto específico o una estrategia concreta.
    • Campaign Term (opcional): determinamos las palabras clave si las hay.
    • Campaign Content (opcional): lo utilizaremos para realizar test A/B o para diferenciar los enlaces que llevan a la misma URL.
    Campos-Campaña
  • Generamos la URL que alberga las etiquetas que hemos creado.
  • La copiamos en el banner, newsletter, ad, post… que corresponda.
  • Revisar que se añade automáticamente a tu panel de campañas de analytics.

Si utilizamos Adwords, todo este proceso se hace automáticamente cuando ponemos un anuncio.

Al igual que en el resto de menús, tendremos una tabla en la parte inferior con los datos obtenidos de cada campaña. Podremos usar los filtros para enfocar nuestro análisis y entrar en cada campaña para ver los detalles.

‘Paid Keywords’ son las palabras clave de adwords y las establecidas en el campo Compaign Term de las campañas. ‘Organic Keywords’ son aquellas palabras clave a través de las cuales nos encuentran los usuarios. En primer lugar, con un mayor número de sesiones, aparece (not provided). Si queremos profundizar más seleccionaremos el filtro Landing Page (página de destino).

Not Provided Landing

‘Cost analysis’ (dentro de ‘Campaigns’) nos da informes siempre y cuando tengamos puestos costes por cada visita, por ejemplo desde AdWords.

6. Menús laterales: Comportamiento

En estos informes recibimos información sobre el comportamiento de nuestros usuarios, es decir, qué hacen.

En ‘Overview’, como siempre, tenemos la vista general de los informes. El primer dato indica el número de páginas vistas, mientras que el segundo dato nos dice cuántas se han visto una vez, descartando la repetición.

Comportamiento

En la parte inferior figuran las páginas más visitadas y el porcentaje correspondiente. Por defecto se muestran las URLs pero podemos cambiar de vista en ‘Page Title’ y ver los títulos de esas páginas. Otro filtro interesante es ‘Search Term’, que muestra las búsquedas hechas dentro del propio site. Así podremos saber lo que más se solicita o lo que menos explicado y/o accesible está. También podemos elegir ver los eventos (‘Event Category’), es decir, las acciones que los usuarios desempeñan dentro de las páginas.

Filtros_Comportamiento

‘Behavour Flow’, al que ya hemos visto que también se puede acceder desde los filtros propios de cualquier pantalla de flujo, muestra qué hacen los usuarios, de dónde vienen, a dónde van o si se caen (rojo). Ubicando el ratón sobre cualquier agrupación de flujos, se nos muestra el detalle general de los datos.


Si hacemos clic podemos elegir opciones de visualización de cada grupo que varían según los datos que haya: destacar los resultados, analizar los datos desde ese grupo y ver sus detalles. En este último se detallan las páginas, el número de usuario que han pasado por ellas y el número de usuarios que se ha ido.

Opciones_Flow

En ‘Site Content’ (contenido del sitio) volvemos a ver datos concretos del comportamiento de los usuarios en nuestro site. Este menú ofrece más opciones permitiendo ver todas las páginas (‘All Pages’) visitadas y los datos de siempre (página más vista, número de visitas de cada página, tiempo de permanencia…).

También podemos filtrar por contenido (‘Content Drilldown’), es decir, por directorios. Si por ejemplo tenemos un menú llamado servicios en nuestro site, podremos ver cuánta gente accede y después analizar qué servicio de la lista les interesa más o menos.

La opción ‘Landing Pages’ indica cuántos usuarios entran a través de la home o a través de otras páginas: un tutorial, un post…

Por último, podremos analizar las páginas a través de las cuales se van nuestros usuarios (‘Exit Pages’). Es importante mencionar que todos los usuarios se acaban yendo del site (no es algo malo), sin embargo, es interesante poder evaluar si es desde una página principal, desde el contacto o desde una página con contenido concreto.

Site Content

Un menú interesante dentro de ‘Comportamiento’ es ‘Site Speed’. En ‘Overview’ podremos consultar si nuestra página es rápida.

  • Tiempo de carga: debe estar en 2 segundos o menos, aunque si tiene muchas imágenes puede aumentar hasta 4 y 5 segundos.
  • Tiempo que tarda en hacer una redirección, debe ser muy rápido.
  • Lookup time: tiempo que el servidor de tu proveedor dominio, tarda en responder a una solicitud de tu dominio o host. Esta medida es útil para evaluar el rendimiento de tu proveedor de dominio.
  • Tiempo medio de conexión al servidor: tiempo que necesita el usuario para conectarse a su servidor.
  • Tiempo medio de respuesta de servidor: tiempo que tarda su servidor en responder a la solicitud de un usuario, incluido el tiempo de red desde la ubicación del usuario a su servidor. Este dato depende del proveedor de hosting, es el tiempo más difícil de bajar.
  • Tiempo medio de descarga de la página: tiempo que tarda tu página en descargarse.
Page-Speed

En ‘Page Timings’ accedemos a un informe más detallado por página donde se muestra el porcentaje de carga a favor o en contra respecto a la media.

‘Speed suggestions’ recomienda mejoras de la velocidad de tu site buscando problemas de rendimiento. Aporta sugerencias específicas para cada página y añade cómo hacerlo desde la parte técnica. Para ello utiliza Google Page Speed.

‘User Timings’ es lo mismo pero desde el punto de vista del usuario. Se deben activar.

El siguiente menú que nos encontramos en ‘Behavour’ es ‘Site Search’ que nos informa de cuántas visitas tenemos que hacen uso del buscador de la página y cuántas no (‘Usage’), así como los términos de búsqueda (‘Search Terms’) o en qué páginas han usado el buscador (‘Search Pages’).

Si lo que queremos es saber las interacciones de los usuarios con el contenido de nuestro site, debemos acudir al menú ‘Events’. Estas interacciones pueden ser de diferentes índoles: botones de play, gadgets, clics en anuncios… y todo aquello que queramos medir como tal.

Un evento tiene los siguientes componentes que introducimos en el código de nuestra página y que aparecerán en todos los informes:

  • Category: nombre para agrupar eventos. Ej. Formulario.
  • Action: nombre del tipo de evento. Ej. Suscripción.
  • Label (opcional pero recomendado): información adicional del evento. Ej. Newsletter.
  • Value (opcional): valor numérico.

Como siempre, tendremos una vista general en ‘Overview’. También dispondremos de un menú ‘Top Events’ que recoge los eventos al detalle y otro menú, ‘Pages’, que da la información según la página en la que sucede el evento. Por último, ‘Events Flow’ mostrará el flujo de interacción con los eventos igual que hemos visto en otros flujos anteriormente.

Los datos que muestran todos los informes son:

  • Total Events: número total de eventos registrados.
  • Unique Events: número de eventos únicos, sin contar los duplicados.
  • Event Value: mide el campo ‘value’ de nuestro evento.
  • Avg. Value: indica el ‘value’ promedio de todos los eventos.
  • Session with Event: número de visitas que incluyen eventos.
  • Event/Session with Event: promedio de eventos por visita.
Por último, si estamos utilizando AdSense o Ad Exchange, debemos enlazar con analytics y podremos ver los informes correspondientes (ingresos totales diarios, qué páginas de nuestro site o dominios externos contribuyen más a nuestros ingresos…) en el menú ‘Publisher’ o ‘Editor’.

7. Conclusiones

Google Analytics es una herramienta muy potente con la que podremos evaluar quiénes son nuestros usuarios y cuántas sesiones abren en nuestro site, de dónde vienen, a dónde van y qué hacen o cómo se comportan. Igualmente, podemos extraer información del propio comportamiento de nuestra web. Todos estos datos se desglosan en informes acompañados de gráficas que facilitan su interpretación. Además, podemos utilizar filtros y segmentos de búsqueda para enfocar nuestro análisis, así como seguir la ruta que ha seguido nuestro usuario hasta irse de nuestra página. Todos estos datos nos permiten tomar decisiones que mejoren tanto el rendimiento de nuestro site, como su calidad.

8. Referencias

Cómo liberar/distribuir versiones de proyectos Maven+Java con submódulos Git en un entorno CI

$
0
0

El objetivo de este tutorial es explicar cómo hemos resuelto una serie de dificultades técnicas en el proceso de liberación y distribución de un proyecto Java cuya arquitectura está basada en submódulos GIT en un entorno genérico de integración continua.

Índice de contenidos

1. Introducción

Siempre hablando desde un punto de vista genérico, uno de los aspectos que no puede faltar en cualquier proyecto, normalmente Java en nuestro caso, es un entorno de integración continua (CI) que nos permita orquestar una serie de tareas/pasos sobre nuestro código fuente para conseguir un producto final.

De entre estos pasos, cabe destacar la importancia que tiene la liberación y distribución de nuevas versiones a medida que se va cerrando la funcionalidad desarrollada. Este paso, como la mayoría de vosotros sabéis, es el responsable de construir y empaquetar nuestra aplicación, la almacena y la etiqueta en nuestro repositorio de código y además la distribuye a través del repositorio de librerías que tengamos configurado para tal efecto.

Una manera muy habitual y sencilla de resolver está situación es hacer uso de Jenkins y la multitud de plugins a nuestra disposición, acompañado de la configuración de Maven necesaria a nivel de proyecto si fuese necesario. En este contexto, es también muy habitual que todo nuestro código fuente esté alojado en un sólo repositorio, independientemente de los módulos que lo conformen, pero ¿qué ocurre si cada uno de los módulos de nuestro proyecto es un repositorio Git independientemente?, ¿se comportarán igual nuestras herramientas en el entorno de CI a la hora de liberar nuevas versiones?, ¿será igual la configuración de nuestro proyecto y nuestro entorno de CI al trabajar con repositorios Git independientes en el proceso de liberación y distribución de nuevas versiones de nuestra aplicación?

La respuesta es “NO”, y por ese motivo os propongo una alternativa sencilla pero para la que habrá que tener en cuenta algunos detalles, si no queréis dedicar más tiempo de lo normal a preparar vuestro entorno de CI para liberar versiones de vuestros proyectos Maven+Java+Submódulos Git.

2. Entorno

El tutorial está escrito usando el siguiente entorno:
  • Hardware: Portátil MacBook Pro Retina 15′ (2.3 Ghz Intel Core I7, 16GB DDR3).
  • Sistema Operativo: Mac OS Sierra 10.12.5
  • Entorno de desarrollo: Eclipse Java EE IDE for Web Developers.Version: Neon.2 Release (4.6.2)
  • Maven 3.5.0
  • Wget 1.19.1
  • Docker for Mac
    • Docker 17.03.1-ce
    • Docker Machine 0.10.0
    • Docker Compose 1.11.2
  • Dockerización de herramientas
    • Gitlab CE
    • Jenkins 2.46.3
    • SonarQube 6.3
    • Nexus 3

3. El problema

El problema, siempre hablando en términos generales, es que la conjunción de plugins a nivel de Maven+Jenkins más utilizados para generar y distribuir las nuevas versiones de aplicaciones Java/Maven como Maven Release Plugin y M2 Release Plugin no saben gestionar, o al menos no saben manejar del todo, los distintos repositorios que conforman un proyecto en cuya arquitectura se esta trabajando con sub-módulos de GIT pero, ¿por qué?, ¿existe alguna diferencia entre en el flujo de liberación de versiones entre proyectos con un solo repositorio y aquellos que trabajan con múltiples repositorios?

En realidad, la diferencia no está en el flujo de liberación de versiones, sino en cómo gestionan este flujo las herramientas/plugins que utilizamos en el proceso. Es importante tener en cuenta que, cuando trabajamos con sub-módulos de GIT, cualquier cambio en cualquiera de los sub-módulos que conforman el proyecto, que a su vez son sub-módulos de GIT y que disponen de su repositorio independiente, genera cambios por defecto en “unstage” en el proyecto “padre/parent”, que también dispone de su propio repositorio de código y que también es necesario versionar.

En este contexto, el problema básicamente es que Maven Release Plugin, a pesar de ejecutar de manera completa el ciclo de vida de release para los sub-módulos, no interactúa con el repositorio de código del proyecto “padre/parent”, por lo que no se hacen los commit & push de los cambios realizados y se impide que la build de release finalice satisfactoriamente, lo que provoca que no se realice la distribución de la nueva versión de la aplicación.

Lamentablemente no podemos cambiar el comportamiento de ninguno de los actores involucrados (Git Submodule, Maven Release Plugin, M2 Release Plugin), al menos de manera sencilla, por lo que hemos tenido que buscar una solución alternativa que paso a explicar a continuación.

4. La solución

Bueno, sin duda la primera opción fue buscar algún plugin que realizase la gestión de los cambios del proyecto “padre/parent” por nosotros. Después de una primera investigación, lo cierto es que no encontramos ningún plugin que realmente se ajustase a las necesidades de una arquitectura donde se están gestionando múltiples repositorios de GIT para el mismo proyecto, aunque se trate de módulos distintos. Esto no significa que no existan, así que, si conocéis algún plugin que permita versionar una única aplicación que aloja el código fuente en distintos repositorios de código, por favor no dudéis en poner un comentario.

Mientras tanto, y dado que conocemos a la perfección cuál es el ciclo de vida de Maven a la hora de liberar una versión lo más rápido y sencillo es:

  • Escribir un shell script.

    Script que se encargará de ir ejecutando las distintas acciones necesarias para liberar y distribuir las nuevas versiones.

  • Cambio de Maven Release Plugin por Maven Versions Plugin más Maven Help Plugin en Maven.

    Plugins sobre los que se apoyará el proceso de liberación de versión para realizar los cambios de las versiones de desarrollo por la releases y de los cambios de la release por la nueva versión de desarrollo.

  • Cambio de M2 Release Plugin por Release Plugin en Jenkins.

    Mientras M2 Release Plugin nos permite únicamente ejecutar comandos de Maven, Release plugin es más completo, permite realizar una mayor configuración. Por ejemplo, nos permitirá ejecutar el shell script encargado del proceso de liberación y distribución de versiones.

4.1 Shell Script

Por favor, tened en cuenta que esto es solo un ejemplo basado en un proyecto con la siguiente estructura:



Podemos observar que el proyecto está conformado por 3 proyectos Maven, un parent con dos módulos que se gestionarán con esta jerarquía en el repositorio Maven. Sin embargo, cada uno de estos proyectos Maven se gestionan a nivel de repositorio de código de manera independiente, haciendo uso de los submódulos de GIT.

Como muchos de vosotros ya sabréis, el ciclo de vida del plugin Maven Release Plugin es:

  • Se modifican los pom.xml de cada proyecto con la versión a liberar.
  • Se hace commit & push con los cambios de la nueva versión.
  • Se etiqueta esta versión en el repositorio.
  • Se hace un deploy de la versión liberada que distribuirá la releases en el almacén de librerías configurado.
  • Se modifican los pom.xml de cada proyecto con la versión SNAPSHOT que corresponda para seguir desarrollando.
  • Se hace commit & push con los cambios de la nueva versión de desarrollo.

Por tanto, el camino a seguir es claro, necesitamos un shell script que realice todo el proceso teniendo en cuenta que cada módulo esta alojado en su repositorio de manera independiente.

Una buena aproximación podría ser:

#!/bin/bash
echo "********** MAIN MODULE - Change all project to new version *******************"

cd /workspace/autentia-tutorial/
mvn -N versions:update-child-modules versions:set -DgroupId=com.autentia -DartifactId=tutoriales -DnewVersion=$1 -DnextSnapshot=false
mvn versions:commit


projectVersion=$(mvn org.apache.maven.plugins:maven-help-plugin:2.2:evaluate -Dexpression=project.version|grep -Ev '(^\[|Download\w+:)')
echo " VERSION TUTORIAL PROJECT ======= ${projectVersion}"


echo "********** SECONDARY SUBMODULES - commit push and tag ***************"

cd /workspace/autentia-tutorial/core
git branch -f master HEAD
git checkout master

git add .
git commit -m "Autentia new tutorial release from version plugin"
git push -f origin master
git tag $projectVersion
git push -f origin $projectVersion


cd /workspace/autentia-tutorial/services
git branch -f master HEAD
git checkout master

git add .
git commit -m "Autentia new tutorial release from version plugin"
git push -f origin master
git tag $projectVersion
git push -f origin $projectVersion


echo "********** MAIN MODULE - commit push and tag  *******************"
cd /workspace/autentia-tutorial/
git branch -f master HEAD
git checkout master

git add .
git commit -m "Autentia new tutorial release from version plugin"
git push -f origin master
git tag $projectVersion
git push -f origin $projectVersion


echo "********** DEPLOY NEW VERSION FOR ALL MODULES  *******************"
cd /workspace/autentia-tutorial/
mvn deploy


echo "********** MAIN MODULE - change all project to new deployment version *******************"
cd /workspace/autentia-tutorial/
mvn -N versions:update-child-modules versions:set -DgroupId=com.autentia -DartifactId=tutorial -DnewVersion=$2 -DnextSnapshot=false
mvn versions:commit


echo "********** SECONDARY SUBMODULES - commit push ***************"
cd /workspace/autentia-tutorial/core
git branch -f master HEAD
git checkout master

git add .
git commit -m "Autentia new development release from version plugin"
git push -f origin master


cd /workspace/autentia-tutorial/services
git branch -f master HEAD
git checkout master

git add .
git commit -m "Autentia new development release from version plugin"
git push -f origin master


echo "********** MAIN MODULE - commit push  to*******************"
cd /workspace/autentia-tutorial/
git branch -f master HEAD
git checkout master

git add .
git commit -m "Autentia new development release from version plugin"
git push -f origin master


echo "********** DEPLOY NEW DEVELOPMENT VERSION FOR ALL MODULES si queremos distribuir la nueva versión de desarrollo  *******************"
cd /workspace/autentia-tutorial/
git checkout master
mvn deploy

Si realizáis una primera lectura creo que no tendréis ningún problema en ver que se está haciendo:

  • Cambio de todos los pom.xml del proyecto.
  • Nos apoyamos en Maven Version Plugin para realizar está acción. Destaca que en el comando de Maven utilizado para tal efecto se hace uso del parámetro -DnewVersion=$1. El valor del parámetro será sustituido por el valor que incluyamos para la nueva versión desde Jenkins a través de Releases Plugin.

    Adicionalmente, en este primer paso del proceso nos apoyamos en Maven Help Plugin para recuperar la versión que acaba de ser incluida en los pom.xml, ya que nos servirá posteriormente para etiquetar nuestra releases.

  • Se hace commit & push & tag de los cambios para cada sub-módulo del proyecto.
  • En este paso del proceso hay dos acciones muy importantes que os pueden dar un quebradero de cabeza.

    • git branch -f master HEAD
    • git checkout master

    La primera acción fuerza a que el HEAD del repositorio local de GIT apunte contra la rama master. Por norma general, trabajando con un único repositorio, la referencia del puntero HEAD siempre es a la rama master. Sin embargo, al trabajar con submódulos de GIT esto no siempre ocurre y si se pierde la referencia estaríamos realizando las posteriores acciones sobre el HEAD y no sobre la rama master.

  • Se hace commit & push & tag de los cambios para proyecto padre.

  • Se hace deploy para todos los proyectos.

    Se encarga de distribuir la nueva versión de cada artefacto en el repositorio de librerías configurado para tal efecto. Por ejemplo, Nexus es una muy buena opción para distribuir las nuevas versiones de nuestras aplicaciones.

A partir de aquí, el proceso es muy similar solo que el resultado será que todos los módulos de nuestro proyecto incluirán la nueva versión SnapShot de desarrollo después de liberar la release.

4.2 Cambio Plugin en Maven

Este apartado, la verdad, no tiene mucho misterio. Anteriormente, y casi de manera tradicional casi siempre he utilizado Maven Release Plugin. Sin embargo y dado que ahora será el shell script el encargado de gestionar el proceso de liberación y distribución de versión no es necesario este plugin y sí hacer uso de Maven Version Plugin que, como hemos podido comprobar, es sobre el que se apoya el script para realizar todos los cambios de versión en todos los proyectos/módulos de la aplicación.

4.3 Cambio Plugin en Jenkins

Del mismo modo que el apartado anterior, la sustitución en Jenkins de un plugin por otro no tiene mucho misterio. Por tanto, una vez instalado el Release Plugin solo será necesario ir a la configuración de nuestro job, al apartado “Entorno de ejecución”, click sobre el check “Configure release build”. En este punto incluimos los parámetros necesarios que viajarán desde el inicio de liberación de versión hasta el shell script encargado del proceso.

Adicionalmente, añadimos un paso posterior a la ejecución de la build en el apartado “After successful release build” que se encargará de lanzar el script con las parámetros que correspondan, como se puede apreciar en la siguiente imagen.

Esta acción, que lanzará la ejecución del shell script, se iniciará cuando desde el backend del plugin se lance un nuevo proceso de liberación de versión. En este punto se solicita la nueva versión y la siguiente versión de desarrollo y el valor que se introduzca será utilizado por el shell script encargado del proceso.

5. Conclusiones

Bueno, mi conclusión respecto a este tema es bastante clara, si no es absolutamente imprescindible o no es una imposición a nivel de proyecto, no dispongáis vuestra aplicaciones de modo que cada módulo este alojado en un repositorio de código distinto. La verdad es que no he encontrado ninguna ventaja trabajando bajo este contexto, al revés, todo han sido inconvenientes, y no solo a la hora de liberar y distribuir versiones sino también en la gestión de cambios en el día a día, que se hace mucho menos sencilla.

Al final, la independencia de repositorios modifica el proceso habitual de algunos procesos habituales en el desarrollo y, la verdad, es que esto es algo con lo que no estoy muy de acuerdo si no es para agilizar estos procesos.

Espero que os sirva de utilidad.

Un cordial saludo

Viewing all 996 articles
Browse latest View live