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

El plan 8 horas descansar, 8 horas de ocio familia y 8 horas de trabajo: es un engaño

$
0
0

Hace poco escuchaba a un sindicalista decir que el objetivo de cualquier persona tendría que ser tener un equilibrio entre el descanso, el ocio/vida familiar y el trabajo: la teoría 888. En mi opinión esta teoría no ayuda a prosperar en la vida, o simplemente no se ajusta a la realidad; hay acontecimientos que hacen que no sea así.

Me explico:

  • Cuando somos bebés, todo es ocio/familia y descanso.
  • A medida que los niños crecen van dedicando más tiempo al trabajo (colegio o deberes).
  • Si la persona estudia en la Universidad, tendrá (aparte de ir 7 u 8 horas a clase) varias horas diarias de estudio
  • (que creo que es la perspectiva que les falta a muchos que empezaron a trabajar pronto y les es poco satisfactorio su trabajo).
  • Si tiene un trabajo monótono y que no le motiva es posible que esté deseando salir pitando del mismo y tener un ocio muy desarrollado. Además, con las juergas le quita tiempo al sueño. Y posiblemente, empalmando de un día a otro (que casi todos seguro que lo hemos hecho) le quitará tiempo al trabajo (o no rinde).
  • Si se es padre, cogerá una baja de 15 días (en España), por lo que trabajará menos y dormirá mucho menos aún.
  • Si reduce la jornada, tendrá más tiempo para el ocio y la familia.
  • Si hace un máster o prepara una oposición tendrá muchos meses malos de extra trabajo y deberá robar horas al sueño.
  • Si se lesiona haciendo deporte o en un accidente doméstico/trabajo, habrá que descansar más y dedicar más tiempo al ocio.

En este dibujo lo representamos en colores, donde siempre suman 24 horas:

Esta tendencia responde a los hábitos más comunes, nos parece bien siempre que restamos tiempo al trabajo pero no cuando se lo sumamos. Por desgracia, también es verdad que muchos suman horas extras y no remuneradas (como en consultoría) cosa de la que estoy completamente en contra.

Partiendo de que trabajamos 8 horas al día, tal vez podamos plantearnos hacer esfuerzos periódicos (pongamos cada n-meses o años) para estar mejor preparados.

  • Parece lógico que cuando nos incorporamos a un nuevo trabajo, si hacemos un esfuerzo adicional, por ejemplo estudiando en casa sin que nadie nos lo pida, el que nos evalúa se llevará una buena primera impresión: no hay segunda oportunidad para primera impresión.
  • Si de vez en cuando asistimos a charlas de nuestro sector, iremos creando una red de contactos y nuestra cara irá sonando. Las mejores oportunidades laborales vienen de contactos. Si somos nosotros los que nos ofrecemos a darlas, mejor.
  • Si leemos libros de Psicología, gestión de grupos, Economía, finanzas o nuevos métodos relacionados con nuestro trabajo, nunca estaremos fuera de mercado y podremos optar a otros puestos u oportunidades.

Parece que la gente tiene la obligación de dedicar el tiempo fuera del trabajo a hacer cualquier cosas que no le ayuden en su trabajo (obviamente esto tiene sentido en profesiones intelectuales). Es como si fuéramos tontos por beneficiar a la empresa.

Alguien podría decir: eso lo dices porque tienes una empresa. Tener una empresa es algo circunstancial, esto lo he hecho antes y lo haré después.

No estoy diciendo que en tu tiempo libre des continuidad sólo a lo que haces en tu tiempo de trabajo (si te gusta). Sólo digo que no te veas obligado a no hacerlo.
También que siempre es buen momento para cambiar hábitos y desarrollar nuevas habilidades y conocimientos.

  • Si te gusta leer novelas, plantéate leer algún libro de otro tipo: educar a niños o adolescentes, cuidar plantas, actuar ante emergencias, sobre alimentación, hablar en público, ligar, etc.
  • Si te gusta escuchar música mientras descansas o haces ejercicio: intercala audio libros de efectividad personal, Psicología, etc
  • Siempre puedes aprender bien otro idioma, a manejar las redes sociales o a escribir a máquina.

Hay millones de cosas que hacer que pueden ser positivas a todos los niveles. Tal vez tenemos que fijarnos en los nerds o geeks. Posiblemente esos que éramos más raritos y sosos de pequeños…

Aunque esta gráfica es una broma, me siento muy identificado :-)
La diferencia entre la gente que se apasiona por una disciplina del conocimiento y vive de ella y el resto, es que a los entusiastas no les da ninguna pereza continuar con su aprendizaje, les provoca satisfacción y beneficio que gran parte de la población no puede entender.

Fuente: http://www.smbc-comics.com/index.php?db=comics&id=2436

Los desequilibrios en la vida pueden crear mejores equilibrios.

¿Por qué tenemos que utilizar el día como unidad de medida? ¿por qué no utilizar el año o la década?
¿Por qué decimos a los niños que se preparen para el futuro y cuando empezamos a trabajar dejamos de hacerlo nosotros?

Espero que esta reflexión os de a vosotros algo que pensar también, y os animo a compartir vuestras opiniones en los comentarios.


Desplegando TNT Concept en Ubuntu Server 14.04 LTS: compatibilizando versiones

$
0
0

TNT Concept es una herramienta de gestión administrativa y de seguimiento de trabajo diario desarrollada en Autentia. Vamos a ver cómo instalarla teniendo en cuenta que está desarrollada en una versión antigua de Java.

Desplegando TNT Concept en Ubuntu Server 14.04 LTS: compatibilizando versiones

La motivación para realizar TNT Concept proviene de las siguientes necesidades, las cuales son recurrentes en la mayoría de las empresas:

  • Crear el esquema funcional de la empresa permitiendo mantener un repositorio con la ficha de cada empleado, los departamentos, las categorías y los convenios laborales.
  • Gestionar la contabilidad de la empresa, permitiendo llevar un control de las facturas, pagarés, cuentas y asientos.
  • Gestionar los proyectos de la empresa, permitiendo dar de alta los nuevos y mantener la relación de colaboradores, interacciones y ofertas.
  • Reflejar el trabajo de los empleados mediante una bitácora. Cada empleado puede imputarse las actividades realizadas cada día y diferenciar si las horas son facturables o no, en qué franja de tiempo, para qué proyecto se han realizado y bajo qué rol.
  • Gestionar la disponibilidad de los empleados en base a los proyectos a los que están asignados y el tiempo que les ocupa.
  • Generar informes de actividad, de facturación, de proyectos, de interacciones, de ofertas, de pedidos y de personal.
  • Gestionar las vacaciones de los empleados, permitiendo que éstos hagan solicitudes y facilitando a recursos humanos la concesión.

0. Índice de contenidos

1. Instalación del entorno de despliegue

TNT Concept está disponible para ser descargado desde el perfil de usuario de Autentia en Github. La release actual de TNT Concept es la 0.25.

TNT Concept fue desarrollado para correr bajo:

  • Java Development Kit, versión 1.6
  • Tomcat 6.0.X o JBoss 4.2.3 JDK6
  • MySQL 5.0 o superior (recomendando emplear 5.1.X)
  • GUI Tools para MySQL

Como se puede apreciar, las versiones son antiguas. El objetivo de este tutorial es instalar sobre un sistema operativo actual, como Ubuntu 14.04 LTS, las versiones recomendadas de lo que consideremos crítico, como el JDK 1.6, y las versiones actualizadas de lo que consideremos menos crítico, que exista una compatibilidad probada o que sea accesorio.

En este tutorial por tanto vamos configurar el siguiente entorno de despliegue para TNT Concept:

  • Oracle Virtual Box 4.3.28
  • Ubuntu Server 14.04 LTS
  • Java SE 6 (JDK 6u45)
  • Apache Tomcat 6.0.44
  • Oracle MySQL 5.5.43
  • phpMyAdmin 4.0.10deb1

Como apuntes, decir que:

  • Es posible levantar TNT Concept en una máquina con Java 8, MySQL 5.1.X y Tomcat 6.
  • Para compilar TNT Concept desde el código fuente, es necesario Maven 2.2 y que la versión del JDK sea menor que 8.

1.1 Instalación de Ubuntu Server 14.04 LTS en Virtual Box

Nuestro compañero Rodrigo de Blas nos ha explicado en un tutorial anterior cómo instalar Ubuntu Server 14.04 LTS en Virtual Box. En lugar de utilizar la imagen que él ha utilizado, podemos descargarnos una imagen limpia desde el sitio oficial de Ubuntu. El resto de pasos a seguir son los mismos que describe en su tutorial.

1.2 Instalación del JDK 6u45

Como ya es conocido, aunque a algunos les puede pillar por sorpresa, Canonical no incluye Java Oracle en Ubuntu desde la distribución 11.10. Desde que apareció Java 7, no se actualizaron más versiones de Java 6 y poco a poco han ido desapareciendo del gestor de paquetes (apt-get), por lo que la instalación de Java en Ubuntu se nos puede complicar.

A continuación se describen los pasos para instalar Java 6 en Ubuntu 14.04.

  • 1) Descargar el JDK
    • Crear una cuenta de Oracle
    • Acceder desde la máquina anfitrión a la página de descargas de Java SE 6. Aceptar el contrato de licencia y seleccionar jdk-6u45-linux-i586.bin.
    • Transferir, por ejemplo por ssh, de la máquina anfitrión a la máquina virtual el binario a la home.
  • 2) Instalar el JDK
    • Para lograr instalar el JDK y configurar la instalación adecuadamente, se ha hecho una síntesis aunando diferentes propuestas aportadas por la comunidad las cuales se complementan.

      Para instalar:

    • Otorgar al instalador permiso de ejecución
sudo chmod a+x [version]-linux-i586.bin
  • Ejecutar el instalador
  • sudo ./[version]-linux-i586.bin

    Tras la instalación, se genera una carpeta denominada jdk1.6.0.45, la cual contiene los ficheros y carpetas del JDK instalado. Si se desea, renombrar la carpeta jdk1.6.0.45 a java-6-oracle o a otro nombre que sea significativo.

  • 3) Configurar la instalación
    • Mover la carpeta jdk1.6.0.45 a /usr/lib/jvm

      sudo mv jdk1.6.0.45 /usr/lib/jvm
    • Añadir la nueva versión de java, javac y javaws como links simbólicos de sistema (system alternative) y otorgarles prioridad 1

      sudo update-alternatives --install "/usr/bin/java" "java" "/usr/lib/jvm/jdk1.6.0.45/bin/java" 1
      sudo update-alternatives --install "/usr/bin/javac" "javac" "/usr/lib/jvm/jdk1.6.0.45/bin/javac" 1
      sudo update-alternatives --install "/usr/bin/javaws" "javaws" "/usr/lib/jvm/jdk1.6.0.45/bin/javaws" 1
    • Marcar las nuevas alternatives para que sean utilizadas
      sudo update-alternatives --config java
      sudo update-alternatives --config javac
      sudo update-alternatives --config javaws
    • Probar que se han creado y actualizado las alternatives para java y javac
      $ java -version

      java version "1.6.0_45"
      Java(TM) SE Runtime Environment (build 1.6.0_45-b06)
      Java HotSpot(TM) 64-Bit Server VM (build 20.45-b01, mixed mode)
      $ javac -version
      javac 1.6.0_45
    • Editar el bash profile para añadir, al final del fichero, las variables de entorno.

      sudo vim /etc/profile
      # Java environment variables
      JAVA_HOME=/usr/lib/jvm/jdk1.6.0.45
      JAVA_BIN=$JAVA_HOME/bin
      PATH=$PATH:$JAVA_HOME:$JAVA_BIN
      export JAVA_HOME
      export JAVA_BIN
      export PATH
    • Comprobar que las variables de entorno se han exportado correctamente
      echo $JAVA_HOME
      /usr/lib/jvm/jdk1.6.0.45
      echo $JAVA_BIN
      /usr/lib/jvm/jdk1.6.0.45/bin
    • Recargar las variables de entorno de alcance del sistema
      . /etc/profile

    1.3 Instalación de Tomcat 6.0.44

    Para instalar la última versión de Tomcat 6 podemos utilizar el gestor de paquetes de Ubuntu:

    sudo apt-get install tomcat6

    Como previamente hemos instalado y configurado Java, no es necesario hacer más configuraciones en Tomcat como por ejemplo indicarle la ruta de $JAVA_HOME. En tal caso, podemos indicar a Tomcat que use la máquina virtual de Java que convengamos, editando el fichero defaults de Tomcat /etc/default/tomcat6:

    sudo vim /etc/default/tomcat6
    JAVA_HOME=/usr/lib/jvm/jdk1.6.0.45

    1.4 Instalación de MySQL 5.5.43

    Para instalar MySQL en Ubuntu primero es necesario instalar el repositorio de paquetes de MySQL, MySQL APT. Después, haciendo uso del comando apt-get podremos instalar la versión 5.5 de MySQL.

    Descargar MySQL APT
    Instalar MySQL APT
    • Siguiendo los pasos de la guía de uso de MySQL APT, extraemos que hay que añadir el repositorio
      dpkg -i mysql-apt-config_0.35-1ubuntu14.04_all.deb
      y obtener la información de las últimas versiones de los paquetes del repositorio MySQL APT
      sudo apt-get update
    • Mientras se añade el repositorio, el instalador preguntará qué versión de MySQL server va a ser la que esté disponible así como otras utilidades.
    Instalar MySQL server 5.5
    • Tras tener disponibles los paquetes de MySQL mediante el repositorio MySQL APT, instalamos MySQL server
      sudo apt-get-install mysql-server-5.5

    1.5 Instalación de phpMyAdmin

    Para facilitar la administración de la base de datos si se desea, se puede instalar una herramienta que nos facilite las tareas las tareas relacionadas. En este caso se ha decidido utilizar phpMyAdmin. Para instalar phpMyAdmin introducir el comando:

    sudo apt-get install phpmyadmin

    La versión en el momento de escribir este tutorial que por defecto instala el gestor de paquetes es la 4.0.10deb1. Para acceder a la herramienta, introducir en el navegador

    {URL_MV}/phpmyadmin

    2. Configuración del entorno de despliegue

    2.1 Base de datos

    Configurar instalación de MySQL

    En el caso de TNT Concept, antes de hacer la importación de la base de datos, es importante modificar el fichero de configuración de MySQL para indicar que en su creación, las tablas de la base de datos sean nombradas en minúscula. De no hacerlo, con la release actual de TNT Concept va a fallar la migración que posteriormente veremos.

    • Abrir el fichero de configuración de MySQL
      sudo vim /etc/mysql/my.cnf
    • Bajo la sección mysqld añadir
      lower_case_table_names=1
    Crear base de datos

    Desde phpMyAdmin crear la base de datos tntconcept. Para ello, desde la pantalla principal pulsamos en la pestaña Bases de datos. En el apartado Crear base de datos damos el nombre a la base de datos y para finalizar pinchamos en el botón Crear.

    1_crear_bbdd

    Crear usuario de administración para la base de datos

    Desde phpMyAdmin hay que proceder a crear un usuario para la aplicación tnt. Es importante indicar en el campo servidor localhost, de esta manera al usuario le estará permitido acceder únicamente desde el servidor a la base de datos. Para ello, desde la pantalla inicial pulsamos en la pestaña Usuarios y pinchamos en el link Agregar usuario. Procedemos entonces a crear el usuario tnt:

    2_crear_usuario

    Para finalizar de crear el usuario, pulsamos en el botón Continuar.

    Una vez creado el usuario, hay que configurar los privilegios para que tenga permisos sobre la base de datos tntconcept que se acaba de crear. Para ello, desde la pestaña Usuario, pinchamos en el link Editar los privilegios.

    En la sección Privilegios específicos para la base de datos, marcar en el desplegable la base de datos tntconcept:

    3_desplegable_privilegios

    Aparece automáticamente una vista para asignar privilegios al usuario tnt sobre la base de datos tntconcept. Marcamos todos los privilegios y pulsamos en ello botón Continuar de la sección Privilegios específicos para la base de datos:

    4_privilegios_tnt

    Importar estructura y datos

    En el installer de la distribución, bajo el directorio sql/mysql se encuentra el fichero createTables.sql. Es un script para la creación de la estructura de la base de datos tntconcept y la inserción de datos.

    Para importar el script, pulsar en la base de datos tntconcept y en la pestaña SQL, copiar el contenido del script de importación. Para efectuar la importación, pulsar en el botón Continuar:

    5_importar_script

    2.2 Servidor Tomcat

    A la instalación del servidor Tomcat hay que añadirle el conector de Java con la base de datos y especificar el contexto de la aplicación.

    Añadir el conector

    El conector a utilizar es Connector/J. Con la configuración original del entorno se utilizaba la versión 5.1.X. Comprobamos por tanto si la versión de Connector/J, 5.1.35 en este caso, es compatible con Java 1.6 y MySQL 5.5.43.

    Una vez descargado Connector/J, poner el conector en el la carpeta /usr/share/tomcat6/lib.

    Finalmente, podría ser necesario añadir el CLASSPATH.

    Especificación del contexto

    Para que el servidor Tomcat sepa cómo acceder a la base de datos y al directorio de configuración de la aplicación, hay que configurar el contexto de la aplicación. Hay que modificar para ello el fichero /etc/tomcat6/context.xml añadiendo justo antes de la etiqueta :

    
    	
    	 
    						
    				
    

    Siendo los campos marcados en rojo:

    $CONFIG: directorio de configuración. Recomendado utilizar /etc/tntconcept. Si no existe, crearlo.
    $DBUSER: usuario de la base de datos tntconcept
    $DBPASSWD: contraseña del usuario de la base de datos

    3. Despliegue

    Antes de iniciar el servidor Tomcat, hay que configurar los ficheros de propiedades de la aplicación, crear las carpetas de informes y de logs y añadir el war de la aplicación en el servidor Tomcat.

    3.1 Propiedades de la aplicación

    En el installer comprimido de la distribución, bajo el directorio config se encuentran los ficheros de propiedades de la aplicación. Hay que copiarlos a la carpeta que se haya definido como $CONFIG en la especificación del contexto de la aplicación. Las propiedades que se recomienda cambiar en el manual son las contenidas en los ficheros:

    log4j.properties
    • En la sección “# Default file appender”, descomentar la propiedad log4j.appender.file.File correspondiente a la plataforma Unix para especificar el directorio donde se quieran volcar las trazas de la aplicación. Estas trazas se generan durante la ejecución de la aplicación y pueden servir para depurar errores u obtener información de lo que está pasando.
    • En la sección “# Migration log appender”, descomentar la propiedad log4j.appender.migration.File correspondiente a la plataforma Unix para especificar el directorio donde se quieran volcar las trazas de migración de la base de datos de la aplicación. Estas trazas sólo se generan cuando se actualiza la aplicación de una versión a otra.
    • En la sección “# Security log appender”, descomentar la propiedad log4j.appender.security.File correspondiente a la plataforma Unix para especificar el directorio donde se quieran volcar las trazas relacionadas con seguridad.
    autentia.properties
    • En la sección “# Uploaded files path”, descomentar la propiedad pathFicheros correspondiente a la plataforma Unix para especificar el directorio donde se quieran almacenar los ficheros anexos de la aplicación. Los ficheros anexos son documentos binarios (ficheros Word, imágenes, PDFs, …) que se pueden subir al servidor asociados a ciertos tipos de registro (por ejemplo, los CVs de los usuarios, los documentos de calidad, etc.)
    • En la sección “# Personal report path”, descomentar la propiedad pathReports correspondiente a la plataforma Unix para especificar el directorio donde se quieran almacenar los ficheros de reportes personales. La aplicación permite emplear reportes propios del usuario (aparte de los ya definidos), que deben crearse con JasperReports y cargarse en este directorio.
    • reportLogoName. Esta propiedad contiene el nombre de la imagen que se usa para los informes. Esta imagen se busca con el nombre indicado aquí y en la ruta de configuración ($CONFIG).
    • isUsingExternalCss. Esta propiedad indica si los ficheros de estilos css se buscan dentro de el directorio $CONFIG (valor true) o se usan los que se incluyen en el .war de la aplicación.
    • documentRoot. Nombre de la carpeta dentro de $CONFIG donde se buscan los ficheros de estilos (.css). Sólo tiene sentido si isUsingExternalCss vale true.

    3.2 Carpetas de informes y logs de la aplicación

    La aplicación TNT Concept genera informes y permite la subida de documentos. Hay que crear la carpeta

    /var/lib/tntconcept/upload

    y la carpeta

    /var/lib/tntconcept/reports

    Los logs de la aplicación se generan en la carpeta

    /var/log/tntconcept

    que también hay que crear.

    Tras crear las carpetas, hay que dar permisos sobre ellas al usuario con el que se ejecuta tomcat

    px aux | grep catalina

    en este caso, tomcat6.

    Para dar permisos al usuario que ejecuta tomcat para esas carpetas, ejecutar el comando

    sudo chown -R tomcat6:tomcat6 /var/lib/tntconcept
    sudo chown -R tomcat6:tomcat6 /var/log/tntconcept

    También es recomendable dar permisos a la carpeta $CONFIG

    sudo chown -R tomcat6:tomcat6 /etc/tntconcept

    3.3 Añadir el war

    En el installer comprimido de la distribución, bajo el directorio app se encuentra el fichero tntconcept.war. Hay que colocarlo en el directorio de la máquina virtual /var/lib/tomcat6/webapps/.

    Para terminar el despliegue, sólo queda iniciar el servidor Tomcat

    sudo service tomcat6 start

    o

    sudo /etc/init.d/tomcat6 start

    Los logs del servidor pueden verse en /var/log/tomcat6.

    4. Primer acceso: migración

    Para acceder a la aplicación TNT Concept, basta con teclear la ruta en el navegador

    {URL_MV}:8080/tntconcept/root.jsf

    El usuario por defecto es admin y la clave por defecto es adminadmin. A los nuevos usuarios la contraseña que se les asigna por defecto es password. En el primer acceso la aplicación está en modo consola, lo que significa que tiene que actualizar la base de datos. Pulsando sobre el botón Migrar base de datos. Cuando la migración se complete, reiniciar el navegador y volver a entrar en la aplicación. En ese momento estará lista para poder utilizarse.

    6_bitacora

    5. Conclusiones

    En este tutorial no sólo hemos aprendido a instalar una herramienta de gestión, sino que hemos aprendido cómo configurar un entorno que sea compatible para desplegar una aplicación que se desarrolló con versiones anteriores de la tecnología actual.

    Se han obtenido además los conocimientos básicos para configurar Java, MySQL y Tomcat en este caso sobre un sistema Ubuntu.

    Esto es Esparta. Creación de grupos de alto rendimiento

    $
    0
    0

    El pasado diciembre Roberto Canales, CEO de Autentia, ofreció un webinar titulado ‘Esto es Esparta. Creación de grupos de alto rendimiento‘, en la sede madrileña de Project Management Institute. En él explicó cómo ha creado un grupo de menos de cuarenta personas con una alta disciplina colectiva en Autentia, donde algunos cuentan con capacidad de dirigir proyectos y transformar organizaciones.

    El webinar impartido por Roberto sostiene su discurso en base a dos pautas, en primer lugar el ponente opina que se ha de “obligar” a las personas a que aprendan en una organización para que la misma se desarrolle. El segundo punto de la argumentación apoya que la dirección de proyectos en exclusiva es demasiado cómoda.

    Quizá por eso Roberto afirma que las categorías tradicionales han dejado de tener significado: programador, analista programador, analista, jefe de proyecto, director de proyectos, en favor de los conceptos ligados con la artesanía: aprendices, oficiales y maestros.

    Tienes la ficha informativa sobre el webinar que ofreció Roberto Canales aquí.

    Maquetación

    $
    0
    0

    En este tutorial trataremos de asentar las bases para hacer una buena maquetación independientemente del formato en el que trabajemos.

    Índice

    Introducción

    Un cartel, una página web, un libro, un flyer, una revista, una presentación, una landing page o cualquier otro formato que se te ocurra, lleva un trabajo detrás de diseño y maquetación.

    Cartel cine expresionista
    Javier Gemar
    flickr.com
    Revista “Tacheles”
    Myriam Martin
    flickr.com
    Thinkr HTML landing page
    Tansh Creative
    flickr.com

    En este tutorial veremos qué es maquetar, las nociones básicas para hacer una buena maquetación y los errores más comunes.

    Maquetar es algo que todos podemos hacer. Sin embargo, para conseguir una alta calidad y que el producto final cumpla su función se necesitan una serie de conocimientos y práctica.

    Qué es maquetar

    Es la manera en que organizamos los elementos gráficos y/o multimedia para componer un diseño equilibrado, estético y funcional dentro de un determinado espacio.

    La maquetación presenta la información de forma atractiva, resalta lo importante, camufla lo irrelevante y llama la atención del público a través de la combinación de colores y el equilibrio entre textos e imagen/vídeo.

    Sistema de trabajo

    Antes de empezar a hacer algo hay que saber de qué se trata. Después, evaluar las herramientas con las que contamos para hacerlo y por último pensar y crear.

    Briefing

    Es la primera toma contacto con el proyecto. El briefing contiene las instrucciones del cliente (jefe, compañero, uno mismo) y generalmente incluye los distintos textos (titulares, párrafos, pies de imágenes…) y las fotografías, diagramas, mapas o ilustraciones a incluir en la maquetación. En muchas ocasiones el logotipo o los colores corporativos se especifican en este documento.

    Por otro lado, el briefing debe aportar datos sobre el estilo y la atmósfera que se quiere conseguir: juvenil, elegante, estructurada, innovadora…

    Estilo autoritario Estilo joven
    Estilo limpio y estructurado

    En esta fase es de gran ayuda hacer bocetos rápidos y sencillos que capten la idea general de lo que quiere el cliente.

    Principios básicos

    Equilibrio

    La colocación de los elementos no puede ser arbitraría. Podemos utilizar una organización simétrica, asimétrica o radial, pero siempre buscando estabilidad visual.


    Maquetación radial
    Maquetación simétrica
    Maquetación asimétrica

    Sencillez y claridad

    Hay que tener en cuenta los espacios y los márgenes ya que ayudan a centrar la atención en determinados puntos. No conviene saturar de contenido el diseño a no ser que se quiera crear un efecto determinado.

    010_coche

    Proporción y jerarquía

    Por norma general, los elementos más grandes son los más relevantes y desde luego los que más atrapan la atención del receptor. Además del tamaño, podemos utilizar titulares, colores o encuadres, por ejemplo, para resaltar contenido.

    011_Proporcion

    Armonía (unidad)

    Es la integración de todos elementos para generar una atmósfera, una determinada reacción en el público. Aquí es importante seguir una estética, elegir un código de colores o seleccionar tipografías adecuadas.

    012_colores

    Contraste

    Es una diferencia notable que generamos entre los elementos para llamar la atención del espectador.

    013_Contraste

    Ritmo

    La disposición de los elementos puede generar sensación de movimiento o indicar una dirección de lectura diferente al normal y con ello hacer un diseño más dinámico.

    014_lectura


    Uniendo los elementos: la retícula

    La retícula (grid) es la división mediante líneas y guías de un área en columnas, espacios y márgenes medidos con precisión para organizar y unificar el espacio a nivel compositivo. Una retícula bien hecha tiene que relacionarse de modo armónico con el formato y orientación del papel o soporte.

    015_reticula


    Errores comunes

    Generalmente, se trabaja a contrarreloj, esto suele tener consecuencias pero la más grave es la falta de tiempo para reflexionar sobre el diseño y por tanto, no generar la retícula. Es necesario dedicar algo del tiempo que tengamos a este proceso ya que da una sensación de calidad a primera vista y será más rápido organizar los elementos.

    Incluso utilizando retícula, es común olvidarse de dejar márgenes y espacios en blanco. No hay que olvidar que los ojos necesitan descansar, sobretodo en pantalla.

    Combinar más de 3 ó 4 tipografías en un mismo documento también es un error y si además no son legibles el resultado no es nada positivo. En general, tipografías “enrevesadas” se aplican a títulos concretos que suelen tener más tamaño y pocas palabras. En medios impresos es mejor utilizar tipografías “serif” ya que ayudan a su lectura. La pantalla ofrece más versatilidad.

    El uso de imágenes con poca resolución, es decir, pixelada es un grave error. Hoy en día las imágenes son el primer reclamo de atención, primero miramos y luego leemos.

    Uso del scroll horizontal en pantalla. Así como el vertical está totalmente estandarizado y es necesario, el scrool horizontal es incómodo para el usuario y poco práctico.

    Conclusiones

    Como se ha visto, maquetar no es poner imágenes y textos “al tun tun”. Requiere una fase de reflexión, organización y esquematización donde se armonizan todos los elementos intrínsecos al diseño para hacer llegar a nuestro público objetivo, un mensaje lo más óptimo posible.

    Hemos visto algunas normas generales que guían una maquetación pero no todos los diseños son iguales. Según el espacio en que trabajemos, encontraremos unas reglas concretas que veremos en próximos tutoriales.

    Crear anotaciones propias en Java

    $
    0
    0

    En este tutorial vamos a crear nuestras propias anotaciones en Java para usarlas en tiempo de ejecución.

    0. Índice de contenidos


    1. Introducción

    Las anotaciones en Java se incluyeron a partir de Java 5. Éstas son metadatos que se pueden asociar a clases, miembros, métodos o parámetros. Nos dan información sobre el elemento que tiene la anotación y permiten definir cómo queremos que sea tratado por el framework de ejecución. Además, las anotaciones no afectan directamente a la semántica del programa, pero sí a la forma en que los programas son tratados por las herramientas y librerías.

    Pero no sólo disponemos de las anotaciones por defecto de Java, si no que podemos crear las nuestras propias y así disponer de metadatos útiles en tiempo de ejecución.


    2. Entorno

    El tutorial está escrito usando el siguiente entorno:

    • Hardware: Portátil MacBook Pro 15′ (2.4 Ghz Intel Core I5, 8GB DDR3).
    • Sistema Operativo: Mac OS Yosemite 10.10.3
    • Entorno de desarrollo:
      • Eclipse Mars
      • Versión de Java: JDK 1.8

    3. Creando la anotación

    En Java, una anotación se define por medio de la palabra reservada @interface. Hay que tener ciertas consideraciones en cuenta a la hora de crearlas:

    • Cada método dentro de la anotación es un elemento.
    • Los métodos no deben tener parámetros o cláusulas throws.
    • Los tipos de retorno están restringidos a tipos primitivos, String, Class, enum, anotaciones, y arrrays de los tipos anteriores.
    • Los métodos pueden tener valores por defecto

    Vamos a crear una anotación que indique cuántas veces debe ejecutarse cada elemento que se anote con ella:

    MultipleInvocation.java
    package com.autentia.tutoriales.annotations.annotations;
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface MultipleInvocation {
    
        int timesToInvoke() default 1;
    }

    Como se ve en el código, hemos usado algunas meta-anotaciones para definir ciertos parámetors en nuestra anotación. Veamos cuáles de estas se pueden usar al crear nuestras anotaciones y qué significan:

    • @Target – Especifica el tipo de elemento al que se va a asociar la anotación.
      • ElementType.TYPE – se puede aplicar a cualquier elemento de la clase.
      • ElementType.FIELD – se puede aplicar a un miebro de la clase.
      • ElementType.METHOD – se puede aplicar a un método
      • ElementType.PARAMETER – se puede aplicar a parámetros de un método.
      • ElementType.CONSTRUCTOR – se puede aplicar a constructores
      • ElementType.LOCAL_VARIABLE – se puede aplicar a variables locales
      • ElementType.ANNOTATION_TYPE – indica que el tipo declarado en sí es un tipo de anotación.
    • @Retention – Especifica el nivel de retención de la anotación (cuándo se puede acceder a ella).
      • RetentionPolicy.SOURCE — Retenida sólo a nivel de código; ignorada por el compilador.
      • RetentionPolicy.CLASS — Retenida en tiempo de compilación, pero ignorada en tiempo de ejcución.
      • RetentionPolicy.RUNTIME — Retenida en tiempo de ejecución, sólo se puede acceder a ella en este tiempo.
    • @Documented – Hará que la anotación se mencione en el javadoc.
    • @Inherited – Indica que la anotación será heredada automáticamente.

    En nuestro caso hemos dicho que sea en tiempo de ejecución y que se aplique a métodos. También le hemos añadido un valor por defecto con la palabra default


    4. Usando la anotación

    El funcionamiento de uso es muy sencillo: basta etiquetar con la anotación los métodos que queramos:

    MultipleInvocation.java
    package com.autentia.tutoriales.annotations.classes;
    
    import com.autentia.tutoriales.annotations.annotations.MultipleInvocation;
    
    public class AutomaticWeapon {
    
        private static final int BURST_FIRE_ROUNDS = 3;
    
        private static final int AUTO_FIRE_ROUNDS = 5;
    
        private int ammo;
    
        public AutomaticWeapon(int ammo) {
            this.ammo = ammo;
        }
    
        @MultipleInvocation
        public void singleFire() {
            ammo--;
            System.out.println("Single fire! Ammo left: " + ammo);
        }
    
        @MultipleInvocation(timesToInvoke = BURST_FIRE_ROUNDS)
        public void burstFire() {
            ammo--;
            System.out.println("Burst fire! Ammo left: " + ammo);
        }
    
        @MultipleInvocation(timesToInvoke = AUTO_FIRE_ROUNDS)
        public void autoFire() {
            ammo--;
            System.out.println("Auto fire! Ammo left: " + ammo);
        }
    }

    Para este ejemplo hemos creado una clase que modeliza de forma sencilla el funcionamiento de un arma automática con tres tipos de disparo:

    • Único: gasta una bala por invocación.
    • Ráfaga: gasta tres balas por invocación.
    • Automático: gasta cinco balas por invocación.

    Para modelizar los tipos de disparos nos valemos de la anotación que hemos creado, pasándole el número de disparos que debe hacer (recordemos que por defecto era 1).


    5. Leyendo la anotación durante la ejecución.

    La lectura de la anotación en tiempo de ejecución se realiza mediante reflexión, pero sólo si ésta tiene un nivel de retención RUNTIME.

    Vamos a acceder a la información de la anotación en tiempo de ejecución. Para ello, creamos una clase Operador que pruebe el arma automática: llamará a todos los métodos del arma y los ejecutará el número de veces que le diga la anotación (si esta existe).

    Operator.java
    package com.autentia.tutoriales.annotations.classes;
    
    import java.lang.reflect.InvocationTargetException;
    import java.lang.reflect.Method;
    
    import com.autentia.tutoriales.annotations.annotations.MultipleInvocation;
    
    public class Operator {
    
        public void operate(AutomaticWeapon weapon) {
            final String className = weapon.getClass().getName();
            try {
                final Method[] methods = Class.forName(className).getMethods();
                for (final Method method : methods) {
                    invokeMethod(method, weapon);
                }
            } catch (final Exception e) {
                System.err.println("Hubo un error:" + e.getMessage());
            }
        }
    
        private void invokeMethod(Method method, AutomaticWeapon weapon)
                throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
    
            final MultipleInvocation multipleInvocation = method.getAnnotation(MultipleInvocation.class);
    
            if (multipleInvocation != null) {
                final int timesToInvoke = multipleInvocation.timesToInvoke();
    
                for (int i = 0; i < timesToInvoke; i++) {
                    method.invoke(weapon, (Object[])null);
                }
            }
    
        }
    }

    Lo que hace esta clase es muy sencillo:

    1. Coge todos métodos de la clase que se le pasa (en nuestro caso, el arma).
    2. Para cada método:
      1. Mira el número de veces a invocar
      2. Invoca dicho número de veces con los argumentos necesarios (en este caso no hay)

    NOTA: Soy consciente de que podría hacerse un código más “eficiente” usando de otra forma el API de Reflection de Java. Se decidió escribirlo así para mostrar mejor la forma de proceder. Esto es un ejemplo sencillo que muestra cómo tomar y usar los metadatos de las anotaciones.


    6. Probando

    Vamos a probar nuestra anotación con un sencillo programa principal que cree un operador y le asigne un arma a probar:

    Operator.java
    package com.autentia.tutoriales.annotations.main;
    
    import com.autentia.tutoriales.annotations.classes.AutomaticWeapon;
    import com.autentia.tutoriales.annotations.classes.Operator;
    
    public class Main {
    
        public static void main(String[] args) {
            final AutomaticWeapon weapon = new AutomaticWeapon(30);
            final Operator operator = new Operator();
            operator.operate(weapon);
        }
    }

    Al ejecutarlo, la salida debería mostrar lo siguiente (salvo orden de métodos):

    Auto fire! Ammo left: 29
    Auto fire! Ammo left: 28
    Auto fire! Ammo left: 27
    Auto fire! Ammo left: 26
    Auto fire! Ammo left: 25
    Single fire! Ammo left: 24
    Burst fire! Ammo left: 23
    Burst fire! Ammo left: 22
    Burst fire! Ammo left: 21

    Vemos que efectivamente se ha ejecutado una vez el disparo único, tres veces el disparo de ráfagas y cinco el automático.


    7. Conclusiones

    Las anotaciones son un método muy útil de añadir u obetner información de los elementos que están anotados. Además nos permiten acceder a ellos en tiempo de ejecución, con lo que los metadatos se pueden llegar a usar como variables internas para modificar el comportamiento del flujo del programa (como hemos visto).

    En este tutorial sólo se ha explorado la forma de acceder en tiempo de ejecución, pero a mi parecer la forma más potente de usar estas anotaciones sería en tiempo de compilación, ya que se puede añadir código al programa para que se comporte de manera diferente, sin tener que modificar el código principal (se encargaría el compilador al crearlo). En futuros tutoriales puede que toque este tema, ya que sólo hemos arañado la superficie.

    El código fuente se puede encontrar en GitHub

    Espero que os haya sido útil. ¡Un saludo!

    Rodrigo de Blas


    8. Referencias

    Formularios en AngularJS: componentes y validación.

    $
    0
    0

    El objetivo de este tutorial es mostrar buenas prácticas en la gestión de formularios, componentes y validación de los mismos en AngularJS.

    0. Índice de contenidos.


    1. Introducción

    Qué framework MVC, o MVVM como prefiráis, escrito en el lenguaje que sea, no proporcionaría un mecanismo de fábrica para permitir al usuario introducir información a través de la interfaz visual mediante componentes de formulario y al programador que la diseña declarar constraints o validaciones para que esa información no llege al modelo, ni se invoque al evento del controlador correspondiente, si no pasan tales validaciones, ¿qué tipo de framework sería? 😉

    En este tutorial vamos diseñar nuestro propio fomulario en AngularJS siguiendo las recomendaciones del framework para entrar dentro del ciclo de vida de la validación y los estados de los componentes de un formulario.

    Obtendremos una mejor experiencia de usuario realizando las validaciones de formularios en cliente, pero no olvidemos que el cliente no deja de ser un “burdo” javascript que cualquiera con conocimientos puede manipular o deshabilitar, no deja de interpretarse por un navegador; con lo que los servicios de backEnd deben ser robustos y realizar una doble validación, si o bajo vuestra responsabilidad.

    2. Entorno.

    El tutorial está escrito usando el siguiente entorno:

    • Hardware: Portátil MacBook Pro 15′ (2.3 GHz Intel Core i7, 16GB DDR3).
    • Sistema Operativo: Mac OS Mavericks 10.9.4
    • AngularJS 1.4.3.
    • Plunker

    3. Componentes.

    Ya comentamos que AngularJS proporciona directivas que convierten los componentes nativos de HTML5 en controles propios de formulario.

    La directiva más importante aunque no lo parezca es la que demarca el contenido del formulario, un formulario no es un formulario sin un componente <form … > … </form>; otra cosa es que AngularJS por el binding y la “magia” del mismo traslade al modelo la información de un <input type=”text” con un ng-model aunque no esté demarcado por un <form pero sin esta etiqueta no podremos gestionar correctamente un formulario en AngularJS.

      <div ng-controller="UserController">
        <form name="userForm" novalidate>
          <label for="name">Nombre</label><br />
          <input name="name" type="text" ng-model="user.name" />
          <br />
          <label for="email">Email</label><br />
          <input name="email" type="email" ng-model="user.email" />
          <br />
          <label for="gender">Género</label><br />
          <input name="gender" type="radio" ng-model="user.gender" value="male" />Masculino
          <input name="gender" type="radio" ng-model="user.gender" value="female" />Femenino
          <br />
          <label for="age">Edad</label><br />
          <input name="age" type="number" ng-model="user.age" />
          
          <br /><br />
          <input type="button" ng-click="reset()" value="Limpiar" />
          <input type="submit" ng-click="update()" value="Guardar" />
        </form>
        <pre>user = {{user | json}}</pre>
      </div>
    
      <script>
        angular.module('formLabs', [])
          .controller('UserController', ['$scope', function($scope) {
            $scope.user = {};
    
            $scope.update = function() {
              console.log($scope.user);
            };
    
            $scope.reset = function() {
              $scope.user = {};
            };
    
            $scope.reset();
          }]);
      </script>
    </body>

    Lo primero que llama la atención es que un formulario no tiene acción, la vinculación entre la vista y el modelo la gestiona el framework y el envío de la información al servidor será responsabilidad de un servicio en el que delege el controlador; ahora solo estamos sacando una traza por consola.

    De lo visto es interesante también:

    • el atributo novalidate de HTML5 que indica que el contenido del formulario no tiene que ser validado por el navegador. Si incluimos componentes propios de HTML5 el propio navegador valida el contenido y quedaría inconsistente que existan componentes que los valide el navegador y otros el propio ciclo de vida de AngularJS.
    • que el mero uso de la directiva ng-model convierta un componente de HTML5 como en el caso del input
    • el filtro {{user | json}} que convierte a json un objeto para imprimirlo en el propio HTML y ver el contenido
    • si el ccontenido de un componente insertado por el usuario no pasa la validación, como puede ser en el formulario actual el correo electrónico incorrecto, el valor no se traslada al modelo hasta que sea correcto,
    • por defecto el ng-model vincula al modelo la información de la vista en cuanto se produce un cambio, pero ese comportamiento se puede modificar usando la directiva ngModelOptions que permite especificar un listado de eventos: ng-model-options=”{ updateOn: ‘blur’ }” solo traslada la información al modelo cuando pierde el foco. Se pueden especificar muchos eventos delimitados por espacios ng-model-options=”{ updateOn: ‘mousedown blur’ }”
    • proporcionar un nombre a los componentes de formulario siempre es una buena práctica, si el formulario se enviase por POST a la acción definida en el mismo, el nombre identifica el parámetro y, aquí con AngularSJS, el nombre tanto a nivel de fomulario como a nivel de componente expone el estado interno del controlador que los gestiona para poder acceder a las propiedades del mismo y conocer, por ejemplo, si un componente “ha sido tocado” o el formulario ha sido submitido.


    4. Validación.

    Como hemos comentado AngularJS proporciona directivas para todos los tipos de componentes de formulario de HTML5 (text, number, url, email, date, radio, checkbox) y con esa tipología proporciona una validación (required, pattern, minlength, maxlength, min, max) y también podemos crearnos nuestros propios validadores.

    A continuación vamos a modificar el ejemplo inicial para añadir validaciones de obligatoriedad y mínimo y máximo, estas solo se producirán cuando los componentes pierdan el foco.

      <div ng-controller="UserController">
        <form name="userForm" novalidate>
          <label for="name">Nombre</label><br />
          <input name="name" type="text" ng-model="user.name" ng-model-options="{ updateOn: 'blur' }" required />
          <br />
          <label for="email">Email</label><br />
          <input name="email" type="email" ng-model="user.email" ng-model-options="{ updateOn: 'blur' }" required />
          <br />
          <label for="gender">Género</label><br />
          <input name="gender" type="radio" ng-model="user.gender" value="male" />Masculino
          <input name="gender" type="radio" ng-model="user.gender" value="female" />Femenino
          <br />
          <label for="age">Edad</label><br />
          <input name="age" type="number" ng-model="user.age" ng-model-options="{ updateOn: 'blur' }" min="0" max="150" />
          
          <br /><br />
          <input type="button" ng-click="reset()" value="Limpiar" />
          <input type="submit" ng-click="update()" value="Guardar" />
        </form>
        <pre>user = {{user | json}}</pre>
      </div>
    
      <script>
        angular.module('formLabs', [])
          .controller('UserController', ['$scope', function($scope) {
            $scope.user = {};
    
            $scope.update = function() {
              console.log($scope.user);
            };
    
            $scope.reset = function() {
              $scope.user = {};
            };
    
            $scope.reset();
          }]);
      </script>
    </body>

    Nos vamos acercando a una buena gestión de formularios solo nos quedan un par de cuestiones.


    4.1. Estado del formulario y mensajes de validación.

    Como hemos comentado anteriormente el hecho de indicar el nombre del formulario y el componente nos permite acceder al estado interno del mismo y, con ello, poder comprobar si el formulario se ha submitido o el componente es inválido.

    Basándonos en esos conceptos y consultando el API (el formulario expone una instancia de FormController y los componentes exponen una instancia de NgModelController) podemos volver a modificar nuestro ejemplo para añadirle mensajes de error personalizados

    <body ng-app="formLabs">
      <div ng-controller="UserController">
        <form name="userForm" novalidate>
          <label for="name">Nombre</label><br />
          <input name="name" type="text" ng-model="user.name" ng-model-options="{ updateOn: 'blur' }" required />
          <span class="messages" ng-show="userForm.$submitted || userForm.name.$touched">
            <span ng-show="userForm.name.$error.required">El campo es obligatorio.</span>
          </span>
          <br />
          <label for="email">Email</label><br />
          <input name="email" type="email" ng-model="user.email" ng-model-options="{ updateOn: 'blur' }" required />
          <span class="messages" ng-show="userForm.$submitted || userForm.email.$touched">
            <span ng-show="userForm.email.$error.required">El campo es obligatorio.</span>
            <span ng-show="userForm.email.$error.email">Formato de email incorrecto.</span>
          </span>
          <br />
          <label for="gender">Género</label><br />
          <input name="gender" type="radio" ng-model="user.gender" value="male" />Masculino
          <input name="gender" type="radio" ng-model="user.gender" value="female" />Femenino
          <br />
          <label for="age">Edad</label><br />
          <input name="age" type="number" ng-model="user.age" ng-model-options="{ updateOn: 'blur' }" min="0" max="150" />
          <span class="messages" ng-show="userForm.$submitted || userForm.age.$touched">
            <span ng-show="userForm.age.$error.max">La edad no puede exceder de 150.</span>
          </span>
          
          <br /><br />
          <input type="reset" ng-click="reset(userForm)" value="Limpiar" />
          <input type="submit" ng-click="update()" value="Guardar" ng-disabled="userForm.$invalid" />
        </form>
        <pre>user = {{user | json}}</pre>
      </div>
    
      <script>
        angular.module('formLabs', [])
          .controller('UserController', ['$scope', function($scope) {
            $scope.user = {};
    
            $scope.update = function() {
              console.log($scope.user);
            };
    
            $scope.reset = function(form) {
              $scope.user = {};
              if (form) {
                form.$setPristine();
                form.$setUntouched();
              }
            };
    
            $scope.reset();
          }]);
      </script>
    </body>

    Ni que decir tiene que esos literales deberían pasar por un filtro de traducción para que estén internacionalizados.

    Adicionalmente a los mensajes de validación podemos comprobar que:

    • hemos añadido una condición de deshabilitación del botón de guardar si el contenido del formulario es inválido ng-disabled=”userForm.$invalid”
    • el evento de reset ahora inicializa el formulario para indicar que todos los componentes están limpios y no se han tocado.

    4.2. Aplicando estilos.

    No por verlo en último lugar es menos importante, puesto que “lo bien hecho, bien parece” y que menos que decorar un componente de formulario con un estilo de inválido cuando existe algún error de validación sobre el mismo.

    ngModel añade las siguientes clases a los componentes en función del estado interno del mismo, esas clases también son aplicables al formulario.

    • ng-valid: el valor de la propiedad del modelo es válido
    • ng-invalid: el valor de la propiedad del modelo es inválido
    • ng-valid-[key]: donde key es el tipo de validación, asi ng-valid-required e indica que la validación para esa constraint es correcta
    • ng-invalid-[key]: idem que el anterior pero con el sentido contrario
    • ng-pristine: aún no se ha interactuado con el componente, en cuanto adquiera el foco, pasa a ng-touched
    • ng-dirty: se ha interactuado con el componente y se ha incluido algún valor en el mismo
    • ng-touched: se ha interactuado con el componente y se ha incluido o no algún valor en el mismo
    • ng-untouched: aún no se ha interactuado con el componente
    • ng-pending: hay validaciones que por su tipología se pueden hacer asíncroncas, con ng-pending podríamos controlar la promesas de las mismas

    Con todo lo anterior y aplicando los siguientes estilos

    <style type="text/css">
    
      .messages {
        color: #FA787E;
      }
      
      form.ng-submitted input.ng-invalid{
        border-color: #FA787E;
      }
      
      form input.ng-invalid.ng-touched {
        border-color: #FA787E;
      }
      
    /*
      form input.ng-valid.ng-touched {
        border-color: #78FA89;
      }
    */  
    </style>

    podríamos disponer de una interfaz de formulario como la que se muestra en la captura:


    5. Código fuente.

    Que mejor que un plunker para que podáis trastear con el código fuente de este tutorial.


    6. Referencias.


    7. Conclusiones.

    Hemos visto como trabajar de manera ordenada con formularios en AngularJS, el desconocimiento del framework y el conocimiento de otros, tipo jQuery, o del propio lenguaje javascript nos pueden llevar a saltarnos el ciclo de vida de la gestión propia de los formularios de AngularJS y a tener en definitiva una aplicación con un mal mantenimiento.

    Nos quedaría ver cómo crear nuestros propios validadores asociados a directivas… stay tuned!

    Un saludo.

    Jose

    Cambio de vistas en tiempo de ejecución con AngularJS

    $
    0
    0

    En este tutorial Samuel Martín nos explica como cambiar programáticamente en tiempo de ejecución vistas parciales de aplicaciones web con AngularJS.

    AngularJS permite de forma sencilla construir una SinglePageApplication con un index y varias vistas que suelen cambiar en función de una ruta que se emula para permitir una “navegación”.

    Lo que no es tan intuitivo es cómo conseguir que una de estas vistas cambie a voluntad en función de eventos que ocurran sin que cambie la ruta de la página.

    Voy a mostraros una forma de conseguir esto. Pueden existir otras formas de lograrlo, pero al igual que las definiciones de un patrón de diseño, esta es una solución probada a un problema conocido.


    Nuestro objetivo es que la vista de detalle (la rosa) y su controlador asociado cambien en función de qué botón pulsemos.

    Nuestro index.html tiene inicialmente esta forma.


    index.html
    <div class="main" ng-controller="mainController as mainCtrl" ng-init="mainCtrl.init()">
    
         <div class="panel-cabecera"  ng-include="'html/app/header.html'" ng-controller="headerController as headerCtrl"></div>
    
          <div class="panel-izquierda"  ng-include="'html/app/flujo.html'" ng-controller="flujoController as flujoController"></div>
    
    <!-- aquí va el ng-include que debe ser dinámico para el detalle -->
     
    </div>

    Un controlador padre llamado mainController con la funcionalidad común entre los hijos, y una vista con controlador por cada parte de la pantalla.

    Para agregar la vista dinámica para el detalle de los procesos del flujo vamos a introducir el nombre del fichero html en una variable en el controlador principal mainController.

    <div class="panel-derecha" ng-include="mainController.tpl.contentUrl"></div>

    Quedando nuestro index.html de esta forma.


    index.html
    <div class="main" ng-controller="mainController as mainCtrl" ng-init="mainCtrl.init()">
    
         <div class="panel-cabecera"  ng-include="'html/app/header.html'" ng-controller="headerController as headerCtrl"></div>
    
          <div class="panel-izquierda"  ng-include="'html/app/flujo.html'" ng-controller="flujoController as flujoController"></div>
    
          <div  class="panel-derecha" ng-include="mainController.contentUrl"></div>
     
    </div>

    En nuestro controlador main tenemos declarada nuestra variable y una función para cambiarla


    mainController.js
    //mainController
    var vm = this;
    vm.contentUrl = 'html/app/compra.html';
    
    vm.changeStep = function (step) {
        vm.contentUrl = 'html/app/' + step + '.html';
    }

    Por si os resulta extraño el uso de vm, es una convención que se sigue, y su funcionamiento es análogo a usar $scope.

    Ahora dentro del html de la vista de la izquierda del control de flujo, introducimos llamadas al mainController para llamar a nuestra función en cada uno de los links mediante el uso de ng-click.

    ng-click="mainCtrl.changeStep('compra')
    
    ng-click="mainCtrl.changeStep('modoPago')
    
    ng-click="mainCtrl.changeStep('realizarPago')

    Y al clicar en cada boton, nuestra vista de detalle cambiará a la que necesitemos.

    Por último, necesitamos un controlador para cada vista específica, en mis pruebas estuve buscando un modo para introducir un controlador dinámico en el ng-include, pero al final he optado por usar el pensamiento lateral en busca de una solución más sencilla, y la forma más cómoda es introducir cada controlador dentro del primer elemento de la plantilla concreta de detalle, ya que este va a ser fijo.

    <div class="detalle-compra" ng-controller="detalleCompraController as detalleCtrl">
        …
    </div>

    Espero que os haya resultado útil y si alguna vez tenéis un problema similar, os ayude a tener algo con lo que empezar.


    Un saludo,

    Samuel Martín Gómez-Calcerrada

    REST y el versionado de servicios

    $
    0
    0

    En este tutorial intentaremos explicar cómo versionar correctamente un servicio en función de los cambios que se produzcan en su API, cuándo los cambios son compatibles hacia atrás y el proceso completo de versionado.

    0. Índice de contenidos.


    1. Introducción

    Ya tuvimos la ocasión de hablar en múltiples tutoriales sobre arquitecturas orientadas a servicios o arquitecturas basadas en REST. Existe un problema muy común en estos modelos de arquitectura que es el cambio del contrato de nuestros servicios, cuando un servicio, por una determinada circunstancia, cambia su API y afecta a los diferentes consumidores del mismo. ¿Sabemos gestionar correctamente dicho cambio de contrato?, ¿cómo actuamos ante esta situación?.

    En este tutorial intentaremos explicar cómo versionar correctamente un servicio en función de los cambios que se produzcan en su API, cuándo los cambios son compatibles hacia atrás y el proceso completo de versionado.


    2. Entorno.

    El tutorial está escrito usando el siguiente entorno:

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

    3. El problema.

    Supongamos que tenemos un caso como el que se representa en la siguiente figura.

    El problema

    Si nos fijamos, contamos con un servicio que es consumido por diferentes clientes. A priori, es un escenario ideal en cualquier arquitectura distribuida ya que nuestro servicio es consumido por diferentes clientes y, por tanto, está siendo reutilizado.

    Hasta aquí todo va perfecto pero, ¿qué sucedería si cambiase el API (contrato) de nuestro servicio?

    Cambio API

    La primera solución que se nos puede venir a la cabeza podría ser realizar una subida sincronizada a producción entre el servicio y todos sus consumidores. Sin embargo, esta acción puede ser demasiado arriesgada e incluso inviable en muchas ocasiones. Podríamos destacar los siguientes inconvenientes:

    • Necesidad de un plan de contingencia por si algo falla. Marcha atrás.
    • Es una subida a producción muy compleja.
    • Casi inviable cuando los clientes afectados son dispositivos móviles.
    • Si un punto falla, se revierte todo.
    • Es posible que uno o varios consumidores no puedan migrarse en ese momento.

    Y entonces, ¿existe alguna solución? Por supuesto, el versionado de servicios… :)


    4. ¿Por qué versionar servicios?.

    El versionado de servicios es una práctica por la cual, al producirse un cambio en el API de un servicio (no tiene por qué ser únicamente un API REST), se libera una nueva versión de ese servicio de manera que la versión nueva y la anterior conviven durante un periodo de tiempo.

    De esta manera, los clientes se irán migrando a la nueva versión del servicio de manera secuencial. Cuando todos los clientes estén consumiendo la última versión del servicio, se retira la anterior.

    Gráficamente podríamos representarlo de la siguiente forma:

    Antes de nada lo que haremos será asegurarnos de que versionamos de manera independiente el API de un servicio y su correspondiente servicio de backend (lógica funcional que encapsula).

    Versionamos API

    Cuando se produce un cambio en el API del servicio, lo siguiente que hacemos es liberar una nueva versión del servicio con su correspondiente nueva versión del API y lógica de backend. Una vez liberada la nueva versión, avisamos a los consumidores del servicio indicando que existe una nueva versión disponible.

    Liberamos nueva versión

    Una vez dado el aviso a los consumidores, éstos se van adaptando para consumir la nueva versión del servicio.

    Migración - Paso 1

    Poco a poco se van migrando la mayoría de los consumidores.

    Migración - Paso 2

    Y llega un punto en el que todos nuestros consumidores están consumiendo la última versión del servicio.

    Migración - Paso 3

    Una vez que ya tenemos a todos los consumidores utilizando la última versión del API del servicio, retiramos la versión antigua.

    Migración - Retirada

    ¿Y por qué todo esto? Pues muy sencillo, porque los servicios cambian, es inevitable. Cambia el contrato (API) y cambia la implementación. El objetivo de todo esto es minimizar el impacto del cambio del contrato.

    Para que los clientes puedan distinguir entre una versión u otra del servicio, se suelen utilizar principalmente dos técnicas (aunque sobre esto suele haber muchísimo debate):

    http://host:puerto/api/v1/recurso

    • Añadir un parámetro en la cabecera de la petición que indique la versión del API que queremos consumir.
    URL: http://host:puerto/api/recurso
    Method: GET
    Headers:
    	api-version: v1

    Por otro lado, la idea de que convivan dos instancias de un mismo servicio (dos war, ear, o lo que sea…) y no añadir la funcionalidad de las dos versiones en una misma instancia es, lógicamente, por temas de mantenibilidad del servicio.


    4.1. Los cambios retrocompatibles.

    No todos los cambios en el API tienen impacto sobre los consumidores de la misma (al menos, no deberían tenerlo). A estos cambios se les suele denominar cambios retrocompatibles. En caso de que nuestra API sufra cambios de este tipo no debería ser necesario liberar una nueva versión, bastaría con reemplazar la actual. Lo que si sería muy conveniente es avisar a nuestros consumidores con los nuevos cambios para que los tengan en cuenta.

    Este es un listado de cambios en un API que NO deberían afectar a los consumidores:

    • Añadir nuevas operaciones al servicio. Traducido a REST sería añadir nuevas acciones sobre un recurso (PUT, POST…)
    • Añadir parámetros de entrada opcionales a peticiones sobre recursos ya existentes. Ej: un nuevo parámetro de filtrado en un GET sobre una colección de recursos
    • Modificar parámetros de entrada de obligatorios a opcionales. Ej: al crear un recurso, una propiedad de dicho recurso que antes fuese obligatoria y que pase a opcional.
    • Añadir nuevas propiedades en la representación de un recurso que nos devuelve el servidor. Ej: ahora a un recurso Persona que anteriormente estuviese compuesto por DNI y nombre, le añadimos un nuevo campo edad.

    Este otro listado muestra cambios que SI deberían afectar a los consumidores:

    • Eliminar operaciones o acciones sobre un recurso. Ej: Ya no se aceptan peticiones POST sobre un recurso.
    • Añadir nuevos parámetros de entrada obligatorios. Ej: ahora para dar de alta un recurso hay que enviar en el cuerpo de la petición un nuevo campo requerido.
    • Modificar parámetros de entrada de opcional a obligatorio. Ej: ahora al crear un recurso Persona, el campo edad, que antes era opcional, ahora es obligatorio.
    • Modificar un parámetro en operaciones (verbos sobre recursos) ya existentes. También aplicable a la eliminación de parámetros. Ej: al consultar un recurso ya no se devuelve determinado campo. Otro ejemplo: un campo que antes era una cadena de caracteres, ahora es numérico.
    • Añadir nuevas respuestas en operaciones ya existentes. Ej: ahora la creación de un recurso puede devolver un código de respuesta 412.

    Estos listados son orientativos e identficados en base a mi experiencia pero pueden existir otro tipo de cambios así que tampoco hay que tomárselo al pie de la letra.


    5. El proceso.

    Haciendo un resumen de lo que hemos visto en este artículo, el proceso de versionado de servicios podría representarse gráficamente de la siguiente manera:

    El proceso

    • Si existen cambios en nuestro servicio que NO afectan al API, únicamente reemplazamos el servicio actual. Ejemplo: solucionamos un error.
    • Si los cambios afectan al API, evaluamos si son retrocompatibles.
      • En caso de cambios retrocompatibles: avisamos a los consumidores con los nuevos cambios y reemplazamos el servicio actual.
      • En caso de cambios NO retrocompatibles: avisamos a los consumidores y liberamos una nueva versión del servicio.
        • Cuando todos los clientes estén utilizando la nueva versión, retiramos la antigua.

    6. Referencias.


    7. Conclusiones.

    El versionado de servicios es una actividad crítica, no sólo en arquitecturas orientadas a servicios, sino siempre que trabajemos con servicios (REST, SOAP…) que sean consumidos por diferentes clientes. Su objetivo es minimizar el impacto del cambio del contrato habilitando una migración secuencial entre nuestros consumidores.

    El versionado de servicios es una solución relativamente sencilla de ejecutar y que nos solucionará bastantes problemas a la hora de gestionar el ciclo de vida de los servicios. Además es una labor imprescindible en la fase de diseño de nuestro gobierno de servicios, aunque este ya sería otro tema que da para varios tutoriales… :)

    Espero que este tutorial os haya sido de ayuda. Un saludo.

    Miguel Arlandy

    marlandy@autentia.com

    Twitter: @m_arlandy


    Promesas en Angular

    $
    0
    0

    En este tutorial vamos a aprender a trabajar asíncronamente en AngularJS con el patrón promise-deferred de la mano de Samuel Martín.

    Según vamos trabajando en aplicaciones mas complejas, se hace necesario el uso de funciónes asíncronas para que ejecuten procesos costosos en segundo plano sin penalizar la experiencia de usuario, como por ejemplo en el caso de llamadas a servidor.

    Para trabajar asíncronamente hacemos uso de los callbacks, que, según la definición de la Wikipedia, son piezas de código ejecutable que se pasan como argumentos a otro código. este último es el encargado de ejecutar este argumento cuando sea posible. En una llamada síncrona esta invocación es inmediata mientras que puede ser posterior en una llamada asíncrona.

    Para clarificar este tutorial voy a usar algunos ejemplos. De toda la documentación de la que me he servido para la creación de este tutorial me han parecido muy claros los ejemplos de Rodrigo Branas en su libro AngularJS Essentials, por lo que voy a utilizarlos para explicar las promesas en AngularJS.

    Una primera implementación de un servicio ficticio de búsqueda de coches llamado ‘carSearchService’ usa callbacks para devolver los resultados al controlador cuando la busqueda del coche ha finalizado.

    serviceCallback.js
    parking.factory('carSearchService', function ($timeout) {
        var _filter = function (cars, criteria, successCallback,
                                errorCallback) {
            $timeout(function () {
                var result = [];
                angular.forEach(cars, function (car) {
                    if (_matches(car, criteria)) {
                        result.push(car);
                    }
                });
                if (result.length > 0) {
                    successCallback(result);
                } else {
                    errorCallback("No se han encontrado resultados");
                }
            }, 1000);
        };
        var _matches = function (car, criteria) {
            return angular.toJson(car).indexOf(criteria) > 0;
        };
        return {
            filter: _filter
        }
    });

    Para llamar a esta funcionalidad del servicio, necesitamos pasarle ambos callbacks, lo haremos en el controlador.

    controladorCallback.js
    var criteria = 'ferrari';
    $scope.cars = ['ford','ferrari'];
    
    $scope.searchCarsByCriteria = function (criteria) {
        carSearchService.filter($scope.cars, criteria, function (result) {
            $scope.searchResult = result;
        }, function (message) {
            $scope.message = message;
        });
    };

    Como vemos, desde nuestro controlador le estamos pasando cuatro parámetros. Los datos donde buscar, el criterio de la búsqueda, y dos funciones anónimas. Estas funciones son los callbacks que se ejecutarán cuando se resuelva la promesa, ya sea porque se ha encontrado el coche o no.

    El problema principal con este tipo de implementación es que es complicado mantener un número creciente de callbacks que a mas inri pueden estar anidados. Este problema ha sido recurrente en multitud de proyectos, por lo que se han establecido unos patrones conocidos como deferred y promise que tenemos implementados en AngularJS. Gestionan estos problemas y cuentan con la ventaja añadida de tener una interfaz común, lo que hace mas sencillo entender código legacy, ya que simplemente conociendo el API puedes trabajar con ellos.

    Deferred

    Para crear una nueva promesa con AngularJS necesitamos inyectar el servicio $q y llamar a su método defer para crear una instancia del objeto deferred, que es como se llama al objeto promesa.

    De su API, las funciones mas importantes son resolve(resultado) para, como su propio nombre indica, resolver la promesa y reject(razon), para rechazar la promesa por el motivo que sea.

    Vamos a ver otra vez el servicio anterior, pero esta vez utilizando deferred:

    servicioDeferred.js
    parking.factory('carSearchService', function ($timeout, $q) {
        var _filter = function (cars, criteria) {
            var deferred = $q.defer();
            $timeout(function () {
                var result = [];
                angular.forEach(cars, function (car) {
                    if (_matches(car, criteria)) {
                        result.push(car);
                    }
                });
                if (result.length > 0) {
                    console.log('promesa resuelta')
                    deferred.resolve(result);
                } else {
                    console.log('promesa rechazada')
                    deferred.reject("No se han encontrado resultados");
                }
            }, 1000);
            return deferred.promise;
        };
        var _matches = function (car, criteria) {
            return angular.toJson(car).indexOf(criteria) > 0;
        };
        return {
            filter: _filter
        }
    });

    En este caso ya no es necesario pasarle como parámetros los callbacks.

    Las partes importantes de este fragmento de código son la asignación de $q.defer() a una variable con nuestra promesa y los métodos resolve y reject de nuestra promesa de los que hemos hablado antes.

    Promise

    Como estamos devolviendo un objeto deferred, en nuestro controlador vamos a poder utilizar los metodos then, catch y finally.

    • then(successCallback, errorCallback, notifyCallback) es invocado cuando la promesa es resuelta (.resolve).
    • catch(errorCallback) se invoca cuando una promesa ha fallado, es equivalente al comportamiento en caso de .then(null, errorCallback).
    • finally(callback) al igual que en otros lenguajes como Java, es un fragmento que se va a invocar independientemente de cual sea el resultado de la promesa.

    Vamos a ver como debe ser el controlador encargado de llamar al servicioDeferred:

    controladorDeferred.js
    var criteria = 'ferrari';
    $scope.cars = ['ford','ferrari'];
    
    scope.filterCars = function (criteria) {
        carSearchService.filter($scope.cars, criteria)
            .then(function (result) {
            	console.log('la promesa se ha resuelto');
                $scope.searchResults = result;
            })
            .catch(function (message) {
                console.log('la promesa se ha rechazado');
                $scope.message = message;
            });
    };

    Mediante el .then y el .catch controlamos la salida de la promesa y mientras esta se resuelve podemos ejecutar otro código importante para el usuario como el renderizado de la página u otras llamadas a servicios.


    Existe demasiada gente que programa simplemente gracias a fragmentos de código que va encontrando sin entender demasiado como funciona por dentro, esto puede tener consecuencias desastrosas si el código que están tocando es vital para alguna organización.

    Espero haber ayudado con mi granito de arena a que se entienda mejor el funcionamiento de las promesas, en este caso con el framework Angular de JavaScript.


    Un saludo,

    Samuel Martín Gómez-Calcerrada

    El paralelismo en Java y el framework Fork/Join

    $
    0
    0

    El paralelismo en Java y el framework Fork/Join

    En este tutorial veremos qué es el paralelismo y cómo podemos implantarlo en nuestras aplicaciones Java.

    0. Índice de contenidos


    1. Introducción

    A la hora de desarrollar un programa con un coste computacional elevado, programar de una forma óptima y eficiente no es suficiente. Los ordenadores que utilizamos actualmente, incluso muchos de los sistemas móviles, disponen de privilegiados recursos hardware que en muy pocas ocasiones son aprovechados como se merecen.

    Java proporciona soporte para la implementación y la ejecución de tareas en paralelo, pero por defecto las aplicaciones desarrolladas en este lenguaje serán ejecutadas de forma secuencial. En este tutorial aprenderemos alguna de las técnicas que el lenguaje pone a nuestra disposición para el desarrollo de aplicaciones con tareas concurrentes o en paralelo. También diferenciaremos dos conceptos distintos, paralelismo y concurrencia, basados ambos en la programación multitarea.

    Por último paralelizaremos una tarea sencilla, tomando como ejemplo para este tutorial el algoritmo de ordenación MergeSort. Tomaremos tiempos de ejecución antes y después de la paralelización, comparándolos y sacando finalmente algunas conclusiones.


    2. Entorno

    Este tutorial está escrito desde una máquina con las siguientes características:

    • Hardware: MacBook Pro 15' (2,5 GHz Intel Core i7, 8 GB DDR3)
    • Sistema Operativo: Mac OS Yosemite 10.10.2
    • Entorno de desarrollo: Eclipse Luna 4.4.0
    • JDK 1.8

    3. Qué es el paralelismo

    Paralelismo es la ejecución simultánea de dos o más tareas. Se considera una propiedad del hardware, ya que requiere recursos físicos para ejecutar cada tarea simultáneamente, y su objetivo se basa en realizar una tarea en el menor tiempo posible.

    El paralelismo acelera la ejecución de una tarea dividiéndola en computaciones independientes y ejecutándola sobre hardware capaz de realizar computaciones simultáneas, como por ejemplo un procesador con varios núcleos. Pero cuando ejecutamos una tarea paralelizada en múltiples ordenadores, en vez de en los múltiples cores de un solo ordenador, decimos que la computación paralela es distribuída. Por ejemplo, cada búsqueda en Google se ejecuta simultáneamente en cientos de ordenadores, cada uno de los cuales busca al mismo tiempo en un subconjunto del índice del web.

    Paralelismo NO es concurrencia

    El término paralelismo suele confundirse en numerosas ocasiones con el concepto de concurrencia. Ambos se basan en la programación multitarea, pero no son lo mismo.

    Mientras que el paralelismo es la ejecución simultánea de dos o más tareas, la concurrencia es la ejecución de dos o más tareas en periodos de tiempo solapados, pero no necesariamente simultáneos. La concurrencia, por tanto, es una propiedad del programa. Su computación debe dividirse en tareas independientes, cuya ejecución puede o no solaparse. Puede existir sin soporte hardware multitarea, por ejemplo, los sistemas operativos multitarea ejecutados en máquinas con un procesador de un solo núcleo asignan breves intervalos de tiempo a cada tarea, dando la ilusión de ejecución simultánea.

    Frente a la computación paralela, la programación concurrente enfatiza más la interacción entre tareas, centrándose en coordinar el acceso a recursos mutables compartidos mediante algoritmos que evitan la interferencia entre tareas y la inconsistencia de los recursos.


    4. El framework Fork/Join

    El framework Fork/Join está disponible en la versión 7 de Java. Fue diseñado para la ejecución de tareas que pueden dividirse en otras subtareas más pequeñas, ejecutándose estas en paralelo y combinando posteriormente sus resultados para obtener el resultado de la tarea única. Las subtareas deberán ser independientes unas de otras, y no contendrán estado.

    Este framework realiza la paralelización de tareas de forma recursiva, aplicando el principio Divide y Vencerás. Existirá un umbral bajo el cual una tarea será indivisible, definido por el tamaño de la misma. Una vez el tamaño de las subtareas llegue al umbral, el rendimiento de las mismas disminuirá en caso de seguir dividiéndolas.

    A continuación podemos ver el pseudocódigo:

    Result solve (Problem problem) {
        if problem.size < SEQUENTIAL_THRESHOLD
            return solveSequentially(problem);
        else {
            Result left, right;
            INVOKE-IN-PARALLEL {
                left = solve(extractLeftHalf(problem));
                right = solve(extractRightHalf(problem));
            }
            return combine(left, right);
        }
    }

    Fork/Join se encuentra en la librería java.util.concurrent. Esta librería contiene una serie de interfaces y clases con las que poder implantar concurrencia o paralelismo en nuestros desarrollos. Entre otros mecanismos, encontramos los semáforos, los tipos atómicos, las barreras o los cerrojos.

    La implementación de Fork/Join toma forma a través de las clases ForkJoinTask y ForkJoinPool. La clase ForkJoinPool invoca a una tarea de tipo ForkJoinTask pero, para poder invocar múltiples subtareas en paralelo de forma recursiva, invocará a una tarea de tipo RecursiveAction, que extiende de ForkJoinTask.

    La clase RecursiveAction contiene el método compute(), que será el encargado de ejecutar nuestra tarea paralelizable. Para paralelizar una tarea secuencial, por tanto, crearemos una clase que extienda de RecursiveAction, y sobrecargaremos el método compute() con dicha tarea. Además, existe una diferencia entre el código de la tarea secuencial y el código que contenga el método compute(); mientras la tarea secuencial consiste en un método que se llame a sí mismo de forma recursiva, el método compute() creará de forma recursiva nuevas instancias de su misma clase, correspondiendo cada nueva instancia a una subtarea distinta.

    En la siguiente sección veremos un ejemplo de paralelización, haciendo uso de este framework.


    5. Paralelizando tareas: MergeSort como ejemplo

    Un buen ejemplo de tarea divisible en subtareas a través de la estrategia Divide y Vencerás es la ordenación de una lista de números enteros, en concreto la ordenación mediante el algoritmo MergeSort. En este algoritmo la ordenación de una lista no depende del resto, lo cual es un requisito fundamental para poder paralelizar cualquier tarea mediante el framework Fork/Join.

    A continuación tenemos el pseudocódigo del algoritmo. Como podremos ver, se ordena cada sublista recursivamente aplicando el ordenamiento por mezcla (function mergesort), y posteriormente se mezclan las dos sublistas en una sola lista ordenada (function merge).

    function mergesort(a)
        var left as array = a[0] ... a[n/2]
        var right as array = a[n/2+1] ... a[n]
        left = mergesort(left)
        right = mergesort(right)
        return merge(left, right)
    end function
    
    function merge(left,right)
        var result as array
        while length(left) > 0 and length(right) > 0 
            if first(left) ≤ first(right)
                append first(left) to result
                left = rest(left)
            else
                append first(right) to result
                right = rest(right)
        if length(left) > 0 
            append rest(left) to result
        if length(right) > 0 
            append rest(right) to result
        return result
    end function

    Al paralelizar cualquier operación sobre una colección de elementos debemos tener en cuenta tanto el tamaño de la misma como el coste de la operación, ya que si la lista es demasiado pequeña o el coste computacional es bajo la paralelización perjudicará en el rendimiento, sobrepasando el tiempo de gestión de los recursos del sistema junto con la ejecución de la tarea en cada hilo al tiempo secuencial.

    En Java la comparación de dos números es bastante rápida, por lo que para obtener mejoras en la paralelización de MergeSort podemos aplicar el algoritmo en una lista considerablemente grande o añadir cierto coste computacional a la operación de la comparación. Como solución, vamos a añadir un pequeño retardo de 100 ms. a la operación de comparación.

    Vamos a ver un ejemplo de cómo se paralelizaría la implementación secuencial del algoritmo mediante el framework Fork/Join, enfrentando después los tiempos de ejecución en serie y en paralelo.


    5.1. De implementación serie a paralela

    Nuestra implementación del algoritmo MergeSort consta de tres partes:

    • El método mergesort(), que ordena cada sublista recursivamente aplicando el ordenamiento por mezcla.
    • El método merge(), que mezcla dos sublistas ordenadas en una lista, ordenada también.
    • El método sort(), que invoca una sola vez al método mergesort(), creando previamente un array auxiliar del mismo tamaño que la lista a ordenar.

    Esta implementación, cuya ejecución se realiza de manera secuencial, se muestra a continuación:

    public class MergeSort {
        public void sort(Integer[] a) {
            Integer[] helper = new Integer[a.length];
            mergesort(a, helper, 0, a.length - 1);
        }
    
        private void mergesort(Integer[] a, Integer[] helper, int lo, int hi) {
            if (lo < hi) {
                int mid = lo + (hi - lo) / 2;
                mergesort(a, helper, lo, mid);
                mergesort(a, helper, mid + 1, hi);
                merge(a, helper, lo, mid, hi);
            } else {
                return;
            }
        }
    
        private void merge(Integer[] a, Integer[] helper, int lo, int mid, int hi) {
            /* code */
        }
    }

    El código del método merge() será el mismo para la implementación en serie que para la implementación en paralelo, y su lógica se encuentra en el pseudocódigo del algoritmo. Es en este método donde podemos introducir el retardo de 100 ms.

    Fork/Join está pensado para la ejecución de tareas que pueden dividirse en otras subtareas más pequeñas. Por esta razón, la tarea que vamos a paralelizar es el método mergesort(). Vamos a crear una clase que extienda de RecursiveAction, llamándola por ejemplo MergeSortTask, y en cuyo método compute() se implemente el código de nuestro antiguo método mergesort(). Además, en vez de realizar dos llamadas recursivas al método mergesort(), se crearán dos nuevas instancias de la clase MergeSortTask de manera recursiva, invocándolas posteriormente.

    Para finalizar nuestra paralelización, en el método sort() declarado fuera de la clase MergeSortTask crearemos un objeto de la clase ForkJoinPool, el cual invocará una instancia de la clase MergeSortTask. Por defecto, ForkJoinPool se instancia con el número de procesadores libres en el sistema, pero podemos indicar en el constructor el nivel de paralelismo deseado. A continuación se muestra el código de MergeSort paralelizado.

    import java.util.concurrent.ForkJoinPool;
    import java.util.concurrent.RecursiveAction;
    
    public class MergeSortForkJoin {
        public void sort(Integer[] a) {
            Integer[] helper = new Integer[a.length];
            ForkJoinPool forkJoinPool = new ForkJoinPool();
            forkJoinPool.invoke(new MergeSortTask (a, helper, 0, a.length-1));
        }
    
        private class MergeSortTask extends RecursiveAction{
            private static final long serialVersionUID = -749935388568367268L;
            private final Integer[] a;
            private final Integer[] helper;
            private final int lo;
            private final int hi;
    
            public MergeSortTask(Integer[] a, Integer[] helper, int lo, int hi){
                this.a = a;
                this.helper = helper;
                this.lo = lo;
                this.hi = hi;
            }
    
            @Override
            protected void compute() {
                if (lo < hi) {
                    int mid = lo + (hi-lo)/2;
                    MergeSortTask left = new MergeSortTask (a, helper, lo, mid);
                    MergeSortTask right = new MergeSortTask (a, helper, mid+1, hi);
                    invokeAll(left, right);
                    merge(this.a, this.helper, this.lo, mid, this.hi);
                } else {
                    return;
                }
            }
    
            private void merge(Integer[] a, Integer[] helper, int lo, int mid, int hi) {
                /* code */
            }
        }
    }

    5.2. Pruebas y rendimiento

    Para comparar ambas implementaciones y verificar que el tiempo de ejecución de una tarea disminuye cuando ésta se ha paralelizado, hemos realizado distintas pruebas tomando en cada una de ellas tanto el tiempo de ejecución como el número de hilos activos. Estas pruebas han consistido en la ordenación de colecciones de distintos tamaños, siendo las colecciones una generación de números enteros aleatorios entre -100.000 y 100.000, y ordenándose cada colección siempre primero en secuencial y después en paralelo.

    Hemos clasificado las pruebas realizadas en dos grupos: colecciones grandes y colecciones pequeñas. El objetivo de nuestras pruebas será comprobar que la paralelización disminuye el tiempo de ejecución en la ordenación de colecciones grandes o en la resolución de problemas con alto coste computacional, pero que perjudica en la ordenación de listas pequeñas o en la resolución de problemas con bajo coste.

    Colecciones grandes

    Para las pruebas de colecciones grandes se han fijado cuatro tamaños para las colecciones a ordenar: 50, 100, 250 y 500 elementos. Para cada tamaño se han generado tres listas, ordenando cada una mediante MergeSort secuencial y posteriormente ordenando cada una a través de MergeSort paralelizado.

    Estos tamaños, en condiciones normales, pueden considerarse pequeños, pero añadiendo un retardo de 100 ms. a cada comparación de números enteros el coste computacional se eleva considerablemente. Tras cada ordenación, se ha tomado el tiempo de ejecución y se ha calculado el rendimiento del algoritmo mediante la aceleración y la eficiencia. Los resultados obtenidos han sido los siguientes:


    50 elementos:

    Lista 1 Lista 2 Lista 3
    Tiempo serie (ms) 29870 29859 29865
    Tiempo paralelo (ms) 11370 11388 11388
    Hilos activos 9 9 9
    Aceleración 2,6271 2,6219 2,6225
    Eficiencia (%) 29,19 29,13 29,14

    100 elementos:

    Lista 1 Lista 2 Lista 3
    Tiempo serie (ms) 70200 70184 70179
    Tiempo paralelo (ms) 24762 25076 24762
    Hilos activos 9 9 9
    Aceleración 2,8349 2,7988 2,8341
    Eficiencia (%) 31,49 31,09 31,49

    250 elementos:

    Lista 1 Lista 2 Lista 3
    Tiempo serie (ms) 208286 208268 208230
    Tiempo paralelo (ms) 68139 67881 67857
    Hilos activos 9 9 9
    Aceleración 3,0568 3,0681 3,0687
    Eficiencia (%) 33,96 34,09 34,10

    500 elementos:

    Lista 1 Lista 2 Lista 3
    Tiempo serie (ms) 468772 466874 465133
    Tiempo paralelo (ms) 147800 140998 138694
    Hilos activos 9 9 9
    Aceleración 3,1717 3,3112 3,3537
    Eficiencia (%) 35,24 36,79 37,26

    Efectivamente, podemos comprobar que el tiempo de ejecución disminuye al paralelizar el algoritmo. Y no solo eso, también observamos que cuanto mayor sea la lista, mayor es el rendimiento.

    Colecciones pequeñas

    Para este tipo de pruebas se han generado colecciones de los siguientes tamaños: 10, 5, 3 y 2 elementos. El número de pruebas ha sido el mismo, siguiéndose la misma dinámica que para las pruebas de colecciones grandes. Vamos a ver los resultados obtenidos en cuanto a tiempo y rendimiento:


    10 elementos:

    Lista 1 Lista 2 Lista 3
    Tiempo serie (ms) 3548 3548 3543
    Tiempo paralelo (ms) 2088 2089 2089
    Hilos activos 9 9 9
    Aceleración 1,6992 1,6984 1,6960

    5 elementos:

    Lista 1 Lista 2 Lista 3
    Tiempo serie (ms) 1253 1254 1251
    Tiempo paralelo (ms) 1043 1044 1045
    Hilos activos 8 5 8
    Aceleración 1,2013 1,2011 1,1971

    3 elementos:

    Lista 1 Lista 2 Lista 3
    Tiempo serie (ms) 512 517 518
    Tiempo paralelo (ms) 528 529 522
    Hilos activos 5 5 5
    Aceleración 0,9697 0,9773 0,9923

    2 elementos:

    Lista 1 Lista 2 Lista 3
    Tiempo serie (ms) 206 204 202
    Tiempo paralelo (ms) 214 216 212
    Hilos activos 3 3 3
    Aceleración 0,9626 0,9444 0,9528

    Existe cierta aceleración en el caso de las listas de tamaño 10 y 5, aunque si tenemos en cuenta el número de hilos activos la aceleración es insignificante. Para las colecciones de tamaños 3 y 2 el tiempo de ejecución paralelo pasa a ser mayor que el tiempo de ejecución secuencial, por lo que aquí descubrimos que a partir de cierto umbral la paralelización perjudica en el rendimiento.


    6. Referencias


    7. Conclusiones

    Como hemos podido comprobar, la paralelización de una tarea es una buena solución para reducir su tiempo de ejecución, siempre y cuando ésta sea divisible en subtareas independientes y nuestro sistema hardware nos lo permita. Gracias al framework Fork/Join podremos paralelizar de forma sencilla cualquier tarea, siempre y cuando se cumplan las condiciones necesarias, aunque no siempre mejoraremos el tiempo de ejecución serie. Aplicar el paralelismo sobre una tarea que trabaje con colecciones pequeñas o ejecute cualquier operación con un coste computacional mínimo puede perjudicar en el rendimiento, como hemos aprendido en este turorial.

    Espero que este tutorial os haya ayudado. Un saludo.

    Natalia

    nroales@autentia.com

    CDI: Conceptos avanzados

    $
    0
    0

    En este tutorial vamos a ver aspectos más avanzados que nos permiten trabajar con conceptos como Alternativas, Especialización, Productores, Eventos e Interceptores en CDI.

    0. Índice de contenidos.

    1. Introducción

    Como ya vimos en el anterior tutorial relacionado (Inyección de dependencias a través de CDI en Tomcat), CDI es una opción factible, y que gana peso con el paso del tiempo, para la definición de contextos y manejo de inyección de dependencias.

    En este tutorial vamos a ver aspectos más avanzados que nos permiten trabajar con conceptos como Alternativas, Especialización, Productores, Eventos e Interceptores.

    Para ayudarnos a retratar estos aspectos, vamos a realizar pequeñas aplicaciones web JSF en las que iremos utilizando las herramientas anteriormente detalladas.

    2. Entorno

    El tutorial está escrito usando el siguiente entorno:

    • Hardware: MacBook Pro 17′ (2.66 GHz Intel Core i7, 8GB DDR3 SDRAM).
    • Sistema Operativo: Mac OS X Lion 10.10.3.
    • NVIDIA GeForce GT 330M 512Mb.
    • Crucial MX100 SSD 512 Gb.
    • Software:
      • Eclipse Mars
      • Java JDK 1.8
      • Maven 3
      • Apache Tomcat 8
      • JBoss Weld 2.2.13

    3. Alternativas

    Mientras estamos desarrollando nuestra aplicación, puede darse el caso de que se tenga multiples implementaciones para un mismo bean, sobre todo si estamos acostumbrados a trabajar mediante diseño por contrato.

    Para dar solución a estas situaciones CDI nos ofrece las Alternativas. Una herramienta aplicable en el momento de despliegue, que nos permite seleccionar entre las distintas implementaciones de una misma interfaz

    Esta solución suele utilizarse en las siguientes situaciones:

    • Manejar lógica de negocio específica de cada cliente en tiempo de ejecución.
    • Seleccionar beans validos para escenarios concretos.
    • Creación de dobles de test.

    Para escenificar el uso de Alternativas vamos a utilizar como base el proyecto CDI del anterior tutorial (ENLACE).

    Aplicando las Alternativas.

    Comenzaremos añadiendo una nueva implementación de la interfaz “Greeting”, la cual utilizamos para implementar los servicios de la aplicación.

    package cdi.webapp.advanced;
    
    import javax.enterprise.inject.Alternative;
    
    @Alternative
    public class AlternativeGreetingServiceImpl implements Greeting {
    
    	public String getSalute() {
    		return "Hi Dude!!!";
    	}
    
    }

    Mediante la anotación @Alternative marcamos la implementación como alternativa dentro de implementaciones existentes, pero no es el único paso para hacer uso de alternativas, siendo necesario declarar esta clase en la sección del fichero beans.xml, como se puede ver a continuación.

    
      
      	cdi.webapp.advanced.AlternativeGreetingServiceImpl
      
    

    El resultado de nuestras modificaciones es que la alternativa de implementación prevalece sobre la opción “default”.

    Utilizando multiples Alternativas.

    Podemos querer hacer uso de las múltiples alternativas que tengamos definidas, para estos casos disponemos de varias posibilidades, en forma de anotaciones:

    • @Named
    • @Qualifier

    En este caso vamos a ampliar el supuesto anterior con otra implementación de la interfaz Greeting para tener un poco más de variedad en el ejemplo, para ello añadimos Alternative2GreetingServiceImpl, tanto al proyecto, como dentro del fichero beans.xml.

    @Named

    package cdi.webapp.advanced;
    
    import javax.enterprise.inject.Alternative;
    
    @Alternative
    public class Alternative2GreetingServiceImpl implements Greeting {
    
    	public String getSalute() {
    		return "Let's Rock!";
    	}
    }
    
          
          	cdi.webapp.advanced.AlternativeGreetingServiceImpl
          	cdi.webapp.advanced.Alternative2GreetingServiceImpl
          
    

    Llegados a este punto, se hace necesario identificar los distintos servicios que tenemos, en un primer momento realizaremos esta identificación a través de la anotación @Named. A continuación lo detallamos para cada implementación:

    @Named("GreetingServiceImpl")
    public class GreetingServiceImpl implements Greeting...
    @Named("AlternativeGreetingServiceImpl")
    public class AlternativeGreetingServiceImpl implements Greeting...
    @Named("Alternative2GreetingServiceImpl")
    public class Alternative2GreetingServiceImpl implements Greeting ...

    En la clase MessageServerBean habrá que establecer dichos servicios…

    @Named
    @RequestScoped
    public class MessageServerBean {
    
    	@Inject
    	@Named("GreetingServiceImpl")
    	private Greeting greetingService;
    	
    	@Inject
    	@Named("AlternativeGreetingServiceImpl")
    	private Greeting alternativeGreetingService;
    	
    	@Inject
    	@Named("Alternative2GreetingServiceImpl")
    	private Greeting alternative2GreetingService;
    	
    	
    	public String getMessage(){
    		return greetingService.getSalute();
    	}
    	
    	public String getMessageAlternative(){
    		return alternativeGreetingService.getSalute();
    	}
    	
    	public String getMessageAlternative2(){
    		return alternative2GreetingService.getSalute();
    	}
    }

    Finalmente modificaremos el fichero xhtml para solicitar los distintos mensajes…

    	
    		Facelet Title
    	
    	
    	    Hello from Facelets
    	    
    Message is: #{messageServerBean.message}
    Message Alternative is: #{messageServerBean.messageAlternative}
    Message Alternative 2 is: #{messageServerBean.messageAlternative2}
    Message Server Bean is: #{messageServerBean}

    El resultado debe ser similar al que se muestra en la imagen a continuación:

    @Qualifier

    Si queremos optar por identificar nuestros servicios a través de @Qualifier tendremos que crearlos, como se ve a continuación:

    @Qualifier
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.FIELD, ElementType.TYPE, ElementType.METHOD})
    public @interface GreetingService {
    }

    Sirva esta declaración a modo de ejemplo para realizar la definición del resto de @Qualifier (AlternativeGretingService y Alternative2GreetingService)

    Después de definirlos, habrá que marcar cada servicio con su correspondiente @Qualifier sustituyendo la anotación @Named por @, por ejemplo, se debe sustituir @Named(“GreetingServiceImpl”) por @GreetingService.

    Este cambio debe realizarse de igual manera en la clase MessageServerBean, quedando de la siguiente manera:

    @Named
    @RequestScoped
    public class MessageServerBean {
    
    	@Inject
    	@GreetingService
    	private Greeting greetingService;
    	
    	@Inject
    	@AlternativeGreetingService
    	private Greeting alternativeGreetingService;
    	
    	@Inject
    	@Alternative2GreetingService
    	private Greeting alternative2GreetingService;
    	
    	public String getMessage(){
    		return greetingService.getSalute();
    	}
    	
    	public String getMessageAlternative(){
    		return alternativeGreetingService.getSalute();
    	}
    	
    	public String getMessageAlternative2(){
    		return alternative2GreetingService.getSalute();
    	}
    }

    El resultado final debe ser similar al que mostramos en el punto anterior.

    4. Especialización

    La epecialización es una solución similar a las alternativas en lo que se refiere a sustituir un bean por otro. Sin embargo, lo realmente interesante de las especializaciones es que permite ampliar la funcionalidad de un bean específico sobreescribiendo sus métodos.

    La especialización se realiza tanto en tiempo de ejecución como de compilación. Cuando se declara que un bean especializa otro, extenderá la clase, y en tiempo de ejcución el bean especializado sustituye por completo al bean inicial. Si el bean inicial se genera mediante un método “Productor”, habrá que sobreescribir el método “Productor” parta que genere el bean especializado.

    Declarar un bean especializado es sencillo, en nuestro caso vamos a especializar el bean Alternative2GreetingService, extendiendo la implementación y sobreescribiendo el método getSalute():

    @Alternative
    @Specializes
    public class SpecializedGreetingService extends Alternative2GreetingServiceImpl{
    
    	@Override
    	public String getSalute() {
    		return "C'mon!!!! ".concat(super.getSalute());
    	}
    }

    Por otro lado, hace falta dejar constancia en el fichero beans.xml.

    
      
      	cdi.webapp.advanced.AlternativeGreetingServiceImpl
      	
      	cdi.webapp.advanced.specialization.SpecializedGreetingService
      
    

    Con estos cambios el resultado que debe obtenerse es el siguiente:

    5. Productores

    Un método productor es aquel que genera un objeto que puede ser inyectado, tiene que estar anotado con javax.enterprise.inject.Produces.

    En este caso vamos a volver al ejemplo base (Inyección de dependencias a través de CDI en Tomcat) para explicar como hacer uso de @Produces.

    Primero ampliaremos los servicios de generación de saludo (AlternativeGreetingServiceImpl y Alternative2GretingServiceImpl) mediante las clases ya expuestas en este tutorial.

    Con posterioridad, eliminaremos de estas (GreetingServiceImpl, AlternativeGreetingServiceImpl y Alternative2GreetingServiceImpl) las antoaciones que tienen.

    A continuación generaremos la clase que contiene al método productor:

    public class GreetingFactory implements Serializable {
    
    	private final GreetingType greetingType = GreetingType.ALTERNATIVE_GREATING;
    
    	@Produces
    	@Greeting
    	public IGreeting getGreeting(){
    		switch(greetingType){
    			case GREATING:
    				return new GreetingService();
    			case ALTERNATIVE_GREATING:
    				return new AlternativeGreetingService();
    			case ALTERNATIVE_2_GREATING:
    				return new Alternative2GreetingService();
    			default:
    				return new GreetingService();
    		}
    	}
    }

    Como se ve en el código anterior, el método productor de Beans se marca con @Produces.Hemos mantenido el Qualifier @Greeting para hacer más cómodo el desarrollo.

    Adicionalmente, hemos creado un enumerado que mantiene los tipos de servicios que mantenemos..

    public enum GreetingType {
    	GREATING, ALTERNATIVE_GREATING, ALTERNATIVE_2_GREATING
    }

    Finalmente, nos aseguramos de que el fichero beans.xml está vacío y de que el contenido del fichero index.xhtml es el correcto:

    
    
    
    
    
    
    	
    		Facelet Title
    	
    	
    	    Hello from Facelets
    	    
    Message is: #{messageServerBean.message}
    Message Server Bean is: #{messageServerBean}

    Por último, el resultado:

    6. Eventos

    Los eventos son el mecanismo que ofrece CDI para que los beans se comuniquen “bajo demanda”. Un bean puede definir un evento, otro puede “disparar” ese evento y un tercero puede ser notificado.

    Un evento consiste en:

    • Un objeto Java que define el evento
    • Los qualifiers de eventos

    Para este ejemplo vamos a hacer un proyecto nuevo por completo. Como ya hemos visto con anterioridad como configurar de base un proyecto CDI con tomcat, saltaremos a la parte de la definición de objetos.

    Primero definimos el evento

    public class HelloEvent {
    
    	private final String message;
    
    	public HelloEvent(final String message) {
    		super();
    		this.message = message;
    	}
    
    	public String getMessage() {
    		return message;
    	}
    
    }

    A continuación creamos un listener que esté observando por si el evento se dispara:

    @Stateless
    public class HelloListener {
    
    	public void listenToHello(@Observes final HelloEvent helloEvent){
    		System.out.println("HelloEvent: " + helloEvent.getMessage());
    	}
    }

    El Listener esta anotado con @Stateless que marca el bean como stateless, y por otro lado, vemos que el parámetro del método listenToHello esta anotado con @Observes, lo que quiere decir que se trata de un parametro del evento, y que hace que el método listenToHello sea declarado como método observer.

    En tercer lugar tenémos la clase HelloMessenger, encargada de disparar el evento, junto con su parámetro:

    @Named("messenger")
    @Stateless
    public class HelloMessenger {
    	
    	@Inject Event events;
    	
    	public void hello() {
    		events.fire(new HelloEvent("from bean " + System.currentTimeMillis()));
    	}
    }

    Finalmente definimos una página web (index.xhtml) donde estableceremos le código que hará la llamada al método hello(), encargado de disparar el evento.

    
    
    
    	
    		Facelet Title
    	
    	 
    
            
    
                
    
            
    	    
    	
    

    Al arrancar la aplicación se verá una página web con un botón (Fire).

    Si el botón es pulsado lanzará el evento, que será recogido por el Listener y se encargará de pintar el mensaje indicado por consola.

    7. Conclusiones

    Como hemos visto a lo largo del tutorial, CDI nos provee de las herramientas necesarias para enfrentarnos a casi cualquier proyeto en el que se necesite un contexto y que requiera de inyección de dependencias.

    Con respecto al futuro de CDI, pasa por su especificación 2.0, con la llegada de JEE 8, que se centrará en dos aspectos principalmente:

    • Soporte CDI para aplicaciones Java SE puras.
    • Modularidad. Definiendo un modelo CDI con mayor granularidad favoreciendo la integración de otras especificaciones Java EE

    8. Referencias

    Configuración de Nexus, Sonar y Jenkins tras un Apache

    $
    0
    0

    A la hora de montar un entorno de integración continua una opción habitual es querer tener las herramientas tras un Apache en vez de exponer los distintos puertos directamente. Sin embargo, dependiendo de las herramientas a montar su configuración no es del todo evidente. En este tutorial veremos cómo tener Jenkins, Nexus y Sonar en un servidor remoto por HTTPS con Apache.

    0. Índice de contenidos


    1. Introducción

    A la hora de montar un entorno de integración continua una opción habitual es querer tener las herramientas tras un Apache en vez de exponer los distintos puertos directamente. Sin embargo, dependiendo de las herramientas a montar su configuración no es del todo evidente. En este tutorial veremos cómo tener Jenkins, Nexus y Sonar en un servidor remoto por HTTPS con Apache.


    2. Entorno

    El tutorial está escrito usando el siguiente entorno:

    • Hardware: Portátil MacBook Pro Retina (2.93 Ghz Intel Core 2 Duo, 8GB DDR3).
    • Sistema Operativo: Mac OS Yosemite 10.10
    • Apache 2
    • Nexus-2.11.3-01-bundle
    • Jenkins 1.621
    • Sonar-5.1.1

    3. Punto de partida

    Partimos de un entorno de integración continua compuesto por las aplicaciones de jenkins, nexus y sonar ya instaladas con las siguientes características:

    • Jenkins está corriendo bajo el dominio example.com:8080.
    • Nexus está corriendo bajo el dominio example.com:8081.
    • Sonar está corriendo bajo el dominio example.com:9000.
    • Las tres aplicaciones están ejecutándose en la misma máquina.

    Dada esta configuración, queremos instalar un apache que actue de reverse proxy de forma que yo pueda redirigir las peticiones de mi entorno de integración de forma segura y sin exponer los puertos de actuación.

    Nuestro apache tiene que:

    • Redirigir las peticiones de http://example.com/sonar a localhost:9000 pasando por https si la petición no es segura.
    • Redirigir las peticiones de http://example.com/nexus a localhost:8081 pasando por https si la petición no es segura.
    • Redirigir las peticiones de http://example.com/jenkins a localhost:8080 pasando por https si la petición no es segura.
    • Securizar el acceso de proxy por autnticación de apache, para restringir el acceso.

    Para lograr esto, primero cambiaremos el context path de las aplicaciones de sonar, jenkins y nexus, y después configuraremos apache para que actue como proxy.

    Una vez comprobado qué tenemos y qué queremos, ¡vamos al lio! 😉


    4. Configuración de sonar

    Según el punto de partida establecido, ejecutamos sonar a partir de http://example.com:9000. Para configurar sonar detrás de apache, necesitamos que la url de ejecución de sonar sea como: http://example.com:9000/sonar.

    Para cambiar el context path de sonar, nos vamos a su sonar.properties, que en mi caso está en la carpeta conf dentro de la carpeta de instalación de sonar.

    En el fichero sonar.properties es dónde cambiamos la propiedad sonar.web.context.

    Reiniciamos el servicio de sonar, y comprobamos la navegación en la ruta:

    http://example.com:9000/sonar

    Esta es toda la configuración que necesitamos tocar por la parte de sonar 😀


    5. Configuración de nexus

    Nexus utiliza el context path de /nexus por defecto. Si no has alterado este parámetro, pasa directamente al punto 5.2.


    5.1 Cambio del context path

    De forma muy similar a sonar:

    • Vamos a $NEXUS_HOME/conf/nexus.properties.
    • Cambiamos la propiedad nexus-webapp-context-path para que se corresponda con nexus-webapp-context-path=/nexus.

    5.2 Forzar la url base

    En nexus, posee una propiedad que fuerza la URL base de forma que cuando se realiza una petición actúa como si estuviera siendo llamado desde la url base que nosotros le digamos.

    Esto es justo lo que queremos, ya que será apache quien se encarge de redirigir esa petición a nexus.

    Para forzar la url en nexus, tenemos que ir a Administracion –> server –> Application Server Settings donde encontraremos la opción Force Base URL.

    Nuestra configuración quedaría de la siguiente manera:

    Que no se nos olvide reiniciar el servicio! 😀


    6. Configuración de jenkins

    Lo primero, igual que en el resto de aplicaciones, es hacer que el context path sea el mismo entre apache y jenkins. Para configurar el el context path debes modificar el fichero de configuración de jenkins, localizado en el directorio /etc/default/jenkins.

    Para ello, en primer lugar declaramos la variable NAME al principio del documento, y que va a ser utilizada como $NAME.

    Por último añadimos esta variable a la propiedad PREFIX, y añadimos el prefijo como parámetro en la variable JENKINS_ARGS como se ve en la siguiente imagen.

    Una vez configurado, reiniciamos el servicio.


    7. Configuración de apache

    Ahora mismo tenemos las aplicaciones corriendo bajo las siguientes URLs:

    • Sonar: example.com/sonar
    • Nexus: example.com/nexus
    • Jenkins: example.com/jenkins

    Ahora vamos a configurar nuestro apache para reciba y maneje estas peticiones, redireccionando del puerto 80 al puerto 443 las peticiones que comiencen con el context path de /sonar, /nexus y /jenkins, de forma que el que hace la petición no sepa donde están los servicios que realiza por debajo.


    7.1. Habilitar módulos proxy

    Para ello necesitamos habilitar los siguientes módulos de apache:

    • mod_proxy: Este módulo permite que apache actúe como proxy.
    • mod_proxy_http: Requiere el módulo anterior para funcionar, y nos permite hacer actuar como proxy para peticiones HTTP y HTTPS.
    • mod_rewrite: Nos da una seríe de reglas basadas un motor de expresiones regulares para mapear URLs.
    • mod_ssl: Nos da soporte para SSL para apache.
    • mod_headers: Nos permite modificar, reemplazar o eliminar cabeceras de las peticiones y respuestas HTTP.

    Para habilitar los módulos de apache utilizamos los siguientes comandos:

    sudo a2enmod proxy
    sudo a2enmod proxy_http
    sudo a2enmod rewrite
    sudo a2enmod ssl
    sudo a2enmod headers

    NOTA: Reiniciamos el servicio para que los módulos se activen los nuevos módulos


    7.2. Creación del Site

    Ahora crearemos el site donde se harán las redirecciones del puerto 80 al puerto 443, y las redirecciones de apache para los servicios que tenemos preparados.

    Para ello creamos el fichero example.conf dentro de la carpeta /etc/apache2/sites-available.

    example.conf
    <VirtualHost *:80>
        ServerName example.com
    
        #Redirect all http traffic to https if it is pointed at /jenkins /nexus /sonar
        RewriteEngine On
        RewriteCond %{HTTPS} off
        RewriteCond %{REQUEST_URI} ^/(jenkins|nexus|sonar)/?.*$
        RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
    
        CustomLog /var/log/apache2/example.log combined
        ErrorLog /var/log/apache2/example-error.log
    </VirtualHost>
    
    <VirtualHost *:443>
    	SSLEngine on
        SSLCertificateFile /etc/apache2/ssl/example.crt
        SSLCertificateKeyFile /etc/apache2/ssl/example.key
    
        <Proxy *>
            Order deny,allow
            Allow from all
        </Proxy>
    
        ProxyRequests Off
        
        ProxyPass /sonar http://localhost:9000/sonar
        ProxyPassReverse /sonar http://localhost:9000/sonar
    
        ProxyPass /nexus http://localhost:8081/nexus
        ProxyPassReverse /nexus http://localhost:8081/nexus
    
        ProxyPass /jenkins http://localhost:8080/jenkins nocanon
        ProxyPassReverse /jenkins http://localhost:8080/jenkins
        AllowEncodedSlashes NoDecode
    
        CustomLog /var/log/apache2/example-https.log combined
        ErrorLog /var/log/apache2/example-https-error.log
    </VirtualHost>

    Vamos a analizar que hace este fichero:

    • Creamos dos host virtuales de apache, en los cuales uno escucha por el puerto 80 (estandar de HTTP) y otro por el puerto 443 (estandar de HTTPS). El “*” significa que cualquiera puede acceder a esta ip.
    • Dentro de la configuración del puerto, vemos que al principio le indicamos el nombre del servidor, con el dominio que hayamos elegido, en este caso example.com
    • Después habilitamos el motor de redirecciones, y establecemos dos condiciones, que son acumulativas y cada una es más restrictiva que la anterior:
      • La primera redirecciona todas las peticiones http a https.
      • La segunda especifica que la uri debe tener en el primer fragmento del context path que hayamos elegido como /nexus, /sonar ó /jenkins de forma que solo estas url serán redireccionadas al puerto 443.
    • Añadimos un par de logs, para tener registro de los accesos y los errores.
    • Declaramos el segundo virtual host, que recibirá las peticiones redireccionadas por el puerto 443.
    • Activamos la configuración de ssl, indicando el certificado y la clave que tenemos creados para el sitio y los colocamos en el directorio /etc/apache2/ssl
    • La etiqueta <Proxy> indica sirve para poder limitar el acceso. En nuestro caso, está habilitado para cualquiera que quiera acceder.
    • El parámetro ProxyRequests Off sirve para que apache no actúe como un forward proxy, ya que nosotros solo queremos que este redireccione las peticiones que vengan de clientes externos.
    • El caso de sonar y nexus es muy parecido: la directiva ProxyPass mapea los servidores remotos en una url local haciendo que la url https://example.com/sonar/example se transforme en https://localhost:9000/sonar/example. La directiva ProxyPassReverse es esencial para que cuando te muevas dentro del mismo sitio, apache redireccione las cabeceras de forma que no salgas pidiendo la url interna.
    • En el caso de la redireccion de jenkins, y tal como dice la documentación es necesario añadir las propiedades nocanon y AllowEncodedSlashed NoDecode ya que son requeridas para diversas características de Jenkins
    • NOTA: En este caso redirigimos a localhost debido a que las tres aplicaciones están instaladas en la misma máquina que apache. En caso de que estén en máquinas distintas simplemente hay que sustituir la url por el dominio donde se encuentren estas aplicaciones

    Una vez creado el fichero, necesitamos decirle a apache que utilice la configuración de este sitio, en lugar de la configuración por defecto. Para ello, ejecutamos los comandos:

    a2ensite example
    a2dissite 000-default

    De esta manera habilitamos la configuración que acabamos de crear, y desabilitamos la configuración por defecto de apache

    ¡¡Ya tenemos nuestro apache actuando como proxy!! 😀


    8. Securización de las aplicaciones

    Aunque ya tenemos nuestro proxy funcionando, nunca viene mal una capa de seguridad extra a la hora de proteger nuestros entornos de integración. Lo que vamos a hacer es añadir la seguridad que nos aporta apache.

    Para ello, habilitamos el módulo auth_digest, que nos permite realizar una autenticación en la que se encriptan los datos antes de ser enviada.

    También nos descargamos la herramienta htdigest, para lo que necesitamos instalar apache2-utils.

    sudo a2enmod
    apt-get install apache2-utils

    Reiniciamos el servicio y creamos el archivo auth_digest dentro de la carpeta /etc/apache2/digest. Para ello ejecutamos el comando:

    htdigest -c /etc/apache2/auth_digest restringido example

    Acto seguido nos pedirá una contraseña para el usuario “example”. Vamos a analizar que hace cada parte del comando que acabamos de ejecutar:

    • La propiedad -c crea el archivo si no existe.
    • La ruta /etc/apache2/auth_digest indica donde se creará el archivo.
    • A continuación se selecciona el nombre del realm (restringido), que es el grupo al que pertenece al usuario y suele ser también el valor que se le da la propiedad AuthName en las directivas que usan digest.
    • Por último va el nombre del usuario, que en nuestro caos se llama example.

    Una vez creado nuestro usuario, modificamos el site example.conf añadiendo la siguiente directiva dentro de las etiquetas <virtualhost *:443></virtualhost>.

    #Digest auth for all https request
    <Location "/*">
        AuthType Digest
        AuthName "restringido"
        AuthUserFile "/etc/apache2/auth_digest"
        Require valid-user
    </Location>

    Analizando el archivo vemos que:

    • La etiqueta <Location> indica restricciones a nivel de url, de modo que aquí le decimos que sin importar la url que sea, nos pida autenticación via digest.
    • El tipo de autenticación se indica con AuthType, en AuthName se indica generalmente el realm elegido.
    • Se indica donde se encuentra el fichero de autenticación con la propiedad AuthUserFile.
    • Por último, se dice que solo se permitan los usuarios que estén autenticados.

    Nuestro fichero de configuración queda de la siguiente manera:

    example.conf
    <VirtualHost *:80>
        ServerName example.com
    
        #Redirect all http traffic to https if it is pointed at /jenkins /nexus /sonar
        RewriteEngine On
        RewriteCond %{HTTPS} off
        RewriteCond %{REQUEST_URI} ^/(jenkins|nexus|sonar)/?.*$
        RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
    
        CustomLog /var/log/apache2/example.log combined
        ErrorLog /var/log/apache2/example-error.log
    </VirtualHost>
    
    <VirtualHost *:443>
    	SSLEngine on
        SSLCertificateFile /etc/apache2/ssl/example.crt
        SSLCertificateKeyFile /etc/apache2/ssl/example.key
    
        <Proxy *>
            Order deny,allow
            Allow from all
        </Proxy>
    
        ProxyRequests Off
        
        ProxyPass /sonar http://localhost:9000/sonar
        ProxyPassReverse /sonar http://localhost:9000/sonar
    
        ProxyPass /nexus http://localhost:8081/nexus
        ProxyPassReverse /nexus http://localhost:8081/nexus
    
        ProxyPass /jenkins http://localhost:8080/jenkins nocanon
        ProxyPassReverse /jenkins http://localhost:8080/jenkins
        AllowEncodedSlashes NoDecode
    
        #Digest auth for all https request
    	<Location "/*">
    	    AuthType Digest
    	    AuthName "restringido"
    	    AuthUserFile "/etc/apache2/auth_digest"
    	    Require valid-user
    	</Location>
    
        CustomLog /var/log/apache2/example-https.log combined
        ErrorLog /var/log/apache2/example-https-error.log
    </VirtualHost>

    Reiniciamos el servicio y comprobamos como al acceder a la url http://example.com/sonar se realiza una redirección a https://example.com/sonar, pidiendo antes la autenticación de apache


    9. Conclusiones

    Poner las aplicaciones detrás de un reverse proxy, es habitual en los desarrollos debido a la capa de protección que ofrece frente ataques directos, así como ocultar los puertos en los que son ejecutados las aplicaciones.


    10. Referencias

    Comunicación entre directivas en AngularJS

    $
    0
    0

    En este tutorial voy a enseñaros cómo comunicar dos o n directivas entre sí. Esto se logra mediante llamadas al controlador que poseen.

    Vamos a ver un ejemplo de una jerarquía de directivas:

    <libro>
        <pagina>
            <parrafo></parrafo>
        </pagina>
    </libro>

    Empezamos creando la directiva libro.

    var app = angular.module("app, []);
    app.directive("libro",function () {
        return {
            restrict: "E",
            controller: function () {
                this.tituloLibro = function (titulo) {
                    console.log("Titulo del libro: " + titulo);
                }
            }
        }
    });

    Tiene un controlador con una funcionalidad de prueba, que imprimirá el título del libro. A este controlador accederemos desde otra directiva, la directiva página.

    app.directive ("pagina", function () {
        return {
            restrict: "E",
            require: "^libro",
            link: function(scope, element, attrs, libroCtrl) {
                libroCtrl.tituloLibro("Dune");
            }
        }
    });

    Si ahora ejecutamos nuestro proyecto, aparecerá por consola el texto:

    Titulo del libro: Dune

    Lo vamos a complicar un poco más. Creamos otra directiva que utilice las dos que ya tenemos. El primer paso es refactorizar la directiva “pagina” para que tenga un controlador propio.

    app.directive ("pagina", function () {
    return {
        restrict: "E",
        controller: function () {
            this.numeroPagina = function (nPagina) {
                console.log("Numero de pagina " + nPagina);
            }
        }
    }
    });

    Ya tenemos una directiva “pagina” con una función en su controlador a la que llamar desde nuestra nueva directiva, “parrafo”. Esta va a recibir un array de controladores, que en este caso van a ser el de “libro” y el de “pagina”.

    app.directive("parrafo", function(){
    return {
        restrict:"E",
        require: ["^libro","^pagina"],
        link: function (scope, element, attrs, ctrls){
            ctrls[0].tituloLibro("Guia del Autoestopista Galactico");
            ctrls[1].numeroPagina("42");
        }
    }
    });

    En esta ocasión la salida por la consola será:

    Titulo del libro: Guia del Autoestopista Galactico
    Numero de pagina 42

    Como anteriormente, hacemos uso del require para obtener información de las otras directivas, la diferencia esta vez radica en que recibimos un array de controladores, y accedemos a ellos en el orden en el que los hemos declarado en el require.

    Espero que os haya servido para crear vuestras propias directivas enlazadas y hacer más mantenibles vuestros programas.

    Si queréis una información más detallada de cómo funciona el “controller” y el “link” de las directivas más allá de que el “controller” se utiliza para exponer un API y el link para consumirla, os aconsejo leer estos artículos (en inglés):

    Ahondando en Junit: Avisando sólo de los errores.

    $
    0
    0

    En este tutorial se introduce una gran aplicación la tecnología de JUnit: detección y alerta de errores.

    0. Índice de contenidos


    1. Introducción

    JUnit es un framework de trabajo para escribir pruebas de tal forma que sean repetibles,es una implementación de la arquitectura xUnit para frameworks de pruebas unitarias. Para la entender este tutorial se requiere tener unos mínimos conocimientos sobre JUnit (ver https://github.com/junit-team/junit/wiki/Getting-started).

    Muchas veces el objetivo de utilizar JUnit, es implementar un sistema automatizado de pruebas unitarias, normalmente para las pruebas de regresión. Este sistema automatizado de pruebas alertará a los desarrolladores de los problemas que presenta una aplicación tras un cambio, todo ello sin la necesidad de repetir las tediosas pruebas unitarias tras cada cambio realizado sobre la aplicación.

    La automatización de pruebas necesita mecanismos que permitan presentar el resultado de las pruebas de la forma más sencilla posible al probador de software. Es decir, de nada vale tener las pruebas automatizadas si interpretar el resultado de estas pruebas es tan costoso como realizar las pruebas manualmente.

    Lo que se presenta en este tutorial es una forma detectar mediante la arquitectura de JUnit cuando se ha producido un error y así alertar al probador. Esto permite por ejemplo preparar un sistema de pruebas que se ejecute todas las noches y envíe un email al responsable de las pruebas del software en el caso de que algo haya ido mal.


    2. Entorno

    El tutorial está escrito usando el siguiente entorno:

    • Hardware: ADM Semprom 2600+, MMX DNow, 1.8GHz 1024MB RAM.
    • Sistema Operativo: Windows XP Professional
    • Entorno de desarrollo:
      • Eclipse IDE for Java Developers Version: Indigo Service Release 2
      • JUnit 4.12

    3. Código del ejemplo

    El ejemplo consta de tres clases:

    • Calculadora. Es el código a probar.
    • PruebaCalculadora. Contiene un caso de prueba y es una implementación con JUnit de una prueba para Calculadora.
    • SiFallaLaPrueba. Esta clase toma el control cuando se ejecuta el caso de prueba. Permite saber si ha fallado (método failed) o ha terminado con éxito (método succeeded). En el caso de este ejemplo el interés está en controlar los casos en que la prueba falla, de ahí el nombre de la clase

    Aquí está el código de las tres clases que componen el ejemplo:

    Calculadora.java
    public class Calculadora {
      public int evalua(String expression) {
        int suma = 0;
        for (String sumando: expression.split("\\+"))
          suma += Integer.valueOf(sumando);
        return suma;
      }
    }
    PruebaCalculadora.java
    import static org.junit.Assert.assertEquals;
    import org.junit.Rule;
    import org.junit.Test;
    
    public class PruebaCalculadora {
      @Rule
       public SiFallaLaPrueba EjemploRegla = new SiFallaLaPrueba(); 
        
      @Test
      public void evaluatesExpression() {
        Calculadora calculadora = new Calculadora();
        int sum = calculadora.evalua("1+2+3+5");
        assertEquals(6, sum);
    
      }
    }
    SiFallaLaPrueba.java
    import org.junit.rules.TestWatcher;
    import org.junit.runner.Description;
    
    public class SiFallaLaPrueba extends TestWatcher {
        @Override
        protected void failed(Throwable e, Description descripcion) {
            System.out.println("Si la prueba falla se ejecuta esto");
        }
    }

    PruebaCalculadora ejecuta un caso de prueba que termina con error puesto que no obtiene el resultado esperado. La suma a realizar es 1+2+3+5=11 y en el caso de prueba se ha codificado que la suma debería dar un resultado de 6. Cuando la prueba falla, JUnit ejecuta lo codificado bajo la anotación @Rule y es el método failed del objeto creado el que deberá contener aquello que se quiera implementar para alertar del error. En el caso del ejemplo la sentencia:

    System.out.println("Si la prueba falla se ejecuta esto");

    La clase SiFallaLaPrueba hereda de TestWatcher. Ésta es una clase base para reglas que toman nota de las acciones de pruebas, sin modificarlas. En este ejemplo, la clase SiFallaLaPrueba permite sin modificar la clase PruebaCalculadora tomar nota de que se ha obtenido un resultado que no era el esperado.


    4. Conclusiones

    Cuando se automatizan pruebas se debe de tener en cuenta que la forma de alertar sobre los errores sea lo más práctica posible. No es de utilidad generar mucha información que posteriormente tenga que ser analizada manualmente. Probablemente sea tan costoso analizar la información resultado de las pruebas como hacer las propias pruebas.

    La técnica mostrada en este tutorial permite reducir el tamaño de los datos resultado de las pruebas automatizadas. El desarrollador solo tendrá que echar un vistazo para saber si todo ha ido bien o no tras la ejecución de las pruebas.


    5. Referencias

    Introducción a Kotlin para el desarrollo de apps en Android

    $
    0
    0

    En este tutorial se habla de Kotlin, un lenguaje que funciona sobre la JVM, como alternativa Java

    Índice de contenidos


    ¿Qué es Kotlin?

    Kotlin es un lenguaje que funciona sobre la JVM, es una alternativa moderna a Java priorizando la compatibilidad con Java. Intenta solucionar los problemas más comunes de Java cómo pueden ser las Null Pointer Exception o la verbosidad. Algunas de las características más destacadas de Kotlin son:

    • Fuerte inferencia de tipos, sobre todo trabajando con genéricos.
    • Protección frente a null, los tipos por defecto no pueden ser null, en caso de que un tipo venga de un API de Java, Kotlin te obliga a tener en cuenta ese null.
    • Características de programación funcional, cómo higher order functions, Pattern Matching y otras funcionalidades provenientes de lenguajes funcionales.

    Kotlin y Android

    Google eligió Java cómo lenguaje para Android, a pesar de que Java ha seguido evolucionando desde ese momento, en Android se sigue utilizando Java 6 (aunque con algunas características de Java 7), a estas versiones se les empieza a notar la edad, teniendo en cuenta todos los avances que han ido habiendo en el diseño de lenguajes, plasmado en algunos cómo Scala o Swift.

    Kotlin soluciona muchos de los problemas que tiene Java sin añadir demasiada complejidad, es un lenguaje pequeño que puede usar todas las librerías que ofrece la JVM sin problemas, por lo que no se convierte en una apuesta arriesgada.

    Todo ello sin perder la genial integración con Android Studio que ya conocemos con Java.

    Además, la naturaleza de las aplicaciones Android, las cuales están bastante ligadas a la capa de presentación, las hace perfectas para un lenguaje cómo Kotlin, el cual es mucho más dinámico que Java.

    Algo a tener en cuenta también es la posibilidad de mezclar clases Java y Kotlin en el mismo proyecto, de manera que migrar proyectos ya existentes a Kotlin no debería ser una tarea titánica sino algo progresivo.


    Añadir Kotlin a un proyecto Android

    Voy a dar por hecho de que el proyecto ya está usando Gradle, si no lo hace deberías migrarlo antes de nada

    Para añadir Kotlin a nuestro proyecto, es tan facil cómo añadir el plugin de Kotlin para Gradle, el cual se encargará de compilar el código de Kotlin de manera que pueda ser ejecutado en Android sin problema y añadir la dependencia de la librería standard de Kotlin.

    En el build.gradle de nuestro proyecto "padre":

    buildscript {
      ext.support_version = '22.2.1'
      ext.kotlin_version = '0.12.1218'
      ext.anko_version = '0.6.3-15s'
      repositories {
          jcenter()
      }
      dependencies {
          classpath 'com.android.tools.build:gradle:1.3.0'
          classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
      }
    }
    
    allprojects {
      repositories {
          jcenter()
      }
    }

    Y el build.gradle del proyecto de aplicación:

    apply plugin: 'com.android.application'
    apply plugin: 'kotlin-android'
    
    android {
        compileSdkVersion 22
        buildToolsVersion "22.0.1"
        defaultConfig {
            ...
        }
    }
    
    dependencies {
        compile fileTree(dir: 'libs', include: ['*.jar'])
        compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
        compile "org.jetbrains.anko:anko:$anko_version"
        ...
    }
    
    buildscript {
        repositories {
            jcenter()
        }
        dependencies {
            classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlin_version"
        }
    }

    Hemos añadido también la librería Anko, esta librería escrita en Kotlin, nos ofrece una serie de funciones de ayuda para agilizar el desarrollo en Android usando Kotlin, estas son totalmente opcionales pero no serán de gran ayuda.

    El siguiente paso sería crear un fichero .kt para comprobar que todo se ha instalado correctamente, para ello en el mismo paquete donde tengamos nuestras clases creamos un fichero de utilidades que nos ayudará con el manejo de trazas.

    import android.util.Log
    
    public val LOG_ENABLED: Boolean = true;
    public val TAG: String = "HNNotify"
    
    public fun log(text: String) {
        if (LOG_ENABLED) {
            Log.d(TAG, text)
        }
    }

    Una vez creada, podemos probar en cualquier actividad si se está ejecutando correctamente, para ello debemos importar el paquete.

    import static com.danieldisu.utils.UtilsPackage.*;
    
    public fun logIfItWorks() {
        log("Hello World");
    }

    Por convención Kotlin crea un paquete usando el nombre del paquete y la palabra "Package"


    Porqué usar Kotlin en Android

    A continuación voy a repasar algunas de las cosas que más me han gustado de usar Kotlin en Android, que suelen ir de la mano con los mayores puntos de verbosidad de Java.


    Lambdas

    Gracias a Java 8 las lambdas se han hecho conocidas en el mundo Java, aunque ya las usábamos de una manera u otra en Java desde bastante antes, por ejemplo, al añadir un OnClickListener a un botón en Android, extendiamos la clase abstracta y sobreescribiamos el único método de esta clase, el cual se llamará en el momento en el que el usuario pulse el botón. En otros muchos lenguajes cómo Javascript, esto se soluciona pasando una función por parámetro e invocando está función en el momento adecuado, Kotlin nos permite hacer esto mismo.

    button.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            log("User clicked!");
        }
    });

    En kotlin sería:

    button.onClick { onUpdateButtonClick(it) }

    o sin abreviar:

    updateButton.onClick { view -> onUpdateButtonClick(view)}

    El método onClick lo añade la librería Anko, al final solo es una manera corta de llamar al método setOnClickListener, en cuanto a la invocación, al tener un solo parámetro podemos usarlo con la keyword it.

    Esto es posible gracias al siguiente punto:


    Extender clases del sistema o librerías.

    Kotlin nos permite añadir metodos/funciones a clases y librerías externas, cómo pueden ser las clases de Android, lo que nos permite hacer metodos de utilidad que podrán ser invocados cón notación infija, lo cual nos dará la sensación de que los metodos pertenecen a la clase, además de permitir al IDE ofrecernos estos metodos cómo un metodo más al escribir el punto tras una variable.

    public fun android.view.View.onClick(l: (v: android.view.View) -> Unit): Unit = setOnClickListener(l)

    Esta es la función que nos extiende la clase View de Android permitiéndonos invocar al método onClick para añadir un listener.

    Estas extensiones tienen limitaciones, ya que se resuelven de manera externa, no pueden acceder a ningún atributo privado de la clase, pero sí a otros métodos no estáticos.


    Null safety

    Kotlin es un lenguaje que ha sido creado para solucionar los problemas más comunes en Java, uno de ellos y quizás el que más problemas crea de manera indirecta es la existencia de nulos. Para ello Kotlin no permite inicializar ninguna variable a null a no ser que declaremos esa variable cómo nullable, es decir declarando explícitamente en el sistema de tipos que esa variable puede contener null. Además, obliga que al usar cualquier variable declarada cómo nullable tengamos en cuenta la posibilidad de que esta puede ser null.

    Vamos a ver cómo es esto en la práctica:

    public var nullableString: String? = null 
      public var nonNullableString: String = null 
      public var nonNullableString: String

    En este caso solo la primera línea será válida, la segunda y tercera producirán un error de compilación.

    fun functionThatUsesString(string: String): Unit {
      log(string)
    }
    
    fun main(){
      functionThatUsesString(nullableString)
    }

    Esto producirá un error de compilación, ya que functionThatUsesString necesita un String y nullableString es de tipo String?, para evitar esto podemos hacerlo de varias maneras:

    var nullableString: String? = null;
    
    if (nullableString != null)
      functionThatUsesString(nullableString)
    else
      doSomethingElse()

    Hacemos exactamente lo mismo que haríamos en Java, pero esta vez obligados por el compilador, lo cual nos ofrece la garantía de que se está teniendo en cuenta.

    var nullableString: String? = null;
    
    functionThatUsesString(nullableString ?: "otherString")

    Usando el operador ?: que nos permite proponer una alternativa en caso de que este sea null.

    var nullableString: String? = null;
    
    functionThatUsesString(nullableString!!)

    Si estamos completamente seguros de que no va a ser null podemos usar el operador !! que nos ofrece Kotlin, en caso de que si fuese null lanzará un Null Pointer Exception, por ello este método no es el más aconsejado.


    Data Classes

    Muchas veces creamos clases que no son más que contenedores de información, en Java una sencilla clase con 3 atributos nos obligaría a hacer un total de 8 metodos (getters , setters, equals y hashcode), podemos definirlas en una sola línea usando la anotación data sobre una clase:

    public class Person {
    
      private String name;
      private String surname;
      private int age;
    
      public Person(String name, String surname, int age) {
          this.name = name;
          this.surname = surname;
          this.age = age;
      }
    
      public String getName() {
          return name;
      }
    
      public void setName(String name) {
          this.name = name;
      }
    
      public String getSurname() {
          return surname;
      }
    
      public void setSurname(String surname) {
          this.surname = surname;
      }
    
      public int getAge() {
          return age;
      }
    
      public void setAge(int age) {
          this.age = age;
      }
    
      @Override
      public boolean equals(Object o) {
          if (this == o) return true;
          if (o == null || getClass() != o.getClass()) return false;
    
          Person person = (Person) o;
    
          if (age != person.age) return false;
          if (name != null ? !name.equals(person.name) : person.name != null) return false;
          return !(surname != null ? !surname.equals(person.surname) : person.surname != null);
    
      }
    
      @Override
      public int hashCode() {
          int result = name != null ? name.hashCode() : 0;
          result = 31 * result + (surname != null ? surname.hashCode() : 0);
          result = 31 * result + age;
          return result;
      }
    }

    Tanto los getter/setter cómo equals y hashCode han sido auto generados por AndroidStudio

    VS la versión de Kotlin:

    data class User(val name: String, val surname: String, val age: Int)

    When Expression

    When es un paso intermedio entre el clásico Switch y match (Pattern Matching) en Scala y otros lenguajes funcionales. Básicamente lo que nos permite es hacer algo cuando una de las condiciones se cumpla:

    Se puede usar directamente con valores:

    when (x) {
      1 -> print("x == 1")
      2 -> print("x == 2")
      else -> { // Note the block
        print("x is neither 1 nor 2")
      }
    }

    Con valores que se obtienen tras invocar un metodo:

    when {
      x.isOdd() -> print("x is odd")
      x.isEven() -> print("x is even")
      else -> print("x is funny")
    }

    O con tipos:

    when (x) {
      is String -> println("isString!")
      is StringBuffer -> println("isStringBuffer!")
      else -> println("isOtherThing")
    }

    Interoperabilidad con Java

    Uno de los principales aspectos de diseño de Kotlin es mantener la interoperabilidad al máximo con Java, esto nos permite usar la gran mayoría de las librerías ya existentes sin ningún tipo de problema.

    En este sencillo ejemplo estamos usando Retrofit, Gson, RxJava sin ningún tipo de problema:

    public interface HNService {
    
        GET("/item/{itemId}.json")
        fun getItem(Path("itemId") itemId: String): Observable<HNItem>
    
    }
    
    object HNClient {
    
        private var client: HNService? = null
    
        fun get(): HNService {
            return this.client ?: createNewClient();
        }
    
        private fun createNewClient(): HNService {
            val restAdapter = RestAdapter.Builder()
                    .setEndpoint("https://hacker-news.firebaseio.com/v0")
                    .build();
            val hnClient = restAdapter.create(javaClass<HNService>())
            this.client = hnClient
            return hnClient;
        }
    }
    
    data class HNItem(val by: String,
                  val descendants: Int,
                  val id: Int,
                  val kids: Array<Int>,
                  val score: Int,
                  val time: Long,
                  val title: String,
                  val type: String,
                  val url: String) {
    }

    Conclusiones

    Hay gente que opinará que esto es solo syntax sugar y que quizás no sea necesario, pero cómo dijo Andrey Breslav (dev lead de Kotlin) en una charla, "todos los lenguajes son syntax sugar sobre código máquina" , por ello creo que debemos dejarnos ayudar por este tipo de avances, que ayudan no solo a la hora de desarrollar si no a la hora de leer y mantener el código.

    Kotlin además es un paso intermedio entre Java y Scala por lo que lo hace un lenguaje más atractivo para aquellos que no necesiten (o quieran) un lenguaje tan potente como Scala, además de ser bastante más pragmático y menos académico.


    Creación de entornos de integración con Ansible y Vagrant

    $
    0
    0

    En este tutorial veremos como crear un entorno de integración utilizando las herramientas Ansible para automatizar tareas y Vagrant como proveedor de máquinas virtuales.

    0. Índice de contenidos


    1. Introducción

    La mayoría de proyectos necesitan de un entorno de integración continua en el cual podemos ver y detectar los fallos de forma temprana, sin llegar al entorno de producción. Este tutorial pretende crear una plantilla de entorno de integración de modo que puedas desplegar de forma sencilla un entorno de integración continua gracias a la gran combinación de vagrant y ansible.


    2. Entorno

    El tutorial está escrito usando el siguiente entorno:

    • Hardware: Portátil MacBook Pro Retina (2.93 Ghz Intel Core 2 Duo, 8GB DDR3).
    • Sistema Operativo: Mac OS Yosemite 10.10
    • Virtual Box
    • Vagrant 1.7.2
    • Ansible 1.9.2

    3. Instalación de requisitos

    Para preparar el entorno de integración continua, crearemos la máquina virtual con Vagrant, un generador de máquinas virtuales a partir de imágenes previamente construidas (boxes). La plataforma en la que estará nuestra máquina virtual será Virtual Box y para configurar la máquina e instalar el software necesario utilizaremos ansible, que automatiza las tareas y despliega de forma sencilla, de modo que pueda replicarse en cualquier máquina remota.


    3.1. Instalación de Virtual Box

    Nos descargamos Virtual Box desde aquí, seleccionamos el paquete que necesitemos dependiendo de nuestro sistema operativo, y seguimos las instrucciones del instalador.


    3.2. Instalación de Vagrant

    Obtenemos Vagrant desde su página de descarga donde seguimos igualmente el proceso de descarga que requiera nuestro sistema operativo. Para comprobar que lo tenemos correctamente instalado, ejecutamos por línea de comandos:

    vagrant --version

    3.3. Instalación de Ansible

    Las instrucciones de descarga de ansible pueden ser encontradas aquí.

    La instalación recomendada es vía pip, el gestor de paquetes de Python. Si no lo tenemos instalado debemos instalar pip primero y luego instalar Ansible.

    sudo easy_install pip
    
    pip --version
    pip 6.1.1 from /Library/Python/2.7/site-packages/pip-6.1.1-py2.7.egg (python 2.7)
    
    sudo pip install ansible
    
    ansible --version
    ansible 1.9.2

    4. Creación y configuración de la máquina virtual

    Una vez instalados Vagrant y Ansible, estamos preparados para crear la máquina virtual de pruebas de nuestro entorno de integración, y configurarla para que nuestras aplicaciones funcionen detrás de un apache que hace de proxy interno.


    4.1. Elección del box de Vagrant

    Para crear la configuración de la máquina virtual creamos el directorio en el cual queramos tener la configuración de la máquina virtual, y después le decimos a vagrant que se descarge la imagen (box) con la que queremos que esté contruida nuestra máquina virtual con el siguiente comando:

    mkdir continuous_integration
    vagrant box add ubuntu/trusty64

    Vagrant ofrece una gran cantidad de boxes, así como la posibilidad de crear tus propias boxes personalizadas. Puedes encontrar boxes en el siguiente enlace.


    4.2. Creación Vagrantfile

    A continuación, para generar el fichero de configuración base de la máquina virtual usamos el comando:

    vagrant init

    Este comando crea el fichero de configuración que será leído cuando arranquemos o levantemos la máquina, y es donde especificaremos la box que va a utilizar vagrant para crear la máquina virtual, si disponemos de algún ‘provider’ para que instale software por defecto, así como configuración entre la máquina real y la máquina virtual. Tras ejecutar este comando, en la carpeta elegida para almacenar la configuración deberían tener la siguiente estructura:

    img1

    Comprobamos que ha aparecido el fichero Vagrantfile, donde estableceremos la configuración de que imagen va a utilizar la máquina virtual, así como quien se va a encargar de instalar sofware en la misma. El fichero debería quedar de la siguiente manera:

    Vagrantfile
    Vagrant.configure(2) do |config|
      config.vm.box = "ubuntu/trusty64"
    
      #Fix SSH forwarded port
      config.vm.network "forwarded_port", guest: 22, host:2222, id: "ssh", auto_correct: true
    
      #Use Apache
      config.vm.network "forwarded_port", guest: 80, host:8080, id: "apache", auto_correct: true
    
      config.vm.define "continuous_integration" do |continuous_integration|
        continuous_integration.vm.provision "ansible" do |ansible|
          ansible.inventory_path = "ansible/environments/continuous_integration/inventory"
          ansible.verbose = 'vvv'
          ansible.playbook = "ansible/continuous_integration.yml"
        end
    
        config.vm.provider "virtualbox" do |v|
          v.memory = 2048
        end
      end
    end

    A continuación analizamos el significado del siguiente fichero:

    Vagrant.configure(2) do |config|
  • El “2” de la primera línea representa la versión del objeto de configuración config que será usado para la configuración del bloque.
  • config.vm.box = "ubuntu/trusty64"
  • Con la siguiente línea le indicamos a vagrant que box utilizará para la creación de la máquina virtual.
  • #Fix SSH forwarded port
    config.vm.network "forwarded_port", guest: 22, host:2222, id: "ssh", auto_correct: true
    
    #Use Apache
    config.vm.network "forwarded_port", guest: 80, host:8080, id: "apache", auto_correct: true
  • La siguiente línea nos permite acceder a un puerto de nuestra máquina real (host) y que todos los datos sean redirigidos a la máquina virtual (guest) a través de un determinado puerto. La propiedad auto_correct:true indica que si hay un conficto debido a que ese puerto está actualmente en funcionamiento, vagrant se encargará de solucionarlo de forma automática cuando esté levantando la máquina virtual. Esto quiere decir que para poder acceder al apache de nuestra máquina virtual por un navegador, usaremos el puerto 8080 en nuestra máquina real (quitar esta opción si vamos a desplegar la aplicación en una máquina remota)
  • config.vm.define "continuous_integration" do |continuous_integration|
  • El uso de la propiedad define sirve para especificar un entorno dentro de la máquina de configuración (ya que vagrant permite la configuración de multiples máquinas virtuales en el mismo Vagrantfile). Nosotros definimos un entorno en el cual todas las propiedades que estén dentro del ámbito de este entorno, solo afectarán a esa única máquina sobreescribiendo las propiedades por defecto en caso de conflicto.
  • Nota: Dentro del ambito de define, “continuous_integration” es sinónimo de “config”.

    continuous_integration.vm.provision "ansible" do |ansible|
  • Dentro del entorno de integración indicamos a la configuración de vagrant que trandrá un encargado de instalar el software a la hora de crear la máquina. En este caso, será ansible.
  • ansible.inventory_path = "ansible/environments/continuous_integration/inventory"
    ansible.verbose = 'vvv'
    ansible.playbook = "ansible/continuous_integration.yml"
  • Para que ansible se ejecute necesita dos ficheros clave: Uno es el inventory, donde se indicarán los grupos que tiene ansible, así como las ip que están dentro de cada uno de ellos. El otro fichero necesario es el playbook, que contiene cuales son las tareas que ansible va a realizar, y en que grupos de host las va a realizar (previamente definidos en el inventory).
  • config.vm.provider "virtualbox" do |v|
      v.memory = 2048
    end
  • Como estamos realizando pruebas en una máquina virtual, le damos más memoria para que no haya problemas a la hora de levantar las aplicaciones.

  • 4.3. Creación del inventory

    Cuando usamos ansible, este necesita saber en qué máquinas debería correr un determinado playbook. La forma de indicarle estás máquinas es mediante el inventory. Vagrant tiene dos formas de crear este fichero, una de forma automática y otra de forma manual. Nosotros usaremos la forma manual, para lo cual rellenaremos el fichero con el siguiente contenido.

    ansible/environments/continuous_integration/inventory
    continuous_integration ansible_ssh_host=127.0.0.1 ansible_ssh_port=2222
    
    [ci]
    continuous_integration
    • En la primera línea especificamos el host de la máquina creada, y el puerto que habíamos establecido anteriormente que redireccionaba al puerto 22 de la máquina que va a ser creada. Esta información debe ser especificada tanto en el Vagrantfile como en el Inventory.
    • A continuación entre paréntesis tenemos el grupo de máquinas en el cual se ejecutará un determinado playbook. Esto quiere decir que si queremos que otra máquina ejecute estas tareas de ansible, a la hora de ejecutar el comando, solo tenemos que añadir aquí la ip de esa máquina y también ejecutará el mismo playbook.
    • Por último, indicamos las máquinas que estan dentro del grupo de máquinas. El nombre de las máquinas en el Vagrantfile y en el inventory deben ser iguales por defecto, de ahí que llamemos continuous_integration a nuestra máquina.

    4.4. Uso de variables en las tareas de ansible

    Durante la creación de los playbook usaremos un único lugar para establecer variables específicas, como el nombre a cambiar de un directorio creado con ansible, o una url de descarga, de forma que no tengamos que buscar dentro de los playbook, y evitar posibles errores humanos.

    Para minimizar este riesgo usaremos variables que estarán localizadas en un directorio group_vars creado a la altura del inventoryque tendrá dentro un fichero con el nombre del grupo de hosts que estemos ejecutando (en nuestro caso se llamará ‘ci’) en el que guardaremos todas estas variables.

    Las variables que utilizaremos en las tareas de los roles son:

    environments/continuous_integration/group_vars/ci
    ---
    
    maven:
        download_url: http://ftp.cixug.es/apache/maven/maven-3/3.3.3/binaries/apache-maven-3.3.3-bin.tar.gz
        file: apache-maven-3.3.3-bin.tar.gz
        home: /opt/apache-maven-3.3.3
    
    sonar:
        download_url: http://downloads.sonarsource.com/sonarqube/sonarqube-5.1.1.zip
        archive: sonar.zip
        version_dir: sonarqube-5.1.1
        home: /opt/sonar
        jdbc_username: sonar
        jdbc_password: sonarpwd
    
    jenkins:
        password: jenkinspwd
        base_dir: /var/lib/jenkins
        apt_key: https://jenkins-ci.org/debian/jenkins-ci.org.key
        repo: deb http://pkg.jenkins-ci.org/debian binary/
        plugins_url: http://updates.jenkins-ci.org/latest
    
    nexus:
        version: 2.11.3-01
        download_url: http://www.sonatype.org/downloads/nexus-2.11.3-01-bundle.tar.gz
        base_dir: /opt
        home: /opt/nexus
        data_dir: /home/nexus
    
    apache:
        server_name: dev.example
        ssl_folder: /etc/apache2/ssl
        certificate_file: /etc/apache2/ssl/dev.example.crt
        certificate_key_file: /etc/apache2/ssl/dev.example.key
        sonar_http: http://localhost:9000/sonar
        nexus_http: http://localhost:8081/nexus
        jenkins_http: http://localhost:8080/jenkins
        custom_log_file: app_custom.log
        error_log_file: app_error.log
        custom_https_log: app_custom_https.log
        error_https_log: app_error_https.log

    Podemos agrupar las variables en grupos más generales.

    Las variables tienen el formato nombre_variable: valor (El espacio es importante!!!) de modo que si quisieramos la variable version del grupo de variables nexus la llamaríamos de la siguiente forma en el playbook:

    {{nexus.version}}

    4.5. Creación del playbook

    Ahora que ya tenemos definida las máquinas en las que se ejecutarán nuestras tareas de ansible, nos falta definir esas tareas. Esto se hace en el playbook, un documento YAML que define una serie de pasos que deben ser ejecutado en una o más máquinas.

    Como de costumbre, escribiremos la estructura de nuestro playbook y luego explicaremos los datos más relevantes:

    continuous_integration.yml
    ---
    
    #file: continuous_integration.yml
    
    - hosts: ci
      sudo: yes
      gather_facts: no
      roles:
         - common_os_setup
         - dependencies
         - java8
         - sonar
         - ansible
         - jenkins
         - maven
         - nexus
         - apache

    Un playbook se estructura en “plays”. Estos son pequeñas divisiones que se van ejecutando en orden descendente.

    • La propiedad hosts indica cual es el grupo de máquinas en las cuales se ejecutaran las tareas de ansible, que coincide con el definido en nuestro inventory.
    • La propiedad sudo indica si se necesitan permisos de root para realizar algunas de las tareas.
    • gather_facts es información derivada de comunicarnos con nuestros sistemas remotos, como información sobre la dirección ip del host remoto, o que sistema operativo usa. En nuestro caso no nos comunicamos con ningún sistema remoto, por lo que esta opción está desabilitada.
    • La propiedad roles irá ejecutando las tareas de cada rol en orden descendente cuando se cargue el playbook. Las tareas podrían ir directamente en el playbook, pero de esta manera, es mucho más sencillo organizarlas por lo que hacen, así como tener separadas las responsabilidades por si en el futuro no necesitas realizar todas las tareas, sino reutilizar algunas antiguas y crear unas nuevas 😉

    4.6. Creación de Roles

    Como ya hemos explicado anteriormente, los roles nos permiten organizar nuestras tareas. Crearemos el directorio roles en la cual estarán localizados todos los roles justo debajo de la carpeta ansible.

    cd ansible
    mkdir roles

    Dentro de la carpeta roles la estructura es la siguiente:

    • roles/[nombre_rol]/tasks: Aquí dentro irá un fichero main.yml que contendrá las tareas de este rol.
    • roles/[nombre_rol]/handlers: Tendrá un fichero main.yml que contiene los handlers, que son respuestas a eventos que pueden cambiar el estado de las tareas. Los veremos más adelante.
    • roles/[nombre_rol]/templates: Dentro van las plantillas de los ficheros que se puede rellenar utilizando las variables declaradas anteriormente.
    • roles/[nombre_rol]/files: A direfencia de las plantillas, aquí pasamos el fichero tal cual, sin modificaciones de variables. Cuando necesitamos un fichero del host real, el mrol viene a buscarlo aquí por defecto.

    Si no va a utilizar handlers, templates, o files no es necesario crearlas. Task suele ser la carpeta que está creada debido a que tiene las tareas a ejecutar por ansible.

    Vamos a crear los roles definidos en el playbook comentando las partes más relevantes de cada uno.


    4.6.1. common_os_setup

    Este rol está encargado de realizar las tareas generales, como la descarga de paquetes de idiomas y asegurarse de que el sistema está actualizado.

    roles/common_os_setup/tasks/main.yml
    ---
    # file: roles/common_os_setup/tasks/main.yml
    
    - name: ensure apt cache is up to date
      apt: update_cache=yes
    
    - name: ensure the language packs are installed
      apt: name={{item}}
      with_items:
      - language-pack-en
      - language-pack-es
    
    - name: reconfigure locales
      command: sudo update-locale LANG=en_US.UTF-8 LC_ADDRESS=es_ES.UTF-8 LC_COLLATE=es_ES.UTF-8 LC_CTYPE=es_ES.UTF-8 LC_MONETARY=es_ES.UTF-8 LC_MEASUREMENT=es_ES.UTF-8 LC_NUMERIC=es_ES.UTF-8 LC_PAPER=es_ES.UTF-8 LC_TELEPHONE=es_ES.UTF-8 LC_TIME=es_ES.UTF-8
    
    - name: ensure apt packages are upgraded
      apt: upgrade=yes

    Cada tarea diferente comienza con un guión (-) y luego tiene una serie de propiedades. La propiedad name indica de forma intuitiva que hace cada tarea. En nuestro caso, tenemos 4 tareas, en las que hay que destacar:

    • En la primera tarea usamos el módulo apt, que maneja los paquetes apt siendo esta linea equivalente al comando apt-get update.
    • En la segunda tarea, usamos el mismo módulo, pero con la propiedad name. Esta propiedad indica el nombre del paquete a instalar, pero también permite el uso de variables. Ansible posee internamente la variable item (las variables van rodeadas de dobles corchetes {{variable}}) que indica que esa operación debe realizarse tantas veces como items haya dentro de la propiedad with_items. En este caso, equivale a los comandos:
    apt-get language-pack-en
    apt-get language-pack-es

    Nota: En la documentación de ansible posees una lista de todos los módulos, así como las propiedades de cada uno.

  • Ansible también permite el uso de comandos de forma esplícita, como es el caso de la tercera tarea, donde se le dice que ejecute el comando descrito, encargado de actualizar los idiomas.

  • 4.6.2. dependencies

    Este rol se encarga de incluir dependencias que podrías necesitar en un entorno de integración continua.

    roles/dependencies/tasks/main.yml
    ---
    
    #file: /roles/dependencies/tasks/main.yml
    
    - name: install unzip command
      apt: name=unzip state=present
      tags: dependency
    
    - name: install git
      apt: name=git state=present
      tags: git

    La propiedad: tag es muy útil durante el testing de tareas. Al especificar que que quieres correr un determinado playbook con una serie de roles, pero solo quieres probar lo último que has añadido, o un grupo determinado de tareas específicas, cuando corra la ejecución de ansible, puedes especificarle que solo ejecute las tareas con un tag determinado.


    4.6.3. java8

    Este rol se encarga de instalar java en la máquina remota.

    roles/java8/tasks/main.yml
    ---
    
    # file: /roles/java8/tasks/main.yml
    
    - name: add Java repository to sources
      apt_repository: repo='ppa:webupd8team/java'
      tags: java
    
    - name: autoaccept license for Java
      debconf: name='oracle-java8-installer' question='shared/accepted-oracle-license-v1-1' value='true' vtype='select'
      tags: java
    
    - name: update APT package cache
      apt: update_cache=yes
      tags: java
    
    - name: install Java 8
      apt: name=oracle-java8-installer state=latest install_recommends=yes
      tags: java
    
    - name: set default environment variable
      apt: name=oracle-java8-set-default
      tags: java

    Los dos módulos a destacar en este rol son:

    • apt-repository: que sirve para añadir o eliminar repositorios apt en ubuntu y debian.
    • debconf se encarga de configurar un paquete, en este caso para autoaceptar la licencia de java.

    4.6.4. sonar

    Este rol es el encargado de instalar y configurar sonar, que nos permite controlar y supervisar la calidad del código.

    roles/sonar/tasks/main.yml
    ---
    
    # file: /roles/sonar/tasks/main.yml
    
    - name: download sonar
      get_url: url="{{sonar.download_url}}" dest="/tmp/{{sonar.archive}}"
      tags: sonar
    
    - name: create sonar group
      group: name=sonar state=present
      tags: sonar
    
    - name: create sonar user
      user: name=sonar comment="Sonar" group=sonar
      tags: sonar
    
    - name: extract sonar
      unarchive: src="/tmp/{{sonar.archive}}" dest=/opt copy=no
      tags: sonar
    
    - name: move sonar to its right place
      shell: mv /opt/{{sonar.version_dir}} {{sonar.home}} chdir=/opt
      tags: sonar
    
    - name: change ownership of sonar dir
      file: path="{{sonar.home}}" owner=sonar group=sonar recurse=yes
      tags: sonar
    
    - name: copy sonar properties
      template: src=sonar.properties dest="{{sonar.home}}/conf/sonar.properties"
      tags: sonar
    
    - name: make sonar runned by sonar user
      replace: dest="{{sonar.home}}/bin/linux-x86-64/sonar.sh" regexp="#RUN_AS_USER=(.*)$" replace="RUN_AS_USER=sonar"
      tags: sonar
    
    - name: add sonar links for service management
      file: src="{{sonar.home}}/bin/linux-x86-64/sonar.sh" dest="{{item}}" state=link
      with_items:
        - /usr/bin/sonar
        - /etc/init.d/sonar
      tags: sonar
    
    - name: ensure sonar is running and enabled as service
      service: name=sonar state=restarted enabled=yes
      tags: sonar

    Este rol tiene chichilla 😀 vamos a analizarlo detenidamente:

    • En la primera tarea, vemos como nos descargamos sonar, pero en este caso desde una url con el módulo get_url. Este módulo tiene dos parametros obligatorios: url, que indica el lugar desde donde descargaremos el archivo y dest que es el lugar donde lo almacenaremos. La segunda cosa a destacar es que estamos usando variables para decirle cual es la url de descarga, así como el nombre del archivo descargado, localizadas en el fichero ci descrito al final del punto anterior.
    • Con los módulos group y user, somos capaces de crear grupos y usuarios respectivamente.
    • El módulo unarchive permite desempaquetar un archivo , y la opción copy=no indica que este archivo ya se encuentra en la máquina remota, y que no necesita ser copiado desde el host.
    • El módulo shell es igual que el módulo visto previamente para realizar comandos.
    • El módulo file nos permite crear directorios, así como estableces sus grupos, usuarios o permisos.
    • La propiedad template en la tarea con el nombre copy sonar properties indica que el archivo mencionado va a ser recogido desde la carpeta templates localizada justo dentro del rol. En esta carpeta podremos introducir nuestras plantillas que pueden utilizar las variables de ansible, y serán rellenadas en el momento en el que las llames. El contenido de la plantilla es:
    • roles/sonar/templates/sonar.properties
    # This file must contain only ISO 8859-1 characters.
    # See http://docs.oracle.com/javase/1.5.0/docs/api/java/util/Properties.html#load(java.io.InputStream)
    #
    # Property values can:
    # - reference an environment variable, for example sonar.jdbc.url= ${env:SONAR_JDBC_URL}
    # - be encrypted. See http://docs.codehaus.org/display/SONAR/Settings+Encryption
    
    
    #--------------------------------------------------------------------------------------------------
    # DATABASE
    #
    # IMPORTANT: the embedded H2 database is used by default. It is recommended for tests but not for
    # production use. Supported databases are MySQL, Oracle, PostgreSQL and Microsoft SQLServer.
    
    # User credentials.
    # Permissions to create tables, indices and triggers must be granted to JDBC user.
    # The schema must be created first.
    sonar.jdbc.username={{sonar.jdbc_username}}
    sonar.jdbc.password={{sonar.jdbc_password}}
    
    #----- Embedded Database (default)
    # It does not accept connections from remote hosts, so the
    # server and the analyzers must be executed on the same host.
    sonar.jdbc.url=jdbc:h2:tcp://localhost:9092/sonar
    
    # H2 embedded database server listening port, defaults to 9092
    sonar.embeddedDatabase.port=9092
    
    
    #----- MySQL 5.x
    #sonar.jdbc.url=jdbc:mysql://localhost:3306/sonar?useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&useConfigs=maxPerformance
    
    #----- Oracle 10g/11g
    # - Only thin client is supported
    # - Only versions 11.2.* of Oracle JDBC driver are supported, even if connecting to lower Oracle versions.
    # - The JDBC driver must be copied into the directory extensions/jdbc-driver/oracle/
    # - If you need to set the schema, please refer to http://jira.codehaus.org/browse/SONAR-5000
    #sonar.jdbc.url=jdbc:oracle:thin:@localhost/XE
    
    
    #----- PostgreSQL 8.x/9.x
    # If you don't use the schema named "public", please refer to http://jira.codehaus.org/browse/SONAR-5000
    
    #----- Microsoft SQLServer 2005/2008
    # Only the distributed jTDS driver is supported.
    #sonar.jdbc.url=jdbc:jtds:sqlserver://localhost/sonar;SelectMethod=Cursor
    
    
    #----- Connection pool settings
    sonar.jdbc.maxActive=20
    sonar.jdbc.maxIdle=5
    sonar.jdbc.minIdle=2
    sonar.jdbc.maxWait=5000
    sonar.jdbc.minEvictableIdleTimeMillis=600000
    sonar.jdbc.timeBetweenEvictionRunsMillis=30000
    
    
    
    #--------------------------------------------------------------------------------------------------
    # WEB SERVER
    
    # Web server is executed in a dedicated Java process. By default its heap size is 768Mb.
    # Use the following property to customize JVM options. Enabling the HotSpot Server VM
    # mode (-server) is recommended.
    # Note that the option -Dfile.encoding=UTF-8 is mandatory.
    #sonar.web.javaOpts=-Xmx768m -XX:MaxPermSize=160m -XX:+HeapDumpOnOutOfMemoryError \
    #  -Djava.awt.headless=true -Dfile.encoding=UTF-8 -Djruby.management.enabled=false
    
    # Binding IP address. For servers with more than one IP address, this property specifies which
    # address will be used for listening on the specified ports.
    # By default, ports will be used on all IP addresses associated with the server.
    #sonar.web.host=0.0.0.0
    
    # Web context. When set, it must start with forward slash (for example /sonarqube).
    # The default value is root context (empty value).
    sonar.web.context=/sonar
    
    # TCP port for incoming HTTP connections. Disabled when value is -1.
    sonar.web.port=9000
    
    # TCP port for incoming HTTPS connections. Disabled when value is -1 (default).
    #sonar.web.https.port=-1
    
    # HTTPS - the alias used to for the server certificate in the keystore.
    # If not specified the first key read in the keystore is used.
    #sonar.web.https.keyAlias=
    
    # HTTPS - the password used to access the server certificate from the
    # specified keystore file. The default value is "changeit".
    #sonar.web.https.keyPass=changeit
    
    # HTTPS - the pathname of the keystore file where is stored the server certificate.
    # By default, the pathname is the file ".keystore" in the user home.
    # If keystoreType doesn't need a file use empty value.
    #sonar.web.https.keystoreFile=
    
    # HTTPS - the password used to access the specified keystore file. The default
    # value is the value of sonar.web.https.keyPass.
    #sonar.web.https.keystorePass=
    
    # HTTPS - the type of keystore file to be used for the server certificate.
    # The default value is JKS (Java KeyStore).
    #sonar.web.https.keystoreType=JKS
    
    # HTTPS - the name of the keystore provider to be used for the server certificate.
    # If not specified, the list of registered providers is traversed in preference order
    # and the first provider that supports the keystore type is used (see sonar.web.https.keystoreType).
    #sonar.web.https.keystoreProvider=
    
    # HTTPS - the pathname of the truststore file which contains trusted certificate authorities.
    # By default, this would be the cacerts file in your JRE.
    # If truststoreFile doesn't need a file use empty value.
    #sonar.web.https.truststoreFile=
    
    # HTTPS - the password used to access the specified truststore file. 
    #sonar.web.https.truststorePass=
    
    # HTTPS - the type of truststore file to be used.
    # The default value is JKS (Java KeyStore).
    #sonar.web.https.truststoreType=JKS
    
    # HTTPS - the name of the truststore provider to be used for the server certificate.
    # If not specified, the list of registered providers is traversed in preference order
    # and the first provider that supports the truststore type is used (see sonar.web.https.truststoreType).
    #sonar.web.https.truststoreProvider=
    
    # HTTPS - whether to enable client certificate authentication.
    # The default is false (client certificates disabled).
    # Other possible values are 'want' (certificates will be requested, but not required),
    # and 'true' (certificates are required).
    #sonar.web.https.clientAuth=false
    
    # The maximum number of connections that the server will accept and process at any given time.
    # When this number has been reached, the server will not accept any more connections until
    # the number of connections falls below this value. The operating system may still accept connections
    # based on the sonar.web.connections.acceptCount property. The default value is 50 for each
    # enabled connector.
    #sonar.web.http.maxThreads=50
    #sonar.web.https.maxThreads=50
    
    # The minimum number of threads always kept running. The default value is 5 for each
    # enabled connector.
    #sonar.web.http.minThreads=5
    #sonar.web.https.minThreads=5
    
    # The maximum queue length for incoming connection requests when all possible request processing
    # threads are in use. Any requests received when the queue is full will be refused.
    # The default value is 25 for each enabled connector.
    #sonar.web.http.acceptCount=25
    #sonar.web.https.acceptCount=25
    
    # Access logs are generated in the file logs/access.log. This file is rolled over when it's 5Mb.
    # An archive of 3 files is kept in the same directory.
    # Access logs are enabled by default.
    #sonar.web.accessLogs.enable=true
    
    # TCP port for incoming AJP connections. Disabled if value is -1. Disabled by default.
    #sonar.ajp.port=-1
    
    
    #--------------------------------------------------------------------------------------------------
    # SEARCH INDEX
    
    # Elasticsearch is used to facilitate fast and accurate information retrieval.
    # It is executed in a dedicated Java process.
    
    # JVM options. Note that enabling the HotSpot Server VM mode (-server) is recommended.
    #sonar.search.javaOpts=-Xmx256m -Xms256m -Xss256k -Djava.net.preferIPv4Stack=true \
    #  -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 \
    #  -XX:+UseCMSInitiatingOccupancyOnly -XX:+HeapDumpOnOutOfMemoryError \
    #  -Djava.awt.headless=true
    
    # Elasticsearch port. Default is 9001. Use 0 to get a free port.
    # This port must be private and must not be exposed to the Internet.
    #sonar.search.port=9001
    
    
    #--------------------------------------------------------------------------------------------------
    # UPDATE CENTER
    
    # Update Center requires an internet connection to request http://update.sonarsource.org
    # It is enabled by default.
    #sonar.updatecenter.activate=true
    
    # HTTP proxy (default none)
    #http.proxyHost=
    #http.proxyPort=
    
    # NT domain name if NTLM proxy is used
    #http.auth.ntlm.domain=
    
    # SOCKS proxy (default none)
    #socksProxyHost=
    #socksProxyPort=
    
    # proxy authentication. The 2 following properties are used for HTTP and SOCKS proxies.
    #http.proxyUser=
    #http.proxyPassword=
    
    
    #--------------------------------------------------------------------------------------------------
    # LOGGING
    
    # Level of information displayed in the logs: NONE (default), BASIC (functional information)
    # and FULL (functional and technical details)
    #sonar.log.profilingLevel=NONE
    
    # Path to log files. Can be absolute or relative to installation directory.
    # Default is /logs
    #sonar.path.logs=logs
    
    
    #--------------------------------------------------------------------------------------------------
    # OTHERS
    
    # Delay in seconds between processing of notification queue. Default is 60 seconds.
    #sonar.notifications.delay=60
    
    # Paths to persistent data files (embedded database and search index) and temporary files.
    # Can be absolute or relative to installation directory.
    # Defaults are respectively /data and /temp
    #sonar.path.data=data
    #sonar.path.temp=temp
    
    
    
    #--------------------------------------------------------------------------------------------------
    # DEVELOPMENT - only for developers
    
    # Dev mode allows to reload web sources on changes and to restart server when new versions
    # of plugins are deployed.
    #sonar.web.dev=false
    
    # Path to webapp sources for hot-reloading of Ruby on Rails, JS and CSS (only core,
    # plugins not supported).
    #sonar.web.dev.sources=/path/to/server/sonar-web/src/main/webapp

    4.6.5. ansible

    Este rol se encarga de instalar ansible, exactamente de la misma manera en la que antes lo instalamos en nuestra máquina. La razón por la que necesitamos ansible en remoto es para automatizar tareas como los despliegues de la aplicación

    roles/ansible/tasks/main.yml
    ---
    # file: roles/ansible/tasks/main.yml
    
    - name: install python setup tools
      apt: name=python-setuptools state=present update_cache=yes
      tags: ansible
    
    - name: install pip to manage ansible installation
      command: easy_install pip
      tags: ansible
    
    - name: install ansible with pip
      command: pip install ansible
      tags: ansible

    4.6.6. jenkins

    Este rol se encarga de instalar jenkins y sus plugins

    roles/jenkins/tasks/main.yml
    ---
    
    # file: /roles/jenkins/tasks/main.yml
    
    - name: add apt key for jenkins
      apt_key: url="{{jenkins.apt_key}}" state=present
      tags: jenkins
    
    - name: add apt repository for jenkins
      apt_repository: repo="{{jenkins.repo}}" state=present
      tags: jenkins
    
    - name: install jenkins
      apt: name=jenkins update_cache=yes
      tags: jenkins
    
    - name: create plugins directory
      file: path="{{jenkins.base_dir}}/plugins" state=directory owner=jenkins group=jenkins
      tags: jenkins
    
    - name: copy jenkins properties
      copy: src={{item}} dest="/etc/default/jenkins"
      with_items:
          - jenkins
      tags: jenkins
    
    - name: ensure plugins are present
      get_url: url="{{jenkins.plugins_url}}/{{item}}.hpi" dest="{{jenkins.base_dir}}/plugins/{{item}}.jpi" owner=jenkins group=jenkins
      with_items:
        - sonar
        - github-oauth
        - credentials
        - git-client
        - scm-api
        - git
        - github-api
        - github
      notify: restart jenkins
      tags: jenkins

    Lo más destacable es la propiedad notify en la última tarea. La acción restart jenkins es denominada por ansible como handlers, y son acciones que se escriben a parte porque pueden ser reutilizadas por varias tareas del mismo rol.

    Por lo tanto para que nuestro rol funcione necesitamos el handler del rol jenkins:

    roles/jenkins/handlers/main.yml
    ---
    
    # file: /roles/jenkins/handlers/main.yml
    
    - name: restart jenkins
      service: name=jenkins state=restarted

    Como se puede observar, el nombre del handler debe ser igual a la propiedad -name de ansible.

    En la tarea:

    - name: copy jenkins properties
      copy: src={{item}} dest="/etc/default/jenkins"
      with_items:
          - jenkins
      tags: jenkins

    En la propiedad src no estamos indicandole la ruta. cuando esto sucede, ansible busca por defecto en el directorio files dentro de la carpeta de su rol. Este fichero sobreescribe las propiedades por defecto de jenkins para cambiar el context path de / a /jenkins:

    roles/jenkins/files/jenkins
    # defaults for jenkins continuous integration server
    
    # pulled in from the init script; makes things easier.
    NAME=jenkins
    
    # location of java
    JAVA=/usr/bin/java
    
    # arguments to pass to java
    JAVA_ARGS="-Djava.awt.headless=true"  # Allow graphs etc. to work even when an X server is present
    #JAVA_ARGS="-Xmx256m"
    #JAVA_ARGS="-Djava.net.preferIPv4Stack=true" # make jenkins listen on IPv4 address
    
    PIDFILE=/var/run/$NAME/$NAME.pid
    
    # user and group to be invoked as (default to jenkins)
    JENKINS_USER=$NAME
    JENKINS_GROUP=$NAME
    
    # location of the jenkins war file
    JENKINS_WAR=/usr/share/$NAME/$NAME.war
    
    # jenkins home location
    JENKINS_HOME=/var/lib/$NAME
    
    # set this to false if you don't want Hudson to run by itself
    # in this set up, you are expected to provide a servlet container
    # to host jenkins.
    RUN_STANDALONE=true
    
    # log location.  this may be a syslog facility.priority
    JENKINS_LOG=/var/log/$NAME/$NAME.log
    #JENKINS_LOG=daemon.info
    
    # OS LIMITS SETUP
    #   comment this out to observe /etc/security/limits.conf
    #   this is on by default because http://github.com/jenkinsci/jenkins/commit/2fb288474e980d0e7ff9c4a3b768874835a3e92e
    #   reported that Ubuntu's PAM configuration doesn't include pam_limits.so, and as a result the # of file
    #   descriptors are forced to 1024 regardless of /etc/security/limits.conf
    MAXOPENFILES=8192
    
    # set the umask to control permission bits of files that Jenkins creates.
    #   027 makes files read-only for group and inaccessible for others, which some security sensitive users
    #   might consider benefitial, especially if Jenkins runs in a box that's used for multiple purposes.
    #   Beware that 027 permission would interfere with sudo scripts that run on the master (JENKINS-25065.)
    #
    #   Note also that the particularly sensitive part of $JENKINS_HOME (such as credentials) are always
    #   written without 'others' access. So the umask values only affect job configuration, build records,
    #   that sort of things.
    #
    #   If commented out, the value from the OS is inherited,  which is normally 022 (as of Ubuntu 12.04,
    #   by default umask comes from pam_umask(8) and /etc/login.defs
    
    # UMASK=027
    
    # port for HTTP connector (default 8080; disable with -1)
    HTTP_PORT=8080
    
    # port for AJP connector (disabled by default)
    AJP_PORT=-1
    
    # servlet context, important if you want to use apache proxying
    PREFIX=/$NAME
    
    # arguments to pass to jenkins.
    # --javahome=$JAVA_HOME
    # --httpPort=$HTTP_PORT (default 8080; disable with -1)
    # --httpsPort=$HTTP_PORT
    # --ajp13Port=$AJP_PORT
    # --argumentsRealm.passwd.$ADMIN_USER=[password]
    # --argumentsRealm.roles.$ADMIN_USER=admin
    # --webroot=~/.jenkins/war
    # --prefix=$PREFIX
    
    JENKINS_ARGS="--webroot=/var/cache/$NAME/war --httpPort=$HTTP_PORT --ajp13Port=$AJP_PORT --prefix=/jenkins"

    4.6.7. maven

    Este rol se encarga de instalar maven.

    roles/maven/tasks/main.yml
    ---
    
    #file: /roles/maven/tasks/main.yml
    
    - name: download maven 3.3.3
      get_url: url="{{maven.download_url}}" dest="/tmp/{{maven.file}}"
      tags: maven
    
    - name: extract maven
      unarchive: src="/tmp/{{maven.file}}" dest=/opt copy=no
      tags: maven
    
    - name: create link to maven folder
      file: src="{{maven.home}}" dest=/opt/maven state=link
      tags: maven
    
    - name: tell the machine to use our maven
      alternatives: name=mvn link=/usr/bin/mvn path=/opt/maven/bin/mvn
      tags: maven
    
    - name: create .m2 folder
      file: path="{{jenkins.base_dir}}/.m2" state=directory owner=jenkins group=jenkins
      tags: maven

    El único módulo a destacar es alternatives, usado cuando hay multiples programas instalados que proveen funcionalidad similar.


    4.6.8. nexus

    Este rol se encarga de instalar nexus, nuestro repositorio de artefactos binario.

    roles/nexus/tasks/main.yml
    ---
    
    # file: /roles/nexus/tasks/main.yml
    
    - name: create nexus group
      group: name=nexus state=present
      tags: nexus
    
    - name: create nexus user
      user: name=nexus comment="Nexus" group=nexus createhome=yes
      tags: nexus
    
    - name: download nexus
      get_url: url="{{nexus.download_url}}" dest=/tmp/nexus
      tags: nexus
    
    - name: extract nexus
      unarchive: src=/tmp/nexus dest="{{nexus.base_dir}}" copy=no
      tags: nexus
    
    - name: update the symbolic link to nexus install
      file: path="{{nexus.home}}" src="{{nexus.base_dir}}/nexus-{{nexus.version}}" owner=nexus group=nexus state=link force=yes
      tags: nexus
    
    - name: set NEXUS_HOME environment variable
      lineinfile: dest=/etc/environment regexp="^export NEXUS_HOME.*" line="export NEXUS_HOME={{nexus.home}}" insertbefore="^PATH.*"
      tags: nexus
    
    - name: move work directory to data directory
      shell: mv {{nexus.base_dir}}/sonatype-work {{nexus.data_dir}}/sonatype-work
      tags: nexus
    
    - name: change nexus work directory in nexus.properties file
      replace: dest="{{nexus.home}}/conf/nexus.properties" regexp="^nexus-work=(.*)$" replace="nexus-work={{nexus.data_dir}}/sonatype-work"
      tags: nexus
    
    - name: change ownership of nexus home
      file: path="{{nexus.base_dir}}/nexus-{{nexus.version}}" owner=nexus group=nexus recurse=yes
      tags: nexus
    
    - name: change ownership of nexus data directory
      file: path="{{nexus.data_dir}}" owner=nexus group=nexus recurse=yes
      tags: nexus
    
    - name: make nexus runned by nexus user
      replace: dest="{{nexus.home}}/bin/nexus" regexp="#RUN_AS_USER=(.*)$" replace="RUN_AS_USER=nexus"
      tags: nexus
    
    - name: change nexus home in binary file
      replace: dest="{{nexus.home}}/bin/nexus" regexp="^NEXUS_HOME=(.*)" replace="NEXUS_HOME={{nexus.home}}"
      tags: nexus
    
    - name: change nexus piddir in binary file
      replace: dest="{{nexus.home}}/bin/nexus" regexp="^#PIDDIR=(.*)" replace="PIDDIR=/home/nexus"
      tags: nexus
    
    - name: create symbolic link to /etc/init.d/nexus
      file: src="{{nexus.home}}/bin/nexus" dest=/etc/init.d/nexus state=link
      tags: nexus
    
    - name: make nexus script executable
      file: path="{{nexus.home}}/bin/nexus" mode=0755
      tags: nexus
    
    - name: enable nexus service
      service: name=nexus enabled=yes
      notify: restart nexus
      tags: nexus

    Nexus también posee un handler que restaura el servicio, por lo que lo creamos:

    roles/nexus/handlers/main.yml
    ---
    
    # file: /roles/nexus/handlers/main.yml
    
    - name: restart nexus
      service: name=nexus state=restarted

    4.6.9 apache

    Este rol se encarga de la instalación y la confi

    roles/apache/tasks/main.yml
    ---
    #file: /roles/apache/tasks/main.yml
    
    - name: install apache2
      apt: name=apache2 state=latest update_cache=yes
      tags: apache
    
    - name: install apache2-utils for authentication
      apt: name=apache2-utils state=latest update_cache=yes
      tags: apache
    
    - name: enable mod_proxy module
      apache2_module: name={{item}} state=present
      with_items:
        - proxy
        - proxy_http
        - rewrite
        - headers
        - ssl
      notify: restart apache
      tags: apache
    
    - name: create certificate folder
      file: path="{{apache.ssl_folder}}" state=directory
      tags: apache
    
    - name: copy SSL certificate
      copy: src={{item}} dest="{{apache.ssl_folder}}"
      with_items:
          - dev.example.crt
          - dev.example.key
      tags: apache
    
    - name: copy virtualhost conf
      template: src=virtualhost.conf dest="/etc/apache2/sites-available/{{apache.server_name}}.conf"
      tags: apache
    
    - name: enable virtualhost conf
      command: a2ensite {{apache.server_name}}
      notify: restart apache
      tags: apache
    
    - name: remove default virtualhost file
      file: path=/etc/apache2/sites-enabled/000-default.conf state=absent
      notify: restart apache
      tags: apache

    En la tarea copy ssl certificate actualmente se enviarían los dos archivos necesarios para que apache funcionara con ssl, de modo que introduciendo en la carpeta files/ tus certificados ansible se encargaría de realizar el resto de la configuración.

    Este rol también necesita de un handler para reiniciar el servicio de apache:

    roles/apache/handlers/main.yml
    ---
    #file: /roles/apache/handlers/main.yml
    
    - name: restart apache
      service: name=apache2 state=restarted

    Por último, le pasamos la configuración del virtual host de forma que apache actúe como un reverse proxy a la hora de acceder a los servicios de sonar, jenkins y nexus.

    NOTA: la configuración para que este acceso a servicios sea via https está DESHABILITADA. Se recomienda crear par de claves para la autenticación ssl. Después solo es necesario descomentar la parte comentada en el primer virtual host y eliminar las propiedades del proxy del mismo.

    roles/apache/templates/virtualhost.conf
    
        #ServerName {{apache.server_name}}
    
        #Redirect all http traffic to https if it is pointed at /jenkins /nexus /sonar
        #RewriteEngine On
        #RewriteCond %{HTTPS} off
        #RewriteCond %{REQUEST_URI} ^/(jenkins|nexus|sonar)/?.*$
        #RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
    
        
            Order deny,allow
            Allow from all
        
    
        ProxyRequests Off
    
        ProxyPass /sonar {{apache.sonar_http}}
        ProxyPassReverse /sonar {{apache.sonar_http}}
    
        ProxyPass /nexus {{apache.nexus_http}}
        ProxyPassReverse /nexus {{apache.nexus_http}}
    
        ProxyPass /jenkins {{apache.jenkins_http}} nocanon
        ProxyPassReverse /jenkins {{apache.jenkins_http}}
        AllowEncodedSlashes NoDecode
    
        CustomLog /var/log/apache2/{{apache.custom_log_file}} combined
        ErrorLog /var/log/apache2/{{apache.error_log_file}}
    
    
    
        SSLEngine on
        SSLCertificateFile {{apache.certificate_file}}
        SSLCertificateKeyFile {{apache.certificate_key_file}}
        SetEnvIf User-Agent ".*MSIE.*" nokeepalive ssl-unclean-shutdown
    
        
            Order deny,allow
            Allow from all
        
    
        ProxyRequests Off
    
        ProxyPass /sonar {{apache.sonar_http}}
        ProxyPassReverse /sonar {{apache.sonar_http}}
    
        ProxyPass /nexus {{apache.nexus_http}}
        ProxyPassReverse /nexus {{apache.nexus_http}}
    
        ProxyPass /jenkins {{apache.jenkins_http}} nocanon
        ProxyPassReverse /jenkins {{apache.jenkins_http}}
        AllowEncodedSlashes NoDecode
    
        CustomLog /var/log/apache2/{{apache.custom_https_log}} combined
        ErrorLog /var/log/apache2/{{apache.error_https_log}}
    

    5. Arranque de la máquina virtual

    Una vez creados los roles del playbook, simplemente levantamos la máquina virtual con el comando:

    vagrant up

    Esto levantará la máquina virtual y llamará a ansible para que instale y configure todos los elementos del entorno que hemos visto previamente en los roles. Hay que tener en cuenta que ansible lo único que hace es ejecutar todos los comando que haríamos nosotros de forma automática, así que la primera vez que ejecutemos el comando tardará sus 10-15 minutos 😀


    6. Comprobación del funcionamiento

    Para comprobar su funcionamiento con nuestro navegador vamos a http://localhost:8080/jenkins y comprobamos como nos hace una redirección al servicio de jenkins. El motivo de usar el puerto 8080 es que en el fichero de configuración de la máquina virtual creamos una redirección del puerto 8080 en el host al puerto 80 de la máquina virtual para poder comprobar nuestros cambios. En caso de que usemos ansible para configurar una máquina externa, no sería necesario definir esta propiedad, sino que nos refeririamos directamente al puerto 80 de esa máquina.

    Si nos fijamos en la url escrita, se ve como ocultamos los puertos internos de la aplicación, usando solo el puerto 80 del apache, que está redirigido al puerto 8080 de la máquina real.

    img2 img3 img4

    Puedes descargar la configuración de vagrant y ansible de este tutorial desde aquí


    7. Conclusiones

    Gracias a la combinación de vagrant y ansible, somos capaces de crear un entornos con gran facilidad, de modo que no solo sirva para crear entornos de integración, si no también entornos para el equipo de desarrollo.


    8. Referencias

    Captura masiva de experiencias de usuario

    $
    0
    0

    Hace unos meses el desarrollador Pablo Almunia ofreció una charla auspiciada por HTML5Spain relativa a la captura masiva de experiencias de usuario que en su día se compartió en el blog de Autentia.

    A lo largo de su exposición, el desarrollador explicó el funcionamiento de Monsync, el sistema que ha creado para averiguar datos sobre la navegación de un usuario en una web en concreto. Gracias al mismo puede averiguar qué páginas visita, cuánto tiempo está en un campo o dónde hace click, entre otros, un visitante de una web y no sólo por dónde desplaza el mouse.

    A diferencia de otras aplicaciones a este respecto que se han desarrollado, el usuario “no detecta que su actividad está siendo monitorizada, ya que de ser así su conducta podría verse alterada”, lo que convierte a este desarrollo en clave para aventajar a sus competidores en el mercado.

    IntroJS, mejorando las guías de usuario

    $
    0
    0

    ¿Qué es lo primero que se queda obsoleto en una guía de ayuda de usuario?, en este tutorial veremos cómo evitar seguir manteniendo “capturas de pantalla”.


    0. Índice de contenidos.


    1. Introducción

    Al entregrar cualquier producto de software, por muy intuitiva que sea su interfaz, lo normal es que el usuario solicite una guía de ayuda para el uso del mismo.

    Tenemos muchas opciones para generar e incorporar una guía de usuario, pero tratándose de aplicaciones web lo más recurrido es generar un PDF con la guía y capturas de pantalla de la interfaz explicando su funcionalidad. También podríamos implementar la guía de usuario con la misma tecnología con la que esté desarrollada la aplicación web en una sección ad hoc, pero para explicar su funcionalidad tendríamos que recurrir de nuevo a capturas de pantalla.

    Por definición, lo que más cambia es la interfaz de usuario puesto que cualquier requisito adicional que se solicite es muy probable que requiera añadir algún componente extra a la misma, con lo que esas capturas de pantalla se quedarán obsoletas muy fácilmente.

    Con librerías como IntroJS podemos integrar las ayudas al usuario dentro de la propia página web, haciendo referencia a las secciones de la página que queremos explicar manteniendo de este modo tanto la interfaz como la propia ayuda en un único sitio; siendo más díficil también que esa ayuda se quede desactualizada.

    Aquí tenemos un ejemplo de uso: integrado en esta misma página.


    2. Entorno.

    El tutorial está escrito usando el siguiente entorno:

    • Hardware: Portátil MacBook Pro 15′ (2.3 GHz Intel Core i7, 16GB DDR3).
    • Sistema Operativo: Mac OS Mavericks 10.9.4
    • Intro.js 1.1.0


    3. Uso vía metadatos.

    La configuración pasa por descargar los fuentes e incluir tanto el javascript minificado como las css en nuestro código:

    <script type="text/javascript" src="intro.js/intro.js"></script>
    <link href="intro.js/introjs.css" rel="stylesheet"></link>

    Para incluir las secciones a integrar dentro de la introducción o guía de usuario basta con hacer uso de los atributos data-intro y data-step dentro de nuestro propio código.

    <div data-step="1" data-intro="Esta sección podemos encontrar el índice de contenidos">
    ...
    </div>
    <div data-step="2" data-intro="Puede resultarte interesante leer primero la introducción">
    ...
    </div>
    ...
    <button onclick="introJs().start();">?</button>
    ...

    Simplemente con esta configuración podríamos comenzar a montar nuestra ayuda de usuario, aunque la librería tiene muchas opciones:

    • showButtons: mostrar los botones o no,
    • showProgress: mostrar una barra de progreso,
    • showBullets: mostrar los bullets o no,
    • showStepNumbers: mostrar los números de paso.

    La recomendación es disponer de una función que configure y arranque la intro y no invocarla directamente desde el botón de ayuda:

    function startIntro(){
    	intro = introJs();
    	intro.setOptions({
    		nextLabel: '>', 
    		prevLabel: '<', 
    		skipLabel: 'Salir', 
    		doneLabel: 'Hecho'
    	});
    	intro.start();
    }

    Eso nos permitirá, por ejemplo, internacionalizar los botones que aparecen en la ayuda.



    4. Configuración dinámica.

    Si, por lo que sea, no podemos modificar el código de nuestro html siempre podemos añadir los pasos de la ayuda de forma dinámica vía javascript.

    <script type="text/javascript">
      function startIntro(){
    		intro = introJs();
    		intro.setOptions({
    			disableInteraction: false,
    			exitOnEsc: false,
    			exitOnOverlayClick: false,
    			showBullets: false,
    			showButtons: false,
    			keyboardNavigation: false,
    			steps: [
    			{
    				element: document.querySelector('#cliente\\:nameInputHelpWrapper'),
    				intro: "Por favor, introduzca el nombre del cliente.",
    				position: 'left'
    			},
    			{
    				element: document.querySelector('#cliente\\:createdAtInputHelpWrapper'),
    				intro: "Por favor, introduzca la fecha de alta en el sistema",
    				position: 'left'
    			},
    			{
    				element: '#cliente\\:ageInputHelpWrapper',
    				intro: "Por favor, introduzca la edad del cliente",
    				position: 'left'
    			}
            	]
    		});
    		intro.start();
    	}
    	startIntro();
    </script>

    De este modo podríamos obtener los pasos de ayuda de cualquier servicio o mantenerlos de forma estática también en la propia página, pero debe existir una referencia al id del componente html.



    5. Referencias.


    6. Conclusiones.

    A la par que mejoramos la experiencia de usuario, nos ahorramos trabajo de mantenibilidad.

    Un saludo.

    Jose

    Control de Log4j2 en ejecución con JMX

    $
    0
    0

    Este tutorial versa sobre cómo la conocida herramienta de logging para Java Log4j2 puede ser controlada “en caliente” (en tiempo de ejecución) mediante otra herramienta de gestión de Java, la JMX

    0. Índice de contenidos


    1. Introducción

    A estas alturas, casi todo desarrllador usa (o debería usar) alguna herramienta de logging que permita conocer las trazas de las aplicaciones que implementa y gestiona. Una de las más conocidas sin duda es Apache Log4j.

    La versión 2 de esta herramienta (a la que nos referiremos en el futuro como Log4j2) dispone (entre otras novedades) de soporte para la conocida JMX, el conjunto de herramientas de Java que permite, entre otras cosas, administrar las aplicaciones Java.

    A lo largo de este tutorial veremos que dicho soporte permite modificar la configuración de Log4j en tiempo de ejecución, ya que algunos componentes de éste se instrumentan con MBeans, y por tanto, pueden ser controlados y monitorizados remotamente.

    Para ello, se dispone de una interfaz gráfica (GUI) que permite ver la salida del componente StatusLogger y reconfigurar Log4j de forma remota mediante la edición del fichero de propiedades o incluso cambiar dicho fichero en tiempo de ejecución.


    2. Entorno

    El tutorial está escrito usando el siguiente entorno:

    • Hardware: Portátil MacBook Pro 15′ (2.4 Ghz Intel Core I5, 8GB DDR3).
    • Sistema Operativo: Mac OS Yosemite 10.10.5
    • Entorno de desarrollo:
      • Eclipse Mars
      • Versión de Java: JDK 1.8.0_51
      • Maven 3
      • Log4j

    3. Creando la aplicación

    Para ilustrar lo que se ha dicho en la introducción, vamos a crear una aplicación Java gestionada con Maven que incluya un componente LOG, el cual será una instancia de Log4j.


    3.1. Requisitos previos

    En este tutorial se ha creado un proyecto Maven sencillo para gestionar la aplicación, ya que es más cómodo para la gestión de dependencias y los import de Java.

    Se supone que el lector posee los siguientes conocimientos previos:

    • Creación de proyectos Maven y Java con Eclipse
    • Gestión de dependencias con Maven
    • Uso de la herramienta Log4j2
    • Uso de threads (hilos) en Java
    • Bases sobre el uso de la terminal de comandos

    ¿Listos? Vamos a ello…


    3.2. Configuración de Maven

    Lo primero que debemos hacer es añadir en nuestro fichero pom.xml las dependencias necesarias para poder usar Log4j2:

    <dependency>
      <groupId>org.apache.logging.log4j</groupId>
      <artifactId>log4j-api</artifactId>
      <version>2.3</version>
    </dependency>
    
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-core</artifactId>
        <version>2.3</version>
    </dependency>

    3.3. Configuración de Log4j2

    Una vez tenemos las dependencias necesarias, debemos crear el fichero de configuración para Log4j2. Éste se localizará (en nuestro caso) en la carpera src/main/resources. Para nuestro ejemplo, usaremos la configuración por defecto (sacada de la página de configuración de Log4j2):

    log4j2.xml
    <?xml version="1.0" encoding="UTF-8"?>
    <Configuration status="WARN">
      <Appenders>
        <Console name="Console" target="SYSTEM_OUT">
          <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
        </Console>
      </Appenders>
      <Loggers>
        <Root level="error">
          <AppenderRef ref="Console"/>
        </Root>
      </Loggers>
    </Configuration>

    Por defecto, el nivel mínimo para sacar un mensaje por consola (SYSTEM_OUT) es error. Esto dice que sólo los mensajes con nivel superior o igual a éste (error y fatal) serán los que salgan por pantalla.


    3.4. Aplicación Java

    Ya tenemos todo lo que tendremos que usar para crear un componente Logger y empezar a utilizarlo.

    Creamos una clase que tenga un miembro de clase Logger y construimos un método main que muestre los distintos niveles de aviso de Log4j2:

    NOTA: No usamos el nivel fatal.

    Log4j2Main.java
    package com.autentia.tutoriales.log4j2.main;
    
    import org.apache.logging.log4j.LogManager;
    import org.apache.logging.log4j.Logger;
    
    public class Log4j2Main {
    
        private static final int NUM_OF_ITERATIONS = 50;
    
        private static final int MILLISECONDS_TO_SLEEP = 2000;
    
        private static final Logger LOG = LogManager.getLogger(Log4j2Main.class);
    
        public static void main(String[] args) throws InterruptedException {
    
            for (int i = 0; i < NUM_OF_ITERATIONS; i++) {
    
                LOG.trace("Mensaje de trace");
                LOG.debug("Mensaje de debug");
                LOG.info("Mensaje de info");
                LOG.warn("Mensaje de warn");
                LOG.error("Mensaje de error");
    
                // para separar las iteraciones
                System.out.println();
    
                Thread.sleep(MILLISECONDS_TO_SLEEP);
            }
        }
    }

    El funcionamiento es bastante simple: cada dos segundos enviar todos los niveles de log que disponemos, y repetir el procedimiento cincuenta veces. Estas constantes pueden variar en función de nuestros deseos. Si la lanzamos, veremos que sólo salen por pantalla los mensajes de error. Eso es porque el nivel de traza está configurado como “ERROR”.


    4. Activando JMX

    El soporte de JMX está activado por defecto. Cuando Log4j se inicializa, sus componentes (StatusLogger, ContextSelector, y todos los LoggerContexts, LoggerConfigs y Appenders) se instrumentan con MBeans (componentes que JMX monitoriza y gestiona).

    Si se desea evitar la creación de estos MBeans, hay que especificar en los argumentos de la JVM la siguiente propiedad:

    log4j2.disable.jmx=true

    Hay más información sobre JMX en este enlace.

    Se puede monitorizar el Log tanto en local como en remoto. Para hacerlo de forma local, no hace falta especificar a la JVM la propiedad arriba mencionada. Este ejemplo es el que veremos durante el tutorial.

    Por su parte, la monitorización remota necesita la siguiente propiedad para la JVM:

    com.sun.management.jmxremote.port=portNum

    donde portNum indica el puerto que activará las conexiones RMI de JMX.


    5. Monitorizando Log4j2

    Ya está lista la aplicación; ahora toca monitorizar el miembro LOG. Esto lo podemos hacer de dos formas.

    Primero vamos a utilizar una herramienta llamada JConsole, que nos permitirá ver la información que deseamos y al mismo tiempo cambiar la configuración de nuestro Logger.

    Más tarde probaremos a usar la propia interfaz gráfica que nos brinda Log4j2, como un plugin de JConsole.


    5.1. JConsole

    La mejor forma de averiguar qué métodos y/o atributos de los componentes de Log4j2 son accesibles mediante JMX es explorarlos directamente mediante JConsole. Para lanzarla, escribimos el siguiente comando:

    $JAVA_HOME/bin/jconsole

    Esto nos abrirá la siguiente pantalla:

    NOTA: la aplicación debe estar corriendo para que aparezca en la lista de procesos. Si no, la arrancamos, pulsamos en Connection -> New Connection y debería aparecer.

    Pulsamos en nuestra aplicación, y tras pulsar en la pestaña MBeans y desplegar el directorio de org.apache.logging.log4j2 -> XXXXXXX (donde XXXXXXX es un número), obtendremos esta otra vista:

    Pulsando en Loggers y seleccionando el único atributo que tiene, veremos en la parte derecha la propiedad Level. Ésta indica el nivel de traza configurado para nuestro LOG. Vemos que su valor es “ERROR”:

    Si cambiamos ese valor por “INFO”, la salida de nuestra aplicación cambiará, mostrando más niveles de traza (info y warn):

    En esta última imagen se ve la iteración exacta en la que se refleja el cambio de nivel.


    5.2. GUI de Log4j2 como plugin de JConsole

    Log4j2 provee una GUI que puede ejecutarse como un plugin de JConsole. Para lanzar ésta con el plugin de Log4j2 ejecutamos el comando:

    $JAVA_HOME/bin/jconsole -pluginpath /ruta/a/log4j-api-2.3.jar:/ruta/a/log4j-core-2.3.jar:/ruta/a/log4j-jmx-gui-2.3.jar

    NOTA: Hay que descargarse los .jar previamente. Pueden descargarse desde este enlace.

    Por defecto, el nivel del StatusLogger (el componente que se muestra al seleccionar la pestaña del plugin) es WARN, por lo que no muestra nada salvo que ocurra algo importante. Para cambiar este nivel, cambiamos su nivel a TRACE. De esta forma aparecerán los mensajes internos de estado. Se puede cambiar el nivel en la propia JConsole o en el fichero de configuración de Log4j2, en la línea:

    <Configuration status="TRACE">

    Este plugin también permite la edición “en caliente” del fichero de configuración mediante un editor de texto:é

    Hay dos maneras de cambiar el fichero de configuración:

    • Especificando un nuevo archivo desde otra localización.
    • Editando el fichero XML actual desde el editor.
    Especificando un nuevo fichero desde otra localización

    Si cambiamos el fichero especificando una nueva localización y pulsamos el botón “Reconfigure from Location” se cargará esa nueva configuración. Para este método, el fichero debe existir y debe poder ser leído desde la aplicación (por ejemplo estando en el directorio src/main/resources de nuestra aplicación). En caso contrario ocurrirá un error y la configuración no cambiará (se mantendrá la configuración por defecto); sin embargo, el editor sí cambiar´ y mostrará el nuevo archivo.á

    Vamos a cambiar con este método la configuración actual. Nuestro fichero original es el siguiente:

    log4j2.xml
    <?xml version="1.0" encoding="UTF-8"?>
    <Configuration status="TRACE">
      <Appenders>
        <Console name="Console" target="SYSTEM_OUT">
          <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
        </Console>
      </Appenders>
      <Loggers>
        <Root level="error">
          <AppenderRef ref="Console"/>
        </Root>
      </Loggers>
    </Configuration>

    Creamos un nuevo fichero xml y lo llamamos log4j2-other.xml. Luego, escribimos el siguiente código:

    log4j2-other.xml
    <?xml version="1.0" encoding="UTF-8"?>
    <Configuration status="WARN">
      <Appenders>
        <Console name="Console" target="SYSTEM_OUT">
          <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
        </Console>
      </Appenders>
      <Loggers>
        <Root level="info">
          <AppenderRef ref="Console"/>
        </Root>
      </Loggers>
    </Configuration>

    Hemos cambiado el nivel del Status Logger y el nivel de trazas del log (de error a info).

    Ejecutamos la aplicación, abrimos JConsole con el plugin de Log4j2, vamos a la pestaña de configuración de Log4j2 y cambiamos el fichero:

    Al pulsar el botón “Reconfigure from Location“, la salida de nuestro programa cambiará:

    Editando el fichero desde el editor

    Para cambiar la configuración mediante este método, sólo tenemos realizar los cambios que queramos y pulsar el otro botón, “Reconfigure with XML Below“. Esto mandará la configuración a la aplicación en tiempo de ejecución. Sin embargo, esto no sobre-escribirá el fichero de configuración, si no que la reconfiguración sólo ocurre en memoria y el texto no se almacena en ningún sitio.

    Probamos el método: lanzamos la aplicación, abrimos JConsole con el plugin de Log4j2, vamos a la pestaña de configuración de Log4j2 y editamos el fichero:

    Sólo cambiamos el nivel de traza de error a info.

    Al pulsar el botón “Reconfigure with XML Below“, la salida de la aplicación mostrará una cantidad considerable de mensajes notificando el cambio y pasará a mostrar los nuevos mensajes:


    6. Conclusiones

    Log4j2 ofrece múltiples novedades respecto a su predecesor, pero sin duda una de las más útiles es el soporte a JMX, ya que permite monitorizar perfectamente nuestro sistema de logging y sus configuraciones de manera sencilla y eficaz.

    Espero que el tutorial os haya animado a usar (o al menos probar) esta potente herramienta. El código del ejemplo (como siempre) se encuentra disponible en Github.

    ¡Nos vemos!

    Rodrigo de Blas


    7. Referencias

    Introducción a Realm

    $
    0
    0

    Tutorial de Realm en el que veremos como utilizar esta herramienta para persistir datos en plataformas móviles como iOS y Android.

    0. Índice de contenidos


    1. Introducción

    En el siguiente tutorial vamos a explicar y ver como funcionan algunas funciones de Realm, aprenderemos a añadir y eliminar datos en la BBDD. La plataforma en la que vamos a realizarlo es iOS y el lenguaje a utilizar Objetive c.


    2. ¿Qué es?

    Realm, una nueva manera de persistir datos en plataformas movile iOS y Android. En la parte iOS es compatible tanto con Objetive-C como con Swift. Viene a ser una alternativa a Core Data, que para mi sigue siendo el referente, ya que es una framework muy madura y con un funcionamiento y rendimiento excelente.


    3. ¿Por qué investigar sobre una alternativa a Core Data si funciona tan bien?

    Bajo mi punto de vista no hay que ser rígido a probar las nuevas frameworks o tecnologías que aparecen, por que ¿quién sabe lo que nos estamos pediendo? y en el peor de los casos al final es aprendizaje. Pero sí que es cierto que en este caso me ha despertado la curiosidad un Benchmark que he visto en el que prometen un gran rendimiento, bastante por encima de Core Data.

    El equipo de Realm lo ha conseguido haciendo que tenga una sola transición a la vez, y podemos pensar, “esto lo hará más lento” pues el hecho de no usar SQLite ha hecho que el equipo de Realm pueda optimizar el desempeño de acceso a la BBDD, incluyendo escrituras, tal y como muestran las gráficas.

    imagen1


    4. El objetivo

    Vamos a realizar una app que podamos agregar personas con un nombre y un apellido y además podremos eliminarlas, tanto de la BBDD como de la tabla.

    En este tipo de ejercicio no notaremos el rendimiento que hemos visto en el anterior Benchmark al manejar una pequeña cantidad de objetos, pero podremos ver su funcionamiento básico y poder seguir investigando y manejar grandes cantidades de datos para ver su rendimiento.

    ¡¡¡Vamos manos a la obra!!!


    5. Entorno

    El equipo donde se van a realizar las pruebas tiene las siguientes características:

    • Macbook pro 15” retina mediados de 2014.
    • Procesador: Intel Core i7 2,5 GHZ. -Memoria: 16GB 1600MHz DDR3.
    • SSD 512GB.
    • Gráficos GeForce GT 750M 2048MB.
    Software necesario
    • Cocoa Pods.
    • Framework Realm.
    • Realm Browser.
    • Xcode.

    6. Inicio de App de ejemplo

    1.- Abrimos Objetive c y creamos un nuevo proyecto, lo podemos hacer en File / New Proyect o bien con Xcode abierto pulsando las teclas shift+cmd+N y seleccionamos Single View Application.

    2.- Le damos nombre, en nuestro caso se llama RealmExample, pero podéis darle el nombre que queráis.


    7. Configurar Cocoa Pods

    1.- Bien, ahora lo que debemos hacer es abrir el terminal de comandos, podemos hacerlo a través de Spotlight con el siguiente comando de teclas, pulsamos cmd+espacio y tecleamos terminal, pulsamos intro y se nos abrirá nuestra consola. En nuestro caso el directorio de nuestra app es Documents / iOS / RealmExample.

    2.- Una vez aquí indicamos que nuestro proyecto va a contener cocoaPods con el siguiente comando.

    pod init

    3.- Ya tenemos nuestro proyecto con un Pod, si a alguien estos conceptos le suenen a chino quizás debería leer sobre que es Cocoa Pods ya que en este tutrorial no se va tratar en profundidad este tema. Os dejo un enlace en el que podéis encontrar mas info: CocoaPods.

    4.- Con el siguiente comando vamos a abrir nuestro Pod para indicarle las dependencias que queremos instalar.

    open -a Xcode Podfile

    Tenemos que conseguir que nuestro Pod quede igual que en la imagen, para ellos tendremos que añadir dos líneas:

    1.- Entre target ‘RealmExample’ do y end deberemos añadir Realm.
    2.- Entre target ‘RealmExampleTest’ do y end deberemos añadir Realm.Headers.

    Después de esto guardamos los cambios realizados en el documento en File / Save o con cmd + S.

    Para que se instalen las dependencias del framework, estando en el mismo directorio de nuestro proyecto escribimos en la terminal:

    pod install

    Después de esto cerramos Xcode y deberemos arrancar la aplicación desde un icono que se nos ha creado, es como el xcodeproj por defecto pero de color blanco. y la extension es xcworkspace.


    7.1. Modelo

    1.- Vamos a crear nuestro modelo persona para que Realm persista estos objetos en BBDD, requisito imprescindible, la clase debe heredar de RealmObject. Para ello podemos ir a File / New File o bien pulsar cmd+N. El prefijo que vamos a utilizar para las clases es AR, por lo que el nombre que en nuestro caso vamos a darle es ARPersonModel.

    Si nos diera algún error debemos asegurarnos de importar el framework en la clase.

    2.- En el fichero ARPersonModel.h vamos a crear las siguientes propiedades, name y lastName, y un array de tipo RLMArray. Esto nos servirá para crear un RLMArray que contenga objetos de tipo de nuestra clase ARPersonModel. Los RLMArray son subclases de de NSArray y los podremos tratar igual que ellos.

    Como observáis no ponemos tipos de referencia en las propiedades por que Realm los ignora. Esto es así por que Realm tiene su propia sintaxis interna. Desde la propia página de Realm nos recomiendan no asignarle ningún tipo de atributo tipo, nonatomic, atomic, strong, copy, weak,etc.

    3.- Ahora eliminaremos nuestro controlador y crearemos uno nuevo que descienda de UITableViewController para poder realizar instancias de nuestro modelo, crear nuestro “Reino” y comenzar a persistir datos. Vamos a nuestro método viewDidLoad de nuestro UITableViewController y realizamos estas acciones.

    Como veis en el código primero creamos una instancia de nuestro modelo y asignamos los valores a los atributos.

    A continuación creamos una instancia de Realm con el método defaultRealm de la clase RLMRealm. Esto nos crea una BBDD con el nombre default.Realm en el directorio Documents de nuestra aplicación.

    Si queremos modificar la ruta de destino de la BBDD y asignarle un nombre podemos realizarlo de la siguiente manera:

    RLMRealm *realm = [RLMRealm realmWithPath:@“/path/to/name.realm”];

    Un dato importante acerca de RLMRealm es que si llamamos varias veces a la instancia en el mismo thread, se rehusa la instancia que se creó la primera vez. Hay una manera de utilizar Realm en varios threads a la vez y consiste en obtener una instancia de RLMRealm en cada thread.

    En el siguiente paso iniciamos el método beginWriteTransaction. Esto nos permite convertir temporalmente nuestra BBDD en modo escritura ya que por defecto es sólo lectura. Solo puede haber abierta una transición simultáneamente ya que la operación es bloqueante. En el caso de de tener varios thread con instancias de RLMRealm se harán de uno en uno.

    Ahora toca añadir los objetos, en este caso son personas, a nuestro “Reino” para persistir datos en nuestra BBDD.


    Por último solo falta liberar el bloqueo de nuestro Realm y que vuelva a ser de sólo lectura. Esto lo conseguimos concluyendo la transacción con el método commitWriteTransaction.

    Ahora vamos a preaparar nuestro UITableViewController para que muestre los objetos creados.

    Crearemos una nueva propiedad que será nuestro “almacén de personas”.

    Ahora nos falta rellenar los datos de los métodos del delegado para configurar nuestra tabla.

    Deberíamos tener algo similar a lo que aparece en la siguiente imagen.

    Vamos a extraer los objetos de la BBDD creada. Con la siguiente línea tendremos todos los objetos en nuestro array, y lo posicionaremos en el viewDidLoad.

    self.persons = [ARPersonModel allObjects];

    Además, en el método delegado “- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath” deberemos añadir la siguiente línea antes de configurar la celda.

    ARPersonModel *model = self.persons[indexPath.row];

    Si ejecutamos la aplicacón deberían aparecer nuestros objetos guardados.


    8. Borrar objetos ya persistidos

    Lo primero que vamos a realizar es crearnos un método para que guarde datos y no tengamos que estar repitiendo una y otra vez las ordenes que queramos que realice Realm. Como se aprecia el método no devuelve nada y entra por parámetro un objeto de nuestro modelo.

    • Creamos nuestro “Reino” abrimos modo escritura de la BBDD.
    • Añadimos objeto a la BBDD.
    • Cerramos transición.

    Vamos a alimentar a nuestra tabla y a nuestra BBDD con nuevos objetos personas. Para ello vamos a incluir una Navigation Bar a nuestra controlador.

    Para ello abrimos nuestro Storyboard, pulsamos sobre nuestro controlador, y en las pestañas de arriba seleccionamos Editor / Embebed In / Navigation Controller.

    También añadiremos un BarButtomItem en la parte de la derecha para añadir nuestros objetos y otro botón en la izquierda para eliminar nuestros objetos. Esto lo haremos en el método viewDidLoad.

    Nuestro método para el selector debería parecerse a lo que aparece en esta imagen.

    1.-Creamos el nuevo objeto que se va a añadir.
    2.-Persistimos los datos y le pasamos como parámetro a nuestro método saveModel.
    3.-Recargamos la tabla para que aparezcan los nuevos datos.

    Eliminar datos de nuestra tabla.

    Una vez ya completado el método añadir vamos a eliminar modelos tanto de la tabla como de nuestra BBDD.

    La función importante para ello es añadir un método delegado del tableView que nos va a facilitar mucho la vida.

    Este método cuando pulsemos sobre el botón editar que nos aparece a la izquierda nos pondrá todos los datos de la tabla en modo edición para poder eliminarlo con total facilidad. Solo con pulsar a la izquierda eliminaremos el modelo de la posición seleccionada, tanto de la BBDD como de la tabla. También podremos realizar esta acción si deslizamos la celda de derecha a izquierda.

    Como veis lo más básico de Realm es lo que se trata en este tutorial y es muy fácil de utilizar, Realm tiene muchas más funcionalidades que podéis ver en la documentación oficial del equipo.


    9. Conclusión

    Realm es una nueva alternativa para la persistencia de datos en el mundo movile con sus puntos mas fuertes y sus puntos mas débiles. La curva de aprendizaje no es muy elevada, por lo que en este sentido no debemos preocuparnos, pero desde mi punto de vista la madurez de Core Data y su funcionamiento, es la primera opción para mí, aquí ya cada uno es libre de decidir que tecnología utilizar.

    Agradeceros la lectura del tutorial.

    Un saludo.

    Autor: Raúl Pedraza León Desarrollador iOS Autentia Business Solutions S.L “Soporte a Desarrollo”

    Enlace del proyecto a git : https://github.com/rpedrazaAutentia/RealmExample.git

    Viewing all 996 articles
    Browse latest View live