Documentos de Académico
Documentos de Profesional
Documentos de Cultura
2019-04-09
Table of Contents
Introducción. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
Consideraciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
Organización . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
Localhost . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
Entorno de trabajo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
Organización . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
Carga de información . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
PDF. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
Json a Pdf . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
Script . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
Publicación y Schedule . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
Preparación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
Parsear el PowerPoint. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
RevealJS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
Conceptos básicos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
Declaración de tipos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
String vs GString. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
Closure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
Bucles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
CliBuilder . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
Ejecutar comandos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
Comando simple . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
Esperar finalización . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
WithBatch . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
De Properties a YML. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
Segundo intento . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
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
Windows. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
Maven (y similares) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
SdkMan . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
Comprobación. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
GroovyConsole . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
Consola . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
Dependencias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64
Domain . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64
Configuración . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
Añadir Libros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
Consultar la biblioteca . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
Test. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
GroovyShell . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
Hello Docker . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
Montando volumen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
Consumir JSON . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
Ejecución programada . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
Gitlab . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
Planificación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
Resultado . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
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
Preparación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
Actualización de versión.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
Tostando imágenes. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
Dockerfile . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
Generando la imagen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
Ejecutando la imagen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
Dependencias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102
Argumentos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102
Resultado . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115
Groogle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125
Personalización . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126
Business . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126
Vista. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127
Groogle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 132
Dependencias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 132
Autorización . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133
Database . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133
Argumentos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 134
Dependencias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142
shareFile. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 146
Groogle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147
Presentación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148
Ejemplo. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 149
Groogle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 151
Identificadores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152
Sheet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152
Calendar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 153
Exp4j . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 157
DSL. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 160
PlotDSL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 160
PlotsDSL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 162
De texto a Closure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 163
JavaFX . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 163
Xml. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 168
Groogle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 170
Business . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171
OpenDataMadrid . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 175
Preparación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 175
OpenDataMadrid . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 176
Dependencias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 178
Parse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 178
Telegram . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 179
Modelo-Vista-Controlador . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 183
Transiciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 185
TwitterFX . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 188
Dependencias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 188
Customize. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 189
A Twitter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 191
Dependencias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 195
Business . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 195
Vista. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 196
Calculo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 201
Vista. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 202
Ejemplo. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 203
Configuración dinámica de fichero remoto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 207
RabbitMQ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 211
Conexion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 212
Resultado . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 214
Docker. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 215
Crear un fichero de texto con todas las operaciones disponibles en el API del INE . . . . . 219
Contacts2QRCode . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 227
Objetivo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 227
Excel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 227
Dependencias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 228
generateQRLink . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 229
GenerateVCard . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 230
Objetivo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 233
Dependencias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 233
Script . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 233
Mail . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 245
TAE . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 251
Stress . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 253
Memoize . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 254
Comparativa . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 255
Dependencias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 256
Argumentos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 256
Modelo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 257
Prepare . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 258
Persistencia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 259
Dependencias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 263
Autorización . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 263
BitBucket . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 264
Report . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 265
Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 265
Introducción
Jorge Aguilera <jorge.aguilera@puravida-software.com [mailto:jorge.aguilera@puravida-
software.com]> 2017-08-01
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)
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)
• 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.
• 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
= 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
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
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
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.
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}
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.
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
generateProducts
|5
101 Groovy Script
createProductAdoc
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
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
① 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
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 ③
③ 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
— 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
Script
El script en sí es muy sencillo:
8|
101 Groovy Script
Cargar dependencias
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
|9
101 Groovy Script
Transformar
days =[:]
speackers =[:]
w.add talk
d[talk.when]= w
days[day.name]=d
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
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()
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.
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
12 |
101 Groovy Script
Script
//tag::dependencies[]
@Grab(group='io.github.http-builder-ng', module='http-builder-ng-apache', version='1.0.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
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 =[:]
w.add talk
d[talk.when]= w
days[day.name]=d
| 13
101 Groovy Script
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"
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()
| 15
101 Groovy Script
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.
16 |
101 Groovy Script
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
}
}
RevealJS
Una vez creado el fichero .adoc necesitamos convertirlo en formato html con la ayuda del
backend reveal.js y de asciidoctor-reveal.js
void dumpRevealJS(){
["git", "clone", "https://github.com/hakimel/reveal.js.git"].execute()
["git", "clone", "https://github.com/asciidoctor/asciidoctor-reveal.js.git"].execute()
}
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()
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()
//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[]
//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
}
}
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.
Cuando usamos una única función en una línea tampoco es necesario el uso de paréntesis
con lo que ganamos en legibilidad
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
println unObjeto
unObjeto = 10
println unObjeto
Además si queremos usar un String multilinea podemos hacerlo mediante el uso de tres
comillas dobles evitando el uso de caracteres de escape
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
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
Si necesitamos conocer en cada iteración además del elemento, la posición que ocupa en
la lista usaremos "eachWithIndex"
| 23
101 Groovy Script
Script
String myString
def unObjeto
println unObjeto
unObjeto = 10
println unObjeto
println "Mira lo que puedo hacer ${2+2} simplemente con el dolar y las llaves"
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.
| 25
101 Groovy Script
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)
}
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 "------------------------------------------------------------------"
}
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
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)
}
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:
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
if (!error.toString().equals("")) ⑤
println "Error al ejecutar el comando"
else{
println "Ejecutado correctamente"
println resultado ⑥
② Ejecutamos el comando.
30 |
101 Groovy Script
Script
if (!error.toString().equals("")) ⑤
println "Error al ejecutar el comando"
else{
println "Ejecutado correctamente"
println resultado ⑥
| 31
101 Groovy Script
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.
Configuration.groovy
import Sql
class Configuration {
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 una instancia mysql con los datos ya definidos.
⑦ Método que nos devuelve una instancia mysql con los datos enviados por parámetro.
32 |
101 Groovy Script
Script.groovy
OtroScript.groovy
| 33
101 Groovy Script
Script
import Sql
class Configuration {
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
batchSize=20
row.a = row.a.toUpperCase().reverse() ⑤
ps.addBatch(row) ⑥
}
}
⑦
| 35
101 Groovy Script
Script
@Grab('mysql:mysql-connector-java:5.1.6')①
@GrabConfig(systemClassLoader=true)
import groovy.sql.Sql
batchSize=20
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
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
| 37
101 Groovy Script
@Grab('org.yaml:snakeyaml:1.17')
import org.yaml.snakeyaml.Yaml
println config.dataSources?.development?.url
println config.dataSources?.production?.url
38 |
101 Groovy Script
Script
@Grab('org.yaml:snakeyaml:1.17')
import org.yaml.snakeyaml.Yaml
println config.dataSources?.development?.url
println config.dataSources?.production?.url
| 39
101 Groovy Script
Primer intento
El primer intento de comprobarlo fue mediante este simple script
(0..100000).each{idx->
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
void executeScript(){
dsl.run()
}
(0..100000).each{idx->
executeScript()
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.
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 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)
}
// wait to jconsole
sleep 1000*10
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 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)
}
// wait to jconsole
sleep 1000*10
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.
• 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
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
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:
② 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
46 |
101 Groovy Script
+-------------+
| mis_scripts |
+-------------+
|
| +-----+
+>| src |
+-----+
| +------+
+--->| main |
+------+
| +-----------+
+--->| resources |
+-----------+
Gradle
build.gradle
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.7.3'
}
}
repositories {
mavenCentral()
}
dependencies {
compile 'org.codehaus.groovy:groovy-all:2.4.12'
testCompile 'junit:junit:4.12'
}
javadoc {
title = "$project.description $project.version API"
}
| 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
}
}
}
③ El nombre del repositorio donde quieres publicar en Bintray (tienes que haberlo
creado antes en Bintray)
Publicando
gradle bintrayUpload
Si todo va bien, se habrá creado tu jar con los scripts embebidos en él y se habrá subido a
Bintray
"Grapeando"
48 |
101 Groovy Script
| 49
101 Groovy Script
Script
50 |
101 Groovy Script
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
• URL a parsear
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
52 |
101 Groovy Script
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.
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
groovyConsole
Para realizar una prueba sencilla podemos ejecutar nuestro primer "Hola Mundo":
HolaMundo.groovy
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
| 55
101 Groovy Script
Script
56 |
101 Groovy Script
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.
A continuación vamos a ver algunos métodos prácticos y básicos para trabajar con listas:
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.
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.
| 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:
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
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.
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.
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.
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)
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
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.
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 ⑤
③ Indicamos así mismo datos necesarios para la conexion como usuario y password
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
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)
| 65
101 Groovy Script
void prepareBiblioteca() {
Biblioteca.withTransaction {
Biblioteca.list().each{ it.delete() }
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
• etc
Añadir Libros
void addLibros(){
Biblioteca.withTransaction{
Biblioteca b = Biblioteca.first()
assert b
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() ①
}
}
Biblioteca.withNewSession{
Biblioteca b = Biblioteca.findByNombre('Biblioteca Nacional') ④
assert b
println "Biblioteca encontrada $b"
② similar a select * from biblioteca . Podemos indicar que en la misma query recupere
el detalle de los libros pasando como argumento fetch:[libros:"join"]
④ 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
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() }
| 69
101 Groovy Script
}
}
//end::biblio[]
//tag::libros[]
void addLibros(){
Biblioteca.withTransaction{
Biblioteca b = Biblioteca.first()
assert b
b.save()
}
}
//end::libros[]
//tag::list[]
void list(){
Biblioteca.withTransaction{
assert Biblioteca.count() ①
}
}
Biblioteca.withNewSession{
Biblioteca b = Biblioteca.findByNombre('Biblioteca Nacional') ④
assert b
println "Biblioteca encontrada $b"
//tag::main[]
initDatastore()
prepareBiblioteca()
addLibros()
println '-'.multiply(20)
70 |
101 Groovy Script
list()
println '-'.multiply(20)
//end::main[]
| 71
101 Groovy Script
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.
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):
① Obviamente necesitarás tener instalado Docker en tu sistema (ya tardas) para poder
ejecutar el comando.
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}"
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.
<1>Este script ejecuta diferentes acciones según el parámetro que le pasemos. Con -a
vuelca las variables de sistema
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)
| 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
}
}
74 |
101 Groovy Script
Script
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)
}
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.
• cuenta en twitter y crear unas credenciales para que tu script pueda tuitear en tu
nombre.
76 |
101 Groovy Script
IncidenciasMadrid.groovy
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
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"
}
}
}
② Obtenemos las ultimas incidencias en formato XML filtrando por las no previstas
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 ④
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
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
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
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] ⑥
⑥ 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"
}
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
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
④ 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] ).
| 85
101 Groovy Script
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) ③
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')}*
"""
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.
| 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.
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:
• 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.
92 |
101 Groovy Script
@Grab('com.aestasit.infrastructure.sshoogr:sshoogr:0.9.25')
@GrabConfig(systemClassLoader=true)
import static com.aestasit.infrastructure.ssh.DefaultSsh.*
options.trustUnknownHosts = true ①
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.
remoteSession("user:password@$server:22"){
remoteFile("$orig/gradle.properties").text = "version=$version"
}
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}
}
}
| 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
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
VOLUME ["/var/watchfile"]
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 images
Ejecutando la imagen
Para ejecutar la imagen simplemente haremos:
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:
| 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
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:
| 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
cli.with { ①
h(longOpt: 'help', 'Import/Export excel file to i18n properties files.', required: false)
action('generateProperties generateExcel', required: true, args:1, argName:'action')
}
cli.usage() ④
② de Excel a properties
③ de properties a Excel
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:
| 103
101 Groovy Script
void generateExcel(filename){
File excelFile = new File(filename)
① cargar en un Properties el fichero por defecto (el cual contienen todos las keys a
traducir)
② preparar un Map<String,Properties>
⑤ 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)
①
new File(dir).eachFileMatch ~"${name}(_[A-Za-z]+)?\\.properties", {
it.delete()
}
List<String> languages = []
Workbook wb = WorkbookFactory.create(inp)
Sheet sheet = wb.getSheetAt(0)
③ 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
| 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')
}
cli.usage() ④
//end::arguments[]
//tag::generateProperties[]
void generateProperties(filename){
File excelFile = new File(filename)
①
new File(dir).eachFileMatch ~"${name}(_[A-Za-z]+)?\\.properties", {
it.delete()
}
List<String> languages = []
Workbook wb = WorkbookFactory.create(inp)
Sheet sheet = wb.getSheetAt(0)
106 |
101 Groovy Script
return
}
//tag::generateExcel[]
void generateExcel(filename){
File excelFile = new File(filename)
| 107
101 Groovy Script
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
scanDir(new File('.'))
Para recorrer cada uno de los directorios y subdirectorios de la ruta indicada por
parámetro utilizamos:
dir.eachFile{ f->
dir.eachDir{ d ->
108 |
101 Groovy Script
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
| 109
101 Groovy Script
Script
max=null
scanDir(new File('.'))
110 |
101 Groovy Script
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.
outputFile.newWriter().withWriter { writer-> ①
inputFile.eachLine { line, indx -> ②
if (line.indexOf(search) != -1)
writer << "$indx: $line\n" ③
}
}
② 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
| 111
101 Groovy Script
Script
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 = [:]
artifact[prp['version']]=version
group[prp['artifactId']]=artifact
groups[prp['groupId']] = group
}
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
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 com.drew.metadata.*
import com.drew.imaging.*
import com.drew.imaging.jpeg.*
import com.drew.metadata.exif.*
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
}
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"
}
| 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 com.drew.metadata.*
import com.drew.imaging.*
import com.drew.imaging.jpeg.*
import com.drew.metadata.exif.*
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
}
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"
}
| 119
101 Groovy Script
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
new File('cleaned.txt').withWriter('ISO-8859-1',{
it.write origen.join('\n') ③
})
② Eliminamos de la lista todos los registros que cumplan la condicion de comenzar por el
ID, por ejemplo.
120 |
101 Groovy Script
Script
new File('cleaned.txt').withWriter('ISO-8859-1',{
it.write origen.join('\n') ③
})
| 121
101 Groovy Script
122 |
101 Groovy Script
repoUrl=""
repoSnapUrl=""
repoId = args[1]
repoUrl = args[2]
repoSnapUrl = args.length > 3 ? args[3] : args[2]
② Comprobamos si existe un Jar o War con el mismo nombre (indica que pertenecen a la
misma version)
| 123
101 Groovy Script
Script
repoUrl=""
repoSnapUrl=""
repoId = args[1]
repoUrl = args[2]
repoSnapUrl = args.length > 3 ? args[3] : args[2]
124 |
101 Groovy Script
Consola Google
En primer lugar deberemos crear una aplicación en la consola de Google en
https://console.developers.google.com
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.login(clientSecret,[CalendarScopes.CALENDAR_READONLY]) ③
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
② Un evento puede ser puntual o recurrente. En este caso deberemos obtener todas las
instancias del mismo
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:
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.
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[]
groogle.login(clientSecret,[CalendarScopes.CALENDAR_READONLY]) ③
//end::login[]
//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 = [:]
//tag::flatten[]
| 129
101 Groovy Script
//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
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.
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 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
| 131
101 Groovy Script
Id del SpreadSheet
https://docs.google.com/spreadsheets/d/xxxxxxxxxtKhml999999999Icp123/edit#
gid=0
Consola Google
En primer lugar deberemos crear una aplicación en la consola de Google en
https://console.developers.google.com
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:
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)
Database
Para la gestión de la base de datos hemos creado una serie de métodos simples:
| 133
101 Groovy Script
② Borrado de una tabla. En nuestro caso y por ser MySQL hacemos un truncate
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
De Google a Database
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
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"
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>>
| 137
101 Groovy Script
if( !google2Database ) {
sheetScript.withSpreadSheet options.spreadSheet, { spreadSheet ->
sheets.each{ gSheet ->
String hojaId = gSheet.properties.title
println "Exportando a hoja $hojaId"
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
}
| 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 -> ①
//tag::g2d[]
if( google2Database ) {
sheetScript.withSpreadSheet options.spreadSheet, { spreadSheet ->
sheets.each { gSheet ->
String hojaId = gSheet.properties.title
println "Importando hoja $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"
140 |
101 Groovy Script
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
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:
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
③ la primera vez abrirá un navegador para identificarnos. Las siguientes usará la caché
De forma resumida, tras hacer login, el script nos permitirá indicar para cada ejecución:
• 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 excepto owner de TODOS los ficheros de mi
Drive (no pasamos argumento) o de una carpeta (proporcionamos su id)
| 143
101 Groovy Script
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)
}
}
}
}
Ambos comandos son parecidos diferenciandose en que uno actúa sobre un fichero dado
y el otro recorre todos los ficheros de una carpeta.
144 |
101 Groovy Script
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:
• 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á.
| 145
101 Groovy Script
Si una vez ejecutado el comando para eliminar permisos volvieramos a ejecutar el dump
obtendríamos algo parecido a:
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:
• 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
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
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.
import java.awt.BorderLayout
import java.awt.BorderLayout as BL
import javax.swing.*
| 147
101 Groovy Script
credentials {
applicationName 'raffle'
withScopes SheetsScopes.SPREADSHEETS
usingCredentials "client_secret.json"
asService false
}
service(SheetServiceBuilder.build(), SheetService)
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.
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
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"
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.
| 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
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:
Como los eventos que vamos a gestionar son eventos de todo el día vamos a formatear la
152 |
101 Groovy Script
events=[]
SheetScript.instance.withSpreadSheet sheetId, {
withSheet 'Eventos',{
int rowIdx=2
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
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
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
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.
| 157
101 Groovy Script
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 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)
}
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.
| 159
101 Groovy Script
DSL
Como podemos ver nuestro DSL cuenta con 2 partes:
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
}
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)
}
| 161
101 Groovy Script
③ 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.
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
① Usaremos una estrategia de delegación entre las varias que permite Groovy
162 |
101 Groovy Script
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
txt=args.join(' ')
if( args.length == 1 && new File(args[0]).exists() ){
txt = new File(args[0]).text
}
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
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 groovy.transform.ToString
class PlotDSL{
//tag::plotDSL[]
List<String> _functions;
PlotDSL(){ ①
_functions=[]
}
PlotDSL(List<String> functions){①
_functions = functions
}
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 = []
//tag::plotsConstructor[]
PlotsDSL plot( Closure closure){
plot(null,closure)
}
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
}
//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)
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
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
| 169
101 Groovy 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.
En nuestro caso vamos a utilizar el último (el primero se autoincluye por dependencia
transitiva)
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
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
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:
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()
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')
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[]
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()
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
— 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:
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
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
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)
| 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.
178 |
101 Groovy Script
task eventosBibliotecas(){
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"
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:
o si usamos windows
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
Qué es JavaFX
— 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.
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" ②
}
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
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()
| 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" ②
}
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 twitter4j.TwitterFactory;
import twitter4j.StatusUpdate
Básicamente:
• librería de twitter
Customize
Para poder ajustar nuestro script de forma cómoda vamos a utilizar las siguientes
variables:
int width=height=400
| 189
101 Groovy Script
def data=[:] ①
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) ③
}
}
}
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
A Twitter
Generar un tweet mediante twitter4j es tan sencillo como crear un StatusUpdate,
adjuntar un fichero si así lo deseamos y enviarlo:
| 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 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
"""
int width=height=400
//end::customize[]
//tag::business[]
def data=[:] ①
//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
//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
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 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')) {
| 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 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')) {
200 |
101 Groovy Script
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:
Calculo
Dado un directorio como argumento, el script lo recorrera recursivamente generando 3
mapas con la extensión como clave:
• 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=[:]
scanDir(new File(args[0]))
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
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)
//tag::calculating[]
bySize=[:]
byNumber=[:]
byBoth=[:]
scanDir(new File(args[0]))
| 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)
206 |
101 Groovy Script
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.
@Grab('com.aestasit.infrastructure.sshoogr:sshoogr:0.9.25')
@GrabConfig(systemClassLoader=true)
| 207
101 Groovy Script
def getFile(file_config,server_name){
remoteSession("user:passwd@$server_name:22") {①
result = remoteFile(file_config).text ②
}
return result.readLines()③
}
③ Con la función readLines obtenemos una lista con el contenido del fichero.
def putFile(file_name,server_name,new_file){
remoteSession("user:passwd@$server_name:22") {①
remoteFile(dir).text = properties ②
}
}
putFile(file_config,server_name,properties)③
208 |
101 Groovy Script
options.trustUnknownHosts = true
| 209
101 Groovy Script
Script
//tag::dependencies[]
@Grab('com.aestasit.infrastructure.sshoogr:sshoogr:0.9.25')
@GrabConfig(systemClassLoader=true)
//end::dependencies[]
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) ①
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
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):
• consumer, recibe los eventos de los diferentes producer’S y los va mostrando por
pantalla
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:
| 211
101 Groovy Script
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='#'
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.
212 |
101 Groovy Script
monitor = args[2]
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)
① Usamos un RandomAccess que nos permite movernos por el fichero en lugar de leerlo
secuencial
| 213
101 Groovy Script
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).
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:
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 ⑥
| 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
$ 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='#'
channel = conn.createChannel()
channel.exchangeDeclare(exchangeName, "direct", true)
channel.queueDeclare(queueName, true, false, false, null)
channel.queueBind(queueName, exchangeName, routingKey)
//end::conexion[]
//tag::producer[]
monitor = args[2]
| 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 {
218 |
101 Groovy Script
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.
import groovyx.net.http.*
operations.each {
ineOperations << "${it}\n" ⑤
}
⑤ Por cada operación que nos devuelve el API, guardamos una línea en el fichero de
texto.
| 219
101 Groovy Script
Script
import groovyx.net.http.*
operations.each {
ineOperations << "${it}\n" ⑤
}
220 |
101 Groovy Script
En este script veremos como usando Groovy y Ratpack podemos crear un servidor web
completo en un solo fichero.
@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!"
}
}
}
| 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.
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
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.
@Grab('com.aestasit.infrastructure.sshoogr:sshoogr:0.9.25')
@GrabConfig(systemClassLoader=true)
import static com.aestasit.infrastructure.ssh.DefaultSsh.*
def getFile(dir,host){
remoteSession("user:passwd@$host:22") {①
result = remoteFile(dir).text ②
}
return result.readLines()③
224 |
101 Groovy Script
③ Con la función readLines obtenemos una lista con el contenido del fichero.
hosts.each{host->
getFile(file,host).each{line->
println line
}
}
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
• Escribir una celda en función de los valores leídos y/o lógica de negocio
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.
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:
| 227
101 Groovy Script
Organization
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')
228 |
101 Groovy Script
② Iteramos en todas las hojas del Excel. Podríamos buscar una en concreto si así lo
quisieramos
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:
② 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 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:
① 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=";
//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
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 ①
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.
④ 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.
<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 ①
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
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.
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:
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);①
def rows = []
while(rowIt.hasNext()) {
row = rowIt.next()
rows << getRowData(row)
}
[headers, rows]
}
② 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:
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…
| 237
101 Groovy Script
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)
}
}
}
| 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);①
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
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
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"
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
}
② Para cada registro que cumpla la query generamos una fila nueva
④ 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"
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
}
244 |
101 Groovy Script
Mail
Miguel Rueda <miguel.rueda.garcia@gmail.com [mailto:miguel.rueda.garcia@gmail.com]>
2017-08-04
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){ ①
| 245
101 Groovy Script
Transport.send( msg ) ③
}
}
③ Enviamos el email.
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"
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
}
| 247
101 Groovy Script
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){ ①
248 |
101 Groovy Script
| 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"
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
}
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.
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
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.
| 251
101 Groovy Script
if( close == 0) { ④
float tae = ( Math.pow(1+interes, 12) -1 )*100
return tae
}
252 |
101 Groovy Script
① 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
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))
}
}
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],
]
Una vez ejecutadas dichas pruebas en mi máquina (i7, 16Gb memoria) tendremos un
resultado similar a:
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.
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
Comparativa
Versión Llamadas a función Tiempo ejecución
| 255
101 Groovy Script
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.
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
• --all, recorre todos los usuarios existentes en la base de datos y los actualiza
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(){
}
| 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
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
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(){
}
//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
//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
Seguramente este caso de uso no será muy común pero espero que te aporte ideas si
trabajas con BitBucket
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
• generar una nueva clave ssh desde nuestro sistema de CI y añadirla a nivel de
organización
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)
Dependencias
Sólo vamos a necesitar http-builder y slf4j:
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.
| 263
101 Groovy Script
creds = "$username:$password".bytes.encodeBase64()
① 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:
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
}
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
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()
//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))
| 267
101 Groovy Script
Qué es SOAP
— 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:
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
| 269
101 Groovy Script
println "${response.GetMothersDayResponse?.GetMothersDayResult}" ①
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>
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>
② 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}"
dependencias
@Grab('com.github.groovy-wslite:groovy-wslite:1.1.2')
@Grab('mysql:mysql-connector-java:5.1.6')
@GrabConfig(systemClassLoader=true)
crear cabecera
272 |
101 Groovy Script
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())
}
}
}
excepciones
① 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.
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
276 |