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

Introducción a NativeScript

$
0
0

Ya son varias las charlas que se han ido dando en Autentia a lo largo de los años. Hoy os queremos contar brevemente y para que sepáis lo que internamente se realiza, la charla de Daniel Otero. Introducción a NativeScript

.

Introducción a NativeScript, el framework Javascript para la creación de Apps para iOS y Android nativas utilizando la misma base de código escrito en lenguaje Javascript.

NativeScript

NativeScript1

En la charla se explica las posibilidades que nos da el framework, cual es la base tecnológica sobre la que está construido el mismo, cuales son sus características principales y como empezar a utilizarlo en nuestros proyectos.

También se ha hablado sobre la integración con Angular 2, y por qué está integración está llamada a ser el futuro del framework. Se habla sobre las diferencias y similitudes hay entre utilizar NativeScript con y sin Angular 2, y también sobre que diferencias y similitudes hay entre una aplicación web que utilice Angular 2 y una aplicación en NativeScript + Angular.

NativeScript+angular


Introducción a NativeScript

$
0
0

Ya son varias las charlas que se han ido dando en Autentia a lo largo de los años. Hoy os queremos contar brevemente y para que sepáis lo que internamente se realiza, la charla de Daniel Otero. Introducción a NativeScript.

Introducción a NativeScript, el framework Javascript para la creación de Apps para iOS y Android nativas utilizando la misma base de código escrito en lenguaje Javascript.

En la charla se explica las posibilidades que nos da el framework, cual es la base tecnológica sobre la que está construido el mismo, cuales son sus características principales y como empezar a utilizarlo en nuestros proyectos.

También se ha hablado sobre la integración con Angular 2, y por qué está integración está llamada a ser el futuro del framework. Se habla sobre las diferencias y similitudes hay entre utilizar NativeScript con y sin Angular 2, y también sobre que diferencias y similitudes hay entre una aplicación web que utilice Angular 2 y una aplicación en NativeScript + Angular.

Integración de MyBatis con Spring Boot y Cache con Redis

$
0
0

En este tutorial vamos a ver cómo integrar MyBatis con Spring Boot y como usar la caché de MyBatis sobre Redis.

0. Índice de contenidos


1. Introducción

El objetivo que perseguimos con el presente tutorial, es integrar el uso de MyBatis a través de los starters de Spring Boot, ya que hasta hace poco, no disponíamos de un starter dedicado a MyBatis y teníamos que configurarlo de la manera tradicional.

Posteriormente, veremos como usar la caché de segundo nivel que ofrece Mybatis con el soporte de Redis, tenéis más detalles del funcionamiento de la caché de MyBatis en Caché MyBatis.


2. Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: MacBook Pro 15' (2.3 GHz Intel Core i7, 16GB DDR3 SDRAM)
  • Sistema Operativo: Mac OS X El Capitan 10.11
  • Docker 1.11.1
  • Spring Boot 1.3.5
  • MyBatis 3.4.0
  • mybatis-spring-boot-starter 1.1.1
  • Redis 3.2.0
  • mybatis-redis-cache 1.0.0-beta2

3. Integrar MyBatis con Spring Boot

Como hemos comentado anteriormente Spring Boot no nos proporciona un starter dedicado a MyBatis y ha sido la gente de MyBatis la que ha desarrollado un starter con el que podemos integrar y configurar facilmente este framework.

El primer paso que deberíamos realizar es incluir la dependencia en nuestro proyecto

<dependency>
      <groupId>org.mybatis.spring.boot</groupId>
      <artifactId>mybatis-spring-boot-starter</artifactId>
      <version>1.1.1</version>
    </dependency>

Como ya habéis podido ver en los diferentes tutoriales publicados sobre MyBatis los elementos fundamentales que hay que configurar para trabajar con MyBatis son: SqlSessionFactoryBean, SqlSessionFactory, SqlSessionTemplate y los diferentes Mappers.

Repasemos lo que realiza mybatis-spring-boot-starter

  • Comprueba la existencia de un DataSource.
  • Crea una instancia de SqlSessionFactoryBean, pasandole como argumento el DataSource y que será la encargada de instanciar SqlSessionFactory
  • Crea una instancia del SqlSessionTemplate a través del SqlSessionFactory
  • Busca los Mappers dentro de nuestra aplicación, les asocia el SqlSessionTemplate y los registra para poder ser inyectados en nuestras clases de negocio.

Como en el resto de starters de Spring Boot podemos añadir una serie de parámetros de configuración dentro del fichero application.properties, usando el prefijo ‘mybatis’

  • mybatis.config-location: Localización del fichero mybatis-config.xml.
  • mapper-locations: Localización de los ficheros xml que representan a los ‘mappers’.
  • type-aliases-package: Paquete donde localizar los ‘alias’ de tipo.
  • type-handlers-package: Paquete donde localizar los ‘alias’ de los handlers.
  • executor-type: Tipo de ejecutor usado por MyBatis SIMPLE, REUSE, BATCH

Podéis ver los fuentes de este tutorial aquí, de todas formas vamos a repasar los aspectos más importantes:

Nuestro 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</groupId>
      	<artifactId>spring-boot-mybatis-demo</artifactId>
      	<version>0.0.1-SNAPSHOT</version>
      	<packaging>jar</packaging>

      	<name>spring-boot-mybatis-demo</name>
      	<description>Demo de la integracion de MyBatis con Spring Boot</description>

      	<parent>
      		<groupId>org.springframework.boot</groupId>
      		<artifactId>spring-boot-starter-parent</artifactId>
      		<version>1.3.5.RELEASE</version>
      		<relativePath/>
      	</parent>

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

      	<dependencies>
      		<!-- Starter MyBatis -->
      		<dependency>
      			<groupId>org.mybatis.spring.boot</groupId>
      			<artifactId>mybatis-spring-boot-starter</artifactId>
      			<version>1.1.1</version>
      		</dependency>
              <!-- H2 BBDD -->
      		<dependency>
      			<groupId>com.h2database</groupId>
      			<artifactId>h2</artifactId>
      			<scope>runtime</scope>
      		</dependency>
              <!-- Test-->
      		<dependency>
      			<groupId>org.springframework.boot</groupId>
      			<artifactId>spring-boot-starter-test</artifactId>
      			<scope>test</scope>
      		</dependency>
      		<dependency>
      			<groupId>org.hamcrest</groupId>
      			<artifactId>hamcrest-all</artifactId>
      			<version>1.3</version>
      		</dependency>
      		<dependency>
      			<groupId>junit</groupId>
      			<artifactId>junit</artifactId>
      		</dependency>
      	</dependencies>

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

      </project>

A parte de la dependencia con mybatis-spring-boot-starter, destacamos la dependencia con H2 para crear una BBDD en memoria sobre la que lanzaremos nuestros Test de Integraci&oacuten. Esta BBDD se creará a partir de un fichero *.sql que le indicaremos en el application.properties

Dentro del directorio src/main/resources vamos a incluir el fichero de propiedades application.properties que tendrá el siguiente aspecto:

# Script de Inicialización de BBDD
spring.datasource.schema=schema.sql

#Fichero de Configuración de MyBatis
mybatis.config-location=mybatis-config.xml

# Configuración de Logs
logging.level.root=WARN
logging.level.com.autentia.mappers=TRACE

El parámetro spring.datasource.schema indica el fichero .sql utilizado para crear y poblar la BBDD.

mybatis.config-location como hemos visto anteriormente indica la ubicación del fichero de configuración de MyBatis.

logging.level.root y logging.level.com.autentia.mappers indican el nivel de traza.

El fichero de configuración de MyBatis (mybatis-config.xml)

<?xml version="1.0" encoding="UTF-8" ?>
  <!DOCTYPE configuration
          PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
          "http://mybatis.org/dtd/mybatis-3-config.dtd">
  <configuration>
      <typeAliases>
          <package name="com.autentia.model"/>
      </typeAliases>
      <mappers>
          <mapper resource="com/autentia/mappers/CourseMapper.xml"/>
      </mappers>
  </configuration>

Como se puede observar, hemos declarado el fichero xml que define un mapper CourseMapper.xml y un paquete com.autentia.model que se utilizará para localizar los alias de tipo que usaremos en los mappers.

Vamos a ver el contenido de schema.sql

-- Create Table
    create table courses (name varchar, credits int);

    -- Insert data
    insert into courses (name, credits) values ('Angular 2', 1);
    insert into courses (name, credits) values ('PrimeFaces', 2);

Este script no presenta mucha complicación simplemente crea una tabla ‘Courses’ y la puebla con 2 registros. Ahora sólo nos falta crear la infraestructura relativa a los ‘mappers’, en primer lugar creamos la interfaz CourseMapper en la que definimos nuestros métodos de acceso a BBDD:

@Mapper
public interface CourseMapper {

    List getCourses();
}

y el fichero asociado CourseMapper.xml

<!DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

    <mapper namespace="com.autentia.mappers.CourseMapper">

        <select id="getCourses" resultType="Course">
            select * from courses
        </select>

    </mapper>

En este fichero vincularemos el método getCourses definido en la interfaz con la query select * from courses para devolver objetos de tipo Course.

Para probar el correcto funcionamiento creamos un test de integración CourseMapperIntegrationTest con el siguiente contenido:

@RunWith(SpringJUnit4ClassRunner.class)
      @SpringApplicationConfiguration(classes = SpringBootMyBatisIntegrationDemo.class)
      public class CourseMapperIntegrationTest {

          @Autowired
          private CourseMapper courseMapper;

          @Test
          public void getCourses() throws Exception {
              List courses = courseMapper.getCourses();
              MatcherAssert.assertThat(courses, hasSize(2));
          }

      }

El test comprueba que existen 2 cursos en nuestra BBDD, lo lanzamos desde nuestro IDE y debería pasar sin problemas

01_spring_boot-mybatis

Sencillo no???


4. Uso de la caché de MyBatis con Redis

En este apartado vamos a ver como podemos activar la caché de segundo nivel de MyBatis para que use como soporte Redis (todos los detalles sobre la caché de MyBatis en el tutorial de mi compañero Jose Luis Caché MyBatis) .

El primer paso es añadir la siguiente dependencia a nuestro pom.xml

<!-- Mybatis Redis Cache -->
    <dependency>
      <groupId>org.mybatis.caches</groupId>
      <artifactId>mybatis-redis</artifactId>
      <version>1.0.0-beta2</version>
    </dependency>

El siguiente paso es añadir el soporte de caché a nivel de mapper para que use redis, para ello incluimos lo siguiente en nuestro CourseMapper.xml

<cache type="org.mybatis.caches.redis.RedisCache"/>

Por último creamos el fichero de configuración en el que especificamos la url y puerto de nuestro servidor de Redis

host=192.168.99.100
port=6379

Pero, falta algo no ??? Efectivamente nos falta un servidor de Redis y como siempre Docker aparece al rescate ;).

docker run -d --name redis_cache -p 6379:6379 redis

Este comando levanta un contenedor con redis y expone el puerto 6379 para que podamos acceder a él. Entramos en el contenedor ejecutando lo siguiente:

docker exec -it redis_cache bash

Una vez dentro del contenedor redis, ejecutamos el cliente redis-cli y ejecutamos monitor para activar el monitor de redis, y poder ver las operaciones que se realicen contra él.

02_spring_boot_redis_cache

Lanzamos la prueba de nuevo y observamos lo que ocurre en el monitor de redis:

03_spring_boot_redis_cache

Como podemos observar por el monitor, se han realizado 2 operaciones: la primera HGET para ver si la consulta estaba cacheada en redis y posteriormente una HSET para insertar los valores devueltos por la consulta a BBDD.

Volvemos a ejecutar la consulta:

04_spring_boot_redis_cache

Como podemos comprobar, se ha realizado únicamente una operación HGET para recuperar los valores de la caché, sin necesidad de acceder a la BBDD. Lo vemos más claro en los logs que presenta nuestro IDE, en esta ejecución no aparecen los logs relativos a la consulta realizada con MyBatis

05_spring_boot_redis_cache


5. Conclusiones

Como hemos podido comprobar, Spring Boot, a través de los starters nos facilita enormemente la creación y configuración de un nuestras aplicaciones, dejándonos más tiempo para centrarnos en las funcionalidades de negocio. (Podéis consultar el catálogo en el siguiente enlace Spring Boot Starters)

Por otro lado hemos visto lo fácil que resulta la activación de la caché de segundo nivel de MyBatis con el soporte de Redis, esta librería está en versión 1.0.0-beta2 y aún le falta el soporte para RedisCluster, pero por lo que hemos estado viendo en su repo están en ello.

Un saludo.


6. Referencias

Introducción a ZooKeeper

$
0
0

En este tutorial vamos a presentar ZooKeeper, el proyecto de Apache que nos provee de un servicio centralizado para diversas tareas como: Mantenimiento de configuración, naming, sincronización distribuida o servicios de agrupación.

Índice de contenidos

ZooKeeper

1. Introducción

ZooKeeper es un proyecto de Apache que nos provee de un servicio centralizado para diversas tareas como por ejemplo mantenimiento de configuración, naming, sincronización distribuida o servicios de agrupación, servicios que normalmente son consumidos por otras aplicaciones distribuidas.

Veremos cuales son las características de ZooKeeper y que nos garantiza, su estructura general, sus operaciones básicas y echaremos un vistazo rápido para ver como funciona internamente.

Para finalizar el tutorial, mostraremos un pequeño proyecto donde implementaremos las operaciones básicas de un cliente de ZooKeeper.

¡Manos a la obra!

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 Yosemite 10.11.4
  • Entorno de desarrollo: IntelliJ IDEA 2016.1
  • Apache Maven 3.3.0
  • JDK 1.8.0_60

3. Características y Garantías

Cuatro son las principales características de ZooKeeper:

  • Sencillo: Permite la coordinación entre procesos distribuidos mediante un namespace jerárquico que se organiza de manera similar a un file system. El namespace consiste en registros (znodes) similares a ficheros o directorios. En contraposición con el típico file system, ZooKeeper mantiene la información en memoria permitiendo obtener latencias bajas y rendimientos altos.
  • Replicable: Permite las replicas instaladas en múltiples hosts, llamados conjuntos o agrupaciones. Los servidores que conforman el servicio ZooKeeper deben conocerse todos entre ellos. Estos mantienen una imagen en memoria del estado, completado con un log de transacciones y snapshots almacenados en un store persistente. Mientras la mayoría de servicios este disponible, ZooKeeper se mantendrá disponible.
  • Ordenado: ZooKeeper se encarga de marcar cada petición con un número que refleja su orden entre todas las transacciones de manera que permite mantener por completo la trazabilidad de las operaciones realizadas.
  • Rápido: Sobre todo en entornos en los que predominen las operaciones de lectura.

Servicio replicado ZooKeeper

Por otro lado, ZooKeeper nos garantiza los siguientes aspectos:

  • Secuencialidad: Asegura que las operaciones de actualización se aplican en el mismo orden en el que se envían.
  • Atomicidad: No existen los resultados parciales, las actualizaciones fallan o son un éxito.
  • Único endpoint: Los clientes verán siempre el mismo servicio independientemente del servidor que lo contenga.
  • Seguridad: En el momento en el que una actualización se ha aplicado, dicha modificación se mantendrá hasta el momento en que un cliente la sobreescriba.
  • Actualización: El cliente tiene garantizado que, en un determinado intervalo temporal, los servicios estarán actualizados.

4. Modelo

Como hemos comentado con anterioridad, el modelo de datos que ZooKeeper permite definir es similar a un sistema de ficheros, en el que se pueden definir nodos de manerar jeraráquica: los Znodos.

Detalle nodos

Los Znodos mantienen información estadística que incluye: número de versión, cambios ACL (Access Control List) y timestamps que permitiran crear validaciones de caché y actualizaciones coordinadas. Cada vez que la información de un Znodo se modifica, su número de versión aumenta.

La información almacenada de cada Znodo que se encuentre en el namespace se lee y escribe de manera automática.


5. API

5 son las operaciones básicas que nos ofrece ZooKeeper:

  • create path data : Crea un nodo con una información (data) y en una localización (path) dados.
  • delete path: Elimina un nodo en una localización (path) dada.
  • exists path: Prueba si un nodo existe en una localización (path) dada.
  • get path [watch]: Lee la información de un nodo en una localización (path) dada. De manera opcional se puede establecer un monitor (watch) que notifique cuando se produzca un cambio.
  • set path data [version]: Escribe información (data) en un nodo localizado (path). De manera opcional se puede hacer uso de la versión del nodo [version] que se quiere modificar.
  • sync path: Espera el cambio en un nodo localizado (path) para propagarlo.

6. Implementación

A continuación se muestran los componentes a alto nivel del servicio ZooKeeper. Con la excepción del Request Processor, cada servidor que conforma el servicio ZooKeeper replica su propia copia de cada componente.

Servicio

La base de datos se encuentra replicada en cada servidor, cada copia es una base de datos en memoria que contiene por completo el árbol de nodos. Cuando se reciben actualizaciones en primer lugar se almacenan serializadas en disco para poder recuperar el estado en caso necesario y, con posterioridad, se aplican sobre la base de datos.

Todos los servidores ZooKeeper proveen a los clientes, pero un cliente se conecta unicamente a un servidor para realizar las peticiones. Las peticiones de lectura se responden desde la replica local de la base de datos de cada servidor. Las peticiones que cambian el estado del servicio, las de escritura, se procesan bajo un protocolo de aceptación.

Como parte del protocolo de aceptación, todas las peticiones de escritura que provengan de los clientes se reenvían a un servidor denominado “lider”, el resto de los servidores, los “seguidores”, reciben las peticiones de mensajes del lider y aceptan acordar la entrega de mensajes. La capa de mensajería se encarga de reemplazar al lider en caso de error y sincronizar los seguidores con el lider.


7. Instalación ZooKeeper

A continuación se detallan los pasos para proceder con la instalación de un servidor standalone ZooKeeper en nuestros equipos Mac.

  1. Descargar la última release estable de ZooKeeper de su página oficial.
  2. Descomprimir el fichero descargado en el path deseado.
  3. Abrir un terminal y acceder al path de instalación de ZooKeeper que se establecio en el punto anterior.
  4. Acceder a la carpeta ./bin
  5. Lanzar el comando “./zkServer.sh start”

De esta manera habremos arrancado el servidor de ZooKeeper que se quedará a la espera de recibir mensajes.

8. Cliente Java ZooKeeper

A continuación se muestra, a modo de ejemplo, una clase para gestionar las principales acciones a realizar con un servidor de ZooKeeper:

@Component
public class ZKClientManager implements ZKManager {

    private static final Logger LOGGER = LoggerFactory.getLogger(ZKClientManager.class);

    private static ZooKeeper zkeeper;

    private static ZKConnection zkConnection;

    public ZKClientManager() {
    }

    @PostConstruct
    private void initialize() {
        try {
            this.zkConnection = new ZKConnection();
            this.zkeeper = zkConnection.connect("localhost");
        } catch (IOException | InterruptedException e) {
            LOGGER.error(" =========> {}",e.getMessage());
        }
    }

    public void closeConnection() {
        try {
            this.zkConnection.close();
        } catch (InterruptedException e) {
            System.out.println(e.getMessage());
        }
    }

    public void create(String path, byte[] data) throws KeeperException, InterruptedException {
        this.zkeeper.create(path,data, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
    }

    public Stat getZNodeStats(String path) throws KeeperException, InterruptedException {
        Stat stat = zkeeper.exists(path, Boolean.TRUE);

        if (stat != null) {
            LOGGER.info("Node exists and the node version is {}", stat.getVersion());
        } else {
            LOGGER.info("Node does not exists");
        }
        return stat;
    }

    public Object getZNodeData(String path, boolean watchFlag) throws KeeperException, InterruptedException {
        Stat stat = getZNodeStats(path);
        byte[] b = null;
        try {
            if (stat != null) {
                if (watchFlag) {
                    ZKWatcher watch = new ZKWatcher();
                    b = this.zkeeper.getData(path, watch, null);
                    watch.await();
                } else {
                    b = this.zkeeper.getData(path, null, null);
                }
                return new String(b, "UTF-8");
            } else {
                LOGGER.info("Node does not exists");
            }
        } catch (UnsupportedEncodingException e) {
            LOGGER.error(" =========> {}", e.getMessage());
        }
        return null;
    }

    public void update(String path, byte[] data) throws KeeperException, InterruptedException {
        int version = this.zkeeper.exists(path, Boolean.TRUE).getVersion();
        this.zkeeper.setData(path, data, version);
    }

    public List getZNodeChildern(String path) throws KeeperException, InterruptedException {
        Stat stat = getZNodeStats(path);
        List children = null;

        if (stat != null) {
            children = this.zkeeper.getChildren(path, Boolean.FALSE);
            for (int i = 0; i < children.size(); i++) {
                LOGGER.info(children.get(i));
            }
        } else {
            LOGGER.info("Node does not exists");
        }
        return children;
    }

    public void delete(String path) throws KeeperException, InterruptedException {
        int version = this.zkeeper.exists(path, Boolean.TRUE).getVersion();
        this.zkeeper.delete(path, version);
    }

}

El resto del código, además de algún otro ejemplo podéis encontrarlo en github.


9. Conclusiones

Como hemos visto Zookeeper es una herramienta que nos permite realizar múltiples tareas de coordinación de una manera fácil gracias a su sencilla API.

Además, se integra fácilmente con otros servicios como por ejemplo el sistema de almacenamiento distribuido Apache Kafka (como podemos ver en los tutoriales Primeros pasos con Apache Kafka ó Monitorización de Apache Kafka.

10. Referencias

Réplica de datos en MongoDB

$
0
0

En este tutorial, explicaremos los fundamentos de la réplica de datos en MongoDB y configuraremos, a través de un sencillo script, una pequeña prueba de ReplicaSet en nuestra propia máquina.

Índice de contenidos


1. Introducción y Objetivo

La réplica de datos es una de las funcionalidades de MongoDB que nos permite mantener una copia de los datos en varios nodos, de forma que tengamos una mayor tolerancia a que uno de ellos pueda fallar, sin perder los datos que estamos persistiendo o recuperando.

En éste articulo explicaremos cómo configurar la réplica de datos (a través de ReplicaSet) en MongoDB.

A lo largo de la explicación iremos componiendo un Script con las instrucciones para montar, en una máquina, un servicio de MongoDB configurado en modo ReplicaSet con tres instancias.

Para ello se utilizará el propio API de MongoDB y la facilidad que implementa para la configuración por defecto de ReplicaSet.

Los pasos que se presentan en el presente artículo NO consituyen la forma recomendada de configurar ReplicaSet, sino que pretenden permitir probar la funcionalidad de réplica de MongoDB de una forma sencilla, al tiempo que sirve para ilustrar y probar los componentes y la arquitectura de nodos para configurar correctamente la réplica de datos. Por este motivo, esta guía no debe utilizarse como guía para la configuración de entornos de producción.


2. Requisitos

Para poder seguir las instrucciones de esta guía y probar el funcionamiento del ReplicaSet en MongoDB, vamos a utilizar un servidor de MongoDB, que levantará varios procesos mongod en una misma máquina y sobre el que probaremos a insertar datos y comprobar que se replican en el resto de Nodos.

Para todo ello, utilizaremos la consola de Mongo y el API JavaScript que ofrece esta consola.

Por tanto, el único requisito es tener una versión de MongoDB instalada. Se requiere que sea al menos la versión 1.6. Dado que es la primera versión donde el API incluye el objeto ReplSetTest, que es del que nos valdremos para configurar el grupo de réplica para pruebas. Este tutorial ha sido escrito utilizando la version 3.2 de MongoDB.

Para ello, vamos a utilizar la propia consola de Mongo, y las utilidades que permite Mongo a través del API para levantar y configurar un ReplicaSet.

En concreto, utilizaremos la clase ReplSetTest, disponible en el API JS de la shell de MongoDB desde la versión 1.6, del que también podemos consultar online su código fuente.


3. Arranque y acceso a la consola con Mongo Shell

Para iniciar la prueba de ReplicaSet necesitamos arrancar la consola de mongo sin conectar contra ningún servidor en concreto. Esto lo conseguimos con el parámetro –nodb

$ mongo --nodb
MongoDB shell version: 3.2.0
>

Es preferible que no tengamos una instancia de MongoDB previamente arrancada, las distintas instancias que formarán parte del grupo de réplica de prueba se irán levantando durante el proceso del script.


4. Creación del ReplicaSet de ejemplo

Para probar el mecanismo de réplica de MongoDB, necesitaremos crear varias instancias de mongod que actúen como servidores y relacionarlas de forma que todas mantengan una copia de los datos.

Habitualmente esto implica que configuraremos estos procesos de forma externa, pero para facilitar la configuración y arranque de estas instancias, MongoDB incluye en el API Javascript de su shell un objeto ReplSetTest que nos permite levantar estas instancias de una forma directa.

Para arrancarlo crearemos un nuevo objeto ReplSetTest. Como argumento al constructor del objeto podemos pasar un JSON con la configuración que queremos dar al grupo de réplica.

El conjunto de atributos que podemos configurar para este ReplicaSet está descrito en el Anexo A de este tutorial.

> sampleReplicaSet = new ReplSetTest({name : “myReplicaSet”, nodes : 3})

Al ejecutar el comando, por consola nos imprimirá el contenido del objeto ReplSetTest que acabamos de crear. Este efecto se produce porque la consola de mongoDB, por defecto siempre imprime el resultado de la evaluación de la última expresión. En este caso, la evaluación de la última expresión es la propia variable sampleReplicaSet que contiene la configuración del grupo de réplica que vamos a crear.

Si queremos evitar este efecto, podemos crear el ReplSetTest de la siguiente forma:

> sampleReplicaSet = new ReplSetTest({name : “myReplicaSet”, nodes : 3}); print(“done”)
done
>

5. Arrancar los procesos mongod del grupo de réplica

Hasta ahora, sólo hemos configurado el grupo de réplica, pero no hemos iniciado ninguna instancia de los nodos que forma parte de dicho grupo de réplica.

Para arrancar los nodos del grupo de réplica ejecutaremos la función startSet() sobre el objeto que representa el grupo de réplica.
> sampleReplicaSet.startSet()

Como salida del comando veremos la configuración de cada nodo del grupo de réplica y los mensajes de logs que nos indican que están arrancando cada una de las instancias.

> sampleReplicaSet.startSet()
	ReplSetTest Starting Set
	ReplSetTest n is : 0
	{
		"useHostName" : true,
		"oplogSize" : 40,
		"keyFile" : undefined,
		"port" : 20012,
		"noprealloc" : "",
		"smallfiles" : "",
		"replSet" : "myReplicaSet",
		"dbpath" : "$set-$node",
		"restart" : undefined,
		"pathOpts" : {
			"node" : 0,
			"set" : "myReplicaSet"
		}
	}
	ReplSetTest Starting....
	Resetting db path '/data/db/myReplicaSet-0'
	2016-02-24T16:36:48.861+0100 I -        [thread1] shell: started program (sh6276):  mongod --oplogSize 40 --port 20012 --noprealloc --smallfiles --replSet myReplicaSet --dbpath /data/db/myReplicaSet-0 --setParameter enableTestCommands=1
	[...]
	d20012| 2016-02-24T16:36:49.290+0100 I FTDC     [initandlisten] Initializing full-time diagnostic data capture with directory '/data/db/myReplicaSet-0/diagnostic.data'
	d20012| 2016-02-24T16:36:49.290+0100 I NETWORK  [HostnameCanonicalizationWorker] Starting hostname canonicalization worker
	d20012| 2016-02-24T16:36:49.342+0100 I NETWORK  [initandlisten] waiting for connections on port 20012
	d20012| 2016-02-24T16:36:50.074+0100 I NETWORK  [initandlisten] connection accepted from 127.0.0.1:62955 #1 (1 connection now open)
	[ connection to Irensaga-2.local:20012 ]

	ReplSetTest n is : 1
	{
		"useHostName" : true,
		"oplogSize" : 40,
		"keyFile" : undefined,
		"port" : 20013,
		"noprealloc" : "",
		"smallfiles" : "",
		"replSet" : "myReplicaSet",
		"dbpath" : "$set-$node",
		"restart" : undefined,
		"pathOpts" : {
			"node" : 1,
			"set" : "myReplicaSet"
		}
	}
	ReplSetTest Starting....
	Resetting db path '/data/db/myReplicaSet-1'
	2016-02-24T16:36:50.077+0100 I -        [thread1] shell: started program (sh6277):  mongod --oplogSize 40 --port 20013 --noprealloc --smallfiles --replSet myReplicaSet --dbpath /data/db/myReplicaSet-1 --setParameter enableTestCommands=1
	[...]
	20013| 2016-02-24T16:36:50.612+0100 I FTDC     [initandlisten] Initializing full-time diagnostic data capture with directory '/data/db/myReplicaSet-1/diagnostic.data'
	d20013| 2016-02-24T16:36:50.654+0100 I NETWORK  [initandlisten] waiting for connections on port 20013
	d20013| 2016-02-24T16:36:51.283+0100 I NETWORK  [initandlisten] connection accepted from 127.0.0.1:62958 #1 (1 connection now open)
	[
		connection to Irensaga-2.local:20012,
		connection to Irensaga-2.local:20013
	]


	ReplSetTest n is : 2
	{
		"useHostName" : true,
		"oplogSize" : 40,
		"keyFile" : undefined,
		"port" : 20014,
		"noprealloc" : "",
		"smallfiles" : "",
		"replSet" : "myReplicaSet",
		"dbpath" : "$set-$node",
		"restart" : undefined,
		"pathOpts" : {
			"node" : 2,
			"set" : "myReplicaSet"
		}
	}
	ReplSetTest Starting....
	Resetting db path '/data/db/myReplicaSet-2'
	2016-02-24T16:36:51.285+0100 I -        [thread1] shell: started program (sh6279):  mongod --oplogSize 40 --port 20014 --noprealloc --smallfiles --replSet myReplicaSet --dbpath /data/db/myReplicaSet-2 --setParameter enableTestCommands=1
	[...]
	d20014| 2016-02-24T16:36:51.877+0100 I NETWORK  [initandlisten] waiting for connections on port 20014
	d20014| 2016-02-24T16:36:52.493+0100 I NETWORK  [initandlisten] connection accepted from 127.0.0.1:62961 #1 (1 connection now open)
	[
		connection to Irensaga-2.local:20012,
		connection to Irensaga-2.local:20013,
		connection to Irensaga-2.local:20014
	]
	[
		connection to Irensaga-2.local:20012,
		connection to Irensaga-2.local:20013,
		connection to Irensaga-2.local:20014
	]

Durante la inicialización vemos los puertos en los que se va levantando cada uno de los nodos. En este caso, y por defecto, se levantan en los puertos 20012, 20013 y 20014.

A partir de aquí, es posible que veamos aparecer trazas en esta consola, ya que por defecto, la salida de los tres nodos que se han arrancado con ReplSetTest se volcará en esta shell.


6. Arrancar el proceso de réplica

En este punto, tenemos los tres procesos de servicio mongod que forman parte de nuestro grupo de réplica, pero no ha arrancado todavía la funcionalidad de réplica de datos.

Para activar la réplica tendremos que invocar la funcion initiate() sobre el ReplSetTest.

> sampleReplicaSet.initiate()

Al ejecutar la función, veremos como salida de consola nos muestra la configuración de miembros del réplica set y a continuación se activa la funcionalidad de réplica en el grupo

> sampleReplicaSet.initiate()
	{
		"replSetInitiate" : {
			"_id" : "myReplicaSet",
			"members" : [
				{
					"_id" : 0,
					"host" : "Irensaga-2.local:20012"
				},
				{
					"_id" : 1,
					"host" : "Irensaga-2.local:20013"
				},
				{
					"_id" : 2,
					"host" : "Irensaga-2.local:20014"
				}
			]
		}
	}
	d20012| 2016-02-24T17:16:10.515+0100 I REPL     [conn1] replSetInitiate admin command received from client
	d20013| 2016-02-24T17:16:10.519+0100 I NETWORK  [initandlisten] connection accepted from 192.168.168.147:49343 #2 (2 connections now open)
	d20014| 2016-02-24T17:16:10.520+0100 I NETWORK  [initandlisten] connection accepted from 192.168.168.147:49344 #2 (2 connections now open)
	d20012| 2016-02-24T17:16:10.520+0100 I REPL     [conn1] replSetInitiate config object with 3 members parses ok
	d20013| 2016-02-24T17:16:10.520+0100 I NETWORK  [conn2] end connection 192.168.168.147:49343 (1 connection now open)
	d20014| 2016-02-24T17:16:10.520+0100 I NETWORK  [conn2] end connection 192.168.168.147:49344 (1 connection now open)
	d20014| 2016-02-24T17:16:10.527+0100 I NETWORK  [initandlisten] connection accepted from 192.168.168.147:49354 #3 (2 connections now open)
	d20013| 2016-02-24T17:16:10.527+0100 I NETWORK  [initandlisten] connection accepted from 192.168.168.147:49353 #3 (2 connections now open)
	[...]
	d20014| 2016-02-24T17:16:24.648+0100 I REPL     [ReplicationExecutor] syncing from: Irensaga-2.local:20012
	d20012| 2016-02-24T17:16:24.650+0100 I NETWORK  [initandlisten] connection accepted from 192.168.168.147:49425 #10 (5 connections now open)
	d20014| 2016-02-24T17:16:24.651+0100 I REPL     [SyncSourceFeedback] setting syncSourceFeedback to Irensaga-2.local:20012
	d20012| 2016-02-24T17:16:24.651+0100 I NETWORK  [conn10] end connection 192.168.168.147:49425 (4 connections now open)
	d20012| 2016-02-24T17:16:24.652+0100 I NETWORK  [initandlisten] connection accepted from 192.168.168.147:49426 #11 (5 connections now open)
	d20012| 2016-02-24T17:16:24.653+0100 I NETWORK  [initandlisten] connection accepted from 192.168.168.147:49430 #12 (6 connections now open)
	d20013| 2016-02-24T17:16:24.653+0100 I ASIO     [NetworkInterfaceASIO-BGSync-0] Successfully connected to Irensaga-2.local:20012
	d20012| 2016-02-24T17:16:24.659+0100 I NETWORK  [initandlisten] connection accepted from 192.168.168.147:49432 #13 (7 connections now open)
	d20014| 2016-02-24T17:16:24.659+0100 I ASIO     [NetworkInterfaceASIO-BGSync-0] Successfully connected to Irensaga-2.local:20012

Una vez activada la funcionalidad de réplica, veremos aparecer trazas en esta consola, ya que por defecto, la salida de los tres nodos que se han arrancado con ReplSetTest se volcará en esta consola.


7. Prueba del grupo de réplica

Una vez que ya está configurado y activo el grupo de réplica, vamos a probar cómo se produce la réplica de datos entre todos los nodos que forman parte del grupo.


7.1. Arranque de una nueva consola

Para evitar que nuestras pruebas se vean entremezcladas con las trazas que los nodos del grupo de réplica están volcando en la consola con la que hemos configurado y arrancado el ReplSetTest, vamos a utilizar una nueva consola.

$ mongo --nodb
MongoDB shell version: 3.2.0
>

7.2. Conexión al nodo primario del grupo de réplica

Entre todos los nodos de un grupo de réplica, éstos pueden jugar dos roles distintos:

  • nodo primario: sólo existe uno y es el único que acepta operaciones tanto de escritura como de lectura.
  • nodo secundario: pueden existir más de uno. Son nodos que replican los datos del nodo primario, pero no aceptan operaciones de escritura ni de lectura (por defecto, aunque puede configurarse para permitir las operaciones de lectura).
  • nodos árbitro: son nodos que no almacenan réplica de datos, su única función es participar en el proceso de elección de un nuevo nodo primario entre los nodos secundarios cuando el nodo primario anterior se cae

Para probar el funcionamiento del grupo de réplica que hemos levantado, es necesario que nos conectemos al nodo primario. Como a priori no podemos saber cuál es el nodo primario (el nodo primario se elige en un proceso de votación entre todos los nodos), deberemos conectarnos a cada uno de ellos y preguntar si es el nodo primario.

Para obtener la conexión a uno de los demonios mongod crearemos un nuevo objeto Mongo pasando como argumento la cadena de conexión compuesta del hostname y el puerto (que corresponderá con los puertos que vimos en la salida al arrancar los procesos en el apartado 5):

> conn = new Mongo("localhost:20013")
connection to localhost:20013

Una vez obtenida la conexión, obtenemos la BD sobre la que realizaremos la prueba. En nuestro caso, para esta prueba vamos a utilizar la propia bd de test.

> testDB = conn.getDB(“test”)
test

Por último, sobre la BD, preguntaremos si es el nodo primario, utilizando la función isMaster():

> testDB.isMaster()
	{
		"hosts" : [
			"Irensaga-2.local:20012",
			"Irensaga-2.local:20013",
			"Irensaga-2.local:20014"
		],
		"setName" : "myReplicaSet",
		"setVersion" : 1,
		"ismaster" : false,
		"secondary" : true,
		"primary" : "Irensaga-2.local:20012",
		"me" : "Irensaga-2.local:20013",
		"maxBsonObjectSize" : 16777216,
		"maxMessageSizeBytes" : 48000000,
		"maxWriteBatchSize" : 1000,
		"localTime" : ISODate("2016-02-24T17:34:41.347Z"),
		"maxWireVersion" : 4,
		"minWireVersion" : 0,
		"ok" : 1
	}

Del objeto devuelto, nos fijaremos en la propiedad ismaster y secondary. Si el nodo al que nos hemos conectado no es primario, volveremos a conectarnos al siguiente nodo del grupo de réplica, hasta que encontremos el nodo primario (o podemos identificarlo como el que está marcado con el atributo primary)

> conn = new Mongo("localhost:20012")
connection to localhost:20012
> testDB = conn.getDB("test")
test
> testDB.isMaster()
{
	"hosts" : [
		"Irensaga-2.local:20012",
		"Irensaga-2.local:20013",
		"Irensaga-2.local:20014"
	],
	"setName" : "myReplicaSet",
	"setVersion" : 1,
	"ismaster" : true,
	"secondary" : false,
	"primary" : "Irensaga-2.local:20012",
	"me" : "Irensaga-2.local:20012",
	"electionId" : ObjectId("56cdd7560000000000000001"),
	"maxBsonObjectSize" : 16777216,
	"maxMessageSizeBytes" : 48000000,
	"maxWriteBatchSize" : 1000,
	"localTime" : ISODate("2016-02-24T17:42:02.950Z"),
	"maxWireVersion" : 4,
	"minWireVersion" : 0,
	"ok" : 1
}

7.3. Insertamos un conjunto de datos sobre el nodo primario

Una vez que ya estamos conectados al nodo primario, vamos a ejecutar una inserción de un conjunto de datos en la colección de ejemplo (en nuestro caso, por ejemplo una serie de entradas de un blog).

> for (i = 0; i < 1000; i++) {
	... testDB.blog_posts.insert(
	...          {
	...           author: "author " + i,
	...           blog_title : "Blog post entry by author " + i
	...          });
	... }
	WriteResult({ "nInserted" : 1 })

Por último, comprobamos que se han almacenado los registros en la colección.

> testDB.blog_posts.count();
1000

7.4. Comprobación de la réplica sobre los nodos secundarios

Una vez que hemos insertado los datos a través del nodo primario, vamos a conectarnos a alguno de los nodos secundarios, y comprobar si se han replicado los datos a ese nodo.

Empezamos por conectarnos a uno de los nodos secundarios, obtener la conexión y comprobamos que, efectivamente, el nodo es secundario:

> connSecondary = new Mongo("localhost:20014")
	connection to localhost:20014
	> secondaryTestDB = connSecondary.getDB("test")
	test
	> secondaryTestDB.isMaster()
	{
		"hosts" : [
			"Irensaga-2.local:20012",
			"Irensaga-2.local:20013",
			"Irensaga-2.local:20014"
		],
		"setName" : "myReplicaSet",
		"setVersion" : 1,
		"ismaster" : false,
		"secondary" : true,
		"primary" : "Irensaga-2.local:20012",
		"me" : "Irensaga-2.local:20014",
		"maxBsonObjectSize" : 16777216,
		"maxMessageSizeBytes" : 48000000,
		"maxWriteBatchSize" : 1000,
		"localTime" : ISODate("2016-02-24T18:27:03.286Z"),
		"maxWireVersion" : 4,
		"minWireVersion" : 0,
		"ok" : 1
	}

Ahora que estamos ya conectados a la base de datos en el nodo secundario dentro del grupo de réplica, vamos a comprobar si los datos que hemos insertado en el nodo primario, se han replicado en este secundario.

Para ello hacemos una consulta a la colección (debería devolver los mismos datos que en la colección del nodo primario):

> secondaryTestDB.blog_post.count();
2016-02-24T19:35:39.814+0100 E QUERY    [thread1] Error: count failed: { "ok" : 0, "errmsg" : "not master and slaveOk=false", "code" : 13435 } :
_getErrorWithCode@src/mongo/shell/utils.js:23:13
DBQuery.prototype.count@src/mongo/shell/query.js:359:11
DBCollection.prototype.count@src/mongo/shell/collection.js:1609:12
@(shell):1:1

En este caso obtenemos un error porque por defecto, tal y como comentamos en el 7.2 sobre la conexión al nodo primario del grupo de réplica, los nodos secundarios en un grupo de réplica, no admiten operaciones ni de escritura ni de lectura. Todas las operaciones deben realizarse siempre sobre el nodo principal.

Sin embargo, podemos activar el permiso para realizar operaciones de lectura sobre un nodo secundario.

Para ello utilizaremos la función setSlaveOK() sobre la conexión al nodo secundario. La invocación de esta función significa que le estamos indicando a mongoDB que somos conscientes de que estamos trabajando sobre un nodo secundario. A partir de aquí la responsabilidad de lo que hagamos es nuestra, 😉

> connSecondary.setSlaveOk()
>

En este momento, ya podemos lanzar la operación de consulta y comprobar que la réplica de datos funciona:

> secondaryTestDB.blog_posts.count();
1000
> secondaryTestDB.blog_posts.findOne();
{
	"_id" : ObjectId("56cdeeb47b058dd8d549a12d"),
	"author" : "author 1",
	"blog_title" : "Blog post entry by author 1"
}

En este caso, comprobamos que los datos que insertamos en el nodo principal se han replicado en el nodo secundario.


8. Promoción automática de un nodo secundario ante la caída del primario

Como hemos explicado, los nodos pueden tomar varios roles (primario, secundario y árbitro) en un grupo de réplica, siendo sólo el nodo primario el que admite las operaciones de escritura y de consulta.

En este apartado, vamos ahora a probar qué ocurre cuando el nodo primario se cae, y comprobar que automáticamente uno de los nodos secundarios toma el papel de primario y empieza a admitir las operaciones.

8.1. Parada del nodo primario

Empezamos por parar específicamente el nodo primario, para simular una caída (o una pérdida de conexión). Para ello podríamos :

  • ejecutar el comando kill a nivel de sistema operativo
  • o simplemente enviar el comando de parada del nodo a través del API JS de la consola.

Optaremos por la segunda opción, pero comprobando previamente que el nodo al que nos hemos conectado es el primario:

> connPrimary = new Mongo("localhost:20014")
connection to localhost:20014
> primaryDB = connPrimary.getDB("test")
test
> primaryDB.isMaster()
{
	"hosts" : [
		"Irensaga-2.local:20012",
		"Irensaga-2.local:20013",
		"Irensaga-2.local:20014"
	],
	"setName" : "myReplicaSet",
	"setVersion" : 1,
	"ismaster" : true,
	"secondary" : false,
	"primary" : "Irensaga-2.local:20014",
	"me" : "Irensaga-2.local:20014",
	"electionId" : ObjectId("56cec22a000000000000000a"),
	"maxBsonObjectSize" : 16777216,
	"maxMessageSizeBytes" : 48000000,
	"maxWriteBatchSize" : 1000,
	"localTime" : ISODate("2016-02-25T14:09:58.014Z"),
	"maxWireVersion" : 4,
	"minWireVersion" : 0,
	"ok" : 1
}
>

En este caso, vemos que, el nodo primario es el 20014. Vamos a parar este nodo, para ello emitimos el comando de apagado:

> primaryDB.adminCommand({shutdown : 1});

En este momento, en la consola de mongo en la que habíamos creado el ReplSetTest, y donde se están volcando las trazas, veremos que se ha detectado una pérdida de conexión (“Error in heartbeat”) con el nodo que hemos apagado e, inmediatamente, se produce un proceso de election para elegir al nuevo nodo primario:

[...]
	d20012| 2016-02-25T16:00:15.419+0100 I REPL     [ReplicationExecutor] Error in heartbeat request to Irensaga-2.local:20014; HostUnreachable Connection refused
	d20012| 2016-02-25T16:00:18.948+0100 I REPL     [ReplicationExecutor] Starting an election, since we've seen no PRIMARY in the past 10000ms
	d20012| 2016-02-25T16:00:18.949+0100 I REPL     [ReplicationExecutor] conducting a dry run election to see if we could be elected
	d20012| 2016-02-25T16:00:18.949+0100 I REPL     [ReplicationExecutor] dry election run succeeded, running for election
	d20012| 2016-02-25T16:00:18.950+0100 I REPL     [ReplicationExecutor] election succeeded, assuming primary role in term 2
	d20012| 2016-02-25T16:00:18.950+0100 I REPL     [ReplicationExecutor] transition to PRIMARY

En las trazas vemos que el demonio d20012 ha perdido conectividad con el pulso del nodo que corre en el puerto 20014, y cómo este inicia un proceso de elecciones en el que se erige en candidato a ser el primario.

Después de ser validado para ser el nuevo primario, promociona a nodo PRIMARY, o master.

8.2. Comprobación del nuevo nodo primario

Vamos a ahora a comprobar que el nodo efectivamente es el primario. Primero obtenemos una conexión al nuevo nodo primario:

> connNewPrimary = new Mongo("localhost:20012")
	connection to localhost:20012
	> newPrimaryDB = connNewPrimary.getDB("test")
	test
	> newPrimaryDB.isMaster()
	{
		"hosts" : [
			"Irensaga-2.local:20012",
			"Irensaga-2.local:20013",
			"Irensaga-2.local:20014"
		],
		"setName" : "myReplicaSet",
		"setVersion" : 1,
		"ismaster" : true,
		"secondary" : false,
		"primary" : "Irensaga-2.local:20012",
		"me" : "Irensaga-2.local:20012",
		"electionId" : ObjectId("56cec22a000000000000000a"),
		"maxBsonObjectSize" : 16777216,
		"maxMessageSizeBytes" : 48000000,
		"maxWriteBatchSize" : 1000,
		"localTime" : ISODate("2016-02-25T14:09:59.014Z"),
		"maxWireVersion" : 4,
		"minWireVersion" : 0,
		"ok" : 1
	}
	>

9. Parada del ReplicaSet de pruebas

Por último, una vez finalizada la prueba de cómo funciona el mecanismo de réplica, vamos a parar el grupo de réplica.

Para ello, en la consola de mongo donde configuramos y arrancamos el grupo de réplica (la consola donde estamos viendo las trazas de los nodos), ejecutamos el siguiente comando para parar el grupo de réplica:

> sampleReplicaSet.stopSet()
	ReplSetTest stop *** Shutting down mongod in port 20012 ***
	2016-02-25T16:19:16.340+0100 I -        [thread1] shell: stopped mongo program on port 20012
	ReplSetTest stop *** Mongod in port 20012 shutdown with code (0) ***
	ReplSetTest stop *** Shutting down mongod in port 20013 ***
	d20013| 2016-02-25T16:19:16.340+0100 I CONTROL  [signalProcessingThread] got signal 15 (Terminated: 15), will terminate after current cmd ends
	d20013| 2016-02-25T16:19:16.340+0100 I FTDC     [signalProcessingThread] Shutting down full-time diagnostic data capture
	d20013| 2016-02-25T16:19:16.344+0100 I REPL     [signalProcessingThread] Stopping replication applier threads
	d20013| 2016-02-25T16:19:16.791+0100 I STORAGE  [conn5] got request after shutdown()

	[...]

	2016-02-25T16:19:20.352+0100 I -        [thread1] shell: stopped mongo program on port 20014
	ReplSetTest stop *** Mongod in port 20014 shutdown with code (0) ***
	ReplSetTest stopSet deleting all dbpaths
	ReplSetTest stopSet *** Shut down repl set - test worked ****
	>

A. Configuración avanzada en la construcción del ReplSetTest

En la creación del ReplSetTest, se puede indicar como argumento un objeto JSON con la configuración detallada de cómo queremos crear el RéplicaSet.

Este objeto JSON puede tener las siguientes propiedades:

  • name: de tipo String, con el nombre que queremos asignar al grupo de réplica. Por defecto toma el valor testReplSet
  • host : de tipo String, con el nombre de la máquina donde se va a crear el grupo de réplica. Por defecto toma el hostname.
  • useHostName: de tipo boolean, indica si se debe utilizar el hostname de la máquina como host (en caso de true) o utiliza “localhost” (en caso de ser false) para los nodos. Por defecto a true.
  • nodes: Indica las instancias de mongod que formarán parte del grupo de réplica. El valor de este atributo de configuración puede ser de varios tipos:
    • de tipo entero, indica el número de instancias de mongod que se crearán en el grupo de réplica. Por defecto toma el valor 0.
    • de tipo objeto JSON, con la configuración de la instancia que quiere que sea parte del grupo de réplica. Este objeto JSON puede tener la siguiente estructura (atributos) de configuración
      • useHostName : de tipo boolean, cuando se configura a true utiliza el hostname de la máquina.
      • forceLock: de tipo boolean, si está puesto a true, borra el fichero de lock
      • dbpath: de tipo string, con la ruta de los ficheros de base de datos. Por defecto /data/db/{nodename}
      • cleanData: de tipo boolean, si está configurado a true, elimina los ficheros que hubiese en la ruta dbpath antes de arrancar la instancia
      • startData: equivalente cleanData.
      • noCleanData: de tipo boolean, si está puesto a true mantiene los ficheros que ya existiesen en la ruta dbpath. Este valor tiene prioridad si está especificado también la propiedad cleanData.
      • arbiter:: de tipo boolean, indica si la instancia se quiere que tome el papel de árbitro para las votaciones en caso de caída del nodo primario del grupo de réplica.
    • de tipo array, con los distintos objetos JSON de configuración (según están descritos en el punto anterior) de cada una de las instancias que se quieren incluir en el grupo de réplica.
  • nodeOptions: de tipo objeto JSON, con las opciones que se quieren aplicar a todos las instancias que formarán parte del grupo de réplica. Este objeto toma los valores de los argumentos que pasaríamos por linea de comandos al comando mongod arrancar las instancias de cada uno de los nodos del grupo de réplica.
  • opLogSize: de tipo numérico, tamaño del registro de operaciones que se mantienen para poder sincronizar los distintos nodos del grupo de réplica después de una caída de uno o varios de ellos. Por defecto toma el valor 40.
  • useSeedList: de tipo boolean, si se configura, la cadena de conexión se utilizará como nombre para el replicaSet. Este valor sobreescribe el valor asignado al atributo name, si se ha configurado. Por defecto toma el valor false
  • protocolVersion: de tipo numérico, indica la versión del protocolo a utilizar en la inicialización del grupo de réplica.

Resolución de problemas


startSet() no arranca los demonios mongod del grupo de réplica, por error en la ruta.

Configuración avanzada en la construcción de ReplSetTest

Si al ejecutar el comando de arranque de los nodos del del grupo de réplica no vemos que se levanten los procesos mongod y obtenemos un error de permiso denegado o de acceso a la ruta donde están los ficheros de la BD:

> sampleReplicaSet.startSet()
ReplSetTest Starting....
Resetting db path '/data/db/myReplicaSet-0'
2016-02-24T16:19:32.710+0100 E QUERY    [thread1] Error: Caught std::exception of type boost::filesystem::filesystem_error: boost::filesystem::create_directory: Permission denied: "/data/db/myReplicaSet-0" :
MongoRunner.runMongod@src/mongo/shell/servers.js:615:13
ReplSetTest.prototype.start@src/mongo/shell/replsettest.js:771:16
ReplSetTest.prototype.startSet@src/mongo/shell/replsettest.js:249:16

deberemos comprobar que tenemos permisos de acceso y escritura en la ruta de configurada como dbpath (por defecto /data/db):

  • Podemos asignar permisos en /data/db al usuario con el que ejecutamos la consola mongo
  • Podemos ejecutar mongo –nodb como superusuario
  • Podemos crear el ReplSetTest utilizando un dbpath diferente para los nodos del grupo de réplica. Ver propiedad dbpath del atributo nodes del objeto JSON que pasamos como configuración al constructor

Uso práctico de Java y Neo4j: Sistema de recomendación

$
0
0
En este tutorial veremos cómo conectarnos y consumir información de la base de datos de grafos Neo4j, para ello realizaremos una sencilla aplicación de recomendación.

Índice de contenidos

1. Introducción

Como ya vimos en el tutorial Primeros pasos con Neo4j, de Juan Alonso Ramos, Neo4j es una base de datos orientada a grafos implementada en java. El uso de Neo4j esta recomendado para:
  • Búsquedas en base a grafos
  • Sistemas de recomendación
  • Redes sociales
  • Gestión de identidades y accesos
  • Detección de fraude
  • Gestión de datos maestros
Tal y como describen en su propia web, Casos de uso de Neo4j, junto con una serie de white papers con más información sobre cada uno de los casos recomendados. En este tutorial realizaremos un ejemplo básico, través de una aplicación Java, de sistema de recomendación en el que para un conjunto personas que se conocen entre ellas, se recomendará a una persona concreta otro conjunto de personas a conocer. ¡Manos a la obra!

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 El Capitan 10.11.5.
  • Entorno de desarrollo: IntelliJ IDEA ULTIMATE 2016.1.2
  • Apache Maven 3.3.0.
  • JDK 1.8.0_71
  • Spring Boot
  • Neo4j 3.0.2

3. Preparar el contexto

3.1. Neo4j

Lo primero que necesitamos es mantener una instancia de Neo4j instalada en nuestro sistema. Para ello aconsejamos seguir el tutorial mencionado con anterioridad Primeros pasos con Neo4j.

3.2. Proyecto IntelliJ

En segundo lugar vamos a crear un esqueleto para nuestro proyecto. Dado que no es el objetivo de este tutorial, no hace falta preocuparse por cómo crearlo, disponéis de todo el código subido en github: SocialRecommendation.

Este repositorio de git mantiene tanto la estructura del proyecto listo para utilizarse, como una copia de la instancia de Neo4j con la información necesaria. No obstante facilitaremos los pasos necesarios para crear una nueva instancia de Neo4j y cómo alimentarla.

3.3. Creación de un nuevo grafo

Con Neo4j se pueden crear grafos atendiendo al momento de la creación y a la necesidad de que la información persista, por ello podemos elegir entre:

  • Por un lado, crear la base de datos de manera estática o de manera dinámica (en tiempo de ejecución),
  • por otro lado, crear la base de datos persistente o temporal (en memoria).

Para este tutorial, hemos creído conveniente crearla de manera estática y persistente. A continuación se muestran los pasos a seguir para proceder con la creación de la instancia.

Primero abriremos el gestor gráfico de neo4j, nos mostrará una ventana como la siguiente:

Configurar_grafo_01

Continuamos pulsando sobre “Choose“, lo que nos ofrecerá un diálogo en el que podremos seleccionar la nueva ubicación de nuestra base de datos Neo4j.

Después nos aseguramos de que la configuración se encuentra correctamente establecida pulsando sobre el botón “Options…“, lo que nos mostrará una ventana como la que se ve a continuación:

Configurar_grafo_02

Una vez comprobada la correcta configuración cerramos la ventana de configuración y procedemos con la creación de la base de datos pulsando sobre el botón “Start“, lo que iniciará el proceso de creación de los ficheros necesarios para dar soporte a la base de datos. No se trata de un proceso inmediato, así que puede que tarde un momento.

Configurar_grafo_03

Cuando el proceso de creación de la nueva base de datos haya finalizado, el cuadro “Status” cambiará a color verde, informandonos del fin de la tarea.

Configurar_grafo_04

Con esto pondremos fin al proceso de creación de nuestra base de datos Neo4j.

Podremos comprobar que todo ha funcionado correctamente accediendo al interfaz web de administración de neo4j en el enlace que se muestra en el cuadro “Status“, en nuestro caso “http://localhost:7474/“.

3.4. Alimentando el grafo

El proceso de alimentación del grafo es muy sencillo.

En este caso vamos a proceder con la carga de información desde ficheros csv donde mantendremos información referente a personas y a las amistades que entre ellos han surgido.

Para ello, debemos colocar los ficheros con dicha información en la carpeta “./neo4j.databas/import/” de la dirección donde hayamos creado nuestra nueva instancia de Neo4j.

A continuación nos dirigimos al gestor web de Neo4j, en nuestro caso “http://localhost:7474/“, y procedemos con los siguientes pasos:

Alimentando_grafo_01

Continuando, en el cuadro de inserción de consultas de Neo4j creamos la clave primaria para nuestra tabla “Person“:

CREATE CONSTRAINT ON (p:Person) ASSERT p.userId IS UNIQUE;

Lo que nos debería arrojar un resultado como el siguiente:

Alimentando_grafo_02 Después realizaremos la carga de las personas mediante:
LOAD CSV FROM "file:///personas.csv" AS row CREATE (:Person {id: toInt(row[0]), name:row[1]});
Lo que nos devolverá un mensaje como el siguiente: Alimentando_grafo_03 Para finalizar cargamos las relaciones entre las personas con:
USING PERIODIC COMMIT;
LOAD CSV FROM "file:///amistades.csv" AS row
MATCH (p1:Person {id: toInt(row[0])}), (p2:Person {id: toInt(row[1])})
CREATE (p1)-[:KNOWS]->(p2);
Lo que nos dará un resultado como: Alimentando_grafo04 Finalmente, si realizamos una consulta simple, podremos visualizar el grafo que hemos generado:
MATCH (n:Person) RETURN n LIMIT 25;
El resultado: Alimentando_grafo_05 Con esto habremos puesto fin al proceso de carga de la información para nuestra aplicación.

4. El código

Ahora procederemos a repasar las principales partes del código fuente de la aplicación.

4.1. La conexión a Neo4j

Establecemos la conexión a la base de datos a través de la siguiente clase de configuración:
SocialRecommendationNeo4jConfiguration.java
@EnableTransactionManagement
@Configuration
@EnableNeo4jRepositories(basePackages = "com.adictosaltrabajo.neo4j.core.repository")
public class SocialRecommendationNeo4jConfiguration extends Neo4jConfiguration {

    @Autowired
    private Environment env;
    private org.neo4j.ogm.config.Configuration config;
    private SessionFactory sessionFactory;

    @Bean
    public org.neo4j.ogm.config.Configuration getConfiguration() {
        config = new org.neo4j.ogm.config.Configuration();
        config.driverConfiguration()
                .setDriverClassName(env.getProperty("neo4j.driverClassName"))
                .setURI(env.getProperty("neo4j.uri"));
        return config;
    }

    @Override
    public SessionFactory getSessionFactory() {
        sessionFactory = new SessionFactory(getConfiguration(), "com.adictosaltrabajo.neo4j.core.model");
        return sessionFactory;
    }

    @Override
    @Bean
    @Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)
    public Session getSession() throws Exception {
        return sessionFactory.openSession();
    }

}
El método getConfiguration establecerá los parámetros de configuración de la conexión a nuestra base de datos Neo4j. Estos parámetros se establecen mediante el fichero de configuración de la aplicación application.properties:
neo4j.driverClassName=org.neo4j.ogm.drivers.http.driver.HttpDriver
neo4j.uri=http://neo4j:adictos@localhost:7474
El método getSessionFactory nos proporciona una factoría de sesiones de la que se hará uso de manera interna cada vez que se quiera consultar a la base de datos. En ella se establece el path hasta los modelos que representarán en Java los elementos de la base de datos. Por último, getSession nos provee de una sesión a través del sessionFactory.

4.2. El sistema de recomendación

Gracias al lenguaje Cypher, que nos permite realizar consultas de manera expresiva y eficiente sobre Neo4j, nuestro sistema de recomendación va a consistir en una simple y llana consulta friendsOfMyFriends.

PersonRepository.java
@Repository
public interface PersonRepository extends GraphRepository {

    @Query("MATCH (b:Person)-[:KNOWS]->(a:Person) " +
            "WHERE b.name = {name} " +
            "RETURN collect(a) " +
            "LIMIT {limit}")
    List friends(@Param("name") String name, @Param("limit") int limit);

    @Query("MATCH (person:Person)-[:KNOWS]->(friend:Person)-[:KNOWS]->(foaf:Person) " +
            "WHERE person.name = {name} " +
                "AND person <> foaf " +
                "AND NOT (person) -[:KNOWS]-> (foaf) " +
            "RETURN collect(distinct(foaf)) " +
            "LIMIT {limit}")
    List friendsOfMyFriends(@Param("name") String name, @Param("limit") int limit);

}
El método friends nos devolverá los amigos directos de una persona filtrando por su nombre.

Tal y como comentábamos con anterioridad, el método friendsOfMyFriends es el encargado de realizar la recomendación de personas. Nos devuelve todas las personas que son conocidas por los amigos de una persona estableciendo varios filtros:

    • El nombre de la persona a la que se va a recomendar otras personas
person.name = {name}
    • Esas personas no pueden ser la propia persona.
AND person <> foaf
    • Por último deben ser personas desconocidas.
AND NOT (person) -[:KNOWS]-> (foaf)

5. La aplicación

Para visualizar este sistema de recomendaciones hemos creado una aplicación web muy sencilla, en la que lo único que hacemos es mostrar información.

Inicialmente cuando entramos en la aplicación mostraremos la tabla de personas existentes en el sistema

Neo4j_aplicacion_1 Cada nombre es un enlace a dos conjuntos de información de la persona: Sus amigos y las recomendaciones: Neo4j_aplicacion_2

6. Conclusiones

Este tutorial es una aproximación muy básica sobre Neo4j, cómo conectarnos y cómo consumir datos desde sus grafos a través de CQL (Cypher Query Language).

Ya sean grafos no dirigidos, dirigidos, con peso… gracias, tanto a la estructura de datos que mantiene Neo4j consumir información de grafos se hace realmente sencillo.

En posteriores tutoriales sería interesante explorar un poco más en profundidad algunos aspectos de Neo4j más avanzados o con mayor complejidad.

Esperamos que este tutorial, aunque sumamente sencillo, os haya servido para perderle el miedo a esta herramienta y os animéis a hacer uso de ella.

Un viaje a Copenhague con Autentia

$
0
0

Hace poco compartí mi experiencia grabando eventos tecnológicos con Autentia y hoy quiero hablaros de otra experiencia que hace pocas semanas tuve la oportunidad de vivir.

¡Un viaje a Copenhague!

El equipo de Comunicación de Autentia lleva activo dos años y medio y aunque por ahora hemos viajado poquito, pues la mayoría de los eventos que grabamos son en Madrid, he podido conocer sitios que todavía no había visitado. Y, ¡viajar por trabajo también mola!. Siempre te queda un ratillo para conocer el sitio en el que estás, tomarte algún vino, cervezas, pintxos…

Os voy a contar el viaje.

Mes de abril. Estábamos grabando el evento de Greach en Madrid en el Teatro Luchana. Uno de los ponentes al que le hicimos entrevista nos dijo que le gustaba mucho la idea de las entrevistas y que lo que hacía Autentia era “really cool!”. Le comentó a mi compañera Alba Roza que estaría bien que fuéramos a Copenhague al evento GR8Conf Europe 2016 que se iba a celebrar en junio.

Cuando nos lo contó a las demás, pensábamos que era lo típico que se dice pero se queda ahí en el aire, olvidado. Pasó un tiempo y el ponente se puso en contacto con uno de los organizadores de Greach pidiéndole el contacto de Alba, para decirle que no se había olvidado de lo que dijo sobre ir a Copenhague. ¡Al día siguiente de hablar con ella teníamos los billetes comprados y la habitación del hotel reservada! Nos íbamos del 29 de mayo al 3 de junio. ¡Una semana en Copenhague!

Yo había estado en abril por primera vez porque mi hermano vive allí. Fui con mi madre, un viaje en familia. No tuve muy mal tiempo, me llovió solamente una mañana y no hizo demasiado frío. Hice mucho turismo pero me volví a Madrid con ganas de regresar allí pronto. Así que imaginaros mi cara de felicidad cuando supe que volvía.

DÍA 1: Vacaciones – Salida de Madrid a las 11:10 – Llegada a Copenhague a las 14:35. Soleado, 25º

Mi hermano nos vino a buscar al aeropuerto a Alba y a mí. No habíamos pensado que íbamos a hacer ese día pero tampoco nos lo pensamos mucho cuando mi hermano nos dijo: ¿queréis ir a la playa?. Claro al principio nos quedamos como… – ¿A la playa?, Vale que era mayo y aquí en España no es tan raro ya poder disfrutar de playas, pero ¿en Copenhague?. Directas al Báltico nos fuimos, a meter los pies porque bikinis no teníamos.

mar-copenhague Playa-Copenhague

Después de un poco de playita nos fuimos al barrio de Christiania. Estaba tocando en directo una banda danesa. La gente estaba sentada con cervezas. Volvimos al centro a cenar, nos tomamos otra cerveza y a dormir.

DÍA 2: Vacaciones – Excursión en tren a Helsingør Soleado, 25º

Para aprovechar este día, decidimos ir a visitar el Castillo de Kronborg, al norte de Dinamarca. Se tarda media hora en tren desde la Estación Central. Compramos un pase para ver todo el castillo por dentro que nos costó 90kr (unos 13 euros aproximadamente)

castillo-copenhague castillo-plaza-copenhague

Durante varios siglos Dinamarca obtuvo importantes ingresos económicos cobrando una tasa a los barcos que atravesaban el estrecho de Oresund, estrecho que separa a la isla danesa de Selandia del territorio sueco. El Castillo de Kronborg cumplió un importante rol en esta tarea recaudatoria.

Como se ve en las fotos, hacía un día estupendo así que cuando terminamos la excursión exploramos la “playa” que había detrás y paramos un ratito a tomar el sol. Por la tarde buscamos un sitio para comer por el pueblo y cogimos el tren de vuelta a Copenhague.

letiyalba-copenhague

DÍA 3: Vacaciones – Ruta en bici por Copenhague. Cena de Speakers – Lluvia y Sol

Cogimos las bicis y salimos por la mañana a dar una vuelta por Nyhavn o “las casitas de colores de Copenhague”. Empezó a diluviar cuando estábamos por ahí con ellas, así que volvimos al hostel en el que se estaba quedando Alba a esperar que parara. Teníamos que ir después a hacer check in al nuevo hotel porque al día siguiente ya empezábamos a trabajar en el evento. Dejamos las bicis en la Estación Central, fuimos a casa de mi hermano a por mi maleta y fuimos en busca del hotel. Sabíamos que se llamaba Wake Up y al buscarlo en internet nos apuntamos la dirección sin saber que en verdad existen dos Wake Up en la ciudad y no están precisamente cerca uno del otro. ¿Qué paso? que efectivamente fuimos al Wake Up equivocado. Nos habíamos empapado en la bici por la mañana, no habíamos comido aún, habíamos ido andando con las maletas y cuando por fin llegamos… “Tenéis que ir al otro Wake Up”, la chica sacó un mapa.

mapa-copenhague

Se nos quedó la cara de “El Grito” de Edward Munch. Por suerte se había despejado y había salido el sol, pero hacía mucho calor para ir andando con las maletas para arriba y para abajo. Fuimos hasta un bus que nos llevaba cerca del hotel. Al llegar teníamos problemas con la reserva de la habitación, la chica nos decía que esa reserva era para una persona. Empezaba a ser como una pesadilla, pero se pudo solucionar en seguida. ¡Bien! Teníamos habitación y habíamos dejado las maletas. Eran las 16:30, aún teníamos horas para pasear en bici antes de la cena de los ponentes a las 19:00.

mapados-copenhague

¡Oh no! Las bicis estaban casi al lado del otro Wake Up… teníamos que volver a por ellas… Terminamos comiendo a las 17:00, en Paludan Café (muy recomendado). Comiendo a las 17:00… y teníamos la cena a las 19:00. Qué le vamos a hacer. Dimos pedales hasta el Parque Langelinie, en la Bahía del Puerto de Copenhague, donde está la famosa Sirenita.

letibici-copenhague marsirenita-copenhague sirenita-copenhague

Dentro de este parque también se encuentra Kastellet, la ciudadela militar.

En la actualidad la ciudadela sirve de acuartelamiento militar, pero es una zona de acceso al público y de hecho es uno de los lugares más típicos de Copenhague. En el interior se pueden encontrar cañones antiguos que formaron parte del armamento defensivo en diferentes épocas

Y también existe en el recinto un molino de viento que a primera vista parece que está fuera de lugar. Pero no se trata de un adorno, la ciudadela era en parte autosuficiente y el molino tenía originalmente su función junto con otros elementos de la fortaleza para cubrir las necesidades de los soldados en caso de asedio

estrella-copenhague

Foto cortesía de http://www.skypix.dk

También visitamos El Palacio de Amalienborg. Es la residencia oficial de la familia real danesa durante el invierno. En realidad no se trata de un solo palacio, sino de cuatro, distribuidos en torno a una plaza presidida por la estatua de su fundador, el rey Frederick V (la Plaza de Amalienborg).

Fue la hora de volver para la cena de los ponentes. Cenamos en el restaurante Vita. Era una antigua farmacia y lleva alli desde 1690. Nos dieron algunos entrantes, una carne con patatas asadas y una tarta de chocolate. Todo acompañado de cervezas y vinos. Fue una cena divertida en la que tuvimos ocasión de volver a hablar con los ponentes de ediciones pasadas

cena-gr8conf cenados-gr8conf cenatres-gr8conf

DÍA 4: ¡Empieza el evento de GR8Conf Europe 2016! – IT University

Nos levantamos pronto y fuimos a desayunar a una cafetería que estaba de camino al metro porque no sabíamos que el hotel incluía el desayuno, (cara de “El Grito” otra vez cuando nos enteramos). Cogimos el metro en Kongens Nytorv y fuimos a la universidad de IT en DR Byen St. Tardábamos 10 minutos en metro. Cada viaje en metro te cuesta 24kr (3,23€) y te incluye mínimo dos zonas. Allí teníamos que hacer las entrevistas a los ponentes, hacer fotos para las redes sociales y grabar un vídeo resumen. Es una pasada la universidad por dentro. Si vais a Copenhague y os gusta la arquitectura tenéis que visitarla.

exterioruniversidad-copenhague interioruniversidad-copenhague
entrevistauno-copenhague entrevistados-copenhague entrevistatres-copenhague

Como terminamos pronto y nos quedaba toda la tarde libre para disfrutar, al salir nos fuimos a Distortion. “Cada año el primer sábado de junio se lía en Copenhague. Festival callejero al canto. La cosa va hacia arriba, es un verdadero foco de atención. Bajo el sugerente nombre de Distortion, se trata de un encuentro sobre la cultura de los clubs, que no clubes en este caso, en la capital danesa. Distortion es de reciente creación, pero ya va llevando su poquita de tradición. Fue en 1998 cuando durante la celebración de una fiesta nocturna de Mantra se decidió brindar a Copenhague la marcha que se merece una ciudad que vive tanto en la calle para el frío que hace.”

(Alejandra Chaves, http://www.abc.es/viajar/dinamarca/abci-distortion-pone-copenhague-boca-201210051530.html)

distortion-copenhague distortiondos-copenhague distortiontres-copenhague

¡Toda una experiencia de ambientazo!

DÍA 5: Segundo día en GR8Conf Europe 2016 – IT University

Pasárselo tan bien en un festival tiene luego sus desventajas cuando te suena el despertador a las 7:30 de la mañana y no te levantas precisamente como una rosa… Pero esta vez yo no me olvidaba de que teníamos desayuno incluido en el hotel así que cuando pensé en toda la comida que podría comer salí como un correcaminos de la habitación y bajé a la cafetería. Tenía media hora para disfrutar de un fantástico buffet. Salimos hacia la universidad y seguimos con las entrevistas y el vídeo. Ese día, la organización había preparado un “Meet&Greet” a las 19:00 con cervezas belgas y alemanas

cervezas-copenhague

DÍA 6: Último día en GR8Conf Europe 2016 y en Copenhague – IT University

Tras terminar el trabajo de este día, Søren Glasius, organizador de GR8Conf Europe, nos felicitó por el trabajo que habíamos hecho y comentó que #gr8conf había sido Trending Topic, algo que nunca antes les había pasado pero que esta vez, gracias al trabajo de Alba Roza en redes sociales, se había conseguido. Volvíamos muy satisfechas. El resultado posterior ha sido muy positivo y podéis verlo en el canal de Autentia Media.

Fuimos al hotel a por las maletas y fuimos hacia la Estación Central para coger el tren que nos llevaba al aeropuerto. ¡Estuvimos a punto de perder el avión y quedarnos en tierra!

Teníamos el vuelo a las 18:55 y cerraban la puerta de embarque a las 18:25. Eran las 17:40, el tren al aeropuerto tarda unos 20 min. Si venía pronto teníamos margen, poco, pero teníamos margen. Fuimos a la vía 5 y cuando miramos la pantalla vimos que el tren venía con retraso. Si su hora de llegada tenían que haber sido las 17:40, su hora prevista era las 18:03. Calculamos. Si viene a las 18:03, llegamos sobre las 18:23. De repente la pantalla cambió a las 18:05, luego a las 18:07… Empezamos a entrar en pánico. Si seguía subiendo y nosotras seguíamos en el andén sin buscar una alternativa, seguro que no íbamos a llegar. Salimos a la calle a buscar un taxi. Nos cogió uno que iba en la dirección del aeropuerto pero tenía que dejar a una señora primero. Le explicamos la situación. Eran las 17:55 y teníamos que estar como máximo a las 18:15 en el aeropuerto.

El taxista nos dijo que lo iba a intentar y nos dijo: “Os cobro lo que marque el taxímetro desde que se baje la señora + 100kr (13€) extra por peligrosidad”. No estaba la cosa como para ponerse a negociar. Luego entendí lo de la “peligrosidad”. Fue adelantando por el carril contrario cuando no venían coches y pisando el acelerador. Llegamos a las 18:15. Salimos corriendo, subimos las escaleras y nos encontramos mucha cola para pasar el control de seguridad. Por suerte delante de nosotras iba un español que tenía que coger el mismo vuelo, pero él estaba bastante tranquilo…nos dio un poco de esperanzas. Cuando vimos que se colaba y se ponía de los primeros para el control, no pudimos evitar hacer lo mismo.

Tuvimos suerte, nadie nos vio, pero para nuestra desgracia… íbamos con tanta prisa que las cervezas y la botella de agua que llevábamos en la mochila de mano habían pasado desapercibidas, para nosotras, pero no para la mujer que estaba mirando por el escaner. Yo lo solucioné rápido tirando la botella a la papelera que había pero a mi compañera Alba le retuvieron su maleta un rato. 18:25. Miramos la pantalla de la puerta de embarque. “Puerta A23 – 9 min andando”. Corrimos y corrimos hasta quedar completamente agotadas. Al llegar estaba el hombre tranquilo de antes y seguía tranquilo. ¡Porque el vuelo no salía a las 18:55 sino a las 19:10 ahora! Estaba todo el mundo sentado esperando y empezaban a salir los ocupantes de nuestro avión que llegaban a Copenhague. Tenían aún que limpiarlo y prepararlo. ¡Qué mal rato!

Nunca me había dado tanta pena dejar una ciudad que no fuera la mía. He vivido una de las mejores experiencias de mi vida y conocido a mucha gente de todas partes del mundo. Me ha gustado mucho poder trabajar fuera, en otro país. Y lo mejor de todo para mí, más allá de todo lo que uno pueda disfrutar ahí, es mandar el resultado del trabajo y que feliciten a Autentia por el trabajo bien hecho y profesional.

Aplicando TDD en concursos

$
0
0

En esta entrada os voy a contar mi experiencia en concursos de programación, por qué en determinados casos es bueno usar TDD y un pequeño ejemplo de cómo aplicarlo en estas circunstancias.


0. Índice de contenidos.



1. ¿A qué viene esta entrada?

A principios del pasado mes de mayo me presenté al Tuenti Challenge. Quería comprobar hasta qué punto se había oxidado mi algoritmia. Al final, por falta tanto de tiempo como de preparación, no obtuve un resultado demasiado bueno. No era el primer concurso al que me presentaba, pero sí el que me ha dado la idea para esta entrada, así que voy a basarme en mi experiencia en él a la hora de escribir esto.

Mientras resolvía los retos, me dí cuenta de que estaba cambiando por completo mi estilo habitual de programación, me centraba en tener la solución lo más rápido posible para refactorizarla y subirla enseguida. Esto en nada se parece al TDD que practicamos en Autentia, la escasez de tiempo y las ganas de hacer lo máximo posible juegan malas pasadas. Cuando me di cuenta era demasiado tarde y ya no podía seguir haciendo más retos. Hacer las cosas bien, aunque tardemos un poco más, puede ahorrarnos mucho tiempo.



2. ¿Qué os voy a contar?

Con esta entrada quiero que veáis que TDD es una práctica más que recomendable para este tipo de concursos. Por supuesto, no busco convenceros de usarlo en cada proyecto que tengáis, es necesario ser flexibles y conocer las ventajas e inconvenientes de nuestras herramientas para sacar el máximo provecho de ellas. Pero en estos casos en particular, creo que la inversión de tiempo merecerá la pena.

Además pretendo ir un paso más allá; quiero mostrar cómo reinterpretar el ciclo natural de TDD para adaptarnos a este tipo de circunstancias, con problemas de rápida implementación en los que el proceso más fuerte de refactorización se aplica sobre una versión ya completamente funcional. No se trata de un cambio drástico en la metodología, no os preocupéis.



3. 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 X El Capitan
  • Entorno de desarrollo: Eclipse Java EE IDE, Mars Release (4.5.2)
  • Java 8
  • Librerías: JUnit 4 y Hamcrest 1.3


4. ¿Por qué nos vamos a plantear usar TDD?

La mayoría de concursos a los que nos podemos enfrentar tienen ciertos factores comunes. Entre ellos hay tres que parecen pedirnos a gritos que “perdamos” un poco de tiempo haciendo tests.

  • Los requisitos, tanto de entrada como de salida, son completamente estáticos; no van a cambiar. Tenemos incluso casos de prueba y ejemplos que facilitan el diseño de los tests.
  • Se suele valorar el estilo de código, la eficiencia. Esto no se consigue en la primera versión, hay un proceso de refactorización bastante fuerte, incluso puede cambiar el algoritmo o las estructuras de datos que usamos. En esta fase, los tests son una ayuda inestimable para detectar rápidamente qué hemos roto y evitar que nos tiremos demasiado de los pelos.
  • En casi todos los retos habrá una serie de casos con un comportamiento ligeramente distinto de los normales. Tener una buena base de tests nos ayuda a saber qué casos estamos contemplando y detectar fallos de análisis más rápidamente.

No obstante, también existen casos en los que seguir TDD no será de demasiada ayuda. No es recomendable hacer TDD cuando los requisitos van a cambiar constantemente, ¿para qué testear un prototipo que seguramente desechemos?. Es el caso de hackathones y concursos de corte similar, donde nos dan las herramientas y tenemos libertad creativa.



5. Advertencias

Vamos a ver cómo afrontar el primero de los retos que proponían desde la perspectiva de TDD. Tened en mente que el efecto no se verá inmediatamente, ya que se trata de una medida de prevención de problemas más que de una herramienta de agilización. Además quiero dejar claro que no todo es seguir un tutorial como éste o saber programar bien, también se necesita cierta cantidad de pensamiento lateral, a veces incluso para llegar a conocer el enunciado que nos proponen.

Voy a dar por hecho que tenéis unas nociones, aunque sean básicas, de qué es TDD y su ciclo de desarrollo (Rojo – Verde – Refactorización). La primera iteración la explicaré detalladamente, pero si lo hiciese con todo el proceso, la entrada (que ya de por sí es larga) se volvería monstruosa. No obstante, en este repositorio está subido el código final. He intentado hacer un commit por cada cambio relevante para que sea más fácil seguir los cambios. Os recomendaría que le echaseis un vistazo mientras leéis esto.



6. Cómo voy a enfrentarme al problema

Las condiciones de trabajo que nos proponen para cada reto son bastante simples:

  • El lenguaje para resolverlo queda a nuestra elección.
  • Nos dan un fichero con los datos de entrada que hay que pasarle al algoritmo.
  • Hay que subir el fichero con los datos de la salida resultante de la ejecución con los datos anteriores y el código en un único fichero.

Me he decantado por Java porque es con el que más cómodo me siento a la hora de programar y hacer testing. Las ideas son fácilmente extrapolares a cualquier otro, no tendréis problemas por eso.

Por su parte, los tests los haremos en una clase separada, como dictan los principios de TDD. No podremos subirlos para corrección, pero tampoco es necesario. En este caso, son solo una herramienta para facilitar el desarrollo de la solución.



7. Procedimiento y ejemplo

Como ya hemos dicho, haremos este tutorial sobre el primero de los retos que se nos proponía: Team Lunch. Nos piden que calculemos el número mínimo de mesas que tendríamos que juntar para sentar a un determinado número de personas, teniendo en cuenta que cada lado de las mesa no puede estar ocupado por más de un comensal. El enunciado completo, con ejemplos, está en el enlace.


7.1. Análisis

El primer paso será, obviamente, leer y analizar el enunciado. Prestaremos especial atención a las entradas, salidas y otros datos característicos, pues son los elementos que más afectarán a nuestros tests. Recomiendo encarecidamente coger papel y bolígrafo durante este proceso, así no nos arriesgamos a dejar nada por el camino.

A continuación, antes de pasar a escribir ni una sola línea de código, pensaremos en la solución. Aún no buscamos la más eficiente ni la mejor implementada, de eso nos preocuparemos después. En este paso debemos centrarnos en identificar un patrón de comportamiento “normal” y aquellos casos que podamos calificar de “especiales”, con esto construiremos nuestro algoritmo y decidiremos cómo organizar los datos que vamos a utilizar.

Tendremos unas estructuras muy simples:

  • 1 entero que indique el número de casos que tenemos.
  • 1 array de tantos enteros como número de casos en lo que tendremos el número de personas que tenemos que sentar en cada uno.
  • 1 array de tantos enteros como número de casos en lo que tendremos el número de mesas necesarias para sentar a todos los asistentes de cada uno.

En base al enunciado y la relación entre entradas y salidas, desarrollaremos un algoritmo que se regirá por estas condiciones iniciales:

  • Por restricción, no es posible que se nos de un número negativo de comensales.
  • Si no hay comensales, no necesitaremos ninguna mesa.
  • Si hay 4 o menos comensales, nos bastará con una sola mesa.
  • Si hay más de 4 comensales y son pares, pondremos a 2 por cada mesa excepto a los dos que presidirán las mesas inicial y final. El número de mesas será (N-2)/2.
  • Si hay más de 4 comensales y son impares, pondremos a 2 por cada mesa excepto al que presidirá la mesa. El número de mesas será (N-1)/2.

7.2. Manos a la obra, ¿cómo adaptamos TDD?

Para lograr la máxima eficiencia, vamos a reinterpretar ligeramente el proceso normal de TDD a nuestras necesidades. Con esto quiero desviar gran parte de la carga de la refactorización a un punto donde ya tenemos tests de todo y corremos menos riesgos de romper nada.

Diagrama del proceso de TDD para concursos.

Primero, y aplicando el ciclo de TDD, crearemos una versión inicial del programa. Aquí la refactorización serán cambios muy pequeños, en su mayoría dirigidos a los tests. Como vemos, dividiremos nuestro código en tres grandes bloques: carga de datos de entrada, salida de los resultados y el algoritmo principal. De esta fase obtendremos una versión inicial plenamente funcional y una batería de tests que cubre todos los casos contemplados.

A continuación nos dedicaremos únicamente a modificar el código que hemos creado para optimizarlo y mejorar su estilo y legibilidad. Como vemos, no siempre pasaremos por la fase en la que los tests no pasan. Cuando estamos haciendo refactorizaciones propiamente dichas, los tests siempre estarán en verde. Solo al hacer mejoras sobre la interfaz de los métodos o puntos clave del análisis deberían fallar. Una vez estemos satisfechos con nuestro código, tendremos ya la versión final que subiremos para evaluación.


7.3. Poniendo los cimientos de nuestro proyecto

Con las estructuras de datos y el algoritmo ya definido, podemos empezar a escribir código. Empecemos por algo similar a esto:

Estructura básica para de la solución de un reto

Tenemos un paquete para la clase principal y otro para los tests. Además hay una carpeta files con dos subdirectorios: input y tests. En el primero pondremos los casos de prueba que nos dan en el concurso y en el segundo los que creemos para nuestros tests.

Para dejar aún más claro el procedimiento que vamos a seguir, iniciamos los tests (y crearemos la clase que usan para quitar los errores de compilación). Observad cómo ya hacemos la distinción en los tres bloques lógicos en los que dividiremos el problema.

Forma inicial de los tests, con los tres bloques de código principales

7.4. Entrada de datos

La entrada de datos será siempre lo primero en lo que trabajaremos. Sería muy difícil poder testear el comportamiento central del algoritmo sin tener un mecanismo para insertar los datos.

Cambiaremos el nombre del test para que sea un poco más descriptivo y escribimos las comprobaciones que necesitamos. Queremos asegurarnos de que el número de casos y el array con los comensales a sentar en cada uno se han iniciado siguiendo los datos del fichero.

@Test
public void shouldCorrectlyInitializeDataStructureFromAnInputFile() throws FileNotFoundException {
    // Given
    String inputFileRoute = "files/tests/test_input.txt";

    // When
    teamLunch.initializeDinnersDataFromInputFile(inputFileRoute);

    // Then
    int casesAmount = teamLunch.getCasesAmount();
    assertThat(casesAmount, is(3));

    int[] dinnersToSitForEachCase = teamLunch.getDinnnersForEachCase();
    assertThat(dinnersToSitForEachCase.length, is(casesAmount));
    assertThat(dinnersToSitForEachCase[0], is(24));
    assertThat(dinnersToSitForEachCase[1], is(5913));
    assertThat(dinnersToSitForEachCase[2], is(3));
}

Ahora crearemos el fichero que hemos dicho que vamos a cargar para que contenga los datos que se amolden a los resultados que esperamos:

3
24
5913
3

Es un test muy simple y, si nos ceñimos a las guías de TDD al 100%, no del todo correcto. En realidad lo que estamos comprobando es que se devuelven unos valores concretos. En ese caso, el test pasaría aún cambiando el fichero, siempre que mantuviésemos los mismos asserts. Además, como nosotros sí iniciamos correctamente los datos, si cambiásemos el fichero pero no los valores que esperamos el test fallaría a pesar de que nuestro método hace lo que debe. Al tratar con un ejemplo, y para no complicarnos demasiado, vamos a dejarlo así. No obstante, una posible opción sería crear al principio del test el fichero con valores aleatorios y usar estos mismos valores generados en las comprobaciones.

A continuación, poblaremos la clase principal con los métodos que usamos para no tener errores de compilación y lanzamos el test. Por supuesto, fallará porque no hemos implementado aún el comportamiento. Primer paso del TDD conseguido, el test debe estar en rojo. Completemos ahora el código para lograr que pase el test:

public class TeamLunch {

    private int casesAmount;

    private int[] dinnersForEachCase;

    public void initializeDinnersDataFromInputFile(String inputFileRoute) throws NumberFormatException, IOException {
        BufferedReader reader = new BufferedReader(new FileReader(inputFileRoute));
        casesAmount = Integer.parseInt(reader.readLine());
        dinnersForEachCase = new int[casesAmount];
        for (int i = 0; i < casesAmount; i++) {
            dinnersForEachCase[i] = Integer.parseInt(reader.readLine());
        }
        reader.close();
    }

    public int getCasesAmount() {
        return casesAmount;
    }

    public int[] getDinnnersForEachCase() {
        return dinnersForEachCase;
    }
}

Si ejecutamos de nuevo el test, esta vez pasará. Segunda parte del ciclo de TDD lograda, lo hemos puesto en verde. Ahora vendría la tercera y última fase de TDD: refactorización. En este caso no hay mucho que hacer. Únicamente extraeremos la carga de los datos para cada caso a otro método, por hacer el código más legible.

public void initializeDinnersDataFromInputFile(String inputFileRoute) throws NumberFormatException, IOException {
    BufferedReader reader = new BufferedReader(new FileReader(inputFileRoute));
    casesAmount = Integer.parseInt(reader.readLine());
    loadDinnersData(reader);
    reader.close();
}

private void loadDinnersData(BufferedReader reader) throws IOException {
    dinnersForEachCase = new int[casesAmount];
    for (int i = 0; i < casesAmount; i++) {
        dinnersForEachCase[i] = Integer.parseInt(reader.readLine());
    }
}

Al lanzar el test vemos que sigue pasando, por lo que hemos hecho todo bien. De ahora en adelante no comentaré tanto los pasos que he ido haciendo, pero podéis verlos en el repositorio.


7.5. Devolviendo los resultados

Al testear la salida de datos nos encontramos con un problema. ¿Cómo especificar el valor de lo que queremos que salga en el fichero?¿Hacemos un setter para la estructura de datos que guardará los resultados? ¿Lo dejamos para el final y la hacemos dependiente del algoritmo?

Yo me decidí por inicializar las estructuras de los resultados con un valor por defecto al leer del fichero. Es una solución sencilla que genera pocas dependencias y no complica innecesariamente unos tests que no se van a entregar. Observad que creamos un test distinto en lugar de reutilizar el que teníamos. Al fin y al cabo vamos a comprobar algo que, aunque se haga en el mismo sitio, corresponde a una lógica distinta.

@Test
public void shouldCorrectlyInitilizeStructuresForResultsAfterReadingFromFile()
        throws NumberFormatException, IOException{
 // Given
    String inputFileRoute = "files/tests/test_input.txt";

    // When
    teamLunch.initializeDinnersDataFromInputFile(inputFileRoute);

    // Then
    int casesAmount = teamLunch.getCasesAmount();
    assertThat(casesAmount, is(3));

    int[] tablesAmountForEachCase = teamLunch.getTablesAmountForEachCase();
    assertThat(tablesAmountForEachCase.length, is(casesAmount));
    for (int i = 0; i < teamLunch.getCasesAmount(); i++) {
        assertThat(tablesAmountForEachCase[i], is(-1));
    }
}

Cuando lo resolvamos, ya podremos testear como se vuelcan los resultados a un fichero. No es necesario comprobar qué valor exacto tienen los datos. Solo asegurarnos que lo que hemos mostrado es lo que correspondía con lo que había en las estructuras de datos.

@Test
public void shouldCreateAnOutputFileWithOneLineDescribingEachCase() throws IOException {
    // Given
    String outputFileRoute = "files/tests/test_output.txt";

    // When
    teamLunch.writeResultsInFile(outputFileRoute);

    // Then
    BufferedReader reader = new BufferedReader(new FileReader(outputFileRoute));
    int[] tablesAmountForEachCase = teamLunch.getTablesAmountForEachCase();
    for (int i = 0; i < teamLunch.getCasesAmount(); i++) {
        assertThat(reader.readLine(), equalTo("Case #" + i + ": " + tablesAmountForEachCase[0]));
    }
    assertThat(reader.read(), is(-1));
    reader.close();
}

7.6. Algoritmo principal

Con las entradas y salidas aseguradas, centremos la atención en el plato principal. Al haber hecho un análisis previo, el trabajo casi se reduce a hacer un test para cada caso encontrado.

Una ventaja de trabajar con un producto tan pequeños es poder empezar por los casos especiales e ir generalizando o al revés; como cada uno se organice mejor. Lo importante es que todos estén contemplados para poder conocer qué hace el algoritmo solamente echando un vistazo a los nombres de los tests. Aquí lo más problemático (y una de mis carencias) es la habilidad para poner nombres descriptivos.

@Test
public void shouldNeedZeroTableWhenThereAreNoDinners() { }

@Test
public void shouldGoThroughAllCasesInTheProcessMethod() { }

@Test
public void shouldNeedOneTableWhenThereAreLessThanFiveDinners() { }

@Test
public void shouldNeedOneTablePerCoupleMinusTwoInBordersWhenMoreThanFourDinnersAndEvenNumber() { }

@Test
public void shouldNeedOneTablePerCoupleMinusOneInBorderWhenMoreThanFourDinnersAndOddNumber() { }

Quiero llamar la atención sobre el test que comprueba que estamos trabajando sobre todos los casos que nos llegan y no solo sobre el primero. Me va a servir para ilustrar que no podemos limitarnos a testear solo los puntos que hemos detectado durante el análisis. También debemos tener en cuenta las peculiaridades de las estructuras que hayamos escogido y asegurarnos de hacer todo el procesamiento correctamente.

Una vez que todos los requisitos están probados e implementados, deberíamos poder resolver las pruebas que nos pone Tuenti para poder subir el fichero. En caso contrario tocaría darle una vuelta al análisis y tests buscando qué se nos ha olvidado contemplar.


7.7. Refactorización y mejora del código

Llegado a este punto, es el momento de que los tests que hemos hecho nos ayuden de verdad. Vamos a modificar el código una y otra vez hasta lograr una versión óptima de la que no nos avergoncemos.

Solo tenemos que seguir un par de reglas. Si estamos haciendo una refactorización (que únicamente debería tocar el interior de un método pero no su interfaz) los tests deben de seguir pasando en todo momento porque la funcionalidad es la misma.

Si hacemos una mejora de cualquier tipo (casos analizados, estructuras en las que almacenamos datos de entrada o resultados…), deberemos primero cambiar los tests para adaptarnos a la misma situación. Por lo tanto estarán en rojo hasta que terminemos de implementar la mejora.

Aunque podamos mejorar cualquier parte, la entrada y salida de datos apenas suelen tocarse porque son similares a todos los retos. En este caso haremos tres modificaciones principales en el código.

  1. Simplificar nuestro if. Podemos ver que en caso de que tengamos más de 4 comensales, sin importar su paridad, la cantidad de mesas que necesitamos será de ⌈(D-2) / 2⌉ (el entero más pequeño que sea mayor o igual a esa cantidad), donde D es el número de comensales de cada caso.
  2. Extraer el procesamiento de un caso concreto a un método privado separado. Esto es principalmente para aumentar la legibilidad del código.
  3. Si volvemos a comprobar el análisis que hicimos, vemos que en realidad el límite para los casos especiales no es el 4, sino el 2. Así que vamos a adaptar el código para esta nueva especificación. En este caso sí que cambiaremos ligeramente los tests, pues aunque la salida vaya a ser la misma no lo serán los casos con los que trabajamos.

Cada vez que completemos uno de estos puntos lanzaremos los tests y el programa con el input de prueba que nos dan. Si los tests siguen pasando significará que no hemos alterado la lógica de nuestra aplicación. De forma similar, podemos usar los datos de entrada de ejemplo para asegurarnos que no hemos dado lugar a un comportamiento extraño que no identificamos en el proceso de análisis.



8. Y ya hemos terminado

Ahora ya tenemos una versión optimizada de nuestro código, que es más legible y se adapta mejor a los requisitos. Y gracias a los tests hemos podido asegurar que durante el proceso todo ha seguido funcionando igual. Seguramente el código se pueda mejorar aún, pero para este ejemplo creo que es suficiente. Hemos hecho una refactorización donde mejoramos la eficiencia del código uniendo dos casos de un if en uno, otra en la que extraemos un método para aumentar la legibilidad y una mejora en la toma de requisitos que detectamos inicialmente (con su correspondiente cambio en tests). Con esto tenemos una muestra de 3 de las modificaciones más probables con las que nos podremos topar durante un proceso de refactorización.

Solo nos quedaría subir el código que hemos creado y prepararnos para lo que espere en el siguiente reto…



9. Conclusiones

Después de una entrada tan larga como esta (he tratado de hacerla todo lo breve posible, pero se me da muy mal) siempre debemos hacer un resumen de las conclusiones a las que hemos llegado.

Cuando afrontamos un problema, una buena batería de pruebas siempre nos ayudará. Aunque a veces no podamos dedicar tiempo a crearla, concursos como este tienen las características idóneas para que ese tiempo sea una muy buena inversión. Si no dejamos que las prisas nos cieguen, vemos que nos permite montar una fantástica defensa contra nuestros propios errores. TDD es la metodología por excelencia para crear estos tests.

Yo he intentado explicar lo mejor posible por qué puede sernos de utilidad y cómo aplicarlo a los concursos. Ahora solo me queda animaros a que comenteis cualquier cosa que queráis y que os planteéis seriamente usarlo la próxima vez que tengáis oportunidad.


15 preguntas clave para realizar una buena división de historias de usuario.

$
0
0

En este artículo presentaremos 15 preguntas clave que te ayudarán a evaluar si una determinada historia de usuario puede ser susceptible de ser dividida para poder cumplir con los principios INVEST.

Índice de contenidos


1. Introducción

La división de historias de usuario es un arte que uno mismo va mejorando a través de la experiencia, y que si eres una persona muy perfeccionista, seguramente siempre estés intentando verificar si la historia que acabas de generar es “perfecta”.

Para aportar más a la causa, en este artículo os presento las 15 preguntas clave que podemos plantearnos para verificar si nuestra historia ha alcanzado un cierto grado de perfección, y que sin duda cumple con los principios INVEST.


2. ¿Por qué debería leer este artículo?

Cuando se generan las historias de usuario (H.U.) en las que se desglosará nuestro proyecto, no siempre es fácil hacer que cumplan los principios INVEST (sobretodo, si es de las primeras veces que creamos historias).

Existe mucha documentación al respecto sobre estos principios, por lo que vamos a recordar muy rápidamente qué significado tiene el acrónimo INVEST antes de entrar en faena.

  • I: Independant
  • N: Negotiable
  • V: Valuable
  • E: Estimable
  • S: Small
  • T: Testable

Por lo tanto, cada una de nuestras H.U. deberían ser independientes unas de otras, deberían aporten valor al usuario final, deberían poder ser estimadas y lo suficientemente pequeñas como para poder abordarlas en un sprint, y sin duda, deberían estar formuladas para que puedan ser testeadas convenientemente.


3. Las 15 preguntas clave para realizar una buena división de Historias de Usuario

A continuación evaluaremos una teórica H.U. recién creada, que por ejemplo, acabemos de generar en nuestro backlog.

3.1 Evaluando la H.U. creada

Disponemos de nuestra H.U. recién creada, y queremos evaluar si debemos dividirla o no. Para ello, podemos plantearnos a las siguientes preguntas:

  • ¿La historia recién creada cumple con los criterios INVEST?
  • ¿El tamaño de la historia es de 1/10 a un 1/6 de la velocidad del equipo?

Si no cumple con los criterios INVEST, entonces podemos combinarla con otra historia o bien, reformularla para obtener una buena historia de la que partir.

Si el tamaño de la historia no está encuadrada en esta fracción de velocidad del equipo, entonces podemos asegurar que nuestra historia es demasiado grande, y que por lo tanto necesitamos dividirlas.

Y os podéis preguntar, ¿y qué pasa si todavía no conozco la velocidad de mi equipo?, pues puedes basarte en tu propia experiencia con equipos anteriores para tener una aproximación.

3.2 Aplicando patrones de división de historias

Imaginemos por tanto, que en base a las preguntas anteriores, concretamos que necesitamos dividir nuestra historia de usuario.

Para dividirla podemos atender a diversos criterios, evaluando si nos aplican o no a través de las siguientes preguntas:


3.2.1 División en base al workflow de la historia

En caso de que la historia de usuario contenga algún tipo de workflow, podemos plantearnos las siguientes preguntas:

  • ¿Se podría dividir la historia para que se haga el principio y el final del workflow primero, y posteriormente enriquecer el grueso del workflow a través de otras historias?
  • ¿Se podría dividir la historia para que se realice la forma más básica posible del workflow, y posteriormente ir enriqueciendo el mismo con otras historias?

3.2.2 División en base a las operaciones que se realizan

En caso de que la historia de usuario incluya múltiples operaciones, es decir, esté relacionada con “administrar” o “configurar” algo, podemos plantearnos las siguientes preguntas:

  • ¿Se podrían separar las operaciones en distintas historias de usuario?

3.2.3. División en base a las variaciones en las reglas de negocio

En caso de que la historia de usuario contenga reglas de negocio, existe la posibilidad de que la historia establezca conceptos que puedan sugerir variaciones en dichas reglas, por ejemplo, términos como “fechas flexibles“. En ese caso:

  • ¿Se podría dividir la historia para abordar primero un subconjunto de las reglas, y enriquecerlas con historias adicionales posteriormente?

3.2.4. División en base a las variaciones en los datos

En caso de que la historia de usuario establezca que se debe realizar la misma acción en diferentes juegos de datos, deberíamos plantearnos la siguiente pregunta:

  • ¿Se podría dividir la historia para procesar un único tipo de dato primero, y aumentar los tipos de datos soportados con historias posteriores?

3.2.5. División en base a las variaciones en la interfaz

Para el caso de que la historia disponga de una interfaz compleja, podemos plantearnos las siguientes preguntas:

  • ¿Se podría identificar una versión reducida del interfaz que se pudiera hacer primero?

Por otra parte, para el caso en el que nuestra historia de usuario sugiera que debe recibir el mismo tipo de datos a través de múltiples interfaces, podemos plantearnos que:

  • ¿Se podría dividir la historia para manejar los datos a través de un único interfaz al principio, e ir enriqueciéndolo con historias posteriores?

3.2.6. División en base a simplicidad / complejidad

En caso de que dentro de nuestra historia hayamos identificado un core simple que proporcione la mayor parte del valor de la historia, podemos plantearnos:

  • ¿Se podría dividir la historia para construir primero ese core, y posteriormente ir enriqueciéndolo con nuevas historias?

3.2.7. División en base a retrasar los requerimientos no funcionales

En caso de que en la historia se identifique excesiva complejidad para satisfacer requerimientos no funcionales, como por ejemplo, el rendimiento, nos podemos plantear:

  • ¿Se podría dividir la historia para construir los requisitos funcionales primero, y posteriormente cumplir los requisitos no funcionales del producto con historias posteriores?

3.3 Evaluando la división de la historia

Una vez que hemos aplicado uno o varios patrones de división de historias de usuario a través de estas preguntas, toca evaluar si la historia ha quedado suficientemente dividida pudiendo utilizar la siguientes preguntas:

  • ¿Las nuevas historias de usuario resultantes, son relativamente iguales en tamaño?
  • ¿Cada una de las nuevas historias son aproximadamente de 1/10 a 1/6 de nuestra velocidad de equipo?
  • ¿Cada una de las nuevas historias satisface los principios INVEST?
  • ¿Han aparecido historias que pueden bajar su prioridad, o incluso, ser borradas?
  • ¿Ha aparecido una historia muy obvia con la que empezar, por que es la que nos va aportar el máximo valor de forma temprana, o conocimiento, o va a mitigar el riesgo, etc..?

En caso de que a cada una de estas preguntas hayamos contestado con un , hemos concluido con éxito la división de nuestra historia de usuario, aunque podríamos seguir evaluando si aplicando distintos patrones de división funciona mejor.

En caso de que a alguna de estas preguntas, la respuesta haya sido un NO, debemos evaluar el uso de otro patrón de división de historia a nuestra historia original, con el objetivo de ir refinando la misma y obtener una buena historia de usuario.


4. Conclusiones

Como comentábamos al inicio de este artículo, la división de la historia de usuario perfecta viene con el tiempo y la experiencia (e incluso podemos plantear si existe la H.U. perfecta).

Con este conjunto de preguntas, podremos evaluar si nuestra historia de usuario puede ser susceptible de ser dividida, y por lo tanto, podamos conseguir una H.U. que cumpla con los principios INVEST.


5. Referencias

Protege tu API Rest mediante JSON Web Tokens

$
0
0

En este artículo veremos cómo proteger una API REST empleando JSON Web Tokens

Índice de contenidos


1. Introducción

En este artículo veremos cómo proteger una API REST empleando JSON Web Tokens. Será una API sencillita, la típica aplicación de notas (sí, ya se que no es muy original, que le vamos a hacer :-/)

Crearemos un pequeño proyecto de ejemplo de API REST con Node.js + Express.js, gestionando la autenticación mediante la librería Passport.js, y además empleando Typescript (3×1 en tutoriales ;-)) . Puedes clonar el repositorio https://github.com/DaniOtero/jwt-express-demo

Para la realización de este tutorial se da por hecho que se cuenta con un entorno con Node.js correctamente configurado.


2. Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro 15’ (2.5 GHz Intel Core i7, 16 GB 1600 MHz DDR3).
  • Sistema Operativo: Mac OS X El Capitán 10.11.2
  • Google Chrome 50.0.2661.102
  • Node.js 6.2.2
  • Postman
  • Atom 1.8.0

3. JWT

JSON Web Token (de ahora en adelante JWT) es un estándar abierto (RFC 7519) que nos permite la transmisión de forma segura y confiable gracias a su firma digital. Los token pueden ser firmados mediante clave simétrica (algoritmo HMAC) o mediante clave asimétrica (RSA)

Algunas de las ventajas de los JWT son su reducido tamaño, ya que al ser JSON tan solo añaden unos pocos bytes a nuestras peticiones contra el servidor, y otra de las mayores ventajas es que el payload del JWT contiene toda la información que necesitemos sobre el usuario, de forma que se evita la necesidad de repetir consultas a base de datos para obtener estos datos.

Los escenarios mas comunes donde utilizar JWT son en la autenticación, y en el intercambio de información (como pueden ser firmados mediante clave asimétrica, se puede verificar la identidad del emisor del mensaje, y puesto que la firma del JWT se calcula empleando el payload, también se verifica que el contenido del mensaje no ha sido manipulado)


4. Dependencias y configuración del entorno

Para facilitar todo lo relativo al proceso de transpilacion al estar utilizando TypeScript se utilizará la herramienta gulp. También se utilizará la herramienta typings para la gestión de las definiciones de TypeScript. Se pueden instalar ambos de forma global utilizando el comando

npm install -g gulp typings

Una vez hecho esto, crearemos un directorio donde albergar el proyecto, y sobre dicho directorio ejecutaremos el comando “npm init” y una vez generado el fichero package.json lo editaremos para añadir las dependencias.

{
  "name": "jwt-rest",
  "version": "1.0.0",
  "description": "",
  "main": "main.ts",
  "scripts": {
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "body-parser": "~1.13.2",
    "cookie-parser": "~1.3.5",
    "debug": "~2.2.0",
    "express": "~4.13.1",
    "morgan": "~1.6.1",
    "passport": "^0.3.2",
    "passport-jwt": "^2.1.0",
    "underscore": "^1.8.3"
  },
  "devDependencies": {
      "gulp": "^3.9.0",
      "gulp-clean": "^0.3.1",
      "gulp-develop-server": "^0.5.0",
      "gulp-mocha": "^2.2.0",
      "gulp-typescript": "^2.10.0"
  }
}

Tras editarlo ejecutaremos “npm install” para descargar las dependencias. Ahora configuraremos las definiciones de TypeScript, primero ejecutamos el comando “typings init”, que nos generará el fichero typings.json, y que editaremos con lo siguiente:

{
    "name": "jwt-rest",
    "dependencies": {},
    "globalDependencies": {
        "bcrypt": "registry:dt/bcrypt#0.0.0+20160316155526",
        "express": "registry:dt/express#4.0.0+20160317120654",
        "express-serve-static-core": "registry:dt/express-serve-static-core#0.0.0+20160625155614",
        "jsonwebtoken": "registry:dt/jsonwebtoken#0.0.0+20160521152605",
        "mime": "registry:dt/mime#0.0.0+20160316155526",
        "node": "registry:dt/node#6.0.0+20160621231320",
        "passport": "registry:dt/passport#0.2.0+20160317120654",
        "passport-jwt": "registry:dt/passport-jwt#2.0.0+20160330163949",
        "passport-strategy": "registry:dt/passport-strategy#0.2.0+20160316155526",
        "serve-static": "registry:dt/serve-static#0.0.0+20160606155157",
        "typescript": "registry:dt/typescript#0.4.0+20160317120654",
        "underscore": "registry:dt/underscore#1.7.0+20160622050840"
    }
}

Crearemos nuestras tareas de gulp en el fichero gulpfile.js

var gulp = require('gulp');
var ts = require('gulp-typescript');
var clean = require('gulp-clean');
var server = require('gulp-develop-server');
var mocha = require('gulp-mocha');

var serverTS = ["**/*.ts", "!node_modules/**", '!bin/**'];

gulp.task('ts', ['clean'], function() {
    return gulp
        .src(serverTS, {base: './'})
        .pipe(ts({ module: 'commonjs', noImplicitAny: true }))
        .pipe(gulp.dest('./'));
});

gulp.task('clean', function () {
    return gulp
        .src([
            'app.js',
            '**/*.js',
            '**/*.js.map',
            '!node_modules/**',
            '!gulpfile.js',
            '!bin/**'
        ], {read: false})
        .pipe(clean())
});

gulp.task('server:start', ['ts'], function() {
    server.listen({path: 'main.js'}, function(error) {
        console.log(error);
    });
});

gulp.task('server:restart', ['ts'], function() {
    server.restart();
});

gulp.task('default', ['server:start'], function() {
    gulp.watch(serverTS, ['server:restart']);
});

Y por último el fichero tsconfig.json con la configuración del transpilador de TypeScript.

{
    "compilerOptions": {
        "target": "es5",
        "module": "commonjs",
        "moduleResolution": "node",
        "isolatedModules": false,
        "jsx": "react",
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true,
        "declaration": false,
        "noImplicitAny": false,
        "noImplicitUseStrict": false,
        "removeComments": true,
        "noLib": false,
        "preserveConstEnums": true,
        "suppressImplicitAnyIndexErrors": true
    },
    "exclude": [
        "node_modules"
    ],
    "compileOnSave": true,
    "buildOnSave": false,
    "atom": {
        "rewriteTsconfig": false
    },
    "filesGlob": ["**/*.ts"]
}

Con esto ya tendríamos el entorno preparado


5. Esqueleto del proyecto

Creamos 3 carpetas, “endpoints”, “models” y “services”. Comenzaremos por los modelos, para ello dentro de la carpeta “models” crearemos el fichero “user.ts”:

import {Note} from './note';

export interface User {
    id : string
    password: string,
    notes?: [Note]
}

Y el fichero “note.ts”

export interface Note {
    name: string;
    text: string;
}

Ahora creamos nuestro servicio. Para simplificar el tutorial, se prescindirá de cualquier tipo de base de datos, y lo que se utilizará será una lista de objetos en memoria. Crearemos el fichero “services/user.service.ts”:

import {User} from '../models/user';
import {Note} from '../models/note';
import * as _ from 'underscore';

export class UserService {
    private static users : [User] = [
        {id: "test", password: "1234"},
        {id: "test2", password: "1234"},
        {id: "test3", password: "1234"},
    ];

    static findById(id: string) : User {
        let user : User = _.find(this.users, (user)=> {
            return user.id === id;
        })
        return user;
    }

    static addNote(id: string, note: Note) {
        let user = this.findById(id);
        if(!user || !note) {
            throw new Error("Bad request");
        } else {
            if (!user.notes) {
                user.notes = [note];
            } else {
                user.notes.push(note);
            }
        }
    }
}

Ahora crearemos los endpoint de la API. Creamos el fichero “endpoints/user.ts”:

import {Request, Response} from "express";
var express = require('express');
var router = express.Router();
import {UserService} from '../services/user.service'
import jsonwebtoken = require ('jsonwebtoken')

router.post('/login', function(req: Request, res: Response, next: Function) {
    let username = req.body.username;
    let password = req.body.password;
    let user = UserService.findById(username);
    if(user && (password === user.password)) {
        res.sendStatus(200);
    } else {
        res.sendStatus(403);
    }
});

export default router;

Y el fichero “endpoints/notes.ts”

import {UserService} from '../services/user.service';
import {Note} from '../models/note';

import {Request, Response} from "express";
var express = require('express');
var router = express.Router();
var passport = require('passport');


router.get('/:user_id', { session: false}), (req: Request, res: Response, next: Function) => {
    let id = req.params.user_id;
    try {
        let notes = UserService.findById(id).notes;
        res.json({notes: notes});
    } catch(err) {
        res.sendStatus(400);
    }
});

router.post('/:user_id', (req: Request, res: Response, next: Function) => {
    let handleError = () => {
        res.sendStatus(400);
    }

    let id = req.params.user_id;
    let note : Note = req.body.note;
    try {
        UserService.addNote(id, note);
        res.sendStatus(201);
    } catch(err) {
        handleError();
    }
})

export default router;

Con esto tendríamos nuestros endpoints pero sin ningún tipo de autenticación, con lo cual cualquier usuario podría publicar notas en nombre de otro.

Con esto ya tenemos nuestros modelos, nuestro servicio y los endpoints, ahora toca unirlo todo. Sobre la raíz del directorio del proyecto, crearemos el fichero “app.ts” donde cargaremos los módulos principales de nuestra aplicación y realizaremos la configuración de nuestra aplicación.

import {Request, Response} from "express";
var express = require('express');
var path = require('path');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');


import users from './endpoints/user';
import notes from './endpoints/notes';

var passportModule = require('passport');
import {Strategy, ExtractJwt, StrategyOptions} from 'passport-jwt'
import {UserService} from './services/user.service'
const app = express();

let apiVersion = 'v1'

app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use('/' + apiVersion + '/user', users);
app.use('/' + apiVersion + '/notes', notes);

// Configure passport js
let opts : StrategyOptions = {
    jwtFromRequest: ExtractJwt.fromAuthHeader(),
    secretOrKey: 'secret'
}

let passport = passportModule.use(new Strategy(opts, (jwtPayload, done) => {
    let user = UserService.findById(jwtPayload.sub);
    if(!user) {
        return done(null, false);
    } else {
        done(null, user);
    }
}))

// catch 404 and forward to error handler
app.use((req: Request, res: Response, next: Function) => {
  var err: any = new Error('Not Found');
  err.status = 404;
  next(err);
});

// error handlers
app.use(function(err: any, req: Request, res: Response, next: Function) {
    console.log(err.message);
    res.sendStatus(err.status || 500);
});

export default app;

Ahora tendríamos nuestra aplicación casi lista para funcionar. Hemos configurado express para utilizar los endpoint y hemos configurado passport.js para que utilice una estrategia de autenticación mediante JWT (si no conocéis el patrón Strategy ya estáis tardando en darle un repaso ;-)).

Fijaos que en la configuración de la estrategia le hemos dicho que utilice como clave ‘secret’ (por lo que más queráis, jamás utilicéis algo así como clave, esto es solo un ejemplo), y que extraiga el token de la cabecera (un poco más abajo retomaremos este tema). Uno de los parámetros es un callback. Este callback se llamará tras decodificar el JWT, y nos proporcionará el payload del JWT enviado. A partir del payload decodificado podemos obtener la información correspondiente al usuario y devolverla.

Por último, crearemos el fichero “main.ts”, que será el punto de entrada a nuestra app e iniciara el server.

/**
 * Module dependencies.
 */

var debug = require('debug')('mosway:server');
var http = require('http');
import app from './app';

/**
 * Get port from environment and store in Express.
 */

var port = normalizePort(process.env.PORT || '3000');
app.set('port', port);

/**
 * Create HTTP server.
 */

var server = http.createServer(app);

/**
 * Listen on provided port, on all network interfaces.
 */

server.listen(port);
server.on('error', onError);
server.on('listening', onListening);

/**
 * Normalize a port into a number, string, or false.
 */

function normalizePort(val: any) {
  var port = parseInt(val, 10);

  if (isNaN(port)) {
    // named pipe
    return val;
  }

  if (port >= 0) {
    // port number
    return port;
  }

  return false;
}

/**
 * Event listener for HTTP server "error" event.
 */

function onError(error: any) {
  if (error.syscall !== 'listen') {
    throw error;
  }

  var bind = typeof port === 'string'
    ? 'Pipe ' + port
    : 'Port ' + port;

  // handle specific listen errors with friendly messages
  switch (error.code) {
    case 'EACCES':
      console.error(bind + ' requires elevated privileges');
      process.exit(1);
      break;
    case 'EADDRINUSE':
      console.error(bind + ' is already in use');
      process.exit(1);
      break;
    default:
      throw error;
  }
}

/**
 * Event listener for HTTP server "listening" event.
 */

function onListening() {
  var addr = server.address();
  var bind = typeof addr === 'string'
    ? 'pipe ' + addr
    : 'port ' + addr.port;
  debug('Listening on ' + bind);
}

Con esto ya tendriamos una versión básica de nuestra API sin emplear aún los JWT


6. Generación del token JWT

Vamos a refactorizar un poco nuestro código para generar y enviar el token JWT cuando el usuario realice login. Para ello modificaremos el código “endpoints/user.ts”, de forma que antes de enviar el código de estado generemos y añadamos a las cabeceras el token JWT. En el payload utilizaremos el id del usuario como “subject”. También convendría utilizar el campo “exp” para definir cuando expira el token, y comprobar que el token no ha expirado cuando se realiza la petición, pero para el propósito de este tutorial es suficiente con el subject.

if(user && (password === user.password)) {
    res.set('jwt', jsonwebtoken.sign({sub: user.id}, 'secret'));
    res.sendStatus(200);
}

Fácil, ¿verdad? Comentar que para ese ejemplo solo hemos añadido el ID del usuario como subject del payload del JWT. Podemos añadir cualquier información que creamos conveniente al payload, solo hay que tener cuenta que algunos de los campos (como iss (issuer), exp (expiration time), sub (subject), aud (audience) entre otros están reservados).

Como estamos utilizando gulp, al detectar cambios en el código se encargará de transpilar de nuevo y rearrancar el server. Si utilizando Postman o alguna herramienta similar realizamos ahora una petición POST contra nuestro servidor, veremos que además del código de estado nos devolverá en las cabeceras el token JWT.

POST /v1/user/login HTTP/1.1
Host: localhost:3000
Content-Type: application/json
{
    "username" : "test",
    "password" : "1234"
}
login

7. Autenticación

Ahora que nuestro mecanismo de login nos devuelve el token JWT, vamos a restringir el acceso al endpoint de notas. Para ello lo que debemos hacer es leer las cabeceras de la petición HTTP, comprobar si existe el campo “Authorization”, extraer el token, decodificarlo, comprobar que la firma coincide, y si todos esos pasos han ido bien podemos asegurar que se trata de un usuario autenticado.

Por suerte passport.js nos proporciona un middleware para realizar la autenticación. Además de rechazar la petición con un código de estado 401 si la validación del JWT falla, este middleware también nos añade la información del usuario a la request, (recordemos que los middleware de Express.js están basados en el patron Chain Of Resposability, de forma que el siguiente eslabón de la cadena o middleware recibirá la información proporcionada por middleware de Passport.js). De esta forma, gracias al JWT podemos saber quién hace la petición. Vamos a refactorizar nuestro fichero “endpoints/notes.ts”

Lo primero que haremos será incluir el middleware de Passport.js a nuestro router, de tal forma que se ejecute para todas las llamadas al endpoint. Una vez hecho esto, ya no necesitaremos indicar el userID como parámetro de la petición HTTP puesto que se extrae del JWT, con lo cual nuestro endpoint quedaría así

import {UserService} from '../services/user.service';
import {Note} from '../models/note';

import {Request, Response} from "express";
var express = require('express');
var router = express.Router();
var passport = require('passport');

router.use(passport.authenticate('jwt', { session: false}))


/* GET home page. */
router.get('/', (req: Request, res: Response, next: Function) => {
    try {
        let notes = UserService.findById(req.user.id).notes;
        res.json({notes: notes});
    } catch(err) {
        res.sendStatus(400);
    }
});

router.post('/', (req: Request, res: Response, next: Function) => {
    let handleError = () => {
        res.sendStatus(400);
    }

    let note : Note = req.body.note;
    try {
        UserService.addNote(req.user.id, note);
        res.sendStatus(201);
    } catch(err) {
        handleError();
    }
})

export default router;

Con esto ya tendríamos securizado nuestro endpoint. Puesto que la firma del token se calcula en base al payload, si un usuario intentase recodificar su token para hacerse pasar por otro usuario la firma no sería valida y el servidor le denegaría el acceso.

post get

8. Conclusiones

Mediante el uso de JSON Web Tokens se hace bastante sencillo implementar la autenticación de nuestras API rest, sobre todo si se compara con Oauth que requiere de un aprendizaje más profundo. En el caso de Express.js, junto con la librería Passport.js la implementación se hace verdaderamente sencilla. Como principal desventaja, es que no existe manera de revocar un JWT, con lo cual se deben asignar tiempos de expiración bastante ajustados para evitar que en caso de que un token haya sido comprometido pueda ser utilizado de forma indefinida.


9. Referencias

http://expressjs.com
https://jwt.io/introduction
http://passportjs.org
https://www.npmjs.com/package/passport-jwt

Influencia: La psicología de la persuasión

$
0
0

Este artículo es un resumen del libro “Influencia: La psicología de la persuasión” que explica seis principios clave de persuasión, por qué funcionan, y cómo aprovecharlos o resistirlos.

  1. Persuasión
    1. Confirmación social
    2. Escasez
    3. Atracción
    4. Reciprocidad
    5. Consistencia
    6. Autoridad
  2. Conclusión

Persuasión

Persuadir es influir sobre el pensamiento ajeno para ajustarlo a nuestros fines. En la vida diaria somos sujeto y objeto de persuasión, ya sea animando a alguien a venir a la playa, o recibiendo la publicidad de empresas y partidos políticos.

Los profesionales del marketing saben que la persuasión opera sobre dos tipos de razonamiento: el deliberado y el automático.

  • El pensamiento deliberado es el que usamos cuando enfocamos nuestra atención en analizar algo. Es mayormente racional, aunque tiende a ignorar hechos que amenazan nuestra autoestima, identidad, o supervivencia, y es vulnerable a las falacias lógicas.
  • El pensamiento automático es el que nos permite conducir sin prestar atención. Este pensamiento atribuye estereotipos, prejuicios, completa rimas, confunde correlación con causa, y sobreestima acontecimientos dramáticos. No es perfecto, pero nos permite navegar un mundo complejo con un mínimo esfuerzo.

El pensamiento automático es el más vulnerable a los engaños. Tanto es así que los intentos de manipulación pueden ocurrir sin disimulos y ser igualmente efectivos. Los perfumes por ejemplo, no tienen cualidades que puedan venderse a distancia asi que se promocionan asociando la vida glamurosa de algún famoso.

El libro “Influencia” se centra en seis tendencias concretas de nuestro pensamiento instintivo:

  • Imitar a otros (confirmación social)
  • Sobrevalorar oportunidades (escasez)
  • Responder a peticiones de alguien
    • que es un ser querido (atracción)
    • que nos hace un favor (reciprocidad)
    • que nos pidió un favor pequeño y ahora nos pide otro más grande (consistencia)
    • que tiene autoridad (autoridad)

Estas seis características son rasgos evolutivos que facilitan la cohesión social y se dan en todas las culturas. Desafortunadamente, en el contexto del mundo moderno también nos hace vulnerables. La solución es ayudarnos del pensamiento consciente cuando sospechamos que intentan manipularnos.

Confirmación social

cuida el medio ambiente y ahorrame una toalla

El principio de confirmación social se basa en creer que la gente a mi alrededor sabe mejor que yo que hacer. Imitar a mis semejantes me ahorra tomar decisiones y facilita las decisiones en grupo. El actor a imitar suele ser un experto, un famoso, alguien como nosotros, o simplemente las risas enlatadas de una comedia.

Estadísticamente existe cierta sabiduría en seguir las recomendaciones de una multitud de usuarios. Por eso son cada vez más relevantes las recomendaciones en redes sociales. Pero cuidado, un millón de heroinómanos sí pueden equivocarse, sobre todo cuando no tienen nuestro interés en mente.

Escasez

La escasez restringe nuestras opciones debido a la existencia limitada de un recurso. Se manifiesta como un sentimiento de urgencia que nos empuja a actuar, o hace más deseables las opciones existentes.

En experimentos con niños, bastaba apartar un juguete o prohibir un caramelo particular para que fuesen los más deseados. La escasez también opera cuando nos traen el último par de zapatos del almacén, o hay un videojuego de oferta.

En el plano romántico la escasez aumenta el atractivo de las potenciales parejas en nuestro grupo excursionista, del personal que queda a la hora de cerrar el bar, o de un amor imposible. Si “sólo puedes quedarte un par de horas” eres automáticamente más atractivo.

Atracción

Sentimos simpatía por gente parecida a nosotros, y más aún si corresponden a nuestra simpatía. Esta afinidad funciona para acortar sentencias criminales, hacer negocios, conseguir votos, etc.

personal relationships

Si te gusta demasiado alguien a quien acabas de conocer, pregúntate porqué. A veces es tu alma gemela y otras un vendedor de coches usados. Pero si tu eres el vendedor de coches usados, ¡ama a todo el mundo!

“The key to this business is personal relationships. Unless you love everybody, you can’t sell to anybody.” –Dicky Fox

Reciprocidad

A veces somos reacios a aceptar favores porque tememos que nos pidan algo a lo cual nos sentiremos obligados de corresponder. No hace mucho me “colocaron” de este modo un bote de cera para el coche:

Aparqué al lado de un taller de coches y una chica vino a hablarme de un producto abrillantador. En seguida comenzó a encerar una esquina con esfuerzo. Por cortesía le deje seguir y quedó realmente brillante, así que compré el producto. Al volver del gimnasio y coger el coche vino a darme consejos para cuidar el coche y me regaló una toalla.

Retrospectivamente, pienso que la tarea inicial apela a la reciprocidad, para ganar tiempo y transmitir un mensaje. La tarea final postcompra “más allá del deber” tiene el mismo efecto, y previene las devoluciones del producto. Tengo que quitarme el sombrero, ¡una venta bien hecha!.

Nada de esto es malo en sí mismo. Pero si te sientes a disgusto ten en cuenta: un regalo no es un regalo cuando es una manipulación encubierta. Si ves el regalo como un truco, te sentirás libre para rechazarlo.

El principio de un favor por otro puede aplicarse con variaciones:

  • Una petición poco razonable será rechazada, pero una petición más pequeña a continuación será vista como una concesión, y posiblemente correspondida aceptandola.
  • Presentar un proyecto con un defecto evidente sirve para que el cliente tenga un objetivo inocuo al que dirigir sus críticas.

Consistencia

Ser consistente es ser predecible y fiable, mientras que cambiar de idea es indicio de debilidad, confusión, y cuestiona mi propia identidad. Por ejemplo, si soy republicano y acepto la idea del cambio climático ¿sigo siendo republicano?. Está en juego la permanencia a la comunidad a la que pertenezco.

Lo curioso es que nuestra mente busca justificaciones para mantener la consistencia con creencias pasadas. Es el motivo por el que el entrenamiento militar incluye rituales de esfuerzo y humillación. Nuestra mente justifica el esfuerzo aumentando el valor de lo que hemos conseguido. En este caso, la pertenencia a un grupo social.

La realidad es que unas ideas importan y otras no. Una mente flexible acepta unas ideas hasta que otras mejores las sustituyen. Sin embargo, traicionar nuestros propios principios morales es causa frecuente de arrepentimiento.

Autoridad

La autoridad es la capacidad de dirección que otorgamos a un líder para guiarnos, pero también se manifiesta como obediencia ciega. Seguir al líder es algo que aprendemos en cada organización social en la que participamos. Por eso allá donde haya un grupo de gente confusa, basta autoproclamarse líder y expresarse con seguridad para ser percibido como tal.

La agenda de un líder no necesariamente coincide con el interés general. A veces un líder es el producto de un sistema que selecciona a los mejores manipuladores de su audiencia. Evalúa si el contrato social que ha encumbrado a tu líder beneficia tus objetivos personales.

Conclusión

Nuestro instinto nos preparó para la vida en pequeñas comunidades, no para el asalto de la publicidad. Conocer los principios de la persuasión sirve para reconocer su empleo y complementar nuestro juicio con decisiones conscientes.

Si te gusta Influencia: La psicología de la persuasión, puedes leer Yes! 50 Scientifically Proven Ways to be Persuasive que contiene 50 ejemplos de esos seis principios básicos de persuasión.

Obtención de certificados con Let’s Encrypt

$
0
0

¿Necesitas tener tráfico seguro en tu servidor pero no tienes un certificado válido para ello? Pues con Let’s Encrypt se te han acabado las excusas. Vamos a ver cómo poder obtener certificados (y cómo renovarlos) de una forma sencilla y totalmente gratuita :-) Lo único que necesitas es, cómo no, tener control sobre el dominio que quieres certificar. ¿Todo listo? Pues vamos a ver de qué va esto \o/.

Índice de contenidos

1. Let’s Encrypt

Let’s Encrypt es una autoridad de certificación (CA) automatizada, libre y gratuita que nace de un esfuerzo conjunto para el beneficio de la comunidad y no para el control de cualquier organización. Es un servicio proporcionado y promovido por el Internet Security Research Group en su empeño por hacer que todo el tráfico en la web sea seguro, grupo que está patrocinado por un gran número de empresas tanto pequeñas como grandes, entre las que se encuentran Mozilla, Cisco, Akamai y Facebook por nombrar algunas.

1.1. Autoridad de certificación y protocolo ACME

Let’s Encrypt utiliza el llamado protocolo ACME (del inglés Automatic Certificate Management Environment). Este protocolo tiene dos partes: la validación del dominio y la petición (o revocación según el caso) del certificado.

Para validar que se tiene control sobre el dominio se utiliza un par de claves pública-privada más una serie de retos (challenges) que el servidor ha de realizar. La primera vez que nuestro cliente de Let’s Encrypt se conecta con la CA se genera el par de claves. El proceso entre el cliente de Let’s Encrypt y la CA (detallado en la página oficial) podría resumirse así:

  1. Let’s Encrypt CA le pide a nuestro cliente que aloje un fichero en una dirección concreta del dominio, y le pasa además un token para que lo firme.

  2. El cliente procede a guardar el archivo bajo el dominio correspondiente y firma el token con la clave privada; notifica a la CA de que está listo.

  3. La CA verifica la firma del token con la clave pública de nuestro servidor e intenta descargar el fichero desde la dirección acordada.

  4. Si todo es correcto, entonces el servidor correspondiente a esa clave pública queda autorizado para realizar peticiones de certificados.

Una vez tenemos el par de claves autorizado, para la petición o revocación de certificados el cliente sólo ha de mandar el mensaje de gestión de certificados cifrado con la clave privada del servidor. La CA descifrará el mensaje con la clave pública y pasará a realizar las tareas de gestión pertinentes.

1.2. Instalación del cliente

Lo primero que necesitamos es instalar un cliente de Let’s Encrypt que se encargue de generar e instalar los certificados. De entre los distintos clientes y librerías que existen nos vamos a centrar en el cliente certbot que es el recomendado en la web de Let’s Encrypt. Hasta mayo de 2016 este cliente se llamaba letsencrypt así que es posible que aún encontréis documentación con esa referencia. Simplemente hay que sustituir letsencrypt y letsnecrypt-auto por certbot y certbot-auto ya su funcionamiento y opciones no han cambiado.

En algunos sistemas operativos se puede obtener directamente desde el sistema estándar de paquetes (como apt-get o yum), pero no está en todos (Ubuntu no lo tiene por ejemplo). En la web certbot.eff.org podemos encontrar instrucciones detalladas para nuestro SO y configuración particular. En cualquier caso siempre podemos obtener el cliente directamente de git:

$ cd /opt
$ git clone https://github.com/certbot/certbot
$ cd certbot
$ ./certbot-auto --help

Esto nos deja el cliente listo para usar en /opt/certbot. A partir de aquí, tenemos la posibilidad de crear únicamente el certificado o de realizar también la instalación mediante los diferentes plugins según el sistema. ¡Sigan leyendo!

Disclaimer: el resto del tutorial está basado en esta instalación y por lo tanto se usa el wrapper certbot-auto, pero si se instala a través del sistema de paquetes basta con reemplazar los certbot-auto por certbot.

1.3. Restricciones en la creación de Certificados

Actualmente Let’s Encrypt cuenta con una serie de restricciones a la hora de obtener los certificados. La más relevante es quizás el límite de 20 certificados para un mismo dominio por semana (esto incluye certificados del mismo dominio pero distinto subdominio). Si sobrepasamos el límite lo único que debemos hacer es esperar 7 días hasta volver a solicitar uno nuevo. Las primeras veces que queramos obtener un certificado es fácil que tengamos que hacer varias peticiones hasta dar con la combinación que buscamos (si tenemos varios dominios, o si tenemos necesidades particulares de seguridad, etc), así que hay que tener cuidado con los intentos.

Otra restricción a tener en cuenta es la validez de los certificados, que es únicamente de 3 meses. Sin embargo en la práctica esta restricción tiene menor impacto ya que Let’s Encrypt ofrece en su cliente un modo de renovación que veremos cómo automatizar para olvidarnos de tener que hacer esta acción por completo.

Por último tenemos el tema de los dominios que podemos incluir en un certificado. Actualmente podemos añadir al certificado hasta 100 dominios que estén vinculados a la máquina (consultar las restricciones para obtener datos actualizados), y hemos de especificar esos dominios uno a uno. Es decir, no podemos pedir un certificado para cualquier subdominio del dominio (wildcard certificates), por ejemplo.

2. Creación de certificados

Ha llegado el momento de entrar en harina. Como hemos dicho, el cliente certbot nos permite crear certificados de Let’s Encrypt y nos permite hacer esto de varias formas según las necesidades que tengamos. Incluso cuenta con varios plugins para hacer la instalación de certificados de forma automática, como veremos después.

¡Importante! El cliente certbot necesita permisos de root tanto para crear el certificado (por ejemplo para poder guardarlo en /etc/letsencrypt) como para hacer la instalación con alguno de los plugins (para leer y modificar la configuración del webserver o hacer el bind del puerto 80 al 443).

Tanto si sólo queremos crear el certificado como si no existe la opción de instalación automática para nuestra plataforma, podemos usar el comando certonly.

Lo siguiente es ejecutar el propio comando. Hemos de indicar con -d cada dominio para el cual queremos el certificado (han de ser dominios asociados a la máquina en la que estamos) y un email de contacto en caso de necesidad. También podemos indicar otras propiedades como el tamaño de la clave RSA, pero eso es ya opcional. La primera vez que ejecutemos el cliente nos pedirá el email (si no lo hemos introducido en la llamada) y que aceptemos el acuerdo de Let’s Encrypt. Esto último también se puede automatizar mediante la opción –agree-tos.

$ ./opt/certbot/certbot-auto certonly -d domain.com --email user@email.com --rsa-key-size 4096

Se nos abrirá una interfaz en el propio terminal donde nos preguntará qué vía queremos usar para hacer la autenticación con la entidad certificadora. Las opciones son: mediante el plugin de apache (que además de autorizarnos para obtener el certificado lo instala, como veremos más adelante), mediante un servidor local indicando el directorio webroot correspondiente o utilizando un servidor web temporal (standalone).

Obtención de certificado con certbot

2.1. Webroot

También podemos acceder directamente a la opción de autenticación que queramos por línea de comandos sin necesidad de pasar por la interfaz de selección. En el caso de la opción webroot sería:

$ ./opt/certbot/certbot-auto certonly --webroot -w /var/www/domain/ -d www.domain.com -d sub.domain.com -w /var/www/other -d other.org

Donde indicamos con la opción -w la dirección del directorio webroot correspondiente a los dominios que se indiquen justo después, cada uno acompañado de la opción -d. Y como se muestra en el ejemplo, se puede indicar más de un directorio webroot para dominios diferentes.

¡OJO! El plugin webroot crea un fichero temporal para cada uno de los dominios solicitados en ${webroot-path}/.well-known/acme-challenge . Esto quiere decir que el servidor ha de estar configurado para servir ficheros desde directorios ocultos, como es el caso de .well-known, o el servidor de validación de Let’s Encrypt no podrá comprobar que controlamos el dominio.

2.2. Standalone

Si no contamos con un servidor web local (o no queremos utilizarlo), podemos realizar la autenticación mediante un servidor temporal con el plugin standalone de certbot. La llamada sería:

$ ./opt/certbot/cerbot-auto certonly --standalone -d www.domain.com -d sub.domain.com -d other.org

El plugin necesita enlazar con el puerto 80 o el 443 para poder validar el dominio así que puede ser necesario parar un servidor existente antes de lanzar el cliente.

También se puede fijar el puerto a utilizar por el cliente mediante las opciones –standalone-supported-challenges http-01 para el 80 y –standalone-supported-challenges tls-sni-01 para el 443.

2.3. Manual

Y si nada de lo anterior te vale o simplemente eres todo un fanático del do it yourself siempre te queda la opción –manual (especificando certonly como en las anteriores) con la que te toca a ti realizar los pasos de la validación uno a uno. Nunca he utilizado esta opción pero según la documentación requiere copiar y pegar comandos en una sesión distinta del terminal que hasta puede estar en una máquina diferente.

2.4. Plugins y extensiones para la instalación automática

Hasta ahora hemos visto distintas formas de obtener el certificado y sólo quedaría instalarlo en función del servidor que tengamos. Aunque esto lo podemos hacer a mano, el cliente certbot cuenta con otros plugins que no sólo realizan la autenticación y nos proveen de un certificado sino que también se encargan de su instalación.

2.4.1. Apache

Si estamos ejecutando Apache 2.4 en un entorno basado en Debian (y con una versión 1.0+ del paquete libaugeas0), tenemos la opción de utilizar el plugin de Apache de Let’s Encrypt. Este nos permite automatizar no sólo la creación del certificado sino también su instalación, y todo esto sólo con añadir la opción –apache al comando anterior:

$ ./opt/certbot/certbot-auto --apache -d www.domain.com -d sub.domain.com -d other.org --email janedoe@email.com --rsa-key-size 4096

Esto lanza una interfaz en la propia línea de comandos donde se nos pide que aceptemos las condiciones legales y nos pregunta si queremos forzar que todo el tráfico sea por HTTPS, redirigiendo las peticiones del puerto 80 al 443. En función de nuestras respuestas, el cliente modifica el fichero de configuración de apache de nuestros dominios (por ejemplo /etc/apache2/sites-enabled/sub.domain.com.conf) y apuntará a los certificados creados.

En particular, se generará un certificado, un fichero de clave privada y uno de cadena de autenticación en el directorio: /etc/letsencrypt/archive/<nombre del dominio>. En nuestro ejemplo sería: /etc/letsencrypt/archive/sub.domain.com . Además, genera enlaces simbólicos a estos ficheros en el directorio: /etc/letsencrypt/live/<nombre del dominio>, que en nuestro ejemplo sería: /etc/letsencrypt/live/sub.domain.com

Aviso y consejo práctico: Let’s Encrypt es un poco especialito y no se lleva bien con los ficheros de configuración que contienen más de un virtualhost, como por ejemplo el del puerto 80 y el del 443 (da un error al no poder asociar un sólo virtualhost al dominio especificado). Si este es nuestro caso, antes de lanzar el cliente es mejor hacer un backup del .conf que ya tenemos en /etc/apache2/site-available y modificar el original para borrar el virtualhost del 443, dejando únicamente el del puerto 80. Luego, una vez hayamos instalado el certificado y haya creado el nuevo fichero para el 443 podemos completar este con las cosas que nos falten del backup.

El cliente crea un nuevo fichero de configuración de apache (/etc/apache2/sites-available/sub.domain.com-le-ssl.conf) donde se incluye la ruta a los enlaces anteriores. Sólo queda copiar del backup de nuestro fichero de configuración todo aquello que falte y añadírselo al fichero creado por Let’s Encrypt.

Podemos comprobar que el certificado se ha instalado correctamente utilizando el servicio de SSLLabs e indicando el nombre del dominio como parámetro del URL, que en nuestro ejemplo quedaría: https://www.ssllabs.com/ssltest/analyze.html?d=www.tea.ms

Ejemplo de resultado satisfactorio en SSL Labs

2.4.2. Plesk

Para el caso de Plesk la creación e instalación de certificados no viene integrada en el propio cliente sino que lo encontramos como un plugin de terceros. Lo podemos encontrar dentro de la propia herramienta de Plesk dentro del catálogo de extensiones. Sólo hemos de seleccionar la extensión de Let’s Encrypt e instalarla en nuestro Plesk.

Hecho esto la extensión aparecerá en nuestro listado de extensiones:

Listado de extensiones en Plesk

Para crear e instalar un nuevo certificado, seleccionamos la extensión de nuestra lista. Al entrar nos mostrará la lista de dominios correspondientes a la máquina. Seleccionamos el dominio sobre el que queremos crear el certificado y esto nos lleva a una nueva página donde hemos de indicar el email que queremos asociar y darle a Instalar. ¡Y listos!

La extensión no sólo crea e instala el certificado, sino que también añade la planificación para renovar el certificado cada 30 días (con lo que no haría falta seguir los pasos del siguiente punto). Vamos, que sólo le falta ponernos un lazo en el certificado 😛

2.4.3. Nginx

Aunque existe un plugin para la obtención e instalación de certificados en nginx está aún en fase experimental y no viene instalado junto con certbot-auto, aunque anuncian que una vez esté instalado podrá invocarse con la opción –nginx. Hasta entonces se puede obtener el certificado con alguno de los métodos comentados al principio de esta sección e instalarlo manualmente en el servidor.

3. Renovación de certificados

Como hemos comentado antes, el cliente de Let’s Encrypt cuenta con un modo de renovación de certificados. Este modo comprueba la validez de los certificados de la máquina y, si alguno está a menos de 30 días del fin de su validez, procede a su renovación con los mismos parámetros con los que se creó y que se guardaron en un fichero de configuración. El comando para ejecutar este modo es el siguiente:

$ ./opt/certbot/certbot-auto renew

Si queremos automatizar la renovación de los certificados de Let’s Encrypt podemos crear un cron en la propia máquina del dominio. Un cron es un proceso que se ejecuta periódicamente de acuerdo con un patrón determinado. Como hemos dicho los certificados caducan a los tres meses, así que podemos hacer una comprobación semanal o mensual de la necesidad de renovación. Estas comprobaciones no cuentan para el límite de certificados semanales a no ser que realmente haya que hacer una renovación. Así que no hay problema en crear el cron con mayor o menor periodicidad.

Para acceder a los crons de la máquina ejecutamos lo siguiente en el terminal:

$ sudo crontab -e

Esto nos abre para su edición el fichero donde están definidos todos los crons instalados en la máquina. Para la renovación de los certificados basta con añadir la siguiente línea al final del fichero:

#Letsencrypt certificate renewal
0 11 * * 1  /opt/certbot/certbot-auto renew >> /var/log/certbot-renew.log

Esto lanzará el proceso de renovación todos los lunes (1) a las 11, y guardará el resultado del proceso al final del log indicado (respetando el contenido que huniera anteriormente o creando el fichero si no existiera).

4. Probando, probando…​

Como hemos comentado Let’s Encrypt nos pone un límite de certificados a la semana. Este límite puede ser insuficiente si es la primera vez que pedimos el certificado para la máquina y no tenemos claros los parámetros o si estamos probando si va a funcionar o no la renovación.

Por suerte los de Let’s Encrypt han pensado en todo y nos ofrecen la opción –dry-run. Al lanzar el cliente con esta opción con certonly o con renew nos dará un certificado de test (no válido) y que no guardará en disco. De esta forma podemos probar todo el proceso sin consumir nuestro límite de certificados y sin alterar nuestro sistema. Para ejecutarlo basta con hacer:

$ certbot-auto renew --dry-run

5. Conclusiones

El tráfico seguro en Internet es algo que nos beneficia a todos y ahora gracias a esta genial iniciativa de Let’s Encrypt cualquiera puede proteger el acceso a sus servidores. Además ha arrancado con un gran apoyo que ha conseguido una buena comunidad en poco tiempo. Prueba de ello es la gran cantidad de clientes y opciones que ya existen para generar nuestros certificados. Y aunque de momento existen ciertas limitaciones no debería representar un problema para usuarios medios.

6. Referencias

Por último, os dejo los enlaces en los que se puede encontrar más información (en inglés) sobre Let’s Encrypt y el cliente certbot:

Mantén a tus bugs cerca y a tus librerías aún más cerca. Ejemplo de Guava

$
0
0

En este tutorial vamos a ver un ejemplo que demuestra cómo es fundamental conocer el funcionamiento de las librerías que usamos en nuestros desarrollo.

0. Índice de contenidos.


1. Introducción

Todos los que desarrollamos sabemos que sin el trabajo de otros en forma de librerías (bibliotecas), poco podríamos hacer, o al menos costaría mucho más esfuerzo y tiempo. Pero también que estas librerías pueden ser un arma de doble filo si no se comportan como nosotros esperamos…Y es que realmente estamos en manos de las librerías, salvo que las hagamos nosotros mismos :) …nos guste o no…

Hace un tiempo escribí un tutorial sobre Guava para poder utilizar el estilo de programación funcional dentro de versiones de Java anteriores a Java 8. Surgió porque en un proyecto en el que he colaborado lo hemos usado masivamente, así que buena parte del código está en manos de la librería de Guava, que es una librería de Google, y que por tanto no hemos implementado nosotros.


2. El problema

En el proyecto usábamos EhCaché para hacer un cacheo sencillo de ciertas funciones que se llaman frecuentemente.

@Cacheable(value = "ffsCache", key = "#configuration")
public List<FareFamilyConditions> getFareFamilyConditionsToDisplay(
  final FareFamilyConditionsConfiguration configuration) throws FfsException{
    return ...;
}

EhCache lo que hace, explicado muy básicamente, es tomar una “key” de entrada, en este caso el objeto configuration de la clase FareFamilyConditionsConfiguration, y obtiene su hash. Si en la tabla de caché no está ese hash, ejecuta el método y toma el resultado, en este caso una lista de POJOS: List y la almacena en su tabla. La próxima vez que se llame con el mismo hash, en vez de ejecutar el método, devolverá el valor del mapa, siempre que las condiciones de caché (expiración) se cumplan. Por cierto, si manejamos anotaciones es porque la caché está configurada con Spring… (más librerías).

¿Con qué problema nos encontramos?

El problema es que detectamos una ocupación exagerada de la caché que no correspondía para nada con unas pequeñas listas de unos pocos elementos de tipo POJO, que era lo que esperábamos. Esto hacía que la caché básicamente no funcionase la caché, algo que no éramos capaces de comprender porque la configuración parecía correcta.

Utilizando un debugger y activando las trazas en modo debug, veíamos que EhCache comenzaba a devolver una gran cantidad de objetos y a navegar sobre ellos. Interiormente tiene unos métodos que recorren todo el árbol de objetos, y para estos casos cuenta con un límite configurable para inspeccionar la caché. Incluso 10.000 objetos se quedaba corto y abortaba el funcionamiento de la caché.

A esto se sumaba que los test de integración de ese método, incluyendo caché, no daba problemas, pero al utilizarlo dentro de otro módulo (que era mucho mayor y su contexto de Spring era un poco “grande”), la caché fallaba por saturación de ésta.


3. La causa

La clave estaba en Guava, aunque mejor dicho, en el uso de las funciones anónimas para generar la lista.

En el tutorial sobre Guava vimos cómo a partir de una lista se puede generar otra filtrada o transformada. Simplemente hace falta la lista original y un objeto de la clase Function que se aplica a cada elemento para obtener otra lista.

Volviendo a nuestro caso real de aplicación, la clase FareFamilyConditionsConfiguration, que es el parámetro de entrada del método cacheado y por tanto “key” de la caché (configuration), tiene dentro una List<Strings> que ha de ser calculada. Para calcularla se hizo con Guava, ya que esa List<Strings> venía de la transformación de una List< LIST_FARE_FAMILYDefType>, que es un tipo algo más complejo. Nosotros sólo queríamos introducir esos objetos y sacar un String de ellos, que formaba parte de una de sus propiedades. Nada mejor que una función anónima generada en un método privado de una clase.

Primero declaramos el modo que genera la función (también lo podíamos haber hecho inline o asignarlo a un objeto):

private Function<LIST_FARE_FAMILYDefType, String> extractFareFamily(){
  return new Function<LIST_FARE_FAMILYDefType, String>(){
    @Override
    public String apply(final LIST_FARE_FAMILYDefType input){
      return input.getFARE_FAMILY();
    }
  };
}

Y esta simpática generación de funciones se usa en el siguiente código

List<String> ffcc = Lists.transform(fareFamilies, extractFareFamily());
FareFamilyConditionsConfiguration  configuration = new ... (ffcc);
ffsService.getFareFamilyConditionsToDisplay(configuration);

En la última línea puedes ver cómo se llama al método que tiene la anotación @Cacheable del que hemos hablado al comienzo.

El problema está en el List<Strings> ffcc, que viene de un List.transform de Guava, y que usa el objeto de tipo Function que hemos indicado antes:

List<String> ffcc = Lists.transform(fareFamilies, extractFareFamily());

Nuestra función espera un objeto en cuyo interior tiene esa lista llamada ffcc… ¿Y qué lista esperamos que sea?

Aquí está el error. Cuando vemos un List<String> esperamos que sea un objeto de tipo lista, pero debemos recordad que java.util.List<E> no es más que una interfaz, por lo que no define nada de lo que almacena internamente. Sólo define unos pocos métodos y el comportamiento de estos, pero no si dentro almacena unos objetos u otros. Puedes consultar la interfaz de Java7 en https://docs.oracle.com/javase/7/docs/api/java/util/List.html

Cuando trabajamos con listas, estamos habituados a usar la clase java.util.ArrayList (https://docs.oracle.com/javase/7/docs/api/java/util/ArrayList.html), que al ser una clase define claramente lo que almacena, y esto no es otra cosa que un vector con los Strings que estamos almacenando.

Lo podemos ver en el código fuente de la clase ArrayList. Básicamente tiene un vector de los objetos contenidos y un indicador del tamaño.

private transient Object[] elementData;
private int size;

Por tanto, si por ejemplo, nuestra lista original fuera [“uno”, “dos”, “tres”] y usásemos una transformación que añadiese un caracter “_” al final, esperaríamos como resultado un ArrayList que tuviese en su interior [“uno_”, “dos_”, “tres_”]. Al llamar al get(0), devolvería “uno_”. Hasta aquí lo normal.

¿Qué almacena una lista de Guava realmente?

El problema está en que usamos Guava, y en concreto el método estático Lists.transform, cuya documentación podemos ver aquí.

Si nos fijamos en el código fuente, que podemos ver en GitHub, podemos ver que:

public static  List transform(
      List fromList, Function function) {
    return (fromList instanceof RandomAccess)
        ? new TransformingRandomAccessList(fromList, function)
        : new TransformingSequentialList(fromList, function);
  }

Dependiendo del tipo de lista de entrada, se usa TransformingRandomAccessList o TransformingSequentialList. Si vamos al código de alguna de estas clases, tendremos la respuesta:

private static class TransformingRandomAccessList<F, T> extends AbstractList<T> implements RandomAccess, Serializable {
  final List<F> fromList;
  final Function<? super F, ? extends T> function;
  ...

De momento, por los nombres, podemos ver que se almacena NO la lista transformada, sino la lista original. Y también la función de transformación, sí, esa que hemos creado antes… ni rastro de la lista completamente transformada. Yendo a la implementación del método List, tenemos la prueba irrefutable:

@Override
public T get(int index) {
  return function.apply(fromList.get(index));
}

Efectivamente, la lista que nos devuelve es de tipo lazy, es decir, no se lleva a cabo la transformación hasta que no se llama al “get”. Por tanto, internamente a nivel de objetos, dentro de la lista que estábamos devolviendo, no está el resultado, sino la lista original y la función.

¿Sorprendente? Por no conocer bien la librería que estamos usando, hemos pensado que estábamos guardando una información que en realidad no se está guardando…

Pero si se almacena la lista original y la función de transformación y esto es determinista… ¿Cuál es el problema? En realidad no hay problema tal debido al almancenamiento de la lista… pero sí de la función.

El problema de la Function

Además de la lista original guardamos la función… bueno, no pasa nada: en nuestra List<String> tenemos un listado de Strings que no es el que necesitamos, y un objeto más… la función… ¿Tan grave es meter un objeto más? No parece que tenga mucho problema… Bueno, en realidad depende del objeto del que se trate.

Y es que, en nuestro caso, el objeto de tipo Function está definido de forma anónima, es decir, dentro del propio código como una expresión (sin el típico class…). Por tanto es una clase anónima y además interna (inner).

La pega está en que las clases anónimas internas (inner anonymous classes) tiene siempre una referencia a la instancia que alberga esa clase, aunque no se use nunca.

En este código del post de ejemplos de Guava podemos verlo:

@Test
public void addSufix_UsingGuava() {
	final Function<String, String> addSufix = new Function<String, String>() {

		@Override
		public String apply(final String word) {
			return word.concat(SUFIX);
		}

	};
	final List<String> listOfStringsWithSuffix = FluentIterable.from(listOfStrings).transform(addSufix).toList();

	assertTrue(listOfStringsWithSuffix.get(0).endsWith(SUFIX));
}

En este código, addSufix es un objeto proveniente de una clase interna anónima… Mira qué pasa si paramos el debugger y vemos su interior:

guava1

Tiene referencia a otras tres listas que están definidas en la clase que está declarada, es decir, la clase que la alberga.

He aquí el foco del problema: en nuestro ejemplo real con el que he comenzado el post, no eran simplemente 3 listas, que pueden tener más o menos tamaño… La clase que albergaba la función anónima era un servicio de Spring, que tenía además un cliente de Apache CXF inyectado con Spring (en singleton, claro), que tiene decenas y decenas de objetos de configuración en su interior.

El resultado es que al final, la lista devuelta por el Lists.transform de Guava distaba mucho de ser un simple ArrayList con 3 Strings, sino que era un árbol de objeto con miles y miles de objetos relacionados los unos con los otros. Y como se usaba como key de EhCache, éste se dedicaba a hacer el hash de miles de objetos que componían la clave, descartando el proceso por volumen desmedido y haciendo que fallase la caché (si no fallase, es poco probable que el Hash se pudiese repetirse al haber tantos miles de objetos).

La solución paso en este caso por evitar en este caso la declaración de la función con una clase anónima interna, usando por ejemplo:

  • Una clase estática dentro de la clase padre, que tiene sentido por sí misma y no le hace falta la referencia a la clase que la alberga
  • También se podría haber devuelto dentro de un método estático que no tiene referencia a objeto padre.
  • O haber incluido un paso intermedio de transformación de la lista de Guava a una lista de tipo ArrayList.

… y todo esto por fiarnos ciegamente de las librerías en las que nos apoyamos y suponer ciertos comportamientos sin entrar en los detalles.


4.Conclusiones

En este tutorial hemos visto un caso real que nos sucedió por suponer que el comportamiento de una librería que usábamos era el más adecuado. En realidad provocó un problema que costó varias horas en resolverse, y que podría haber causado problemas en producción.

La moraleja es que debemos conocer el comportamiento de las librerías en las que nos basamos, aunque si bien quizá sea imposible, al menos no fiarnos nunca de ellas: debemos tenerlas más cerca que a nuestros propios enemigos

PS: gracias a Alejandro Ortiz, que fue capaz de descubrir el problema descrito en este post.

Ordenación de Listas en java

$
0
0

List tiene un nuevo método sort(Comparable). Resulta útil, porque permite a las implementaciones especificar cómo ordenar su estructura interna de datos. Vector sincroniza de una vez la lista, no por elemento. ArrayList evita copiar el array si no es necesario. CopyOnWriteArrayList funciona.

Este artículo es una traducción al castellano de la entrada original publicada, en inglés, por Dr. Heinz Kabutz en su número 239 del JavaSpecialists newsletter. Puedes consultar el texto original en Javaspecialists’ Newsletter #239: Sorting Lists

Este artículo se publica en Adictos al Trabajo, con permiso del autor, traducido por David Gómez García, (@dgomezg) consultor tecnológico en Autentia, colaborador de Javaspecialists e instructor certificado para impartir los cursos de Javaspecialists en Español.

Bienvenidos a la edición número 239 del Javatm Specialists’ Newsletter, enviada desde la bella isla de Creta. Estoy sentado en mi balcón bajo la luna llena, escuchando el sonido rural de nuestra oveja “Darling” al deambular moviendo su cencerro, sin duda en busca del último penacho verde de hierba. La oveja tiene un nombre, lo que significa que no nos la podemos comer, ni a ella ni ninguno de sus descendientes hasta la décima generación.

El viernes pasado tuvimos un “Festival de la sandía” en nuestro pueblo de Chorafakia. La idea es genial. Por 5 euros, te dan una botella de tsikoudia (aguardiente) para compartir entre 4, y toda la sandía que seas capaz de comer. Por supuesto, también puedes comprar otras bebidas y carnes a la parrilla. Música de Creta en vivo, bailarines y una jornada festiva y alegre. La idea es fantástica y, sin duda, volveré el año que viene. Incluso podría ser voluntario para ayudar con la parrilla. Al fin y al cabo, podría ser Chorafakiano ya, aunque nací y me crié en Sudáfrica.


Ordenación de Listas en Java

En versiones anteriores de Java, utilizamos normalmente Collections.sort(List) para ordenar una lista de objetos. Algunas listas implementan el interfaz RandomAccess, indicando que podemos acceder a cualquier elemento de la lista ubicado en cualquier posición en un tiempo constante. Un ejemplo de este tipo de listas es ArrayList. Por el contrario, LinkedList no lo es. Una búsqueda tiene una complejidad de tiempo de O(n). El método Collections.sort(List) no considera si la List implementa RandomAccess, sino que la convierte siempre a un Array, para después ordenarlo con Arrays.sort(Object[]) y volcar de nuevo en la lista con el método set() de ListIterator. La siguiente es la forma en que podríamos ordenar las listas que tenían complejidad en tiempo de O(n) para la búsqueda:

public static  void sort(List list) {
    Object[] a = list.toArray();
    Arrays.sort(a);
    ListIterator i = list.listIterator();
    for (int j=0; j<a.length; j++) {
      i.next();
      i.set(a[j]);
    }
  }

Internamente, el método Arrays.sort() también crea arrays temporales del mismo tamaño de los datos de entrada. Tenemos, por tanto, dos puntos en los que se crean arrays temporales: Collections.sort() y Arrays.sort(). Antes de Java 7, teníamos Merge Sort, que creaba un array del tamaño exacto del array de entrada. A partir de Java 7 tenemos Tim Sort, que crea algunos arrays temporales adicionales y que incrementa un poquito el total de objetos ‘basura’ creados.

En Java 8, el interfaz List contiene un nuevo método sort(Comparator). La implementación por defecto parece la misma que la del método Collections.sort(List, Comparator) de Java 7. Sin embargo, dado que se encuentra en el interfaz, ahora podemos tener especializaciones en las implementaciones de List. Esto significa que ArrayList no necesita ya copiar sus contenidos en un array, y puede ordenar directamente. CopyOnWriteArrayList puede ordenar sus contenidos sin lanzar la excepción UnsupportedOperationException en su método set(). La vida es bella.

En cualquier caso, cualquier sagaz ingeniero informático podría fácilmente caer en la cuenta de que la creación de objetos no es el cuello de botella del método sort(). Al revés, la fusión y la propia ordenación son las que se llevan la mayor parte del tiempo. Podemos comprobarlo mirando la tasa de creación de objetos. En mi máquina, puedo crear hasta 4 Gb por segundo. En este caso, estamos creando sólo unos 12 Mb por segundo. Comprobaremos la tasa de creación de objetos, sólo por curiosidad.

Me gustaría en este punto agradecer a nuestro suscriptor Michael Inden, autor de un buen libro, Java 8 – Die Neuerungen (en alemán). Yo ya sabía que List tenía un método sort() pero, gracias a su libro, aprendí las diferentes especializaciones de las implementaciones de List. Curioso.

Para comprobar la tasa de creación de objetos, he simplificado el ByteWatcher

import javax.management.*;
    import java.lang.management.*;

    /**
     * Reduced version of the ByteWatcher, described here:
     * http://www.javaspecialists.eu/archive/Issue232.html
     */
    public class ByteWatcher {
      private static final String GET_THREAD_ALLOCATED_BYTES =
          "getThreadAllocatedBytes";
      private static final String[] SIGNATURE =
          new String[]{long.class.getName()};
      private static final MBeanServer mBeanServer;
      private static final ObjectName name;

      private final Object[] PARAMS;
      private final long MEASURING_COST_IN_BYTES; // usually 336
      private final long tid;

      private long allocated = 0;

      static {
        try {
          name = new ObjectName(
              ManagementFactory.THREAD_MXBEAN_NAME);
          mBeanServer = ManagementFactory.getPlatformMBeanServer();
        } catch (MalformedObjectNameException e) {
          throw new ExceptionInInitializerError(e);
        }
      }

      public ByteWatcher() {
        this.tid = Thread.currentThread().getId();
        PARAMS = new Object[]{tid};

        long calibrate = threadAllocatedBytes();
        // calibrate
        for (int repeats = 0; repeats < 10; repeats++) {
          for (int i = 0; i < 10_000; i++) {
            // run a few loops to allow for startup anomalies
            calibrate = threadAllocatedBytes();
          }
          try {
            Thread.sleep(50);
          } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            break;
          }
        }
        MEASURING_COST_IN_BYTES = threadAllocatedBytes() - calibrate;
        reset();
      }

      public void reset() {
        allocated = threadAllocatedBytes();
      }

      private long threadAllocatedBytes() {
        try {
          return (long) mBeanServer.invoke(
              name,
              GET_THREAD_ALLOCATED_BYTES,
              PARAMS,
              SIGNATURE
          );
        } catch (Exception e) {
          throw new IllegalStateException(e);
        }
      }

      /**
       * Calculates the number of bytes allocated since the last
       * reset().
       */
      public long calculateAllocations() {
        long mark1 = ((threadAllocatedBytes() -
            MEASURING_COST_IN_BYTES) - allocated);
        return mark1;
      }
    }

A continuación, cogí una pequeña utilidad Memory que escribí para los ejercicios de mi Extreme Java – Advanced Topics Course (que también impartimos en español). Es similar a la enumeración TimeUnit en su estructura. Lo usamos para convertir la información de bytes asignados en números fáciles de leer:

public enum Memory {
    BYTES {
      public double toBytes(double d) {
        return d;
      }

      public double toKiloBytes(double d) {
        return toBytes(d) / 1024;
      }

      public double toMegaBytes(double d) {
        return toKiloBytes(d) / 1024;
      }

      public double toGigaBytes(double d) {
        return toMegaBytes(d) / 1024;
      }

      public double toTeraBytes(double d) {
        return toGigaBytes(d) / 1024;
      }
    },
    KILOBYTES {
      public double toBytes(double d) {
        return toKiloBytes(d) * 1024;
      }

      public double toKiloBytes(double d) {
        return d;
      }

      public double toMegaBytes(double d) {
        return toKiloBytes(d) / 1024;
      }

      public double toGigaBytes(double d) {
        return toMegaBytes(d) / 1024;
      }

      public double toTeraBytes(double d) {
        return toGigaBytes(d) / 1024;
      }
    },
    MEGABYTES {
      public double toBytes(double d) {
        return toKiloBytes(d) * 1024;
      }

      public double toKiloBytes(double d) {
        return toMegaBytes(d) * 1024;
      }

      public double toMegaBytes(double d) {
        return d;
      }

      public double toGigaBytes(double d) {
        return toMegaBytes(d) / 1024;
      }

      public double toTeraBytes(double d) {
        return toGigaBytes(d) / 1024;
      }
    },
    GIGABYTES {
      public double toBytes(double d) {
        return toKiloBytes(d) * 1024;
      }

      public double toKiloBytes(double d) {
        return toMegaBytes(d) * 1024;
      }

      public double toMegaBytes(double d) {
        return toGigaBytes(d) * 1024;
      }

      public double toGigaBytes(double d) {
        return d;
      }

      public double toTeraBytes(double d) {
        return toGigaBytes(d) / 1024;
      }
    },
    TERABYTES {
      public double toBytes(double d) {
        return toKiloBytes(d) * 1024;
      }

      public double toKiloBytes(double d) {
        return toMegaBytes(d) * 1024;
      }

      public double toMegaBytes(double d) {
        return toGigaBytes(d) * 1024;
      }

      public double toGigaBytes(double d) {
        return toTeraBytes(d) * 1024;
      }

      public double toTeraBytes(double d) {
        return d;
      }
    };

    public abstract double toBytes(double d);

    public abstract double toKiloBytes(double d);

    public abstract double toMegaBytes(double d);

    public abstract double toGigaBytes(double d);

    public abstract double toTeraBytes(double d);

    public static String format(double d, Memory unit,
                                int decimals) {
      String unitStr;
      double val;
      double bytes = unit.toBytes(d);
      if (bytes < 1024) {
        val = bytes;
        unitStr = "B";
      } else if (bytes < 1024 * 1024) {
        val = BYTES.toKiloBytes(bytes);
        unitStr = "KB";
      } else if (bytes < 1024 * 1024 * 1024) {
        val = BYTES.toMegaBytes(bytes);
        unitStr = "MB";
      } else if (bytes < 1024 * 1024 * 1024 * 1024L) {
        val = BYTES.toGigaBytes(bytes);
        unitStr = "GB";
      } else {
        val = BYTES.toTeraBytes(bytes);
        unitStr = "TB";
      }
      return String.format("%." + decimals + "f%s", val, unitStr);
    }
  }

Por último, la clase de prueba, de nombre ListSorting. Vamos a probar 5 implementaciones diferentes de List: ArrayList, LinkedList, Vector, CopyOnWriteArrayList y la lista devuelta por Arrays.asList(...). Cada una de las listas contiene objetos Double. Empezamos generando un stream de valores nativos double utilizando ThreadLocalRandom, los convertimos implícitamente a Double y los recogemos en la List. Así se crea la lista que ordenaremos.

El método test() toma por parámetro la forma de construir la lista (por ejemplo, ArrayList::new o LinkedList::new). Cada una de los tipos de listas tienen que tener un constructor que acepte una lista como parámetro. También proporcionaremos esa lista desordenada al método test(). De esta manera, nuestro método test() puede probar diferentes formas de ordenar las listas, antiguas y nuevas.

import java.io.*;
  import java.lang.management.*;
  import java.util.*;
  import java.util.concurrent.*;
  import java.util.function.*;
  import java.util.stream.*;

  public class ListSorting {
    private static final ByteWatcher byteWatcher =
        new ByteWatcher();

    public static void main(String... args) throws IOException {
      for (int i = 0; i < 10; i++) {
        testAll();
        System.out.println();
      }
    }

    private static void testAll() {
      for (int size = 100_000; size <= 10_000_000; size *= 10) {
        List jumble =
            ThreadLocalRandom.current()
                .doubles(size)
                .boxed()
                .collect(Collectors.toList());
        test(ArrayList::new, jumble);
        test(LinkedList::new, jumble);
        test(Vector::new, jumble);
        test(CopyOnWriteArrayList::new, jumble);
        test(doubles ->
            Arrays.asList(
                jumble.stream().toArray(Double[]::new)
            ), jumble);
      }
    }

    private static void test(
        UnaryOperator<List> listConstr,
        List list) {
      sortOld(listConstr.apply(list));
      sortNew(listConstr.apply(list));
    }

    private static void sortOld(List list) {
      measureSort("Old", list, () -> sort(list));
    }

    private static void sortNew(List list) {
      measureSort("New", list, () -> list.sort(null));
    }

    private final static ThreadMXBean tmbean =
        ManagementFactory.getThreadMXBean();

    private static void measureSort(String type,
                                    List list,
                                    Runnable sortJob) {
      try {
        long time = tmbean.getCurrentThreadUserTime();
        byteWatcher.reset();
        sortJob.run();
        long bytes = byteWatcher.calculateAllocations();
        time = tmbean.getCurrentThreadUserTime() - time;
        time = TimeUnit.MILLISECONDS.convert(
            time, TimeUnit.NANOSECONDS);
        System.out.printf(
            "%s sort %s %,3d in %dms and bytes %s%n",
            type,
            list.getClass().getName(),
            list.size(),
            time,
            Memory.format(bytes, Memory.BYTES, 2));
      } catch (UnsupportedOperationException ex) {
        System.out.println("Old sort: Cannot sort " +
            list.getClass().getName() + " " + ex);
      }
    }

    /**
     * {@linkplain java.util.Collections#sort Copied from Java 7}
     */
    @SuppressWarnings({"unchecked", "rawtypes"})
    public static  void sort(List list) {
      Object[] a = list.toArray();
      Arrays.sort(a);
      ListIterator i = list.listIterator();
      for (Object e : a) {
        i.next();
        i.set((E) e);
      }
    }
  }

LinkedList utiliza el método por defecto de ordenación de Java 7, que ahora reside en el método sort() por defecto del interface List. La mayoría de las implementaciones de List, como ArrayList, Arrays.ArrayList, Vector y CopyOnWriteArrayList, han sobre-escrito el método con versiones más eficientes. Al ejecutar el código, verás que ArrayList reserva menos bytes que antes, pese a que no se ve una gran diferencia en el tiempo de CPU. También verás que CopyOnWriteArrayList ahora se puede ordenar, mientras que antes nos daba un UnsupportedOperationException.


Ordenando como un campeón en Java 8

En Java 8, tenemos un Arrays.parallelSort(), pero no hay un Collections.parallelSort() equivalente. Pero podemos lograr nuestro nirvana de la ordenación con parallel streams. En mi método parallelSort(), especificamos una lista y una Function que se utilizará para crear el tipo adecuado de List. R es el tipo del resultado, por ejemplo ArrayList<E> o LinkedList<E>. Tiene que ser un subtipo de List<E>. Así, podemos convertir la lista en un parallel stream, ordenarla, y recoger sus valores en una lista. El resultado es un ArrayList, wue luego convertimos con la función listConstructor.

import java.util.*;
  import java.util.function.*;
  import java.util.stream.*;

  public class SortingLikeABoss {
    public static <E, R extends List> R parallelSort(
        List list,
        Function<List, R> listConstructor) {
      return listConstructor.apply(
          list.parallelStream()
              .sorted()
              .collect(Collectors.toList()));
    }
  }

Aqui tenemos un ejemplo donde generamos tres tipos distintos de listas utilizando nuestro método parallelSort():

import org.junit.Test;

  import java.util.*;
  import java.util.concurrent.*;
  import java.util.stream.*;

  import static org.junit.Assert.assertEquals;

  public class SortingLikeABossTest {
    @Test
    public void testBossSorting() {
      List jumble =
          ThreadLocalRandom.current()
              .doubles(1_000_000)
              .parallel()
              .boxed()
              .collect(Collectors.toList());

      List sorted = new ArrayList(jumble);
      Collections.sort(sorted);

      ArrayList al = SortingLikeABoss.parallelSort(
          jumble, ArrayList::new
      );
      assertEquals(sorted, al);

      LinkedList ll = SortingLikeABoss.parallelSort(
          jumble, LinkedList::new
      );
      assertEquals(sorted, ll);

      CopyOnWriteArrayList cowal =
          SortingLikeABoss.parallelSort(
              jumble, CopyOnWriteArrayList::new
          );
      assertEquals(sorted, cowal);
    }
  }

Hay un pero: La implementación actual en Java 8 de la ordenación en paralelo sólo funciona si hay al menos un hilo disponible en el common fork join pool. No pasará por defecto a utilizar el hilo que invoca la ordenación, como pasaría con otras parallel tasks. Creo que esto es un bug, no una feature ;). La he enviado, pero no creo que sea resuelta pronto. No es grave porque, en cualquier caso, se supone que no deberíamos bloquear todos los hilos del common pool.

Gracias por leer mi artículo. Espero que lo hayas disfrutado tanto como yo al escribirlo :-)

Saludos

Heinz.

Arranca con Spring Initializr

$
0
0
En este tutorial vamos a ver cómo utilizar la herramienta Spring Initializr para crear fácilmente la estructura básica de un proyecto Spring Boot.

Índice de contenidos

1. Introducción

En muchos casos empezar un proyecto con maven/gradle y spring es más complejo de lo que parece. Crear un andamiaje básico es siempre un primer paso que, si no se hace bien, nos acaba dando más de un quebradero de cabeza: dependencias solapadas, dependencias con más de una versión… En principio para esto ya existen herramientas como el plugin archetype de Maven o su equivalencia en Gradle. Sin embargo en el primer caso la gente de Spring parece que no da soporte ya a esa plantilla, que apunta a versiones muy antiguas. En el caso de Gradle no existe siquiera un init type para Spring Boot. Hoy vamos a aprender a crear un proyecto spring boot utilizando Spring Initializr. Esta herramienta nos va a ayudar a generar un proyecto desde cero especificando la configuración y dependencias de nuestro proyecto de forma sencilla y visual a través de una aplicación web. Por último veremos como utilizar Spring Initializr a través de la integración con IntelliJ IDEA.

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 El Capitan 10.11.5
  • Entorno de desarrollo: IntelliJ IDEA 2016.2 EAP

3. Initializr desde la web

La forma más sencilla de utilizar Spring Initializr es desde la propia web. Una vez dentro nos recibe una sencilla interfaz en la que podemos introducir algunos valores típicos para configurar un proyecto de maven o gradle. La web tiene 2 modos de uso entre los que se puede alternar a través de un link en la parte inferior: simple y completa.

3.1. Versión simple

La versión simple de la página ofrece unas pocas opciones básicas suficientes para generar el proyecto.

initializr_09

  • Tipo de proyecto: Proyecto Maven o Proyecto Gradle. El artefacto que se va a generar lo hará bien con un archivo pom.xml con su script y archivos para el wrapper de maven, bien con un archivo build.gradle y sus correspondientes archivos para el wrapper de gradle
  • Versión de Spring Boot: La versión del starter parent de Spring Boot de la que vamos a depender. Aquí nos permiten incluso depender de versiones de desarrollo.
  • Group: Será el campo groupId en el descriptor de maven y el nombre del paquete base de las clases de nuestra aplicación.
  • Artifact: Nombre de nuestro artefacto. En maven se va a convertir en los campos artifactId y name. En gradle irá a parar al campo jar.baseName. Este será además el nombre del archivo zip que se va a generar.
  • Dependencies: Por último tenemos un buscador de dependencias que básicamente se corresponden con los starters de spring boot disponibles. El campo funciona haciendo una búsqueda por texto sobre la que se nos van dando sugerencias.

3.2. Versión completa

La versión completa nos permite definir el resto de los campos necesarios para nuestra build inicial. Además de los campos de la versión simple, la versión completa nos permite escoger algunos más:

initializr_11

  • Name: Va a parar al campo name de nuestro archivo pom.xml. En gradle no tiene efecto.
  • Description: Va a parar al campo description de nuestro archivo pom.xml. En gradle no tiene efecto.
  • Package Name: Nombre del paquete base de las clases de la aplicación en caso de que sea diferente a nuestro campo Group.
  • Packaging: Empaquetado de nuestro artefacto: jar o war. Esto afecta a los plugins de construcción que se especifican en el descriptor de la aplicación (pom.xml o build.gradle) y a la estructura de archivos que se crea.
  • Java Version: Versión de java que vamos a especificar para nuestro artefacto. En el momento de escribir este tutorial da a escoger entre 1.6, 1.7 y 1.8
  • Language: En el momento de escribir el tutorial da a escoger entre Java, Groovy y Kotlin.
  • Dependencies: Lo mismo que en la versión simple de la página pero en esta ocasión se nos presenta una serie de checkboxes con todas las opciones posibles.

3.3. Estructura generada

Cuando terminamos de establecer todas las opciones y tras hacer clic en el botón “Generate Project” nos descargaremos un archivo comprimido que contiene la estructura base de nuestro proyecto. Vamos a echar un vistazo a ver qué contiene. Tomamos como ejemplo los proyectos construidos para maven con empaquetado de jar. La estructura generada para gradle es casi idéntica. Como podemos ver se generan los directorios necesarios para los paquetes pertenecientes a código (src/main/java) y pruebas (src/test/java). Además se genera el directorio de recursos (src/main/resources) con un fichero application.properties vacío dentro.

initializr_01

En caso de haber especificado el empaquetado como war la estructura generada incluye además dos directorios adicionales: Un directorio para los recursos estáticos utilizados por la aplicación web tales como archivos .css e imagenes (src/main/resources/static) y otro para las vistas de la tecnología que utilicemos (src/main/resources/templates). Estos son los directorios que utiliza spring boot para este tipo de recursos por defecto.

initializr_02

4. Initializr desde IntelliJ IDEA

En caso de ser usuario de IntelliJ IDEA el propio IDE cuenta con integración con el portal de Spring Initializr, con lo que seremos capaces de generar el proyecto sin tener siquiera que abrir un navegador web. En este caso los pasos a seguir son: Creamos un proyecto o módulo nuevo desde el menú con File > New > Project… o File > New > Module…. Esto hará aparecer la ventana de selección de tipo de proyecto.

initializr_03

En esa ventana seleccionamos como tipo de proyecto/módulo “Spring Initializr”. En este momento nos dejará escoger la JDK a utilizar de entre las existentes y la URL de Spring Initializr, que dejaremos tal como está.

initializr_04

En las siguientes ventana elegimos los mismos campos que en la interfaz web. Nada que no hayamos visto en la sección anterior.

initializr_05 initializr_06

Finalmente el wizard nos pide el nombre y ruta locales del nuevo proyecto o módulo y nos preguntará si queremos importarlo. Ya tenemos el nuevo proyecto en nuestro entorno disponible para comenzar a trabajar. initializr_08

5. Conclusiones

Hemos visto lo sencillo que resulta iniciar un proyecto Spring Boot utilizando Spring Initializr. Esta opción sustituye de facto a la herramienta archetype de Maven. Hemos aprendido a utilizar la herramienta a través de su interfaz web y a través de IntelliJ IDEA.

6. Referencias


Extendiendo sonarqube con un plugin personalizado

$
0
0
En este tutorial veremos cómo extender sonarqube, la herramienta open source más conocida para medir la calidad del código de nuestros proyectos, con un plugin personalizado.

Extendiendo sonarqube con un plugin personalizado.

 

0. Índice de contenidos.

1. Introducción

Ya hemos hablado en otros tutoriales sobre cómo extender sonarqube para, por ejemplo, añadir nuestras propias reglas haciendo uso de xpath. En este tutorial vamos a ir un paso más allá, creando nuestro propio plugin. Los puntos de extensión sobre los que podemos trabajar son básicamente los siguientes:
  • añadir reglas de código,
  • añadir eventos para notificar de los resultados de análisis a aplicaciones externas,
  • añadir métricas,
  • añadir el soporte de nuevos lenguajes de programación,
  • añadir el soporte de sistemas de control de versiones,
  • añadir proveedores de autenticación,
  • extender con widgets y traducir la interfaz de usuario.
A todo lo anterior podemos añadir que siempre existe la posibilidad de hacer uso del API REST para desarrollar nuestras propias aplicaciones, fuera del entorno propiamente dicho de sonarqube. El objetivo de este tutorial es examinar las posibilidades que tenemos para la creación de un plugin propio, que se instale como tal en sonarqube y que permita incluir en la interfaz de usuario un widget que explote la información de las métricas de un proyecto. Y si a alguien le sabe a poco, además vamos a trabajar con un sonarqube dockerizado, para que montarnos el entorno de desarrollo y desplegar el plugin sea sencillo.

2. Entorno.

El tutorial está escrito usando el siguiente entorno:
  • Hardware: Portátil MacBook Pro 15′ (2.5 GHz Intel Core i7, 16GB DDR3).
  • Sistema Operativo: Mac OS El Capitan 10.11
  • Sonarqube 5.6
  • Docker 1.11.2.

3. Preparación del entorno de desarrollo.

Como comentábamos en la intro vamos a trabajar con una imagen de docker para desplegar sonarqube y hacer nuestras pruebas de despliegue del plugin; en realidad vamos a trabajar con docker-compose que nos permite configurar y desplegar en una sola acción más de un contenedor de docker ya que, en nuestro caso, necesitamos una base de datos, no queremos trabajar con la embebida y la propia aplicación de sonarqube. Para ello, lo primero es crearnos un fichero docker-compose.yml con el siguiente contenido que luego ubicaremos en la raíz de nuestro proyecto.
version: '2'
  services:
    sonarqube:
      build:
        context: .
      image: sonarqube:5.6
      ports:
        - 9000:9000
      links:
        - db:mysql
      networks:
        - sonarnet
      environment:
        - SONARQUBE_JDBC_URL=jdbc:mysql://mysql:3306/sonarqube?useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true
        - SONARQUBE_JDBC_USERNAME=sonarqube
        - SONARQUBE_JDBC_PASSWORD=sonarqube
     volumes:
        - ./src/main/conf/:/opt/sonarqube/conf/

    db:
      image: mysql:5.6
      hostname: mysql
      ports:
        - 3306:3306
      networks:
        - sonarnet
      environment:
        - MYSQL_ROOT_PASSWORD=root
        - MYSQL_USER=sonarqube
        - MYSQL_PASSWORD=sonarqube
        - MYSQL_DATABASE=sonarqube

  networks:
    sonarnet:
      driver: bridge

Estamos usando la sintaxis de la última versión de docker con dos contenedores basados en las imágenes de sonar:5.6 y mysql:5.6; la de sonar depende de la de mysql y ambas se despliegan en una red creada ad hoc. La parte de “volumes” del contenedor de sonarqube nos permite configurar un volumen de datos y “exponer” un directorio de nuestra máquina hacia el contenedor, de modo que para nuestros propósitos podemos mantener los ficheros de configuración de sonar dentro de nuestro propio proyecto para configurarlos de forma externa al contenedor y distribuirlos con el propio entorno de desarrollo. Por último, haremos uso de un par de instrucciones dentro de un fichero Dockerfile para que, crear una imagen nueva copiando el plugin generado al directorio de plugins del contenedor y así poder probar nuestro desarrollo.

FROM sonarqube
ADD  ./target/sonar-tnt-audit-plugin-0.0.1-SNAPSHOT.jar /opt/sonarqube/extensions/plugins/sonar-tnt-audit-plugin-0.0.1-SNAPSHOT.jar

En este último punto nos estamos adelantando a la creación del proyecto del plugin propiamente dicho, pero es justo lo que vamos a ver a continuación.

4. Creación del proyecto.

Sonarqube proporciona soporte para crear un proyecto de plugin basado en maven, para ello no tenemos más que crearnos un proyecto java simple, haciendo uso de arquetipo maven-archetype-quickstart y sustituir el pom.xml por uno como el siguiente:
<?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/maven-v4_0_0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>com.autentia.sonar.plugins</groupId>
	<artifactId>sonar-tnt-audit-plugin</artifactId>
	<packaging>sonar-plugin</packaging>
	<version>0.0.1-SNAPSHOT</version>

	<name>SonarQube Autentia audit :: Plugin</name>
	<description>Autentia audit plugin for SonarQube</description>

	<organization>
		<name>Autentia</name>
		<url>http://www.autentia.com</url>
	</organization>
	<licenses>
		<license>
			<name>GNU LGPL 3</name>
			<url>http://www.gnu.org/licenses/lgpl.txt</url>
			<distribution>repo</distribution>
		</license>
	</licenses>

	<properties>
		<sonar.pluginName>Autentia audit</sonar.pluginName>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<sonar.apiVersion>5.6</sonar.apiVersion>
		<jdk.min.version>1.8</jdk.min.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.sonarsource.sonarqube</groupId>
			<artifactId>sonar-plugin-api</artifactId>
			<version>${sonar.apiVersion}</version>
			<scope>provided</scope>
		</dependency>
		<dependency>
			<groupId>org.sonarsource.sonarqube</groupId>
			<artifactId>sonar-ws</artifactId>
			<version>${sonar.apiVersion}</version>
		</dependency>
		<dependency>
			<!-- packaged with the plugin -->
			<groupId>commons-lang</groupId>
			<artifactId>commons-lang</artifactId>
			<version>2.6</version>
		</dependency>


		<!-- unit tests -->
		<dependency>
			<groupId>org.sonarsource.sonarqube</groupId>
			<artifactId>sonar-testing-harness</artifactId>
			<version>${sonar.apiVersion}</version>
			<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.sonarsource.sonar-packaging-maven-plugin</groupId>
				<artifactId>sonar-packaging-maven-plugin</artifactId>
				<version>1.16</version>
				<extensions>true</extensions>
				<configuration>
					<pluginClass>com.autentia.sonar.plugins.AuditPlugin</pluginClass>
				</configuration>
			</plugin>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>3.5.1</version>
				<configuration>
					<source>${jdk.min.version}</source>
					<target>${jdk.min.version}</target>
				</configuration>
			</plugin>
			<plugin>
				<!-- UTF-8 bundles are not supported by Java, so they must be converted
					during build -->
				<groupId>org.codehaus.mojo</groupId>
				<artifactId>native2ascii-maven-plugin</artifactId>
				<version>1.0-beta-1</version>
				<executions>
					<execution>
						<goals>
							<goal>native2ascii</goal>
						</goals>
					</execution>
				</executions>
			</plugin>
		</plugins>
	</build>
</project>

En el plugin de empaquetación debemos indicar la ruta y nombre de una clase que hará el registro de nuestros componentes del plugin en los distintos contextos.

<pluginClass>com.autentia.sonar.plugins.AuditPlugin</pluginClass>

Con el soporte del plugin de maven de sonarqube, se empaquetará un jar preparado para ser desplegado en sonarqube, justo el artefacto que configurábamos en el punto anterior para copiar en el directorio de plugins del contenedor en el Dockerfile. Solo nos falta un paso más, opcional, si queremos configurar sonar en modo desarrollo podemos extraer el fichero sonar.properties del contenedor y ubicarlo en un directorio de conf que será el que se monte como volumen en el contenedor: Si ejecutamos la siguiente secuencia de comandos, se levantarán los dos contenedores en segundo plano:

mvn package
  docker-compose up -d

Como comentaba, podemos copiarnos el fichero de propiedades haciendo uso del siguiente comando:

docker cp sonarqube:/opt/sonarqube/conf/sonar.properties ./conf/sonar.properties

En el mismo podríamos modificar la siguiente variable

sonar.web.dev=true

y reiniciar el contenedor para que tome en caliente los cambios

docker-compose restart sonarqube

Al arrancar docker, si utilizamos las tools, nos informa de la ip asociada a la máquina virtual:

##         .
                  ## ## ##        ==
               ## ## ## ## ##    ===
           /"""""""""""""""""\___/ ===
      ~~~ {~~ ~~~~ ~~~ ~~~~ ~~~ ~ /  ===- ~~~
           \______ o           __/
             \    \         __/
              \____\_______/


docker is configured to use the default machine with IP 192.168.99.100
For help getting started, check out the docs at https://docs.docker.com

Si accedemos desde un navegador a sonarqube, en mi caso http://192.168.99.100:9000, entramos con un usuario administrador (admin:admin) al update center
sonarqube-custom-plugin-01
Podríamos comprobar que el plugin se encuentra instalado
sonarqube-custom-plugin-02
Si tenemos problemas con la copia del artefacto, del jar, podemos entrar en el contenedor:

docker exec -it sonartntauditplugin_sonarqube_1 bash

Para comprobar que el jar está en la ubicación correcta:

root@0c5f7cf0a83a:/opt/sonarqube/extensions/plugins# pwd
  /opt/sonarqube/extensions/plugins
  root@0c5f7cf0a83a:/opt/sonarqube/extensions/plugins# ls -la
  total 26816
  drwxr-xr-x 2 root root    4096 Jul 12 07:22 .
  drwxr-xr-x 6 root root    4096 Jul  8 15:00 ..
  -rw-r--r-- 1 root root     128 Apr 11 09:58 README.txt
  -rw-r--r-- 1 root root 7797781 Apr  7 15:23 sonar-csharp-plugin-5.0.jar
  -rw-r--r-- 1 root root 3191477 Apr 28 08:44 sonar-java-plugin-3.13.1.jar
  -rw-r--r-- 1 root root 1678073 Apr  7 15:23 sonar-javascript-plugin-2.11.jar
  -rw-r--r-- 1 root root 3233128 Apr  7 15:23 sonar-scm-git-plugin-1.2.jar
  -rw-r--r-- 1 root root 6564535 Apr  7 15:23 sonar-scm-svn-plugin-1.3.jar
  -rw-r--r-- 1 root root 4970028 Jul 12 07:26 sonar-tnt-audit-plugin-0.0.1-SNAPSHOT.jar
  root@0c5f7cf0a83a:/opt/sonarqube/extensions/plugins#

Si no se encontrarse, siempre podemos ejecutar una copia manual desde el contenedor, ejecutando el siguiente comando:

docker cp target/sonar-tnt-audit-plugin-0.0.1-SNAPSHOT.jar sonartntauditplugin_sonarqube_1:/opt/sonarqube/extensions/plugins/sonar-tnt-audit-plugin-0.0.1-SNAPSHOT.jar

Si tenemos sonar en modo desarrollo se recargará, sino podemos recargar el contenedor manualmente

docker-compose restart sonarqube

El modo de trabajo a partir de este punto será, modificar el código del proyecto, compilar y empaquetar con el soporte de maven:

mvn clean package

Después, hacer una copia del artefacto generado:

docker cp target/sonar-tnt-audit-plugin-0.0.1-SNAPSHOT.jar sonartntauditplugin_sonarqube_1:/opt/sonarqube/extensions/plugins/sonar-tnt-audit-plugin-0.0.1-SNAPSHOT.jar

y esperar a que se redespliegue o hacer un reinicio manual

docker-compose restart sonarqube

También podríamos montar un volumen externo que apunte al directorio de plugins fuera, en vez de hacer copias del fichero; sería otra opción.

5. Explorando las distintas opciones.

Una vez disponemos de un entorno de desarrollo y con el objetivo en mente de crear nuestro plugin debemos ver qué tipo de funcionalidad vamos a implementar puesto que en función de la misma tenemos accesibles distintos tipos de APIs de sonarqube:
  • @Batch: para la ejecución de componentes dentro del proceso de análisis de código,
  • @ComputeEngineSide: para generar informes en base al análisis de código con la posibilidad de persistir la información en la base de datos,
  • @ServerSide: para generar componentes del lado del servidor, que respondan a llamadas HTTP
Nuestro objetivo es integrarnos en la UI de sonarqube, configurar un widget que permita realizar una llamada a un componente de servidor y probar a descargar información sobre un análisis ya realizado.

5.1. UI widget.

Lo primero que vamos a hacer es configurar un widget que permita la inclusión de un componente visual en el dashboard a nivel de proyecto. Tanto los componentes como los controladores se programan en Ruby, más bien en Ruby On Rails. Para ello, creamos una clase que implemente RubyRailsWidget con un código como el siguiente:
package com.autentia.sonar.plugins.widgets;

import org.sonar.api.web.AbstractRubyTemplate;
import org.sonar.api.web.Description;
import org.sonar.api.web.RubyRailsWidget;
import org.sonar.api.web.WidgetProperties;
import org.sonar.api.web.WidgetProperty;
import org.sonar.api.web.WidgetPropertyType;
import org.sonar.api.web.WidgetScope;


@Description("Exports Autentia audit report")
@WidgetScope(org.sonar.api.web.WidgetScope.PROJECT)
@WidgetProperties({
  @WidgetProperty(key = "max", type = WidgetPropertyType.INTEGER, defaultValue = "80")
})
public class AuditWidget extends AbstractRubyTemplate implements RubyRailsWidget {
	public String getId() {
		return "autentia_widget";
	}

	public String getTitle() {
		return "Autentia audit widget";
	}

	protected String getTemplatePath() {
		return "/widgets/audit.erb";
	}

}
Proporcionamos un identificador de widget, un título y una ruta a la plantilla que tendrá la “lógica de presentación”. Este componente de tipo widget lo tenemos que registrar en la clase que implementa la interfaz Plugin y que previamente ya hemos configurado en el pom.xml como pluginClass:
package com.autentia.sonar.plugins;

  import java.util.Arrays;

  import org.sonar.api.Plugin;

  import com.autentia.sonar.plugins.widgets.AuditWidget;
  import com.autentia.sonar.ws.ExportAuditReportWS;

  public class AuditPlugin implements Plugin {

  	@Override
  	public void define(Context context) {
  		context.addExtensions(Arrays.asList(AuditWidget.class));
  	}
  }
Un último paso, sería crear la plantilla del widget, bajo la carpeta de resources de nuestro proyecto maven, en la ubicación indicada widgets, con un código similar al siguiente:
<span class="widget-label">Informe de auditoría&ly;/span>
  <p><a href="<%= ApplicationController.root_context -%>/api/reports/audit/export?project=<%= @snapshot.project_id %>">Descarga</a></p>
  <br />
Lo único que estamos haciendo por ahora es incluir un componente visual con un enlace que permite la invocación a un componente de servidor al que le pasaremos como parámetro el id del proyecto. compilamos, empaquetamos, copiamos el artefacto y redesplegamos… A nivel visual, en el dashboard del proyecto, podremos añadir el widget:
sonarqube-custom-plugin-03
Y hacer uso también del mismo
sonarqube-custom-plugin-04
Aunque ahora invoca a un recurso del servidor que aún no existe. La renderización del widget también se puede probar invocando a la siguiente URL con el id del widget:
http://192.168.99.100:9000/widget?id=autentia_widget

5.2. Controlador en ROR.

La primera opción que tenemos para implementar un componente del lado del servidor es crear un controlador en ruby, que reciba la petición con el id del proyecto y devuelva información sobre las métricas del mismo. Para registrar un controlador debemos configurar una aplicación ROR añadiendo en la ruta org/sonar/ror/tntaudit/ un fichero init.rb Dentro de esa misma ruta crearemos una carpeta /app/controllers/ y otra /app/view de modo que tendremos una estructura de directorios como la siguiente:
sonarqube-custom-plugin-05
El código del controlador podría tener el siguiente contenido como prueba de descarga de información del proyecto
class ExportProjectIssuesController < ApplicationController require 'builder' def index if params[:id] @project=Project.by_key(params[:id]) return project_not_found unless @project end xml_data = "" xml = Builder::XmlMarkup.new(:target => xml_data, :indent => 2 )
  	xml.instruct! :xml, :encoding => "UTF-8"

  	xml.project {
  		xml.comment! @project.attributes.inspect
  		xml.id @project.uuid
  		xml.name @project.name
  	}

    send_data( xml_data, :filename => "#{@project.name}.xml" , :type => "application/xml" )

    end

    private

    def project_not_found
      flash[:error] = message('dashboard.project_not_found')
      redirect_to :controller => 'dashboard', :action => 'index'
    end

  end

Para invocar al controlador no tenemos más que realizar una llamada a la siguiente URL, http://192.168.99.100:9000/export_project_issues/index?id=1, no hay prefijos, por eso recomiendan tener cuidado con la nomenclatura para no pisar los controladores propios de la aplicación. El resultado de la invocación será un código como el siguiente:

<?xml version="1.0" encoding="UTF-8"?>
<project>
  <!-- {"authorization_updated_at"=>1467990070180, "copy_resource_id"=>nil, "created_at"=>Fri Jul 08 15:01:10 +0000 2016, "deprecated_kee"=>"com.autentia.tnt:tnt-labs", "description"=>nil, "enabled"=>true, "id"=>1, "kee"=>"com.autentia.tnt:tnt-labs", "language"=>nil, "long_name"=>"tnt-labs", "module_uuid"=>nil, "module_uuid_path"=>".AVXLBtOQndgnqZ1jOKLd.", "name"=>"tnt-labs", "path"=>nil, "person_id"=>nil, "project_uuid"=>"AVXLBtOQndgnqZ1jOKLd", "qualifier"=>"TRK", "root_id"=>nil, "scope"=>"PRJ", "uuid"=>"AVXLBtOQndgnqZ1jOKLd"} -->
  <id>AVXLBtOQndgnqZ1jOKLd</id>
  <name>tnt-labs</name>
</project>

No hay documentado un API que permita el acceso a las métricas o las violaciones de código de un proyecto desde un controlador Ruby, este tipo de componente está más orientado a generar una vista, renderizaría por defecto una view en la ruta /ror/tntaudit/app/views/export_project_issues, como el nombre del controlador y la acción definida /index.erb; convención sobre configuración! La idea sería generar una vista HTML y con el soporte del API javascript del propio sonarqube consumir los servicios REST que permiten el acceso a la información de métricas de un proyecto. Cabría también la posibilidad de invocar a un componente Java desde el controlador Ruby, con un código como el siguiente:

auditReport = Api::Utils.java_facade.getComponentByClassname('tntaudit', 'com.autentia.sonar.plugins.reports.AuditReport')
xml_data = auditReport.generate(@project.kee)

Donde AuditReport es una clase java que se encuentra en el paquete com.autentia.sonar.plugins.reports y tntaudit el identificador de nuestro plugin. Esa clase podría tener la lógica de generación de información si bien para acceder a la misma la propuesta de sonarqube es hacer uso del API REST y no está especialmente pensada la inyección de dependencias para disponer de ese API en un componente Java.

5.3. Web service en java.

Hay una alternativa al uso de controladores Ruby si lo que pretendemos es únicamente descargar información sobre métricas del proyecto en un formato estándar; es el uso de servicios REST programados en Java. Para crear un servicio web debemos añadir una clase con un código como el siguiente:
package com.autentia.sonar.ws;

  import org.sonar.api.server.ws.Request;
  import org.sonar.api.server.ws.RequestHandler;
  import org.sonar.api.server.ws.Response;
  import org.sonar.api.server.ws.WebService;

  import com.autentia.sonar.plugins.reports.AuditReport;

  public class ExportAuditReportWS implements WebService {

  	private static final String PROJECT_KEY = "project";

  	@Override
  	   public void define(Context context) {
  	     NewController controller = context.createController("api/reports/audit");
  	     controller.setDescription("Export audit report");

  	     controller.createAction("export")
  	       .setDescription("Export audit report")
  	       .setHandler(new RequestHandler() {
  	          @Override
  	         public void handle(Request request, Response response) {
  	        	  final String project = request.mandatoryParam(PROJECT_KEY);
  	           response.newJsonWriter()
  	             .beginObject()
  	             .prop("project", request.mandatoryParam(PROJECT_KEY))
  	             .prop("issues", AuditReport.getSingleton().generate(request, project))
  	             .endObject()
  	             .close();
  	         }
  	      })
  	      .createParam(PROJECT_KEY).setDescription("Project key").setRequired(true);

  	    controller.done();
  	   }
  }

Se pueden definir parámetros obligatorios o no. El servicio hay que registrarlo en el plugin junto con el widget:

public class AuditPlugin implements Plugin {

  	@Override
  	public void define(Context context) {

  		context.addExtensions(Arrays.asList(AuditWidget.class, ExportAuditReportWS.class));
  	}

  }

Para consumir el servicio REST bastaría con realizar una invocación vía GET a la siguiente URL: http://192.168.99.100:9000/api/reports/audit/export?project=com.autentia.tnt:tnt-labs, la idea es realizar esa llamada desde el código HTML del widget. Ahora sí, desde el servicio web o desde una clase delegada y apoyándonos en la petición original pueden desencadenar peticiones haciendo uso de una conexión local a servicios web del api pública desde nuestro servicio web, que es el camino recomendado por el fabricante.

final org.sonarqube.ws.client.issue.SearchWsRequest searchIssuesRequest = new org.sonarqube.ws.client.issue.SearchWsRequest();
  searchIssuesRequest.setProjectKeys(Arrays.asList(new String[] { project }));
  final WsClient wsClient = WsClientFactories.getLocal().newClient(request.localConnector());
  final org.sonarqube.ws.Issues.SearchWsResponse issues = wsClient.issues().search(searchIssuesRequest);

6. Referencias.

7. Conclusiones.

Una vez estudiadas las posibilidades y teniendo acceso a las métricas y violaciones del proyecto, solo nos queda programar la exportación en el formato que elijamos. A disfrutarlo!. Un saludo. Jose

Usar Marked 2 para previsualizar AsciiDoc

$
0
0

Marked 2 es una aplicación para previsualizar ficheros en formato Markdown, pero ¿sabías que puede trabajar con casi cualquier fichero de marcas? En este artículo te enseñamos cómo trabajar con ficheros en formato AsciiDoc.

Índice de contenidos


Marked 2 con AsciiDoc


1. Introducción

He trabajado bastante tiempo con Markdown como sistema de documentación, pero después de probar AsciiDoc me quedo con este último por ser igual de sencillo que el primero pero encima mucho más flexible y completo.

Para saber más sobre AsciiDoc @esloho nos lo pone fácil con el tutorial Documentación con Asciidoctor: buena, bonita y barata. Muy recomendable.

En mis tiempos de Markdown me compré Marked 2, una estupenda aplicación de escritorio para hacer la pre-visualización de lo que vas escribiendo. Es sencilla y muy barata para la buena funcionalidad que proporciona. Al empezar a trabajar con AsciiDoc dejé esta aplicación de lado, pero sólo por torpeza ya que Marked 2 no sólo es capaz de trabajar con Markdown, sino prácticamente con cualquier lenguaje que podamos procesar mediante algún programa.

Así que en este tutorial vamos a ver cómo podemos usar Asciidoctor (el procesador de AsciiDoc) para mostrar los resultados en Marked 2.

Sí, sí, ya sé que Atom tiene un plugin que nos permite hacer directamente una pre-visualización del AsciiDoc que estamos escribiendo pero, qué queréis que os diga, para estas cosas me cuesta dejar mi querido Vim (cada uno puede usar su editor preferido). Además un poquito de postureo nunca viene mal, y siendo más analíticos Atom, entre otras cosas, no soporta todavía corrección ortográfica en otros idiomas que no sea el inglés y sí, Vim también tiene cursor múltiple.


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: Mac OS X El Capitan 10.11.5

  • Asciidoctor 1.5.4

  • Marked 2 v2.5.6 (922)


3. Instalación de Asciidoctor

Lo primero que tenemos que hacer es instalar Asciidoctor para poder procesar los ficheros de texto en formato AsciiDoc. En el tutorial de @esloho mencionado antes tenéis una opción, aquí os voy a mostrar otra.

Vamos a aprovechar que en OS X ya viene instalado Ruby para instalarlo directamente. Abrimos un terminal y ejecutamos:

$ sudo gem update --system
$ sudo gem install -n /usr/local/bin asciidoctor
  1. Actualizamos las librerías de Ruby

  2. Instalamos la gema de Asciidoctor. Cabe destacar la opción -n para instalar el ejecutable asciidoctor en una ruta donde el sistema operativo nos deje escribir (por defecto se instalaría en /usr/bin pero OS X, no nos dejará escribir en esta ruta por restricciones de seguridad)


4. Configuración de Marked 2

Si la aplicación Marked 2 la habéis comprado a través del App Store, la configuración que se expone aquí no os va a funcionar correctamente y os va a dar un error diciendo que no puede ejecutar el procesador externo porque no tiene permisos. Esto se debe a una serie de restricciones de seguridad que impone el OS X a las aplicaciones del App Store para que no puedan ejecutar procesos externos en nuestro ordenador. Podéis ver en el hilo en: custom-preprocessor-is-not-executable.

A mi me pasaba esto, puesto que la aplicación la compré en el App Store, y para solucionarlo lo mejor es que uséis este formulario para poneros en contacto con el creador de Marked 2, Brett, explicándole vuestro problema, para que os dé un código de licencia válido para la aplicación descargada desde la web.

Una vez tenemos instalada la aplicación descargada de la web, o si esta era nuestra situación inicial por no haber usado el App Store para la compra, podemos pasar a realizar la sencilla configuración.

Id a las preferencias, y abrid la pestaña de “Advanced”. En esta pestaña debéis activar el check de “Enable Custom Processor” y poned los siguientes valores:

Path: /usr/local/bin/asciidoctor
Args: --backend html5 -o - -

Os dejo una imagen para que veáis como debe quedar:

Marked 2 Advanced Preferences

Es importante que os aparezca el “OK” en verde. Para ello, la primera vez que pongáis el valor es probable que os pida permiso para acceder a esa ruta, aceptad el diálogo que os parece.


5. Conclusiones

Con estos sencillos pasos ya podéis abrir ficheros en formato AsciiDoc (extensión .adoc) y trabajar con ellos como si de Markdown se tratase. Os dejo una imagen de como he escrito este mismo tutorial usando Vim + AsciiDoc + Marked 2.

Vim AsciiDoc and preview with Marked 2

Y acordaos de lo que os digo siempre, explorad las opciones de vuestras herramientas de trabajo para sacarles el máximo partido. Veréis cómo en muchos casos os sorprenderéis de lo que os estáis perdiendo 😉


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”

Auditoría de entidades con Hibernate Envers y Spring Data JPA.

$
0
0

En este tutorial veremos cómo configurar un sistema de auditoría de cambios en las entidades de nuestra capa de persistencia con el soporte de Hibernate Envers y Spring Data JPA.

Auditoría de entidades con Hibernate Envers y Spring Data JPA.


0. Índice de contenidos.


1. Introducción

Antes o después, en cualquier aplicación de gestión empresarial, se plantea la necesidad de mantener un histórico de quién ha hecho qué, cuándo y desde dónde con la información que se mantiene en nuestro sistema. No solo por aspectos legales, sino de simple control de cambios, en algún momento nos pedirán mantener una auditoría de cambios sobre esa información.

A nivel técnico podemos implementarlo de muchas maneras:

  • a bajo nivel directamente en la base de datos con triggers,
  • enganchándonos con los eventos del ORM que nos de soporte a persistencia,
  • con el soporte de AOP si usamos algún framework que nos de soporte para ello,
  • ensuciando el código con consultas innecesarias, consumo de memoria y objetos en sesión para realizar comparaciones y la persistencia a mano,…

Entendiendo que la última opción es inadmisible, en este tutorial vamos a ver cómo usando Hibernate o el soporte de éste para JPA, podemos implementar un control de cambios de una manera muy limpia y poco intrusiva en nuestro código, con la extensión Hibernate Envers.

Para enriquecer un poco más el tutorial además harenos uso de la librería Spring Data Envers, que extiende los repositorios de Spring Data para permitir, de una manera sencilla, el acceso al histórico de modificaciones de una entidad.

2. Entorno.

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro 15′ (2.5 GHz Intel Core i7, 16GB DDR3).
  • Sistema Operativo: Mac OS El Capitan 10.11
  • Hibernate 4.3.11.Final
  • Spring Data 1.10.2.RELEASE
  • Spring Data Envers 1.0.2.RELEASE

3. Estrategia de versionado.

Hibernate Envers extiende el soporte por defecto del ORM para engancharse en el ciclo de vida de persistencia de una entidad; sabiendo que una entidad está marcada como detached y los cambios realizados sobre la misma, puesto que tiene que realizar un update de esos cambios, “aprovecha” para almacenar la información de los mismos en una tabla de auditoría.

Esa estrategia pasa por mantener una tabla espejo de la original añadiendo dos columnas adicionales por defecto:

  • REV: identificador de la revisión
  • REVTYPE: tipo de revisión (1 inserción, 2 modificación o 3 borrado)

El identificador de la revisión mantiene una clave foránea con una tabla con el histórico de información de todas las revisiones que podemos enriquecer con la información que estimemos necesaria: timestamp, user_name, remote_address,…

hibernate-envers-spring-data-01

Si estamos usando el soporte de Hibernate para generar el modelo en nuestro entorno de tests, para crear las tablas automáticamente solo debemos hacer uso de la propiedad: <property name=”hibernate.hbm2ddl.auto” value=”update” />


4. Configuración.

Lo primero, como no podía ser de otra forma es añadir las dependencias de las librerías que vamos a necesitar en nuestro pom.xml:

<dependency>
      <groupId>org.springframework.data</groupId>
      <artifactId>spring-data-jpa</artifactId>
      <version>1.10.2.RELEASE</version>
  </dependency>
  <dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-envers</artifactId>
    <version>1.0.2.RELEASE</version>
  </dependency>

Es importante la correlación de versiones, una versión inferior de la librería spring-data-jpa hará que los repositorios no funcionen correctamente.

Desde el punto de vista estríctamente de Hibernate Envers lo único que debemos hacer es configurar, si lo estimamos necesario, las propiedades que nos permiten renombrar las tablas o sufijos de las mismas donde se realizará la auditoría. Así, en la declaración de la factoría de entityManagers para JPA.

<bean id="entityManagerFactory"
    class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
    <property name="dataSource" ref="rbacDataSource" />
    <property name="jpaVendorAdapter">
      <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
        <property name="databasePlatform" value="${hibernate.dialect}" />
        <property name="showSql" value="${hibernate.show_sql}" />
      </bean>
    </property>
    <property name="jpaProperties" >
      <map>
        <entry key="org.hibernate.envers.audit_table_suffix" value="_AUDIT" />
        <entry key="org.hibernate.envers.revision_field_name" value="REVISION_ID" />
        <entry key="org.hibernate.envers.revision_type_field_name" value="REVISION_TYPE" />
      </map>
      </property>
    <property name="packagesToScan"
      value="com.autentia.tnt.persistence.**.domain" />
  </bean>

Desde el punto de vista de los repositorios de Spring Data debemos añadir la siguiente configuración:

<jpa:repositories entity-manager-factory-ref="entityManagerFactory"
  base-package="com.autentia.tnt.persistence.**.repository"
  transaction-manager-ref="transactionManager"
  factory-class="org.springframework.data.envers.repository.support.EnversRevisionRepositoryFactoryBean"/>

Cuando se generen las implementaciones de los repositorios, la factoría EnversRevisionRepositoryFactoryBean añadirá las implementación de las operaciones necesarias para recuperar el listado de revisiones de una entidad y permitirá el acceso al detalle de una revisión concreta.


5. Uso.

Para enriquecer la tabla de auditoría debemos primero mapearla añadiendo una entidad como la que sigue, con las propiedades que estimemos necesarias para almacenar:

package com.autentia.tnt..persistence.domain;

  import java.io.Serializable;
  import java.util.Date;

  import javax.persistence.Column;
  import javax.persistence.Entity;
  import javax.persistence.GeneratedValue;
  import javax.persistence.GenerationType;
  import javax.persistence.Id;
  import javax.persistence.SequenceGenerator;
  import javax.persistence.Table;
  import javax.persistence.Temporal;
  import javax.persistence.TemporalType;

  import org.hibernate.envers.DefaultRevisionEntity;
  import org.hibernate.envers.RevisionEntity;
  import org.hibernate.envers.RevisionNumber;
  import org.hibernate.envers.RevisionTimestamp;

  @Entity
  @Table(name="REVISION_INFO", schema="TEST")
  @RevisionEntity(CustomRevisionListener.class)
  public class Revision implements Serializable {

  	@Id
  	@GeneratedValue(strategy=GenerationType.SEQUENCE, generator="revision_seq")
  	@SequenceGenerator(
  		name="revision_seq",
  		sequenceName="rbac.seq_revision_id"
  	)
  	@RevisionNumber
  	private int id;

  	@Column(name="REVISION_DATE")
  	@Temporal(TemporalType.DATE)
  	@RevisionTimestamp
  	private Date date;

  	@Column(name="USER_NAME")
      private String userName;

      public void setUserName(String userName) {
  		this.userName = userName;
  	}

      public String getUserName() {
  		return userName;
  	}

      public Date getDate() {
  		return date;
  	}

  }

Con la anotación @RevisionNumber marcamos la propiedad con la clave única de la revisión y con la anotación @RevisionTimestamp la propiedad que almacenará la fecha de modificación.

Con la anotación @RevisionEntity haremos referencia a un listener que se ejecutará previo a las operaciones de auditoría que será el que realmente dote de contenido dicha información.

Como se puede ver a continuación podemos añadir información sobre el usuario obteniéndolo del contexto.

package com.autentia.tnt.persistence.domain.listeners;

import org.hibernate.envers.RevisionListener;

import com.autentia.tnt.context.AccountThreadContext;

public class CustomRevisionListener implements RevisionListener {

	public void newRevision(Object revisionEntity) {
		final Revision revision = (Revision) revisionEntity;
		revision.setUserName(getThreadAccountUserName());
    }

	private String getThreadAccountUserName() {
		if (AccountThreadContext.getAccount() != null){
			return AccountThreadContext.getAccount().getCode();
		}
		return "NOT_FOUND";
	}

}

Lo interesante es que este listener es capaz de engancharse con Spring Security para recuperar información del usuario conectado, así como de la ip remota de acceso.

En este punto ya podemos añadir la configuración necesaria a nuestras entidades para auditarlas, bastaría la anotación @Audited, como se puede ver a continuación:

@Entity
@Table(name="GROUPS_VERSION", schema="RBAC")
@Audited(targetAuditMode = RelationTargetAuditMode.NOT_AUDITED)
@AuditTable(value="GROUPS_VERSION_AUDIT", schema="TEST")
public class Book extends BaseDomain{

A más podemos indicar que solo audite las relaciones de la entidad, no las entidades relacionadas con targetAuditMode = RelationTargetAuditMode.NOT_AUDITED.

También podemos renombrar la estrategia por defecto de nombres indicando con la anotación @AuditTable el nombre y esquema de la tabla de auditoría.

Por último, para habilitar el acceso a las operaciones de recuperación de información en el repositorio de Spring Data basta con extender de la interfaz RevisionRepository.

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.repository.history.RevisionRepository;
import org.springframework.data.repository.query.Param;

import com.autentia.tnt.persistence.domain.Book;

public interface BookRepository extends RevisionRepository, JpaRepository {

}

Esta interfaz añade operaciones como las siguientes:

hibernate-envers-spring-data-02

6. Tests.

Como no podía ser de otra forma comprobaremos que todo funciona correctamente con un test de integración que validará que la información de las revisiones se almacena correctamente.

Podría tener un código como el siguiente:

@Test
	public void shouldPersistRevisionInfo() throws Exception {

		TestTransaction.flagForCommit();

		final Book book = repository.findById(ID);

    book.setSomeThing("SOME_THING");

		repository.save(book);

		TestTransaction.end();

		TestTransaction.start();
		final List> revisions = repository.findRevisions(ID).getContent();
		assertThat(revisions.size(), equalTo(1));

		TestTransaction.end();

	}

Tres cuestiones a destacar:

  • con la operación save se almacenará no solo la información de la entidad sino de la revisión, pero al enganchar con el ciclo de vida de las operaciones de persistencia solo será efectiva cuando se haga un flush o se fuerce un commit.
  • en el entorno de tests con el soprote de Spring podemos hacer uso de las operaciones de la clase de utilidades TestTransaction para forzar la apertura y cierre de transacciones.
  • en los repositorios de Spring Data habilitados como hemos visto ahora tendemos operaciones del tipo repository.findRevisions(…) para recuperar la información del histórico de revisiones.

7. Referencias.


8. Conclusiones.

Ahora solo nos faltaría programar un proceso de historificación de los datos de las tablas de auditoría… ups!

Un saludo.

Jose

Introducción a Backbone.js

$
0
0

Introducción a Backbone.js

 

Índice de contenidos.

1. Introducción

En este tutorial os voy a presentar Backbone.js, una potente librería que nos permite desarrollar aplicaciones javascript de una manera sencilla implementando el patrón MVC.

El objetivo de este primer tutorial es haceros una pequeña introducción a Backbone, ver los pilares sobre los que se sostiene y, poco a poco, a través de posteriores tutoriales ir ampliando más materia sobre esta potente librería para, finalmente, desarrollar una pequeña aplicación en la que podamos aplicar todo lo aprendido.

2. ¿Qué es Backbone.js?

Backbone es una librería que nos permite construir de una manera muy sencilla aplicaciones javascript de tipo SPA (Single Page App), implementando el patrón MVC (Modelo Vista Controlador).

Es aquí donde reside la gran potencia de Backbone. Hasta hace unos años era bastante complicado tener aplicaciones javascript que siguiesen cierto orden o estructura. Gracias a Backbone, podemos estructurar el código siguiendo el patrón MVC.

Dentro de nuestra aplicación:
  • El Modelo gestionará los datos de la aplicación y controlará la persistencia de los mismos.
  • La Vista se encarga de la interacción con el usuario y por lo tanto de mostrar los datos de la aplicación al mismo.
  • El Controlador actuará de intermediario entre la capa de datos (el Modelo), y la representación de los mismos (la Vista). Se encargará de comunicar ambas capas.
backbone

3. ¿Cómo funciona?

Backbone se sostiene sobre una serie de piezas clave que nos permitirán, de una forma sencilla, construir cualquier aplicación:
  • Modelos
  • Vistas
  • Colecciones
  • Eventos
Dentro de una aplicación, los datos que ésta gestiona son manejados a través de ‘modelos’, los cuales nos permitirán operar sobre dichos datos, validarlos, procesarlos, etc. Al mismo tiempo, cada vez que realicemos una operación sobre el modelo, Backbone lanzará eventos para comunicar al resto de componentes qué tipo de operación se está realizando sobre éste. Así, de esta forma, podremos asociar dichos modelos a las ‘vistas’ para que, al mismo tiempo, reaccionen al cambio de estado del modelo y se muestren de una manera u otra en función del estado de éste. De esta manera se consigue separar perfectamente la lógica de negocio de la vista.

4. Modelos

En Backbone los modelos son la base de cualquier aplicación. Nos permiten gestionar los datos y la lógica que manejará nuestra aplicación.

Internamente, los modelos implementan un “map” que albergará todos y cada uno de los datos de la aplicación, gestionarán la lógica de negocio y, además, nos proporciona soporte para gestionar el sincronismo entre la capa de datos y la capa de persistencia.

Cada vez que un modelo sufre un cambio, éste lanza un evento que es escuchado por otros componentes de la aplicación. Lo más habitual es asociar un modelo a una vista de la aplicación, así ésta es capaz de reaccionar a dichos cambios de estado y cambiar según el estado de éste. backbone-models Para crear un modelo necesitamos extender de la clase ‘Model’ de Backbone mediante el uso de la palabra reservada ‘extend’. Con esto crearemos una clase de tipo Model en nuestra aplicación:
var Curso = Backbone.Model.extend({});
Para crear una instancia de nuestro modelo bastará con usar la palabra reservada ‘new’:
var curso = new Curso();
En el ejemplo anterior hemos creado un clase de tipo Model sin atributos, pero igualmente podemos crear una clase con una serie de atributos definidos por defecto. Por ejemplo, vamos a definir nuestra clase con los atributos ‘idCurso’ y ‘titulo’.

Para ello será necesario definir en el atributo ‘defaults’ un json con los atributos por defecto que el modelo contendrá cuando se instancie:
var Curso = Backbone.Model.extend({
	defaults: {
		idCurso: '',
		titulo: ''
	}
});

var curso = new Curso();
Si creamos una nueva instancia de nuestro modelo, ésta tendrá los atributos ‘idCurso’ y ‘titulo’ definidos por defecto con sus valores vacíos.

Si queremos crear una instancia pasándole los valores de nuestro curso en el constructor, bastará con pasarle a la clase un json con los valores de los atributos:
var Curso = Backbone.Model.extend({
	defaults: {
		idCurso: '',
		titulo: ''
	}
});

var curso = new Curso({idCurso: '1', titulo: 'Introducción a Backbone'})
Si por el contrario no queremos crear la instancia pasándole los valores desde el constructor, podemos almacenar los datos en el modelo mediante la función ‘set’:
var Curso = Backbone.Model.extend({
	defaults: {
		idCurso: '',
		titulo: ''
	}
});

var curso = new Curso();

curso.set('idCurso', '1');
curso.set('idCurso', 'Primeros pasos con Backbone');
Para recuperar los valores del modelo usaremos la función ‘get’:
console.log(curso.get('idCurso'));
console.log(curso.get('titulo'));
Son muchas las funciones que Backbone nos ofrece para gestionar los modelos de nuestra aplicación. Aquí están las más básicas, hay muchas más, pero será en otros tutoriales en donde profundizaremos más sobre estas funciones.

5. Vistas.

Las vistas nos permiten representar, a nivel de interfaz de usuario, los datos que son manejados por los modelos.

Normalmente su funcionamiento consiste en suscribirse a los eventos que los modelos lanzarán cuando su estado cambie. Una vez se ha capturado un evento, la vista mostrará la representación del modelo en el navegador.

Backbone no da soporte a plantillas HTML de forma directa, por lo que será necesario usar librerías de terceros para gestionar, de manera sencilla, las plantillas HTML. En futuros tutoriales veremos cómo usarlas.

Para crear una vista necesitamos extender de la clase ‘View’ de Backbone mediante el uso de la palabra reservada ‘extend’.

Cuando estamos definiendo una vista, definimos elemento HTML que va a representar la vista mediante el atributo ‘tagName’. Si no indicamos este atributo, por defecto la vista que se crea será un ‘div’.
var Curso = Backbone.View.extend({
	tagName: "div”
});
La gran potencia de Backbone reside en la programación orientada a eventos. Como hemos anticipado en la introducción, cada uno de sus componente son capaces de lanzar eventos que son escuchados por el resto, los cuales podrán suscribirse a los que les interese.

Dentro de las vistas seremos capaces de suscribir dicha vista a los eventos resultado de las posibles interacciones entre el usuario y dicha vista.

Para llevarlo a cabo tendremos que definir un atributo ‘events’ dentro de nuestra vista que contendrá el tipo de evento que escuchará dentro del trozo de HTML que nuestra vista representa:
var Curso = Backbone.View.extend({

  tagName: "div",

  events: {
    "click .button": "greetings"
  },

  greetings: function() {
	alert('Hola mundo');
  }

});
En el ejemplo anterior cuando se lance un evento ‘click’ sobre el elemento HTML que contiene nuestra vista, lanzará una llamada a la función ‘greetings’. Cabe destacar que se lanzará sólo en el caso de que la acción de click se haga sobre el elemento que contenga un .class llamado ‘button.’

Mediante el atributo ‘template’ definiremos la plantilla HTML asociada a nuestra vista. Como comentamos anteriormente, Backbone no nos proporciona un sistema de plantilla para representar plantillas HTML. Para ello usaremos librerías de terceros para gestionar de manera sencilla este punto (en el siguiente ejemplo se está usando Underscore.js).
var Curso = Backbone.View.extend({

  tagName: "div",

  events: {
    "click .button": "greetings"
  },

  template: _.template('<button class="button">Hola</button>'),

  greetings: function() {
	alert('Hola mundo');
  }

});


6. Colecciones.

Cuando desarrollemos una aplicación con Backbone nos vamos a encontrar multitud de casos en los que tengamos que lidiar con colecciones de modelos. Y será en esos casos cuando nos surgirá la siguiente pregunta: ¿Cómo puedo representar y gestionar en mi aplicación una lista de modelos de manera sencilla?.

Para eso, Backbone nos proporciona una herramienta muy potente, las colecciones (Collection). Las colecciones son conjuntos ordenados de modelos y lo que nos permiten es gestionar dichos modelos mediante una multitud de funciones y utilidades.

Al igual que ocurre con los modelos, las colecciones también lanzan eventos al resto de elementos de nuestra aplicación cuando su estado, o el de los modelos que contiene, cambian. backbone-collections Para crear una colección de modelos, al igual que en los apartados anteriores, usaremos la palabra reservada ‘extend’ para extender de la clase ‘Collection’ de Backbone. Que duda cabe que antes de crear una collection, necesitaremos definir el tipo de modelo que la colección va a gestionar:
var Curso = Backbone.Model.extend({
	defaults: {
		idCurso: '',
		titulo: ''
	}
});

var CursosCollection = Backbone.Collection.extend({
	model: Curso
});

var cursoCollection = new CursosCollection();
Una vez creada nuestra colección, Backbone nos ofrece una multitud de funciones a aplicar sobre la colección:
  • add: añade un modelo a la colección.
  • remove: elimina un modelo de la colección.
  • reset: cambia el contenido de la colección por el contenido de otra; si no le pasamos otra colección lo que hace es eliminar su contenido.
  • set: actualiza una colección de modelos con la colección de modelos que recibe por parámetro.
  • get: recupera un modelo de una colección, buscándolo por id o pasándole un modelo completo.
  • at: devuelve un modelo de la colección, especificando su posición dentro de la colección.
  • push: añade un nuevo modelo al final de la colección.
  • pop: devuelve y elimina el último modelo de una colección.

7. Eventos.

Como hemos comentado anteriormente, Backbone nos proporciona un sistema de comunicación entre componentes mediante el uso de eventos, de formar que cada uno de los componentes de nuestra aplicación sea capaz de lanzar eventos y suscribirse a otros, si fuese necesario.

Para que un objeto lance un evento en nuestra aplicación, se hace uso de la función ‘trigger’, a la cual se le pasa el nombre del evento a lanzar y los posibles parámetros que viajarán con el evento.

Para que un objeto se pueda suscribir a un evento, será necesario lanzar la función ‘on’ sobre el objeto que lanza dicho evento, pasando el nombre del evento sobre el que se va a suscribir.
var greeting = {};

_.extend(greeting, Backbone.Events);

greeting.on("alert", function(msg) {
  alert("Se ha lanzado " + msg);
});

greeting.trigger("alert", "un evento");
En el ejemplo anterior podemos ver que que al objeto ‘greeting’ se le está dando la posibilidad de lanzar eventos (en tutoriales futuros profundizaremos más sobre este tema), y básicamente es él mismo el que está capturando el evento que está lanzando, mostrando un alert con el siguiente texto: ‘Se ha lanzado un evento’.

Ya hemos comentado anteriormente que tanto los modelos como las colecciones lanzan eventos, a los que nos podemos suscribir, cuando cambian su estado. Aquí os dejo alguno de ellos:
  • ‘add’:cuando un modelo es añadido a una colección
  • ‘remove’: cuando un modelo es eliminado de una colección
  • ‘update’: cuando una colección se ha modificado
  • ‘reset’: cuando una colección ha sido ‘reseteada’
  • ‘sort’: cuando una colección se ha ordenado
  • ‘change’: cuando el atributo de un modelo se ha modificado
  • ‘change:[key]’: cuando un atributo especifico (determinado por ‘key’), se ha modificado

8. Conclusiones.

A día de hoy son muchas las librerías de javascript que tenemos disponibles para desarrollar aplicaciones SPA. Backbone.js es una de las más usadas y debido a su gran potencia y escalabilidad es una de las que, multitud de grandes empresas, han seleccionado para desarrollar sus aplicaciones empresariales.

Como ya os he comentado a lo largo del tutorial, poco a poco, os iré posteando distintos tutoriales profundizando en cada una de las partes que componen esta potente librería para finalmente desarrollar una aplicación completa que iremos mejorando.

Notificaciones push

$
0
0

Queremos evaluar cómo enviar notificaciones a dispositivos móviles.

  1. ¿Qué es una notificación?
  2. Consideraciones al diseñar una notificación
    1. Cuando enviarla
    2. Evita interrupciones
    3. Usa badges
    4. Pide pre-permiso
    5. Copy
    6. Personalización
    7. A|B Testing
    8. Throttle
  3. Opciones de implementación
    1. Componentes en servidor
    2. Plataformas
    3. Conclusión
  4. Mecánica de envío de una notificación
    1. iOS
    2. Android
    3. Web
  5. Referencias

¿Qué es una notificación?

Una notificación push es un mensaje enviado desde un servidor a un cliente, que puede ser un navegador o dispositivo móvil.

¿Quién la envía?
El envío lo inicia el servidor, sin necesidad de que el cliente esté siquiera conectado (lo recibirá la próxima vez que esté online). Para ello cada cliente se registra con el servidor de push de su fabricante (Google, Apple, Mozilla, etc.) cuando es instalado en el sistema operativo.
¿Como se gestiona?
Si el sistema operativo recibe la notificación cuando el cliente está abierto, la notificación es gestionada por un método delegado del código de la aplicación. Si la aplicación está en segundo plano, el sistema operativo muestra una alerta, o un número superpuesto al icono de la aplicación.
¿Qué contiene?
Las notificaciones pueden contener texto, imágenes, e incluso interfaces interactivos a medida. Si pulsamos en una notificación abrimos la aplicación asociada, aunque el uso más común es leerlas y descartarlas.

La notificación es un canal asíncrono con el usuario, pero más directo que otros medios porque aparece como alerta en un dispositivo que el usuario lleva consigo y usa con frecuencia. Para diseñar una notificación conviene conocer que valoran los usuarios de nuestro servicio, y adaptarla a sus intereses y rutina diaria.

Consideraciones al diseñar una notificación

Cuando enviarla

Uso del dispositivo a lo largo del día.

Evita interrupciones

El sistema de notificaciones está diseñado para que el usuario tenga el control. Si la notificación interrumpe al usuario sin ofrecer valor, el usuario se sentirá molesto y silenciará o desinstalará la aplicación.

Las notificaciones se perciben como más disruptivas* cuando

  • El usuario está ocupado.
  • La respuesta requiere una acción compleja.
  • El remitente no es una persona, o es un desconocido.
  • La información no es útil en ese contexto.

Usa badges

Las notificaciones inoportunas son la primera causa de desinstalación de aplicaciones. Si tus notificaciones no están justificadas, no las envíes o rebájalas a “badges” (el numerito en el icono de la app).

Contenido que no requiere mi atención inmediata.

Pide pre-permiso

La petición de permisos para notificaciones debe vincular el permiso con un beneficio para el usuario. Para ello conviene solicitar permisos como parte de una tarea que el usuario realiza, o escribir mensajes personalizados que reflejen el beneficio.

Si el usuario deniega permisos a la aplicación móvil, la aplicación no puede preguntar de nuevo, y es necesario que el usuario otorgue permisos en los ajustes del dispositivo. Para evitarlo, es práctica común preguntar si el usuario desea recibir notificaciones con un dialogo de aplicación, y solo en caso afirmativo lanzamos el dialogo del sistema. Esto requiere dos clicks en vez de uno, pero si la primera pregunta es negativa podremos solicitar permisos en otro momento.

Pre-pregunta

Copy

La comunicación debería ser concisa, clara, original, y subordinada a un objetivo. La longitud del texto disponible es 4k en Android y 2k en iOS, aunque rara vez deberías usar más de un par de frases.

Si tu objetivo es influenciar la conducta hay dos estilos:

  • Racional (“hard sell”): enuncia un beneficio tangible del producto e insta a la acción.
  • Creativo (“soft sell*”): asocia el producto a cualidades ajenas como emociones, o un estilo de vida. Por ejemplo haciendo reír “te echo de menos, vuelve, ¡puedo cambiar!”, asustando “haz esto o sufre las consecuencias” etc. Es el usado por productos sin rasgos diferenciadores, o difíciles de describir, como perfumes.

Un mensaje gracioso tiene valor por sí mismo.

Personalización

La personalización aporta credibilidad, el mensaje está dirigido a nuestras necesidades, y demuestra un esfuerzo extra. Podemos personalizar en base a una acción del usuario, cualquier hecho cercano, o simplemente el nombre.

Sencilla, personalizada, oportuna.

A|B Testing

El A|B Testing consiste en comparar dos versiones de algo para ver cuál cumple mejor su objetivo. Hay varios modos de hacerlo, por ejemplo podemos enviar dos notificaciones distintas a parte de nuestra audiencia, medir que versión recibe más interacciones, y enviar la versión ganadora al resto del público.

Tener un objetivo claro para una funcionalidad es un pre-requisito para establecer unos indicadores de éxito, y entender si la funcionalidad implementada contribuye al éxito global del producto. Este es un aspecto clave del desarrollo de cualquier producto.

Throttle

Si hay varias notificaciones del mismo tipo, combinalas para no acaparar la pantalla.

Combina notificaciones del mismo tipo.

Opciones de implementación

Si envías mensajes a Android e iOS, tendrás que comunicarte con el servidor APNS de Apple, y el FCM connection server de Google. Para ello sería deseable

  • usar una API común para ambos sistemas,
  • con una implementación de terceros que nos abstraiga de cambios en estos servidores,
  • y coste bajo o cero.

Hay dos opciones

  • Un componente en nuestro servidor. Desventaja: habrá que actualizarlo periódicamente.
  • Un API a una plataforma de terceros. Desventaja: lo pagaremos en dinero o cediendo los datos anonimizados de nuestros usuarios.

Componentes en servidor

URL Multiplataforma Lenguaje Open Source Licencia
Pushy iOS Java MIT
Java-APNS iOS Java MIT-like
Aerogear iOS + Android Java y Node Apache

Plataformas

URL Multiplataforma SDK Precio
Firebase Cloud Messaging REST gratis o casi
OneSignal REST datos anonimizados
Urbanairship REST $99/0-10,000
Amazon SNS Varios $1/millón o menos

* Firebase Cloud Messaging es el nuevo nombre de Google Cloud Messaging for Android (GCM).

Hay muchas plataformas de notificaciones porque la funcionalidad básica puede montarse sobre componentes de código abierto. La competencia está en la diferenciación.

Diferenciación

Funcionalidades extra que ofrece Urbanairship:

  • Editor web de mensajes
  • A|B testing
  • Personalización de mensajes (parametros para usuarios)
  • Segmentación por localización geográfica o dispositivo
  • Estadísticas

Una de las plataformas ofrecía un editor de interfaces nativos, que se muestran en respuesta a notificaciones. Esto requería adjuntar un framework de la plataforma.

Conclusión

Amazon SNS me parece la mejor opción porque

  • Es una plataforma de terceros que nos da un interfaz común y nos aísla de los cambios en los servidores de push.
  • Es de pago y muy barato. Esto implica que no tendrémos que pagar cediendo datos como otras opciones “gratis”, lo cual tendría implicaciones legales.
  • Tiene soporte en varios lenguajes y plataformas.
  • Es una solución escalable.
  • No necesitamos funcionalidades extra.

Mecánica de envío de una notificación

En todos los casos hay tres actores:

  • Una aplicación cliente que recibe notificaciones.
  • Una aplicación servidor que envía notificaciones a través de un servidor de push.
  • Un servidor de push que remite al cliente las notificaciones enviadas por el servidor.
Y su relación sigue estos pasos:
  • El usuario otorga un permiso explícito al cliente.
  • El cliente envía a la aplicación servidor un identificador.
  • La aplicación servidor envía una notificación al servidor de push, que utiliza el identificador de cliente para localizarlo y enviarle una notificación.

iOS

El servidor push de Apple se llama APNS (Apple Notification Service).

Para que la aplicación servidor se comunique con el servidor APNS necesita el certificado SSL del desarrollador de la aplicación móvil, y un token único de cada instancia de la aplicación móvil. Esto le permitirá tener permiso para enviar, y saber a quien enviarle.

El funcionamiento general sigue estos pasos:

  1. La aplicación móvil arranca y recibe automáticamente un token del servidor APNS de Apple que envía las notificaciones.
  2. La aplicación móvil envía este token a la aplicación servidor mediante una llamada HTTP.
  3. La aplicación servidor envía una notificación al servidor APNS de Apple.
  4. El servidor APNS envía la notificación al dispositivo móvil.

El servidor de notificaciones de Apple no garantiza el orden de envío, y puede descartar notificaciones si las recibe demasiado rápido. Apple notifica fallos en el envío, pero no fallos por descarte.

Android

El servidor de notificaciones de Google se llama Firebase Cloud Messaging (FCM).

  1. La aplicación Android envía un identificador de desarrollador y un identificador de aplicación al servidor FCM.
  2. El servidor FCM devuelve al dispositivo un identificador de registro.
  3. La aplicación cliente envía el identificador de registro a la aplicación servidor.
  4. La aplicación servidor envía la notificación y el identificador de registro al servidor FCM.
  5. El servidor FCM envía la notificación al dispositivo.

Web

Mozilla y Chrome usan la Push API, que funciona así:

  1. El sitio web pide permiso al usuario y registra un service worker (un script que será ejecutado cuando llegue la notificación).
  2. Si el usuario otorga permiso el sitio web obtiene un token de dispositivo y lo envía a la aplicación servidor.
  3. La aplicación servidor hace un POST del token de dispositivo al servidor de push, que es propiedad del fabricante del navegador.
  4. El servidor de push envía la notificación.
  5. El navegador la recibe y ejecuta el service worker.

Safari usa el servidor APNS de Apple. Su funcionamiento similar pero más complejo. Hay que registrarse como desarrollador con Apple. Las notificaciones se envían como zips que contienen json y recursos gráficos. Hay más detalles en Notification Programming Guide for Websites.

Safari también permite notificaciones locales. Se invocan mediante javascript, funcionan solo mientras la página está activa, y usan el estandar Web Notifications.

Referencias

Viewing all 989 articles
Browse latest View live