Está en la página 1de 285

101 Groovy Script

que en mi local funcionan


Varios

2019-04-09
Table of Contents
Introducción. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1  

Consideraciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1  

Organización . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2  

Localhost . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2  

Entorno de trabajo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3  

Organización . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4  

Preparación del entorno. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5  

Carga de información y creación de los productos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5  

Carga de información . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5  

Creación de cada producto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5  

Construcción del catálogo. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6  

PDF. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
 

Personalización del pdf . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6  

Json a Pdf . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8  

Script . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
 

Publicación y Schedule . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12  

Convertir PowerPoint to HTML. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16  

Preparación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16  

Parsear el PowerPoint. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16  

RevealJS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 

Conversión con Asciidoctor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17  

Conceptos básicos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21  

Punto y coma, paréntesis y demás . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21  

Declaración de tipos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21  

Comillas simples y dobles. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22  

String vs GString. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22  

Closure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
 

Listas, mapas y otros seres. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23  

Bucles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
 

CliBuilder . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25 

Ejecutar comandos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29  

Comando simple . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29  

Esperar finalización . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29  

Llamadas entre scripts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32  

WithBatch . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35  

De Properties a YML. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37  

Consumo de recursos con DSLs. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40  


Primer intento. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40  

Segundo intento . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40  

Base de datos de DSLs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41  

Empaquetado Scripts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45  

Directorio compartido. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45  

Alojado en SVN/Git . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45  

Servidor HTTP. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46  

Jar. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
 

Publicando Jar. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46  

Estructura directorios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46  

Gradle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
 

Publicando. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48  

"Grapeando" . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48  

Extraer una Table HTML a CSV . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51  

Instalación y primeros pasos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53  

Usando los binarios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53  

Windows. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53  

Maven (y similares) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53  

SdkMan . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53  

Comprobación. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53  

GroovyConsole . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54  

Consola . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55 

Groovy Web Console . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55  

Métodos últiles para trabajar con listas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57  

Contar registros de base de datos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62  

GORM en tus scripts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64  

Dependencias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64  

Domain . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64  

Configuración . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65  

Crear una Biblioteca . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65  

Añadir Libros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66  

Consultar la biblioteca . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67  

Test. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
 

Docker y Groovy (básico) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72  

GroovyShell . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72  

Hello Docker . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72  

Montando volumen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72  

Consumir JSON . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73  

Ejecución programada . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76  
Gitlab . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
 

Planificación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78  

Resultado . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78  

Mantenimiento de un Registry de Docker . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81  

Dependencias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81  

Configuración y argumentos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82  

Autorización . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82  

Iterar repositorios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83  

Obtener Tags . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83  

Report . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
 

Asciidoctor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85  

Limpieza. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86  

Biblioteca (docudocker) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91  

Herramientas que vamos a emplear. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91  

Preparación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92  

Descripción del script . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92  

Dependencias y configuración necesarias. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93  

Actualización de versión.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93  

Subida de nuevos documentos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93  

Creación de la imagen docuDocker . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93  

Tostando imágenes. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97  

Dockerfile . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97  

Generando la imagen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98  

Ejecutando la imagen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98  

De Excel a i18n y viceversa . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101  

Dependencias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102  

Argumentos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102  

De i18n a Excel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103  

De Excel a i18n . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104  

Buscar el fichero de mayor tamaño recursivamente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108  

Buscar en fichero y volcar coincidentes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111  

Maven Inventory . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113  

Resultado . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115
 

Organizar fotografías por fecha . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116  

Eliminar lineas con error . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 120  

Scan and Execute . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122  

A qué dedicas tu tiempo libre (Google Calendar) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125  

Consola Google . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125  

Groogle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125
 
Personalización . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126  

Business . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126
 

Vista. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127
 

Import/Export de Google Sheet a Database . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131  

Consola Google . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 132  

Groogle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 132
 

Dependencias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 132  

Autorización . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133  

Database . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133
 

Argumentos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 134  

Comparando Metadatos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 135  

De Google a Database . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136  

De Database a Google . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137  

Gestionando permisos de Google Drive . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142  

Dependencias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142  

Opciones y login . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142  

dumpFile / dumpAll. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144  

removeFile / removeAll . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 145  

shareFile. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 146
 

Documentación adicional . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 146  

Raffle (Google Sheet + Meetups) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147  

Consola Google . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147  

Groogle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147
 

Leyendo la hoja . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148  

Presentación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148  

Ejemplo. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 149
 

Organizando eventos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 151  

Consola Google . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 151  

Groogle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 151
 

Identificadores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152  

Sheet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152
 

Calendar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 153
 

Crea tu propio DSL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 157  

Caso de uso . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 157 

Exp4j . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 157
 

Ejemplo DSL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 158  

DSL. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 160
 

PlotDSL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 160
 

PlotsDSL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 162
 
De texto a Closure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 163  

JavaFX . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 163
 

Xml a Calendar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 168  

Xml. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 168
 

Consola Google . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 169  

Groogle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 170
 

Eliminando antiguos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171  

Consumiendo XML. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171  

Business . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171
 

OpenDataMadrid . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 175  

Preparación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 175 

Scripts vs Task. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 175  

OpenDataMadrid . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 176  

Dependencias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 178  

Parse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 178
 

Telegram . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 179
 

Ejecución manual. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 180  

Monitorea un servidor (o lo que quieras). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 182  

Caso de Uso . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 182 

Modelo-Vista-Controlador . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 183  

Transiciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 185  

TwitterFX . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 188
 

Caso de Uso . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 188 

Twitter App . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 188 

Dependencias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 188  

Customize. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 189
 

Obtener Top Sales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 189  

Generamos el gráfico de tartas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 190  

A Twitter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 191
 

Cerrar automáticamente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 192  

VisualSheet (Mejora la interface de tus scripts) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 195  

Caso de Uso . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 195 

Dependencias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 195  

Business . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 195
 

Vista. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 196
 

Visualizar Extension de Ficheros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 201  

Calculo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 201
 

Vista. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 202
 

Ejemplo. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 203
 
Configuración dinámica de fichero remoto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 207  

Envío de eventos con RabbitMQ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 211  

RabbitMQ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 211
 

Conexion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 212
 

Monitorizar fichero (producer) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 212  

Recibir eventos (consumer). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 214  

Resultado . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 214
 

Docker. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 215
 

Crear un fichero de texto con todas las operaciones disponibles en el API del INE . . . . . 219  

Servidor web en 1 fichero . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 221  

Leer un fichero desde un host remoto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 224  

Contacts2QRCode . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 227  

Objetivo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 227
 

Excel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 227
 

Dependencias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 228  

Parseo del Excel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 228  

Dump de QRCodes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 229  

generateQRLink . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 229  

GenerateVCard . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 230  

Desmenuzando un Pdf . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 233  

Objetivo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 233
 

Dependencias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 233  

Script . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 233
 

En una linea (más Grappe) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 234  

Importar datos de un excel a una tabla . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 236  

Volcar datos de una tabla a Excel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 242  

Mail . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 245
 

Mail con documentos adjuntos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 248  

Memento en el TAE . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 251  

TAE . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 251
 

Stress . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 253
 

Memoize . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 254
 

Comparativa . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 255  

TweetReport (Persistencia con SQLite) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 256  

Dependencias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 256  

Argumentos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 256  

Modelo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 257
 

Prepare . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 258
 

Update User . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 258 


Report User . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 258
 

Persistencia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 259
 

Buscando claves ssh en BitBucket . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 262  

Rest al rescate . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 262


 

OAuth Consumer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 263  

Dependencias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 263  

Autorización . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 263
 

BitBucket . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 264
 

Listar repositorios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 264  

Inspeccionar un repo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 265  

Report . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 265
 

Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 265
 

Valida tu NIF o SOAP hecho fácil . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 268  

SOAP y Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 269


 

SOAP y Groovy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 269  

Consulta de NIF válido según AEAT . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 270  

Consultar un NIF . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 271  

Consultar hasta 10K NIFs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 272  

Ejecución y certificado . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 274  


101 Groovy Script

Introducción
Jorge Aguilera <jorge.aguilera@puravida-software.com [mailto:jorge.aguilera@puravida-
software.com]> 2017-08-01

Puedes acceder a todo el contenido en formato HTML a través de este


 enlace https://groovy-lang.gitlab.io/101-scripts/

Puedes acceder a todo el contenido en formato eBook a través de este


 enlace 101-groovy-scripts-epub.epub

101 Scripts de Groovy pretende ser una serie de prácticos scripts desarrollados en
groovy y que puedan servir como base para aplicarlos en su día a día. Tú puedes aportar
el tuyo, simplemente tienes que seguir esta guía:

• Debes tener una cuenta en Gitlab (si sabes como hacer un fork desde Github o similar
cuentanoslo)

• Puedes encontrar el repositorio en https://gitlab.com/groovy-lang/101-scripts

• Debes hacer un fork del proyecto a tu espacio (https://docs.gitlab.com/ce/workflow/


forking_workflow.html)

• Cuando tengas tu script y su documentación lista deberás hacer un Merge Request


para su revisión y aceptación

Consideraciones
• Queremos añadir scripts que solucionen un problema específico (real o inventando).
No importa que tu script sea particular a una situación concreta pero seguro que
aporta ideas de cómo atacar situaciones parecidas.

• Tampoco importa que sea fácil o simple. Lo que a tí te parece sencillo a otro le puede
resolver un problema (y así llegamos a 101 antes)

• Debes adjuntar al menos un script y una documentación. Si quieres adjuntar imágenes


perfecto pero no es vital.

• El script debe ser funcional y completo. La idea es que bajo las circunstancias
documentadas el script funcione tal como se explice.

• Hemos creado una serie de categorías pero no es una lista cerrada. Si quieres que tu
script pertenezca a una nueva categoría comentálo y se crea.

• Usamos Asciidoctor como herramienta para documentar. Es imperativo porque es la


herramienta que nos permite publicar en Html, Pdf y ePub sin modificar la
documentación. Sin embargo no tienes porqué usar todas sus funcionalidades.

• Para la parte web usamos el generador de contenidos estático JBake. Hay otros pero a
nosotros nos gusta este. Lo bueno es que ya está montado y no tendrás que pelearte
con él.

|1
101 Groovy Script

Organización
Tu script se debe de alojar en src/jbake/assets/script/categoria donde categoria es una en la
que creas que mejor encaja. Actualmente tenemos basico,bbdd,docker,file ,google,office,
etc

Tu documentación se debe de alojar en src/jbake/content/categoria (atención a los


directorios)

Como normal general el script lo capitalizamos y la documentación no. Si tu


documentación está en ingles es preferible que le añadas el sufijo -en al nombre (por
ejemplo la-docu-en.adoc ). Si está en español no es necesario.

Toda documentación tiene que contener una cabecera similar a:

= Titulo
Nombre <email@email.dominio>
yyyy-mm-dd
:jbake-type: post
:jbake-status: published
:jbake-tags: blog, asciidoc, los tags que quieras
:jbake-category: la categoria a la que pertenece el script
:jbake-script: /scripts/categoria/NombreScript.groovy
:jbake-lang: es (o en si está en inglés. Por defecto se toma español)
:idprefix:
:imagesdir: ../images

Tu documentacion comienza aqui

jbake-category es el atributo que sirve al generador para ubicar tu script en el menu


jbake-script es el atributo que sirve al generador para enlazar tu scritp con la docu jbake-
lang es el atributo que indica en que idioma está la documentación jbake-type indica que
es un documento tipo post (el que usamos) jbake-status indica que el documento está listo
para su publicación (si no deberías usar draft)

Localhost
Si cuentas con Java instalado en tu máquina es posible ejecutar el blog en local y así te
será más fácil revisarlo antes de hacer el Merge Request. Para ello ejecuta simplemente
desde el raiz del proyecto

./gradlew serveApp

y tras bajarse media internet podrás acceder a http://localhost:8081/101-scripts

2|
101 Groovy Script

Entorno de trabajo
Puedes usar desde un simple editor a un IDE sofisticado. Nosotros trabajamos con Intellij
pero probablemente Eclipse te sirva también. Simplemente recuerda que debes reiniciar
la tarea serveApp para que te vuelva a generar el contenido

|3
101 Groovy Script

Unresolved directive in 101-groovy-scripts-pdf.adoc -


include::../../jbake/content/content/about_template.adoc[] <<<< :jbake-script:
/scripts/asciidoctor/Catalogo.groovy = Catálogo de productos con Asciidoctor Miguel Rueda
<miguel.rueda.garcia@gmail.com [mailto:miguel.rueda.garcia@gmail.com]> 2018-01-25

El supuesto que vamos a plantear en este documento es la creación de un catálogo de


productos de una tienda.

Dicho catálogo consistirá en un Pdf donde mostraremos, para cada artículo de interés, la
descripción, el precio y una imagen del producto.

Para la generación de este catálogo nos vamos a basar en Asciidoctor


[http://asciidoctor.org/]. Para una consulta rápida sobre esta herramienta
 puedes consultar el siguiente tutorial [http://jorge-aguilera.gitlab.io/
tutoasciidoc/], el cual es una guía básica pero completa sobre esta materia.

En este caso vamos a utilizar imágenes en nuestro local pero podemos


apuntar a links donde tengamos nuestras imágenes . En nuestro ejemplo
 el id de cada item corresponderá con el nombre de imagen a usar para el
artículo.

El schema de nuestra base de datos será algo muy simple, consistente en


 una única tabla con el id, descripción, precio de cada artículo.

Organización
Nuestro script necesitará conectarse a una base de datos de donde obtendrá la
información de cada artículo, y para cada uno de ellos generará una "página" del Pdf. Para
ello usaremos un fichero plantilla común a todos, aunque utilizar uno diferente en base a
la categoría, precio, etc de cada artículo es una modificación trivial.

product.tpl

.Ref: ${sku}
----
Descripcion: ${description}

Referencia ${sku}, ${price}


----

image::${sku}.png[${description}]

4|
101 Groovy Script

catalog.tpl

= Catalogo de Productos
Miguel Rueda <miguel.rueda.garcia@gmail.com>
:idprefix:
:icons: font
:imagesdir: ./images

Este es el catálogo de nuestros productos a día de hoy. En el puede encontrar las referencias,
junto con precios e imágenes, de cada uno de ellos.

Preparación del entorno

Unresolved directive in /builds/groovy-lang/101-scripts/src/jbake/content/asciidoctor/catalogo.adoc -


include::{sourcedir}{jbake-script}[tags=prepararEntorno]

Para empezar a trabajar lo primero es cargar las plantillas anteriormente mencionadas


(las del producto: product.tpl y la del catálogo general: catalog.tpl), al igual que la query
para la base de datos junto con la condición que requiera el cliente.

El siguiente paso será limpiar todos los posibles ficheros con extensión adoc que podamos
tener en la ruta de ejecución de nuestro script

Por último inicializaremos nuestra variable engine que nos servirá para tratar las
plantillas.

Carga de información y creación de los productos


A continuación pasamos a explicar como cargamos de la base de datos los productos y
como posteriormente generamos partiendo de un fichero base nuestros productos.

Carga de información

Para ello utilizaremos la función generateProducts() la cual se conecta a la base de datos


que le indiquemos, ejecutará la query anteriormente definida y por cada producto llamará
al método createProductAdoc encargado del hacer el .adoc del producto que recibirá por
parámetro:

generateProducts

Unresolved directive in /builds/groovy-lang/101-scripts/src/jbake/content/asciidoctor/catalogo.adoc -


include::{sourcedir}{jbake-script}[tags=generateProducts]

Creación de cada producto

Mediante el engine previamente inicializado con la plantilla correspondiente


conseguiremos generar un fichero para cada artículo donde se sustituyan las variables de

|5
101 Groovy Script

la plantilla con los valores del producto en cuestión;

createProductAdoc

Unresolved directive in /builds/groovy-lang/101-scripts/src/jbake/content/asciidoctor/catalogo.adoc -


include::{sourcedir}{jbake-script}[tags=create_adoc]

Construcción del catálogo.


Para la construcción del catálogo en sí usaremos la misma técnica que para cada
producto, utlizando esta vez una plantilla catalog.tpl De esta forma podremos
parametrizar el aspecto inicial del catálogo.

Sobre el fichero generado iremos añadiendo directivas asciidoctor para incluir los adoc
generados para cada artículo tal como se detalla en el apartado anterior. El resultado final
será el fichero catalogo.adoc con tantos includes como artículos hemos obtenido en la
consulta:

generateCatalog

Unresolved directive in /builds/groovy-lang/101-scripts/src/jbake/content/asciidoctor/catalogo.adoc -


include::{sourcedir}{jbake-script}[tags=create_catalog]

PDF
Por último nos resta invocar a Asciidoctor para que tomando como base los ficheros
generados (catalogo y sus includes) nos genere un Pdf catalogo.pdf

cretatePDF

Unresolved directive in /builds/groovy-lang/101-scripts/src/jbake/content/asciidoctor/catalogo.adoc -


include::{sourcedir}{jbake-script}[tags=create_pdf]

① Creamos los atributos que contendrá nuestro fichero asciidoctor. En los cuales
podemos indicar el tipo de documento si tendrá o no tabla de contenidos…

② Indicamos que opciones para crear nuestro catálogo entre ellas backend('pdf') ya que
es el formato que deseamos obtener

③ Conversión del adoc a pdf

Personalización del pdf

Si quisiéremos personalizar aún más nuestro pdf podemos crear un "tema" para nuestro
pdf y con ello aumentar las características del mismo. Para ello tenemos que crear un
fichero con la extensión yml en el que podemos incluir el tipo de fuente, el tamaño,
imágenes de fondo y un largo de etcétera de caraterísticas. Vamos a ver un ejemplo de
tema:

6|
101 Groovy Script

title_page: ①
  align: left
base:
  font_family: Times-Roman ②
  font_size: 12 ③

① Indicamos que el titulo de nuestra imagen está alineado a la izquierda

② La fuente de nuestro texto es Times-Roman

③ El tamaño de letra de es 12

Para que Asciidoctor utilize este tema simplemente hay que indicarlo en los atributos a la
hora de su invocación:

  asciidoctor = Factory.create();
  attributes = AttributesBuilder.attributes().attribute("pdf-style", "tema.yml").
  docType('book').
  tableOfContents(true).
  sectionNumbers(true).
  get()

|7
101 Groovy Script

Json a Pdf
Jorge Aguilera <jorge.aguilera@puravida-software.com [mailto:jorge.aguilera@puravida-
software.com]> 2018-08-13

"Commit es la conferencia donde nos reunimos a discutir diferentes


perspectivas en la forma de hacer y gestionar software. Ven con
nosotros para vivir dos días compartiendo y aprendiendo todo lo que
tiene la tecnología, y por la oportunidad de romper con la rutina y
experimentar algo nuevo."

— CommitConf

Commit es una conferencia anual que cuenta con un agenda de charlas y talleres muy
extensa. Este año (2018) son dos días con 12 tracks simulatáneos. Puedes consultar la
agenda en https://www.koliseo.com/events/commit-2018/r4p/5630471824211968/agenda

Aunque la página es "responsibe" a veces se hace dificil elegir entre tantas charlas por lo
que vamos a desarrollar un pequeño script que consumirá la propia agenda en formato
JSON y lo transformará en un PDF agrupando las charlas por rangos horarios además de
mostrar un listado de speakers al final del documento con las charlas que dará cada uno

En resumen vamos a consumir una estructura de datos pensada para un tipo de


presentación y la vamos a transformar en otro formato.

Para ello vamos a utilizar:

• http-builder-ng para consumir el JSON

• asciidoctor para generar el Pdf

Script
El script en sí es muy sencillo:

8|
101 Groovy Script

Cargar dependencias

@Grab(group='io.github.http-builder-ng', module='http-builder-ng-apache', version='1.0.3')

@Grab(group='org.asciidoctor', module='asciidoctorj', version='1.5.6')


@Grab(group='org.asciidoctor', module='asciidoctorj-pdf', version='1.5.0-alpha.16')
@Grab(group='org.jruby', module='jruby-complete', version='9.1.15.0')

import groovyx.net.http.*
import static groovyx.net.http.HttpBuilder.configure
import static groovyx.net.http.ContentTypes.JSON
import static groovy.json.JsonOutput.prettyPrint
import static groovy.json.JsonOutput.toJson

import org.asciidoctor.OptionsBuilder
import org.asciidoctor.AttributesBuilder
import org.asciidoctor.SafeMode
import org.asciidoctor.Asciidoctor.Factory

Recuperar agenda

def http = configure {


  request.uri = 'https://www.koliseo.com/'
  request.contentType = JSON[0]
  request.accept = JSON[0]
}.get{
  request.uri.path='/events/commit-2018/r4p/5630471824211968/agenda'
}

|9
101 Groovy Script

Transformar

days =[:]
speackers =[:]

http.days.each{ day ->


  def slots = []
  day.tracks.each{ track ->
  track.slots.each{ slot ->
  if( ['TALK'].contains(slot.contents?.type) ){
  def talk = [
  title: slot.contents.title,
  day: "$day.name",
  when: "$slot.start $slot.end",
  slot: slot.id,
  authors: slot.contents.authors.collect{ it.name }.join(',')
  ]
  def d = days[day.name] ?: [:]
  def w = d[talk.when] ?: []

  w.add talk
  d[talk.when]= w
  days[day.name]=d

  slot.contents.authors.each{ author ->


  def speacker = speackers[author.name] ?: []
  speacker.add talk
  speackers[author.name] = speacker
  }
  }
  }
  }
}

La transformación simplemente itera entre los days del JSON y por cada uno itera por los
tracks y de estos por los 'slots`. Para cada slot del tipo TALK crea un mapa con los datos
que nos interesa de la charla, como puede ser el titulo y los ponentes.

Como cada charla la pueden dar varios ponentes, para cada una de ellas los buscamos y
creamos un elemento en el mapa de speakers con la información que nos interese del
mismo.

10 |
101 Groovy Script

Asciidoctor

file = new File("commit2018.adoc")


file.write "= Commit 2018\n"
file << "Agenda\n\n\n"
file << ":chapter-label:\n"
file << "\n"

days.sort().each{ day ->


  file << "== $day.key\n"
  day.value.sort{it.key}.each{
  file << "=== $it.key\n"
  it.value.each{
  file << "- $it.title ($it.authors)\n\n"
  }
  }
}

file << "== Speackers\n\n"

speackers.sort{it.key}.each{
  file << "=== $it.key\n"
  it.value.each{
  file << "- $it.title\n"
  file << " $it.day $it.when \n\n"
  }
  file << "\n"
}

asciidoctor = Factory.create();
attributes = AttributesBuilder.attributes().
  docType('book').
  tableOfContents(true).
  sectionNumbers(false).
  sourceHighlighter("coderay").
  get()

options = OptionsBuilder.options().
  backend('pdf').
  attributes(attributes).
  safe(SafeMode.UNSAFE).
  get()

asciidoctor.convertFile(new File("commit2018.adoc"), options)

Para generar el PDF vamos a generar un fichero .adoc (texto plano) utilizando la sintáxis
de Asciidoctor (título, subtitulo, etc) e invocaremos al conversor utilizando las APIs que
nos proporciona.

| 11
101 Groovy Script

Publicación y Schedule
Una vez que tenemos nuestro conversor listo nos gustaría poder compartir el documento
por lo que vamos a utilizar la capacidad de Gitlab de servir contenido estático generado
por nuestro proyecto.

Con alguna pequeña modificación también podrías usar otros productos


 como Github, Bitbucket etc

Para ello crearemos un proyecto en nuestra cuenta de Gitlab y añadiremos además del
script un fichero .gitlab-ci.yml

..gitlab-ci.yml

commit2018:
 image: groovy
 stage: build
 script:
 - groovy commit2018.groovy
 - mkdir build
 - mv commit2018.pdf build
 artifacts:
  paths:
  - build

pages:
 stage: deploy
 script:
 - mkdir public
 - cp -R build/* public
 artifacts:
  paths:
  - public

Mediante este fichero Gitlab será capaz de ejecutar tu script en sus servidores (utilizará la
imagen Docker groovy que especificamos en el job commit2018) y si todo va bien lo
publicará en un servidor de contenido estático

Puedes ver el resultado final en este enlace https://jorge-aguilera.gitlab.io/commit2018.pdf

Como la agenda puede sufrir modificaciones utilizaremos la funcion Schedule de Gitlab


que nos permite programar de forma recurrente nuestros jobs de tal forma que podremos
actualizar diariamente (por ejemplo) el documento.

12 |
101 Groovy Script

Script

//tag::dependencies[]
@Grab(group='io.github.http-builder-ng', module='http-builder-ng-apache', version='1.0.3')

@Grab(group='org.asciidoctor', module='asciidoctorj', version='1.5.6')


@Grab(group='org.asciidoctor', module='asciidoctorj-pdf', version='1.5.0-alpha.16')
@Grab(group='org.jruby', module='jruby-complete', version='9.1.15.0')

import groovyx.net.http.*
import static groovyx.net.http.HttpBuilder.configure
import static groovyx.net.http.ContentTypes.JSON
import static groovy.json.JsonOutput.prettyPrint
import static groovy.json.JsonOutput.toJson

import org.asciidoctor.OptionsBuilder
import org.asciidoctor.AttributesBuilder
import org.asciidoctor.SafeMode
import org.asciidoctor.Asciidoctor.Factory
//end::dependencies[]

//tag::consumir[]
def http = configure {
  request.uri = 'https://www.koliseo.com/'
  request.contentType = JSON[0]
  request.accept = JSON[0]
}.get{
  request.uri.path='/events/commit-2018/r4p/5630471824211968/agenda'
}
//end::consumir[]

//tag::transformar[]
days =[:]
speackers =[:]

http.days.each{ day ->


  def slots = []
  day.tracks.each{ track ->
  track.slots.each{ slot ->
  if( ['TALK'].contains(slot.contents?.type) ){
  def talk = [
  title: slot.contents.title,
  day: "$day.name",
  when: "$slot.start $slot.end",
  slot: slot.id,
  authors: slot.contents.authors.collect{ it.name }.join(',')
  ]
  def d = days[day.name] ?: [:]
  def w = d[talk.when] ?: []

  w.add talk
  d[talk.when]= w
  days[day.name]=d

  slot.contents.authors.each{ author ->

| 13
101 Groovy Script

  def speacker = speackers[author.name] ?: []


  speacker.add talk
  speackers[author.name] = speacker
  }
  }
  }
  }
}
//end::transformar[]

println "talks:"
days.each{ d->
  d.each{
  println it
  }
}
println "speackers:"
speackers.each{
  println it
}

//tag::asciidoctor[]
file = new File("commit2018.adoc")
file.write "= Commit 2018\n"
file << "Agenda\n\n\n"
file << ":chapter-label:\n"
file << "\n"

days.sort().each{ day ->


  file << "== $day.key\n"
  day.value.sort{it.key}.each{
  file << "=== $it.key\n"
  it.value.each{
  file << "- $it.title ($it.authors)\n\n"
  }
  }
}

file << "== Speackers\n\n"

speackers.sort{it.key}.each{
  file << "=== $it.key\n"
  it.value.each{
  file << "- $it.title\n"
  file << " $it.day $it.when \n\n"
  }
  file << "\n"
}

asciidoctor = Factory.create();
attributes = AttributesBuilder.attributes().
  docType('book').
  tableOfContents(true).
  sectionNumbers(false).
  sourceHighlighter("coderay").
  get()

14 |
101 Groovy Script

options = OptionsBuilder.options().
  backend('pdf').
  attributes(attributes).
  safe(SafeMode.UNSAFE).
  get()

asciidoctor.convertFile(new File("commit2018.adoc"), options)


//end::asciidoctor[]

| 15
101 Groovy Script

Convertir PowerPoint to HTML


Miguel Rueda <miguel.rueda.garcia@gmail.com [mailto:miguel.rueda.garcia@gmail.com]>
2018-04-29

Supongamos que por causas del destino hasta ahora has tenido que trabajar con el
formato .pptx (PowerPoint) ya sea para la creación de una presentación propia, para la
empresa donde trabajas, etc pero ahora quieres compartirla en internet y todos sabemos
que el formato powerpoint NO es el indicado, sino que te gustaría usar HTML para que se
pudiera ver en cualquier navedor. Para ello vamos a convertir tu presentación a HTML
usando el framework de presentaciones RevealJS.

En este script vamos a poder leer cada una de las diapositivas del documento y con la
ayuda de una plantilla defininida crear un fichero .adoc que nos permitirá crear la nueva
presentación utilizando el backend RevealJs de Asciidoctor.

Preparación
Estos son los plugins que vamos a necesitar:

Grapes

@Grapes([
  @Grab(group='org.apache.poi', module='poi', version='3.17' ),
  @Grab(group='org.apache.poi', module='poi-ooxml', version='3.17' ),
  @Grab(group='org.asciidoctor', module='asciidoctorj', version='1.5.6' ),
  @Grab(group='org.jruby', module='jruby-complete', version='9.1.15.0')
])

Parsear el PowerPoint
XMLSlideShow nos va permitir acceder a la presentación pasando como argumento un
objeto tipo FileInputStream que contiene nuestra presentación en formato .pptx.

El siguiente paso es recorrer cada una de las diapositivas extrayendo el texto y la imagen
(si la tiene) de cada una de las diapositivas. Si nuestro script encuentra un objeto de tipo
XSLFPictureShape será una imagen y creará un png con ella.

Una vez ha terminado de parsear la diapositiva incrementará el fichero slide.adoc con la


información obtenida de la misma creando un nuevo apartado asciidoctor

16 |
101 Groovy Script

File file = new File("slide.adoc")


XMLSlideShow ppt = new XMLSlideShow(new FileInputStream('titulo.pptx'))

ppt.getSlides().each{slide->
  String text = ""
  String image=""
  slide.getShapes().each{shape->
  if (shape instanceof XSLFPictureShape ){
  if (shape.getPictureData()){
  new File(shape.getPictureData().getFileName()) << shape.getPictureData().getData()
  image = "image:${shape.getPictureData().getFileName()}[]"
  }
  }else{
  if (shape.text)
  text = shape.text
  }
  }

  file << """


== ${text}
${slide_c.image}
"""
}

RevealJS
Una vez creado el fichero .adoc necesitamos convertirlo en formato html con la ayuda del
backend reveal.js y de asciidoctor-reveal.js

Como ambos deben encontrarse descargados usaremos el método dumpRevealJS que se


encargará de descargar estos proyectos en los subdiretorios adecuados:

void dumpRevealJS(){
  ["git", "clone", "https://github.com/hakimel/reveal.js.git"].execute()
  ["git", "clone", "https://github.com/asciidoctor/asciidoctor-reveal.js.git"].execute()
}

Conversión con Asciidoctor


Por último sólo queda la conversión con la ayuda de Asciidoctor y Reveal.js para lo cual
usaremos un OptionsBuilder al que vamos a indicar donde se encuentran las "templates"
que nos hemos bajado previamente. Así mismo vamos a indicar que usaremos como
backend el revealjs.

Si todo ha ido bien, el script nos habrá creado un slide.html con nuestra presentación.

| 17
101 Groovy Script

void createSlides(){
  asciidoctor = Factory.create()
  options = OptionsBuilder.options().
  templateDirs(new File('./asciidoctor-reveal.js/','templates')).
  backend('revealjs').
  inPlace(true).
  safe(SafeMode.UNSAFE).
  get()

  asciidoctor.convertFile(new File("slide.adoc"), options)


}

18 |
101 Groovy Script

Script

//tag::grapes[]
@Grapes([
  @Grab(group='org.apache.poi', module='poi', version='3.17' ),
  @Grab(group='org.apache.poi', module='poi-ooxml', version='3.17' ),
  @Grab(group='org.asciidoctor', module='asciidoctorj', version='1.5.6' ),
  @Grab(group='org.jruby', module='jruby-complete', version='9.1.15.0')
])
//end::grapes[]

import org.apache.poi.xslf.usermodel.*
import org.apache.poi.xslf.usermodel.XMLSlideShow

import org.asciidoctor.SafeMode
import org.asciidoctor.OptionsBuilder
import org.asciidoctor.Asciidoctor.Factory

//tag::create[]
void createSlides(){
  asciidoctor = Factory.create()
  options = OptionsBuilder.options().
  templateDirs(new File('./asciidoctor-reveal.js/','templates')).
  backend('revealjs').
  inPlace(true).
  safe(SafeMode.UNSAFE).
  get()

  asciidoctor.convertFile(new File("slide.adoc"), options)


}
//end::create[]

//tag::load[]
void dumpRevealJS(){
  ["git", "clone", "https://github.com/hakimel/reveal.js.git"].execute()
  ["git", "clone", "https://github.com/asciidoctor/asciidoctor-reveal.js.git"].execute()
}
//end::load[]

// limpiamos si hubiera restos anteriores


new File('.').eachFileMatch(~/slide.adoc/) { file ->
  file.delete()
}

//tag::each[]
File file = new File("slide.adoc")
XMLSlideShow ppt = new XMLSlideShow(new FileInputStream('titulo.pptx'))

ppt.getSlides().each{slide->
  String text = ""
  String image=""
  slide.getShapes().each{shape->
  if (shape instanceof XSLFPictureShape ){
  if (shape.getPictureData()){
  new File(shape.getPictureData().getFileName()) << shape.getPictureData().getData()
  image = "image:${shape.getPictureData().getFileName()}[]"
  }
  }else{

| 19
101 Groovy Script

  if (shape.text)
  text = shape.text
  }
  }

  file << """


== ${text}
${slide_c.image}
"""
}
//end::each[]

dumpRevealJS()
createSlides()
println "Creado!!!"

20 |
101 Groovy Script

Conceptos básicos
Jorge Aguilera <jorge.aguilera@puravida-software.com [mailto:jorge.aguilera@puravida-
software.com]> 2017-08-20

Groovy es muy parecido a Java, de hecho puedes usar código Java y prácticamente será
válido en Groovy. Al igual que en Java, puedes declarar clases, definir herencia, interfaces,
etc pero además Groovy aporta algunas funcionalidades y clases muy útiles que le hacen
menos "verbose" que Java.

Una de estas funcionalidades es el poder crear ficheros Scripts que pueden ser ejecutados
directamente por Groovy sin necesidad de pasar por todo el proceso de compilación lo
cual le hace muy útil por ejemplo para crear utilidades orientadas a los administradores
de sistemas.

En este post vamos a crear un script básico donde poder ver alguna de las características
que le hacen especial.

Punto y coma, paréntesis y demás


Sí, el famoso asunto del punto y coma es lo primero que se comenta cuando empiezas con
Groovy. Efectivamente Groovy no necesita que termines las sentencias en ";" pues es
capaz de deducirlo por la sintaxis y el contexto. De todas formas si quieres ponerlo
tampoco le molesta.

Cuando usamos una única función en una línea tampoco es necesario el uso de paréntesis
con lo que ganamos en legibilidad

println("no necesito un punto y coma al final")

println "Yo tampoco, ni los parentesis"

Declaración de tipos
La siguiente batalla que afrontarás será la declaración de tipos o no (tipado estático vs
tipado dinámico).

Groovy admite ambos tipos de declaración por lo que puedes declarar tus variables y
funciones indicando el tipo de dato, pero disponer de una declaración dinámica de tipos
puede hacer tu código mucho más versatil y legible.

Cuando no nos importe el tipo de dato de una variable (o retorno de una función)
usaremos def de tal forma que Groovy buscará en tiempo de ejecución si ese objeto
implementa los métodos que indiquemos

| 21
101 Groovy Script

String myString

def unObjeto

unObjeto = "soy un string"

println unObjeto

unObjeto = 10

println unObjeto

Comillas simples y dobles


Podemos declarar cadenas usando tanto la comilla simple como la comilla doble, de tal
forma que podemos incluir una de ellas en la otra.

Además si queremos usar un String multilinea podemos hacerlo mediante el uso de tres
comillas dobles evitando el uso de caracteres de escape

println "Necesito una comilla simple ' "


println 'Necesito una comilla doble " '
println """Necesito muchas lineas
donde poder ver los retornos de carro
sin tanto lio como concatenando String """

String vs GString
Groovy añade la clase GString que es muy similar a String. De hecho en los ejemplos
anteriores cuando usabamos la cadena doble en realidad estabamos instanciando un
GString.

La utilidad que tiene esta clase es que nos permite insertar código en la cadena que será
evaluado en el momento de utilizar la variable evitando las tipicas concatenaciones de
cadenas de java:

println "Mira lo que puedo hacer ${2+2} simplemente con el dolar y las llaves"

Closure
Mucho antes de que Java implementará las famosas Lambdas, Groovy proporciona el
concepto de closure

22 |
101 Groovy Script

Una closure es un pedazo de código anónimo, en el sentido de que no se declara como una
función sino que se asigna a una variable. Por lo demás puede recibir parámetros y
devolver un resultado como cualquier función. Podemos invocarla mediante el método
implícito call(argumentos) o incluso pasarla como parámetro

def unaClosure = { param ->


  "${param}".reverse()
}

println unaClosure.call("una cadena")


println unaClosure.call(10)

Listas, mapas y otros seres


Declarar listas y mapas, asi como asignarlos es trivial

def lista = [1, 3, 4, "Una cadena", new Date() ]


List otraLista = [1, 3, 4, "Una cadena", new Date() ]

def mapa = [ clave : "valor", otra_clave: "otro valor"]

Bucles
Además del típico for(int i=0; i<10;i++){ } y sus derivados, Groovy proporciona alguna
forma más de realizar bucles siendo "each", para recorrer todos los elementos de una
lista/map, uno de los más usados

lista.each{ println "it es un objeto de la lista $it"}


mapa.each{ println "it es un Map.Entry $it.key=$it.value" }

Si necesitamos conocer en cada iteración además del elemento, la posición que ocupa en
la lista usaremos "eachWithIndex"

lista.eachWithIndex{ ele, idx -> ①


  println "posicion $idx, elmenento $ele"
}

① La closure recibe dos parámetros

| 23
101 Groovy Script

Script

println("no necesito un punto y coma al final")

println "Yo tampoco, ni los parentesis"

String myString

def unObjeto

unObjeto = "soy un string"

println unObjeto

unObjeto = 10

println unObjeto

println "Necesito una comilla simple ' "


println 'Necesito una comilla doble " '
println """Necesito muchas lineas
donde poder ver los retornos de carro
sin tanto lio como concatenando String """

println "Mira lo que puedo hacer ${2+2} simplemente con el dolar y las llaves"

def unaClosure = { param ->


  "${param}".reverse()
}

println unaClosure.call("una cadena")


println unaClosure.call(10)

def lista = [1, 3, 4, "Una cadena", new Date() ]


List otraLista = [1, 3, 4, "Una cadena", new Date() ]

def mapa = [ clave : "valor", otra_clave: "otro valor"]

lista.each{ println "it es un objeto de la lista $it"}


mapa.each{ println "it es un Map.Entry $it.key=$it.value" }

lista.eachWithIndex{ ele, idx -> ①


  println "posicion $idx, elmenento $ele"
}

24 |
101 Groovy Script

CliBuilder
Miguel Rueda <miguel.rueda.garcia@gmail.com [mailto:miguel.rueda.garcia@gmail.com]>
2017-08-30

Se puede dar la casuística de tener un script que realiza unas determinadas funciones que
queremos parametrizar.

La opción más fácil es usar la variable args ímplicita en el script y que es un array de
String que contiene los parámetros que se pasan tras el nombre del fichero.

Sin embargo cuentas también con CliBuilder, una utilidad de Groovy que te permite
hacer que los argumentos que se le pueden pasar a un script sean más explícitos.

Un script muy básico utilizando esta herramienta podría ser el siguiente:

| 25
101 Groovy Script

def cli = new CliBuilder(usage: 'groovy CliBuilder.groovy -[habcd]')

cli.with { ①
  h(longOpt: 'help', 'Usage Information \n', required: false)
  a(longOpt: 'Option a','Al seleccionar "a" pinta seleccionada -> a ', required: false)
  b(longOpt: 'Option b','Al seleccionar "b" pinta seleccionada -> b ', required: false)
  c(longOpt: 'Option c','Al seleccionar "c" pinta seleccionada -> c ', required: false)
  d(longOpt: 'Option d','Al seleccionar "d" pinta seleccionada -> d ', required: false)
}

def options = cli.parse(args)

if (!options) {
  return
}
if (options.h) { ②
  cli.usage()
  return
}
if (options.a) { ③
  println "------------------------------------------------------------------"
  println "Seleccionada ha sido la 'a'"
  println "------------------------------------------------------------------"
}
if (options.b) {
  println "------------------------------------------------------------------"
  println "Seleccionada ha sido la 'b'"
  println "------------------------------------------------------------------"
}
if (options.c) {
  println "------------------------------------------------------------------"
  println "Seleccionada ha sido la 'c'"
  println "------------------------------------------------------------------"
}
if (options.d) {
  println "------------------------------------------------------------------"
  println "Seleccionada ha sido la 'd'"
  println "------------------------------------------------------------------"
}

① Definimos los parámetros que vamos a necesitar.

② Mostrará la leyenda de nuestro comando.

③ Esta parte se ejecutará al mandar como parametro -a.

Si por ejemplo llamamos a nuestro script pasando el parámtro -h obtendremos la leyenda


de nuestro comando:

26 |
101 Groovy Script

groovy clibuilder_ebook.groovy -h
usage: clibuilder_ebook.groovy -[habcd]
 -a,--Option a Al seleccionar "a" pinta seleccionada -> a
 -b,--Option b Al seleccionar "b" pinta seleccionada -> b
 -c,--Option c Al seleccionar "c" pinta seleccionada -> c
 -d,--Option d Al seleccionar "d" pinta seleccionada -> d
 -h,--help Usage Information

| 27
101 Groovy Script

Script

def cli = new CliBuilder(usage: 'groovy CliBuilder.groovy -[habcd]')

cli.with { ①
  h(longOpt: 'help', 'Usage Information \n', required: false)
  a(longOpt: 'Option a','Al seleccionar "a" pinta seleccionada -> a ', required: false)
  b(longOpt: 'Option b','Al seleccionar "b" pinta seleccionada -> b ', required: false)
  c(longOpt: 'Option c','Al seleccionar "c" pinta seleccionada -> c ', required: false)
  d(longOpt: 'Option d','Al seleccionar "d" pinta seleccionada -> d ', required: false)
}

def options = cli.parse(args)

if (!options) {
  return
}
if (options.h) { ②
  cli.usage()
  return
}
if (options.a) { ③
  println "------------------------------------------------------------------"
  println "Seleccionada ha sido la 'a'"
  println "------------------------------------------------------------------"
}
if (options.b) {
  println "------------------------------------------------------------------"
  println "Seleccionada ha sido la 'b'"
  println "------------------------------------------------------------------"
}
if (options.c) {
  println "------------------------------------------------------------------"
  println "Seleccionada ha sido la 'c'"
  println "------------------------------------------------------------------"
}
if (options.d) {
  println "------------------------------------------------------------------"
  println "Seleccionada ha sido la 'd'"
  println "------------------------------------------------------------------"
}

28 |
101 Groovy Script

Ejecutar comandos
Miguel Rueda <miguel.rueda.garcia@gmail.com [mailto:miguel.rueda.garcia@gmail.com]>
2017-08-31

Es muy común en el día a día que tengamos que ejecutar comandos de shell de forma
repetida y en función de la respuesta del mismo optar por realizar acciones, como por
ejemplo ejecutar un comando u otro, etc. Estos comandos van desde un simple listado de
ficheros ( dir ), copiar ficheros ( cp, copy ) a otros más elaborados.

Así mismo numerosas veces debemos ejecutar esos comandos de forma repetida (una vez
por cada directorio, por cada fichero, etc) e incluso condicional (si la invocación de este
comando ha ido bien realizar estas acciones y si no estas otras). Para ello solemos recurrir
a ficheros por lotes ( .bat en Windows, .sh en Linux ) donde podemos tratar los problemas
comentados anteriormente.

En este entorno, Groovy nos ofrece poder ejecutar comandos con la ayuda del método
.execute() de la clase String y tratar la salida de este como si fuera una cadena, utilizando
toda la potencia del lenguaje.

Comando simple
Supongamos que en un sistema *NIX quisieramos realizar un listado de los ficheros de un
directorio y mostrar la salida en mayúsculas. Nuestro script sería:

String resultado = "ls -lt ".execute().text


println resultado.toUpperCase()

Como podemos observar, simplemente tenemos que construir un String con el comando y
llamar a su método execute() Este método nos devuelve un objeto que entro otras cosas
nos ofrece la salida del comando como una cadena mediante la property text que
podemos asignar a una variable

Esperar finalización
Si lo que necesitamos es lanzar un comando y esperar a que termine para lanzar de nuevo
otro comando u otra acción se puede realizar de la siguiente manera:

| 29
101 Groovy Script

def resultado = new StringBuilder() ①


def error = new StringBuilder()

def comando = "ls -lt".execute() ②


comando.consumeProcessOutput(resultado, error) ③
comando.waitForOrKill(1000) ④

if (!error.toString().equals("")) ⑤
  println "Error al ejecutar el comando"
else{
  println "Ejecutado correctamente"
  println resultado ⑥

① Definimos la variables donde volcaremos el resultado de nuestro execute.

② Ejecutamos el comando.

③ Obtenemos la salida del comando resultado de su ejecución.

④ Establecemos un time out a nuestro comando.

⑤ En error obtendremos la salida en caso de que falle nuestro comando y en caso de


funcionar correctamente nuestro valor se guardará en resultado.

⑥ El resultado es correcto podemos continuar.

30 |
101 Groovy Script

Script

def resultado = new StringBuilder() ①


def error = new StringBuilder()

def comando = "ls -lt".execute() ②


comando.consumeProcessOutput(resultado, error) ③
comando.waitForOrKill(1000) ④

if (!error.toString().equals("")) ⑤
  println "Error al ejecutar el comando"
else{
  println "Ejecutado correctamente"
  println resultado ⑥

| 31
101 Groovy Script

Llamadas entre scripts


Miguel Rueda <miguel.rueda.garcia@gmail.com [mailto:miguel.rueda.garcia@gmail.com]>
2017-08-31

El caso más fácil de explicar es cuando tenemos varios scripts en los cuales hay una
función/acción que se repite en cada uno de ellos y cada vez que creamos un nuevo script
debemos incluir. Está acción repetitiva puede ir desde la conexión a una base de datos,
exportar a excel o simplemente contener los datos de conexión a la base de datos.

A continuación vamos a crear un script llamado Configuration.groovy el cual contendrá


toda la infomación necesaria para conectarnos a una base de datos de nuestro entorno.
Esta configuración podrá ser llamada desde cualquier de nuestros scripts, por lo tanto de
esta manera evitaremos tener que copiar y pegar esa parte de código para como en este
caso a una base de datos de nuestro contexto.

Configuration.groovy

import Sql

class Configuration {

  String user = "user" ①


  String passwd = "passwd" ②
  String server = "localhost" ③
  String database = "test" ④
  String url = "jdbc:mysql://$server:3306/$database?jdbcCompliantTruncation=false" ⑤

  def instanceMysql(){ ⑥
  return Sql.newInstance( "jdbc:mysql://$server:3306/$database?jdbcCompliantTruncation=false",
"$user", "$passwd", "com.mysql.jdbc.Driver")
  }
  def instanceMysql(my_user,my_passwd,my_server,my_database){ ⑦
  return Sql.newInstance(
"jdbc:mysql://$my_server:3306/$my_database?jdbcCompliantTruncation=false", "$my_user", "$my_passwd",
"com.mysql.jdbc.Driver")
  }
}

① Método que nos devuelve usuario para conectarnos.

② Método que nos devuelve la contraseña.

③ Método que nos devuelve el servidor donde nos vamos conectar.

④ Método que nos devuelve la base de datos.

⑤ Método que nos devuelve la url para conectarnos.

⑥ Método que nos devuelve una instancia mysql con los datos ya definidos.

⑦ Método que nos devuelve una instancia mysql con los datos enviados por parámetro.

Para obtener una instancia podemos realizarlo de la siguiente manera:

32 |
101 Groovy Script

Script.groovy

def sql = Configuration.instanceMysql()

O con los datos de conexión enviados por parámetro:

OtroScript.groovy

def my_user = "user",


def my_passwd = "passwd",
def my_server ="localhost"
def database = "test"
def my_sql = Configuration.instanceMysql(my_user,my_passwd,my_server,my_database)

| 33
101 Groovy Script

Script

import Sql

class Configuration {

  String user = "user" ①


  String passwd = "passwd" ②
  String server = "localhost" ③
  String database = "test" ④
  String url = "jdbc:mysql://$server:3306/$database?jdbcCompliantTruncation=false" ⑤

  def instanceMysql(){ ⑥
  return Sql.newInstance( "jdbc:mysql://$server:3306/$database?jdbcCompliantTruncation=false",
"$user", "$passwd", "com.mysql.jdbc.Driver")
  }
  def instanceMysql(my_user,my_passwd,my_server,my_database){ ⑦
  return Sql.newInstance(
"jdbc:mysql://$my_server:3306/$my_database?jdbcCompliantTruncation=false", "$my_user", "$my_passwd",
"com.mysql.jdbc.Driver")
  }
}

34 |
101 Groovy Script

WithBatch
Miguel Rueda <miguel.rueda.garcia@gmail.com [mailto:miguel.rueda.garcia@gmail.com]>
2017-08-31

A veces nos surge que tenemos que extraer los datos de una base de datos e insertarlas en
otra base de datos (o tabla). Si simplemente fuera una extracción y una carga tu motor de
datos probablemente incluya alguna herramienta import/export, pero si tienes que
realizar alguna transformación en los registros la cosa ya se complica.

Además realizar un insert por cada registro probablemente no sea la forma más óptima
de cargar los datos pues cada insert realiza una transacción con su consiguiente coste.

Mediante este script vamos a leer los datos de una tabla, realizar una transformación a un
campo e insertar los registros en modo batch

Fijate que el número de registros a incluir en cada batch se puede indicar de una forma
simple mediante un argumento en la llamada withBatch

@Grab('mysql:mysql-connector-java:5.1.6')①
@GrabConfig(systemClassLoader=true)
import groovy.sql.Sql

def sql_orig = Sql.newInstance( "jdbc:mysql://localhost:3306/origen?jdbcCompliantTruncation=false", "user",


"password", "com.mysql.jdbc.Driver")②
def sql_dest = Sql.newInstance( "jdbc:mysql://localhost:3306/destino?jdbcCompliantTruncation=false", "user",
"password", "com.mysql.jdbc.Driver")

batchSize=20

sql_dest.withBatch( batchSize, "insert into TABLE_DESTINO(a,b,c) values(?,?,?)"){ ps-> ③

  sql_orig.eachRow"select a,b,c from TABLE_ORIGEN",{ row -> ④

  row.a = row.a.toUpperCase().reverse() ⑤

  ps.addBatch(row) ⑥
  }
}

① Usamos Grape para cargar las dependencias

② Definimos la conexión con las bases de datos.

③ Preparamos un insert en modo batch de batchSize elementos

④ Leemos la tabla origen. Podemos usar sort, where, etc

⑤ Hacemos nuestra transformacion de negocio, por ejemplo, pasar a mayuscula y


revertir la cadena

⑥ Vamos insertando en el batch. Cada batchSize elementos la closure los volcara en la


bbdd

⑦ Al finalizar la lectura todas las closures realizan el cerrado de recursos

| 35
101 Groovy Script

Script

@Grab('mysql:mysql-connector-java:5.1.6')①
@GrabConfig(systemClassLoader=true)
import groovy.sql.Sql

def sql_orig = Sql.newInstance( "jdbc:mysql://localhost:3306/origen?jdbcCompliantTruncation=false", "user",


"password", "com.mysql.jdbc.Driver")②
def sql_dest = Sql.newInstance( "jdbc:mysql://localhost:3306/destino?jdbcCompliantTruncation=false", "user",
"password", "com.mysql.jdbc.Driver")

batchSize=20

sql_dest.withBatch( batchSize, "insert into TABLE_DESTINO(a,b,c) values(?,?,?)"){ ps-> ③

  sql_orig.eachRow"select a,b,c from TABLE_ORIGEN",{ row -> ④

  row.a = row.a.toUpperCase().reverse() ⑤

  ps.addBatch(row) ⑥
  }
}

36 |
101 Groovy Script

De Properties a YML
Jorge Aguilera <jorge.aguilera@puravida-software.com [mailto:jorge.aguilera@puravida-
software.com]> 2018-03-07

Cuando tu script es sencillo y requiere unos pocos parámetros de configuración


probablemente te sea suficiente con interpretar la linea de comando, usando por ejemplo
CliBuilder . Sin embargo cuando la configuración se vuelve más compleja esta opción no
resulte cómoda y la opción más obvia es utilizar un fichero properties de Java donde cada
línea es un clave=valor.

Aunque es una opción válida y muy usada resulta muy oscura y propensa al error. En este
post vamos a ver lo sencillo que es utilizar un formato más amigable para el humano y
que permite expresar mucho mejor configuraciones "de niveles" llamado YAML
https://es.wikipedia.org/wiki/YAML

Mediante este formato podemos escribir no sólo pares de clave,valor sino expresar arrays,
mapas, etc Por ejemplo una configuración típica para conexiones a base de datos puede
ser como la siguiente:

# config jerarquizada
dataSources:
  development:
  url: jdbc:mysql:localhost://desa
  username: user
  password: pwd
  production:
  url: jdbc:mysql:dataserver://prod
  username: sasdfoi123k
  password: asfd9.dslsd0

# array de [username,password]
logins:
  - username: pp
  password: PP

  - username: otro
  password: pazzz

En este ejemplo simplemente leemos un fichero de texto y si no está bien formado el


parseador generará una excepción. Una vez cargado el fichero podemos navegar a través
del mapa usando las características de groovy:

• comprobar si una clave existe con el operador '?'

• recorrer un array con each o eachWithIndex

| 37
101 Groovy Script

@Grab('org.yaml:snakeyaml:1.17')
import org.yaml.snakeyaml.Yaml

Yaml parser = new Yaml()

config = parser.load( new File('config_script.yml').text )

println config.doesntExists ?: "doesnExists doesn't exists"

println config.dataSources?.development?.url

println config.dataSources?.production?.url

config.logins.eachWithIndex{ user, idx->


  println "index $idx:"
  println "$user.username = $user.password"
}

38 |
101 Groovy Script

Script

@Grab('org.yaml:snakeyaml:1.17')
import org.yaml.snakeyaml.Yaml

Yaml parser = new Yaml()

config = parser.load( new File('config_script.yml').text )

println config.doesntExists ?: "doesnExists doesn't exists"

println config.dataSources?.development?.url

println config.dataSources?.production?.url

config.logins.eachWithIndex{ user, idx->


  println "index $idx:"
  println "$user.username = $user.password"
}

| 39
101 Groovy Script

Consumo de recursos con DSLs


Jorge Aguilera <jorge.aguilera@puravida-software.com [mailto:jorge.aguilera@puravida-
software.com]> 2018-08-26

Hace unos días surgió en un conversación si había comprobado el consumo de recursos


en una aplicación que hiciera un uso intensivo de DSL (Domain Specific Language)
porque mi interlocutor estaba seguro que había perdidas de memoria incluso reportadas
y sin solución. Así que me puse manos a la obra para comprobarlo

Primer intento
El primer intento de comprobarlo fue mediante este simple script

(0..100000).each{idx->

  new GroovyShell().parse("println 0")

  if( idx%100 == 0) { System.gc(); sleep 100}


}

y utilizando jconsole comprobé alarmado que era verdad: El consumo de memoria


crecía sin parar.

Entonces me fijé en un pequeño detalle: el número de clases cargadas también crecía!!

Efectivamente: cada vez que invocamos a parse Groovy compila el texto y genera una
clase nueva que es cargada y no se libera porque … no es una instancia de un objeto, es
código!!.

Segundo intento
El segundo intento fue entonces parsear una sóla vez el script y mantener su referencia:

40 |
101 Groovy Script

dsl = new GroovyShell().parse("println 0")

void executeScript(){
  dsl.run()
}

(0..100000).each{idx->

  executeScript()

  if( idx%100 == 0) { System.gc(); sleep 100}


}

Sin embargo, aunque en menor medida, seguía teniendo el mismo problema de no liberar
recursos … hasta que aumenté a 1 segundo el sleep y entonces empecé a comprobar que
el consumo de recursos fluctuaba pero en un rango estable.

Base de datos de DSLs


Si nuestra aplicación va a tener que ejecutar miles de veces diferentes scripts/dsls y no
tenemos en cuenta esta situación nos encontraremos con que al cabo del tiempo nuestra
aplicación habrá consumido todos los recursos y tendremos problemas. Así pues una
posible solución es mantener un repositorio de scripts donde nuestra responsabilidad sea
buscar si el código fuente ya ha sido compilado y utilizar el Script asociado

En este pequeño ejemplo implementamos esta idea:

Creamos al inicio una lista de posibles Scripts a ejecutar y en un Map asociamos cada
String con su Script de tal forma que cuando queremos ejecutar uno de ellos, lo buscamos
en este Map.

| 41
101 Groovy Script

dsls = [

"println new Date()",

"println 1",

"""
println new Random().with {(1..9).collect {(('a'..'z')).join()[
nextInt((('a'..'z')).join().length())]}.join()}
"""

database = [:]
dsls.each{
  database[it] = new GroovyShell().parse(it)
}

void executeDSL( int idx ){


  database[ dsls[idx] ].run()
}

// wait to jconsole
sleep 1000*10

// run a lot of times


(0..100000).each{

  executeDSL( (it % dsls.size()) )

  if( (it % 1000) == 0) {


  sleep 2000
  println "liberando "
  System.gc()
  sleep 2000
  }
}

Utilizando jconsole podemos comprobar que el consumo de recursos se mantiene estable:

42 |
101 Groovy Script

Lógicamente estos scripts son muy simples y no son parametrizables por lo que queda
como ejercicio para el lector implementar una posible solución más completa

| 43
101 Groovy Script

Script

dsls = [

"println new Date()",

"println 1",

"""
println new Random().with {(1..9).collect {(('a'..'z')).join()[
nextInt((('a'..'z')).join().length())]}.join()}
"""

database = [:]
dsls.each{
  database[it] = new GroovyShell().parse(it)
}

void executeDSL( int idx ){


  database[ dsls[idx] ].run()
}

// wait to jconsole
sleep 1000*10

// run a lot of times


(0..100000).each{

  executeDSL( (it % dsls.size()) )

  if( (it % 1000) == 0) {


  sleep 2000
  println "liberando "
  System.gc()
  sleep 2000
  }
}

44 |
101 Groovy Script

Empaquetado Scripts
Jorge Aguilera <jorge.aguilera@puravida-software.com [mailto:jorge.aguilera@puravida-
software.com]> 2017-11-14

En este artículo vamos a tratar cómo podemos agrupar nuestros scripts en un jar de tal
forma que sean accesibles al resto del equipo (o del mundo) de una forma cómoda y
sencilla.

Como puedes ver, en el resto de artículos tratamos scripts sueltos que cumplen una
función (buscar ficheros, transformar datos, invocar servicios web, etc) pero puede darse
la situación que todos, o parte de ellos, sean de utilidad para ciertos usuarios en diferentes
situaciones y queremos que puedan utilizarlos bien sea ejecutándolos directamente o
bien como base para otros scripts.

Así por ejemplo podríamos tener las siguientes situaciones:

• Scripts de utilidad directa

• Scripts base destinados a ser personalizados (extendidos) por otros

• Scripts con lógica de negocio que nos interesa ocultar.

• Scripts que dependen de otros para su ejecución correcta

• etc

Directorio compartido
La primera solución que se nos presenta para poder reutilizar nuestros scripts es
ubicarlos en una carpeta compartida, bien sea en una carpeta de red por ejemplo SAMBA
o bien en una carpeta local de una máquina a la que podemos acceder.

La ventaja de esta solución obviamente es la sencillez con el añadido que si dicha carpeta
se encuentra versionada (SVN, Git, etc) es muy fácil tenerla actualizada

Obviamente una desventaja de estos scripts es que estarán restringuidos a ejecutarse en


el entorno de esta máquina. Es decir si por ejemplo tenemos un script que escanea un
directorio buscando un fichero determinado para trabajar con él sólo tendría sentido
buscarlo en esta máquina y no en la nuestra por ejemplo.

Alojado en SVN/Git
Otra opción es disponer de un SCM (source control manager) tipo subversion SVN, git o
similar donde los usuarios puedan clonarse el proyecto en sus máquinas y ejecutarlos
desde ellas.

Así simplemente mediante comandos de de actualización como por ejemplo git pull
refrescaríamos el directorio local con la última versión y podríamos ejecutar el comando.

| 45
101 Groovy Script

Sin embargo, a parte de que necesitamos instalar el control de versiones en la máquina


donde queramos ejecutar los scripts, tendríamos que revisar que hacemos el pull de
forma habitual etc

Servidor HTTP
Gracias a la capacidad de Groovy de poder ejecutar scripts en URL remotas podemos
hacer que los scripts se alojen en un servidor web (Apache, Nginx, o similar) y que los
usuarios los invoquen vía http

groovy http://groovy-lang.gitlab.io/101-scripts/scripts/office/ExtractPdf.groovy
https://www.boe.es/boe/dias/2017/09/21/pdfs/BOE-B-2017-54046.pdf

Como podemos ver en este ejemplo sólo necesitamos tener Groovy instalado y ejecutar
scripts que residen en un servidor web a la vez que le pasamos argumentos

Jar
Utilizando la capacidad de Groovy de poder ejecutar scripts contenidos en un zip/jar
podemos agrupar nuestros ficheros con un simple comando:

java cvf mis_scripts.jar *.groovy ①


groovy jar:file:mis_scripts.jar'!'/FindFile.groovy ②

① Empaquetamos nuestros scripts en un jar (lo que viene siendo un zip)

② Mediante esa URI conseguimos ubicar nuestro script dentro del jar y así poder
ejecutarlo

Publicando Jar
Si disponemos de un servidor Maven (o una cuenta en MavenCentral, Bintray, etc)
podremos así mismo publicar este jar para poder usarlo mediante @Grape en otros
scripts.

En este apartado vamos a explicar cómo preparar un proyecto Gradle que nos permita
generar el Jar y publicarlo en Bintray para que el resto de nuestros scripts puedan usarlos
(por lo que deberás tener una cuenta creada en Bintray)

Estructura directorios

Establece una estructura de directorios como el de la imagen. Nuestros scripts se


ubicarán en el directorio resources

46 |
101 Groovy Script

+-------------+
| mis_scripts |
+-------------+
  |
  | +-----+
  +>| src |
  +-----+
  | +------+
  +--->| main |
  +------+
  | +-----------+
  +--->| resources |
  +-----------+

Gradle

En el directorio raiz mis_scripts copia el siguiente fichero:

build.gradle

buildscript {
  repositories {
  jcenter()
  }
  dependencies {
  classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.7.3'
  }
}

apply plugin: 'groovy'


apply plugin: 'java-library-distribution'
apply plugin: 'maven'
apply plugin: 'maven-publish'
apply plugin: 'com.jfrog.bintray'

repositories {
  mavenCentral()
}

dependencies {
  compile 'org.codehaus.groovy:groovy-all:2.4.12'
  testCompile 'junit:junit:4.12'
}

javadoc {
  title = "$project.description $project.version API"
}

task sourceJar(type: Jar, dependsOn: classes) {


  classifier = 'sources'
  from sourceSets.main.allSource
}

task javadocJar(type: Jar) {


  classifier = "javadoc"
  from javadoc

| 47
101 Groovy Script

distZip.shouldRunAfter(build)
publishing {
  publications {
  maven(MavenPublication) {
  artifactId 'mis-scripts' ①
  from components.java
  artifact sourceJar {
  classifier "sources"
  }
  }
  }
}
bintray {
  user = System.getenv("BINTRAY_USER") ?: project.hasProperty("bintrayUser") ? project.bintrayUser : '' ②
  key = System.getenv("BINTRAY_KEY") ?: project.hasProperty("bintrayKey") ? project.bintrayKey : ''
  publications = ['maven']
  publish = true
  pkg {
  repo = "mi-repositorio" ③
  userOrg = project.hasProperty('userOrg') ? project.userOrg : 'mi-organization' ④
  name = "mis-scripts"
  desc = "Groovy Scripts"
  websiteUrl = "https://tuwebsite.com"
  licenses = ['Apache-2.0']
  publicDownloadNumbers = true
  version {
  name = project.version
  }
  }
}

① Indica el nombre del artefacto que quieres publicar en Bintray

② Configuración de user/token por variable de entorno, gradle.properties o valor por


defecto

③ El nombre del repositorio donde quieres publicar en Bintray (tienes que haberlo
creado antes en Bintray)

④ El nombre de tu organización (tienes que haberlo creado antes en Bintray)

Publicando

Para empaquetar y publicar tu artefacto simplemente ejecuta:

gradle bintrayUpload

Si todo va bien, se habrá creado tu jar con los scripts embebidos en él y se habrá subido a
Bintray

"Grapeando"

A partir de aqui nuestro artefacto es accesible mediante cualquier gestor de dependencias


como puede ser Maven o Gradle y por supuesto Grape por lo que podremos crear nuevos

48 |
101 Groovy Script

scripts que dependan de nuestro artefacto simplemente poniendo las "coordenadas"


(repositorio, organización, artefacto y versión) oportunas en el script

| 49
101 Groovy Script

Script

println "Hola mundo"

50 |
101 Groovy Script

Extraer una Table HTML a CSV


Jorge Aguilera <jorge.aguilera@puravida-software.com [mailto:jorge.aguilera@puravida-
software.com]> 2018-06-24

Este sencillo script parsea una página HTML de Internet que contenga una tabla (en
nuestro caso identificada por un atributo id pero es fácil adaptarlo a otros casos de uso) y
la volcará a un fichero en formato csv

@Grab('org.ccil.cowan.tagsoup:tagsoup:1.2.1')
import org.ccil.cowan.tagsoup.Parser

new File(args[1]).withWriter('UTF-8') { writer ->


  def slurper = new XmlSlurper(new Parser())
  def url = args[0]
  def htmlParser = slurper.parse(url)
  htmlParser.'**'.find {it.@id==args[3] }.tr.each{
  writer.writeLine it.td.collect{"$it".trim()}.join(';')
  }
}

Básicamente el script solicita tres argumentos:

• URL a parsear

• Nombre del fichero a generar

• Identificador de la tabla a descargar

Una vez descargada la página y parseada, buscará el elemento que coincida con el id
proporcionado y recorrerá todos sus elementos tr

Para cada elemento tr que encuentre generará una única línea concatenando todos los
campos td que contenga

| 51
101 Groovy Script

Script

@Grab('org.ccil.cowan.tagsoup:tagsoup:1.2.1')
import org.ccil.cowan.tagsoup.Parser

new File(args[1]).withWriter('UTF-8') { writer ->


  def slurper = new XmlSlurper(new Parser())
  def url = args[0]
  def htmlParser = slurper.parse(url)
  htmlParser.'**'.find {it.@id==args[3] }.tr.each{
  writer.writeLine it.td.collect{"$it".trim()}.join(';')
  }
}

52 |
101 Groovy Script

Instalación y primeros pasos


Miguel Rueda <miguel.rueda.garcia@gmail.com [mailto:miguel.rueda.garcia@gmail.com]>
2017-08-02

Groovy es un lenguaje de programación dinámico para la máquina virtual de java (Java


Virtual Machine). Esto quiere decir que el código que genera puede ser ejecutado por el
Runtime de Java y por ello se requiere tener instalado Java 6+ (recomendado Java 8 y muy
pronto Java 9)

Usando los binarios


Simplemente hay que descargarse los binarios desde la página oficial, descomprimirlo en
un directorio a nuestro antojo y ajustar las variables de entorno GROOVY_HOME y PATH

Windows
Existe un instalador de Windows basado en NSIS que nos permitirá instalar y desinstalar
Groovy en nuestra máquina

Maven (y similares)
Es posible también embeber Groovy en nuestros proyectos utilizando los artefactos:

<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy</artifactId>
<version>2.4.12</version>

SdkMan
Para incorporar la herramienta groovy recomandamos instalar SdkMan (
http://sdkman.io/). Este software es muy práctico tanto para instalar como para manejar
las diferentes versiones de las herramientas instaladas.

Con el sdkman en nuestro sistema realizar desde la consola.

sdk install groovy

Comprobación
Una vez finalizado el comando de instalación pasaremos a comprobar que tenemos el
groovy preparado.

| 53
101 Groovy Script

groovy -version

GroovyConsole

Ya podemos lanzar nuestra primera consola de groovy

groovyConsole

En pocos segundos se abrirá nuestra consola de groovy donde podemos empezar a


trabajar con los primeros scripts

Para realizar una prueba sencilla podemos ejecutar nuestro primer "Hola Mundo":

HolaMundo.groovy

println "Hola mundo"

Mediante el menú de GroovyConsole podemos ejecutar el script y visualizar el resultado


en el panel inferior.

54 |
101 Groovy Script

Consola

La consola de groovy es muy útil cuando estamos desarrollando nuestro script porque nos
permite ejecutarlo, corregirlo y volver a ejecutarlo en un mismo sitio. Sin embargo
también podemos usar nuestro editor de texto preferido (que no es el Word) como puede
ser Notepad, Notepad++, gedit, vi, emacs, etc y ejecutarlo desde consola (o embebido en un
bash por ejemplo)

groovy HolaMundo.groovy

Groovy Web Console


Por último, también puedes probar a utilizar la consola on-line
https://groovyconsole.appspot.com/ la cual te permitirá ejecutar algunos scripts sencillos
(además de compartirlos de forma pública)

| 55
101 Groovy Script

Script

println "Hola mundo"

56 |
101 Groovy Script

Métodos últiles para trabajar con listas


Miguel Rueda <miguel.rueda.garcia@gmail.com [mailto:miguel.rueda.garcia@gmail.com]>
2017-11-29

Tanto si son tus primero pasos con groovy como si llevas tiempo disfrutando con él seguro
que has podido trabajar con arrays y has podido ver el gran potencial que poseen.

Estos objetos los puedes encontrar diariamente en tu desarrollo de diferentes maneras


pueden ser: los datos que devuelve una consulta sql, un criteria, un JSON o un simple
fichero de configuración fuera de tu aplicaciones que contiene una lista con parámetros
de configuración… Es decir, te va tocar sí o sí trabajar con ellas por lo tanto es siempre útil
conocer métodos simples para trabajar con ellas de la manera más simple y elegante
posible.

A continuación vamos a ver algunos métodos prácticos y básicos para trabajar con listas:

Recorrer una lista con hilos.


Para ello debemos utilizar el plugin:

@Grab(group='org.codehaus.gpars', module='gpars', version='1.0.0')

Una vez realizado será tan sencillo como:

  withPool(10) {
  lista.eachParallel{l->
  println l
  }
  }

Con withPool indicamos que queremos que se ejecute en hilos, en este caso del 10 en 10 y
con eachParallel que los lance en paralelo.

De dos listas obtener los valores semejantes.


Imaginemos que tenemos dos listas y de las cuales solo no queremos quedar con los
valores iguales para ello tenemos:

  lista_1.intersect(lista_2)

En este método lo que hacemos el simplemente recorrer una de las listas y para cada
mapa buscar en la segunda lista si existe una mapa con el mismo key y de ser así los
guardamos en otra lista donde al final quedarás los objetos con el mismo key.

De dos listas obtener los valores diferentes.

| 57
101 Groovy Script

En este caso queremos que además de diferente key el value del map también sea
diferente. Podíamos emplear el método anterior utilizando simplemente un !row en el if y
en método find incluir el value pero vamos a realizarlo de una manera diferente:

  def commons = lista_1.intersect(lista_2)


  def diff_part1 = lista_1.plus(lista_2)
  diff_part1.removeAll(commons)

El método intersect nos proporciona los elementos iguales (tanto key como value), que
están en ambas listas. Este método no nos serviría para el caso anterior ya que en él
pedíamos que tuvieran sólo la misma key.

Con el método plus lo que conseguimos en crear una nueva lista con los valores de la
original más la lista que pasamos por parámetro.

Y finalmente con removeAll eliminamos la lista de valores comunes entre las dos listas

Ordenar una lista

  def list_sort = list.sort{it."${criterio}"}

En este caso groovy nos ofrece el método sort al cual le pasaremos por parámetro por
cual criterio queremos ordenar y nos devolverá la lista con el formato indicado.

Agrupar una lista

  def list_group = list.groupBy{it."${criterio}"}

Para realizar la agrupación por un criterio dentro de nuestra lista será tan simple como
utilizar el groupby indicando por cual criterio deseamos realizar esta acción.

Eliminar valores de una lista

  def list_rm = list.removeAll {it."${criterio}" == 1}

Al igual que en los otros casos utilizaremos el método en cuestión en este caso removeAll
pasando por parámetro el valor que queremos eliminar.

Obtener valores no repetidos

  def list_unique = list.unique()

Con esta funcionalidad lo que obtenemos es una lista de valores sin duplicidades
obteniendo una array con valores únicos. Para emplearlo simpletemente utilizaremos el
método unique().

58 |
101 Groovy Script

Script

//tag::grab[]
@Grab(group='org.codehaus.gpars', module='gpars', version='1.0.0')
//end::grab[]
@GrabConfig(systemClassLoader=true)

import static groovyx.gpars.GParsPool.withPool

def lista_1 = [
  [id:1,value:5],
  [id:2,value:8],
  [id:3,value:4],
  [id:4,value:1],
  [id:5,value:2]
]
def lista_2 = [
  [id:11,value:5],
  [id:12,value:18],
  [id:13,value:14],
  [id:14,value:11],
  [id:15,value:12]
]
def lista_3 = [
  [id:1,value:15],
  [id:2,value:8],
  [id:3,value:4],
  [id:4,value:1],
  [id:5,value:2]
]
def lista_4 = [
  [id:1,value:1],
  [id:1,value:1],
  [id:2,value:2],
  [id:2,value:2],
  [id:5,value:2]

]
def lista_5 = [
  [id:1,value:1],
  [id:1,value:11],
  [id:2,value:2],
  [id:2,value:21],
  [id:5,value:2]

]
def recorrerListaHilos(lista){
  //tag::withPool[]
  withPool(10) {

| 59
101 Groovy Script

  lista.eachParallel{l->
  println l
  }
  }
  //end::withPool[]
}
def unirValoresIgualesPorKey(lista_1,lista_2){
  //tag::unir[]
  def list_all = []
  lista_1.each{a->
  def row = lista_2.find{it.key == a.key}
  if (row)
  list_all << row
  }
  //end::unir[]
  return list_all
}
def mostrarDiferencias(lista_1, lista_2){
  //tag::diff[]
  def commons = lista_1.intersect(lista_2)
  def diff_part1 = lista_1.plus(lista_2)
  diff_part1.removeAll(commons)
  //end::diff[]
  return diff_part1
}
def unirValoresIguales(lista_1,lista_2){
  //tag::iguales[]
  lista_1.intersect(lista_2)
  //end::iguales[]
  return lista_1.intersect(lista_2)
}
def ordenarPor(list,criterio){
  //tag::sort[]
  def list_sort = list.sort{it."${criterio}"}
  //end::sort[]
  return list_sort
}
def agruparPor(list,criterio){
  //tag::groupby[]
  def list_group = list.groupBy{it."${criterio}"}
  //end::groupby[]
  return list_group
}
def obtenerValoresUnicos(list){
  //tag::unique[]
  def list_unique = list.unique()
  //end::unique[]
  return list_unique
}
def eliminarValores(list,criterio){

60 |
101 Groovy Script

  //tag::remove[]
  def list_rm = list.removeAll {it."${criterio}" == 1}
  //end::remove[]
  return list_rm
}

| 61
101 Groovy Script

Contar registros de base de datos


Jorge Aguilera, jorge.aguilera@puravida-software.com [mailto:jorge.aguilera@puravida-
software.com] 2017-08-26

Supongamos que tenemos una tabla en una base de datos MySQL donde cada servidor de
nuestra red reporta su estado. Digamos que cada servidor realizar un insert/update en la
tabla actualizando un campo lastupdate con la fecha del sistema para poder saber así
cuando fue la última actualización de ese servidor.

Como sysadmin te interesa saber en un momento determinado cúantos servidores se


encuentran caídos desde un momento determinado (desde hace 1 minuto, 1 hora, 1 día …
it’s up to you) para poder determinar si tienes problemas.

Con este script simplemente te conectas a la base de datos, realizas una consulta COUNT y
muestras el resultado por pantalla. Esta salida podrás concatenarla con algún otro
programa/script que te permita reaccionar cuando el número supera un umbral
determinado como enviar una alarma, etc.

@Grab('mysql:mysql-connector-java:5.1.6')①
@GrabConfig(systemClassLoader=true)
import groovy.sql.Sql

sql=Sql.newInstance(
  "jdbc:mysql://localhost/granja", ②
  "user", "password", "com.mysql.jdbc.Driver" ③
)

row = sql.firstRow('SELECT count(*) caidos FROM hosts where lastreport < ?',[new Date()-1]) ④

println row?.caidos ⑤

① Cargamos la dependencia al driver MySQL

② Indicamos el host y la base de datos alojada

③ Indicamos así mismo datos necesarios para la conexion como usuario y password

④ Buscamos el primer registro que cumpla la condicion. Fijate que la query es


parametrizable con ?

⑤ Si obtenemos resultados lo imprimimos por pantalla

Este script muestra cómo conectarnos y buscar un registro mediante una query
parametrizable. Fijate que usamos el caracter ? para indicar donde se deben de sustituir
los parámetros. En este ejemplo usamos el día de ayer como parámetro

62 |
101 Groovy Script

Script

@Grab('mysql:mysql-connector-java:5.1.6')①
@GrabConfig(systemClassLoader=true)
import groovy.sql.Sql

sql=Sql.newInstance(
  "jdbc:mysql://localhost/granja", ②
  "user", "password", "com.mysql.jdbc.Driver" ③
)

row = sql.firstRow('SELECT count(*) caidos FROM hosts where lastreport < ?',[new Date()-1]) ④

println row?.caidos ⑤

| 63
101 Groovy Script

GORM en tus scripts


Jorge Aguilera, jorge.aguilera@puravida-software.com [mailto:jorge.aguilera@puravida-
software.com] 2018-03-30

GORM (Groovy o Grails?) Object Relational Mapping,


http://gorm.grails.org/, es la librería pertenciente al proyecto Grails que
permite el acceso y gestión de bases de datos. En un principio sólo
 contemplaba Hibernate pero con las últimas versiones permite acceder a
bases de datos de grafos como Neo4J, NoSQL como MongoDB, etc
manteniendo la misma sintáxis y funcionalidades

En este artículo vamos a desarrollar un Groovy Script simple donde veremos que
podemos utilizar todas las funcionalidades de GORM (creación y mantenimiento de las
tablas, relaciones entre ellas, persistencia en nuestro modelo de dominio, etc)
desarrollando una pequeña biblioteca con libros.

Dependencias

@GrabConfig(systemClassLoader=true)
@Grab('org.slf4j:slf4j-api:1.7.10')
@Grab('org.xerial:sqlite-jdbc:3.21.0')
@Grab('org.grails:grails-datastore-gorm-hibernate5:6.1.6.RELEASE')
@Grab('com.enigmabridge:hibernate4-sqlite-dialect:0.1.2')

import groovy.sql.Sql
import groovy.transform.ToString
import org.grails.orm.hibernate.HibernateDatastore
import grails.gorm.annotation.Entity

Domain
Nuestros objetos de Dominio constan de Biblioteca y Libro donde el primero contiene una
relación de los segundos

En nuestro script declararemos dos clases @Entity donde indicaremos los atributos y
relaciones entre ellas tal como requiere GORM

64 |
101 Groovy Script

@Entity
@ToString
class Libro {
  String codigo
  String titulo
  static constraints = {
  codigo unique:true
  }
}

@Entity
@ToString
class Biblioteca{
  String nombre
  static hasMany = [ libros : Libro]
}

Simplemente por anotar nuestros objetos de dominio con @Entity, GORM los recubrirá
con toda una gama de funciones y capacidades que nos permitirán persistir estos objetos
así como recuperarlos. Por simplificar este script no utiliza -validators_ ni otras
funcionalidades de GORM.

Configuración
Puesto que nuestro script va a utilizar Hibernate y una persistencia SQLite debemos
configurar dicha librería para ello:

Map getConfiguration(){
  [
  'dataSource.url' :'jdbc:sqlite:example.db',
  'dataSource.drive' :'org.sqlite.JDBC',
  'hibernate.hbm2ddl.auto' : 'update',
  'hibernate.dialect' :'com.enigmabridge.hibernate.dialect.SQLiteDialect'
  ]
}

HibernateDatastore initDatastore(){
  new HibernateDatastore( configuration, Biblioteca, Libro)
}

Simplemente indicamos la url a utilizar, el dialecto y las clases que queremos que
Hibernate gestione (las marcadas como @Entity)

Crear una Biblioteca

| 65
101 Groovy Script

void prepareBiblioteca() {
  Biblioteca.withTransaction {

  Biblioteca.list().each{ it.delete() }

  new Biblioteca(nombre: 'Biblioteca Nacional').save()


  }
}

Mediante este método accederemos a la base de datos y borraremos todas las bibliotecas
que hubiera (probablemente en tu caso no quieras hacer esto. En el nuestro es para
demostrar las funcionalidades de GORM) para posteriormente crear una nueva

Como puedes ver usamos un método withTransaction que NO hemos declarado en


nuestra @Entity Biblioteca pero que GORM le ha añadido.

Así mismo GORM nos ha añadido métodos como:

• list() para recuperar todos los objetos de ese tipo

• delete() para eliminarlo de la base de datos

• count() para saber cuantos tenemos

• save() para persistir un objeto de dominio

• etc

Añadir Libros

void addLibros(){
  Biblioteca.withTransaction{
  Biblioteca b = Biblioteca.first()
  assert b

  b.addToLibros(codigo:'abc', titulo: 'libro 1')

  b.save()
  }
}

Mediante este método vamos a añadir un libro a la primera biblioteca que exista en la
base de datos. Como podemos ver GORM nos ha añadido un método addToLibros a la
Biblioteca de tal forma que añadir un Libro y crear la relación de dependencia entre ellos
nos es transparente

66 |
101 Groovy Script

Consultar la biblioteca
Realizar consultas a la base de datos es realmente sencillo y sobre todo con GORM nos
olvidamos de sentencias SELECT:

void list(){
  Biblioteca.withTransaction{
  assert Biblioteca.count() ①

  def list = Biblioteca.list() ②


  list.each{ Biblioteca b->
  println "Biblioteca $b.nombre ($b.id): "
  b.libros.each{ Libro l-> ③
  println "\t $l.codigo con titulo $l.titulo"
  }

  }
  }

  Biblioteca.withNewSession{
  Biblioteca b = Biblioteca.findByNombre('Biblioteca Nacional') ④
  assert b
  println "Biblioteca encontrada $b"

  List list = Biblioteca.findAllByNombreLike('Bibli%') ⑤


  assert list.size()

  List list2 = Biblioteca.withCriteria{ ⑥


  eq 'nombre', 'Biblioteca Nacional'
  }
  assert list2.size()
  }
}

① similar a select count(*) from biblioteca

② similar a select * from biblioteca . Podemos indicar que en la misma query recupere
el detalle de los libros pasando como argumento fetch:[libros:"join"]

③ similar a select * from libros where biblioteca_id = ?

④ findByXXX donde XXX puede ser cualquier atributo del objeto de dominio

⑤ findAll similar a findBy pero recupera una lista en lugar de un sólo registro

⑥ para querys más complejas podemos utilizar una closure withCriteria

Test

| 67
101 Groovy Script

initDatastore()

prepareBiblioteca()

addLibros()

println '-'.multiply(20)

list()
println '-'.multiply(20)

68 |
101 Groovy Script

Script

//tag::dependencias[]
@GrabConfig(systemClassLoader=true)
@Grab('org.slf4j:slf4j-api:1.7.10')
@Grab('org.xerial:sqlite-jdbc:3.21.0')
@Grab('org.grails:grails-datastore-gorm-hibernate5:6.1.6.RELEASE')
@Grab('com.enigmabridge:hibernate4-sqlite-dialect:0.1.2')

import groovy.sql.Sql
import groovy.transform.ToString
import org.grails.orm.hibernate.HibernateDatastore
import grails.gorm.annotation.Entity
//end::dependencias[]

//tag::domain[]
@Entity
@ToString
class Libro {
  String codigo
  String titulo
  static constraints = {
  codigo unique:true
  }
}

@Entity
@ToString
class Biblioteca{
  String nombre
  static hasMany = [ libros : Libro]
}
//end::domain[]

//tag::config[]
Map getConfiguration(){
  [
  'dataSource.url' :'jdbc:sqlite:example.db',
  'dataSource.drive' :'org.sqlite.JDBC',
  'hibernate.hbm2ddl.auto' : 'update',
  'hibernate.dialect' :'com.enigmabridge.hibernate.dialect.SQLiteDialect'
  ]
}

HibernateDatastore initDatastore(){
  new HibernateDatastore( configuration, Biblioteca, Libro)
}
//end::config[]

//tag::biblio[]
void prepareBiblioteca() {
  Biblioteca.withTransaction {

  Biblioteca.list().each{ it.delete() }

  new Biblioteca(nombre: 'Biblioteca Nacional').save()

| 69
101 Groovy Script

  }
}
//end::biblio[]

//tag::libros[]
void addLibros(){
  Biblioteca.withTransaction{
  Biblioteca b = Biblioteca.first()
  assert b

  b.addToLibros(codigo:'abc', titulo: 'libro 1')

  b.save()
  }
}
//end::libros[]

//tag::list[]
void list(){
  Biblioteca.withTransaction{
  assert Biblioteca.count() ①

  def list = Biblioteca.list() ②


  list.each{ Biblioteca b->
  println "Biblioteca $b.nombre ($b.id): "
  b.libros.each{ Libro l-> ③
  println "\t $l.codigo con titulo $l.titulo"
  }

  }
  }

  Biblioteca.withNewSession{
  Biblioteca b = Biblioteca.findByNombre('Biblioteca Nacional') ④
  assert b
  println "Biblioteca encontrada $b"

  List list = Biblioteca.findAllByNombreLike('Bibli%') ⑤


  assert list.size()

  List list2 = Biblioteca.withCriteria{ ⑥


  eq 'nombre', 'Biblioteca Nacional'
  }
  assert list2.size()
  }
}
//end::list[]

//tag::main[]
initDatastore()

prepareBiblioteca()

addLibros()

println '-'.multiply(20)

70 |
101 Groovy Script

list()
println '-'.multiply(20)
//end::main[]

| 71
101 Groovy Script

Docker y Groovy (básico)


Jorge Aguilera <jorge.aguilera@puravida-software.com [mailto:jorge.aguilera@puravida-
software.com]> 2017-10-09

Al igual que con Java, Groovy cuenta desde hace un tiempo con una imagen oficial
(bueno, en realidad varias, una para cada versión de JDK disponible) lo que hace que
posible que se puedan ejecutar proyectos y scripts en este lenguaje dentro de un
contenedor Docker, crear/extender tus imagenes, compartir volúmenes, etc.

El proyecto se encuentra alojado en https://github.com/groovy/docker-groovy y la(s)


imagen(es) las puedes encontrar en el repo oficial de docker https://hub.docker.com/_/
groovy/

GroovyShell
En su forma más simple el ejecutar un contenedor con cualquiera de estas imágenes nos
iniciará una shell de Groovy (lo cual no tiene mucha utilidad en sí mismo):

docker run --rm -it groovy:latest groovy ①

① Obviamente necesitarás tener instalado Docker en tu sistema (ya tardas) para poder
ejecutar el comando.

Lo interesante es que en ese momento estás ejecutando un contenedor de docker con


groovy instalado y con todas las características de cualquier contenedor: network,
volúmenes, incluirlo en un docker-compose, etc.

Hello Docker
En el siguiente ejemplo vamos a comprobar que efectivamente se ejecuta en un
contenedor mediante un HolaDocker:

docker run --rm -e hola=caracola -it groovy:latest groovy -e "println System.getenv().each{println it}"

Si el comando se ejecuta correctamente deberías ver en consola las variables de entorno


propias del contenedor incluida la que proporcionamos por argumento. Por ejemplo
HOSTNAME indicará el nombre que le ha asignado docker a tu imagen (y que no
encontrarás después porque le hemos indicado con el argumento rm que la elimine al
finalizar)

Montando volumen
Una de las características propias de Docker es permitirte montar un directorio del host

72 |
101 Groovy Script

como si fuera propio del contenedor, de tal forma que este pueda leer/escribir en el
mismo sin importar que una vez finalizado eliminemos el contenedor.

Si aprovechamos esta característica podemos ubicar nuestros scripts en un directorio del


host e indicar al contenedor que los ejecute en su entorno. Para ello nos descargamos el
script /scripts/docker/DockerBasico.groovy en un directorio de nuestra maquina y
ejecutamos desde ese directorio:

docker run --rm -v "$PWD":/home/groovy/scripts -w /home/groovy/scripts groovy:latest groovy


DockerBasico.groovy -a ①

<1>Este script ejecuta diferentes acciones según el parámetro que le pasemos. Con -a
vuelca las variables de sistema

Este script simplemente ejecuta:

if (options.a) {
  println "------------------------------------------------------------------"
  println "Hello"
  System.getenv().each{
  println it
  }
  println "------------------------------------------------------------------"
}

Si todo se ejecuta correctamente obtendrás una salida muy parecida a la del ejemplo
anterior (casi seguro que HOSTNAME no coinciden y la variable hola no aparece)

Consumir JSON
Como un ejemplo más elaborado vamos a realizar una petición a un servicio externo que
nos devolverá un JSON el cual indica de forma aleatoria la URL a una imagen de perros
(Para más información consultar el API https://dog.ceo/dog-api)

Para ello ejecutaremos:

docker run --rm -v "$PWD":/home/groovy/scripts -w /home/groovy/scripts groovy:latest groovy


DockerBasico.groovy -d ①

<1>El parámetro -d ejecuta la acción de recuperar el JSON y descargar la imgen

Este script simplemente ejecuta:

| 73
101 Groovy Script

if( options.d){
  def json = new groovy.json.JsonSlurper().parse(new URL("https://dog.ceo/api/breed/hound/images/random") )
  if(json.status=='success'){
  new File('perrito.jpg').bytes = new URL(json.message).bytes
  }
}

Si el script se ejecuta correctamente deberías tener un fichero nuevo en tu directorio


perrito.jpg que se regenera con una imagen nueva cada vez que ejecutes el script.

74 |
101 Groovy Script

Script

def cli = new CliBuilder(usage: 'groovy DockerBasico.groovy]')

cli.with { ①
  h(longOpt: 'help', 'Usage Information \n', required: false)
  a(longOpt: 'Hello','Al seleccionar "a" te saludara ', required: false)
  d(longOpt: 'Dogs', 'Genera imagenes de perros', required:false)
}

def options = cli.parse(args)

if (!options || options.h) {
  cli.usage
  return
}

//tag::hello[]
if (options.a) {
  println "------------------------------------------------------------------"
  println "Hello"
  System.getenv().each{
  println it
  }
  println "------------------------------------------------------------------"
}
//end::hello[]

//tag::dogs[]
if( options.d){
  def json = new groovy.json.JsonSlurper().parse(new URL("https://dog.ceo/api/breed/hound/images/random") )
  if(json.status=='success'){
  new File('perrito.jpg').bytes = new URL(json.message).bytes
  }
}
//end::dogs[]

| 75
101 Groovy Script

Ejecución programada
Jorge Aguilera <jorge.aguilera@puravida-software.com [mailto:jorge.aguilera@puravida-
software.com]> 2018-03-19

En el post Docker y Groovy (básico) vimos cómo podemos hacer que la imagen oficial de
Groovy para Docker ejecute nuestros scripts en host donde no se encuentre instalado
Groovy. Así mismo esta imagen nos permite aprovechar todas las características de
Docker como montar volúmenes, conexión entre contenedores, etc de tal forma que
nuestros scripts puedan interactuar con otros contenedores y/o sistema anfitrión.

Si pensamos un poco sobre ello vemos que podemos utilizar estas características para
ejecutar de forma programada nuestros scripts incluso desde un servidor remoto, de tal
forma que no tenemos ni tan siquiera que tener instalado Groovy en nuestro equipo.

Para este ejemplo vamos a utilizar un script que se ejecutará de forma recurrente y
que accederá a la información del Ayuntamiento de Madrid [https://datos.madrid.es/
portal/site/egob/menuitem.c05c1f754a33a9fbe4b2e4b284f1a5a0/?
vgnextoid=255e0ff725b93410VgnVCM1000000b205a0aRCRD&
vgnextchannel=374512b9ace9f310VgnVCM100000171f5a0aRCRD&vgnextfmt=default] sobre
incidencias que se están produciendo en ese momento en las vías de esta ciudad.
Esta información viene formateada en XML y, de todas las incidencias, el script
buscará aquellas que no han sido planificadas y las irá tuiteando una a una.

Para la ejecución recurrente aprovecharemos la posibilidad que ofrece Gitlab de


ejecutar Pipelines de forma planificada de tal forma que nuestro código será
ejecutado por un runner cuando así lo determinemos.

Así pues deberás tener:

• cuenta en Gitlab así como crear un repositorio y añadir el script al mismo.

• cuenta en twitter y crear unas credenciales para que tu script pueda tuitear en tu
nombre.

76 |
101 Groovy Script

IncidenciasMadrid.groovy

@Grab(group='org.twitter4j', module='twitter4j-core', version='4.0.6')

import twitter4j.TwitterFactory
import twitter4j.StatusUpdate
import twitter4j.conf.ConfigurationBuilder

tf = TwitterFactory.singleton
if( new File('twitter4j.properties').exists() == false ){ ①
  def env = System.getenv()
  ConfigurationBuilder cb = new ConfigurationBuilder()
  cb.setDebugEnabled(true)
  .setOAuthConsumerKey(env['CONSUMER_KEY'])
  .setOAuthConsumerSecret(env['CONSUMER_SECRET'])
  .setOAuthAccessToken(env['ACCESS_TOKEN'])
  .setOAuthAccessTokenSecret(env['ACCESS_SECRET']);
  tf = new TwitterFactory(cb.build())
}
twitter = tf.instance

body = new URL("http://informo.munimadrid.es/informo/tmadrid/incid_aytomadrid.xml").newReader()


NewDataSet = new XmlSlurper().parse(body) ②
NewDataSet.Incidencias.each{

  if( "$it.incid_prevista" == 'N' && "$it.incid_planificada"=='N' ){

  String tweet="""
@101GroovyScript te informa
Atención, incidencia no prevista
$it.nom_tipo_incidencia:
$it.descripcion
"""

  try{
  twitter.updateStatus tweet ③
  } catch(e){
  println "no se ha enviado"
  }
  }
}

① Si no existe fichero de credenciales de Twitter usamos variables de entorno

② Obtenemos las ultimas incidencias en formato XML filtrando por las no previstas

③ tuiteamos un mensaje con el contenido de cada una.

Gitlab
Para que Gitlab pueda ejecutar nuestro código tenemos que especificarle cómo debe
hacerlo y para ello usaremos un fichero .gitlab-ci.yml que deberá estar en el raiz de
nuestro proyecto. Para más información consulta la documentación oficial
https://about.gitlab.com/features/gitlab-ci-cd/

| 77
101 Groovy Script

gitlab-ci.yml

execute incidencias: ①
 image:
  name: groovy:2.4-jdk8 ②
 only:
  - schedules ③
 stage: build
 script:
  - groovy IncidenciasMadrid.groovy ④

① identificamos nuestro job con un nombre. Podemos tener varios más

② usaremos la imagen oficial de Groovy para ejecutar nuestro script

③ este job sólo se ejecuta de forma planificada por Gitlab

④ comando a ejecutar. Como usamos la imagen Groovy el comando groovy se encuentra


disponible

Planificación
Desde la consola web de Gitlab podemos configurar cuando queremos que se ejecute el
Pipeline mediante una expresión chron (minutos horas dia etc) e incluso especificar qué
rama de nuestro repo queremos utilizar para ello, así como variables de entorno a
utilizar:

Resultado
Como resultado tendremos que cada cierto tiempo Twitter publicará las incidencias
usando nuestra cuenta como en este ejemplo:

78 |
101 Groovy Script

Como puedes ver gracias a la imagen Docker podemos ejecutar nuestros scripts de forma
desatendida y con todas las funcionalidades que ofrece el lenguaje (consumir servicios
REST, SOAP, conexión a servicios externos y/o internos, etc)

| 79
101 Groovy Script

Script

@Grab(group='org.twitter4j', module='twitter4j-core', version='4.0.6')

import twitter4j.TwitterFactory
import twitter4j.StatusUpdate
import twitter4j.conf.ConfigurationBuilder

tf = TwitterFactory.singleton
if( new File('twitter4j.properties').exists() == false ){ ①
  def env = System.getenv()
  ConfigurationBuilder cb = new ConfigurationBuilder()
  cb.setDebugEnabled(true)
  .setOAuthConsumerKey(env['CONSUMER_KEY'])
  .setOAuthConsumerSecret(env['CONSUMER_SECRET'])
  .setOAuthAccessToken(env['ACCESS_TOKEN'])
  .setOAuthAccessTokenSecret(env['ACCESS_SECRET']);
  tf = new TwitterFactory(cb.build())
}
twitter = tf.instance

body = new URL("http://informo.munimadrid.es/informo/tmadrid/incid_aytomadrid.xml").newReader()


NewDataSet = new XmlSlurper().parse(body) ②
NewDataSet.Incidencias.each{

  if( "$it.incid_prevista" == 'N' && "$it.incid_planificada"=='N' ){

  String tweet="""
@101GroovyScript te informa
Atención, incidencia no prevista
$it.nom_tipo_incidencia:
$it.descripcion
"""

  try{
  twitter.updateStatus tweet ③
  } catch(e){
  println "no se ha enviado"
  }
  }
}

80 |
101 Groovy Script

Mantenimiento de un Registry de
Docker
Jorge Aguilera <jorge.aguilera@puravida-software.com [mailto:jorge.aguilera@puravida-
software.com]> 2018-02-10

Si utilizas una cuenta de Docker Hub gratuita no te habrás


preocupado mucho por el tema de cuantas imágenes has subido a tu
repositorio. De hecho probablemente te guste tener todo el historial de las
mismas por si alguien las sigue usando. Sin embargo cuando utilizas un
repositorio privado el asunto cambia porque estos servicios suelen cobrar
por el número de imágenes que tienes alojadas en ellos. Así que si quieres
tener "controlada" la factura te toca hacer revisiones periódicas y eliminar
aquellas imágenes que consideras obsoletas.

Mediante este script vamos a obtener un informe de la situación de todas tus imágenes
alojadas en Docker Hub que podrás automatizar y que te informará de qué imágenes
pueden ser eliminadas en base a un criterio que definas (en nuestro caso vamos a querer
mantener sólo las 4 últimas). Así mismo, si lo quieres, el propio script podrá realizar el
borrado por tí

Dependencias
Docker Hub ofrece un servicio REST que te permite autentificarte y gestionar los
repositorios, imágenes, tags, etc alojados en el espacio del usuario. Para esta gestión REST
vamos a utilizar HttpBuilder-NG (https://http-builder-ng.github.io/http-builder-ng/
asciidoc/html5)

Así mismo para la generación del report vamos a utilizar las capacidades de marcado
incluidas en el propio Groovy usando MarkupBuilder (http://docs.groovy-lang.org/docs/
groovy-latest/html/api/groovy/xml/MarkupBuilder.html)

| 81
101 Groovy Script

@Grapes([
  @Grab(group='org.asciidoctor', module='asciidoctorj', version='1.5.6'),
  @Grab(group='io.github.http-builder-ng', module='http-builder-ng-core', version='1.0.3'),
  @Grab(group='org.slf4j', module='slf4j-simple', version='1.7.25', scope='test')
])
import static groovyx.net.http.HttpBuilder.configure
import static groovyx.net.http.ContentTypes.JSON
import groovy.json.*
import groovy.xml.MarkupBuilder

import org.asciidoctor.OptionsBuilder
import org.asciidoctor.AttributesBuilder
import org.asciidoctor.SafeMode
import org.asciidoctor.Asciidoctor.Factory

Configuración y argumentos

REPORT=false ①
ASCIIDOC=true
DELETE=false ②
TRESHOLD=4 ③

user=args[0] ④
password=args[1] ⑤
namespace=args[2] ⑥

① Si queremos que genere el HTML de report

② Si queremos que borre automaticamente las imagenes obsoletas

③ Número de imágenes a mantener (de más reciente a menos)

④ Primer argumento es el usuario para hacer login

⑤ Segundo argumento es la password para hacer login

⑥ El repositorio que queremos analizar (un usuario puede tener más de uno. Lo usual es
que sea el mismo que el usuario)

Autorización

82 |
101 Groovy Script

token = configure { ①
  request.uri='https://hub.docker.com/'
  request.contentType=JSON[0]
  request.accept=['application/json']
}.post { ②
  request.uri.path='/v2/users/login'
  request.body=[username:user,password:password]
}.token ③

dockerHub = configure { ④
  request.uri='https://hub.docker.com/'
  request.contentType=JSON[0]
  request.accept=['application/json']
  request.headers['Authorization']="JWT $token"
}

① Configuramos la peticion para enviar un json

② Realizamos un GET con un json en el body

③ Si la petición se realiza correctamente podemos trabajar con la respuesta directamente

④ Creamos un configure para utilizar en sucesivas llamadas con el token obtenido

Iterar repositorios

repos=dockerHub.get { ①
  request.uri.path="/v2/repositories/$namespace"
  request.uri.query=[page_size:200]
}.results.collect{ repo-> ②
  [name:repo.name,lastUpdate:Date.parse("yyyy-MM-dd'T'HH:mm:ss",repo.last_updated)]
}.sort{ a,b-> ③
  a.name <=> b.name
}

① Crearemos una lista local de mapas repos con los results obtenidos

② Para cada result extraemos un subconjunto de valores que nos interesan como el
nombre y la última actualización

③ Ordenamos la lista alfabéticamente

Obtener Tags

| 83
101 Groovy Script

repos.each{ repo->
  repo.tags =dockerHub.get { ①
  request.uri.path="/v2/repositories/$namespace/$repo.name/tags"
  request.uri.query=[page_size:200]
  }.results.collect{ tag-> ②
  [id:tag.id,name:tag.name,lastUpdate:Date.parse("yyyy-MM-dd'T'HH:mm:ss",tag.last_updated)]
  }.sort{ a,b-> ③
  b.lastUpdate<=>a.lastUpdate
  }
  if(repo.tags.size()>=TRESHOLD){
  repo.tags.takeRight(repo.tags.size()-TRESHOLD).each{ it.toRemove=true } ④
  }
}

① A cada repo le asignamos una lista de mapas tags con los resultados obtenidos

② Para cada result extraemos un subconjunto de valores que nos interesan como el
nombre y la última actualización

③ Ordenamos la lista de más reciente a más antigüo

④ Si la lista sobrepasa el límite marcado marcamos los más antigüos como candidatos
para ser eliminados

Report
Si así lo indicamos el script creará un HTML a modo de report ( ver ejemplo en este enlace
Docker Report [./../scripts/docker/docker_report.html] ) que puede ser enviado por correo
electrónico por ejemplo. Para generar el report, simplemente usaremos MarkupBuilder
recorriendo la lista de repos y para cada repo la lista de tags. Así mismo para los tags
candidatos a ser eliminados los crearemos en una sección diferente para ayudar a su
identificación (un h3 en este caso)

84 |
101 Groovy Script

  writer=new StringWriter()
  html=new MarkupBuilder(writer)
  html.html {
  head {
  title "Docker Hub Images reporting"
  }
  body(id: "main") {
  h1 id: "namespace", "Repository $namespace"
  repos.each{ repo->
  div {
  h2 "$repo.name (Last update:${repo.lastUpdate.format('yyyy-MM-dd HH:mm')})"
  repo.tags.findAll{!it.toRemove}.each{tag->
  p "$tag.name (Last update:${tag.lastUpdate.format('yyyy-MM-dd HH:mm')})"
  }
  if(repo.tags.find{it.toRemove} )
  h3 "Candidates to remove"
  repo.tags.findAll{it.toRemove}.each{tag->
  p "$tag.name (Last update:${tag.lastUpdate.format('yyyy-MM-dd HH:mm')})"
  }

  }
  }
  }
  }
  new File('docker_report.html').newWriter().withWriter { w -> w << writer.toString() }

Asciidoctor
Otra posibilidad para generar el report es usando Asciidoctor (ver ejemplo en este enlace
Docker Report en Asciidoctor [./../scripts/docker/asciidoc_docker_report.html] ).

Para generar el report, simplemente crearemos un fichero de forma dinámica siguiendo


la sintaxis adecuada recorriendo la lista de repos y para cada repo la lista de tags. Así
mismo para los tags candidatos a ser eliminados los crearemos en una sección diferente
para ayudar a su identificación

| 85
101 Groovy Script

  file = new File("/tmp/asciidoc_docker_report.adoc") ①


  file.newWriter().withPrintWriter { mainWriter ->
  mainWriter.println "= Docker Hub status"
  mainWriter.println "Jorge Aguilera <jorge.aguilera@puravida-software.com>"
  mainWriter.println new Date().format('yyyy-MM-dd')
  mainWriter.println ":icons: font"
  mainWriter.println ":toc: left"
  mainWriter.println ""
  mainWriter.println """
[abstract]
Report status of repository *${namespace}* at *${new Date().format('yyyy-MM-dd HH:mm')}*
  """

  repos.each { repo -> ②


  mainWriter.println "\n== ${repo.name}"
  mainWriter.println "Last update: ${repo.lastUpdate.format('yyyy-MM-dd HH:mm')}"
  mainWriter.println ""

  repo.tags.findAll { !it.toRemove }.each { tag ->


  mainWriter.println "- ${tag.name} ${tag.lastUpdate.format('yyyy-MM-dd HH:mm')}"
  }
  if (repo.tags.find { it.toRemove }) {
  mainWriter.println "\n=== Candidates to remove"
  }
  repo.tags.findAll { it.toRemove }.each { tag ->
  mainWriter.println "- ${tag.name} ${tag.lastUpdate.format('yyyy-MM-dd HH:mm')}"
  }
  }
  }

  asciidoctor = Factory.create();
  attributes = AttributesBuilder.attributes().
  sectionNumbers(true).
  sourceHighlighter("coderay").
  get()
  options = OptionsBuilder.options().
  backend('html').
  attributes(attributes).
  safe(SafeMode.UNSAFE).
  get()
  asciidoctor.convertFile(file, options) ③

① Creamos el fichero y especificamos su cabecera

② Creamos secciones para cada repo

③ Invocamos a Asciidoctor para generar el fichero html

Limpieza
Si queremos que el script realize la limpieza de aquellas imágenes que consideramos
obsoletas simplemente lo configuraremos para ello de tal forma que el script pueda
invocar la(s) llamada(s) REST DELETE oportuna(s).

86 |
101 Groovy Script

  repos.each{ repo->
  repo.tags.findAll{it.toRemove}.each{ tag->
  dockerHub.delete {
  request.uri.path="/v2/repositories/$namespace/$repo.name/tags/$tag.name"
  }
  }
  }

| 87
101 Groovy Script

Script

//tag::dependencies[]
@Grapes([
  @Grab(group='org.asciidoctor', module='asciidoctorj', version='1.5.6'),
  @Grab(group='io.github.http-builder-ng', module='http-builder-ng-core', version='1.0.3'),
  @Grab(group='org.slf4j', module='slf4j-simple', version='1.7.25', scope='test')
])
import static groovyx.net.http.HttpBuilder.configure
import static groovyx.net.http.ContentTypes.JSON
import groovy.json.*
import groovy.xml.MarkupBuilder

import org.asciidoctor.OptionsBuilder
import org.asciidoctor.AttributesBuilder
import org.asciidoctor.SafeMode
import org.asciidoctor.Asciidoctor.Factory
//end::dependencies[]

//tag::config[]
REPORT=false ①
ASCIIDOC=true
DELETE=false ②
TRESHOLD=4 ③

user=args[0] ④
password=args[1] ⑤
namespace=args[2] ⑥
//end::config[]

//tag::auth[]
token = configure { ①
  request.uri='https://hub.docker.com/'
  request.contentType=JSON[0]
  request.accept=['application/json']
}.post { ②
  request.uri.path='/v2/users/login'
  request.body=[username:user,password:password]
}.token ③

dockerHub = configure { ④
  request.uri='https://hub.docker.com/'
  request.contentType=JSON[0]
  request.accept=['application/json']
  request.headers['Authorization']="JWT $token"
}
//end::auth[]

//tag::repos[]
repos=dockerHub.get { ①
  request.uri.path="/v2/repositories/$namespace"
  request.uri.query=[page_size:200]
}.results.collect{ repo-> ②
  [name:repo.name,lastUpdate:Date.parse("yyyy-MM-dd'T'HH:mm:ss",repo.last_updated)]
}.sort{ a,b-> ③
  a.name <=> b.name
}

88 |
101 Groovy Script

//end::repos[]

//tag::tags[]
repos.each{ repo->
  repo.tags =dockerHub.get { ①
  request.uri.path="/v2/repositories/$namespace/$repo.name/tags"
  request.uri.query=[page_size:200]
  }.results.collect{ tag-> ②
  [id:tag.id,name:tag.name,lastUpdate:Date.parse("yyyy-MM-dd'T'HH:mm:ss",tag.last_updated)]
  }.sort{ a,b-> ③
  b.lastUpdate<=>a.lastUpdate
  }
  if(repo.tags.size()>=TRESHOLD){
  repo.tags.takeRight(repo.tags.size()-TRESHOLD).each{ it.toRemove=true } ④
  }
}
//end::tags[]

if( REPORT ){
//tag::report[]
  writer=new StringWriter()
  html=new MarkupBuilder(writer)
  html.html {
  head {
  title "Docker Hub Images reporting"
  }
  body(id: "main") {
  h1 id: "namespace", "Repository $namespace"
  repos.each{ repo->
  div {
  h2 "$repo.name (Last update:${repo.lastUpdate.format('yyyy-MM-dd HH:mm')})"
  repo.tags.findAll{!it.toRemove}.each{tag->
  p "$tag.name (Last update:${tag.lastUpdate.format('yyyy-MM-dd HH:mm')})"
  }
  if(repo.tags.find{it.toRemove} )
  h3 "Candidates to remove"
  repo.tags.findAll{it.toRemove}.each{tag->
  p "$tag.name (Last update:${tag.lastUpdate.format('yyyy-MM-dd HH:mm')})"
  }

  }
  }
  }
  }
  new File('docker_report.html').newWriter().withWriter { w -> w << writer.toString() }
//end::report[]
}

if(ASCIIDOC){
  //tag::asciidoctor[]
  file = new File("/tmp/asciidoc_docker_report.adoc") ①
  file.newWriter().withPrintWriter { mainWriter ->
  mainWriter.println "= Docker Hub status"
  mainWriter.println "Jorge Aguilera <jorge.aguilera@puravida-software.com>"
  mainWriter.println new Date().format('yyyy-MM-dd')
  mainWriter.println ":icons: font"
  mainWriter.println ":toc: left"
  mainWriter.println ""
  mainWriter.println """

| 89
101 Groovy Script

[abstract]
Report status of repository *${namespace}* at *${new Date().format('yyyy-MM-dd HH:mm')}*
  """

  repos.each { repo -> ②


  mainWriter.println "\n== ${repo.name}"
  mainWriter.println "Last update: ${repo.lastUpdate.format('yyyy-MM-dd HH:mm')}"
  mainWriter.println ""

  repo.tags.findAll { !it.toRemove }.each { tag ->


  mainWriter.println "- ${tag.name} ${tag.lastUpdate.format('yyyy-MM-dd HH:mm')}"
  }
  if (repo.tags.find { it.toRemove }) {
  mainWriter.println "\n=== Candidates to remove"
  }
  repo.tags.findAll { it.toRemove }.each { tag ->
  mainWriter.println "- ${tag.name} ${tag.lastUpdate.format('yyyy-MM-dd HH:mm')}"
  }
  }
  }

  asciidoctor = Factory.create();
  attributes = AttributesBuilder.attributes().
  sectionNumbers(true).
  sourceHighlighter("coderay").
  get()
  options = OptionsBuilder.options().
  backend('html').
  attributes(attributes).
  safe(SafeMode.UNSAFE).
  get()
  asciidoctor.convertFile(file, options) ③
  //end::asciidoctor[]
}

if( DELETE ){
//tag::delete[]
  repos.each{ repo->
  repo.tags.findAll{it.toRemove}.each{ tag->
  dockerHub.delete {
  request.uri.path="/v2/repositories/$namespace/$repo.name/tags/$tag.name"
  }
  }
  }
//end::delete[]
}

90 |
101 Groovy Script

Biblioteca (docudocker)
Miguel Rueda <migue.rueda.garcia@gmail.com [mailto:migue.rueda.garcia@gmail.com]> 2018-
03-05

A continuación se describe el caso de uso que vamos a desarrollar: Un cliente nos traslada
la necesidad de crear una biblioteca de documentos, donde colgar manuales o notas
informativas para sus empleados. Esta información cambia de manera mensual, tanto el
contenido como la estructura de directorios. Además debe de correr en cualquier sistema
operativo ya que al trabajar con varios clientes cada uno cuenta con el suyo propio.

Nuestra solución se basará en una imagen Docker con un servidor web de contenido
estático, donde habremos copiado los documentos proporcionados por el cliente
generando un image versionada con esta información.

Así pues nuestro sistema debe de realizar los siguientes pasos:

Un cliente proporciona la nueva documentación para la biblioteca, esta es


actualizada en el servidor preparado para ella (con la función scpDocuments). Con la
información ya en el servidor lanzamos la tareas de gradle (createImage) y con ello
obtenemos la imagen docker con nuestra biblioteca de documentos versionada en el
repositorio en el que estemos trabajando.

Herramientas que vamos a emplear.


• DocuDocker [https://gitlab.com/jorge.aguilera/static-documents]:

| 91
101 Groovy Script

En este enlace @jagedn explica paso a paso como crear una biblioteca con docker y jbake
utilizando el proyecto static-documents.

 https://www.youtube.com/watch?v=5x8i9Ft_29Y (YouTube video)

Dentro de este proyecto podemos encontrar el fichero Dockerfile que contiene la "receta"
para generar nuestra imagen docker de la biblioteca:

FROM httpd:2.4
EXPOSE 80
COPY . /usr/local/apache2/htdocs/

Este Dockerfile parte de la imagen de docker httpd:2.4 y copia el contenido que tenemos
en la ruta actual dentro de su servidor apache. Por lo tanto a la hora de actualizar los
documentos debemos centrarnos en este paso. En el ejemplo sobre el que vamos a
trabajar tiene la estructura: un directorio RRHH (que contiene Convenio_Actual.pdf) y
otro Administración (que contiene Cierre_mensual.pdf)

La herramientas que utiliza este proyecto es gradle y para la creación del entorno web
estático es JBake.

Preparación
Para la creación de nuestra biblioteca debemos ejecutar los siguientes pasos:

• Bajarnos en el servidor que deseamos crear la biblioteca el proyecto static-documents


para posteriormente realizar las tareas de gradle:

git clone https://gitlab.com/jorge.aguilera/static-documents.git

• Con el proyecto bajado debemos copiar dentro del proyecto en el directorio


src/documents/ la estructura de directorios(en nuestro caso RRHH y Administración )
que deseamos encontrarnos en nuestra biblioteca , así como la información dentro de
cada directorio.

• Cuando tengamos recopilada toda la información, ejecutar las tareas de gradle para
generar la imagen docker. Utilizaremos sshoogr [https://github.com/aestasit/sshoogr] ya que
en este caso que vamos a ver la información estará en un servidor remoto.

Descripción del script


A continuación vamos a pasar a describir el script encargado de realizar cada una de las
funciones descritas anteriormente:

92 |
101 Groovy Script

Dependencias y configuración necesarias.

Estas son las que vamos a necesitar:

@Grab('com.aestasit.infrastructure.sshoogr:sshoogr:0.9.25')
@GrabConfig(systemClassLoader=true)
import static com.aestasit.infrastructure.ssh.DefaultSsh.*

options.trustUnknownHosts = true ①

① Desactivación de la comprobación estricta de la clave del host

Actualización de versión.

El primer paso es eliminar del fichero build.gradle del proyecto los campos version y
tagVersion para impedir que en la creación de nuestra imagen tomen estas referencias a
la hora de crear nuestra biblioteca. Nuestro script creará un fichero gradle.properties con
la versión que hemos recibo por parametro y que será el tag de la imagen.

Utilizamos la función updateVersion que recibe por parámetro el servidor donde se


encuentra el proyecto y la ruta del fichero gradle.properties que será actualizado.

  remoteSession("user:password@$server:22"){
  remoteFile("$orig/gradle.properties").text = "version=$version"
  }

Subida de nuevos documentos

Para este paso utilizaremos la función scpDocuments, a la cual le debemos indicar la ruta
donde se encuentra la documentación por parámetro y la ruta en el servidor destino

  remoteSession("user:password@$server:22") {
  scp{
  from { localDir orig}
  into { remoteDir dest}

  }
  }

Creación de la imagen docuDocker


Ya solo nos queda ejecutar las tareas gradle build y pushDockerRegistry (para subirla a
nuestro repositorio). Para ello realizaremos:

| 93
101 Groovy Script

  remoteSession("user:password@$server:22") {
  exec "cd $src; ./gradlew build"
  exec "cd $src; ./gradlew pushDockerRegistry"
  }

En este punto una nueva imagen etiquetada se encuentra en nuestro repositorio y puede
ser descargada por los clientes para ver la última versión de los documentos

94 |
101 Groovy Script

Script

@Grab('com.aestasit.infrastructure.sshoogr:sshoogr:0.9.25')
@GrabConfig(systemClassLoader=true)
import static com.aestasit.infrastructure.ssh.DefaultSsh.*

options.trustUnknownHosts = true

String server = "src_server"

def cli = new CliBuilder(usage: 'groovy CliBuilder.groovy -[husc]')


cli.with { (1)
  h(longOpt: 'help ','Usage Information \n', required: false)
  u(longOpt: 'Actualización ','-u version servidor ruta -> Ejemplo: -u v1 localhost', required:
false)
  s(longOpt: 'Copiado de ficheros','-s servidor origen destino -> Ejemplo: -s localhost /tmp /home/docu',
required: false)
  c(longOpt: 'Crear Imagen ','-c servidor ruta -> Ejemplo: -c ruta', required: false)
}

def options = cli.parse(args)

if (!options) {
  return
}
if (options.h) {
  cli.usage()
  return
}
if (options.u) {
  if (options.arguments().size() < 3) {
  cli.usage()
  return
  }
  updateVersion(options.arguments()[0],options.arguments()[1],options.arguments()[2])
}
if (options.s) {
  if (options.arguments().size() < 3) {
  cli.usage()
  return
  }
  scpDocuments(options.arguments()[0],options.arguments()[1],options.arguments()[2])
}
if (options.c) {
  if (options.arguments().size() < 3) {
  cli.usage()
  return
  }
  createImage(options.arguments()[0],options.arguments()[1])
}
def updateVersion(version,server,orig){
  //tag::version[]
  remoteSession("user:password@$server:22"){
  remoteFile("$orig/gradle.properties").text = "version=$version"
  }
  //end::version[]
}

def createImage(server,src){
  //tag::create[]
  remoteSession("user:password@$server:22") {
  exec "cd $src; ./gradlew build"
  exec "cd $src; ./gradlew pushDockerRegistry"
  }

| 95
101 Groovy Script

  //end::create[]
}

def scpDocuments(server,orig,dest){
  //tag::put_dir[]
  remoteSession("user:password@$server:22") {
  scp{
  from { localDir orig}
  into { remoteDir dest}

  }
  }
  //end::put_dir[]
}

96 |
101 Groovy Script

Tostando imágenes
Jorge Aguilera <jorge.aguilera@puravida-software.com [mailto:jorge.aguilera@puravida-
software.com]> 2017-10-14

En el post Docker y Groovy (básico) vimos cómo podemos hacer que la imagen oficial de
Groovy para Docker ejecute nuestros scripts en host donde no se encuentre instalado
Groovy. Así mismo esta imagen nos permite aprovechar todas las características de
Docker como montar volúmenes, conexión entre contenedores, etc de tal forma que
nuestros scripts puedan interactuar con otros contenedores y/o sistema anfitrión.

En este post lo que vamos a ver es cómo utilizarla como base para crear nuestras propias
imágenes de tal forma que podamos subirlas a nuestro repositorio ( Hub Docker, nexus
privado, Gitlab, Google o cualquier otro en el que tengas cuenta).

Para este ejemplo vamos a utilizar un script muy sencillo cuya función va a ser mostrar
por pantalla el contenido de un fichero que le pasemos por parámetro y esperar 30seg
para volver a repetir la acción (vamos lo que viene siendo a ser un bucle infinito de toda
la vida).

WatchFile.groovy

if( !args.size() ){
  println "necesito un fichero que vigilar"
  return
}

while( true ){
  File f = new File(args[0])
  f.eachLine{ line ->
  println line
  }
  sleep 30*1000
}

INFO
Como puedes ver el script no es lo más importante en este artículo

Dockerfile
Lo primero que vamos a necesitar es un fichero de instrucciones para que Docker pueda
crear nuestra imagen. Este fichero suele tener el nombre de Dockerfile y para nuestro
ejemplo tiene esta pinta:

| 97
101 Groovy Script

FROM groovy:2.4.12-jre8-alpine

COPY WatchFile.groovy /home/groovy/

VOLUME ["/var/watchfile"]

ENTRYPOINT ["groovy", "WatchFile.groovy"]

CMD []

Mediante este fichero simplemente indicamos de qué imagen vamos a extender


(groovy:2.4.12-jre8-alpine), copiamos nuestro script en la imagen e indicamos qué se
debe ejecutar al arrancar ( groovy WatchFile.groovy).

También preparamos nuestra imagen para que si no le proporcionamos parámetros de


ejecución el script lo detecte y nos pueda mostrar alguna ayuda de cómo ejecutarlo, o
realizar una acción por defecto, (CMD [])

Así mismo vamos a montar un volumen particular para esta imagen en /var/watchfile, lo
cual nos permitirá, por ejemplo, montar volúmenes de otros contenedores o del host
anfitrión para que nuestro script pueda "vigilarlo"

Generando la imagen
Desde el directorio donde tengamos estos dos ficheros ejecutaremos:

docker build --rm -t jagedn/watchfile . ①

① jagedn/watchfile será el nombre de nuestra imagen. Fijate que el comando acaba en un


punto

Si todo va bien, en tu máquina existirá ahora una imagen jagedn/watchfile (o el nombre


que le hayas dado)

Para comprobarlo puedes ejecutar

docker images

Ejecutando la imagen
Para ejecutar la imagen simplemente haremos:

docker run --rm -it -v /un/path/cualquiera:/var/watchfile jagedn/watchfile /var/watchfile/mifichero.log ①

98 |
101 Groovy Script

Esto debe ejecutar un proceso que cada 30 segundos nos muestre el contenido de
mifichero.log

Como puedes ver, este ejemplo en sí no realiza nada interesante salvo que muestra los
pasos a realizar para hacer que nuestro script se pueda distribuir y ejecutar en un
entorno dockerizado por lo que es perfectamente integrable en pipelines de continuous
delivery por ejemplo.

Recuerda que para poder subir tu imagen a un repositorio simplemente deberás hacer un
push, por ejemplo:

docker push jagedn/watchfile

| 99
101 Groovy Script

Script

if( !args.size() ){
  println "necesito un fichero que vigilar"
  return
}

while( true ){
  File f = new File(args[0])
  f.eachLine{ line ->
  println line
  }
  sleep 30*1000
}

100 |
101 Groovy Script

De Excel a i18n y viceversa


Jorge Aguilera <jorge.aguilera@puravida-software.com [mailto:jorge.aguilera@puravida-
software.com]> 2018-03-03

La mayoría de las aplicaciones Java utilizan la funcionalidad que ofrece para el manejo de
mensajes internacionalizados el cual se basa en una serie de ficheros properties que
comparten un nombre común más un sufijo que indica el idioma al que corresponden las
traducciones incluidas en el mismo.

Por ejemplo, supongamos que nuestra aplicación va a mostrar por defecto los mensajes en
inglés pero necesitamos poder mostrarlos también en español y francés. Este caso en Java
se corresponde con estos ficheros:

theapp.properties

login=Login
welcome=Welcome

theapp_es.properties

login=Identificacion
welcome=Bienvenido

theapp_fr.properties

login=Identifier
welcome=Bienvenue

Cuando la aplicación crece, el número de mensajes a mostrar suele hacerlo también y nos
vamos centrando en el fichero por defecto hasta tener la mayor cantidad posible de
identificadores. Si en este momento queremos realizar la traducción (o encargarsela a
alguien) nos encontramos con una serie de ficheros incompletos e inconexos y aunque
existen herramientas para facilitar la edición, estas no suelen ser del agrado de quien
tiene que traducirlos.

Mediante este script vamos a poder realizar dos procesos diferentes aunque relacionados:

• Partiendo de un conjunto de ficheros properties de traducciones incompletas


crearemos un fichero Excel donde cada columna corresponderá a un idioma y cada
fila a un elemento a traducir en cada idioma

• Partiendo de un excel con las traducciones completas crearemos un conjunto de


ficheros properties cada uno con las traducciones que le correspondan.

| 101
101 Groovy Script

Vamos a usar Apache POI para ambas situaciones pero para la escritura
 vamos a usar el DSL Groovy Excel Builder de James Kleeh para
demostrar lo fácil que es escribir un excel con Groovy.

Dependencias
De forma general vamos a usar las librerías de Apache POI para leer y escribir el Excel,
pero como el DSL que vamos a usar las incluye en sus dependencias podemos indicar
simplemente este en Grappe:

@Grab('com.jameskleeh:excel-builder:0.4.2')

import org.apache.poi.ss.usermodel.*
import org.apache.poi.hssf.usermodel.*
import org.apache.poi.xssf.usermodel.*
import org.apache.poi.ss.util.*
import org.apache.poi.ss.usermodel.*
import com.jameskleeh.excel.ExcelBuilder

Argumentos
Como hemos dicho el script podrá ejecutar dos acciones diferentes por lo que preparamos
un CliBuilder que nos permita interpretar la línea de comandos y los argumentos
proporcionados por el usuario:

102 |
101 Groovy Script

def cli = new CliBuilder(usage: 'groovy ExcelI18n.groovy -[h] -action generateProperties/generateExcel


filename.xls ')

cli.with { ①
  h(longOpt: 'help', 'Import/Export excel file to i18n properties files.', required: false)
  action('generateProperties generateExcel', required: true, args:1, argName:'action')
}

def options = cli.parse(args)


if (!options){
  return
}
if(options.h || options.arguments().size()==0) {
  cli.usage()
  return
}

if( options.action == 'generateProperties'){ ②


  return generateProperties(options.arguments()[0])
}

if( options.action == 'generateExcel'){ ③


  return generateExcel(options.arguments()[0])
}

cli.usage() ④

① preparamos las opciones disponibles

② de Excel a properties

③ de properties a Excel

④ si no hay acción correcta mostramos el uso

De i18n a Excel
Partiendo de una situación en la que tenemos un conjunto de traducciones incompletas lo
que pretendemos hacer es cargar todos estos ficheros en un Excel organizando por filas y
columnas los códigos y los idiomas respectivamente.

Para determinar los idiomas que queremos manejar en nuestra aplicación hay que fijarse
en que todos ellos siguen el patrón:

filename (_ i18code)? .properties , es decir un nombre de fichero común, un guión bajo y


un código de idioma (por ejemplo es, fr, ca, etc) opcionales y una extension fija .properties

| 103
101 Groovy Script

void generateExcel(filename){
  File excelFile = new File(filename)

  String dir = excelFile.absolutePath.split(File.separator).dropRight(1).join(File.separator)


  String name = excelFile.name.split('\\.').dropRight(1).join('.')

  Properties defaultProperties = new Properties()


  defaultProperties.load( new File("${name}.properties").newInputStream() ) ①

  Map<String,Properties> propertiesMap = [:] ②

  new File(dir).eachFileMatch ~"${name}(_[A-Za-z]+)\\.properties", { ③


  def matcher = (it.name =~ "${name}(_[A-Za-z]+)\\.properties" )
  String lang = matcher[0][1]?.substring(1)
  Properties prp = new Properties()
  prp.load( it.newInputStream() )
  propertiesMap[ lang ] = prp ④
  }

  ExcelBuilder.output(new FileOutputStream(new File("${filename}"))) {


  sheet {
  row{
  cell("Code")
  cell("Default")
  propertiesMap.keySet().each { lang ->
  cell(lang.toUpperCase())
  }
  }
  defaultProperties.propertyNames().each{ String property-> ⑤
  row{
  cell(property)
  cell(defaultProperties[property])
  propertiesMap.keySet().each { lang ->
  cell(propertiesMap[lang][property])
  }
  }
  }
  }
  }
}

① cargar en un Properties el fichero por defecto (el cual contienen todos las keys a
traducir)

② preparar un Map<String,Properties>

③ buscar los ficheros de traducciones particulares que cumplen el patrón explicado

④ cargar en el mapa el properties identificado por su código de idioma

⑤ crear un Excel donde cada key será una fila y cada idioma volcará su texto

De Excel a i18n
Una vez completado el Excel con las traducciones adecuadas necesitarremos volver a
reescribir los ficheros properties

104 |
101 Groovy Script

void generateProperties(filename){
  File excelFile = new File(filename)

  String dir = excelFile.absolutePath.split(File.separator).dropRight(1).join(File.separator)


  String name = excelFile.name.split('\\.').dropRight(1).join('.')
  InputStream inp = new FileInputStream(excelFile)

  ①
  new File(dir).eachFileMatch ~"${name}(_[A-Za-z]+)?\\.properties", {
  it.delete()
  }

  List<String> languages = []

  Workbook wb = WorkbookFactory.create(inp)
  Sheet sheet = wb.getSheetAt(0)

  sheet.iterator().eachWithIndex{ Row row, int idx-> ②

  if( idx == 0){ ③


  languages = ['']+row.cellIterator().collect{ '_'+it.stringCellValue }.drop(2)*.toLowerCase()
  return
  }

  String code = row.getCell(0)


  languages.eachWithIndex{ String lang, int i -> ④
  String txt = row.getCell(i+1)?.stringCellValue
  if(txt)
  new File("${name}${lang}.properties") << "$code=$txt\n" ⑤
  }
  }
}

① limpiamos ficheros de traduccion si los hubiera

② iteramos por las filas del Excel

③ la primera fila nos indica los lenguajes que se contemplan además del por defecto

④ para cada lenguaje indicado en la primera fila buscamos si hay traducción en el excel

⑤ si tenemos traducción la concatenamos al fichero correspondiente al idioma

| 105
101 Groovy Script

Script

//tag::dependencies[]
@Grab('com.jameskleeh:excel-builder:0.4.2')

import org.apache.poi.ss.usermodel.*
import org.apache.poi.hssf.usermodel.*
import org.apache.poi.xssf.usermodel.*
import org.apache.poi.ss.util.*
import org.apache.poi.ss.usermodel.*
import com.jameskleeh.excel.ExcelBuilder
//end::dependencies[]

//tag::arguments[]
def cli = new CliBuilder(usage: 'groovy ExcelI18n.groovy -[h] -action generateProperties/generateExcel
filename.xls ')

cli.with { ①
  h(longOpt: 'help', 'Import/Export excel file to i18n properties files.', required: false)
  action('generateProperties generateExcel', required: true, args:1, argName:'action')
}

def options = cli.parse(args)


if (!options){
  return
}
if(options.h || options.arguments().size()==0) {
  cli.usage()
  return
}

if( options.action == 'generateProperties'){ ②


  return generateProperties(options.arguments()[0])
}

if( options.action == 'generateExcel'){ ③


  return generateExcel(options.arguments()[0])
}

cli.usage() ④
//end::arguments[]

//tag::generateProperties[]
void generateProperties(filename){
  File excelFile = new File(filename)

  String dir = excelFile.absolutePath.split(File.separator).dropRight(1).join(File.separator)


  String name = excelFile.name.split('\\.').dropRight(1).join('.')
  InputStream inp = new FileInputStream(excelFile)

  ①
  new File(dir).eachFileMatch ~"${name}(_[A-Za-z]+)?\\.properties", {
  it.delete()
  }

  List<String> languages = []

  Workbook wb = WorkbookFactory.create(inp)
  Sheet sheet = wb.getSheetAt(0)

  sheet.iterator().eachWithIndex{ Row row, int idx-> ②

  if( idx == 0){ ③


  languages = ['']+row.cellIterator().collect{ '_'+it.stringCellValue }.drop(2)*.toLowerCase()

106 |
101 Groovy Script

  return
  }

  String code = row.getCell(0)


  languages.eachWithIndex{ String lang, int i -> ④
  String txt = row.getCell(i+1)?.stringCellValue
  if(txt)
  new File("${name}${lang}.properties") << "$code=$txt\n" ⑤
  }
  }
}
//end::generateProperties[]

//tag::generateExcel[]
void generateExcel(filename){
  File excelFile = new File(filename)

  String dir = excelFile.absolutePath.split(File.separator).dropRight(1).join(File.separator)


  String name = excelFile.name.split('\\.').dropRight(1).join('.')

  Properties defaultProperties = new Properties()


  defaultProperties.load( new File("${name}.properties").newInputStream() ) ①

  Map<String,Properties> propertiesMap = [:] ②

  new File(dir).eachFileMatch ~"${name}(_[A-Za-z]+)\\.properties", { ③


  def matcher = (it.name =~ "${name}(_[A-Za-z]+)\\.properties" )
  String lang = matcher[0][1]?.substring(1)
  Properties prp = new Properties()
  prp.load( it.newInputStream() )
  propertiesMap[ lang ] = prp ④
  }

  ExcelBuilder.output(new FileOutputStream(new File("${filename}"))) {


  sheet {
  row{
  cell("Code")
  cell("Default")
  propertiesMap.keySet().each { lang ->
  cell(lang.toUpperCase())
  }
  }
  defaultProperties.propertyNames().each{ String property-> ⑤
  row{
  cell(property)
  cell(defaultProperties[property])
  propertiesMap.keySet().each { lang ->
  cell(propertiesMap[lang][property])
  }
  }
  }
  }
  }
}
//end::generateExcel[]

| 107
101 Groovy Script

Buscar el fichero de mayor tamaño


recursivamente
Miguel Rueda <miguel.rueda.garcia@gmail.com [mailto:miguel.rueda.garcia@gmail.com]>
2017-08-23

A veces nos surge que necesitamos saber qué fichero está consumiendo más espacio pero
debido a que tenemos un número elevado de subdirectorios nos es dificil encontrarlo. En
determinados sistemas operativos es "fácil" encontrarlo concatenando varios comandos
como du grep etc.

Mediante este script encontraremos la ruta del fichero con el mayor tamaño sin importar
el número de subdirectorios

max=null

void scanDir( File dir ){


  dir.eachFile{ f->
  max = max && max.size() >= f.size() ? max : f
  }
  dir.eachDir{ d ->
  scanDir(d)
  }
}

scanDir(new File('.'))

println "El fichero mayor es: $max.path y ocupa ${max.size()} bytes"

A continuación pasamos a explicar de forma detallada el contenido de nuestro script de


groovy que cumple la función de encontrar el fichero de mayor tamaño:

Definiremos una función llamada scanDir encargada de recorrer de manera recursiva


todos los directorios y subdirectorios de la ruta indicada por parámetro. Comparará el
tamaño de cada uno de los ficheros que van apareciendo y guadará el mayor.

Para recorrer cada uno de los directorios y subdirectorios de la ruta indicada por
parámetro utilizamos:

  dir.eachFile{ f->

Para recorrer los ficheros del directorio/subdirectorio encontrado utilizamos:

  dir.eachDir{ d ->

108 |
101 Groovy Script

Realizaremos la comparación entre el tamaño de los diferentes ficheros utilizando


f.size()(este método nos devuelve la dimensión del fichero):

  max = max && max.size() >= f.size() ? max : f

Para ejecutar este proceso simplemente llamaremos a la función scanDir indicando la ruta
que queremos revisar (en este caso .):

scanDir(new File('.'))

Al final del proceso obtendremos el fichero del cual podemos obtener la ruta al mismo,
tamaño, etc

println "El fichero mayor es: $max.path y ocupa ${max.size()} bytes"

| 109
101 Groovy Script

Script

max=null

void scanDir( File dir ){


  dir.eachFile{ f->
  max = max && max.size() >= f.size() ? max : f
  }
  dir.eachDir{ d ->
  scanDir(d)
  }
}

scanDir(new File('.'))

println "El fichero mayor es: $max.path y ocupa ${max.size()} bytes"

110 |
101 Groovy Script

Buscar en fichero y volcar coincidentes


Jorge Aguilera <jorge.aguilera@puravida-software.com [mailto:jorge.aguilera@puravida-
software.com]> 2017-08-23

En algunos sistemas operativos tenemos el comando grep o find que buscan una cadena
en los ficheros de un directorio y la vuelcan por consola. Con este script vamos a realizar
algo parecido para ver cómo podemos leer ficheros de gran tamaño y cómo escribir en
otro fichero.

if( args.length != 3){


  println "Hey necesito el fichero de entrada, el de salida y la cadena a buscar"
  return
}

inputFile = new File(args[0])


outputFile = new File(args[1])
search = args[2]

outputFile.newWriter().withWriter { writer-> ①
  inputFile.eachLine { line, indx -> ②
  if (line.indexOf(search) != -1)
  writer << "$indx: $line\n" ③
  }
}

① Creamos o sobreescribimos un fichero de salida y usamos un writer para escribir en él

② readLine es indicada para leer ficheros de gran tamaño, teniendo otras formas de leer
un fichero como .text que leerían todo el fichero en una cadena

③ utilizamos el operator leftshit para ir enviando cadenas al fichero

| 111
101 Groovy Script

Script

if( args.length != 3){


  println "Hey necesito el fichero de entrada, el de salida y la cadena a buscar"
  return
}

inputFile = new File(args[0])


outputFile = new File(args[1])
search = args[2]

outputFile.newWriter().withWriter { writer-> ①
  inputFile.eachLine { line, indx -> ②
  if (line.indexOf(search) != -1)
  writer << "$indx: $line\n" ③
  }
}

112 |
101 Groovy Script

Maven Inventory
Jorge Aguilera <jorge.aguilera@puravida-software.com [mailto:jorge.aguilera@puravida-
software.com]> 2019-01-28

Muchas de las librerías que usamos a diario están construidas con Maven y muchas de
ellas incluyen un fichero pom.properties en el directorio Manifest con los detalles del build
(como el grupo, el artefacto y la version)

A veces nos encontramos con un directorio con un buen número de estas librerías y
puede suceder que se encuentren duplicadas en diferentes directorios o lo que es peor
que tengamos para el mismo artefacto diferentes versiones y aparezcan problemas con el
cargador de clases.

Con este pequeño script podremos hacer un scaneo rápido de estos artefactos y generar
un informe con toda esta información que nos permita descubrir estos conflictos.

| 113
101 Groovy Script

import java.util.jar.JarEntry
import java.util.jar.JarFile
import groovy.json.JsonOutput

groups = [:]

path = args.length > 0 ? args[0] : System.properties["user.home"]+"/.m2/repository"; ①

new File(path).traverse(type: groovy.io.FileType.FILES) { it ->


  if( it.name.endsWith('.jar')){
  inspectJar(it)
  }
}
println JsonOutput.prettyPrint(new JsonOutput().toJson(groups))

void inspectJar(File f){


  final JarFile jarFile = new JarFile(f);
  Enumeration<JarEntry> enumOfJar = jarFile.entries()
  JarEntry pom = enumOfJar.find{it.name.endsWith("/pom.properties")}
  if( pom )
  inspectPom( jarFile, pom )
}

void inspectPom(JarFile jarFile, JarEntry jarEntry){

  InputStream input = jarFile.getInputStream(jarEntry);


  Properties prp = new Properties();
  prp.load(input);

  def group = groups[prp['groupId']] ?: [:]


  def artifact = group[prp['artifactId']] ?: [:]
  def version = artifact[prp['version']] ?: []

  version << jarFile.name

  artifact[prp['version']]=version
  group[prp['artifactId']]=artifact
  groups[prp['groupId']] = group
}

① si no se proporciona una ruta se usa la típica de maven

• Iteramos por todos los ficheros que terminan en .jar recursivamente

• Inspecionamos cada fichero buscando si contiene un pom.properties

• Actualizamos un Map (groups) de Map (artifacts) de List (versions) y añadimos el


nombre del fichero a la lista de versiones. Si dos ficheros coinciden aparece una
entrada duplicada

AL final de escaneo volcamos en pantalla la información en formato JSON pero puedes


usar cualquier otro formato de tu conveniencia.

114 |
101 Groovy Script

Resultado
Este es un ejemplo de lo que se vería por pantalla:

  "org.apache.maven.shared": {
  "maven-shared-utils": {
  "0.1": [
  ".m2/repository/org/apache/maven/shared/maven-shared-utils/0.1/maven-shared-utils-0.1.jar"
  ],
  "0.3": [
  ".m2/repository/org/apache/maven/shared/maven-shared-utils/0.3/maven-shared-utils-0.3.jar"
  ]
  },
  "maven-shared-incremental": {
  "1.1": [
  ".m2/repository/org/apache/maven/shared/maven-shared-incremental/1.1/maven-shared-
incremental-1.1.jar"
  ]
  },
  "maven-repository-builder": {
  "1.0-alpha-2": [
  ".m2/repository/org/apache/maven/shared/maven-repository-builder/1.0-alpha-2/maven-
repository-builder-1.0-alpha-2.jar"
  ]
  },
  "maven-invoker": {
  "2.1.1": [
  ".m2/repository/org/apache/maven/shared/maven-invoker/2.1.1/maven-invoker-2.1.1.jar"
  ],
  "2.2": [
  ".m2/repository/org/apache/maven/shared/maven-invoker/2.2/maven-invoker-2.2.jar"
  ]
  },
  "maven-plugin-testing-harness": {
  "1.1": [
  ".m2/repository/org/apache/maven/shared/maven-plugin-testing-harness/1.1/maven-plugin-
testing-harness-1.1.jar"
  ]
  },
  "file-management": {
  "1.2.1": [
  ".m2/repository/org/apache/maven/shared/file-management/1.2.1/file-management-1.2.1.jar"
  ],
  "1.1": [
  ".m2/repository/org/apache/maven/shared/file-management/1.1/file-management-1.1.jar"
  ]
  },
  "maven-shared-io": {
  "1.1": [
  ".m2/repository/org/apache/maven/shared/maven-shared-io/1.1/maven-shared-io-1.1.jar"
  ]
  },

| 115
101 Groovy Script

Organizar fotografías por fecha


Jorge Aguilera <jorge.aguilera@puravida-software.com [mailto:jorge.aguilera@puravida-
software.com]> 2018-08-12

Tengo una cámara nueva que hace unas fotos realmente fantásticas. Incluso puedo
conectar mi teléfono y transferirme las fotos a este para compartirlas al momento. Sin
embargo lo que no hace (o yo no he encontrado) es organizarme las fotos por carpetas
según la fecha en que la tomé.

Con este pequeño script vamos a poder acceder a la información EXIF que se guarda
dentro de la imagen para buscar la fecha en que fue tomada y usarla para organizar todas
las fotos de un directorio dado.

116 |
101 Groovy Script

@Grab("org.yaml:snakeyaml:1.16")
@Grab("com.drewnoakes:metadata-extractor:2.11.0")

import static groovy.io.FileType.FILES

import com.drew.metadata.*
import com.drew.imaging.*
import com.drew.imaging.jpeg.*
import com.drew.metadata.exif.*

dir = new File(args[0])

outdir = new File(args[1])


outdir.mkdirs()

files = []
dir.traverse(type: FILES, maxDepth: 0) { files << it }
files = files.findAll{ file ->
  String ext = file.name.toLowerCase().split("\\.").last()
  ['jpg','png'].contains(ext)
}.sort{ it.lastModified() }

if( !files.size() ){
  println "No files"
  return
}

files.each{ file ->


  String gname = new Date(file.lastModified()).format('yyyy-MM-dd')

  try {
  Metadata metadata = ImageMetadataReader.readMetadata(file)
  metadata.directories.find{ it.name=="Exif IFD0"}.tags.findAll{
  it.tagName =="Date/Time"}.each{ tag ->
  gname = tag.description[0..10].replaceAll(":","-")
  }
  }catch( e ){
  println "error extracting metadata for $file.name : $e.message"
  }

  File newdir = new File(outdir,gname)


  newdir.mkdirs()
println "Move $file.name to $newdir.name"
  file.renameTo( new File(newdir, file.name) )
}

El script es muy simple:

| 117
101 Groovy Script

Dado un directorio de origen y uno de destino, recorrerá todos los ficheros .jpg y .png del
primero y para cada uno de ellos extraerá la información meta que contiene. Esta
información es muy extensa y cada fabricante añade las suyas propias. De todas ellas
buscaremos el tag EXIF Date/Time el cual es usado para guardar la fecha en que se tomó la
foto.

Simplemente usaremos este tag como nombre del subdirectorio donde mover la foto.

118 |
101 Groovy Script

Script

@Grab("org.yaml:snakeyaml:1.16")
@Grab("com.drewnoakes:metadata-extractor:2.11.0")

import static groovy.io.FileType.FILES

import com.drew.metadata.*
import com.drew.imaging.*
import com.drew.imaging.jpeg.*
import com.drew.metadata.exif.*

dir = new File(args[0])

outdir = new File(args[1])


outdir.mkdirs()

files = []
dir.traverse(type: FILES, maxDepth: 0) { files << it }
files = files.findAll{ file ->
  String ext = file.name.toLowerCase().split("\\.").last()
  ['jpg','png'].contains(ext)
}.sort{ it.lastModified() }

if( !files.size() ){
  println "No files"
  return
}

files.each{ file ->


  String gname = new Date(file.lastModified()).format('yyyy-MM-dd')

  try {
  Metadata metadata = ImageMetadataReader.readMetadata(file)
  metadata.directories.find{ it.name=="Exif IFD0"}.tags.findAll{
  it.tagName =="Date/Time"}.each{ tag ->
  gname = tag.description[0..10].replaceAll(":","-")
  }
  }catch( e ){
  println "error extracting metadata for $file.name : $e.message"
  }

  File newdir = new File(outdir,gname)


  newdir.mkdirs()
println "Move $file.name to $newdir.name"
  file.renameTo( new File(newdir, file.name) )
}

| 119
101 Groovy Script

Eliminar lineas con error


Jorge Aguilera <jorge.aguilera@puravida-software.com [mailto:jorge.aguilera@puravida-
software.com]> 2018-02-01

A continuación vamos a ver un script realmente sencillo para un caso de uso frecuente:

Tenemos un fichero que contiene un elevado número de líneas y en otro fichero tenemos
identificadas las lineas que queremos suprimir del primero (tal vez sean unos IDs,
nombres, etc).

La idea es que tendríamos que recorrer el fichero que contiene los registros a suprimir y
buscar en el fichero de origen aquellos que cumplan la codición reescribiendo una y otra
vez el fichero hasta haber procesado todos los registros del fichero de error.

Mediante este script lo que hacemos es leer el fichero de origen y convertirlo a una lista
(obviamente este script está diseñado para ficheros con varios miles de lineas pero con un
tamaño que no exceda el de la memoria disponible). Después iremos eliminando de esta
lista aquellos registros que cumplan la condición específica, para lo que usaremos el
método removeIf de Groovy

origen=new File('dump.txt').text.split('\n') as List

new File('errors.txt').withReader{ reader ->


  line=''
  while( line=reader.readLine() ){
  String id=line.split(' ')[0] ①
  origen.removeIf{ it.startsWith(id) } ②
  }
}

new File('cleaned.txt').withWriter('ISO-8859-1',{
  it.write origen.join('\n') ③
})

① Suponemos que el primer campo de cada linea es el Id a buscar

② Eliminamos de la lista todos los registros que cumplan la condicion de comenzar por el
ID, por ejemplo.

③ Generamos un fichero nuevo uniendo la lista con retornos de carro

120 |
101 Groovy Script

Script

origen=new File('dump.txt').text.split('\n') as List

new File('errors.txt').withReader{ reader ->


  line=''
  while( line=reader.readLine() ){
  String id=line.split(' ')[0] ①
  origen.removeIf{ it.startsWith(id) } ②
  }
}

new File('cleaned.txt').withWriter('ISO-8859-1',{
  it.write origen.join('\n') ③
})

| 121
101 Groovy Script

Scan and Execute


Jorge Aguilera <jorge.aguilera@puravida-software.com [mailto:jorge.aguilera@puravida-
software.com]> 2017-09-16

En algún momento de tu vida tendrás que recorrer un directorio de ficheros buscando


aquellos que cumplan una condición especial y ejecutar algún comando si lo encuentras.
A veces la condición es simple, como buscar un texto dentro del fichero, que el nombre
del fichero cumpla un patrón, etc, pero otras será algo más complicado.

Recientemente surgió la necesidad de subir todo el contenido de un repositorio maven en


local (.m2) a un repositorio remoto Nexus3 por lo que un simple copy&paste no era
suficiente. En concreto las condiciones del proceso se resumían en:

• el repositorio Nexus3 está protegido por usuario/password

• buscar recursivamente en un directorio dado

• buscar si existe un fichero POM para el artefacto

• comprobar si existía un jar o un war con el mismo nombre del POM

• si la versión a la que apuntan es un snapshot deberán instalarse en un repo diferente


que si es una release

• ejecutar el comando mvn deploy:deploy-file con los parámetros correctos

Obviamente en tu caso los requisitos serán totalmente distintos y


probablemente podrás resolverlo con un BASH pero tal vez las
 condiciones se vayan complicando y será cuando quieras hacerte un
script como este

122 |
101 Groovy Script

repoUrl=""
repoSnapUrl=""

void scanDir( dir ){


  dir.eachFile{ f->
  if( f.name.endsWith(".pom") ){ ①
  def jar = dir.listFiles().find{
  (it.name.endsWith(".jar") || it.name.endsWith(".war")) && ②
  it.name.split('\\.').dropRight(1).join('.') == f.name.split('\\.').dropRight(1).join
('.')
  }
  if( jar ){
  String url = jar.name.toLowerCase().contains("-snapshot.") ? repoSnapUrl : repoUrl ③
  """mvn deploy:deploy-file
  -Dfile=$jar.absolutePath
  -DpomFile=$f.absolutePath
  -Durl=$url
  -DrepositoryId=$repoId""".execute() ④
  }
  }
  }
  dir.eachDir{ d-> ⑤
  scanDir(d)
  }
}

repoId = args[1]
repoUrl = args[2]
repoSnapUrl = args.length > 3 ? args[3] : args[2]

scanDir( new File(args[0]) )

① Comprobamos si existe un fichero POM

② Comprobamos si existe un Jar o War con el mismo nombre (indica que pertenecen a la
misma version)

③ Configuramos repositorio para snapshot o para release

④ Construimos una cadena y la ejecutamos contra el sistema operativo

⑤ Recorremos recursivamente los subdirectorios

| 123
101 Groovy Script

Script

repoUrl=""
repoSnapUrl=""

void scanDir( dir ){


  dir.eachFile{ f->
  if( f.name.endsWith(".pom") ){ ①
  def jar = dir.listFiles().find{
  (it.name.endsWith(".jar") || it.name.endsWith(".war")) && ②
  it.name.split('\\.').dropRight(1).join('.') == f.name.split('\\.').dropRight(1).join
('.')
  }
  if( jar ){
  String url = jar.name.toLowerCase().contains("-snapshot.") ? repoSnapUrl : repoUrl ③
  """mvn deploy:deploy-file
  -Dfile=$jar.absolutePath
  -DpomFile=$f.absolutePath
  -Durl=$url
  -DrepositoryId=$repoId""".execute() ④
  }
  }
  }
  dir.eachDir{ d-> ⑤
  scanDir(d)
  }
}

repoId = args[1]
repoUrl = args[2]
repoSnapUrl = args.length > 3 ? args[3] : args[2]

scanDir( new File(args[0]) )

124 |
101 Groovy Script

A qué dedicas tu tiempo libre (Google


Calendar)
Jorge Aguilera <jorge.aguilera@puravida-software.com [mailto:jorge.aguilera@puravida-
software.com]> 2017-12-3

Para este post vamos a necesitar los siguientes requisitos: una


cuenta en Google y eventos creados en el calendario. Por defecto usaremos
el calendario principal (pero se podría usar cualquier otro de los que
Google nos permite crear). Así mismo para poder acceder a las APIs de
Google deberemos obtener unas credenciales que autoricen a la aplicación
para acceder a nuestra cuenta.

En este post vamos a utilizar un GroovyScript para conectar el calendario de Google


revisando todos los eventos que se encuentran creados, agrupándolos en función del día
de la semana y el "tema" que le hayamos asignado.

Un evento del calendario de por sí NO incluye ningún campo del tipo


"tema" por lo que para poder agruparlos podríamos usar, por ejemplo,
algún identificador en el texto del mismo. En este ejemplo lo que vamos a
 usar es la capacidad de asignar un color a un evento, de tal forma que si
en nuestro calendario asignamos el color "rojo" a los eventos de tipo
"ocio", el color "azul" a los de "formación", etc. podremos de una forma
muy fácil agrupar eventos dispares.

Consola Google
En primer lugar deberemos crear una aplicación en la consola de Google en
https://console.developers.google.com

En esta aplicación habilitaremos (al menos) la API "Google Calendar API"

Así mismo deberemos crear unas credenciales de "Servicio" obteniendo la posibilidad de


descargarlas en un fichero JSON y que deberemos ubicar junto con el script.

Groogle
Para facilitar la parte técnica de autentificación y creación de servicios he creado un
proyecto de código abierto, Groogle, disponible en https://gitlab.com/puravida-software/
groogle el cual publica un artefacto en Bintray y que podremos usar en nuestros scripts
simplemente incluyendo su repositorio.

| 125
101 Groovy Script

@GrabResolver(name='puravida', root="https://dl.bintray.com/puravida-software/repo" )
@Grab(group='org.groovyfx',module='groovyfx',version='8.0.0',transitive=false, noExceptions=true)
@Grab(group = 'com.puravida.groogle', module = 'groogle-core', version = '1.0.0')

import com.puravida.groogle.GroogleScript
import com.google.api.services.calendar.CalendarScopes

Groogle nos permitirá realizar el login de nuestra aplicación y guardar la autorización en


nuestro disco de tal forma que en ejecuciones posteriores no se requiera de autorizar de
nuevo la aplicación (mientras no borremos el fichero con la autorización generada)

InputStream clientSecret = new File('client_secret.json').newInputStream() ①

def groogle = new GroogleScript(applicationName:'101-scripts') ②

groogle.login(clientSecret,[CalendarScopes.CALENDAR_READONLY]) ③

① client-secret.json es el fichero que hemos descargado de la consola de Google y que


contiene las credenciales

② Groogle realiza el login y guarda la autorización en


$HOME/.credentials/${applicationName}

③ En este post únicamente requerimos acceso a Calendar para lectura

Personalización
Como los temas son personales cada uno deberá configurar los suyos según sus
preferencias. Así mismo creará más o menos temas según el grado de detalle que se
quiera obtener. Para este post vamos a utilizar únicamente 3 temas:

def colors = [
  '0':'Generico',
  '10':'Reuniones', ①
  '11':'Ocio'
]

① Hasta ahora no he encontrado cómo saber el ID del color a priori por lo que deberás
ejecutarlo primero y ver cuales te asigna.

Business
La lógica de negocio de este script consistirá básicamente en recorrer todos los eventos
que nos devuelva Google y para cada uno de ellos buscar en un mapa si existe un sub-
mapa "tema" y si para este sub-mapa existe otro sub-map para el día de la semana donde
ir acumulando eventos. En caso de que no exista, simplemente lo inicializaremos a cero

126 |
101 Groovy Script

para ir acumulando sobre él.

def week = ['D','L','M','X','J','V','S']


def stadistics = [:]

groogle.calendarService.events().list(calendarId).execute().items.each{ event -> ①

  def events = !event.recurrentId ? [event] : ②


  groogle.calendarService.events().instances(calendarId, event.id).execute().items

  events.each{ item ->


  String colorId = item.colorId ?: '0'
  if( !stadistics."${colors[colorId]}"){ ③
  stadistics."${colors[colorId]}" =[:]
  (java.util.Calendar.SUNDAY..java.util.Calendar.SATURDAY).each{ day ->
  stadistics."${colors[colorId]}"."${week[day-1]}"= 0
  }
  }
  int day = new Date( (event.start.date ?: event.start.dateTime).value)[java.util.Calendar.DAY_OF_WEEK]
  stadistics."${colors[colorId]}"."${week[day-1]}" +=1 ④
  }
}

① Recorrer todos los eventos que nos devuelva Google

② Un evento puede ser puntual o recurrente. En este caso deberemos obtener todas las
instancias del mismo

③ Buscar en el mapa el tema asociado al evento y si no existe inicializarlo

④ Incrementar en uno para el día de la semana del evento

Vista
Para la vista usaremos un GroovyFX [http://groovyfx.org/] que nos muestre una gráfica de
barras barChart tal que nos permite visualizar agrupados por temas los diferentes
contadores de los días de la semana. Sin embargo este componente nos pide que le
proporcionemos la serie de datos en una sucesión de 'clave', 'valor', 'clave', 'valor' en lugar
de un mapa, por lo que lo primero que haremos será transformar nuestro mapa en uno
más adecuado:

def flatten =[:]


stadistics.each{
  flatten."$it.key" = it.value.collect{ [it.key,it.value]}.flatten() ①
}

Y ahora ya podemos construir la vista, generando las series de una forma dinámica.

| 127
101 Groovy Script

start {
  stage(visible:true,width:640,height:480) { ②
  scene{
  tilePane() {
  barChart(
  yAxis:new NumberAxis(label:'Ocupacion', tickLabelRotation:90),
  xAxis:new CategoryAxis(label:'Actividad'),
  barGap:3,
  categoryGap:20) {
  flatten.each{ ①
  series(name: it.key, data:it.value)
  }
  }
  }
  }
  }
}

① Creamos dinámicamente las series en función de los temas que tengamos inicialmente.

Mi calendario quedaría de esta forma:

128 |
101 Groovy Script

Script

//tag::dependencies[]
@GrabResolver(name='puravida', root="https://dl.bintray.com/puravida-software/repo" )
@Grab(group='org.groovyfx',module='groovyfx',version='8.0.0',transitive=false, noExceptions=true)
@Grab(group = 'com.puravida.groogle', module = 'groogle-core', version = '1.0.0')

import com.puravida.groogle.GroogleScript
import com.google.api.services.calendar.CalendarScopes
//end::dependencies[]

import static groovyx.javafx.GroovyFX.start


import javafx.scene.chart.CategoryAxis;
import javafx.scene.chart.NumberAxis;
import javafx.application.Platform
import javafx.scene.SnapshotParameters
import javax.imageio.ImageIO
import java.awt.image.BufferedImage
//tag::login[]
InputStream clientSecret = new File('client_secret.json').newInputStream() ①

def groogle = new GroogleScript(applicationName:'101-scripts') ②

groogle.login(clientSecret,[CalendarScopes.CALENDAR_READONLY]) ③
//end::login[]

String calendarId = 'primary'


/*
Si quieres saber todos tus calendarios simplemente ejecuta esta linea
println groogle.calendarService.calendarList().list().execute().items*.id
*/

//tag::temas[]
def colors = [
  '0':'Generico',
  '10':'Reuniones', ①
  '11':'Ocio'
]
//end::temas[]

//tag::business[]
def week = ['D','L','M','X','J','V','S']
def stadistics = [:]

groogle.calendarService.events().list(calendarId).execute().items.each{ event -> ①

  def events = !event.recurrentId ? [event] : ②


  groogle.calendarService.events().instances(calendarId, event.id).execute().items

  events.each{ item ->


  String colorId = item.colorId ?: '0'
  if( !stadistics."${colors[colorId]}"){ ③
  stadistics."${colors[colorId]}" =[:]
  (java.util.Calendar.SUNDAY..java.util.Calendar.SATURDAY).each{ day ->
  stadistics."${colors[colorId]}"."${week[day-1]}"= 0
  }
  }
  int day = new Date( (event.start.date ?: event.start.dateTime).value)[java.util.Calendar.DAY_OF_WEEK]
  stadistics."${colors[colorId]}"."${week[day-1]}" +=1 ④
  }
}
//end::business[]

//tag::flatten[]

| 129
101 Groovy Script

def flatten =[:]


stadistics.each{
  flatten."$it.key" = it.value.collect{ [it.key,it.value]}.flatten() ①
}
//end::flatten[]

//tag::vista[]
start {
  stage(visible:true,width:640,height:480) { ②
  scene{
  tilePane() {
  barChart(
  yAxis:new NumberAxis(label:'Ocupacion', tickLabelRotation:90),
  xAxis:new CategoryAxis(label:'Actividad'),
  barGap:3,
  categoryGap:20) {
  flatten.each{ ①
  series(name: it.key, data:it.value)
  }
  }
  }
  }
  }
}
//end::vista[]

130 |
101 Groovy Script

Import/Export de Google Sheet a


Database
Jorge Aguilera <jorge.aguilera@puravida-software.com [mailto:jorge.aguilera@puravida-
software.com]> 2017-12-30

Para este post es necesario tener una cuenta en Google así como
tener al menos una Hoja de Cálculo de Google Drive (SpreadSheet). Así
mismo para poder acceder a las APIs de Google deberemos obtener unas
credenciales que autorizen a la aplicación a acceder a nuestra cuenta
habilitandolo para ello desde la consola developer de Google.

A veces el departamento de informática tiene que dar soporte a otros departamentos en


las tareas de exportar y/o importar datos de la base de daos usando formatos que a otros
departamentos les sea cómodo. Sin lugar a dudas, el formato elegido en la mayoría de los
casos es un fichero Excel de Microsoft. (Ver post en categoría bbdd y office para consultar
scripts ya documentados)

Sin embargo muchas veces este Excel debe ser revisado por más de una persona antes de
volver a ser enviado al departamento de informática para su importación en la base de
datos por lo que se produce un riesgo de disparidad de versiones en el fichero, infinidad
de correos adjuntando la última versión, etc.

En un mundo colaborativo como el de hoy en día, sería interesante si todas las partes
(incluido el departamento de informática) pudieran utilizar un único fichero de
referencia siendo Google Sheet uno de los productos que lo permiten.

Así pues nuestro script va a consistir en, dado un SpreadSheet de Google y una cuenta de
autorización:

• conectarse a la base de datos

• conectarse al SpreadSheet

• si la operación que se le indica es de exportar, recorrer todas las hojas del SpreadSheet
y volcar sobre cada una de ellas los datos de la tabla cuyo nombre coincida con la hoja

• si la operación es de importar, recorrer todas las hojas del SpreadSheet e importar


todos las filas de la hoja en la tabla cuyo nombre coincida con la hoja.

| 131
101 Groovy Script

Id del SpreadSheet

En este post vamos a utilizar un GroovyScript para conectarse a nuestro


SpreadSheet de Google para leer y/o escribir en el mismo por lo que necesitaremos
saber su id. Este id se puede obtener de la propia URL que utiliza el navegador
cuando lo estamos editando. Por ejemplo, si la URL tiene este formato:

https://docs.google.com/spreadsheets/d/xxxxxxxxxtKhml999999999Icp123/edit#
gid=0

Entonces xxxxxxxxxtKhml999999999Icp123 es el id que necesitamos


proporcionar como argumento al script.

Consola Google
En primer lugar deberemos crear una aplicación en la consola de Google en
https://console.developers.google.com

En esta aplicación habilitaremos (al menos) la API "Google Sheet API"

Así mismo deberemos crear unas credenciales de "Servicio" tras lo cual dispondremos de
la posibilidad de descargar un fichero JSON con las mismas y que deberemos ubicar junto
con el script

Groogle
Para facilitar la parte técnica de autentificación y creación de servicios he creado un
proyecto de código abierto, Groogle, disponible en https://gitlab.com/puravida-software/
groogle el cual publica un artefacto en Bintray y que podremos usar en nuestros scripts
simplemente incluyendo su repositorio. Groogle se compone de varios componentes:

• groogle-core, contiene la parte de autentificación y autorización del usuario así como


proporcionar servicios básicos

• groogle-sheet, facilita la gestión de Sheets mediante un lenguaje específico (DSL)


permitiendo la lectura y escritura de una hoja de calculo

• groogle-drive, facilita la gestión de ficheros en Drive (aún está en desarrollo)

• groogle-calendar, facilita la gestión de Eventos de un Calendario mediante un lenguaje


específico

Dependencias

132 |
101 Groovy Script

@GrabResolver(name='puravida', root="https://dl.bintray.com/puravida-software/repo" )
@Grab(group = 'com.puravida.groogle', module = 'groogle-sheet', version = '1.1.1')
@Grab('mysql:mysql-connector-java:5.1.23')
@GrabConfig(systemClassLoader=true)

import com.google.api.services.sheets.v4.SheetsScopes
import com.puravida.groogle.GroogleScript
import com.puravida.groogle.SheetScript
import groovy.sql.Sql

Autorización
Groogle nos permitirá realizar el login de nuestra aplicación y guardar la autorización en
nuestro disco de tal forma que ejecuciones posteriores no requieran de volver a autorizar
la aplicación (mientras no borremos el fichero con la autorización generada)

clientSecret = new File('client_secret.json').newInputStream() ①


groogleScript = new GroogleScript('101-scripts', clientSecret,[SheetsScopes.SPREADSHEETS]) ②
sheetScript = new SheetScript(groogleScript: groogleScript) ③

① client-secret.json es el fichero que hemos descargado de la consola de Google y que


contiene las credenciales

② Groogle realiza el login y guarda la autorización en


$HOME/.credentials/${applicationName}

③ SheetScript ofrece un DSL para la gestión de lectura/escritura del SpreadSheet

Database
Para la gestión de la base de datos hemos creado una serie de métodos simples:

| 133
101 Groovy Script

sqlInstance = Sql.newInstance( "jdbc:mysql://localhost:3306/origen?jdbcCompliantTruncation=false", "user",


"password", "com.mysql.jdbc.Driver")
List metadataTable(String table){ ①
  def ret = []
  sqlInstance.rows( "select * from $table limit 1".toString(),{ meta->
  (1..meta.columnCount).each{
  ret.add(meta.getColumnName(it))
  }
  })
  ret
}

void cleanTable(String table){ ②


  sqlInstance.execute "truncate table $table".toString()
}

void insertRowsIntoTable(List params, String table, List metadata){ ③


  String sql = "insert into $table (${metadata.join(',')}) values (${'?,'.multiply(metadata.size()-1)}?)"
  sqlInstance.withBatch(sql){ ps->
  params.each{ param ->
  ps.addBatch param
  }
  }
}

List moreRows(String table, int from, int size){ ④


  sqlInstance.rows( "select * from $table order by 1".toString(),from, size)*.values()
}

① Dado el nombre de una table, otener sus columnas mediante el metadata

② Borrado de una tabla. En nuestro caso y por ser MySQL hacemos un truncate

③ Insertado optimizado de un List<List<Object>> (lista de lista de valores)

④ Lectura de un lote de valores a partir de un registro dado

Argumentos
Para la ejecución correcta del script necesitamos que nos proporcionen el id de la hoja así
como la intención de la ejecución, es decir, "De Google a Database" o "De Database a
Google" lo cual lo representamos mediante los comandos g2d o d2g respectivamente

134 |
101 Groovy Script

cli = new CliBuilder(usage: '-i -s spreadSheetId g2d|d2g')


cli.with {
  h(longOpt: 'help', args:0,'Usage Information', required: false)
  s(longOpt: 'spreadSheet', args:1, argName:'spreadSheetId', 'El id de la hoja a usar', required: true)
}
options = cli.parse(args)
if (!options) return
if (options.h || options.arguments().size()!=1) {
  cli.usage()
  return
}
if( ['g2d','d2g'].contains(options.arguments()[0]) == false ){
  cli.usage()
  return
}
google2Database = options.arguments()[0] == 'g2d'

Así por ejemplo una posible ejecución del scritp sería:

De Google a Database

groovy ImportExportSheet.groovy -s xxxxxxxxxtKhml999999999Icp123 g2d

Comparando Metadatos
En primer lugar, el script validará que los esquemas de la hoja y de la base de datos se
corresponden, de tal forma que ambos tienen las mismas columnas (para cada hoja de
datos que contenga el SpreadSheet).

Para ello lee la primera fila de cada hoja y la compara con el metadata de la tabla con el
mismo nombre en la base de datos. Si no coinciden (por ejemplo la hoja contiene una
columna más que la bbdd, o alguien ha cambiado el nombre de una columna) se aborta el
script mediante un assert

| 135
101 Groovy Script

sheetScript.withSpreadSheet options.spreadSheet, { spreadSheet -> ①

  sheets.each{ gSheet -> ②


  String hojaId = gSheet.properties.title
  println "Validando schema de $hojaId"

  withSheet hojaId, { sheet -> ③

  def sheetColumns = readRows("A1", "AA1").first() ④

  def tableColumns = metadataTable(gSheet.properties.title)

  assert sheetColumns.intersect(tableColumns).size() == sheetColumns.size(), """


  $gSheet.properties.title incompatible schemas:
  Sheet: $sheetColumns
  MySQL: $tableColumns
  """
  }
  }
}

① withSpreadSheet(id, closure), ejecuta la closure dentro de un contexto referenciado al


SpreadSheet indicado

② el DSL nos permite acceder a todas las hojas mediante sheets

③ withSheet(id, closure) ejecuta la closure dentro de un contexto referenciado a la hoja


indicada

④ readRows (rangoA, rangoB) realiza la lectura de un rango de celdas devolviendo un


List<List<Object>>

De Google a Database
Una vez validados los metadatas de cada hoja el script puede realizar la importanción
desde las hojas a la base de datos (si el argumento de la ejecución es g2d). Para ello
recorrerá las filas de la hoja mediante readRows hasta que no haya datos (cuando
readRows devuelva null) y para evitar un número excesivo de lecturas lo hará en bloques
de 100

136 |
101 Groovy Script

if( google2Database ) {
  sheetScript.withSpreadSheet options.spreadSheet, { spreadSheet ->
  sheets.each { gSheet ->
  String hojaId = gSheet.properties.title
  println "Importando hoja $hojaId"

  withSheet hojaId, { sheet ->


  def sheetColumns = readRows("A1", "AA1").first()
  cleanTable(hojaId)

  int idx = 2
  def rows = readRows("A$idx", "AA${idx + 100}")
  while (rows) {
  insertRowsIntoTable(rows, hojaId, sheetColumns)
  idx += rows.size()
  rows = readRows("A$idx", "AA${idx + 100}")
  }

  }

  }
  }
}

De Database a Google
Una vez validados los metadatas de cada hoja el script puede realizar la importanción
desde la base de datos a las hojas (si el argumento de la ejecución es d2g). Para ello
recorrerá los registros de la base de datos en bloques de 100 y los insertará en la hoja
simplemente llamando el método appendRows que admite un List<List<Object>>

Previamente cada hoja es inicializada mediante clearRange añadiendo también los


títulos mediante appendRow que requiere un List<Object>

| 137
101 Groovy Script

if( !google2Database ) {
  sheetScript.withSpreadSheet options.spreadSheet, { spreadSheet ->
  sheets.each{ gSheet ->
  String hojaId = gSheet.properties.title
  println "Exportando a hoja $hojaId"

  withSheet hojaId, { sheet ->


  def sheetColumns = readRows("A1", "AA1").first()
  clearRange('A','AA')

  appendRow(sheetColumns)

  int idx=0
  def rows = moreRows(hojaId, idx, 100)
  while( rows.size() ){

  appendRows(rows)
  idx+=rows.size()+1
  rows = moreRows(hojaId, idx, 100)
  }
  }

  }
  }
}

138 |
101 Groovy Script

Script

//tag::dependencies[]
@GrabResolver(name='puravida', root="https://dl.bintray.com/puravida-software/repo" )
@Grab(group = 'com.puravida.groogle', module = 'groogle-sheet', version = '1.1.1')
@Grab('mysql:mysql-connector-java:5.1.23')
@GrabConfig(systemClassLoader=true)

import com.google.api.services.sheets.v4.SheetsScopes
import com.puravida.groogle.GroogleScript
import com.puravida.groogle.SheetScript
import groovy.sql.Sql

//end::dependencies[]

//tag::cli[]
cli = new CliBuilder(usage: '-i -s spreadSheetId g2d|d2g')
cli.with {
  h(longOpt: 'help', args:0,'Usage Information', required: false)
  s(longOpt: 'spreadSheet', args:1, argName:'spreadSheetId', 'El id de la hoja a usar', required: true)
}
options = cli.parse(args)
if (!options) return
if (options.h || options.arguments().size()!=1) {
  cli.usage()
  return
}
if( ['g2d','d2g'].contains(options.arguments()[0]) == false ){
  cli.usage()
  return
}
google2Database = options.arguments()[0] == 'g2d'
//end::cli[]

//tag::database[]
sqlInstance = Sql.newInstance( "jdbc:mysql://localhost:3306/origen?jdbcCompliantTruncation=false", "user",
"password", "com.mysql.jdbc.Driver")
List metadataTable(String table){ ①
  def ret = []
  sqlInstance.rows( "select * from $table limit 1".toString(),{ meta->
  (1..meta.columnCount).each{
  ret.add(meta.getColumnName(it))
  }
  })
  ret
}

void cleanTable(String table){ ②


  sqlInstance.execute "truncate table $table".toString()
}

void insertRowsIntoTable(List params, String table, List metadata){ ③


  String sql = "insert into $table (${metadata.join(',')}) values (${'?,'.multiply(metadata.size()-1)}?)"
  sqlInstance.withBatch(sql){ ps->
  params.each{ param ->
  ps.addBatch param
  }
  }
}

List moreRows(String table, int from, int size){ ④


  sqlInstance.rows( "select * from $table order by 1".toString(),from, size)*.values()
}

| 139
101 Groovy Script

//end::database[]

//tag::login[]
clientSecret = new File('client_secret.json').newInputStream() ①
groogleScript = new GroogleScript('101-scripts', clientSecret,[SheetsScopes.SPREADSHEETS]) ②
sheetScript = new SheetScript(groogleScript: groogleScript) ③
//end::login[]

//tag::schemas[]
sheetScript.withSpreadSheet options.spreadSheet, { spreadSheet -> ①

  sheets.each{ gSheet -> ②


  String hojaId = gSheet.properties.title
  println "Validando schema de $hojaId"

  withSheet hojaId, { sheet -> ③

  def sheetColumns = readRows("A1", "AA1").first() ④

  def tableColumns = metadataTable(gSheet.properties.title)

  assert sheetColumns.intersect(tableColumns).size() == sheetColumns.size(), """


  $gSheet.properties.title incompatible schemas:
  Sheet: $sheetColumns
  MySQL: $tableColumns
  """
  }
  }
}
//end::schemas[]

//tag::g2d[]
if( google2Database ) {
  sheetScript.withSpreadSheet options.spreadSheet, { spreadSheet ->
  sheets.each { gSheet ->
  String hojaId = gSheet.properties.title
  println "Importando hoja $hojaId"

  withSheet hojaId, { sheet ->


  def sheetColumns = readRows("A1", "AA1").first()
  cleanTable(hojaId)

  int idx = 2
  def rows = readRows("A$idx", "AA${idx + 100}")
  while (rows) {
  insertRowsIntoTable(rows, hojaId, sheetColumns)
  idx += rows.size()
  rows = readRows("A$idx", "AA${idx + 100}")
  }

  }

  }
  }
}
//end::g2d[]

//tag::d2g[]
if( !google2Database ) {
  sheetScript.withSpreadSheet options.spreadSheet, { spreadSheet ->
  sheets.each{ gSheet ->
  String hojaId = gSheet.properties.title
  println "Exportando a hoja $hojaId"

  withSheet hojaId, { sheet ->

140 |
101 Groovy Script

  def sheetColumns = readRows("A1", "AA1").first()


  clearRange('A','AA')

  appendRow(sheetColumns)

  int idx=0
  def rows = moreRows(hojaId, idx, 100)
  while( rows.size() ){

  appendRows(rows)
  idx+=rows.size()+1
  rows = moreRows(hojaId, idx, 100)
  }
  }

  }
  }
}
//end::d2g[]

| 141
101 Groovy Script

Gestionando permisos de Google Drive


Jorge Aguilera <jorge.aguilera@puravida-software.com [mailto:jorge.aguilera@puravida-
software.com]> 2018-10-12

Para este post es necesario tener una cuenta en Google así como
tener al menos algunos ficheros en Google Drive. Así mismo para poder
acceder a las APIs de Google deberemos obtener unas credenciales que
autorizen a la aplicación a acceder a nuestra cuenta. Para obtener dichas
credenciales debes acceder a https://console.google.developers y crearlas
desde allí. Para este script el tipo de credenciales serán de tipo cuenta (es
decir el script se ejecutará en representación del usuario una vez que lo
autorize)

Hace unos días un usuario/amigo me planteaba una duda en Twitter sobre Groogle:

tengo un problema con mi Drive. Tengo cienes de carpetas y docs


compartidos con URL y los quiero "descompartir" todos los que están
dentro de una carpeta …

— Michael Gallego, https://twitter.com/micael_gallego/status/1049370562487865347

A partir de la versión 1.5.1 Groogle incluye un DSL específico para la gestión de permisos
que analizaremos en este post.

Dependencias
La versión 1.5.1 no ha sido liberada todavía, así que utilizaremos la versión en preview
disponible en Bintray:

@GrabResolver(name='puravida', root="https://dl.bintray.com/puravida-software/repo" )
@Grapes([
  @Grab(group = 'com.puravida.groogle', module = 'groogle-drive', version = '1.5.1-dev.32+5d37817')
])
import com.google.api.services.drive.DriveScopes
import com.puravida.groogle.DriveScript

Opciones y login
El script acepta un parámetro para indicar el comando a ejecutar y si es necesario un
segundo argumento. Al ser un script de ejemplo no es la intención demostrar las
posibilidades de Groovy para trabajar con argumentos proporcionados por línea de

142 |
101 Groovy Script

comando (en la sección Básico puedes encontrar un post donde se explica mejor)

Así mismo como todas las opciones van a necesitar identificar al usuario, usamos una
función login de utilidad

drive = DriveScript.instance ①

void login() {
  drive.with {
  login {
  applicationName 'test-permissions'
  withScopes DriveScopes.DRIVE
  usingCredentials '/client_secret.json'
  asService false ②
  }
  }
}

login() ③

switch( args[0] ){ ④
  case "dumpFile":
  return dumpFile(args[1])
  case "dumpFolder":
  return dumpPermissions(args.length>1 ?args[1]:null)
  case "removeFolder":
  return removePermissions(args.length>1 ?args[1]:null)
  case "removeFile":
  return removeFilePermissions(args[1])
  case "shareFile":
  return shareFilePermissions(args[1],args[2])
}

① referencia a DriveScript

② usaremos una cuenta de usuario para acceder a todos sus documentos

③ la primera vez abrirá un navegador para identificarnos. Las siguientes usará la caché

④ opciones que nos va a permitir el script

De forma resumida, tras hacer login, el script nos permitirá indicar para cada ejecución:

• si queremos volcar por pantalla los permisos de un fichero (necesitamos su id)

• si queremos volcar TODOS los ficheros de mi Drive (no pasamos argumento) o de una
carpeta (proporcionamos su id)

• si queremos revocar todos los permisos de un fichero (necesitamos su id)

• si queremos revocar TODOS los permisos excepto owner de TODOS los ficheros de mi
Drive (no pasamos argumento) o de una carpeta (proporcionamos su id)

| 143
101 Groovy Script

• compartir un fichero con un usuario proporcionado como segundo argumento

Existen más casos de uso, como revocar permisos sólo a ficheros en una carpeta,
compartir con role de edición, etc que son fáciles de implementar siguiendo este post y la
documentación de Groogle

dumpFile / dumpAll
Estos métodos se ofrecen a nivel de comprobar los permisos que tiene un fichero/carpeta
en un momento dado. Podemos ejecutarlo por ejemplo al principio para comprobar la
situación y al final para ver si se han aplicado los permisos que queríamos.

void dumpFile(fileId){
  drive.with {
  withFile fileId, {
  println "$file.name ($file.id)"
  file.permissions.each { permission ->
  println "\t $permission"
  }
  }
  }
}
void dumpPermissions(folderId) {
  def self = this
  drive.with {
  withFiles {
  if(folderId)
  parentId folderId
  eachFile { file, idx ->
  println "$id: $file.name ($file.id)"
  file.permissions.each { permission ->
  println "\t $permission"
  }
  if( file.mimeType=='application/vnd.google-apps.folder') ①
  self.dumpPermissions(file.id)
  }
  }
  }
}

① workarround para trabajar con las subcarpetas

Ambos comandos son parecidos diferenciandose en que uno actúa sobre un fichero dado
y el otro recorre todos los ficheros de una carpeta.

El script, una vez obtenido el fichero (OJO! no es un java.io.File sino un


com.google.api.services.drive.model.File) itera sobre sus permisos, por ejemplo para
volcarlos por pantalla obteniendo un resultado similar a:

144 |
101 Groovy Script

Documento con 2 permisos (owner y user)

Ejemplo1 (1HRnKA0o_xxxxxxxSSSSSSSSaaaaaaa)
  [deleted:false, displayName:Jorge Aguilera Gonzalez, emailAddress:jorge.aguilera@puravida-
software.com, id:12312, kind:drive#permission, photoLink:https://lh3.googleusercontent.com/photo.jpg,
role:reader, type:user]
  [deleted:false, displayName:groovy groogle, emailAddress:groovy.groogle@gmail.com, id:111,
kind:drive#permission, photoLink:https://lh6.googleusercontent.com/s64/photo.jpg, role:owner, type:user]

Utilizando las APIs de Google podríamos en este punto eliminar o añadir nuevos
permisos, pero Groogle nos permite hacerlo mediante un DSL permissions

removeFile / removeAll
Al igual que los métodos dump, ambos comandos son similares salvo que uno actúa sobre
un fichero y el otro sobre todos. En ambos casos vamos a eliminar todos los permisos que
pudiera tener el fichero excepto el de owner (que por otra parte Google no nos dejaría
dando un error)

La misión del DSL es hacer coincidir los permisos existentes con los indicados en el
mismo, permitiendo chequear diferentes tipos y roles. Por ejemplo:

• permitir a ciertos usuarios editar el documento

• permitir a ciertos usuarios leer el documento

• permitir a cualquiera con el enlace leer el documento

• permitir a los usuarios de un dominio editar el documento

• etc

Para ello provee diferentes métodos como usersAsReader 'email@email.email' para indicar
que queremos que un usuario dado pueda leer el documento.

Por defecto el DSL buscará aquellos que hayan sido especificados y NO existen en el
documento y los creará.

Si además especificamos strick true el DSL se ejecutará en modo estricto y borrará


aquellos que estén presentes en el fichero pero no se haya permitido en el DSL

Eliminar todos los permisos

  withFile fileId, { file ->


  permissions {
  strick true
  }
  }

En el ejemplo anterior estamos eliminando TODOS los permisos de acceso (excepto el de


owner) que pudiera tener el documento.

| 145
101 Groovy Script

Si una vez ejecutado el comando para eliminar permisos volvieramos a ejecutar el dump
obtendríamos algo parecido a:

Documento con 1 permiso (owner)

Ejemplo1 (1HRnKA0o_xxxxxxxSSSSSSSSaaaaaaa)
  [deleted:false, displayName:groovy groogle, emailAddress:groovy.groogle@gmail.com, id:111,
kind:drive#permission, photoLink:https://lh6.googleusercontent.com/s64/photo.jpg, role:owner, type:user]

shareFile
Por último el script nos va a permitir compartir un fichero con un usuario,
proporcionados ambos por línea de comando para que este pueda acceder en modo
lectura al documento.

Share File

  withFile fileId, {
  permissions {
  usersAsReader args

  //others:
  //usersAsWriter args
  //domainAsReader args
  //anyoneAsReader
  //anyoneAsWriter

  strick true
  }
  }

En un mismo DSL permisssions podemos especificar diferentes casos como por ejemplo:

• usersAsWriter para especificar una lista de usuarios que pueden editar

• domainAsReader para especificar un dominio como lectura

• anyoneAsRead para indicar que cualquiera con el enlace puede leer

• etc

Documentación adicional
Para más info se puede consultar la documentación de Groogle:

https://puravida-software.gitlab.io/groogle/groogle-drive/

146 |
101 Groovy Script

Raffle (Google Sheet + Meetups)


Jorge Aguilera <jorge.aguilera@puravida-software.com [mailto:jorge.aguilera@puravida-
software.com]> 2019-04-9

Para este post vamos a necesitar los siguientes requisitos: una


cuenta en Google y una hoja de cálculo Sheet de Google. La estructura de
la hoja puede ser como quieras pero para este caso vamos a usar las 3
primeras columnas con los valores "nombre", "apellido" y "email"

En este post vamos a utilizar un GroovyScript para leer una hoja Sheet de Google donde
los asistentes a una charla se han registrado (tal vez con Google Form) para realizar un
sorteo eligiendo a uno de ellos al azar. Si el elegido no se encuentra (o rechaza el premio)
se le elimina de la lista y se vuelve a realizar el sorteo.

Consola Google
En primer lugar deberemos crear una aplicación en la consola de Google en
https://console.developers.google.com

En esta aplicación habilitaremos (al menos) la API "Google Sheet API"

Así mismo deberemos crear unas credenciales de "usuario" obteniendo la posibilidad de


descargarlas en un fichero JSON y que deberemos ubicar junto con el script.

Groogle
Para facilitar la parte técnica de autentificación y creación de servicios he creado un
proyecto de código abierto, Groogle, disponible en https://groogle.gitlab.com/ el cual
publica un artefacto en Bintray y que podremos usar en nuestros scripts simplemente
incluyendo su repositorio.

@Grab(group='com.puravida.groogle', module='groogle-sheet', version='2.0.0')


import com.google.api.services.sheets.v4.SheetsScopes
import com.puravida.groogle.*
import com.puravida.groogle.sheet.*
import groovy.swing.SwingBuilder

import java.awt.BorderLayout
import java.awt.BorderLayout as BL
import javax.swing.*

Groogle nos permitirá realizar el login de nuestra aplicación y guardar la autorización en


nuestro disco de tal forma que en ejecuciones posteriores no se requiera de autorizar de

| 147
101 Groovy Script

nuevo la aplicación (mientras no borremos el fichero con la autorización generada)

  credentials {
  applicationName 'raffle'
  withScopes SheetsScopes.SPREADSHEETS
  usingCredentials "client_secret.json"
  asService false
  }
  service(SheetServiceBuilder.build(), SheetService)

① client-secret.json es el fichero que hemos descargado de la consola de Google y que


contiene las credenciales

② Groogle realiza el login y guarda la autorización en


$HOME/.credentials/${applicationName}

③ En este post únicamente requerimos acceso a Sheet para lectura

Leyendo la hoja
Mediante un bucle iremos recuperando filas en bloques de 100 hasta que no haya más
elementos y los iremos añadiendo a un List

(La hoja se puede llamar como se desee, por ejemplo una hoja por cada Meetup y sería
fácilmente adaptable para ser proporcionada por parámetro. En este ejemplo vamos a
usar raffle-example-meetup-1 )

  withSheet 'raffle-example-meetup-1', {
  int i=2
  range = writeRange("A$i", "C${i+99}").get()
  while( range ){
  all.addAll range
  i+=99
  range = writeRange("A$i", "C${i+99}").get()
  }
  }

Presentación
Para la interface de usuario vamos a utilizar SwingBuilder , un DSL de groovy, que permite
crear interfaces Swing de forma fácil.

Nuestro interface va a consistir básicamente en un JList con el array de asistentes y un


botón que realizará la acción de elegir uno de ellos aleatoriamente.

Tras mostrar el elegido, si el moderador lo desea puede indicar que el asistente no está
para repetir el sorteo sin él.

148 |
101 Groovy Script

new SwingBuilder().edt {
  frame(title: 'GroogleRaffe',
  defaultCloseOperation: JFrame.EXIT_ON_CLOSE,
  extendedState: JFrame.MAXIMIZED_BOTH,
  show: true) {

  borderLayout()

  panel(constraints: BorderLayout.PAGE_START){
  label("Meetup Raffle")
  }

  panel(constraints: BorderLayout.PAGE_END){
  button(text:'Raffle',
  actionPerformed: {
  Random rnd = new Random()
  int selected = rnd.nextInt(all.size())
  String msg = "<html><b>Is <font color='red'>${all[selected][0]} ${all[selected][1]}
here ?</font></b></html>"
  int yepes = JOptionPane.showConfirmDialog(current,msg)
  if( yepes != JOptionPane.YES_OPTION ){
  theList.model.remove(selected)
  }
  }, constraints:BL.SOUTH)

  }

  scrollPane(constraints: BorderLayout.CENTER){
  list(id:'theList', listData: all,
  cellRenderer: { list, value, index, isSelected, cellHasFocus->
  new JLabel(value[0]+" "+value[1])
  }
  )
  }

  }
}

Ejemplo

| 149
101 Groovy Script

150 |
101 Groovy Script

Organizando eventos
Jorge Aguilera <jorge.aguilera@puravida-software.com [mailto:jorge.aguilera@puravida-
software.com]> 2018-02-4

Para este post vamos a necesitar los siguientes requisitos: una


cuenta, una SpreadSheet y un Calendario todo ello de Google . Se
recomienda crear un calendario específico para este ejercicio puesto que
vamos a borrar y crear eventos en el mismo de forma genérica y si lo
hacemos sobre el principal podríamos perder eventos de nuestro interés.
Así mismo para poder acceder a las APIs de Google deberemos obtener
unas credenciales que autoricen a la aplicación para acceder a nuestra
cuenta.

Este post está inspirado en una iniciativa de Alba Roza (@Alba_Roza)


 https://twitter.com/Alba_Roza/status/957936830657257473

Consola Google
En primer lugar deberemos crear una aplicación en la consola de Google en
https://console.developers.google.com

En esta aplicación habilitaremos las APIs de "Google Calendar API" y "Google Sheet API"

Así mismo deberemos crear unas credenciales de "Servicio" obteniendo la posibilidad de


descargarlas en un fichero JSON y que deberemos ubicar junto con el script.

Groogle
Para facilitar la parte técnica de autentificación y creación de servicios he creado un
proyecto de código abierto, Groogle, disponible en https://puravida-software.gitlab.io/
groogle/ el cual publica un artefacto en Bintray y que podremos usar en nuestros scripts
simplemente incluyendo su repositorio.

Este proyecto se divide a su vez en 4 subprojectos:

• groogle-core, contiene la parte de autentificación principalmente

• groogle-drive, para el manejo de ficheros y carpetas en Drive

• groogle-sheet, para la gestión específica de hojas de cálculo Sheet

• groogle-calendar, para la gestión específica de calendarios

| 151
101 Groovy Script

En nuestro caso vamos a utilizar estos dos últimos (el primero se autoincluye por
dependencia transitiva)

@GrabResolver(name='puravida', root="https://dl.bintray.com/puravida-software/repo" )
@Grab(group = 'com.puravida.groogle', module = 'groogle-core', version = '1.4.1')
@Grab(group = 'com.puravida.groogle', module = 'groogle-sheet', version = '1.4.1')
@Grab(group = 'com.puravida.groogle', module = 'groogle-calendar', version = '1.4.1')
@GrabConfig(systemClassLoader=true)

import com.google.api.services.calendar.CalendarScopes
import com.google.api.services.sheets.v4.SheetsScopes
import com.puravida.groogle.GroogleScript
import com.puravida.groogle.CalendarScript
import com.puravida.groogle.SheetScript

Groogle nos permitirá realizar el login de nuestra aplicación y guardar la autorización en


nuestro disco de tal forma que en ejecuciones posteriores no se requiera de autorizar de
nuevo la aplicación (mientras no borremos el fichero con la autorización generada)

GroogleScript.instance.applicationName='101-groovy'
clientSecret = this.class.getResource('client_secret.json').newInputStream()
GroogleScript.instance.login(clientSecret,[CalendarScopes.CALENDAR, SheetsScopes.SPREADSHEETS])
CalendarScript.instance.groogleScript=GroogleScript.instance
SheetScript.instance.groogleScript=GroogleScript.instance

Identificadores
Inicializamos (por comodidad) el id de la hoja de cálculo así como el del calendario

sheetId='xxxxxxxxxxxxxxxx_yyyyyyyyy-fMLg'
calendarId='aaaaaaabbbbbbbbbccccccc@group.calendar.google.com'

Sheet
Para nuestro ejemplo la hoja Eventos a utilizar será muy sencilla y contendrá la siguiente
estructura:

año mes día evento lugar

2018 1 1 Fiesta Año mi casa


Nuevo

2018 2 1 Codificando Mordor


como locos

Como los eventos que vamos a gestionar son eventos de todo el día vamos a formatear la

152 |
101 Groovy Script

fecha a YYYY-MM-DD lo cual nos permitirá posteriormente crear eventos en el calendario


de forma muy cómoda. Si por ejemplo quisieramos tratar eventos "desde-hasta"
simplemente modificaríamos la estructura de la hoja y no realizaríamos este formateo

events=[]
SheetScript.instance.withSpreadSheet sheetId, {
  withSheet 'Eventos',{
  int rowIdx=2

  def row = readRows("A$rowIdx","E$rowIdx")


  def calendarScript = CalendarScript.instance

  while( row ){
  def when =sprintf( '%1$04d-%2$02d-%3$02d',row.first()[0] as int,row.first()[1] as int,row.
first()[2] as int)
  def summary ="${row.first()[3]}"
  def description="${row.first()[4]}"
  println "$when $summary $description"
  events.add([when:when,summary:summary,description:description])
  rowIdx++
  row = readRows("A$rowIdx","E$rowIdx")
  }
  }
}

Recorremos todas las filas (excepto la primera) y recogemos en un mapa los detalles de
cada evento. Al finalizar tendremos una lista de eventos que podremos "limpiar", unificar,
etc

Calendar
Una vez que disponemos de los eventos realizaremos en primer lugar una limpieza del
calendario:

CalendarScript.instance.withCalendar( calendarId, {
  eachEvent {
  removeFromCalendar()
  }
})

Por último simplemente tenemos que recorrer la lista de los eventos e ir creandolos en
nuestro calendario. Como ya hemos dicho nuestro ejemplo es para eventos todo el día por
lo que usaremos el método allDay yyyy-MM-dd Si quisieramos gestionar eventos más
complejos (varios días, desde una hora hasta otra, etc) utilizaríamos el método between
dateTime1 dateTime2

Así mismo podríamos añadir recordatorios a ciertos eventos, añadir lista de personas
interesadas etc.

| 153
101 Groovy Script

events.each{ evt ->


  CalendarScript.instance.createEvent( calendarId,{
  event.summary = evt.summary
  event.description = evt.description
  allDay evt.when
  })
}

154 |
101 Groovy Script

Script

//tag::dependencies[]
@GrabResolver(name='puravida', root="https://dl.bintray.com/puravida-software/repo" )
@Grab(group = 'com.puravida.groogle', module = 'groogle-core', version = '1.4.1')
@Grab(group = 'com.puravida.groogle', module = 'groogle-sheet', version = '1.4.1')
@Grab(group = 'com.puravida.groogle', module = 'groogle-calendar', version = '1.4.1')
@GrabConfig(systemClassLoader=true)

import com.google.api.services.calendar.CalendarScopes
import com.google.api.services.sheets.v4.SheetsScopes
import com.puravida.groogle.GroogleScript
import com.puravida.groogle.CalendarScript
import com.puravida.groogle.SheetScript

//end::dependencies[]

sheetId='1hf6q540q45gvhdHH9BOVv_Ij7Wk0KVPFCs42Xc-fMLg'
calendarId='puravida-software.com_n9tci5mfaqqdegi5askjetpa7s@group.calendar.google.com'

//tag::login[]
GroogleScript.instance.applicationName='101-groovy'
clientSecret = this.class.getResource('client_secret.json').newInputStream()
GroogleScript.instance.login(clientSecret,[CalendarScopes.CALENDAR, SheetsScopes.SPREADSHEETS])
CalendarScript.instance.groogleScript=GroogleScript.instance
SheetScript.instance.groogleScript=GroogleScript.instance
//end::login[]

//tag::readFromSheet[]
events=[]
SheetScript.instance.withSpreadSheet sheetId, {
  withSheet 'Eventos',{
  int rowIdx=2

  def row = readRows("A$rowIdx","E$rowIdx")


  def calendarScript = CalendarScript.instance

  while( row ){
  def when =sprintf( '%1$04d-%2$02d-%3$02d',row.first()[0] as int,row.first()[1] as int,row.
first()[2] as int)
  def summary ="${row.first()[3]}"
  def description="${row.first()[4]}"
  println "$when $summary $description"
  events.add([when:when,summary:summary,description:description])
  rowIdx++
  row = readRows("A$rowIdx","E$rowIdx")
  }
  }
}
//end::readFromSheet[]

//tag::removeFromCalendar[]
CalendarScript.instance.withCalendar( calendarId, {
  eachEvent {
  removeFromCalendar()
  }
})
//end::removeFromCalendar[]

//tag::writeToCalendar[]
events.each{ evt ->
  CalendarScript.instance.createEvent( calendarId,{
  event.summary = evt.summary

| 155
101 Groovy Script

  event.description = evt.description
  allDay evt.when
  })
}
//end::writeToCalendar[]

156 |
101 Groovy Script

Crea tu propio DSL


Jorge Aguilera <jorge.aguilera@puravida-software.com [mailto:jorge.aguilera@puravida-
software.com]> 2018-02-4

En este artículo vamos a ver cómo puedes crear tu propia DSL


(domain specific language) con un ejemplo práctico donde tus usuarios
podrán generar gráficas de funciones usando una sintáxis específica
creada para ello. De esta forma podrás , por ejemplo, transformar las
complejidades técnicas a un lenguaje de negocio en aquellas situaciones
donde un simple paso de argumentos se queda corto.

Para este ejemplo vamos a utilizar la librería Exp4j


https://www.objecthunter.net/exp4j/ la cual es capaz de parsear una
 función cualquiera y evaluarla cuando le proporcionamos los valores de
las incógnitas.

Caso de uso
Queremos crear un script capaz de dibujar una gráfica de una función proporcionada por
el usuario. Para ello deberá proporcionar el rango de valores que quiere representar así
como la "granularidad" del dibujo. Así mismo si el usuario lo indica volcaremos a fichero
la gráfica genereda con el nombre que nos proporcione.

Como bola extra queremos darle la posibilidad de poder dibujar varias funciones en la
misma gráfica pudiendo indicar para cada una de ellas rangos diferentes.

Como se ve a simple vista los requisitos son demasiados complejos como para poder optar
por una solución basada en el paso de parámetros, por lo que optaremos por crearnos
nuestro propio lenguaje.

Exp4j
Antes de introducirnos en el diseño de la DSL vamos a ver la parte técnica que usaremos
para la generación del diagrama.

exp4j es una librería que permite parsear cualquier función (e incluso añadir las tuyas) y
proporcionandole los valores de las incógnitas puede evaluarla y devolvernos el valor.

Este es un ejemplo básico:

| 157
101 Groovy Script

Expression e = new ExpressionBuilder("3 * sin(x) - 2 / (x - 2)")


  .variables("x")
  .build()
  .setVariable("x", 2.3);
double result = e.evaluate();

Así pues si somos capaces de proporcionarle la función que nos indique el usuario
podremos realizar un bucle en el rango especificado proporcionando valores a Expression
y obtener así una serie de valores que la representan.

  private List calculateFunction(String function, String variable, double ini, double end, double step){
  Expression e = new ExpressionBuilder(function).variables(variable).build()
  def data=[]
  for(double i=ini; i<end; i+=step){
  e.setVariable(variable,i)
  double v = e.evaluate()
  data << [i, Double.isNaN(v) ? 0 : v]
  }
  data.flatten()
  }

También nos será cómodo tener un método que nos pueda agrupar varias series de
funciones calculadas en un mapa:

  private Map calculateFunctions(List functions, String variable, double ini, double end, double step){
  Map ret=[:]
  functions.each{func->
  ret[func] = calculateFunction(func, variable, ini, end, step)
  }
  ret
  }

Ejemplo DSL
Tras evaluar con nuestros usuarios las diferentes sintáxis que podemos crear y que le
serían legibles en su lenguaje de negocio llegamos a un acuerdo como el siguiente:

158 |
101 Groovy Script

// Podemos comentar el fichero con dos barras


/*
  o un bloque con barra + asterisco
*/

// queremos dibujar una funcion usando unos rangos


plot "sin(x*e)", {

  // podemos usar from to e incrementing en el orden que le sea más cómodo


  from (-4) to 6 incrementing 0.01
}

// podemos unir varios plots en el mismo diagrama con diferentes rangos

plot { //si no especificamos funcion lo podemos hacer dentro del DSL

  // "function" necesita una cadena con la funcion a dibujar


  // y "and" nos permite concatener funciones

  function "cos(sin(x))" and "x*cos(e*x)" and "t^4/x"

  // podemos especificar los rangos indicando from y to asi como la granularidad con incrementing
  from (-3) incrementing 0.01 to 3

  // no he sabido cómo usar directamente números negativos así que hay que recubrirlos con (-x)
}

// queremos que nos genere la imagen en fichero


saveAs "test3.png"

Como se puede ver en el ejemplo, una vez aprendida la sintáxis, al usuario le es muy fácil
crear nuevos casos de uso.

Como resultado, el usuario obtendrá un fichero "test3.png" con la imagen, parecida a la


siguiente:

| 159
101 Groovy Script

DSL
Como podemos ver nuestro DSL cuenta con 2 partes:

1. una lista de plot y el nombre de un fichero a generar

2. cada plot especifica un caso con

a. una o varias function que se pueden indicar de varias formas

b. un rango con from to e incrementing

PlotDSL
PlotDSL será una clase embebida en el propio script que nos permitirá modelar la parte
más interna de nuestro DSL. Implementará los métodos "from", "to", "and", "function", etc
y básicamente irá guardando en properties los valores que se vayan pasando a cada uno.
Contiene también, por simplificar el ejemplo, la parte de lógica de negocio en la que con
estos valores genera las series de valores vista al principio aunque esto puede realizarse
en alguna otra parte del script.

160 |
101 Groovy Script

  List<String> _functions;
  PlotDSL(){ ①
  _functions=[]
  }
  PlotDSL(List<String> functions){①
  _functions = functions
  }

  PlotDSL function(String f){ ①


  _functions = [ f ]
  this
  }

  PlotDSL and(String f){ ②


  _functions << f
  this
  }

  String _variable="x"
  PlotDSL using(String variable){ ③
  _variable = variable
  this
  }

  double _from
  PlotDSL from(idx){
  _from=idx as double
  this
  }

  double _to
  PlotDSL to(idx){
  _to=idx as double
  this
  }

  double _steps
  PlotDSL incrementing(idx){
  _steps=idx as double
  this
  }

  Map series(){ ④
  calculateFunctions(_functions, _variable, _from, _to, _steps)
  }

① 3 maneras de inicializar la función inicial

② cuando escribimos and "x*cos(e*x)" estamos ejecutando el metodo and(String func)

| 161
101 Groovy Script

pasando x*cos(e*x) como parámetro

③ using nos permite indicar a exp4j el nombre de la variable que usamos en la funcion
i.e. t*cos(e*t) using "t"

④ método ajeno al DSL que realiza la lógica de negocio de convertir las funciones a series
de datos

PlotsDSL
PlotsDSL (ojo a la s) será la clase embebida encargada de construir los diferentes plot que
pueda haber en el DSL.

Un PlotsDSL contiene un array de PlotDSL al que irá añadiendo elementos según se


invoque a su función plot

Esta función plot tiene dos variantes:

• con una closure como argumento

• con un string y una cloruse que corresponde

plot "sin(x*e)", {
}

plot {
}

Así pues PlotsDSL implementa las dos formas donde una de ellas es simplemente una
comodidad para invocar a la segunda. Lo importante en este punto es que la closure que
recibe como argumento será invocada pero usando un PlotDSL como contexto

  PlotsDSL plot( Closure closure){


  plot(null,closure)
  }

  PlotsDSL plot( String function, Closure closure){


  PlotDSL plot = function ? new PlotDSL( [function] ) : new PlotsDSL()
  Closure cl = closure.clone()
  cl.resolveStrategy = Closure.DELEGATE_ONLY ①
  cl.delegate=plot ②
  cl(plot) ③
  plots << plot ④
  this
  }

① Usaremos una estrategia de delegación entre las varias que permite Groovy

② el código de la closure se ejecutará en el contexto del delegate

162 |
101 Groovy Script

③ invocamos a la closure (puede hacerse tambien como cl.call() )

④ una vez ejecutada la closure el objeto plotDSL ha sido inicializado

De texto a Closure
El primer argumento a nuestro script será un fichero que contiene el DSL que queremos
ejecutar así que el script lo leerá y convertirá el texto a closure. Como "mejora" si el
fichero no existe, el script interpretará que la unión de todos los argumentos como cadena
será el DSL a ejecutar

Para evaluar un texto y convertirlo a Closure usaremos el método evaluate de GroovyShell.


Simplemente tenemos que tener en cuenta que la cadena que espera este método
represente una closure por lo que recubrimos el texto del fichero mediante llaves { →
$texto }

txt=args.join(' ')
if( args.length == 1 && new File(args[0]).exists() ){
  txt = new File(args[0]).text
}

cl = new GroovyShell().evaluate("{ dsl-> $txt}")


plotsDSL = new PlotsDSL()
plotsDSL.parse(cl)

JavaFX
Por último sólo resta que un PlotsDSL ejecute la closure en su contexto y si no se produce
un error generar la vista con la lógica de negocio generada por aquel, para lo que
usaremos GroovyFX (Groovy+JavaFX) y su propia DSL

| 163
101 Groovy Script

void saveScreenshot(saveNode, fileName){


  def snapshot = saveNode.snapshot(new SnapshotParameters(), null);
  BufferedImage bufferedImage = new BufferedImage(saveNode.width as int, saveNode.height as int,
BufferedImage.TYPE_INT_ARGB);
  BufferedImage image = javafx.embed.swing.SwingFXUtils.fromFXImage(snapshot, bufferedImage)

  File file = new File(fileName)


  ImageIO.write(image, "png", file )
}

start {
  stage(id:'st',title: "GrExp4j", x: 1024, y: 768, visible: true, style: "decorated", primary: true) {
  scene(id: "sc", width: 1024, height: 768) {
  saveNode=lineChart(animated:true, createSymbols:false, legendVisible:true, data: plotsDSL.
series())
  }
  }

  if( plotsDSL._screenshot ){
  saveScreenshot(saveNode,plotsDSL._screenshot)
  }
}

Simplemente crea una aplicación JavaFX y crea un lineChart proporcionandole las series
de datos que construirá nuestro DSL. Si en el DSL se indica que queremos generar un
gráfico usaremos la funcion saveScreenshot para ello.

164 |
101 Groovy Script

Script

@Grapes([
  @Grab(group='net.objecthunter', module='exp4j', version='0.4.8'),
  @Grab(group='org.groovyfx',module='groovyfx',version='8.0.0',transitive=false, noExceptions=true),
])

import net.objecthunter.exp4j.function.Function
import net.objecthunter.exp4j.operator.Operator
import net.objecthunter.exp4j.*

import static groovyx.javafx.GroovyFX.start


import javafx.application.Platform
import javafx.scene.SnapshotParameters
import javax.imageio.ImageIO
import java.awt.image.BufferedImage

import groovy.transform.ToString

class PlotDSL{

//tag::plotDSL[]
  List<String> _functions;
  PlotDSL(){ ①
  _functions=[]
  }
  PlotDSL(List<String> functions){①
  _functions = functions
  }

  PlotDSL function(String f){ ①


  _functions = [ f ]
  this
  }

  PlotDSL and(String f){ ②


  _functions << f
  this
  }

  String _variable="x"
  PlotDSL using(String variable){ ③
  _variable = variable
  this
  }

  double _from
  PlotDSL from(idx){
  _from=idx as double
  this
  }

  double _to
  PlotDSL to(idx){
  _to=idx as double
  this
  }

  double _steps
  PlotDSL incrementing(idx){
  _steps=idx as double
  this
  }

| 165
101 Groovy Script

  Map series(){ ④
  calculateFunctions(_functions, _variable, _from, _to, _steps)
  }
//end::plotDSL[]

  // tag::calculateFunction[]
  private List calculateFunction(String function, String variable, double ini, double end, double step){
  Expression e = new ExpressionBuilder(function).variables(variable).build()
  def data=[]
  for(double i=ini; i<end; i+=step){
  e.setVariable(variable,i)
  double v = e.evaluate()
  data << [i, Double.isNaN(v) ? 0 : v]
  }
  data.flatten()
  }
  // end::calculateFunction[]

  // tag::calculateFunctions[]
  private Map calculateFunctions(List functions, String variable, double ini, double end, double step){
  Map ret=[:]
  functions.each{func->
  ret[func] = calculateFunction(func, variable, ini, end, step)
  }
  ret
  }
  // end::calculateFunctions[]

class PlotsDSL{

  List<PlotDSL> plots = []

  PlotsDSL parse( Closure closure ){


  Closure cl = closure.clone()
  cl.resolveStrategy = Closure.DELEGATE_ONLY
  cl.delegate=this
  cl(this)
  }

  //tag::plotsConstructor[]
  PlotsDSL plot( Closure closure){
  plot(null,closure)
  }

  PlotsDSL plot( String function, Closure closure){


  PlotDSL plot = function ? new PlotDSL( [function] ) : new PlotsDSL()
  Closure cl = closure.clone()
  cl.resolveStrategy = Closure.DELEGATE_ONLY ①
  cl.delegate=plot ②
  cl(plot) ③
  plots << plot ④
  this
  }
  //end::plotsConstructor[]

  String _screenshot
  PlotsDSL saveAs(String screenshot){
  _screenshot=screenshot
  this
  }

  Map series(){

166 |
101 Groovy Script

  def ret=[:]
  plots.each{ plot->
  plot.series().each{ serie->
  ret[serie.key] = serie.value
  }
  }
  ret
  }
}

// tag::txtAClosure[]
txt=args.join(' ')
if( args.length == 1 && new File(args[0]).exists() ){
  txt = new File(args[0]).text
}

cl = new GroovyShell().evaluate("{ dsl-> $txt}")


plotsDSL = new PlotsDSL()
plotsDSL.parse(cl)
// end::txtAClosure[]

//tag::view[]
void saveScreenshot(saveNode, fileName){
  def snapshot = saveNode.snapshot(new SnapshotParameters(), null);
  BufferedImage bufferedImage = new BufferedImage(saveNode.width as int, saveNode.height as int,
BufferedImage.TYPE_INT_ARGB);
  BufferedImage image = javafx.embed.swing.SwingFXUtils.fromFXImage(snapshot, bufferedImage)

  File file = new File(fileName)


  ImageIO.write(image, "png", file )
}

start {
  stage(id:'st',title: "GrExp4j", x: 1024, y: 768, visible: true, style: "decorated", primary: true) {
  scene(id: "sc", width: 1024, height: 768) {
  saveNode=lineChart(animated:true, createSymbols:false, legendVisible:true, data: plotsDSL.
series())
  }
  }

  if( plotsDSL._screenshot ){
  saveScreenshot(saveNode,plotsDSL._screenshot)
  }
}
//end::view[]

| 167
101 Groovy Script

Xml a Calendar
Jorge Aguilera <jorge.aguilera@puravida-software.com [mailto:jorge.aguilera@puravida-
software.com]> 2018-05-10

Para este post vamos a necesitar los siguientes requisitos: una


cuenta de Google y crear un Calendario de Google. Se recomienda crear un
calendario específico para este ejercicio puesto que vamos a borrar y crear
eventos en el mismo de forma genérica y si lo hacemos sobre el principal
podríamos perder eventos de nuestro interés. Así mismo para poder
acceder a las APIs de Google deberemos obtener unas credenciales que
autoricen a la aplicación para acceder a nuestra cuenta.

En este post vamos ver cómo consumir un XML con los eventos gratuitos
que se van a realizar en las bibliotecas municipales de Madrid usando el
servicio de Datos Abiertos. Nuestro script se va a ejecutar de forma
 periódica, obteniendo del servicio remoto los eventos para los próximos
60 días, parseando el Xml y creando los eventos en el calendario con la
información de interés (Título, descripción, fecha de inicio y fin, etc).
Para más información consultar https://datos.madrid.es/portal/site/egob/

Xml
El endpoint que ofrece la web de Datos Abiertos de Madrid nos devuelve un array de
elementos contenido tal como se muestra a continuación:

168 |
101 Groovy Script

<contenido>
<tipo>Evento</tipo>
<atributos idioma="es">
<atributo nombre="ID-EVENTO">10701791</atributo>
<atributo nombre="TITULO">A la carta</atributo>
<atributo nombre="GRATUITO">1</atributo>
<atributo nombre="EVENTO-LARGA-DURACION">0</atributo>
<atributo nombre="FECHA-EVENTO">2018-05-19 00:00:00.0</atributo>
<atributo nombre="FECHA-FIN-EVENTO">2018-05-19 23:59:00.0</atributo>
<atributo nombre="HORA-EVENTO">12:00</atributo>
<atributo nombre="DESCRIPCION">
<![CDATA[ A la carta: Fundación Arte que alimenta (clásica). ]]>
</atributo>
<atributo nombre="CONTENT-URL">
http://www.madrid.es/sites/v/index.jsp?vgnextchannel=1ccd566813946010VgnVCM100000dc0ca8c0RCRD&vgnextoid=60d3e
5e031b23610VgnVCM2000001f4a900aRCRD
</atributo>
<atributo nombre="TITULO-ACTIVIDAD">Conciertos en la Biblioteca Eugenio Trías</atributo>
<atributo nombre="CONTENT-URL-ACTIVIDAD">
http://www.madrid.es/sites/v/index.jsp?vgnextchannel=1ccd566813946010VgnVCM100000dc0ca8c0RCRD&vgnextoid=8c43e
5e031b23610VgnVCM2000001f4a900aRCRD
</atributo>
<atributo nombre="LOCALIZACION">
<atributo nombre="CONTENT-URL-INSTALACION">
http://www.madrid.es/sites/v/index.jsp?vgnextchannel=9e4c43db40317010VgnVCM100000dc0ca8c0RCRD&vgnextoid=e791b
ed05ceed310VgnVCM1000000b205a0aRCRD
</atributo>
<atributo nombre="NOMBRE-INSTALACION">
Biblioteca Pública Municipal Eugenio Trías. Casa de Fieras de El Retiro
</atributo>
<atributo nombre="ID-INSTALACION">6893633</atributo>
<atributo nombre="COORDENADA-X">442345</atributo>
<atributo nombre="COORDENADA-Y">4474221</atributo>
<atributo nombre="LATITUD">40.4166091081099</atributo>
<atributo nombre="LONGITUD">-3.6795930368034804</atributo>
<atributo nombre="LOCALIDAD">MADRID</atributo>
<atributo nombre="PROVINCIA">MADRID</atributo>
<atributo nombre="DISTRITO">RETIRO</atributo>
</atributo>
<atributo nombre="TIPO">/contenido/actividades/Musica</atributo>
</atributos>
</contenido>

De todos estos datos nosotros vamos a usar sólo un pequeño subconjunto de ellos como
son TITULO-ACTIVIDAD, FECHA-EVENTO y LOCALIZACION, teniendo este último la
particularidad de que es a su vez un array de attributo

Para ello tendremos que ser capaces de buscar en los nodos aquellos que tengan el
atributo nombre con el valor de interés

Consola Google
En primer lugar deberemos crear una aplicación en la consola de Google en
https://console.developers.google.com

En esta aplicación habilitaremos al menos las APIs de "Google Calendar API"

| 169
101 Groovy Script

Así mismo deberemos crear unas credenciales de "OAuth" obteniendo la posibilidad de


descargarlas en un fichero JSON y que deberemos ubicar junto con el script.

Groogle
Para facilitar la parte técnica de autentificación y creación de servicios he creado un
proyecto de código abierto, Groogle, disponible en https://puravida-software.gitlab.io/
groogle/ el cual publica un artefacto en Bintray y que podremos usar en nuestros scripts
simplemente incluyendo su repositorio.

Este proyecto se divide a su vez en otros subprojectos, como por ejemplo:

• groogle-core, contiene la parte de autentificación principalmente

• groogle-drive, para el manejo de ficheros y carpetas en Drive

• groogle-sheet, para la gestión específica de hojas de cálculo Sheet

• groogle-calendar, para la gestión específica de calendarios

En nuestro caso vamos a utilizar el último (el primero se autoincluye por dependencia
transitiva)

@Grab(group='io.github.http-builder-ng', module='http-builder-ng-apache', version='1.0.3')


@Grab(group='ch.qos.logback', module='logback-classic', version='1.2.3')

@Grab(group = 'com.puravida.groogle', module = 'groogle-core', version = '1.5.0-alpha2')


@Grab(group = 'com.puravida.groogle', module = 'groogle-calendar', version = '1.5.0-alpha2')
@GrabConfig(systemClassLoader=true)

import com.google.api.services.calendar.CalendarScopes
import com.puravida.groogle.GroogleScript
import com.puravida.groogle.CalendarScript

import groovyx.net.http.*
import static groovyx.net.http.HttpBuilder.configure
import static groovyx.net.http.ContentTypes.JSON
import static groovy.json.JsonOutput.prettyPrint
import static groovy.json.JsonOutput.toJson

Groogle nos permitirá realizar el login de nuestra aplicación y guardar la autorización en


nuestro disco de tal forma que en ejecuciones posteriores no se requiera de autorizar de
nuevo la aplicación (mientras no borremos el fichero con la autorización generada)

  login {
  applicationName 'groogle-example'
  withScopes CalendarScopes.CALENDAR
  usingCredentials new File('client_secret.json')
  asService false
  }

170 |
101 Groovy Script

Eliminando antiguos
Para mantener el calendario "limpio" con sólo los eventos de interés vamos a realizar en
primer lugar un borrado de todos los eventos que existen en el mismo

  String groogleCalendarId = "0v757vib7jf1hj0bjsq041vco8@group.calendar.google.com"


  withCalendar groogleCalendarId, {
  eachEvent{
  removeFromCalendar()
  }
  }

Consumiendo XML
Para realizar la petición al servicio remoto usaremos HttpBuilder-Ng el cual nos permite
recuperar el contenido en formato Xml tal como se muestra a continuación:

XML = ["application/xml", "text/xml", "application/xhtml+xml", "application/atom+xml"]


xmlEncoder = NativeHandlers.Encoders.&xml

http = configure {
  request.uri = 'https://datos.madrid.es/egob/catalogo/206717-0-agenda-eventos-bibliotecas.xml'
  request.encoder XML, xmlEncoder
  request.contentType = 'application/xml'
}.get{

Una vez realizado el GET , el objeto http será un groovy.util.Node el cual nos permitirá
navegar a través de sus nodos buscando la información de interés.

Business
Por último sólo resta ir navegando por los nodos recuperados extrayendo la información
de interés y creando un evento en el calendario para cada una de ellas

| 171
101 Groovy Script

  http.contenido.each{
  String titulo = it.atributos.atributo.find{ it.'@nombre'=='TITULO-ACTIVIDAD' }.text()
  String descripcion = it.atributos.atributo.find{ it.'@nombre'=='DESCRIPCION' }.text()
  String inicio = it.atributos.atributo.find{ it.'@nombre'=='FECHA-EVENTO' }.text()
  String fin = it.atributos.atributo.find{ it.'@nombre'=='FECHA-FIN-EVENTO' }.text()
  String hora = it.atributos.atributo.find{ it.'@nombre'=='HORA-EVENTO' }.text()
  String where = it.atributos.atributo.find{ it.'@nombre'=='LOCALIZACION' }?.atributo.find{it
.'@nombre'=='NOMBRE-INSTALACION'}.text()

  Date dini = Date.parse('yyyy-MM-dd HH:mm:ss.S', inicio)


  Date dfin = Date.parse('yyyy-MM-dd HH:mm:ss.S', fin)

  if( descripcion.trim()=="" )
  return
  if( dini < fromDate )
  return

  createEvent groogleCalendarId, {
  it.event.summary = titulo
  it.event.description = "($hora) $descripcion"
  it.event.location=where
  from dini
  until dfin
  }
  }

172 |
101 Groovy Script

Script

//tag::dependencies[]
@Grab(group='io.github.http-builder-ng', module='http-builder-ng-apache', version='1.0.3')
@Grab(group='ch.qos.logback', module='logback-classic', version='1.2.3')

@Grab(group = 'com.puravida.groogle', module = 'groogle-core', version = '1.5.0-alpha2')


@Grab(group = 'com.puravida.groogle', module = 'groogle-calendar', version = '1.5.0-alpha2')
@GrabConfig(systemClassLoader=true)

import com.google.api.services.calendar.CalendarScopes
import com.puravida.groogle.GroogleScript
import com.puravida.groogle.CalendarScript

import groovyx.net.http.*
import static groovyx.net.http.HttpBuilder.configure
import static groovyx.net.http.ContentTypes.JSON
import static groovy.json.JsonOutput.prettyPrint
import static groovy.json.JsonOutput.toJson

//end::dependencies[]

//tag::http[]
XML = ["application/xml", "text/xml", "application/xhtml+xml", "application/atom+xml"]
xmlEncoder = NativeHandlers.Encoders.&xml

http = configure {
  request.uri = 'https://datos.madrid.es/egob/catalogo/206717-0-agenda-eventos-bibliotecas.xml'
  request.encoder XML, xmlEncoder
  request.contentType = 'application/xml'
}.get{

}
//end::http[]

Date fromDate = new Date() - 10

CalendarScript.instance.with {
  //tag::login[]
  login {
  applicationName 'groogle-example'
  withScopes CalendarScopes.CALENDAR
  usingCredentials new File('client_secret.json')
  asService false
  }
  //end::login[]

  //tag::prepare[]
  String groogleCalendarId = "0v757vib7jf1hj0bjsq041vco8@group.calendar.google.com"
  withCalendar groogleCalendarId, {
  eachEvent{
  removeFromCalendar()
  }
  }
  //end::prepare[]

  //tag::business[]
  http.contenido.each{
  String titulo = it.atributos.atributo.find{ it.'@nombre'=='TITULO-ACTIVIDAD' }.text()
  String descripcion = it.atributos.atributo.find{ it.'@nombre'=='DESCRIPCION' }.text()
  String inicio = it.atributos.atributo.find{ it.'@nombre'=='FECHA-EVENTO' }.text()
  String fin = it.atributos.atributo.find{ it.'@nombre'=='FECHA-FIN-EVENTO' }.text()
  String hora = it.atributos.atributo.find{ it.'@nombre'=='HORA-EVENTO' }.text()
  String where = it.atributos.atributo.find{ it.'@nombre'=='LOCALIZACION' }?.atributo.find{it

| 173
101 Groovy Script

.'@nombre'=='NOMBRE-INSTALACION'}.text()

  Date dini = Date.parse('yyyy-MM-dd HH:mm:ss.S', inicio)


  Date dfin = Date.parse('yyyy-MM-dd HH:mm:ss.S', fin)

  if( descripcion.trim()=="" )
  return
  if( dini < fromDate )
  return

  createEvent groogleCalendarId, {
  it.event.summary = titulo
  it.event.description = "($hora) $descripcion"
  it.event.location=where
  from dini
  until dfin
  }
  }
  //end::business[]
}

174 |
101 Groovy Script

OpenDataMadrid
Jorge Aguilera <jorge.aguilera@puravida-software.com [mailto:jorge.aguilera@puravida-
software.com]> 2018-2-17

En este post vamos a utilizar Gradle como entorno de ejecución de


nuestro script, en lugar del propio Groovy, y su ecosistema de plugins,
 para consumir una URL con datos abiertos sobre la ciudad de Madrid y
enviar una notificación diaria a un canal de Telegram con un resumen de
los próximos eventos que van a ocurrir en la ciudad

Gradle es un sistema de automatización de construcción de código


abierto que construye sobre los conceptos de Apache Ant y Apache
Maven e introduce un lenguaje especifico del dominio (DSL) basado en
Groovy en vez de la forma XML utilizada por Apache Maven para
declarar la configuración de proyecto.

— Wikipedia, https://es.wikipedia.org/wiki/Gradle

Preparación
En primer lugar deberemos preparar nuestro entorno de desarrollo con Gradle. Para ello
disponemos de varios métodos:

• descargarlo, descomprimirlo y ajustar las variables de entorno indicadas

• usar sdkman e instalarlo con sdkman install gradle

• descargar este proyecto semilla https://gitlab.com/groovy-lang/gradle-seed y trabajar


sobre él

Con los dos primeros métodos deberemos ejecutar en el directorio donde queramos
trabajar el comando gradle init para que nos cree los ficheros necesarios. Si optamos por
clonar el repo indicado en el tercer método esto ya se ha hecho y simplemente deberemos
trabajar sobre el directorio donde hayamos descargado el repo

Scripts vs Task
Gradle es una herramienta para automatizar la construcción de artefactos basada en el
concepto de task, los cuales los podemos ver como pequeños scripts, y que nos permite
definir dependencias entre ellas. Así mismo proporciona en su DSL la capacidad de
definir dependencias a los típicos artefactos externos (librerías) para incluir en la
ejecución del build.

De esta forma podemos usar Gradle como un entorno donde utilizar multitud de librerías

| 175
101 Groovy Script

existentes y mediante scripts interaccionar con el sistema no sólo para construir


artefactos sino para ejecutar acciones variopintas como apagar un servidor, enviar un
correo electrónico con el contenido de un fichero, etc

OpenDataMadrid
La ciudad de Madrid, España, cuenta con un amplio catálogo de datos abiertos fácilmente
accesible vía http en diferentes formatos como pueden ser xml, json, excel e incluso un
API REST (https://datos.madrid.es/portal/site/egob)

En este post vamos a ver cómo interpretar el catálogo de actividades programadas de las
bibliotecas que se encuentran en la url https://datos.madrid.es/egob/catalogo/206717-0-
agenda-eventos-bibliotecas.json

Si inspeccionamos este recurso veremos que las actividades están organizadas en un


elemento @graph y cada uno de ellas indica la fecha de inicio y de fín así como el título,
lugar, entre otros:

176 |
101 Groovy Script

"@graph": [
  {
  "@id": "https://datos.madrid.es/egob/catalogo/tipo/evento/10847853-bestia-volvoreta.json",
  "@type": "https://datos.madrid.es/egob/kos/actividades/CuentacuentosTiteresMarionetas",
  "id": "10847853",
  "title": "A lo bestia con Volvoreta",
  "description": "El miedo, como el amor, es un sentimiento inherente al ser humano, y los cuentos
consiguen ponerles cara y ojos a todas esas angustias que nos configuran. Ayudándonos a exorcizar todas
las inquietudes que durante una narración en grupo se comparten y en el mejor de los casos, se superan.
Por eso, esta sesión es un homenaje a algunos cuentos que ya son clásicos, pero también a aquellas
historias que acaban de nacer porque para no tener pesadillas por la noche, nada como soñar despierto
durante una hora de cuentos. A partir de 4 años.",
  "price": 1,
  "dtstart": "2019-04-11 18:00:00.0",
  "dtend": "2019-04-11 23:59:00.0",
  "excluded-days": "",
  "audience": "Niños,Familias",
  "uid": "10847853",
  "link":
"http://www.madrid.es/sites/v/index.jsp?vgnextchannel=ca9671ee4a9eb410VgnVCM100000171f5a0aRCRD&vgnextoid=3762
de882dba7610VgnVCM2000001f4a900aRCRD",
  "event-location": "Biblioteca Pública Municipal Aluche (Latina)",
  "references": {
  "@id":
"http://www.madrid.es/sites/v/index.jsp?vgnextchannel=ca9671ee4a9eb410VgnVCM100000171f5a0aRCRD&vgnextoid=4224
a209a1c71510VgnVCM2000000c205a0aRCRD"
  },
  "relation": {
  "@id": "https://datos.madrid.es/egob/catalogo/tipo/entidadesyorganismos/1757-biblioteca-publica-
municipal-aluche-latina-.json"
  },

  "address": {
  "district": {
  "@id":
"https://datos.madrid.es/egob/kos/Provincia/Madrid/Municipio/Madrid/Distrito/Latina"
  }
  },
  "location": {
  "latitude": 40.39597072367931,
  "longitude": -3.7562716852987847
  }
  },

La idea de nuestro script es parsear este JSON cada día y buscar qué actividades se van a
realizar a dos (2) días vista y enviar un mensaje a un canal de Telegram con un resumen
de las mismas (en caso de que haya actividades)

Para el envío de Telegram usaremos un plugin de Gradle que nos permite


enviar a diferentes redes sociales como Twitter, Telegram o Slack,
 (https://puravida-gradle.gitlab.io/social-network/) lo cual nos servirá para
demostrar la potencia de Gradle.

| 177
101 Groovy Script

Dependencias
Mientras que en un típico script usabamos @Grab para resolver las dependencias, con
Gradle usaremos su DSL buildSrc

Así mismo Gradle nos permite añadir otro tipo de dependencias no existentes con @Grab
como son los plugins. Básicamente son artefactos orientados a implementar tareas que se
pueden integrar en nuestro build tanto para ser ejecutadas directamente como para
extenderlas. En nuestro caso vamos a utilizar el plugin social-network y su tarea
telegram

plugins {
  id 'com.puravida.gradle.socialnetwork' version '0.1.1'
}
import groovy.json.JsonSlurper

Parse
Nuestra tarea de parseo va a ser una tarea básica que no extiende de ninguna previa y
que va a generar, en caso de que haya eventos, un fichero build/telegram.txt con el texto
a enviar en formato Markdown.

Utilizando la sintáxis de Gradle, nuestro script se ejecutará dentro de la closure doLast , en


la cual descargamos el JSON y recorremos los elementos comentados anteriormente. Para
cada elemento de interés iremos añadiendo a un array las líneas que queremos volcar al
fichero (nótese que podíamos ir escribiendo directamente en el fichero, simplemente
estamos mostrando otras formas de actualizar un fichero de texto).

178 |
101 Groovy Script

task eventosBibliotecas(){

  def msg = file("$buildDir/telegram.txt")


  outputs.files msg

  doLast{
  def arr = []
  def today = Calendar.instance.time

  arr.add "-".multiply(10)
  arr.add "\n"
  arr.add "*Hoy es ${today.format( 'dd/MM/yyyy' )}*"
  arr.add "\n"
  arr.add "\n"

  def json = new JsonSlurper().parseText('https://datos.madrid.es/egob/catalogo/206717-0-agenda-


eventos-bibliotecas.json'.toURL().text)
  json."@graph".each{ evt->
  use(groovy.time.TimeCategory) {
  def strstart = Date.parse('yyyy-MM-dd HH:mm:ss',evt.dtstart)
  def strend = Date.parse('yyyy-MM-dd HH:mm:ss',evt.dtend)
  def duration = strend - today
  switch( duration.days ){
  case 2:
  arr.add "Ἱ️*${strstart.format( 'dd/MM/yyyy' )} al ${strend.format( 'dd/MM/yyyy'
)}*"
  arr.add "Dentro de *${duration.days}* días en ${evt.'event-location'}"
  arr.add "$evt.title"
  arr.add "\n"
  }
  }
  }
  if( arr.size() )
  msg.text = arr.join('\n')
  }
}

Telegram
Como hemos comentado usaremos un plugin de Gradle para el envío a un canal de
Telegram al cual sólo le tenemos que configurar con las credenciales necesarias así como
el id del canal al que enviar el mensaje, mediante las propiedades telegram_token y
telegram_channel (consultar documentación del plugin)

Como bola extra podemos ver las funcionalidades de Gradle para hacer depender unas
tareas de otras (telegram de parse en nuestro caso), así como decidir si una tarea debe ser
ejecutada o no en función de las condiciones que necesitemos

| 179
101 Groovy Script

telegram{
  credentials {
  token project.findProperty('telegram_token')
  channel project.findProperty('telegram_channel')
  }
  message file('build/telegram.txt')
  sendAsMarkdown
}

telegram.onlyIf{
  file("$buildDir/telegram.txt").exists()
}
telegram.dependsOn eventosBibliotecas

Ejecución manual
Para ejecutar nuestro "script/tarea" simplemente ejecutaremos desde el directorio
principal del proyecto:

si usamos Linux ejectaremos:

./gradlew -b OpenDataMadrid.gradle build -Ptelegram_token=123123ZZZZ "-Ptelegram_channel=@opendatamadrid" ①

① debido a que @ es un caracter especial en la linea de comandos, lo recubrimos entre


comillas

o si usamos windows

gradlew.bat -b OpenDataMadrid.gradle build -Ptelegram_token=123123ZZZZ -Ptelegram_channel=@opendatamadrid

Si todo va bien, y hay eventos, los usuarios suscritos al canal verían un mensaje parecido
a este:

180 |
101 Groovy Script

| 181
101 Groovy Script

Monitorea un servidor (o lo que


quieras)
Jorge Aguilera <jorge.aguilera@puravida-software.com [mailto:jorge.aguilera@puravida-
software.com]> 2017-10-23

Qué es JavaFX

JavaFX es una familia de productos y tecnologías de Oracle


Corporation (inicialmente Sun Microsystems), para la creación de
Rich Internet Applications (RIAs), esto es, aplicaciones web que tienen
las características y capacidades de aplicaciones de escritorio,
incluyendo aplicaciones multimedia interactivas.

Las tecnologías incluidas bajo la denominación JavaFX son JavaFX


Script y JavaFX Mobile, aunque hay más productos JavaFX planeados

— Wikipedia, https://es.wikipedia.org/wiki/JavaFX

JavaFX puede verse como el sucesor a Swing en la medida en que nos permite crear
aplicaciones gráficas aunque en realidad pretende ocupar espacios donde este no llegó
(lectores DVD por ejemplo). Cuenta además con un lenguaje de scripting Java FX Script
que permite desarrollar aplicaciones con menos código que con el stack tradicional
Swing. (Sí, podríamos decir que es la misma idea que Groovy y Groovy Script).

Y como era de esperar, Groovy cuenta con un proyecto que nos va a permitir hacernos la
vida más fácil a la hora de definir nuestros elementos gráficos así como conseguir que los
cambios que se producen en el modelo se transmitan a la vista de una forma realmente
sencilla, el cual puedes encontrar en http://groovyfx.org/

Caso de Uso
Para este ejemplo vamos a desarrollar un pequeño script que se va a conectar a un
servidor web (parametrizable) y va a ir monitorizando el tiempo de respuesta, de tal
forma que si se encuentra caído en algún momento podamos verlo rápidamente. Tanto si
está caído como el tiempo que tarda en conectar lo vamos a mostrar en una ventana
gráfica que se irá refrescando automáticamente.

Como el resto de scripts, este pretende ofrecerte las herramientas para


 que puedas ajustarlo a tus propias necesidades. En este caso no es tan
importante la funcionalidad sino el cómo.

Más o menos esto es lo que veremos en pantalla:

182 |
101 Groovy Script

Modelo-Vista-Controlador
No. Tranquilo. No vamos a desarrollar ningún sistema MVC complejo (aunque si te
interesa el tema te recomiendo que eches un ojo al proyecto http://griffon-framework.org).
Simplemente vamos a separar dentro del mismo script la parte correspondiente al
negocio de la parte correspondiente a la vista para hacer nuestro script más legible.

Al realizar esta separación lo más importante es conseguir que los cambios que se
producen en el negocio sean reflejados en la vista (y viceversa aunque en este ejemplo no
vamos a tratar esta dirección). Para ello JavaFX proporciona unas clases similares a los
Beans tanto en su funcionalidad como en su verbosidad, pero tranquilo que GroovyFx
nos permite reducirla.

Como puedes ver a continuación la clase Business simplemente consta de un String para
guardar el server que queremos monitorizar y un FXBindable String el cual guarda el
tiempo de respuesta a la última petición. Por lo demás simplemente un par de métodos
para calcular cuanto tarda una conexión a un puerto mediante sockets, nada del otro
mundo (probablemente para tu caso específico será aquí donde el problema no sea tan
trivial):

| 183
101 Groovy Script

class Business {
  String server

  @FXBindable
  String delay = '' ①

  void refresh() {
  Date start = new Date()
  if (!isReachable(10 * 1000)) { //10seg max
  delay = "DOWN"
  return
  }
  Date stop = new Date()
  groovy.time.TimeDuration td = groovy.time.TimeCategory.minus(stop, start)
  delay = "$td" ②
  }

  boolean isReachable(int timeOutMillis, int port = 80) {


  try {
  Socket soc = new Socket()
  soc.connect(new InetSocketAddress(server, port), timeOutMillis);
  true
  } catch (IOException ex) {
  println ex.toString()
  false;
  }
  }
}

① FXBindable es propia del proyecto GroovyFx

② Al actualizar el valor como un String, los cambios se propagan

La segunda parte del script va a hacer uso del DSL que nos ofrece GroovyFX para poder
diseñar los elementos gráficos ( stage, scene, label, inputs, buttons, etc) así como
personalizar cada uno con los atributos que queramos, como tamaño, posición, colores,
texto etc.

Lo más importante (a parte de tener gusto para saber crear la parte gráfica) es unir
nuestro modelo con la vista, lo cual conseguimos mediante el método bind. Así en este
script le indicamos a un label que su texto NO es un texto fijo sino que está enlazado a una
propiedad delay del objecto business

184 |
101 Groovy Script

  stage(title: 'Latencia', visible: true) { ①


  scene(fill: BLACK, width: 800, height: 250) {
  hbox(padding: 60) {
  text(text: "Ping to $business.server :", font: '20pt sanserif') { ②
  fill linearGradient(endX: 0, stops: [PALEGREEN, SEAGREEN])
  }
  label(text: bind(business, 'delay'), font: '40pt sanserif') { ③
  fill linearGradient(endX: 0, stops: [CYAN, DODGERBLUE])
  effect dropShadow(color: DODGERBLUE, radius: 25, spread: 0.25)
  }
  }
  }
  }

① vamos creando los elementos gráficos

② bind crea un listener a la propiedad de business y actualiza el elemento gráfico en


consecuencia

Transiciones
Mediante JavaFX/GroovyFX no sólo tenemos aplicaciones estáticas que reaccionan ante
eventos del usuario sino que podemos crear nuestras propias transiciones de una forma
contínua, de tal forma que nuestra aplicación se puede ir refrescando ella sóla.

  sequentialTransition(cycleCount: INDEFINITE) { ①
  pauseTransition(10.s) {
  onFinished {
  business.refresh() ②
  }
  }
  }.playFromStart()

① Creamos un bucle infinito de transiciones

② Al terminar cada transicion refrescamos nuestro negocio

| 185
101 Groovy Script

Script

//tag::dependencies[]
@GrabConfig(systemClassLoader=true)
@Grab(group='org.groovyfx',module='groovyfx',version='8.0.0',transitive=false, noExceptions=true)
//end::dependencies[]

import groovyx.javafx.beans.FXBindable
import static groovyx.javafx.GroovyFX.start

//tag::business[]
class Business {
  String server

  @FXBindable
  String delay = '' ①

  void refresh() {
  Date start = new Date()
  if (!isReachable(10 * 1000)) { //10seg max
  delay = "DOWN"
  return
  }
  Date stop = new Date()
  groovy.time.TimeDuration td = groovy.time.TimeCategory.minus(stop, start)
  delay = "$td" ②
  }

  boolean isReachable(int timeOutMillis, int port = 80) {


  try {
  Socket soc = new Socket()
  soc.connect(new InetSocketAddress(server, port), timeOutMillis);
  true
  } catch (IOException ex) {
  println ex.toString()
  false;
  }
  }
}
//end::business[]

start {
  def business = new Business(server: args[0])

  //tag::vista[]
  stage(title: 'Latencia', visible: true) { ①
  scene(fill: BLACK, width: 800, height: 250) {
  hbox(padding: 60) {
  text(text: "Ping to $business.server :", font: '20pt sanserif') { ②
  fill linearGradient(endX: 0, stops: [PALEGREEN, SEAGREEN])
  }
  label(text: bind(business, 'delay'), font: '40pt sanserif') { ③
  fill linearGradient(endX: 0, stops: [CYAN, DODGERBLUE])
  effect dropShadow(color: DODGERBLUE, radius: 25, spread: 0.25)
  }
  }
  }

186 |
101 Groovy Script

  }
  //end::vista[]

  //tag::bucle[]
  sequentialTransition(cycleCount: INDEFINITE) { ①
  pauseTransition(10.s) {
  onFinished {
  business.refresh() ②
  }
  }
  }.playFromStart()
  //end::bucle[]

  business.refresh()
}

| 187
101 Groovy Script

TwitterFX
Jorge Aguilera <jorge.aguilera@puravida-software.com [mailto:jorge.aguilera@puravida-
software.com]> 2017-11-23

Caso de Uso
Nuestra empresa ha entrado con ganas en el mundo de las redes sociales y quiere generar
contenido sobre nuestros productos. Hasta hemos contratado a una persona que se va a
encargar de generarlos y publicarlos en Twitter.

Esta persona querría poder compartir de una forma sencilla los "Top Ventas" de nuestros
productos así que para ello todos los días te pide que le busques en las bases de datos qué
3 productos son los más vendidos y cuantas unidades hemos vendido de ellos.

Con esta información, y mediante alguna aplicación ofimática tipo Excel, es capaz de
generar un gráfico de tartas, salvar la imagen a su disco, abrir Twitter, escribir un
mensaje, adjuntar la imagen y twittearla. Fácil. Pesado. Laborioso.

Twitter App
Para poder publicar tweets de forma automática, el primer paso es crear una app en
https://apps.twitter.com/ para que Twitter nos genere un conjunto de claves. Si sigues los
pasos que ofrece la página al final del proceso obtendras 4 claves que deberás guardar en
el fichero twitter4j.properties en el mismo directorio que resida nuestro script.

twitter4j.properties

oauth.consumerKey=XXXXXXXXXX
oauth.consumerSecret=YYYYYYYYYYYYYYYYYYYYYY
oauth.accessToken=527902906-asdfdsafassssssssssssssss
oauth.accessTokenSecret=123mlkjdfd9sfldsjlkj

Dependencias
Nuestro script va a necesitar las siguientes dependencias agrupadas por funcionalidad:

188 |
101 Groovy Script

@GrabConfig(systemClassLoader=true)
@Grab('mysql:mysql-connector-java:5.1.6')
@Grab(group='org.groovyfx',module='groovyfx',version='8.0.0',transitive=false, noExceptions=true)
@Grab(group='org.twitter4j', module='twitter4j-core', version='4.0.6')

import groovy.sql.Sql

import static groovyx.javafx.GroovyFX.start


import javafx.application.Platform
import javafx.scene.SnapshotParameters
import javax.imageio.ImageIO
import java.awt.image.BufferedImage

import twitter4j.TwitterFactory;
import twitter4j.StatusUpdate

Básicamente:

• conexión a la base de datos

• librerías graficas JavaFX

• librería de twitter

Customize
Para poder ajustar nuestro script de forma cómoda vamos a utilizar las siguientes
variables:

String message ="""


Estamos que nos salimos!!!
Top Ventas de nuestro catálogo. Gracias a todos por elegirnos
#groovy-script
"""

String chartTitle="Top Ventas"

int width=height=400

Obtener Top Sales


Obtener los productos más vendidos puede ser desde una operación trivial hasta una
operación compleja de agregados. Eso ya dependerá de tu negocio. Para nuestro caso
vamos a suponer que disponemos de una tabla MySQL donde ya se encuentran agregados
las ventas por su descripción

| 189
101 Groovy Script

def data=[:] ①

Sql sql = Sql.newInstance( "jdbc:mysql://localhost:3306/scripts?jdbcCompliantTruncation=false",


  "root",
  "my-secret-pw",
  "com.mysql.jdbc.Driver")

sql.eachRow("select product_name, sales from sales order by sales limit 4"){


  data[it.product_name] = it.sales as double ②
}

① Usaremos un mapa "producto"=unidades para generar el gráfico de tartas

② Buscamos en la bbdd e insertamos en nuestro mapa

Generamos el gráfico de tartas


En esta parte podremos usar infinidad de técnicas y capacidades que nos ofrece JavaFX,
como por ejemplo objetos 3D, rotaciones, hojas de estilo, etc. Por ahora a nosotros nos
bastará con un gráfico de tartas donde se reflejen los productos obtenidos anteriormente
y un título para el gráfico.

Una vez renderizado el gráfico nos interesará hacer una foto (snapshot) al nodo en
concreto que contiene el gráfico y volcar a fichero la imagen (en formato PNG).

start {
  def saveNode ①
  stage(visible: true) { ②
  scene(fill: BLACK, width: width, height: height) {
  saveNode = tilePane() {
  pieChart(data: data, title: chartTitle) ③
  }
  }
  }

① saveNode será una referencia al nodo que contiene el gráfico

② creamos un gráfico de tartas con los datos obtenidos en la bbdd y almacenados en el


mapa

③ asignamos la referencia para poder usarla después

Una vez que la scene JavaFX se encuentra lista realizamos el volcado. Básicamente
pediremos a JavaFX que nos vuelque en un objeto Image los pixeles y así podamos
convertirlos a fichero PNG

190 |
101 Groovy Script

  def snapshot = saveNode.snapshot(new SnapshotParameters(), null);


  BufferedImage bufferedImage = new BufferedImage(saveNode.width as int, saveNode.height as int,
BufferedImage.TYPE_INT_ARGB);
  BufferedImage image = javafx.embed.swing.SwingFXUtils.fromFXImage(snapshot, bufferedImage)

  File file = new File('screenshot.png')


  ImageIO.write(image, "png", file )

A Twitter
Generar un tweet mediante twitter4j es tan sencillo como crear un StatusUpdate,
adjuntar un fichero si así lo deseamos y enviarlo:

  StatusUpdate status = new StatusUpdate(message) ①


  status.media(file ) ②
  TwitterFactory.singleton.updateStatus status

① El mensaje que definimos en la parte de customizacion al principio

② El fichero png screenshot que hemos generado en el apartado anterior

Aquí puedes ver cómo quedaría un tweet:

| 191
101 Groovy Script

Cerrar automáticamente
Por último una vez cumplida su misión simplemente indicamos a JavaFX que cierre la
ventana automáticamente

  Platform.exit();
  System.exit(0);

192 |
101 Groovy Script

Script

//tag::dependencies[]
@GrabConfig(systemClassLoader=true)
@Grab('mysql:mysql-connector-java:5.1.6')
@Grab(group='org.groovyfx',module='groovyfx',version='8.0.0',transitive=false, noExceptions=true)
@Grab(group='org.twitter4j', module='twitter4j-core', version='4.0.6')

import groovy.sql.Sql

import static groovyx.javafx.GroovyFX.start


import javafx.application.Platform
import javafx.scene.SnapshotParameters
import javax.imageio.ImageIO
import java.awt.image.BufferedImage

import twitter4j.TwitterFactory;
import twitter4j.StatusUpdate
//end::dependencies[]

//tag::customize[]
String message ="""
Estamos que nos salimos!!!
Top Ventas de nuestro catálogo. Gracias a todos por elegirnos
#groovy-script
"""

String chartTitle="Top Ventas"

int width=height=400
//end::customize[]

//tag::business[]
def data=[:] ①

Sql sql = Sql.newInstance( "jdbc:mysql://localhost:3306/scripts?jdbcCompliantTruncation=false",


  "root",
  "my-secret-pw",
  "com.mysql.jdbc.Driver")

sql.eachRow("select product_name, sales from sales order by sales limit 4"){


  data[it.product_name] = it.sales as double ②
}
//end::business[]

//tag::vista[]
start {
  def saveNode ①
  stage(visible: true) { ②
  scene(fill: BLACK, width: width, height: height) {
  saveNode = tilePane() {
  pieChart(data: data, title: chartTitle) ③
  }
  }
  }
  //end::vista[]

  //tag::snapshot[]
  def snapshot = saveNode.snapshot(new SnapshotParameters(), null);
  BufferedImage bufferedImage = new BufferedImage(saveNode.width as int, saveNode.height as int,
BufferedImage.TYPE_INT_ARGB);
  BufferedImage image = javafx.embed.swing.SwingFXUtils.fromFXImage(snapshot, bufferedImage)

| 193
101 Groovy Script

  File file = new File('screenshot.png')


  ImageIO.write(image, "png", file )
  //end::snapshot[]

  //tag::twitter[]
  StatusUpdate status = new StatusUpdate(message) ①
  status.media(file ) ②
  TwitterFactory.singleton.updateStatus status
  //end::twitter[]

  //tag::exit[]
  Platform.exit();
  System.exit(0);
  //end::exit[]
}

194 |
101 Groovy Script

VisualSheet (Mejora la interface de tus


scripts)
Jorge Aguilera <jorge.aguilera@puravida-software.com [mailto:jorge.aguilera@puravida-
software.com]> 2018-02-17

Caso de Uso
Este script no tiene un caso de uso práctico como tal sino que pretende demostrar la
forma de mejorar el interface gráfico de los mismos para conseguir una mejor interacción
con el usuario.

En este caso vamos a mostrar un diálogo donde cargaremos en una tabla una hoja de
Google y el usuario podrá interactuar con los valores recuperados.

Dependencias
Nuestro script va a necesitar las siguientes dependencias agrupadas por funcionalidad:

@GrabConfig(systemClassLoader=true)
@Grab(group='org.groovyfx',module='groovyfx',version='8.0.0',transitive=true, noExceptions=true)
@Grab(group = 'com.puravida.groogle', module = 'groogle-sheet', version = '1.4.1')

import groovy.transform.Canonical
import groovyx.javafx.beans.FXBindable
import javafx.collections.FXCollections
import javafx.collections.ObservableList

import static groovyx.javafx.GroovyFX.start

import com.google.api.services.sheets.v4.SheetsScopes
import com.puravida.groogle.GroogleScript
import com.puravida.groogle.SheetScript

Por un lado vamos a necesitar las dependencias de GroovyFX para la parte visual y por
otro vamos a necesitar Groogle para la parte de interactuar con GoogleSheet

Business
Nuestro negocio va a ser realmente muy simple. Vamos a representar cada fila de la hoja
de cálculo mediante un objeto FxRow el cual contiene dos valores recuperados de esta así
como un valor calculado por aplicación (coordinate) y otro que se actualiza tras la
interacción del usuario con la fila en cuestión (derivate)

| 195
101 Groovy Script

enum Status {
  ON, OFF
}

@Canonical
class FxRow {
  @FXBindable String coordinates
  @FXBindable String name
  @FXBindable Status status
  @FXBindable String derivate
}

rows=FXCollections.observableArrayList([])
self = this

Para la carga de este modelo vamos a usar una función loadFile la cual leerá la hoja de
cálculo e inicializará el array de FxRows. Esta función será llamada únicamente cuando el
usuario así lo desee pulsando un botón de la vista

def loadFile(){
  SheetScript.instance.withSpreadSheet args[0], { spreadSheet ->
  withSheet 'Hoja 1',{
  int idx = 2
  def googleRows = readRows("A$idx", "B${idx + 100}")
  while (googleRows ) {
  googleRows.eachWithIndex{ gRow, rIdx->
  self.rows << new FxRow(coordinates: "A${idx+rIdx}",
  name:gRow[0],
  status: gRow[1]=='on'?Status.ON:Status.OFF,
  derivate: ''
  )
  }
  idx += googleRows.size()
  googleRows = readRows("A$idx", "B${idx + 100}")
  }
  }
  }
}

Vista
La vista se compone de un botón que activará una action y una tabla que mostrará de
forma dinámica el array de FxRows. Así mismo podremos interactuar con los objetos en
las columnas name y status de tal forma que podríamos actualizar la hoja, calcular
valores o en definitiva cualquier acción que requiera el negocio.

196 |
101 Groovy Script

start {
  actions {
  fxaction(id: 'loadFile',onAction: {loadFile()})
  }
  stage(title: 'VisualSheet', height: 600, visible: true) {
  scene(fill: BLACK, width: 800, height: 250) {
  hbox(padding: 60) {
  button( loadFile, text:'Load')
  }
  hbox(padding: 60) {
  tableView(selectionMode: "single", cellSelectionEnabled: true, editable: true, items: bind
(self,'rows')) {

  tableColumn(editable: false, property: "coordinates", text: "Row", prefWidth: 50)

  tableColumn(editable: true, property: "name", text: "Name", prefWidth: 150,


  onEditCommit: { event ->
  FxRow item = event.tableView.items.get(event.tablePosition.row)
  item.name = event.newValue
  }
  )

  tableColumn(editable: true, property: "status", text: "Status", prefWidth: 150, type:


Status,
  onEditCommit: { event ->
  FxRow item = event.tableView.items.get(event.tablePosition.row)
  item.status = event.newValue
  item.derivate = event.newValue==Status.ON ? "Yes" : "NO"
  }
  )

  tableColumn(editable: false, property: "derivate", text: "Calculada", prefWidth: 150)


  }
  }
  }
  }
}

Esto es una imagen de cómo podría quedar la aplicación:

| 197
101 Groovy Script

198 |
101 Groovy Script

Script

//tag::dependencies[]
@GrabConfig(systemClassLoader=true)
@Grab(group='org.groovyfx',module='groovyfx',version='8.0.0',transitive=true, noExceptions=true)
@Grab(group = 'com.puravida.groogle', module = 'groogle-sheet', version = '1.4.1')

import groovy.transform.Canonical
import groovyx.javafx.beans.FXBindable
import javafx.collections.FXCollections
import javafx.collections.ObservableList

import static groovyx.javafx.GroovyFX.start

import com.google.api.services.sheets.v4.SheetsScopes
import com.puravida.groogle.GroogleScript
import com.puravida.groogle.SheetScript
//end::dependencies[]

//tag::business[]
enum Status {
  ON, OFF
}

@Canonical
class FxRow {
  @FXBindable String coordinates
  @FXBindable String name
  @FXBindable Status status
  @FXBindable String derivate
}

rows=FXCollections.observableArrayList([])
self = this
//end::business[]

//tag::load[]
def loadFile(){
  SheetScript.instance.withSpreadSheet args[0], { spreadSheet ->
  withSheet 'Hoja 1',{
  int idx = 2
  def googleRows = readRows("A$idx", "B${idx + 100}")
  while (googleRows ) {
  googleRows.eachWithIndex{ gRow, rIdx->
  self.rows << new FxRow(coordinates: "A${idx+rIdx}",
  name:gRow[0],
  status: gRow[1]=='on'?Status.ON:Status.OFF,
  derivate: ''
  )
  }
  idx += googleRows.size()
  googleRows = readRows("A$idx", "B${idx + 100}")
  }
  }
  }
}
//end::load[]

//tag::login[]
GroogleScript.instance.applicationName='101-scripts'
clientSecret = new File('../google/client_secret.json').newInputStream()
SheetScript.instance.groogleScript=GroogleScript.instance.login(clientSecret,[SheetsScopes.SPREADSHEETS])
//end::login[]

| 199
101 Groovy Script

//tag::view[]
start {
  actions {
  fxaction(id: 'loadFile',onAction: {loadFile()})
  }
  stage(title: 'VisualSheet', height: 600, visible: true) {
  scene(fill: BLACK, width: 800, height: 250) {
  hbox(padding: 60) {
  button( loadFile, text:'Load')
  }
  hbox(padding: 60) {
  tableView(selectionMode: "single", cellSelectionEnabled: true, editable: true, items: bind
(self,'rows')) {

  tableColumn(editable: false, property: "coordinates", text: "Row", prefWidth: 50)

  tableColumn(editable: true, property: "name", text: "Name", prefWidth: 150,


  onEditCommit: { event ->
  FxRow item = event.tableView.items.get(event.tablePosition.row)
  item.name = event.newValue
  }
  )

  tableColumn(editable: true, property: "status", text: "Status", prefWidth: 150, type:


Status,
  onEditCommit: { event ->
  FxRow item = event.tableView.items.get(event.tablePosition.row)
  item.status = event.newValue
  item.derivate = event.newValue==Status.ON ? "Yes" : "NO"
  }
  )

  tableColumn(editable: false, property: "derivate", text: "Calculada", prefWidth: 150)


  }
  }
  }
  }
}
//end::view[]

200 |
101 Groovy Script

Visualizar Extension de Ficheros


Jorge Aguilera <jorge.aguilera@puravida-software.com [mailto:jorge.aguilera@puravida-
software.com]> 2018-02-17

JavaFX (y GroovyFX) nos permiten visualizar datos de forma muy cómoda. En este caso
vamos a realizar un escaneo de nuestros ficheros y por cada extensión vamos a acumular
el tamaño de los ficheros y su número, de tal forma que podamos visualizar la
distribución de los mismos en tres gráficas:

• distribución por tamaños (qué extensión ocupa más disco)

• distribución por número (qué extensión tiene más ficheros)

• relación entre ambos conceptos

Calculo
Dado un directorio como argumento, el script lo recorrera recursivamente generando 3
mapas con la extensión como clave:

• por tamaño, bySize

• por contador, byNumber

• por ambos mediante un submapa con la unión de los 3 conceptos (extension, numero
y tamaño)

| 201
101 Groovy Script

bySize=[:]
byNumber=[:]
byBoth=[:]

void scanDir( File dir ){


  dir.eachFile FileType.FILES,{ f->
  split = f.name.split('\\.')
  ext = split.size() ? split.last() : '.'

  bySize."$ext" = (bySize."$ext" ?: 0)+f.size()


  byNumber."$ext" = (byNumber."$ext" ?: 0)+1

  both = byBoth."$ext" ?: [ext,0,0]


  both[1] +=f.size()
  both[2] +=1
  byBoth."$ext" = both
  }
  dir.eachDir{ d ->
  scanDir(d)
  }
}

scanDir(new File(args[0]))

bySize = bySize.sort{ a,b-> b.value<=>a.value }


byNumber = byNumber.sort{ a,b-> b.value<=>a.value }

La gestión de Groovy con mapas es tan potente que nos permite crear elementos en el
mapa de una forma realmente simple

Vista
La vista utilizará 3 gráficas, una para cada mapa calculado previamente

202 |
101 Groovy Script

  stage title: "My Files", visible: true, {


  scene {
  saveNode = stackPane {
  scrollPane {
  tilePane(padding: 10, prefTileWidth: 480, prefColumns: 2) {

  pieChart(data: bySize, title: "Size")

  pieChart(data: byNumber, title: "Number")

  scatterChart title:'Size vs Number', {


  byBoth.values().each { bb->
  series(name: bb[0], data: [bb[1],bb[2]])
  }
  }
  }
  }
  }
  }
  }

Ejemplo
A continuación se muestra un ejemplo de cómo se representaría un directorio, en
concreto el blog donde vemos que la extensión mayoritaria tanto en tamaño como en
número es SVG.

| 203
101 Groovy Script

204 |
101 Groovy Script

Script

//tag::dependencies[]

import groovy.io.FileType

@GrabConfig(systemClassLoader=true)
@Grab(group='org.groovyfx',module='groovyfx',version='8.0.0',transitive=false, noExceptions=true)

import static groovyx.javafx.GroovyFX.start


import javafx.application.Platform
import javafx.scene.SnapshotParameters
import javax.imageio.ImageIO
import java.awt.image.BufferedImage
//end::dependencies[]

//tag::calculating[]
bySize=[:]
byNumber=[:]
byBoth=[:]

void scanDir( File dir ){


  dir.eachFile FileType.FILES,{ f->
  split = f.name.split('\\.')
  ext = split.size() ? split.last() : '.'

  bySize."$ext" = (bySize."$ext" ?: 0)+f.size()


  byNumber."$ext" = (byNumber."$ext" ?: 0)+1

  both = byBoth."$ext" ?: [ext,0,0]


  both[1] +=f.size()
  both[2] +=1
  byBoth."$ext" = both
  }
  dir.eachDir{ d ->
  scanDir(d)
  }
}

scanDir(new File(args[0]))

bySize = bySize.sort{ a,b-> b.value<=>a.value }


byNumber = byNumber.sort{ a,b-> b.value<=>a.value }
//end::calculating[]

start { app ->


  //tag::view[]
  stage title: "My Files", visible: true, {
  scene {
  saveNode = stackPane {
  scrollPane {
  tilePane(padding: 10, prefTileWidth: 480, prefColumns: 2) {

  pieChart(data: bySize, title: "Size")

  pieChart(data: byNumber, title: "Number")

  scatterChart title:'Size vs Number', {


  byBoth.values().each { bb->
  series(name: bb[0], data: [bb[1],bb[2]])
  }
  }
  }
  }

| 205
101 Groovy Script

  }
  }
  }
  //end::view[]

  //tag::snapshot[]
  def snapshot = saveNode.snapshot(new SnapshotParameters(), null);
  BufferedImage bufferedImage = new BufferedImage(saveNode.width as int, saveNode.height as int,
BufferedImage.TYPE_INT_ARGB);
  BufferedImage image = javafx.embed.swing.SwingFXUtils.fromFXImage(snapshot, bufferedImage)

  File file = new File('myfiles.png')


  ImageIO.write(image, "png", file )
  //end::snapshot[]
}

206 |
101 Groovy Script

Configuración dinámica de fichero


remoto
Miguel Rueda <miguel.rueda.garcia@gmail.com [mailto:miguel.rueda.garcia@gmail.com]>
2017-10-13

El caso que vamos a tratar es la modificación del contenido de un fichero con el formato
clave:valor.

Si trabajamos con varias máquinas, seguramente nos hayamos encontrado con que cada
una de ellas tiene ficheros de configuración que indiquen la url de la base de datos a la
que debe conectectarse, messages.properties de nuestra aplicación …

El problema de trabajar con estos ficheros es que en la mayoría de los casos, sobre todo
cuando hablamos de ficheros de configuración, son personalizados y en el caso de tener
que cambiar algún valor debemos de tener mucho cuidado ya que no son ficheros que
podamos copiar a todas nuestras maquinas de manera masiva porque dedemos respetar
la configuración que tienen cada uno de ellos.

A continuación vamos a explicar como crear un script que modifique el usuario con el
que hacemos la conexión a la base de datos en base a cierta condición propia de la
máquina.

Vamos a usar una herramienta que ya hemos utilizado en alguno de


 nuestros scripts sshoogr [https://github.com/aestasit/sshoogr].

• Añadimos la dependencia de sshoogr

@Grab('com.aestasit.infrastructure.sshoogr:sshoogr:0.9.25')
@GrabConfig(systemClassLoader=true)

• Definimos el nombre del servidor y el de nuestro fichero de configuración.

def server_name = "server_1"


def file_config = '/tmp/file.properties'

• Creamos el método de lectura de nuestro fichero

| 207
101 Groovy Script

def getFile(file_config,server_name){
  remoteSession("user:passwd@$server_name:22") {①
  result = remoteFile(file_config).text ②
  }
  return result.readLines()③
}

① Establecemos los parámtros de conexión usuario = user, contraseña = passwd


mientras que el servidor es variable.

② En la función remoteFile le pasamos por parámetro la ruta complete la fichero.

③ Con la función readLines obtenemos una lista con el contenido del fichero.

• Creamos el método para sobreescribir el fichero con los datos actualizados

def putFile(file_name,server_name,new_file){
  remoteSession("user:passwd@$server_name:22") {①
  remoteFile(dir).text = properties ②
  }
}

① Establecemos los parámtros de conexión usuario = user, contraseña = passwd


mientras que el servidor es variable.

② Actualizamos el contenido de nuestro fichero de configuración.

• Aplicamos la lógica de sustitución. En nuestro caso vamos a cambiar la variable


user.sql por el valor de una variable de entorno más una cadena fija _changeme

def read = getFile(file_config,server_name) ①

def properties = read.collect{


  if (it.startWith("user.sql")){ ②
  return "user.sql=${System.getenv('HOSTNAME')}_changeme"
  }
  return it
}.join('\n')

putFile(file_config,server_name,properties)③

① Lo primero que hacemos es emplear nuestra función getFile para obtener en


contenido del fichero.

② Si econtramos la clave en nuestro fichero que queremos actualizar la modificamos


y si no saltamos a la siguiente linea.

③ Llamaremos a nuestro método putFile el cúal nos actualizará el contenido de


nuestro fichero de configuración.

208 |
101 Groovy Script

Si te da error al conectar con el host, prueba a desactivar la


 comprobación estricta de la clave del host:

options.trustUnknownHosts = true

| 209
101 Groovy Script

Script

//tag::dependencies[]
@Grab('com.aestasit.infrastructure.sshoogr:sshoogr:0.9.25')
@GrabConfig(systemClassLoader=true)
//end::dependencies[]

import static com.aestasit.infrastructure.ssh.DefaultSsh.*

options.trustUnknownHosts = true

//tag::config[]
def server_name = "server_1"
def file_config = '/tmp/file.properties'
//end::config[]

//tag::main[]
def read = getFile(file_config,server_name) ①

def properties = read.collect{


  if (it.startWith("user.sql")){ ②
  return "user.sql=${System.getenv('HOSTNAME')}_changeme"
  }
  return it
}.join('\n')

putFile(file_config,server_name,properties)③
//end::main[]

//tag::write[]
def putFile(file_name,server_name,new_file){
  remoteSession("user:passwd@$server_name:22") {①
  remoteFile(dir).text = properties ②
  }
}
//end::write[]

//tag::read[]
def getFile(file_config,server_name){
  remoteSession("user:passwd@$server_name:22") {①
  result = remoteFile(file_config).text ②
  }
  return result.readLines()③
}
//end::read[]

210 |
101 Groovy Script

Envío de eventos con RabbitMQ


Jorge Aguilera,<jorge.aguilera@puravida-software.com [mailto:jorge.aguilera@puravida-
software.com]> 2018-01-27

En este artículo vamos a desarrollar una solución muy simple para monitorizar ficheros
de logs en múltiples máquinas , enviando los cambios que se van produciendo en los
mismos a un componente central el cual será el encargado de su tratamiento (persistencia
a base de datos, análisis de las lineas recibidas, alarmas, etc).

Para ello vamos a utilizar el mismo script en dos modos diferentes (proporcionando un
argumento en su ejecución):

• producer , monitoriza un fichero y cada vez que se produzca un cambio en el mismo


lo notificará enviando la última línea que se haya escrito en el fichero. Este modo será
ejecutado en varias máquinas de forma simultánea.

• consumer, recibe los eventos de los diferentes producer’S y los va mostrando por
pantalla

Para ello usaremos un gestor de mensajería RabbitMQ, el cual será el encargado de


proporcionar el canal de envío y recepción así como de la persistencia de los mensajes
hasta su consumo.

RabbitMQ
RabbitMQ es un software destinado a la gestión del intercambio de mensajes, en su más
amplia aceptación, entre aplicaciones. Un software cliente se subscribe al mismo para
recibir notificaciones de cuando otro software cliente envía un mensaje siendo el gestor el
encargado de gestionar la entrega, persistencia hasta su consumo, reintentos, etc.

Para nuestro scrip vamos a crear una instancia de este broker mediante el uso de la
imagen oficial Docker, pero no vamos a entrar en detalles de cómo hacer backups,
seguridad etc.

Una vez que tengamos instalado Docker en alguna de nuestras máquinas ejecutaremos:

$docker run -d --hostname my-rabbit \


  --name some-rabbit \
  -v $(pwd)/rabbit://var/lib/rabbitmq/mnesia/rabbit \
  -p 5672:5672 -p 15672:15672 \
  rabbitmq:3-management

Básicamente creamos una instancia docker some-rabbit que va a usar un subdirectorio


rabbit de la máquina donde se está ejecutando para la persistencia de los mensajes y por
último vamos a utlizar un par de puertos para poder acceder a dicha instancia (sólo
necesitamos 5672 pero si quieres acceder a la consola de RabbitMQ vía web para poder

| 211
101 Groovy Script

inspeccionarlo usamos 15672)

Conexion
Lo primero que hará el script es establecer conexión con el gestor (en nuestro caso vamos
a usar una conexión en localhost con las credenciales por defecto) y configurar en qué
entorno publicará/recibirá los mensajes:

exchangeName="groovy-script"
queueName="grabbit"
routingKey='#'

factory = new ConnectionFactory()


  factory.username='guest'
  factory.password='guest'
  factory.virtualHost='/'
  factory.host= args[0] ?: 'localhost'
  factory.port=5672
conn = factory.newConnection()

channel = conn.createChannel()
channel.exchangeDeclare(exchangeName, "direct", true)
channel.queueDeclare(queueName, true, false, false, null)
channel.queueBind(queueName, exchangeName, routingKey)

Por simplificar configuramos tanto el exchange, el routing y la cola en el mismo sitio sin
importar si es consumer o producer

RabbitMQ nos permite crear los canales de múltiples formas (con o sin persistencia,
subscripción o directo, etc) Nosotros vamos a usar una configuración típica donde los
mensajes se guarden para asegurar su entrega.

Monitorizar fichero (producer)


En el modo producer el script utilizará la librería de Apache VFS2 para monitorizar un
fichero de tal forma que cada vez que se modifique este fichero recibiremos una llamada
en nuestro código (`void fileChanged(FileChangeEvent evt) throws Exception `)

212 |
101 Groovy Script

  monitor = args[2]

  if (new File(monitor).exists() == false)


  new File(monitor).newPrintWriter().println "${new Date()}"

  FileSystemManager manager = VFS.getManager();


  FileObject fobj = manager.resolveFile(new File(monitor).absolutePath)
  DefaultFileMonitor fm = new DefaultFileMonitor(this as FileListener)
  fm.delay = 500
  fm.addFile(fobj)
  fm.start()

Cada vez que recibimos el evento de que el fichero ha sido modificado, lo leeremos desde
el final hacia atrás buscando el último retorno de carro y de esta forma obtener la última
línea del fichero. Obviamente no es una solución robusta para sistemas donde el cambio
en el fichero puedan ser de varias líneas, en nuestro caso es un simple ejemplo sujeto de
ser modificado a situaciones más robustas

Por otra parte, la librería de RabbitMQ nos permite una gran granularidad en el envío del
mensaje. En nuestro caso vamos a usar la forma más simple en la cual simplemente
indicamos dónde queremos publicar el mensaje y lo enviamos como una simple cadena
de texto (como bytes)

void fileChanged(FileChangeEvent evt) throws Exception {

  RandomAccessFile randomAccessFile = new RandomAccessFile(evt.file.localFile, "r"); ①


  long fileLength = evt.file.localFile.length() - 1;
  randomAccessFile.seek(fileLength);
  StringBuilder stringBuilder = new StringBuilder()

  for(long pointer = fileLength; pointer >= 0; pointer--){


  randomAccessFile.seek(pointer)
  char c = (char)randomAccessFile.read()
  if(c == '\n' && fileLength == pointer){
  continue
  }
  if(c == '\n' && fileLength != pointer){
  break
  }
  stringBuilder.append(c)
  }
  println "Soy el producer enviando ${stringBuilder.toString().reverse()}"
  channel.basicPublish(exchangeName, routingKey, null, stringBuilder.toString().reverse().bytes) ②
}

① Usamos un RandomAccess que nos permite movernos por el fichero en lugar de leerlo
secuencial

② Publicamos en el sistema la linea leída

| 213
101 Groovy Script

Recibir eventos (consumer)


La parte "consumer" del script simplemente va a conectarse al gestor de mensajes el cual
le avisará cada vez que haya un mensaje que cumpla las condiciones de exchange/routing
indicadas. En nuestro caso el script simplemente lo traceará en la consola

  boolean autoAck = true;


  channel.basicConsume(queueName, autoAck, new DefaultConsumer(channel) {
  public void handleDelivery(String consumerTag,
  Envelope envelope,
  AMQP.BasicProperties properties,
  byte[] body)
  throws IOException{
  println "Soy el consumer recibiendo ${new String(body)}"
  }
  });

Resultado
En este screenshot puedes ver un caso de ejecución. En la ventana inferior se encuentra
corriendo el consumer a la espera de eventos de RabbitMQ, en la superior se encuentra el
producer esperando eventos de VFS y en la superior derecha se realiza un cambio en el
fichero que se está monitorizando (volcando la fecha de ese momento).

El cambio en el fichero es detectado por el producer y propagado hasta el consumer. En


este ejemplo todo se ejecuta en el mismo equipo pero como hemos dicho su interés radica
en que estos componentes pueden estar ejecutándose en otras máquinas e incluso redes

214 |
101 Groovy Script

Docker
Para comprobar que la solución puede ser ejecutada en un entorno distribuido vamos a
crear una "mini-red" docker con varios contenedores:

• rabbitmq-container, será el contenedor donde estará corriendo el servidor de colas


RabbitMQ

• consumer, será el contenedor donde estará corriendo el script en modo consumer


(mostrará por pantalla los eventos que reciba)

• producer1, será un contenedor observando un fichero

• producer2, será un contenedor observando otro fichero diferente

Para ello vamos a crear un fichero docker-compose.yml donde vamos a configurar todos
estos containers:

docker-compose.yml

version: '2'

services:

  ①
  rabbitmq-container:
  image: rabbitmq:3-management
  hostname: my-rabbit
  volumes:
  - ./rabbit:/var/lib/rabbitmq/mnesia/rabbit
  ports:
  - 5672:5672
  - 15672:15672

  ②
  consumer:
  image: groovy:2.4-jdk8
  volumes:
  - ./:/home/groovy/
  command: groovy /home/groovy/grabbit.groovy rabbitmq-container consumer ③

  ④
  producer1:
  image: groovy:2.4-jdk8
  volumes:
  - ./:/home/groovy/
  command: groovy /home/groovy/grabbit.groovy rabbitmq-container producer /home/groovy/grabbit.log ⑤

  ④
  producer2:
  image: groovy:2.4-jdk8
  volumes:
  - ./:/home/groovy/
  command: groovy /home/groovy/grabbit.groovy rabbitmq-container producer /home/groovy/grabbit2.log ⑥

① rabbitmq-container es el "hostname" por el que el resto de containers lo pueden


encontrar en su network

| 215
101 Groovy Script

② consumer es el nombre del container donde corre el script en modo lectura de la cola

③ indicamos el script a ejecutar así como el nombre del host a conectarse y el modo

④ tenemos dos producers (producer1 y producer2)

⑤ producer1 observará un fichero en lo que en su directorio de trabajo

⑥ producer2 observará un fichero diferente en lo que en su directorio de trabajo

Una vez definidos nuestros componentes levantamos el entorno:

$ docker-compose up

y actualizamos cualquiera de los dos ficheros que están siendo observados. En la consola
de docker iremos viendo indentificados cada container en las trazas que generan

216 |
101 Groovy Script

Script

@Grapes([
  @Grab(group='com.rabbitmq', module='amqp-client', version='3.1.2'),
  @Grab(group='org.apache.commons', module='commons-vfs2', version='2.2')
])

import com.rabbitmq.client.*
import groovy.json.*

import org.apache.commons.vfs2.FileChangeEvent
import org.apache.commons.vfs2.FileListener
import org.apache.commons.vfs2.FileObject
import org.apache.commons.vfs2.FileSystemManager
import org.apache.commons.vfs2.VFS
import org.apache.commons.vfs2.impl.DefaultFileMonitor

//tag::conexion[]
exchangeName="groovy-script"
queueName="grabbit"
routingKey='#'

factory = new ConnectionFactory()


  factory.username='guest'
  factory.password='guest'
  factory.virtualHost='/'
  factory.host= args[0] ?: 'localhost'
  factory.port=5672
conn = factory.newConnection()

channel = conn.createChannel()
channel.exchangeDeclare(exchangeName, "direct", true)
channel.queueDeclare(queueName, true, false, false, null)
channel.queueBind(queueName, exchangeName, routingKey)
//end::conexion[]

if( args[1] == 'producer' ) {

  //tag::producer[]
  monitor = args[2]

  if (new File(monitor).exists() == false)


  new File(monitor).newPrintWriter().println "${new Date()}"

  FileSystemManager manager = VFS.getManager();


  FileObject fobj = manager.resolveFile(new File(monitor).absolutePath)
  DefaultFileMonitor fm = new DefaultFileMonitor(this as FileListener)
  fm.delay = 500
  fm.addFile(fobj)
  fm.start()
  //end::producer[]

if( args[1] == 'consumer' ) {


  //tag::consumer[]
  boolean autoAck = true;
  channel.basicConsume(queueName, autoAck, new DefaultConsumer(channel) {
  public void handleDelivery(String consumerTag,
  Envelope envelope,

| 217
101 Groovy Script

  AMQP.BasicProperties properties,
  byte[] body)
  throws IOException{
  println "Soy el consumer recibiendo ${new String(body)}"
  }
  });
  //end::consumer[]
}

//tag::filechanged[]
void fileChanged(FileChangeEvent evt) throws Exception {

  RandomAccessFile randomAccessFile = new RandomAccessFile(evt.file.localFile, "r"); ①


  long fileLength = evt.file.localFile.length() - 1;
  randomAccessFile.seek(fileLength);
  StringBuilder stringBuilder = new StringBuilder()

  for(long pointer = fileLength; pointer >= 0; pointer--){


  randomAccessFile.seek(pointer)
  char c = (char)randomAccessFile.read()
  if(c == '\n' && fileLength == pointer){
  continue
  }
  if(c == '\n' && fileLength != pointer){
  break
  }
  stringBuilder.append(c)
  }
  println "Soy el producer enviando ${stringBuilder.toString().reverse()}"
  channel.basicPublish(exchangeName, routingKey, null, stringBuilder.toString().reverse().bytes) ②
}
//end::filechanged[]

void fileCreated(FileChangeEvent arg0) throws Exception {

void fileDeleted(FileChangeEvent arg0) throws Exception {

218 |
101 Groovy Script

Crear un fichero de texto con todas las


operaciones disponibles en el API del
INE
Jesús J. Ballano <jjballano@gmail.com [mailto:jjballano@gmail.com]> 2018-02-13

En este script vamos a guardar en un fichero de texto todas las operaciones disponibles
que nos devuelve el INE (Instituto Nacional de Estadística de España) desde su endpoint
http://servicios.ine.es//wstempus/js/ES/OPERACIONES_DISPONIBLES.

Crear ficheros de texto en Groovy es fácil y acceder a cualquier API REST con
HttpBuilderNG [https://github.com/http-builder-ng/http-builder-ng] también, así que los vamos a
combinar.

HttpBuilderNG es una librería que nos facilita mucho el acceso a cualquier API y que nos
permite elegir entre 3 posibles implementaciones: core, apache o okhttp. Para este ejemplo
vamos a usar la implementación core.

@Grab(group='io.github.http-builder-ng', module='http-builder-ng-core', version='1.0.3') ①

import groovyx.net.http.*

String baseUrl = "http://servicios.ine.es"


String path = "/wstempus/js/ES/OPERACIONES_DISPONIBLES"

def httpBin = HttpBuilder.configure { ②


  request.uri = baseUrl
}

def operations = httpBin.get { ③


  request.uri.path = path
}

File ineOperations = new File('/tmp/ineOperations.txt') ④

operations.each {
  ineOperations << "${it}\n" ⑤
}

① Recogemos la librería de HttpBuilderNG.

② Creamos la configuración con la uri base.

③ Ejecutamos la operación get.

④ Accedemos al fichero donde queremos guardar las distintas operaciones.

⑤ Por cada operación que nos devuelve el API, guardamos una línea en el fichero de
texto.

| 219
101 Groovy Script

Script

@Grab(group='io.github.http-builder-ng', module='http-builder-ng-core', version='1.0.3') ①

import groovyx.net.http.*

String baseUrl = "http://servicios.ine.es"


String path = "/wstempus/js/ES/OPERACIONES_DISPONIBLES"

def httpBin = HttpBuilder.configure { ②


  request.uri = baseUrl
}

def operations = httpBin.get { ③


  request.uri.path = path
}

File ineOperations = new File('/tmp/ineOperations.txt') ④

operations.each {
  ineOperations << "${it}\n" ⑤
}

220 |
101 Groovy Script

Servidor web en 1 fichero


Holger Garcia <holger@holgergarcia.com [mailto:holger@holgergarcia.com]> 2018-04-07

En este script veremos como usando Groovy y Ratpack podemos crear un servidor web
completo en un solo fichero.

Para un simple Hello world sería tan sencillo como:

@Grapes([
  @Grab('io.ratpack:ratpack-groovy:1.5.2'),
  @Grab('org.slf4j:slf4j-simple:1.7.25')
])
import static ratpack.groovy.Groovy.ratpack

ratpack {
  handlers {
  get {
  render "Hello World!"
  }
  }
}

Si guardamos este fichero con el nombre ratpack.groovy y lo ejecutamos con groovy


ratpack.groovy obtendremos una traza como la siguiente

[main] INFO ratpack.server.RatpackServer - Starting server...


[main] INFO ratpack.server.RatpackServer - Building registry...
[main] INFO ratpack.server.RatpackServer - Ratpack started (development) for http://localhost:5050

Voila!, hemos creado nuestro primer servidor en unas pocas líneas.

Ahora probemos con algo un poco mas interesante:

Por ejemplo podríamos servir ficheros locales usando

| 221
101 Groovy Script

@Grapes([
  @Grab('io.ratpack:ratpack-groovy:1.5.2'),
  @Grab('org.slf4j:slf4j-simple:1.7.25')
])
import static ratpack.groovy.Groovy.ratpack

ratpack {
  handlers {
  files {
  dir "."
  }
  }
}

En este caso estariamos sirviendo todo el directorio en el que se encuentre nuestro script,
perfecto si necesitas compartir rapidamente ciertos ficheros con tus compañeros

Tambien podríamos facilmente trabajar con JSON, por ejemplo en caso de que queramos
preparar una respuesta sencilla con la que los desarrolladores frontend puedan comenzar
a trabajar mientras desarrollamos nuestro backend

@Grapes([
  @Grab('io.ratpack:ratpack-groovy:1.5.2'),
  @Grab('org.slf4j:slf4j-simple:1.7.25')
])
import static ratpack.groovy.Groovy.ratpack
import static ratpack.jackson.Jackson.json;

ratpack {
  handlers {
  get {
  render(json([Ratpack: 'is really cool']))
  }
  }
}

Estos pequeños ejemplos solo arañan la superficie de lo que ratpack es capaz de hacer,
desde pequeños scripts rapidos como los presentados hasta aplicaciones completas con
conexion a base datos, multiples integraciones y toda la logica de negocio que puedas
desear.

Para mas informacion recomiendo visitar la documentacion oficial de Ratpack


[https://ratpack.io/manual/current/]

222 |
101 Groovy Script

Script

@Grapes([
  @Grab('io.ratpack:ratpack-groovy:1.5.2'),
  @Grab('org.slf4j:slf4j-simple:1.7.25')
])
import static ratpack.groovy.Groovy.ratpack

ratpack {
  handlers {
  files {
  dir "."
  }
  }
}

| 223
101 Groovy Script

Leer un fichero desde un host remoto


Miguel Rueda <miguel.rueda.garcia@gmail.com [mailto:miguel.rueda.garcia@gmail.com]>
2017-10-12

Seguramente os haya surgido la necesidad de comprobar el contenido de unos


determinados equipos remotos desde vuestro equipo como por ejemplo ver las trazas de
un fichero .log que va dejando la aplicación que habéis instalado, comprobar versiones o
propiedades guardadas en un fichero, modificar uno existente y querer comprobar que
unos determinados ficheros de configuración apuntan correctamente, etc.

Cuando sólo tienes un equipo que administrar esta tarea no es muy compleja y
probablemente la realizas a mano, pero cuando el número de equipos a comprobar crece
la tarea se vuelve muy repetitiva y cansada y es cuando piensas en crearte un script que
realice esta tarea en todas las máquinas por tí.

Para crear el script que facilite esta tarea vamos a emplear sshoogr [https://github.com/
aestasit/sshoogr].

Esta herramienta nos permite realizar sobre nuestros equipos muchas más funciones de
las que vamos a ver en este post, como copiar archivos y directorios, ejecutar comandos
remotos … os ánimo a que reviseis la documentación que seguro que os será de gran
ayuda.

A continuación pasamos a explicar en funcionamiento de nuestro script paso a paso:

• Añadimos la dependencia de sshoogr:

@Grab('com.aestasit.infrastructure.sshoogr:sshoogr:0.9.25')
@GrabConfig(systemClassLoader=true)
import static com.aestasit.infrastructure.ssh.DefaultSsh.*

• Creamos la función para leer el fichero:

def getFile(dir,host){
  remoteSession("user:passwd@$host:22") {①
  result = remoteFile(dir).text ②
  }
  return result.readLines()③

① Establecemos los parámtros de conexión usuario = user, contraseña = passwd


mientras que el servidor es variable.

② En la función remoteFile le pasamos por parámetro la ruta complete la fichero.

224 |
101 Groovy Script

③ Con la función readLines obtenemos una lista con el contenido del fichero.

• Creamos la lista de hosts y el nombre del fichero que queremos leer:

def hosts = ['server_1','server_2','server_3','server_4','server_5']


def file = '/var/log/www/access_log'

• recorremos los host y leemos el archivo en cuestion:

hosts.each{host->
  getFile(file,host).each{line->
  println line
  }
}

Si te da error al conectar con el host, prueba a desactivar la


 comprobación estricta de la clave del host:

options.trustUnknownHosts = true

| 225
101 Groovy Script

Script

//tag::dependencies[]
@Grab('com.aestasit.infrastructure.sshoogr:sshoogr:0.9.25')
@GrabConfig(systemClassLoader=true)
import static com.aestasit.infrastructure.ssh.DefaultSsh.*
//end::dependencies[]

options.trustUnknownHosts = true

//tag::config[]
def hosts = ['server_1','server_2','server_3','server_4','server_5']
def file = '/var/log/www/access_log'
//end::config[]

//tag::main[]
hosts.each{host->
  getFile(file,host).each{line->
  println line
  }
}
//end::main[]

//tag::read[]
def getFile(dir,host){
  remoteSession("user:passwd@$host:22") {①
  result = remoteFile(dir).text ②
  }
  return result.readLines()③

}
//end::read[]

226 |
101 Groovy Script

Contacts2QRCode
Jorge Aguilera <jorge.aguilera@puravida-software.com [mailto:jorge.aguilera@puravida-
software.com]> 2017-11-03

En ete script vamos a tratar las siguientes capacidades de Groovy:

• Leer todas las Hojas, Filas y Columnas de un Excel

• Escribir una celda en función de los valores leídos y/o lógica de negocio

• Invocar a un servicio remoto y volcar el resultado a un fichero

Objetivo
Contacts2QRCode es un script que partiendo de un Excel donde se hayan guardado datos
de contactos de una serie de personas, como por ejemplo los empleados de tu
organización, generar para cada uno de ellos un código QRCode para incluir en las
tarjetas de visita.

Un código QR (del inglés Quick Response code, "código de respuesta


rápida") es la evolución del código de barras. Es un módulo para
 almacenar información en una matriz de puntos o en un código de
barras bidimensional.

QRCode tiene numerosas utilidades y formatos. En este post trataremos


 cómo generar un VCard, formato destinado a especificar los datos de
contacto de una persona, como nombre, puesto, dirección, teléfono, etc.

Para la generación del código QRCode utilizaremos el servicio gratuito


 zxing (http://zxing.org/)

Excel
El excel con los datos de nuestros contactos contendrá la siguiente estructura, siendo la
primera fila destinada a contener la cabecera y por ello ignorada por el script:

Id id del empleado se utiliza como nombre de


fichero QR

Name nombre del empleado puede contener espacios y


ser compuesto con apellidos,
etc

JobTitle puesto puede ser blanco

| 227
101 Groovy Script

Phone telefóno asegurate que el formato es


una cadena para que no se
trate como numérico

eMail puede ser blanco

Address dirección en una sola línea

Organization

URL http://xxxx.yyy/zzzz si disponeis de página


personal para cada
empleado o en la empresa

QR esta columan será escrita


por el script cada vez que se
invoque

Dependencias
Para la lectura del Excel utilizaremos esta vez las librerías de Apache, POI

@Grab('org.apache.poi:poi:3.14')
@Grab('org.apache.poi:poi-ooxml:3.14')

Parseo del Excel


Durante la primera pasada, el script leerá todas las hojas de cálculo y sus filas suponiendo
que siguen el formato anteriormente explicado, generando para cada fila una URL
personalizada:

WorkbookFactory.create(f,null,false).withCloseable { workbook -> ①


  0.step workbook.getNumberOfSheets(), 1, { sheetNum -> ②
  println "Working on ${workbook.getSheetName(sheetNum)}"
  Sheet sheet = workbook.sheets[sheetNum]
  for( Row row : sheet){
  if( row.rowNum ){ ③
  row.createCell(8).cellValue = generateQRLink(row) ④
  }
  }
  }
  bos = new ByteArrayOutputStream()
  workbook.write(bos) ⑤
}
f << bos.toByteArray() ⑥

① Leemos el excel en modo escritura

228 |
101 Groovy Script

② Iteramos en todas las hojas del Excel. Podríamos buscar una en concreto si así lo
quisieramos

③ Iteramos por todas las filas ignorando la primera

④ Invocamos a la función que genera el link

⑤ Una vez terminado reescribimos el excel en memoria

⑥ Volcamos el buffer de memoria al fichero de origen

Dump de QRCodes
Si se especifica un segundo parámetro, este indicará el directorio donde queremos que se
generen los QRCodes de cada empleado. Realizaremos el mismo bucle de lectura que en el
apartado anterior pero esta vez buscaremos la columna que se actualizó. Dicha columna
es una URL que nos devuelve un PNG con el código generado, por lo que usaremos la
sintaxis de Groovy para descargarlo directamente a un fichero:

if( args.length > 1 ){


  File dumpFolder = new File(args[1])
  dumpFolder.mkdirs()
  WorkbookFactory.create(f,null,true).withCloseable { workbook -> ①
  0.step workbook.getNumberOfSheets(), 1, { sheetNum ->
  println "Dumping on ${workbook.getSheetName(sheetNum)}"
  Sheet sheet = workbook.sheets[sheetNum]
  for( Row row : sheet){
  if( row.rowNum ){
  new File(dumpFolder,"${row.getCell(0)}.png").withOutputStream {
  it.bytes = "${row.getCell(8)}".toURL().bytes ②
  }
  }
  }
  }
  }
}

① Leemos el excel en modo lectura

② Gracias a Groovy toURL() genera una URL y bytes nos devuelve el contenido de la
response como un array de bytes

generateQRLink
La función generateQRLink simplemente lee los campos de la fila Excel y los concatena a
la URL de Zxing devolviendo la cadena resultante

| 229
101 Groovy Script

String generateQRLink(Row range){


  int col=1
  String name = "${range.getCell(col++)}"
  String title = "${range.getCell(col++)}"
  String tlf= "${range.getCell(col++)}"
  String email="${range.getCell(col++)}"
  String addr="${range.getCell(col++)}"
  String organization="${range.getCell(col++)}"
  String url="${range.getCell(col++)}"

  String vcard = urlRoot + generateVCard(name, title , tlf , email, addr, organization, url)
  vcard
}

GenerateVCard
La función generateVCard a la que se hace referencia en el código anterior es una simple
concatenación de String en formato VCARD:

String generateVCard( name, title , tlf , email, addr, organization, url){


  String getvcard = "BEGIN%3AVCARD%0AVERSION%3A3.0";
  if(name) getvcard += "%0AN%3A"+URLEncoder.encode(name,"UTF-8") ①
  if(organization) getvcard += "%0AORG%3A"+URLEncoder.encode(organization,"UTF-8")
  if(title) getvcard += "%0ATITLE%3A"+URLEncoder.encode(title,"UTF-8")
  if(tlf) getvcard += "%0ATEL%3A"+URLEncoder.encode(tlf,"UTF-8");
  if(email) getvcard += "%0AEMAIL%3A"+URLEncoder.encode(email,"UTF-8");
  if(addr) getvcard += "%0AADR%3A"+URLEncoder.encode(addr,"UTF-8")
  if(url) getvcard += "%0AURL%3A"+URLEncoder.encode(url,"UTF-8");
  getvcard += "%0AEND%3AVCARD";
  return getvcard;
}

① Utilizaremos URLEncoder para conseguir que los campos con espacio, acentos etc sean
convertidos correctamente

230 |
101 Groovy Script

Script

//tag::dependencies[]
@Grab('org.apache.poi:poi:3.14')
@Grab('org.apache.poi:poi-ooxml:3.14')
//end::dependencies[]

import org.apache.poi.ss.usermodel.*

if (args.length == 0) {
  println "Use:"
  println " groovy Constact2QRCode.groovy [excel-file]"
  return 1
}

urlRoot = "http://zxing.org/w/chart?cht=qr&chs=350x350&chld=H&choe=UTF-8&chl=";

File f = new File(args[0]);


ByteArrayOutputStream bos

//tag::readExcel[]
WorkbookFactory.create(f,null,false).withCloseable { workbook -> ①
  0.step workbook.getNumberOfSheets(), 1, { sheetNum -> ②
  println "Working on ${workbook.getSheetName(sheetNum)}"
  Sheet sheet = workbook.sheets[sheetNum]
  for( Row row : sheet){
  if( row.rowNum ){ ③
  row.createCell(8).cellValue = generateQRLink(row) ④
  }
  }
  }
  bos = new ByteArrayOutputStream()
  workbook.write(bos) ⑤
}
f << bos.toByteArray() ⑥
//end::readExcel[]

//tag::dumpQr[]
if( args.length > 1 ){
  File dumpFolder = new File(args[1])
  dumpFolder.mkdirs()
  WorkbookFactory.create(f,null,true).withCloseable { workbook -> ①
  0.step workbook.getNumberOfSheets(), 1, { sheetNum ->
  println "Dumping on ${workbook.getSheetName(sheetNum)}"
  Sheet sheet = workbook.sheets[sheetNum]
  for( Row row : sheet){
  if( row.rowNum ){
  new File(dumpFolder,"${row.getCell(0)}.png").withOutputStream {
  it.bytes = "${row.getCell(8)}".toURL().bytes ②
  }
  }
  }
  }
  }
}
//end::dumpQr[]

| 231
101 Groovy Script

//tag::generateVCard[]
String generateVCard( name, title , tlf , email, addr, organization, url){
  String getvcard = "BEGIN%3AVCARD%0AVERSION%3A3.0";
  if(name) getvcard += "%0AN%3A"+URLEncoder.encode(name,"UTF-8") ①
  if(organization) getvcard += "%0AORG%3A"+URLEncoder.encode(organization,"UTF-8")
  if(title) getvcard += "%0ATITLE%3A"+URLEncoder.encode(title,"UTF-8")
  if(tlf) getvcard += "%0ATEL%3A"+URLEncoder.encode(tlf,"UTF-8");
  if(email) getvcard += "%0AEMAIL%3A"+URLEncoder.encode(email,"UTF-8");
  if(addr) getvcard += "%0AADR%3A"+URLEncoder.encode(addr,"UTF-8")
  if(url) getvcard += "%0AURL%3A"+URLEncoder.encode(url,"UTF-8");
  getvcard += "%0AEND%3AVCARD";
  return getvcard;
}
//end::generateVCard[]

//tag::generateQRLink[]
String generateQRLink(Row range){
  int col=1
  String name = "${range.getCell(col++)}"
  String title = "${range.getCell(col++)}"
  String tlf= "${range.getCell(col++)}"
  String email="${range.getCell(col++)}"
  String addr="${range.getCell(col++)}"
  String organization="${range.getCell(col++)}"
  String url="${range.getCell(col++)}"

  String vcard = urlRoot + generateVCard(name, title , tlf , email, addr, organization, url)
  vcard
}
//end::generateQRLink[]

232 |
101 Groovy Script

Desmenuzando un Pdf
Jorge Aguilera <jorge.aguilera@puravida-software.com [mailto:jorge.aguilera@puravida-
software.com]> 2017-11-03

En ete script vamos a tratar las siguientes capacidades de Groovy:

• Incluir dependencias externas, en concreto PdfBox de Apache

• Realizar bucles anidados de una forma sencilla

• Generar mapas (key/value) al vuelo

Objetivo
Mediante este script y usando la librería PdfBox de Apache, dividiremos las páginas de un
Pdf en áreas de tal forma que hagamos un "barrido" en cada una de ellas y podamos
extraer el texto que se encuentre en las regiones. Al ir haciendo el "barrido" podremos
identificar cada línea leída con una posición dentro de la página

Dependencias
Para la lectura del Pdf utilizaremos esta vez las librerías de Apache, PdfBox

@Grapes(
  @Grab(group='org.apache.pdfbox', module='pdfbox', version='2.0.8')
)
import org.apache.pdfbox.pdmodel.PDDocument
import org.apache.pdfbox.text.*
import java.awt.Rectangle

Script

| 233
101 Groovy Script

margenright = 10 ①

PDDocument document = PDDocument.load(new URL(args[0]).bytes)

def pages = [:]


document.documentCatalog.pages.eachWithIndex { page , pageIndex->

  def paragraphs = [:]

  PDFTextStripperByArea stripper = new PDFTextStripperByArea(sortByPosition:true)


  (0..42).each { ②
  stripper.addRegion("$it", new Rectangle( margenright, it * 20, 1500, 20)); ③
  }
  stripper.extractRegions(page)

  stripper.regions.eachWithIndex{ r, index->
  def str = stripper.getTextForRegion(r)
  if (str.trim().length() > 0)
  paragraphs["$index"] = str.trim() ④
  }
  pages["$pageIndex"] = paragraphs ⑤
}
println pages

① Puedes "jugar" a definir el margen derecho para afinar cuanto texto tomar desde la
derecha.

② Definimos 42 regiones de 20 pixeles, suficientes para un A4

③ Definimos un área de 1500x20 suficientes para leer el ancho de un A4

④ Para cada region que encuentra con texto la añadimos dinámicamente a un mapa
creado para la página

⑤ Para cada página añadimos las áreas encontradas en un mapa creado para todo el
documento.

En una linea (más Grappe)


@jmiguel en un afán minimalista nos comenta que el código anterior cumple su función
en una sóla línea:

println new PDFTextStripper().getText(PDDocument.load(new URL(args[0]).bytes))①

<1>Si el primer argumento es una ruta local a un PDF, comienzalo con file://

234 |
101 Groovy Script

Script

//tag::dependencies[]
@Grapes(
  @Grab(group='org.apache.pdfbox', module='pdfbox', version='2.0.8')
)
import org.apache.pdfbox.pdmodel.PDDocument
import org.apache.pdfbox.text.*
import java.awt.Rectangle
//end::dependencies[]

//tag::sourceCode[]
margenright = 10 ①

PDDocument document = PDDocument.load(new URL(args[0]).bytes)

def pages = [:]


document.documentCatalog.pages.eachWithIndex { page , pageIndex->

  def paragraphs = [:]

  PDFTextStripperByArea stripper = new PDFTextStripperByArea(sortByPosition:true)


  (0..42).each { ②
  stripper.addRegion("$it", new Rectangle( margenright, it * 20, 1500, 20)); ③
  }
  stripper.extractRegions(page)

  stripper.regions.eachWithIndex{ r, index->
  def str = stripper.getTextForRegion(r)
  if (str.trim().length() > 0)
  paragraphs["$index"] = str.trim() ④
  }
  pages["$pageIndex"] = paragraphs ⑤
}
println pages
//end::sourceCode[]

| 235
101 Groovy Script

Importar datos de un excel a una tabla


Miguel Rueda <miguel.rueda.garcia@gmail.com [mailto:miguel.rueda.garcia@gmail.com]>
2017-10-08

Seguramente a lo largo de tu vida te habrás encontrado con algún


cliente/compañero/amigo/no tan amigo al cual le has preguntado dónde guarda una
determinada información sobre la que estáis trabajando y te ha contestado que en un
Excel en su local. Tras la risa nerviosa habrás pasado por la irá, hasta acabar en la etapa
del miedo. Bueno una vez la calma llegue podrás darle la opción de incluirlo en la base de
datos con la que trabajáis. Para ello pasamos a explicar cómo realizarlo.

Para trabajar con este script vamos a necesitar las siguientes librerías:

@Grab('org.apache.poi:poi:3.8')
@Grab('org.apache.poi:poi-ooxml:3.8')
@Grab('mysql:mysql-connector-java:5.1.6')

El siguiente paso que vamos a realizar será la conexión con la base de datos, en este caso
contra nuestra máquina.

def sql = Sql.newInstance( "jdbc:mysql://localhost:3306/origen?jdbcCompliantTruncation=false",


  "user",
  "password",
  "com.mysql.jdbc.Driver")

Las variables necesarias para este script las hemos definido dentro del mismo pero una
forma mejor de hacerlo sería recibir estar información por parámetro y comprobando
que cada uno de los datos son correctos, es decir, que el fichero existe, que la conexión
con la base de datos es correcta y por último que existe la tabla en la que queremos cargar
los datos. En este caso práctico nos vamos a centrar únicamente en la carga de datos por
lo tanto vamos a dar valor a cada uno de los valores necesarios:

def table = "myTable"


def path_file = "${System.properties['user.home']}/myExcel.xlsx"
def list = parse(path_file)

Una vez tenemos los requisitos necesarios vamos a crear el método parse al cual le
pasaremos por parámetro la ruta donde se encuentra nuestro Excel y él creará un array
con los datos de nuestro fichero, en este caso un nombre fijado a myExcel.xlsx

236 |
101 Groovy Script

def parse(path) {
  InputStream inp = new FileInputStream(path)
  Workbook wb = WorkbookFactory.create(inp);
  Sheet sheet = wb.getSheetAt(0);①

  Iterator<Row> rowIt = sheet.rowIterator() ②


  Row row = rowIt.next()
  def headers = getRowData(row) ③

  def rows = []
  while(rowIt.hasNext()) {
  row = rowIt.next()
  rows << getRowData(row)
  }
  [headers, rows]
}

① Con el objeto WorkbookFactory, accediendo al método create podemos acceder a nuestro


fichero y con el método getSheetAt(0) del objeto Workbook podremos obtener la primera
hoja (Sheet) de nuestro Excel.

② Para recorrer nuestra hoja seleccionada utilizaremos la función rowIterator() que nos
va permitir acceder a los valores que se encuentran en cada una de las filas.

③ La primera fila asumimos que es la cabecera del Excel y que contiene el nombre
descriptivo de cada columna. Para obtener este mapa utilizamos nuestro método
getRowData:

def getRowData(Row row) {


  def data = []
  for (Cell cell : row) {
  getValue(row, cell, data)
  }
  data
}

El cual recorre cada una las filas que tiene nuestro Excel convirtiendo cada valor en
información con la que podamos trabajar, dependiendo del formato que se encuentre en
cada celda, es decir, si en una cadena lo formaterá a String, si es un valor entero a int…

Para este trabajo contamos con el método getValue:

| 237
101 Groovy Script

def getValue(Row row, Cell cell, List data) {


  def rowIndex = row.getRowNum()
  def colIndex = cell.getColumnIndex()
  def value = ""
  switch (cell.getCellType()) {
  case Cell.CELL_TYPE_STRING:
  value = cell.getRichStringCellValue().getString();
  break;
  case Cell.CELL_TYPE_NUMERIC:
  if (DateUtil.isCellDateFormatted(cell)) {
  value = cell.getDateCellValue();
  } else {
  value = cell.getNumericCellValue();
  }
  break;
  case Cell.CELL_TYPE_BOOLEAN:
  value = cell.getBooleanCellValue();
  break;
  case Cell.CELL_TYPE_FORMULA:
  value = cell.getCellFormula();
  break;
  default:
  value = ""
  }
  data[colIndex] = value
  data
}

En el cual podemos ver claramente como formatea cada unos de los valores. Como lo
realizar por ejemplo si el formato de la celda es un String:

  case Cell.CELL_TYPE_STRING:
  value = cell.getRichStringCellValue().getString();
  break;

Bien ya hemos conseguido leer el nuestro fichero y convertirlo en una lista con la que
podemos trabajar. Nuestro siguiente paso será abordar nuestra tabla en este caso sobre
una base de datos mysql. Para ello vamos a utilizar:

238 |
101 Groovy Script

def insertValues(table,list,sql){
  int index = 0
  sql.rows(""" desc $table """.toString()).each{c-> ①
  keys = index == 0?(keys + ":$c.Field"):(keys + ",:$c.Field")
  index++

  }
  sql.withBatch(20, """ insert into $table values ( $keys ) """.toString()) { ps -> ②
  list.each { t->
  ps.addBatch(t)
  }
  }
}

① Obtenemos el esquema de la tabla recibida.

② A través de la herramienta withBatch vamos a recorrer la lista de valores e insertar de


20 en 20 en nuestra base de datos.

| 239
101 Groovy Script

Script

//tag::grab[]
@Grab('org.apache.poi:poi:3.8')
@Grab('org.apache.poi:poi-ooxml:3.8')
@Grab('mysql:mysql-connector-java:5.1.6')
//end::grab[]

import org.apache.poi.ss.usermodel.*
import org.apache.poi.hssf.usermodel.*
import org.apache.poi.xssf.usermodel.*
import org.apache.poi.ss.util.*
import org.apache.poi.ss.usermodel.*
import java.io.*
import groovy.sql.Sql
//tag::mysql[]
def sql = Sql.newInstance( "jdbc:mysql://localhost:3306/origen?jdbcCompliantTruncation=false",
  "user",
  "password",
  "com.mysql.jdbc.Driver")
//end::mysql[]
//tag::variables[]
def table = "myTable"
def path_file = "${System.properties['user.home']}/myExcel.xlsx"
def list = parse(path_file)
//end::variables[]
insertValues(table,list,sql)
//tag::insert[]
def insertValues(table,list,sql){
  int index = 0
  sql.rows(""" desc $table """.toString()).each{c-> ①
  keys = index == 0?(keys + ":$c.Field"):(keys + ",:$c.Field")
  index++

  }
  sql.withBatch(20, """ insert into $table values ( $keys ) """.toString()) { ps -> ②
  list.each { t->
  ps.addBatch(t)
  }
  }
}
//end::insert[]
//tag::parse[]
def parse(path) {
  InputStream inp = new FileInputStream(path)
  Workbook wb = WorkbookFactory.create(inp);
  Sheet sheet = wb.getSheetAt(0);①

  Iterator<Row> rowIt = sheet.rowIterator() ②


  Row row = rowIt.next()
  def headers = getRowData(row) ③

  def rows = []
  while(rowIt.hasNext()) {
  row = rowIt.next()
  rows << getRowData(row)
  }

240 |
101 Groovy Script

  [headers, rows]
}
//end::parse[]
//tag::row[]
def getRowData(Row row) {
  def data = []
  for (Cell cell : row) {
  getValue(row, cell, data)
  }
  data
}
//end::row[]
//tag::value[]
def getValue(Row row, Cell cell, List data) {
  def rowIndex = row.getRowNum()
  def colIndex = cell.getColumnIndex()
  def value = ""
  switch (cell.getCellType()) {
  //tag::string_f[]
  case Cell.CELL_TYPE_STRING:
  value = cell.getRichStringCellValue().getString();
  break;
  //end::string_f[]
  case Cell.CELL_TYPE_NUMERIC:
  if (DateUtil.isCellDateFormatted(cell)) {
  value = cell.getDateCellValue();
  } else {
  value = cell.getNumericCellValue();
  }
  break;
  case Cell.CELL_TYPE_BOOLEAN:
  value = cell.getBooleanCellValue();
  break;
  case Cell.CELL_TYPE_FORMULA:
  value = cell.getCellFormula();
  break;
  default:
  value = ""
  }
  data[colIndex] = value
  data
}
//end::value[]

| 241
101 Groovy Script

Volcar datos de una tabla a Excel


Miguel Rueda <miguel.rueda.garcia@gmail.com [mailto:miguel.rueda.garcia@gmail.com]>
2017-10-08

El caso práctico en el cuál este script nos puede resultar muy útil es cuando por ejemplo
existe la necesidad de extraer información de una base de datos para crear una serie de
informes que nos solicita un cliente, como puede ser obtener información de unos
determinados productos.

Muchos motores de bases de datos y/o herramientas son capaces de exportar esta
información a fichero CSV (fichero delimitado por comas, punto y coma, pipes, etc) y
posteriormente importarlo en un Excel.

Supongamos que disponemos de una tabla de productos donde entre otra información
guardamos el código de producto y la descripción.

SKU DESCRIPTION

1 LAPICES

2 SACAPUNTAS

XXXXX UN PRODUCTO SUPERCOTIZADO

Extraer esta información es trivial con una simple sentencia SQL:

select concat(sku,'|', description, '|') from products order by sku

y redirigir la salida de esta sentencia a un fichero para ser abierto con Excel.

Pero con este script nos evitamos todo el proceso de conversión, generando un Excel
directamente:

242 |
101 Groovy Script

@Grab('mysql:mysql-connector-java:5.1.6')①
@Grab('net.sourceforge.jexcelapi:jxl:2.6.12')
@GrabConfig(systemClassLoader=true)

import jxl.*
import jxl.write.*
import groovy.sql.Sql

filename = "informe.xls"

sql = Sql.newInstance( "jdbc:mysql://localhost:3306/origen?jdbcCompliantTruncation=false",


  "user",
  "password",
  "com.mysql.jdbc.Driver")

workbook = Workbook.createWorkbook(new File(filename))

sheet = workbook.createSheet("productos", 0)

first=true
i=0
sql.eachRow("select sku, description from products order by sku") { row -> ②

  if( first ){
  sheet.addCell (new Label (0,i,"Sku"))
  sheet.addCell ( new Label (1,i,"Product") ) ③
  first=false
  }

  sheet.addCell( new Label (0,i+1,"$l.sku") ) ④


  sheet.addCell ( new Label (1,i+1,l.description) )
  i++
}
workbook.write()
workbook.close()

① Cargamos dependencias (MySQL y JExcel)

② Para cada registro que cumpla la query generamos una fila nueva

③ Podemos generar cabeceras en la primera fila

④ Ejemplo de cómo crear dos celdas de tipo texto, pero existen otros tipos como Date o
Number

| 243
101 Groovy Script

Script

@Grab('mysql:mysql-connector-java:5.1.6')①
@Grab('net.sourceforge.jexcelapi:jxl:2.6.12')
@GrabConfig(systemClassLoader=true)

import jxl.*
import jxl.write.*
import groovy.sql.Sql

filename = "informe.xls"

sql = Sql.newInstance( "jdbc:mysql://localhost:3306/origen?jdbcCompliantTruncation=false",


  "user",
  "password",
  "com.mysql.jdbc.Driver")

workbook = Workbook.createWorkbook(new File(filename))

sheet = workbook.createSheet("productos", 0)

first=true
i=0
sql.eachRow("select sku, description from products order by sku") { row -> ②

  if( first ){
  sheet.addCell (new Label (0,i,"Sku"))
  sheet.addCell ( new Label (1,i,"Product") ) ③
  first=false
  }

  sheet.addCell( new Label (0,i+1,"$l.sku") ) ④


  sheet.addCell ( new Label (1,i+1,l.description) )
  i++
}
workbook.write()
workbook.close()

244 |
101 Groovy Script

Mail
Miguel Rueda <miguel.rueda.garcia@gmail.com [mailto:miguel.rueda.garcia@gmail.com]>
2017-08-04

Creamos la clase Mail.groovy

import javax.mail.*
import javax.mail.internet.*
import java.util.Properties;
import java.text.DecimalFormat;
import javax.activation.DataHandler;
import javax.activation.DataSource;
import javax.activation.FileDataSource;
import javax.mail.BodyPart;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Multipart;
import javax.mail.PasswordAuthentication;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;

class Mail{
  def static sendEmail(subjetc,text,from,to){ ①

  def d_email = from,


  d_uname = "",
  d_password = "",
  d_host = "smtp-relay.gmail.com",
  d_port = "587", //465,587
  m_to = to,
  m_subject = subjetc,
  m_text = text

  def props = new Properties() ②


  props.put("mail.smtp.user", d_email)
  props.put("mail.smtp.host", d_host)
  props.put("mail.smtp.port", d_port)
  props.put("mail.smtp.starttls.enable","true")
  props.put("mail.smtp.debug", "false")
  props.put("mail.smtp.auth", "false")
  props.put("mail.protocol", "smtp")

| 245
101 Groovy Script

  def session = Session.getInstance(props)


  session.setDebug(true);

  def msg = new MimeMessage(session)


  msg.setText(m_text)
  msg.setSubject(m_subject)
  msg.setFrom(new InternetAddress(d_email))
  msg.addRecipient(Message.RecipientType.TO, new InternetAddress(m_to))

  Transport.send( msg ) ③
  }
}

① Definimos el método sendEmail(subjetc,text,from,to) al cual mandamos como


argumento el asunto, el texto que que queremos incluir en el email, el emisor y el
receptor.

② Definimos el properties que utilizaremos para enviar el email.

③ Enviamos el email.

Llamada y envío del email


Abrimos una consola de groovy en la misma posición en la que se encuentre nuesto
Mail.groovy.

Mail.sendEmail("Email de test","Esto es una prueba de lo bien que lo


hacemos",'emisor@gmail.com','receptor@gmail.com)

246 |
101 Groovy Script

Script

@Grab('mysql:mysql-connector-java:5.1.6')①
@Grab('net.sourceforge.jexcelapi:jxl:2.6.12')
@GrabConfig(systemClassLoader=true)

import jxl.*
import jxl.write.*
import groovy.sql.Sql

filename = "informe.xls"

sql = Sql.newInstance( "jdbc:mysql://localhost:3306/origen?jdbcCompliantTruncation=false",


  "user",
  "password",
  "com.mysql.jdbc.Driver")

workbook = Workbook.createWorkbook(new File(filename))

sheet = workbook.createSheet("productos", 0)

first=true
i=0
sql.eachRow("select sku, description from products order by sku") { row -> ②

  if( first ){
  sheet.addCell (new Label (0,i,"Sku"))
  sheet.addCell ( new Label (1,i,"Product") ) ③
  first=false
  }

  sheet.addCell( new Label (0,i+1,"$l.sku") ) ④


  sheet.addCell ( new Label (1,i+1,l.description) )
  i++
}
workbook.write()
workbook.close()

| 247
101 Groovy Script

Mail con documentos adjuntos


Miguel Rueda <miguel.rueda.garcia@gmail.com [mailto:miguel.rueda.garcia@gmail.com]>
2017-08-04

Partimos de la clase Mail creada en el apartado anterior:

import javax.mail.*
import javax.mail.internet.*
import java.util.Properties;
import java.text.DecimalFormat;
import javax.activation.DataHandler;
import javax.activation.DataSource;
import javax.activation.FileDataSource;
import javax.mail.BodyPart;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Multipart;
import javax.mail.PasswordAuthentication;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;

class Mail{
  def static sendEmail(path,subjetc,text,from,to){ ①

  def d_email = from,


  d_uname = "",
  d_password = "",
  d_host = "smtp-relay.gmail.com",
  d_port = "587", //465,587
  m_to = to,
  m_subject = subjetc,
  m_text = text

  def props = new Properties()


  props.put("mail.smtp.user", d_email)
  props.put("mail.smtp.host", d_host)
  props.put("mail.smtp.port", d_port)
  props.put("mail.smtp.starttls.enable","true")
  props.put("mail.smtp.debug", "false")
  props.put("mail.smtp.auth", "false")
  props.put("mail.protocol", "smtp")

248 |
101 Groovy Script

  def session = Session.getInstance(props)


  session.setDebug(true);

  def msg = new MimeMessage(session)


  msg.setText(m_text)
  msg.setSubject(m_subject)
  msg.setFrom(new InternetAddress(d_email))
  msg.addRecipient(Message.RecipientType.TO, new InternetAddress(m_to))

  Multipart multipart = new MimeMultipart()


  def messageBodyPart = new MimeBodyPart()
  DataSource source = new FileDataSource(path) ②
  messageBodyPart.setDataHandler(new DataHandler(source))
  messageBodyPart.setFileName(filename)
  multipart.addBodyPart(messageBodyPart)
  msg.setContent(multipart)
  Transport.send( msg )
  }
}

① Incluir la parámetro: path donde se encuentra nuestro fichero

② En esta sea crea el objeto DataSource.

| 249
101 Groovy Script

Script

@Grab('mysql:mysql-connector-java:5.1.6')①
@Grab('net.sourceforge.jexcelapi:jxl:2.6.12')
@GrabConfig(systemClassLoader=true)

import jxl.*
import jxl.write.*
import groovy.sql.Sql

filename = "informe.xls"

sql = Sql.newInstance( "jdbc:mysql://localhost:3306/origen?jdbcCompliantTruncation=false",


  "user",
  "password",
  "com.mysql.jdbc.Driver")

workbook = Workbook.createWorkbook(new File(filename))

sheet = workbook.createSheet("productos", 0)

first=true
i=0
sql.eachRow("select sku, description from products order by sku") { row -> ②

  if( first ){
  sheet.addCell (new Label (0,i,"Sku"))
  sheet.addCell ( new Label (1,i,"Product") ) ③
  first=false
  }

  sheet.addCell( new Label (0,i+1,"$l.sku") ) ④


  sheet.addCell ( new Label (1,i+1,l.description) )
  i++
}
workbook.write()
workbook.close()

250 |
101 Groovy Script

Memento en el TAE
Jorge Aguilera <jorge.aguilera@puravida-software.com [mailto:jorge.aguilera@puravida-
software.com]> 2019-09-19

En ete script vamos a hacer uso de la anotación @Memoized de Groovy, útil para
situaciones de cálculo recursivo, mediante un ejemplo práctico para calcular el TAE
(Tasa Anual Equivalente) de un préstamo.

No soy financiero por lo que algunas definiciones y/o explicaciones en


 este ámbito pueden no ajustarse exactamente a la definición formal.

TAE
La Tasa Anual Equivalente es una referencia orientativa del coste o
rendimiento efectivo anual de un producto financiero,
independientemente de su plazo. Su cáculo incluye la tasa de interés
nominal, los gastos, comisiones y permite comparar de una manera
homogénea el rendimiento de productos financieros diferentes

— Wikipedia

Como se puede observar de la definición, el cálculo de la TAE es orientativo. Es decir, no


existe una fórmula matemática exacta que sirva para calcularla. Así mismo tiene en
cuenta los costes en que se puede incurrir durante la inversión, por ello es un concepto
diferente al interés de una inversión pues este no tiene en cuenta los gastos.

Digamos que queremos pedir un préstamo de 1000 US$ y queremos devolverlo en 10


meses. Supongamos que este prestamo nos lo realiza un (buen) amigo y no nos pone
ningún interés, es decir, cada mes le devolveremos 10 US$ y transcurridos los 10 meses
habremos cancelado nuestra deuda, con un interés 0% y una TAE 0%

Volvemos a pedir este prestamo pero a un conocido que no nos quiere cobrar ningún
interés y que simplemente nos pide que le invitemos a unas cevezas que al final nos
cuesta 10 US$. El tipo de interés es 0% pero ahora el préstamo no nos sale gratis y vemos
que tiene una TAE de cerca de un 2.21%. Si realizas el ejercicio con una calculadora TAE
online verás que puede variar algo dependiendo de la "granularidad" que se aplique.

Básicamente el cálculo es un proceso iterativo que partiendo de un tipo de interés


determinado trata de buscar por aproximación el valor de la inversión. Si aplicando ese
interés, el valor de la inversión está por debajo del préstamo "real" lo subimos y si por el
contrario está por encima lo bajamos. Este proceso se repite el número de veces que se
quiera hasta hallar un interés que aproxime ambos valores.

int closeTo( float a , float b){ ①

| 251
101 Groovy Script

  float diff = a-b


  if( 0 == diff || Math.abs(diff) < 0.01){
  return 0
  }
  return a < b ? -1 : 1
}
float calculaValorInversionPlazo(float cuota, float interes, int plazo){
  counter++ ②
  cuota/ Math.pow( (1+interes), plazo)
}
float calculaValorInversion(float cuota, float interes, int plazos){
  (1..plazos).sum{n->
  calculaValorInversionPlazo(cuota, interes, n)
  }
}
float calculaTae( float prestamo, float comision, int plazos ){
  boolean forward=true
  float interes = 0.001
  float step = 0.001
  int deep=1

  float cuota = prestamo / plazos


  float objetivo = prestamo - comision

  float lastVac=valorInversion( cuota, interes, plazos) ③

  for(int iter=0; iter<ITERACIONES; iter++){

  int close = closeTo(lastVac, objetivo)

  if( close == 0) { ④
  float tae = ( Math.pow(1+interes, 12) -1 )*100
  return tae
  }

  // estamos por encima del objetivo, poco interes,


  if( close > 0 ){
  if( !forward ){
  deep = deep * 10
  forward = true
  }
  interes += (step/deep)
  }

  // estamos por debajo del objetivo, mucho interes


  if( close < 0 ){
  // si hay que retrodecer cuando ibamos para adelante hay que profundizar
  if( forward ){
  deep = deep * 10
  forward = false
  }
  interes -= (step/deep)
  }

252 |
101 Groovy Script

  lastVac=valorInversion( cuota, interes, plazos) ②


  }
  return 0
}

① Una forma simple de decidir si dos números son aproximados o uno es mayor que el
otro

② counter nos servirá para conocer el número de veces que se ejecuta la función. No
forma parte del cálculo de la TAE

③ invocaremos a esta función numerosas veces con parámetros diferentes

④ hemos encontrado un tipo de interés aproximado para la inversión

Como ayuda para testear el código tenemos una función ajena al cálculo del TAE:

void assertTae( float prestamo, float comision, int plazos, float expected){ ②
  float tae=calculaTae(prestamo,comision,plazos)
  println "tae encontrada $tae%"
  if( closeTo(tae, expected) != 0 ){
  throw new RuntimeException(
  String.format("TAE de %f eur a %d meses y apertura %f debia ser %f pero dice %f",
  prestamo, plazos, comision, expected, tae))
  }
}

① utilidad para comprobar si una tae cumple con un valor esperado

Así pues nuestro script puede servir para dado un importe, unos gastos de comisión y un
número de meses para devolverlo calcular la TAE simplemente ejecutando
calculaTae(prestamo,comision,plazos)

Stress
Como hemos visto, buscar la TAE es un proceso intenso en cálculo y con la particularidad
de que para una búsqueda pocas veces se llama a una misma función con los parámetros
valiendo lo mismo pues por ejemplo el tipo de interés va cambiando en cada iteración. Sin
embargo podemos imaginar situaciones donde este cálculo se tenga que realizar para
cientos de clientes donde las condiciones del préstamos sean las mismas (importe, plazo,
etc) y en las que nos interese optimizar lo máximo la búsqueda.

Para simular estas situaciones el script cuenta con una función de stress donde se van a
simular una serie de préstamos (6 casos diferentes) un número elevado de veces (10.000
veces cada una). Para evaluar el rendimiento, esta función devolverá el tiempo
transcurrido desde el inicio hasta completar todas las simulaciones.

Así mismo como se explicó al inicio dispondremos de un contador global que iremos
incrementando cada vez que se llame a la función calculaValorInversionPlazo para
determinar el número de veces que se ha ejecutado la misma

| 253
101 Groovy Script

def runExamples() {
  examples = [
  [1000, 10, 0, 0],
  [1000, 10, 10, 2.21],
  [500, 10, 10, 4.52],
  [500, 20, 10, 9.37],
  [647, 10, 10, 3.47],
  [10000, 100, 24, 0.97],
  ]

  Date start = new Date()


  (0..10000).each {
  examples.each { List item ->
  assertTae(item[0], item[1], item[2], item[3])
  }
  }
  Date end = new Date()
  end-start
}

Una vez ejecutadas dichas pruebas en mi máquina (i7, 16Gb memoria) tendremos un
resultado similar a:

pasamos por counter 15221522 veces. Tardó 35.964 seconds

Memoize
Como podemos observar en la prueba de stress, algunas funciones son ejecutadas
repetidas veces y en muchos casos el valor de los parámetros han sido los mismos. Si
pudieramos disponer de un cache donde asociar los parámetros con el resultado
podríamos optimizar nuestro código evitando ejecutar el mismo caso repetidas veces.

En Groovy esto se consigue simplemente anotando el método con @Memoize. Así pues
simplemente tenemos que añadir esta anotación a aquellas funciones que se vayan a
ejecutar múltiples veces con los mismos parámetros.

Para poder comparar las dos alternativas en un mismo script vamos a


"duplicar" los métodos anotando uno de ellos con @Memoize y el otro no.
 El método anotado como @Memoize simplemente llamará al método
visto anteriormente.

Así mismo vamos a usar una variable memoize que indique qué versión
del método queremos ejecutar, de tal forma que cuando esta variable
 valga false el algoritmo invocará a la función no anotada con @Memoize
y a la inversa

254 |
101 Groovy Script

@Memoized
int closeTo( float a , float b){ ①
  float diff = a-b
  if( 0 == diff || Math.abs(diff) < 0.01){
  return 0
  }
  return a < b ? -1 : 1
}
@Memoized
float calculaValorInversionPlazoMemoized(float cuota, float interes, int plazo){
  calculaValorInversionPlazo(cuota, interes, plazo)
}
@Memoized
float calculaValorInversionMemoized(float cuota, float interes, int plazos){
  calculaValorInversion(cuota, interes, plazos)
}

① Una forma simple de decidir si dos números son aproximados o uno es mayor que el
otro

② counter nos servirá para conocer el número de veces que se ejecuta la función. No
forma parte del cálculo de la TAE

En las mismas condiciones repetimos los test de stress y obtenemos:

pasamos por counter 1482 veces. Tardó 10.044 seconds

Comparativa
Versión Llamadas a función Tiempo ejecución

No memoize 15221522 36seg

Memoize 1482 10seg

| 255
101 Groovy Script

TweetReport (Persistencia con SQLite)


Jorge Aguilera <jorge.aguilera@puravida-software.com [mailto:jorge.aguilera@puravida-
software.com]> 2017-12-09

En este post vamos a analizar un caso de negocio que seguramente no ocurre muy a
menudo pero que espero nos ayude a resolver situaciones parecidas. En este caso vamos a
desarrollar un script que se podrá ejecutar repetidas veces a lo largo del tiempo (cada
hora, varias veces al día, etc) cuya lógica de negocio queremos que se mantenga durante
la ejecución de las mismas.

Nuestro Community Manager siente curiosidad por ciertos usuarios de Twitter que nos
siguen en nuestra cuenta oficial y quiere hacer un pequeño estudio sobre los mismos que
va a durar un cierto tiempo (dias/semanas/meses). Durante este tiempo nos irá
proporcionando usuarios de Twitter de los que tendremos que obtener ciertos datos
públicos e ir guardándolos. Así mismo durante este período nos irá pidiendo que le
hagamos un pequeño informe con los datos guardados.

En un escenario convencional, podríamos optar por escribir en un fichero plano la


información de cada ejecución o si queremos algo más robusto instalaríamos un motor de
base de datos tipo MySQL, Postgre, SQLServer, etc. Mientras que lo primero es muy simple
de mantener resulta muy complejo de gestionar/programar. Por el contrario la segunda
opción es mucho más robusta pero más compleja.

A medio de camino de ambas soluciones contamos con proyectos como SQLite que nos
permitirán crear y acceder a los datos mediante un acceso JDBC (base de datos) pero sin
las complejidades de tener que instalar y mantener un motor de datos más completo.

Dependencias
Para este scritp necesitaremos acceder a Twitter y a una base de datos SQLite por lo que
definimos las dependencias:

@GrabConfig(systemClassLoader=true)
@Grab(group='org.twitter4j', module='twitter4j-core', version='4.0.6')
@Grab(group='org.xerial', module='sqlite-jdbc', version='3.21.0')
import groovy.sql.Sql
import twitter4j.Twitter;
import twitter4j.TwitterFactory;
import twitter4j.StatusUpdate

Argumentos
El script admitirá la siguiente lista de argumentos:

256 |
101 Groovy Script

• --initialize, recrea la base de datos

• --username, busca un usuario proporcionado por parámetro y lo incluye en la base de


datos

• --all, recorre todos los usuarios existentes en la base de datos y los actualiza

• --report, genera un informe con los datos guardados hasta la fecha

def cli = new CliBuilder(usage: '-i -u username -r')


cli.with {
  h(longOpt: 'help', args:0,'Usage Information', required: false)
  i(longOpt: 'initialize',args:0,'Borrar todos los datos y crear la base de datos en limpio', required:
false)
  u(longOpt: 'username', args:1, argName:'username', 'El usuario a investigar', required: false)
  a(longOpt: 'all',args:0,'Actualiza la info de todos los usuarios registrados', required: false)
  r(longOpt: 'report',args:0,'Genera un report con los usuarios existentes', required: false)
}
def options = cli.parse(args)
if (options.h ) {
  cli.usage()
  return
}

Modelo
Para ayudar en la encapsulación de los datos definimos una clase TwitterUser la cual se
auto-actualiza si usamos el constructor que proporciona un id de Twitter:

class TwitterUser{
  String id
  String name
  int tweets
  int followers
  int friends
  String timeZone

  TwitterUser(){
  }

  TwitterUser( String id){


  def tuser = TwitterFactory.singleton.users().showUser(id)
  this.id=id
  this.name = tuser.name
  this.tweets = tuser.statusesCount
  this.followers = tuser.followersCount
  this.friends = tuser.friendsCount
  this.timeZone = tuser.timeZone
  }
}

| 257
101 Groovy Script

Prepare
Antes de realizar ninguna acción contra la base de datos el propio script realiza una
preparación de la misma. Si se indicó el parámetro --initialize realizará un borrado de la
base de datos. En cualquier caso creará la tabla si no existe

void prepareDatabase(boolean drop) {


  Sql sql = Sql.newInstance("jdbc:sqlite:tweetreport.db")
  if( drop )
  sql.execute "drop table if exists tweetreport"
  String sentence = """CREATE table if not EXISTS tweetreport
  (id varchar(20), name VARCHAR(40), tweets NUMBER(5), followers NUMBER(5), friends number(5), timezone
varchar(10))
  """
  sql.execute sentence
}

Update User
Si se indica un usuario, el script creará un objeto de la clase TwitterUser y mediante una
simple select por su id determinará si existe ya. Si existe se procederá a actualizar el
registro mientras que si no existe se insertará un registro nuevo

void updateUser(TwitterUser user){


  Sql sql = Sql.newInstance("jdbc:sqlite:tweetreport.db")
  def row = sql.firstRow("select * from tweetreport where id=?",[user.id])
  String sentence = row ?
  "update tweetreport set name=?.name, tweets=?.tweets, followers=?.followers, friends=?.friends,
timezone=?.timeZone where id=?.id"
  :
  "insert into tweetreport (id,name,tweets,followers,friends,timezone) values
(?.id,?.name,?.tweets,?.followers,?.friends,?.timeZone)"
  sql.execute sentence, user
}

Mediante la opción --all hacemos que el script recorra todos los usuarios guardados hasta
la fecha e invoque el método upate para cada uno de ellos

Report User
En cualquier momento podemos solicitar que el script genere un informe con el estado de
la base de datos, lo cual en nuestro ejemplo consistirá en recorrer los registros y
mostrarlos por consola:

258 |
101 Groovy Script

void report(){
  Sql sql = Sql.newInstance("jdbc:sqlite:tweetreport.db")
  sql.eachRow"select * from tweetreport order by id",{ row->
  println row
  }
}

Persistencia
Al ejecutar el script se creará un fichero tweetreport.db (indicado en la cadena de
conexión Sql) que SQLite utilizará para ofrecer el servicio de persistencia.

De esta forma simple podremos dotar a nuestros scritps de la capacidad de tener una base
de datos sencilla y sin complejidades de instalación ni administración

| 259
101 Groovy Script

Script

//tag::dependencies[]
@GrabConfig(systemClassLoader=true)
@Grab(group='org.twitter4j', module='twitter4j-core', version='4.0.6')
@Grab(group='org.xerial', module='sqlite-jdbc', version='3.21.0')
import groovy.sql.Sql
import twitter4j.Twitter;
import twitter4j.TwitterFactory;
import twitter4j.StatusUpdate
//end::dependencies[]

//tag::model[]
class TwitterUser{
  String id
  String name
  int tweets
  int followers
  int friends
  String timeZone

  TwitterUser(){
  }

  TwitterUser( String id){


  def tuser = TwitterFactory.singleton.users().showUser(id)
  this.id=id
  this.name = tuser.name
  this.tweets = tuser.statusesCount
  this.followers = tuser.followersCount
  this.friends = tuser.friendsCount
  this.timeZone = tuser.timeZone
  }
}
//end::model[]

//tag::prepare[]
void prepareDatabase(boolean drop) {
  Sql sql = Sql.newInstance("jdbc:sqlite:tweetreport.db")
  if( drop )
  sql.execute "drop table if exists tweetreport"
  String sentence = """CREATE table if not EXISTS tweetreport
  (id varchar(20), name VARCHAR(40), tweets NUMBER(5), followers NUMBER(5), friends number(5), timezone
varchar(10))
  """
  sql.execute sentence
}
//end::prepare[]

//tag::update[]
void updateUser(TwitterUser user){
  Sql sql = Sql.newInstance("jdbc:sqlite:tweetreport.db")
  def row = sql.firstRow("select * from tweetreport where id=?",[user.id])
  String sentence = row ?
  "update tweetreport set name=?.name, tweets=?.tweets, followers=?.followers, friends=?.friends,
timezone=?.timeZone where id=?.id"
  :
  "insert into tweetreport (id,name,tweets,followers,friends,timezone) values
(?.id,?.name,?.tweets,?.followers,?.friends,?.timeZone)"
  sql.execute sentence, user
}
//end::update[]

List listUsers(){

260 |
101 Groovy Script

  Sql sql = Sql.newInstance("jdbc:sqlite:tweetreport.db")


  sql.rows("select id from tweetreport")*.id
}

//tag::report[]
void report(){
  Sql sql = Sql.newInstance("jdbc:sqlite:tweetreport.db")
  sql.eachRow"select * from tweetreport order by id",{ row->
  println row
  }
}
//end::report[]

//tag::cli[]
def cli = new CliBuilder(usage: '-i -u username -r')
cli.with {
  h(longOpt: 'help', args:0,'Usage Information', required: false)
  i(longOpt: 'initialize',args:0,'Borrar todos los datos y crear la base de datos en limpio', required:
false)
  u(longOpt: 'username', args:1, argName:'username', 'El usuario a investigar', required: false)
  a(longOpt: 'all',args:0,'Actualiza la info de todos los usuarios registrados', required: false)
  r(longOpt: 'report',args:0,'Genera un report con los usuarios existentes', required: false)
}
def options = cli.parse(args)
if (options.h ) {
  cli.usage()
  return
}
//end::cli[]

prepareDatabase(options.i )

if( options.username ) {
  TwitterUser user = new TwitterUser(options.username)
  updateUser(user)
}
if( options.all ){
  listUsers().each{ id->
  println "update $id"
  TwitterUser user = new TwitterUser(id)
  updateUser(user)
  }
}
if( options.report ){
  report()
}

| 261
101 Groovy Script

Buscando claves ssh en BitBucket


Jorge Aguilera <jorge.aguilera@puravida-software.com [mailto:jorge.aguilera@puravida-
software.com]> 2018-03-09

Seguramente este caso de uso no será muy común pero espero que te aporte ideas si
trabajas con BitBucket

Hace unos días en B2Boost [http://www.b2boost.eu] estabamos intentando executar un


pipeline de integración contínua para un proyecto que dependía de otros y necesitaba
clonarlos primeramente para construir el producto.

Sin embargo surgió un problema cuando nos dimos cuenta que necesitabamos configurar
todos los proyectos involucrados y añadirle una clave ssh común para poder clonarlos. En
un escenario ideal esto hubiera sido tan simple como ubicar esta clave a nivel de
Organización y todos los proyectos que dependen de ella quedarían configurados a su vez.
Pero BitBucket no permite usar una misma clave ssh en dos proyectos y/o organizaciones
a la vez y por razones históricas nosotros la estabamos usando en algunos repositorios. El
problema era que no sabíamos en cuales

Así pues, teníamos dos opciones:

• generar una nueva clave ssh desde nuestro sistema de CI y añadirla a nivel de
organización

• buscar qué proyectos usaban la clave y eliminarla

La primera opción probablemente era la más rápida pero se nos quedarían repositorios
con claves sin usar y además corríamos el riesgo de que otros proyectos se vieran
afectados.

La segunda opción era fácil y limpia pero tediosa porque conllevaba navegar entre los
repos buscando la clave en los settings de cada uno … y eran más de 45

Rest al rescate
BitBucket tiene un interface REST (https://developer.atlassian.com/server/bitbucket/how-
tos/command-line-rest/) con dos versiones (/1.0 and /2.0) con las que puedes manejar tus
repositorios tras haber obtenido un token de autentificación.

Si consultas el manual verás que puedes usar herramientas de línea como curl pero
implementar toda la lógica que necesitamos en un bash puede convertirlo en dificil de
entender, depurar y reutilizar. Pero con Groovy y HttpBuilder-ng lo puedes hacer en un
script de menos de 50 lineas

262 |
101 Groovy Script

OAuth Consumer
En primer lugar necesitas crear un Oauth Consumer en tu cuenta de BitBucket (Profile,
Settings, Oauth)

Después de crearlo obtendrás un Key y un Secret que se lo proporcionaremos al script vía


argumentos de línea junto con el nombre de nuestra organización

Dependencias
Sólo vamos a necesitar http-builder y slf4j:

@Grab(group='io.github.http-builder-ng', module='http-builder-ng-apache', version='1.0.3')


@Grab(group='ch.qos.logback', module='logback-classic', version='1.2.3')
import groovyx.net.http.*
import static groovyx.net.http.HttpBuilder.configure
import static groovyx.net.http.ContentTypes.JSON
import static groovy.json.JsonOutput.prettyPrint
import static groovy.json.JsonOutput.toJson

Autorización
Esta parte fue la más difícul, no por sus detalles técnicos sino porque el mecanismo de
autentificación usado por HttpBuilder-NG no es compatible 100% con BitBucket.

HttpBulder-Ng tiene el método basic in the auth para enviar la cabecera Authorization:
Basic xxxx pero sólo lo envía si el servidor lo requiere y BitBucket lo requiere en la
primera petición.

La solución pasa por construir nosotros la cabecera de forma manual codificando el


usuario y la password en base64 (donde usuario y password son el Key`y el `Secret). Así
mismo tenemos que enviar la petición en formato form porque no hemos conseguido que
BitBucket reconozca esta petición como JSON

| 263
101 Groovy Script

organization = args[0] // organization


username = args[1] //secretId
password = args[2] //token

creds = "$username:$password".bytes.encodeBase64()

def http = configure {


  request.uri = 'https://bitbucket.org/'
  request.headers['Authorization'] = "Basic $creds"
  request.contentType = JSON[0]
}

def token = http.post{


  request.uri.path='/site/oauth2/access_token'
  request.body=[grant_type:'client_credentials'] ①
  request.contentType = 'application/x-www-form-urlencoded'
  request.encoder 'application/x-www-form-urlencoded', NativeHandlers.Encoders.&form
}.access_token

① send the grant_type as form because BitBucket doesn’t reconigze JSON in this request

BitBucket
Una vez obtenido un token podemos configurar un objecto bitbucket que usaremos a lo
largo del script con los parámetros comunes a todas las peticiones:

def bitbucket = configure {


  request.uri = 'https://api.bitbucket.org/'
  request.headers['Authorization'] = "Bearer $token"
  request.accept=['application/json']
  request.contentType = JSON[0]
}

Listar repositorios
Podemos obtener los detalles de todos nuestros repos usando una petición paginada. Cada
repo obtenido contiene gran cantidad de información de la que nosotros vamos a usar
únicamente 'slug' como identificador del repo

264 |
101 Groovy Script

def page=1
def repos=[]
while( true ) {
  def list = bitbucket.get{
  request.uri.path="/2.0/repositories/$organization"
  request.uri.query=[page:page]
  }
  repos.addAll list.values
  if( repos.size() >= list.size )
  break
  page++
}

Inspeccionar un repo

def keys=[:] ①
repos.findAll{it.slug}.each{ repo->
  def repokeys = bitbucket.get{
  request.uri.path="/1.0/repositories/$organization/${repo.slug}/deploy-keys"
  }
  keys[repo.slug]=repokeys
}

① construimos un mapa con valores tipo repo-name:json

Report
Una vez obtenidos todos los repos iteraremos por todos ellos haciendo una petición en
busca de las claves ssh que tiene y si alguna es el ID que buscamos

println "Total repos founded : ${repos.size()}"


println "Total keys founded : ${keys.size()}"
println prettyPrint(toJson(keys))

def remove = keys.findAll{ it.value.find{ it.key.indexOf('XXXXXXXXXXXXXXXXXXX')!=-1 } } ①


println "Remove: ${remove*.key}"

① filtrar claves que contienen nuestro ID

Conclusion
Tras ejecutar el script encontramos que sólo unos pocos repos estaban configurados con
esta clave ssh pero sin el script probablemente hubieramos perdido una cantidad de
tiempo importante, navegando por todos ellos corriendo el riesgo de olvidar alguno lo que
nos obligaría a volver a comenzar

| 265
101 Groovy Script

Script

//tag::dependencies[]
@Grab(group='io.github.http-builder-ng', module='http-builder-ng-apache', version='1.0.3')
@Grab(group='ch.qos.logback', module='logback-classic', version='1.2.3')
import groovyx.net.http.*
import static groovyx.net.http.HttpBuilder.configure
import static groovyx.net.http.ContentTypes.JSON
import static groovy.json.JsonOutput.prettyPrint
import static groovy.json.JsonOutput.toJson
//end::dependencies[]

//tag::login[]
organization = args[0] // organization
username = args[1] //secretId
password = args[2] //token

creds = "$username:$password".bytes.encodeBase64()

def http = configure {


  request.uri = 'https://bitbucket.org/'
  request.headers['Authorization'] = "Basic $creds"
  request.contentType = JSON[0]
}

def token = http.post{


  request.uri.path='/site/oauth2/access_token'
  request.body=[grant_type:'client_credentials'] ①
  request.contentType = 'application/x-www-form-urlencoded'
  request.encoder 'application/x-www-form-urlencoded', NativeHandlers.Encoders.&form
}.access_token
//end::login[]

//tag::bitbucket[]
def bitbucket = configure {
  request.uri = 'https://api.bitbucket.org/'
  request.headers['Authorization'] = "Bearer $token"
  request.accept=['application/json']
  request.contentType = JSON[0]
}
//end::bitbucket[]

//tag::repos[]
def page=1
def repos=[]
while( true ) {
  def list = bitbucket.get{
  request.uri.path="/2.0/repositories/$organization"
  request.uri.query=[page:page]
  }
  repos.addAll list.values
  if( repos.size() >= list.size )
  break
  page++
}

266 |
101 Groovy Script

//end::repos[]

//tag::v10[]
def keys=[:] ①
repos.findAll{it.slug}.each{ repo->
  def repokeys = bitbucket.get{
  request.uri.path="/1.0/repositories/$organization/${repo.slug}/deploy-keys"
  }
  keys[repo.slug]=repokeys
}
//end::v10[]

//tag::report[]
println "Total repos founded : ${repos.size()}"
println "Total keys founded : ${keys.size()}"
println prettyPrint(toJson(keys))

def remove = keys.findAll{ it.value.find{ it.key.indexOf('XXXXXXXXXXXXXXXXXXX')!=-1 } } ①


println "Remove: ${remove*.key}"
//end::report[]

| 267
101 Groovy Script

Valida tu NIF o SOAP hecho fácil


Jorge Aguilera <jorge.aguilera@puravida-software.com [mailto:jorge.aguilera@puravida-
software.com]> 2017-10-26

Qué es SOAP

SOAP (originalmente las siglas de Simple Object Access Protocol) es


un protocolo estándar que define cómo dos objetos en diferentes
procesos pueden comunicarse por medio de intercambio de datos
XML. Este protocolo deriva de un protocolo creado por Dave Winer en
1998, llamado XML-RPC. SOAP fue creado por Microsoft, IBM y otros.
Está actualmente bajo el auspicio de la W3C. Es uno de los protocolos
utilizados en los servicios Web.

— Wikipedia, https://es.wikipedia.org/wiki/Simple_Object_Access_Protocol

Si como yo, siempre has creido que la S no era de Simple sino de Sufre, y
 la P de Pánico, este es tu post.

En este script vamos a explicar como consumir un servicio SOAP ofrecido por la Agencia
Tributaria Española para consultas de calidad de datos identificativos, es decir, validar si
un NIF corresponde a la persona que dice ser. Mediante este WebServices podemos enviar
una lista de NIFs junto con el nombre del titular y la respuesta nos indicará para cada uno
de ellos si según sus registros existe una correspondencia total, parcial o no existe. De esta
forma podemos mejorar la calidad de los datos de nuestros clientes por ejemplo.

Para completar el script haremos que la lista de NIFs/Nombres a consultar se tome de una
base de datos que será actualizada con la información de respuesta. Así pues deberemos
contar con una tabla nifes con la siguiente estructura:

Field Type Description

nif varchar(10) NIF proporcionado por el


usuario

nombre varchar(200) Apellido y nombre


proporcionado por el
usuario

estado varchar(10) INCORRECTO /


IDENTIFICADO /
PARCIALMENTE-
IDENTIFICADO

nombre_aeat varchar(200) Apellidos y Nombre


proporcionado por AEAT

268 |
101 Groovy Script

Así pues el script primeramente creará una petición con los NIF/Nombres en estado
Incorrecto (o nulo) y parseará la respuesta actualizándolos con los datos obtenidos de la
AEAT

SOAP y Java
Si nunca has tenido que consumir un servicio SOAP puedes echarle un ojo a este artículo
https://docs.oracle.com/javaee/5/tutorial/doc/bnayn.html de Oracle donde en apariencia no
es tan difícil de hacer, sobre todo gracias a las anotaciones que existen hoy en día.

Básicamente SOAP es una forma de consumir un servicio remoto vía HTTP a través de
intercambios de mensajes XML (y qué mensajes!!) Al ser HTTP no hace falta exponer
puertos especiales en nuestros sistemas (o si los abrimos podremos tratarlos como
cualquier puerto que atienda este protocolo), así como aprovechar todo el stack de
seguridad como puede ser el uso de SSL.

Por una parte diseñas el interface a exponer junto con sus parámetros tanto de entrada
como de salida y con la ayuda del veneno que prefieras (Axis, Spring, CXF, …) generas un
punto de entrada al mismo.

Con ayuda de tu veneno expones también el WSDL que es lo que necesitará cualquier
programa cliente que quiera consumirlo (como puedes imaginar, el XML no está pensado
para los ojos humanos, al menos no para los mios) usando a su vez su propio veneno

wsimport y wsdl2java son sólo algunos de los venenos que puedes elegir para ayudarte a
generar la parte cliente y que de forma genérica te solicitarán una URL donde resida el
WSDL (suele ser un servidor web o también una ruta a un fichero si te lo has descargado).
Con esta definición tu veneno será capaz de crearte una clases "esqueleto" para que las
incluyas en tu proyecto y puedas así invocar al servicio de forma transparente.

SOAP y Groovy
Groovy cuenta con el proyecto groovy-wslite [https://github.com/jwagenleitner/groovy-wslite] el
cual hace realmente simple el consumir un servicio SOAP.

Como avisa el README del proyecto, esta librería asume que conoces el
servicio que vas a consumir. Es decir, necesitas saber el "nombre" del
 método que quieres ejecutar así como sus parámetros. En mi caso esto ha
ocurrido en el 99.9999% de las veces, independiente de que un veneno
me generara el stub

Mediante esta librería tendremos control absoluto tanto de la Request como de la


Response, así como de todos los atributos que se necesiten enviar. Incluso en el peor de
los casos, puedes construirte el XML mediante un String y enviarlo directamente.

La idea principal es que modelaremos nuestro intercambio mediante closures y la


librería generará los mensajes al vuelo, sin necesidad de una fase inicial de

| 269
101 Groovy Script

convertir el WSDL a código:

def client = new SOAPClient('http://www.holidaywebservice.com/Holidays/US/Dates/USHolidayDates.asmx') ①


def response = client.send() {
  body {
  GetMothersDay('xmlns':'http://www.27seconds.com/Holidays/US/Dates/') { ②
  year(2011) ③
  }
  }
}

① Construimos un cliente SOAP indicando la ruta al servicio

② Invocamos la acción GetMothersDay pudiendo incluso cualificarla con su namespace

③ Pasamos un parámetro que nos pide el método llamado year

Así mismo la respuesta la podemos analizar sin necesidad de ningún stub:

println "${response.GetMothersDayResponse?.GetMothersDayResult}" ①

① Vamos "navegando" por la estructura retornada

Consulta de NIF válido según AEAT


Saber cuándo es el día de la madre de un año cualquiera está muy bien pero con poca
utilidad, así que vamos a desarrollar un pequeño script que nos valide si un NIF es de la
persona que dice ser. Para ello la Agencia Tributaria ofrece un servicio SOAP de consulta
(hasta 10K NIF en una sóla petición!!) donde nos confirma si un NIF corresponde con un
nombre en base a sus registros.

Puedes encontrar la definición de este servicio en Información sobre Web Services de


Calidad de Datos Identificativos
[http://www.agenciatributaria.es/AEAT.internet/Inicio/Ayuda/Manuales__Folletos_y_Videos/Manuales_tec
nicos/Web_service/Modelos_030__036__037/Informacion_sobre_Web_Services_de_Calidad_de_Datos_Identi
ficativos/Informacion_sobre_Web_Services_de_Calidad_de_Datos_Identificativos.shtml]

Para poder usar este servicio necesitarás un certificado digital que te


identifique. Puede ser de persona física, empleado público, FNMT o
 empresas. Según entiendo se requiere simplemente para evitar el abuso
del servicio

A continuación un ejemplo de consulta y su respuesta extraidas del documento

270 |
101 Groovy Script

Ejemplo de consulta

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:vnif="http://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/burt/j
dit
/ws/VNifV2Ent.xsd"> ①
<soapenv:Header/>
<soapenv:Body>
<vnif:VNifV2Ent> ②
<vnif:Contribuyente> ③
<vnif:Nif>99999999R</vnif:Nif> ④
<vnif:Nombre>ESPAÑOL ESPAÑOL JUAN</vnif:Nombre>
</vnif:Contribuyente>
</vnif:VNifV2Ent>
</soapenv:Body>
</soapenv:Envelope>

① Schema "vnif" que hay que utilizar.

② Usando el schema "vnif" identificamos la funcion a ejecutar

③ Usando el schema "vnif" describimos una estructura de entrada

④ Usando el schema "vnif" indicamos un parametro

Ejemplo de respuesta

<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
 <env:Body>
 <VNifV2Sal:VNifV2Sal
xmlns:VNifV2Sal="http://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/b
urt/jdit/ws/VNifV2Sal.xsd"> ①
 <VNifV2Sal:Contribuyente> ②
 <VNifV2Sal:Nif>99999999R</VNifV2Sal:Nif>
 <VNifV2Sal:Nombre>ESPAÑOL ESPAÑOL JUAN</VNifV2Sal:Nombre>
 <VNifV2Sal:Resultado>Identificado</VNifV2Sal:Resultado>
 </VNifV2Sal:Contribuyente>
 </VNifV2Sal:VNifV2Sal>
 </env:Body>
</env:Envelope>

① Schema "VNIFV2Sal" a utilizar

② Estructura de respuesta

Consultar un NIF
Para invocar ese servicio un script simple sería:

| 271
101 Groovy Script

@Grab('com.github.groovy-wslite:groovy-wslite:1.1.2')
def client = new SOAPClient('https://www1.agenciatributaria.gob.es/wlpl/BURT-JDIT/ws/VNifV2SOAP')
def response = client.send() {
  envelopeAttributes([
  "xmlns:vnif":
"http://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/burt/jdit/ws/VNif
V2Ent.xsd"
  ])
  body {
  'vnif:VNifV2Ent' {
  'vnif:Contribuyente'{
  'vnif:Nif'(0123456789X')
  'vnif:Nombre'('APELLIDO APELLIDO NOMBRE')
  }
  }
  }
}
println "${response.VNifV2Sal.Contribuyente.Resultado}"

Consultar hasta 10K NIFs


Partiendo del ejemplo anterior donde consultamos un NIF vamos a desarrollar un script
más complejo donde la petición se construye dinámicamente en función de los valores de
la base de datos. Así mismo trataremos una respuesta iterando por cada elemento de
interés.

dependencias

@Grab('com.github.groovy-wslite:groovy-wslite:1.1.2')
@Grab('mysql:mysql-connector-java:5.1.6')
@GrabConfig(systemClassLoader=true)

crear SOAP Client

  def client = new SOAPClient('https://www1.agenciatributaria.gob.es/wlpl/BURT-JDIT/ws/VNifV2SOAP') ①

① Proporcionamos directamente la URL al servicio

crear cabecera

  def response = client.send() {


  envelopeAttributes([
  "xmlns:vnif":
"http://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/burt/jdit/ws/VNif
V2Ent.xsd" ①
  ])

① Podemos ajustar la petición, incluyendo namespaces por ejemplo

272 |
101 Groovy Script

body dinámico y con un bucle

  body {

  sql.eachRow("select * from nifes where estado is null or estado <> 'IDENTIFICADO' "){ row-> ①
  'vnif:VNifV2Ent' { ②
  'vnif:Contribuyente'{ ③
  'vnif:Nif'(row.nif)
  'vnif:Nombre'(row.nombre.toUpperCase())
  }
  }
  }

① Recorremos la tabla y construimos elementos dinámicamente

② Cualificamos la función según el schema que corresponda si es necesario

③ Cualificamos los parametros según el schema que corresponda si es necesario

tratamiento respuesta dinámico

  sql.withBatch( batchSize, "update people set nombre_aeat=?,estado=? where nif=?"){ ps->


  response.VNifV2Sal.Contribuyente.each{ result -> ①
  ps.addBatch([
  "$result.Nombre".toString(), ②
  "$result.Resultado".toString(),
  "$result.Nif".toString()
  ])
  erroneos+= "$result.Resultado".equals('IDENTIFICADO') ? 0 : 1
  }
  }

① response.VNifV2Sal.Contribuyente nos devuelve un array de elementos Contribuyente

② Obtenemos el valor de un elemento mediante .text() o con .toString()

excepciones

} catch (SOAPFaultException sfe) { ①


  println sfe.request?.contentAsString
  println sfe.request.contentAsString
} catch (SOAPClientException sce) { ②
  // This indicates an error with underlying HTTP Client (i.e., 404 Not Found)
  println sce.request?.contentAsString
  println sce.response?.contentAsString
}

① Excepción de "negocio"

② Excepción de "transporte"

| 273
101 Groovy Script

Ejecución y certificado
Como ya se ha comentado el servicio de la AEAT requiere que la conexión sea realizada
con un certificado por parte del cliente que sirva para identificarle. La forma más común
es tener este certificado en un fichero .p12 protegido con contraseña de tal forma que al
ejecutar el script podamos indicarle a Groovy (en realidad a Java) donde encontrarlo.

Así pues la ejecución del script sería algo parecida a:

groovy -Djavax.net.ssl.keyStore=./test.p12 -Djavax.net.ssl.keyStorePassword=LAPWD


-Djavax.net.ssl.keyStoreTYpe=PKCS12 ConsultaNif.groovy USER PWD ①

① LAPWD correspondería a la password para abrir el keystore mientras que USER Y


PWD corresponderían al usuario de la base de datos

274 |
101 Groovy Script

Script

//tag::dependencies[]
@Grab('com.github.groovy-wslite:groovy-wslite:1.1.2')
@Grab('mysql:mysql-connector-java:5.1.6')
@GrabConfig(systemClassLoader=true)
//end::dependencies[]

import groovy.sql.Sql
import wslite.soap.*
import groovy.xml.*

try{
  sql=Sql.newInstance("jdbc:mysql://localhost/mydatabase",args[0], args[1], "com.mysql.jdbc.Driver")

  //tag::cliente[]
  def client = new SOAPClient('https://www1.agenciatributaria.gob.es/wlpl/BURT-JDIT/ws/VNifV2SOAP') ①
  //end::cliente[]

  //tag::cabecera[]
  def response = client.send() {
  envelopeAttributes([
  "xmlns:vnif":
"http://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/burt/jdit/ws/VNif
V2Ent.xsd" ①
  ])
  //end::cabecera[]
  //tag::body[]
  body {

  sql.eachRow("select * from nifes where estado is null or estado <> 'IDENTIFICADO' "){ row-> ①
  'vnif:VNifV2Ent' { ②
  'vnif:Contribuyente'{ ③
  'vnif:Nif'(row.nif)
  'vnif:Nombre'(row.nombre.toUpperCase())
  }
  }
  }
  //end::body[]
  }
  }

  batchSize=20
  erroneos=0

  //tag::respuesta[]
  sql.withBatch( batchSize, "update people set nombre_aeat=?,estado=? where nif=?"){ ps->
  response.VNifV2Sal.Contribuyente.each{ result -> ①
  ps.addBatch([
  "$result.Nombre".toString(), ②
  "$result.Resultado".toString(),
  "$result.Nif".toString()
  ])
  erroneos+= "$result.Resultado".equals('IDENTIFICADO') ? 0 : 1
  }
  }
  //end::respuesta[]
  println "Hay $erroneos no identificados o identificados parcialmente"

  //tag::exceptions[]
} catch (SOAPFaultException sfe) { ①
  println sfe.request?.contentAsString
  println sfe.request.contentAsString

| 275
101 Groovy Script

} catch (SOAPClientException sce) { ②


  // This indicates an error with underlying HTTP Client (i.e., 404 Not Found)
  println sce.request?.contentAsString
  println sce.response?.contentAsString
}
  //end::exceptions[]

276 |

También podría gustarte