285

101 Groovy Script · • 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

  • Upload
    others

  • View
    5

  • Download
    0

Embed Size (px)

Citation preview

101 Groovy Scriptque en mi local funcionan

Varios

2019-04-09

Table of ContentsIntroducción. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .  1

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Ejecución programada . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .  76

Gitlab. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .  77

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Groogle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .  125

Personalización . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .  126

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

PlotsDSL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .  162

De texto a Closure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .  163

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Ejemplo. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .  203

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

IntroducciónJorge Aguilera <[email protected] [mailto:jorge.aguilera@puravida-

software.com]> 2017-08-01

Puedes acceder a todo el contenido en formato HTML a través de esteenlace https://groovy-lang.gitlab.io/101-scripts/

Puedes acceder a todo el contenido en formato eBook a través de esteenlace 101-groovy-scripts-epub.epub

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

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

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

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

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

Consideraciones• Queremos añadir scripts que solucionen un problema específico (real o inventando).

No importa que tu script sea particular a una situación concreta pero seguro queaporta 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 puederesolver un problema (y así llegamos a 101 antes)

• Debes adjuntar al menos un script y una documentación. Si quieres adjuntar imágenesperfecto pero no es vital.

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

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

• Usamos Asciidoctor como herramienta para documentar. Es imperativo porque es laherramienta que nos permite publicar en Html, Pdf y ePub sin modificar ladocumentación. Sin embargo no tienes porqué usar todas sus funcionalidades.

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

101 Groovy Script

| 1

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

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

Como normal general el script lo capitalizamos y la documentación no. Si tudocumentación está en ingles es preferible que le añadas el sufijo -en al nombre (porejemplo la-docu-en.adoc ). Si está en español no es necesario.

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

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

Tu documentacion comienza aqui

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

LocalhostSi cuentas con Java instalado en tu máquina es posible ejecutar el blog en local y así teserá más fácil revisarlo antes de hacer el Merge Request. Para ello ejecuta simplementedesde el raiz del proyecto

./gradlew serveApp

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

101 Groovy Script

2 |

Entorno de trabajoPuedes usar desde un simple editor a un IDE sofisticado. Nosotros trabajamos con Intellijpero probablemente Eclipse te sirva también. Simplemente recuerda que debes reiniciarla tarea serveApp para que te vuelva a generar el contenido

101 Groovy Script

| 3

Unresolved directive in 101-groovy-scripts-pdf.adoc -include::../../jbake/content/content/about_template.adoc[] <<<< :jbake-script:/scripts/asciidoctor/Catalogo.groovy = Catálogo de productos con Asciidoctor Miguel Rueda<[email protected] [mailto:[email protected]]> 2018-01-25

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

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

Para la generación de este catálogo nos vamos a basar en Asciidoctor[http://asciidoctor.org/]. Para una consulta rápida sobre esta herramientapuedes consultar el siguiente tutorial [http://jorge-aguilera.gitlab.io/

tutoasciidoc/], el cual es una guía básica pero completa sobre esta materia.

En este caso vamos a utilizar imágenes en nuestro local pero podemosapuntar a links donde tengamos nuestras imágenes . En nuestro ejemploel id de cada item corresponderá con el nombre de imagen a usar para elartículo.

El schema de nuestra base de datos será algo muy simple, consistente enuna única tabla con el id, descripción, precio de cada artículo.

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

product.tpl

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

Referencia ${sku}, ${price}----

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

101 Groovy Script

4 |

catalog.tpl

= Catalogo de Productos

Miguel Rueda <[email protected]>

:idprefix:

:icons: font

:imagesdir: ./images

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

Preparación del entorno

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

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

Para empezar a trabajar lo primero es cargar las plantillas anteriormente mencionadas(las del producto: product.tpl y la del catálogo general: catalog.tpl), al igual que la querypara la base de datos junto con la condición que requiera el cliente.

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

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

Carga de información y creación de los productosA continuación pasamos a explicar como cargamos de la base de datos los productos ycomo posteriormente generamos partiendo de un fichero base nuestros productos.

Carga de información

Para ello utilizaremos la función generateProducts() la cual se conecta a la base de datosque le indiquemos, ejecutará la query anteriormente definida y por cada producto llamaráal método createProductAdoc encargado del hacer el .adoc del producto que recibirá porparámetro:

generateProducts

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

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

Creación de cada producto

Mediante el engine previamente inicializado con la plantilla correspondienteconseguiremos generar un fichero para cada artículo donde se sustituyan las variables de

101 Groovy Script

| 5

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

createProductAdoc

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

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

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

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

generateCatalog

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

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

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

cretatePDF

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

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

① Creamos los atributos que contendrá nuestro fichero asciidoctor. En los cualespodemos 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 quees el formato que deseamos obtener

③ Conversión del adoc a pdf

Personalización del pdf

Si quisiéremos personalizar aún más nuestro pdf podemos crear un "tema" para nuestropdf y con ello aumentar las características del mismo. Para ello tenemos que crear unfichero 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 detema:

101 Groovy Script

6 |

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

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

② La fuente de nuestro texto es Times-Roman

③ El tamaño de letra de es 12

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

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

101 Groovy Script

| 7

Json a PdfJorge Aguilera <[email protected] [mailto:jorge.aguilera@puravida-

software.com]> 2018-08-13

"Commit es la conferencia donde nos reunimos a discutir diferentesperspectivas en la forma de hacer y gestionar software. Ven connosotros para vivir dos días compartiendo y aprendiendo todo lo quetiene la tecnología, y por la oportunidad de romper con la rutina yexperimentar algo nuevo."

— CommitConf

Commit es una conferencia anual que cuenta con un agenda de charlas y talleres muyextensa. Este año (2018) son dos días con 12 tracks simulatáneos. Puedes consultar laagenda 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 loque vamos a desarrollar un pequeño script que consumirá la propia agenda en formatoJSON y lo transformará en un PDF agrupando las charlas por rangos horarios además demostrar un listado de speakers al final del documento con las charlas que dará cada uno

En resumen vamos a consumir una estructura de datos pensada para un tipo depresentación y la vamos a transformar en otro formato.

Para ello vamos a utilizar:

• http-builder-ng para consumir el JSON

• asciidoctor para generar el Pdf

ScriptEl script en sí es muy sencillo:

101 Groovy Script

8 |

Cargar dependencias

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

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

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

@Grab(group='org.jruby', module='jruby-complete', version='9.1.15.0')

import groovyx.net.http.*

import static groovyx.net.http.HttpBuilder.configure

import static groovyx.net.http.ContentTypes.JSON

import static groovy.json.JsonOutput.prettyPrint

import static groovy.json.JsonOutput.toJson

import org.asciidoctor.OptionsBuilder

import org.asciidoctor.AttributesBuilder

import org.asciidoctor.SafeMode

import org.asciidoctor.Asciidoctor.Factory

Recuperar agenda

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

101 Groovy Script

| 9

Transformar

days =[:]speackers =[:]

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

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

  slot.contents.authors.each{ author ->  def speacker = speackers[author.name] ?: []  speacker.add talk  speackers[author.name] = speacker  }  }  }  }}

La transformación simplemente itera entre los days del JSON y por cada uno itera por lostracks y de estos por los 'slots`. Para cada slot del tipo TALK crea un mapa con los datosque 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 ycreamos un elemento en el mapa de speakers con la información que nos interese delmismo.

101 Groovy Script

10 |

Asciidoctor

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

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

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

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

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

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

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

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

101 Groovy Script

| 11

Publicación y ScheduleUna vez que tenemos nuestro conversor listo nos gustaría poder compartir el documentopor lo que vamos a utilizar la capacidad de Gitlab de servir contenido estático generadopor nuestro proyecto.

Con alguna pequeña modificación también podrías usar otros productoscomo Github, Bitbucket etc

Para ello crearemos un proyecto en nuestra cuenta de Gitlab y añadiremos además delscript 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á laimagen Docker groovy que especificamos en el job commit2018) y si todo va bien lopublicará en un servidor de contenido estático

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

Como la agenda puede sufrir modificaciones utilizaremos la funcion Schedule de Gitlabque nos permite programar de forma recurrente nuestros jobs de tal forma que podremosactualizar diariamente (por ejemplo) el documento.

101 Groovy Script

12 |

Script

//tag::dependencies[]

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

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

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

@Grab(group='org.jruby', module='jruby-complete', version='9.1.15.0')

import groovyx.net.http.*

import static groovyx.net.http.HttpBuilder.configure

import static groovyx.net.http.ContentTypes.JSON

import static groovy.json.JsonOutput.prettyPrint

import static groovy.json.JsonOutput.toJson

import org.asciidoctor.OptionsBuilder

import org.asciidoctor.AttributesBuilder

import org.asciidoctor.SafeMode

import org.asciidoctor.Asciidoctor.Factory

//end::dependencies[]

//tag::consumir[]

def http = configure {

  request.uri = 'https://www.koliseo.com/'

  request.contentType = JSON[0]

  request.accept = JSON[0]

}.get{

  request.uri.path='/events/commit-2018/r4p/5630471824211968/agenda'

}

//end::consumir[]

//tag::transformar[]

days =[:]

speackers =[:]

http.days.each{ day ->

  def slots = []

  day.tracks.each{ track ->

  track.slots.each{ slot ->

  if( ['TALK'].contains(slot.contents?.type) ){

  def talk = [

  title: slot.contents.title,

  day: "$day.name",

  when: "$slot.start $slot.end",

  slot: slot.id,

  authors: slot.contents.authors.collect{ it.name }.join(',')

  ]

  def d = days[day.name] ?: [:]

  def w = d[talk.when] ?: []

  w.add talk

  d[talk.when]= w

  days[day.name]=d

  slot.contents.authors.each{ author ->

101 Groovy Script

| 13

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

  speacker.add talk

  speackers[author.name] = speacker

  }

  }

  }

  }

}

//end::transformar[]

println "talks:"

days.each{ d->

  d.each{

  println it

  }

}

println "speackers:"

speackers.each{

  println it

}

//tag::asciidoctor[]

file = new File("commit2018.adoc")

file.write "= Commit 2018\n"

file << "Agenda\n\n\n"

file << ":chapter-label:\n"

file << "\n"

days.sort().each{ day ->

  file << "== $day.key\n"

  day.value.sort{it.key}.each{

  file << "=== $it.key\n"

  it.value.each{

  file << "- $it.title ($it.authors)\n\n"

  }

  }

}

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

speackers.sort{it.key}.each{

  file << "=== $it.key\n"

  it.value.each{

  file << "- $it.title\n"

  file << " $it.day $it.when \n\n"

  }

  file << "\n"

}

asciidoctor = Factory.create();

attributes = AttributesBuilder.attributes().

  docType('book').

  tableOfContents(true).

  sectionNumbers(false).

  sourceHighlighter("coderay").

  get()

101 Groovy Script

14 |

options = OptionsBuilder.options().

  backend('pdf').

  attributes(attributes).

  safe(SafeMode.UNSAFE).

  get()

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

//end::asciidoctor[]

101 Groovy Script

| 15

Convertir PowerPoint to HTMLMiguel Rueda <[email protected] [mailto:[email protected]]>2018-04-29

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

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

PreparaciónEstos 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 PowerPointXMLSlideShow nos va permitir acceder a la presentación pasando como argumento unobjeto 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 tipoXSLFPictureShape será una imagen y creará un png con ella.

Una vez ha terminado de parsear la diapositiva incrementará el fichero slide.adoc con lainformación obtenida de la misma creando un nuevo apartado asciidoctor

101 Groovy Script

16 |

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

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

ppt.getSlides().each{slide->

  String text = ""

  String image=""

  slide.getShapes().each{shape->

  if (shape instanceof XSLFPictureShape ){

  if (shape.getPictureData()){

  new File(shape.getPictureData().getFileName()) << shape.getPictureData().getData()

  image = "image:${shape.getPictureData().getFileName()}[]"

  }

  }else{

  if (shape.text)

  text = shape.text

  }

  }

  file << """

== ${text}

${slide_c.image}

"""

}

RevealJSUna vez creado el fichero .adoc necesitamos convertirlo en formato html con la ayuda delbackend reveal.js y de asciidoctor-reveal.js

Como ambos deben encontrarse descargados usaremos el método dumpRevealJS que seencargará de descargar estos proyectos en los subdiretorios adecuados:

void dumpRevealJS(){

  ["git", "clone", "https://github.com/hakimel/reveal.js.git"].execute()

  ["git", "clone", "https://github.com/asciidoctor/asciidoctor-reveal.js.git"].execute()

}

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

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

101 Groovy Script

| 17

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

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

101 Groovy Script

18 |

Script

//tag::grapes[]

@Grapes([

  @Grab(group='org.apache.poi', module='poi', version='3.17' ),

  @Grab(group='org.apache.poi', module='poi-ooxml', version='3.17' ),

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

  @Grab(group='org.jruby', module='jruby-complete', version='9.1.15.0')

])

//end::grapes[]

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

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

import org.asciidoctor.SafeMode

import org.asciidoctor.OptionsBuilder

import org.asciidoctor.Asciidoctor.Factory

//tag::create[]

void createSlides(){

  asciidoctor = Factory.create()

  options = OptionsBuilder.options().

  templateDirs(new File('./asciidoctor-reveal.js/','templates')).

  backend('revealjs').

  inPlace(true).

  safe(SafeMode.UNSAFE).

  get()

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

}

//end::create[]

//tag::load[]

void dumpRevealJS(){

  ["git", "clone", "https://github.com/hakimel/reveal.js.git"].execute()

  ["git", "clone", "https://github.com/asciidoctor/asciidoctor-reveal.js.git"].execute()

}

//end::load[]

// limpiamos si hubiera restos anteriores

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

  file.delete()

}

//tag::each[]

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

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

ppt.getSlides().each{slide->

  String text = ""

  String image=""

  slide.getShapes().each{shape->

  if (shape instanceof XSLFPictureShape ){

  if (shape.getPictureData()){

  new File(shape.getPictureData().getFileName()) << shape.getPictureData().getData()

  image = "image:${shape.getPictureData().getFileName()}[]"

  }

  }else{

101 Groovy Script

| 19

  if (shape.text)

  text = shape.text

  }

  }

  file << """

== ${text}

${slide_c.image}

"""

}

//end::each[]

dumpRevealJS()

createSlides()

println "Creado!!!"

101 Groovy Script

20 |

Conceptos básicosJorge Aguilera <[email protected] [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 hacenmenos "verbose" que Java.

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

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

Punto y coma, paréntesis y demásSí, el famoso asunto del punto y coma es lo primero que se comenta cuando empiezas conGroovy. Efectivamente Groovy no necesita que termines las sentencias en ";" pues escapaz de deducirlo por la sintaxis y el contexto. De todas formas si quieres ponerlotampoco le molesta.

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

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

println "Yo tampoco, ni los parentesis"

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

Groovy admite ambos tipos de declaración por lo que puedes declarar tus variables yfunciones indicando el tipo de dato, pero disponer de una declaración dinámica de tipospuede 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 objetoimplementa los métodos que indiquemos

101 Groovy Script

| 21

String myString

def unObjeto

unObjeto = "soy un string"

println unObjeto

unObjeto = 10

println unObjeto

Comillas simples y doblesPodemos declarar cadenas usando tanto la comilla simple como la comilla doble, de talforma que podemos incluir una de ellas en la otra.

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

println "Necesito una comilla simple ' "println 'Necesito una comilla doble " 'println """Necesito muchas lineasdonde poder ver los retornos de carrosin tanto lio como concatenando String """

String vs GStringGroovy añade la clase GString que es muy similar a String. De hecho en los ejemplosanteriores cuando usabamos la cadena doble en realidad estabamos instanciando unGString.

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 decadenas de java:

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

ClosureMucho antes de que Java implementará las famosas Lambdas, Groovy proporciona elconcepto de closure

101 Groovy Script

22 |

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

def unaClosure = { param ->  "${param}".reverse()}

println unaClosure.call("una cadena")println unaClosure.call(10)

Listas, mapas y otros seresDeclarar listas y mapas, asi como asignarlos es trivial

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

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

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

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

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

lista.eachWithIndex{ ele, idx -> ①  println "posicion $idx, elmenento $ele"}

① La closure recibe dos parámetros

101 Groovy Script

| 23

Script

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

println "Yo tampoco, ni los parentesis"

String myString

def unObjeto

unObjeto = "soy un string"

println unObjeto

unObjeto = 10

println unObjeto

println "Necesito una comilla simple ' "println 'Necesito una comilla doble " 'println """Necesito muchas lineasdonde poder ver los retornos de carrosin tanto lio como concatenando String """

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

def unaClosure = { param ->  "${param}".reverse()}

println unaClosure.call("una cadena")println unaClosure.call(10)

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

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

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

lista.eachWithIndex{ ele, idx -> ①  println "posicion $idx, elmenento $ele"}

101 Groovy Script

24 |

CliBuilderMiguel Rueda <[email protected] [mailto:[email protected]]>2017-08-30

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

La opción más fácil es usar la variable args ímplicita en el script y que es un array deString 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 permitehacer que los argumentos que se le pueden pasar a un script sean más explícitos.

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

101 Groovy Script

| 25

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

cli.with { ①

  h(longOpt: 'help', 'Usage Information \n', required: false)

  a(longOpt: 'Option a','Al seleccionar "a" pinta seleccionada -> a ', required: false)

  b(longOpt: 'Option b','Al seleccionar "b" pinta seleccionada -> b ', required: false)

  c(longOpt: 'Option c','Al seleccionar "c" pinta seleccionada -> c ', required: false)

  d(longOpt: 'Option d','Al seleccionar "d" pinta seleccionada -> d ', required: false)

}

def options = cli.parse(args)

if (!options) {

  return

}

if (options.h) { ②

  cli.usage()

  return

}

if (options.a) { ③

  println "------------------------------------------------------------------"

  println "Seleccionada ha sido la 'a'"

  println "------------------------------------------------------------------"

}

if (options.b) {

  println "------------------------------------------------------------------"

  println "Seleccionada ha sido la 'b'"

  println "------------------------------------------------------------------"

}

if (options.c) {

  println "------------------------------------------------------------------"

  println "Seleccionada ha sido la 'c'"

  println "------------------------------------------------------------------"

}

if (options.d) {

  println "------------------------------------------------------------------"

  println "Seleccionada ha sido la 'd'"

  println "------------------------------------------------------------------"

}

① Definimos los parámetros que vamos a necesitar.

② Mostrará la leyenda de nuestro comando.

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

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

101 Groovy Script

26 |

groovy clibuilder_ebook.groovy -husage: 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

101 Groovy Script

| 27

Script

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

cli.with { ①

  h(longOpt: 'help', 'Usage Information \n', required: false)

  a(longOpt: 'Option a','Al seleccionar "a" pinta seleccionada -> a ', required: false)

  b(longOpt: 'Option b','Al seleccionar "b" pinta seleccionada -> b ', required: false)

  c(longOpt: 'Option c','Al seleccionar "c" pinta seleccionada -> c ', required: false)

  d(longOpt: 'Option d','Al seleccionar "d" pinta seleccionada -> d ', required: false)

}

def options = cli.parse(args)

if (!options) {

  return

}

if (options.h) { ②

  cli.usage()

  return

}

if (options.a) { ③

  println "------------------------------------------------------------------"

  println "Seleccionada ha sido la 'a'"

  println "------------------------------------------------------------------"

}

if (options.b) {

  println "------------------------------------------------------------------"

  println "Seleccionada ha sido la 'b'"

  println "------------------------------------------------------------------"

}

if (options.c) {

  println "------------------------------------------------------------------"

  println "Seleccionada ha sido la 'c'"

  println "------------------------------------------------------------------"

}

if (options.d) {

  println "------------------------------------------------------------------"

  println "Seleccionada ha sido la 'd'"

  println "------------------------------------------------------------------"

}

101 Groovy Script

28 |

Ejecutar comandosMiguel Rueda <[email protected] [mailto:[email protected]]>2017-08-31

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

Así mismo numerosas veces debemos ejecutar esos comandos de forma repetida (una vezpor cada directorio, por cada fichero, etc) e incluso condicional (si la invocación de estecomando ha ido bien realizar estas acciones y si no estas otras). Para ello solemos recurrira ficheros por lotes ( .bat en Windows, .sh en Linux ) donde podemos tratar los problemascomentados 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, utilizandotoda la potencia del lenguaje.

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

String resultado = "ls -lt ".execute().textprintln resultado.toUpperCase()

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

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

101 Groovy Script

| 29

def resultado = new StringBuilder() ①def error = new StringBuilder()

def comando = "ls -lt".execute() ②comando.consumeProcessOutput(resultado, error) ③comando.waitForOrKill(1000) ④

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

}

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

② Ejecutamos el comando.

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

④ Establecemos un time out a nuestro comando.

⑤ En error obtendremos la salida en caso de que falle nuestro comando y en caso defuncionar correctamente nuestro valor se guardará en resultado.

⑥ El resultado es correcto podemos continuar.

101 Groovy Script

30 |

Script

def resultado = new StringBuilder() ①def error = new StringBuilder()

def comando = "ls -lt".execute() ②comando.consumeProcessOutput(resultado, error) ③comando.waitForOrKill(1000) ④

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

}

101 Groovy Script

| 31

Llamadas entre scriptsMiguel Rueda <[email protected] [mailto:[email protected]]>2017-08-31

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

A continuación vamos a crear un script llamado Configuration.groovy el cual contendrátoda la infomación necesaria para conectarnos a una base de datos de nuestro entorno.Esta configuración podrá ser llamada desde cualquier de nuestros scripts, por lo tanto deesta manera evitaremos tener que copiar y pegar esa parte de código para como en estecaso a una base de datos de nuestro contexto.

Configuration.groovy

import Sql

class Configuration {

  String user = "user" ①

  String passwd = "passwd" ②

  String server = "localhost" ③

  String database = "test" ④

  String url = "jdbc:mysql://$server:3306/$database?jdbcCompliantTruncation=false" ⑤

  def instanceMysql(){ ⑥

  return Sql.newInstance( "jdbc:mysql://$server:3306/$database?jdbcCompliantTruncation=false",

"$user", "$passwd", "com.mysql.jdbc.Driver")

  }

  def instanceMysql(my_user,my_passwd,my_server,my_database){ ⑦

  return Sql.newInstance(

"jdbc:mysql://$my_server:3306/$my_database?jdbcCompliantTruncation=false", "$my_user", "$my_passwd",

"com.mysql.jdbc.Driver")

  }

}

① Método que nos devuelve usuario para conectarnos.

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

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

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

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

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

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

Para obtener una instancia podemos realizarlo de la siguiente manera:

101 Groovy Script

32 |

Script.groovy

def sql = Configuration.instanceMysql()

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

OtroScript.groovy

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

101 Groovy Script

| 33

Script

import Sql

class Configuration {

  String user = "user" ①

  String passwd = "passwd" ②

  String server = "localhost" ③

  String database = "test" ④

  String url = "jdbc:mysql://$server:3306/$database?jdbcCompliantTruncation=false" ⑤

  def instanceMysql(){ ⑥

  return Sql.newInstance( "jdbc:mysql://$server:3306/$database?jdbcCompliantTruncation=false",

"$user", "$passwd", "com.mysql.jdbc.Driver")

  }

  def instanceMysql(my_user,my_passwd,my_server,my_database){ ⑦

  return Sql.newInstance(

"jdbc:mysql://$my_server:3306/$my_database?jdbcCompliantTruncation=false", "$my_user", "$my_passwd",

"com.mysql.jdbc.Driver")

  }

}

101 Groovy Script

34 |

WithBatchMiguel Rueda <[email protected] [mailto:[email protected]]>2017-08-31

A veces nos surge que tenemos que extraer los datos de una base de datos e insertarlas enotra base de datos (o tabla). Si simplemente fuera una extracción y una carga tu motor dedatos probablemente incluya alguna herramienta import/export, pero si tienes querealizar 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 óptimade 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 uncampo e insertar los registros en modo batch

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

@Grab('mysql:mysql-connector-java:5.1.6')①

@GrabConfig(systemClassLoader=true)

import groovy.sql.Sql

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

"password", "com.mysql.jdbc.Driver")②

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

"password", "com.mysql.jdbc.Driver")

batchSize=20

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

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

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

  ps.addBatch(row) ⑥

  }

}

① Usamos Grape para cargar las dependencias

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

③ Preparamos un insert en modo batch de batchSize elementos

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

⑤ Hacemos nuestra transformacion de negocio, por ejemplo, pasar a mayuscula yrevertir la cadena

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

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

101 Groovy Script

| 35

Script

@Grab('mysql:mysql-connector-java:5.1.6')①

@GrabConfig(systemClassLoader=true)

import groovy.sql.Sql

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

"password", "com.mysql.jdbc.Driver")②

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

"password", "com.mysql.jdbc.Driver")

batchSize=20

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

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

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

  ps.addBatch(row) ⑥

  }

}

101 Groovy Script

36 |

De Properties a YMLJorge Aguilera <[email protected] [mailto:jorge.aguilera@puravida-

software.com]> 2018-03-07

Cuando tu script es sencillo y requiere unos pocos parámetros de configuraciónprobablemente te sea suficiente con interpretar la linea de comando, usando por ejemploCliBuilder . Sin embargo cuando la configuración se vuelve más compleja esta opción noresulte cómoda y la opción más obvia es utilizar un fichero properties de Java donde cadalínea es un clave=valor.

Aunque es una opción válida y muy usada resulta muy oscura y propensa al error. En estepost vamos a ver lo sencillo que es utilizar un formato más amigable para el humano yque permite expresar mucho mejor configuraciones "de niveles" llamado YAMLhttps://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 puedeser como la siguiente:

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

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

  - username: otro  password: pazzz

En este ejemplo simplemente leemos un fichero de texto y si no está bien formado elparseador generará una excepción. Una vez cargado el fichero podemos navegar a travésdel mapa usando las características de groovy:

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

• recorrer un array con each o eachWithIndex

101 Groovy Script

| 37

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

Yaml parser = new Yaml()

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

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

println config.dataSources?.development?.url

println config.dataSources?.production?.url

config.logins.eachWithIndex{ user, idx->  println "index $idx:"  println "$user.username = $user.password"}

101 Groovy Script

38 |

Script

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

Yaml parser = new Yaml()

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

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

println config.dataSources?.development?.url

println config.dataSources?.production?.url

config.logins.eachWithIndex{ user, idx->  println "index $idx:"  println "$user.username = $user.password"}

101 Groovy Script

| 39

Consumo de recursos con DSLsJorge Aguilera <[email protected] [mailto:jorge.aguilera@puravida-

software.com]> 2018-08-26

Hace unos días surgió en un conversación si había comprobado el consumo de recursosen una aplicación que hiciera un uso intensivo de DSL (Domain Specific Language)porque mi interlocutor estaba seguro que había perdidas de memoria incluso reportadasy sin solución. Así que me puse manos a la obra para comprobarlo

Primer intentoEl primer intento de comprobarlo fue mediante este simple script

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

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

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

y utilizando jconsole comprobé alarmado que era verdad: El consumo de memoriacrecía sin parar.

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

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

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

101 Groovy Script

40 |

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

void executeScript(){  dsl.run()}

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

  executeScript()

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

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

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

En este pequeño ejemplo implementamos esta idea:

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

101 Groovy Script

| 41

dsls = [

"println new Date()",

"println 1",

"""

println new Random().with {(1..9).collect {(('a'..'z')).join()[

nextInt((('a'..'z')).join().length())]}.join()}

"""

]

database = [:]

dsls.each{

  database[it] = new GroovyShell().parse(it)

}

void executeDSL( int idx ){

  database[ dsls[idx] ].run()

}

// wait to jconsole

sleep 1000*10

// run a lot of times

(0..100000).each{

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

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

  sleep 2000

  println "liberando "

  System.gc()

  sleep 2000

  }

}

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

101 Groovy Script

42 |

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

101 Groovy Script

| 43

Script

dsls = [

"println new Date()",

"println 1",

"""

println new Random().with {(1..9).collect {(('a'..'z')).join()[

nextInt((('a'..'z')).join().length())]}.join()}

"""

]

database = [:]

dsls.each{

  database[it] = new GroovyShell().parse(it)

}

void executeDSL( int idx ){

  database[ dsls[idx] ].run()

}

// wait to jconsole

sleep 1000*10

// run a lot of times

(0..100000).each{

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

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

  sleep 2000

  println "liberando "

  System.gc()

  sleep 2000

  }

}

101 Groovy Script

44 |

Empaquetado ScriptsJorge Aguilera <[email protected] [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 talforma que sean accesibles al resto del equipo (o del mundo) de una forma cómoda ysencilla.

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

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

• Scripts de utilidad directa

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

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

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

• etc

Directorio compartidoLa primera solución que se nos presenta para poder reutilizar nuestros scripts esubicarlos en una carpeta compartida, bien sea en una carpeta de red por ejemplo SAMBAo 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 carpetase encuentra versionada (SVN, Git, etc) es muy fácil tenerla actualizada

Obviamente una desventaja de estos scripts es que estarán restringuidos a ejecutarse enel entorno de esta máquina. Es decir si por ejemplo tenemos un script que escanea undirectorio buscando un fichero determinado para trabajar con él sólo tendría sentidobuscarlo en esta máquina y no en la nuestra por ejemplo.

Alojado en SVN/GitOtra opción es disponer de un SCM (source control manager) tipo subversion SVN, git osimilar donde los usuarios puedan clonarse el proyecto en sus máquinas y ejecutarlosdesde ellas.

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

101 Groovy Script

| 45

Sin embargo, a parte de que necesitamos instalar el control de versiones en la máquinadonde queramos ejecutar los scripts, tendríamos que revisar que hacemos el pull deforma habitual etc

Servidor HTTPGracias a la capacidad de Groovy de poder ejecutar scripts en URL remotas podemoshacer que los scripts se alojen en un servidor web (Apache, Nginx, o similar) y que losusuarios 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 ejecutarscripts que residen en un servidor web a la vez que le pasamos argumentos

JarUtilizando la capacidad de Groovy de poder ejecutar scripts contenidos en un zip/jarpodemos agrupar nuestros ficheros con un simple comando:

java cvf mis_scripts.jar *.groovy ①groovy jar:file:mis_scripts.jar'!'/FindFile.groovy ②

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

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

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

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

Estructura directorios

Establece una estructura de directorios como el de la imagen. Nuestros scripts seubicarán en el directorio resources

101 Groovy Script

46 |

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

Gradle

En el directorio raiz mis_scripts copia el siguiente fichero:

build.gradle

buildscript {

  repositories {

  jcenter()

  }

  dependencies {

  classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.7.3'

  }

}

apply plugin: 'groovy'

apply plugin: 'java-library-distribution'

apply plugin: 'maven'

apply plugin: 'maven-publish'

apply plugin: 'com.jfrog.bintray'

repositories {

  mavenCentral()

}

dependencies {

  compile 'org.codehaus.groovy:groovy-all:2.4.12'

  testCompile 'junit:junit:4.12'

}

javadoc {

  title = "$project.description $project.version API"

}

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

  classifier = 'sources'

  from sourceSets.main.allSource

}

task javadocJar(type: Jar) {

  classifier = "javadoc"

  from javadoc

101 Groovy Script

| 47

}

distZip.shouldRunAfter(build)

publishing {

  publications {

  maven(MavenPublication) {

  artifactId 'mis-scripts' ①

  from components.java

  artifact sourceJar {

  classifier "sources"

  }

  }

  }

}

bintray {

  user = System.getenv("BINTRAY_USER") ?: project.hasProperty("bintrayUser") ? project.bintrayUser : '' ②

  key = System.getenv("BINTRAY_KEY") ?: project.hasProperty("bintrayKey") ? project.bintrayKey : ''

  publications = ['maven']

  publish = true

  pkg {

  repo = "mi-repositorio" ③

  userOrg = project.hasProperty('userOrg') ? project.userOrg : 'mi-organization' ④

  name = "mis-scripts"

  desc = "Groovy Scripts"

  websiteUrl = "https://tuwebsite.com"

  licenses = ['Apache-2.0']

  publicDownloadNumbers = true

  version {

  name = project.version

  }

  }

}

① Indica el nombre del artefacto que quieres publicar en Bintray

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

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

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

Publicando

Para empaquetar y publicar tu artefacto simplemente ejecuta:

gradle bintrayUpload

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

"Grapeando"

A partir de aqui nuestro artefacto es accesible mediante cualquier gestor de dependenciascomo puede ser Maven o Gradle y por supuesto Grape por lo que podremos crear nuevos

101 Groovy Script

48 |

scripts que dependan de nuestro artefacto simplemente poniendo las "coordenadas"(repositorio, organización, artefacto y versión) oportunas en el script

101 Groovy Script

| 49

Script

println "Hola mundo"

101 Groovy Script

50 |

Extraer una Table HTML a CSVJorge Aguilera <[email protected] [mailto:jorge.aguilera@puravida-

software.com]> 2018-06-24

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

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

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

Básicamente el script solicita tres argumentos:

• URL a parsear

• Nombre del fichero a generar

• Identificador de la tabla a descargar

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

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

101 Groovy Script

| 51

Script

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

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

101 Groovy Script

52 |

Instalación y primeros pasosMiguel Rueda <[email protected] [mailto:[email protected]]>2017-08-02

Groovy es un lenguaje de programación dinámico para la máquina virtual de java (JavaVirtual Machine). Esto quiere decir que el código que genera puede ser ejecutado por elRuntime de Java y por ello se requiere tener instalado Java 6+ (recomendado Java 8 y muypronto Java 9)

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

WindowsExiste un instalador de Windows basado en NSIS que nos permitirá instalar y desinstalarGroovy 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>

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

Con el sdkman en nuestro sistema realizar desde la consola.

sdk install groovy

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

101 Groovy Script

| 53

groovy -version

GroovyConsole

Ya podemos lanzar nuestra primera consola de groovy

groovyConsole

En pocos segundos se abrirá nuestra consola de groovy donde podemos empezar atrabajar con los primeros scripts

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

HolaMundo.groovy

println "Hola mundo"

Mediante el menú de GroovyConsole podemos ejecutar el script y visualizar el resultadoen el panel inferior.

101 Groovy Script

54 |

Consola

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

groovy HolaMundo.groovy

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

101 Groovy Script

| 55

Script

println "Hola mundo"

101 Groovy Script

56 |

Métodos últiles para trabajar con listasMiguel Rueda <[email protected] [mailto:[email protected]]>2017-11-29

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

Estos objetos los puedes encontrar diariamente en tu desarrollo de diferentes maneraspueden ser: los datos que devuelve una consulta sql, un criteria, un JSON o un simplefichero de configuración fuera de tu aplicaciones que contiene una lista con parámetrosde configuración… Es decir, te va tocar sí o sí trabajar con ellas por lo tanto es siempre útilconocer métodos simples para trabajar con ellas de la manera más simple y eleganteposible.

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

Recorrer una lista con hilos.

Para ello debemos utilizar el plugin:

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

Una vez realizado será tan sencillo como:

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

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

De dos listas obtener los valores semejantes.

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

  lista_1.intersect(lista_2)

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

De dos listas obtener los valores diferentes.

101 Groovy Script

| 57

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

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

El método intersect nos proporciona los elementos iguales (tanto key como value), queestán en ambas listas. Este método no nos serviría para el caso anterior ya que en élpedí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 laoriginal más la lista que pasamos por parámetro.

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

Ordenar una lista

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

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

Agrupar una lista

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

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

Eliminar valores de una lista

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

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

Obtener valores no repetidos

  def list_unique = list.unique()

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

101 Groovy Script

58 |

Script

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

import static groovyx.gpars.GParsPool.withPool

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

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

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

101 Groovy Script

| 59

  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){

101 Groovy Script

60 |

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

101 Groovy Script

| 61

Contar registros de base de datosJorge Aguilera, [email protected] [mailto:jorge.aguilera@puravida-

software.com] 2017-08-26

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

Como sysadmin te interesa saber en un momento determinado cúantos servidores seencuentran caídos desde un momento determinado (desde hace 1 minuto, 1 hora, 1 día …it’s up to you) para poder determinar si tienes problemas.

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

@Grab('mysql:mysql-connector-java:5.1.6')①

@GrabConfig(systemClassLoader=true)

import groovy.sql.Sql

sql=Sql.newInstance(

  "jdbc:mysql://localhost/granja", ②

  "user", "password", "com.mysql.jdbc.Driver" ③

)

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

println row?.caidos ⑤

① Cargamos la dependencia al driver MySQL

② Indicamos el host y la base de datos alojada

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

④ Buscamos el primer registro que cumpla la condicion. Fijate que la query esparametrizable con ?

⑤ Si obtenemos resultados lo imprimimos por pantalla

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

101 Groovy Script

62 |

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 ⑤

101 Groovy Script

| 63

GORM en tus scriptsJorge Aguilera, [email protected] [mailto:jorge.aguilera@puravida-

software.com] 2018-03-30

GORM (Groovy o Grails?) Object Relational Mapping,http://gorm.grails.org/, es la librería pertenciente al proyecto Grails quepermite el acceso y gestión de bases de datos. En un principio sólocontemplaba Hibernate pero con las últimas versiones permite acceder abases de datos de grafos como Neo4J, NoSQL como MongoDB, etcmanteniendo la misma sintáxis y funcionalidades

En este artículo vamos a desarrollar un Groovy Script simple donde veremos quepodemos utilizar todas las funcionalidades de GORM (creación y mantenimiento de lastablas, 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.Sqlimport groovy.transform.ToStringimport org.grails.orm.hibernate.HibernateDatastoreimport grails.gorm.annotation.Entity

DomainNuestros objetos de Dominio constan de Biblioteca y Libro donde el primero contiene unarelación de los segundos

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

101 Groovy Script

64 |

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

@Entity@ToStringclass 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 objetosasí como recuperarlos. Por simplificar este script no utiliza -validators_ ni otrasfuncionalidades de GORM.

ConfiguraciónPuesto que nuestro script va a utilizar Hibernate y una persistencia SQLite debemosconfigurar 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 queHibernate gestione (las marcadas como @Entity)

Crear una Biblioteca

101 Groovy Script

| 65

void prepareBiblioteca() {  Biblioteca.withTransaction {

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

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

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

Como puedes ver usamos un método withTransaction que NO hemos declarado ennuestra @Entity Biblioteca pero que GORM le ha añadido.

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

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

• delete() para eliminarlo de la base de datos

• count() para saber cuantos tenemos

• save() para persistir un objeto de dominio

• etc

Añadir Libros

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

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

  b.save()  }}

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

101 Groovy Script

66 |

Consultar la bibliotecaRealizar consultas a la base de datos es realmente sencillo y sobre todo con GORM nosolvidamos de sentencias SELECT:

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

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

  }  }

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

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

  List list2 = Biblioteca.withCriteria{ ⑥  eq 'nombre', 'Biblioteca Nacional'  }  assert list2.size()  }}

① similar a select count(*) from biblioteca

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

③ similar a select * from libros where biblioteca_id = ?

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

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

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

Test

101 Groovy Script

| 67

initDatastore()

prepareBiblioteca()

addLibros()

println '-'.multiply(20)

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

101 Groovy Script

68 |

Script

//tag::dependencias[]

@GrabConfig(systemClassLoader=true)

@Grab('org.slf4j:slf4j-api:1.7.10')

@Grab('org.xerial:sqlite-jdbc:3.21.0')

@Grab('org.grails:grails-datastore-gorm-hibernate5:6.1.6.RELEASE')

@Grab('com.enigmabridge:hibernate4-sqlite-dialect:0.1.2')

import groovy.sql.Sql

import groovy.transform.ToString

import org.grails.orm.hibernate.HibernateDatastore

import grails.gorm.annotation.Entity

//end::dependencias[]

//tag::domain[]

@Entity

@ToString

class Libro {

  String codigo

  String titulo

  static constraints = {

  codigo unique:true

  }

}

@Entity

@ToString

class Biblioteca{

  String nombre

  static hasMany = [ libros : Libro]

}

//end::domain[]

//tag::config[]

Map getConfiguration(){

  [

  'dataSource.url' :'jdbc:sqlite:example.db',

  'dataSource.drive' :'org.sqlite.JDBC',

  'hibernate.hbm2ddl.auto' : 'update',

  'hibernate.dialect' :'com.enigmabridge.hibernate.dialect.SQLiteDialect'

  ]

}

HibernateDatastore initDatastore(){

  new HibernateDatastore( configuration, Biblioteca, Libro)

}

//end::config[]

//tag::biblio[]

void prepareBiblioteca() {

  Biblioteca.withTransaction {

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

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

101 Groovy Script

| 69

  }

}

//end::biblio[]

//tag::libros[]

void addLibros(){

  Biblioteca.withTransaction{

  Biblioteca b = Biblioteca.first()

  assert b

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

  b.save()

  }

}

//end::libros[]

//tag::list[]

void list(){

  Biblioteca.withTransaction{

  assert Biblioteca.count() ①

  def list = Biblioteca.list() ②

  list.each{ Biblioteca b->

  println "Biblioteca $b.nombre ($b.id): "

  b.libros.each{ Libro l-> ③

  println "\t $l.codigo con titulo $l.titulo"

  }

  }

  }

  Biblioteca.withNewSession{

  Biblioteca b = Biblioteca.findByNombre('Biblioteca Nacional') ④

  assert b

  println "Biblioteca encontrada $b"

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

  assert list.size()

  List list2 = Biblioteca.withCriteria{ ⑥

  eq 'nombre', 'Biblioteca Nacional'

  }

  assert list2.size()

  }

}

//end::list[]

//tag::main[]

initDatastore()

prepareBiblioteca()

addLibros()

println '-'.multiply(20)

101 Groovy Script

70 |

list()

println '-'.multiply(20)

//end::main[]

101 Groovy Script

| 71

Docker y Groovy (básico)Jorge Aguilera <[email protected] [mailto:jorge.aguilera@puravida-

software.com]> 2017-10-09

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

El proyecto se encuentra alojado en https://github.com/groovy/docker-groovy y la(s)imagen(es) las puedes encontrar en el repo oficial de docker https://hub.docker.com/_/groovy/

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

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

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

Lo interesante es que en ese momento estás ejecutando un contenedor de docker congroovy instalado y con todas las características de cualquier contenedor: network,volúmenes, incluirlo en un docker-compose, etc.

Hello DockerEn el siguiente ejemplo vamos a comprobar que efectivamente se ejecuta en uncontenedor mediante un HolaDocker:

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

Si el comando se ejecuta correctamente deberías ver en consola las variables de entornopropias del contenedor incluida la que proporcionamos por argumento. Por ejemploHOSTNAME indicará el nombre que le ha asignado docker a tu imagen (y que noencontrarás después porque le hemos indicado con el argumento rm que la elimine alfinalizar)

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

101 Groovy Script

72 |

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

Si aprovechamos esta característica podemos ubicar nuestros scripts en un directorio delhost e indicar al contenedor que los ejecute en su entorno. Para ello nos descargamos elscript /scripts/docker/DockerBasico.groovy en un directorio de nuestra maquina yejecutamos desde ese directorio:

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

DockerBasico.groovy -a ①

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

Este script simplemente ejecuta:

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

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

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

Para ello ejecutaremos:

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

DockerBasico.groovy -d ①

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

Este script simplemente ejecuta:

101 Groovy Script

| 73

if( options.d){

  def json = new groovy.json.JsonSlurper().parse(new URL("https://dog.ceo/api/breed/hound/images/random") )

  if(json.status=='success'){

  new File('perrito.jpg').bytes = new URL(json.message).bytes

  }

}

Si el script se ejecuta correctamente deberías tener un fichero nuevo en tu directorioperrito.jpg que se regenera con una imagen nueva cada vez que ejecutes el script.

101 Groovy Script

74 |

Script

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

cli.with { ①

  h(longOpt: 'help', 'Usage Information \n', required: false)

  a(longOpt: 'Hello','Al seleccionar "a" te saludara ', required: false)

  d(longOpt: 'Dogs', 'Genera imagenes de perros', required:false)

}

def options = cli.parse(args)

if (!options || options.h) {

  cli.usage

  return

}

//tag::hello[]

if (options.a) {

  println "------------------------------------------------------------------"

  println "Hello"

  System.getenv().each{

  println it

  }

  println "------------------------------------------------------------------"

}

//end::hello[]

//tag::dogs[]

if( options.d){

  def json = new groovy.json.JsonSlurper().parse(new URL("https://dog.ceo/api/breed/hound/images/random") )

  if(json.status=='success'){

  new File('perrito.jpg').bytes = new URL(json.message).bytes

  }

}

//end::dogs[]

101 Groovy Script

| 75

Ejecución programadaJorge Aguilera <[email protected] [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 deGroovy para Docker ejecute nuestros scripts en host donde no se encuentre instaladoGroovy. Así mismo esta imagen nos permite aprovechar todas las características deDocker como montar volúmenes, conexión entre contenedores, etc de tal forma quenuestros scripts puedan interactuar con otros contenedores y/o sistema anfitrión.

Si pensamos un poco sobre ello vemos que podemos utilizar estas características paraejecutar de forma programada nuestros scripts incluso desde un servidor remoto, de talforma 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 yque accederá a la información del Ayuntamiento de Madrid [https://datos.madrid.es/portal/site/egob/menuitem.c05c1f754a33a9fbe4b2e4b284f1a5a0/?vgnextoid=255e0ff725b93410VgnVCM1000000b205a0aRCRD&

vgnextchannel=374512b9ace9f310VgnVCM100000171f5a0aRCRD&vgnextfmt=default] sobreincidencias 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 scriptbuscará aquellas que no han sido planificadas y las irá tuiteando una a una.

Para la ejecución recurrente aprovecharemos la posibilidad que ofrece Gitlab deejecutar Pipelines de forma planificada de tal forma que nuestro código seráejecutado por un runner cuando así lo determinemos.

Así pues deberás tener:

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

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

101 Groovy Script

76 |

IncidenciasMadrid.groovy

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

import twitter4j.TwitterFactory

import twitter4j.StatusUpdate

import twitter4j.conf.ConfigurationBuilder

tf = TwitterFactory.singleton

if( new File('twitter4j.properties').exists() == false ){ ①

  def env = System.getenv()

  ConfigurationBuilder cb = new ConfigurationBuilder()

  cb.setDebugEnabled(true)

  .setOAuthConsumerKey(env['CONSUMER_KEY'])

  .setOAuthConsumerSecret(env['CONSUMER_SECRET'])

  .setOAuthAccessToken(env['ACCESS_TOKEN'])

  .setOAuthAccessTokenSecret(env['ACCESS_SECRET']);

  tf = new TwitterFactory(cb.build())

}

twitter = tf.instance

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

NewDataSet = new XmlSlurper().parse(body) ②

NewDataSet.Incidencias.each{

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

  String tweet="""

@101GroovyScript te informa

Atención, incidencia no prevista$it.nom_tipo_incidencia:

$it.descripcion

"""

  try{

  twitter.updateStatus tweet ③

  } catch(e){

  println "no se ha enviado"

  }

  }

}

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

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

③ tuiteamos un mensaje con el contenido de cada una.

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

101 Groovy Script

| 77

gitlab-ci.yml

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

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

② usaremos la imagen oficial de Groovy para ejecutar nuestro script

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

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

PlanificaciónDesde la consola web de Gitlab podemos configurar cuando queremos que se ejecute elPipeline 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 autilizar:

ResultadoComo resultado tendremos que cada cierto tiempo Twitter publicará las incidenciasusando nuestra cuenta como en este ejemplo:

101 Groovy Script

78 |

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

101 Groovy Script

| 79

Script

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

import twitter4j.TwitterFactory

import twitter4j.StatusUpdate

import twitter4j.conf.ConfigurationBuilder

tf = TwitterFactory.singleton

if( new File('twitter4j.properties').exists() == false ){ ①

  def env = System.getenv()

  ConfigurationBuilder cb = new ConfigurationBuilder()

  cb.setDebugEnabled(true)

  .setOAuthConsumerKey(env['CONSUMER_KEY'])

  .setOAuthConsumerSecret(env['CONSUMER_SECRET'])

  .setOAuthAccessToken(env['ACCESS_TOKEN'])

  .setOAuthAccessTokenSecret(env['ACCESS_SECRET']);

  tf = new TwitterFactory(cb.build())

}

twitter = tf.instance

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

NewDataSet = new XmlSlurper().parse(body) ②

NewDataSet.Incidencias.each{

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

  String tweet="""

@101GroovyScript te informa

Atención, incidencia no prevista$it.nom_tipo_incidencia:

$it.descripcion

"""

  try{

  twitter.updateStatus tweet ③

  } catch(e){

  println "no se ha enviado"

  }

  }

}

101 Groovy Script

80 |

Mantenimiento de un Registry deDockerJorge Aguilera <[email protected] [mailto:jorge.aguilera@puravida-

software.com]> 2018-02-10

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

preocupado mucho por el tema de cuantas imágenes has subido a tu

repositorio. De hecho probablemente te guste tener todo el historial de las

mismas por si alguien las sigue usando. Sin embargo cuando utilizas un

repositorio privado el asunto cambia porque estos servicios suelen cobrar

por el número de imágenes que tienes alojadas en ellos. Así que si quieres

tener "controlada" la factura te toca hacer revisiones periódicas y eliminar

aquellas imágenes que consideras obsoletas.

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

DependenciasDocker Hub ofrece un servicio REST que te permite autentificarte y gestionar losrepositorios, imágenes, tags, etc alojados en el espacio del usuario. Para esta gestión RESTvamos 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 marcadoincluidas en el propio Groovy usando MarkupBuilder (http://docs.groovy-lang.org/docs/groovy-latest/html/api/groovy/xml/MarkupBuilder.html)

101 Groovy Script

| 81

@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=trueDELETE=false ②TRESHOLD=4 ③

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

① Si queremos que genere el HTML de report

② Si queremos que borre automaticamente las imagenes obsoletas

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

④ Primer argumento es el usuario para hacer login

⑤ Segundo argumento es la password para hacer login

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

Autorización

101 Groovy Script

82 |

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

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

① Configuramos la peticion para enviar un json

② Realizamos un GET con un json en el body

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

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

Iterar repositorios

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

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

② Para cada result extraemos un subconjunto de valores que nos interesan como elnombre y la última actualización

③ Ordenamos la lista alfabéticamente

Obtener Tags

101 Groovy Script

| 83

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 elnombre y la última actualización

③ Ordenamos la lista de más reciente a más antigüo

④ Si la lista sobrepasa el límite marcado marcamos los más antigüos como candidatospara ser eliminados

ReportSi así lo indicamos el script creará un HTML a modo de report ( ver ejemplo en este enlaceDocker Report [./../scripts/docker/docker_report.html] ) que puede ser enviado por correoelectrónico por ejemplo. Para generar el report, simplemente usaremos MarkupBuilderrecorriendo la lista de repos y para cada repo la lista de tags. Así mismo para los tagscandidatos a ser eliminados los crearemos en una sección diferente para ayudar a suidentificación (un h3 en este caso)

101 Groovy Script

84 |

  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() }

AsciidoctorOtra posibilidad para generar el report es usando Asciidoctor (ver ejemplo en este enlaceDocker Report en Asciidoctor [./../scripts/docker/asciidoc_docker_report.html] ).

Para generar el report, simplemente crearemos un fichero de forma dinámica siguiendola sintaxis adecuada recorriendo la lista de repos y para cada repo la lista de tags. Asímismo para los tags candidatos a ser eliminados los crearemos en una sección diferentepara ayudar a su identificación

101 Groovy Script

| 85

  file = new File("/tmp/asciidoc_docker_report.adoc") ①

  file.newWriter().withPrintWriter { mainWriter ->

  mainWriter.println "= Docker Hub status"

  mainWriter.println "Jorge Aguilera <[email protected]>"

  mainWriter.println new Date().format('yyyy-MM-dd')

  mainWriter.println ":icons: font"

  mainWriter.println ":toc: left"

  mainWriter.println ""

  mainWriter.println """

[abstract]

Report status of repository *${namespace}* at *${new Date().format('yyyy-MM-dd HH:mm')}*

  """

  repos.each { repo -> ②

  mainWriter.println "\n== ${repo.name}"

  mainWriter.println "Last update: ${repo.lastUpdate.format('yyyy-MM-dd HH:mm')}"

  mainWriter.println ""

  repo.tags.findAll { !it.toRemove }.each { tag ->

  mainWriter.println "- ${tag.name} ${tag.lastUpdate.format('yyyy-MM-dd HH:mm')}"

  }

  if (repo.tags.find { it.toRemove }) {

  mainWriter.println "\n=== Candidates to remove"

  }

  repo.tags.findAll { it.toRemove }.each { tag ->

  mainWriter.println "- ${tag.name} ${tag.lastUpdate.format('yyyy-MM-dd HH:mm')}"

  }

  }

  }

  asciidoctor = Factory.create();

  attributes = AttributesBuilder.attributes().

  sectionNumbers(true).

  sourceHighlighter("coderay").

  get()

  options = OptionsBuilder.options().

  backend('html').

  attributes(attributes).

  safe(SafeMode.UNSAFE).

  get()

  asciidoctor.convertFile(file, options) ③

① Creamos el fichero y especificamos su cabecera

② Creamos secciones para cada repo

③ Invocamos a Asciidoctor para generar el fichero html

LimpiezaSi queremos que el script realize la limpieza de aquellas imágenes que consideramosobsoletas simplemente lo configuraremos para ello de tal forma que el script puedainvocar la(s) llamada(s) REST DELETE oportuna(s).

101 Groovy Script

86 |

  repos.each{ repo->

  repo.tags.findAll{it.toRemove}.each{ tag->

  dockerHub.delete {

  request.uri.path="/v2/repositories/$namespace/$repo.name/tags/$tag.name"

  }

  }

  }

101 Groovy Script

| 87

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

}

101 Groovy Script

88 |

//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 <[email protected]>"

  mainWriter.println new Date().format('yyyy-MM-dd')

  mainWriter.println ":icons: font"

  mainWriter.println ":toc: left"

  mainWriter.println ""

  mainWriter.println """

101 Groovy Script

| 89

[abstract]

Report status of repository *${namespace}* at *${new Date().format('yyyy-MM-dd HH:mm')}*

  """

  repos.each { repo -> ②

  mainWriter.println "\n== ${repo.name}"

  mainWriter.println "Last update: ${repo.lastUpdate.format('yyyy-MM-dd HH:mm')}"

  mainWriter.println ""

  repo.tags.findAll { !it.toRemove }.each { tag ->

  mainWriter.println "- ${tag.name} ${tag.lastUpdate.format('yyyy-MM-dd HH:mm')}"

  }

  if (repo.tags.find { it.toRemove }) {

  mainWriter.println "\n=== Candidates to remove"

  }

  repo.tags.findAll { it.toRemove }.each { tag ->

  mainWriter.println "- ${tag.name} ${tag.lastUpdate.format('yyyy-MM-dd HH:mm')}"

  }

  }

  }

  asciidoctor = Factory.create();

  attributes = AttributesBuilder.attributes().

  sectionNumbers(true).

  sourceHighlighter("coderay").

  get()

  options = OptionsBuilder.options().

  backend('html').

  attributes(attributes).

  safe(SafeMode.UNSAFE).

  get()

  asciidoctor.convertFile(file, options) ③

  //end::asciidoctor[]

}

if( DELETE ){

//tag::delete[]

  repos.each{ repo->

  repo.tags.findAll{it.toRemove}.each{ tag->

  dockerHub.delete {

  request.uri.path="/v2/repositories/$namespace/$repo.name/tags/$tag.name"

  }

  }

  }

//end::delete[]

}

101 Groovy Script

90 |

Biblioteca (docudocker)Miguel Rueda <[email protected] [mailto:[email protected]]> 2018-03-05

A continuación se describe el caso de uso que vamos a desarrollar: Un cliente nos trasladala necesidad de crear una biblioteca de documentos, donde colgar manuales o notasinformativas para sus empleados. Esta información cambia de manera mensual, tanto elcontenido como la estructura de directorios. Además debe de correr en cualquier sistemaoperativo 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 contenidoestático, donde habremos copiado los documentos proporcionados por el clientegenerando un image versionada con esta información.

Así pues nuestro sistema debe de realizar los siguientes pasos:

Un cliente proporciona la nueva documentación para la biblioteca, esta esactualizada en el servidor preparado para ella (con la función scpDocuments). Con lainformación ya en el servidor lanzamos la tareas de gradle (createImage) y con elloobtenemos la imagen docker con nuestra biblioteca de documentos versionada en elrepositorio en el que estemos trabajando.

Herramientas que vamos a emplear.• DocuDocker [https://gitlab.com/jorge.aguilera/static-documents]:

101 Groovy Script

| 91

En este enlace @jagedn explica paso a paso como crear una biblioteca con docker y jbakeutilizando el proyecto static-documents.

 https://www.youtube.com/watch?v=5x8i9Ft_29Y (YouTube video)

Dentro de este proyecto podemos encontrar el fichero Dockerfile que contiene la "receta"para generar nuestra imagen docker de la biblioteca:

FROM httpd:2.4EXPOSE 80COPY . /usr/local/apache2/htdocs/

Este Dockerfile parte de la imagen de docker httpd:2.4 y copia el contenido que tenemosen la ruta actual dentro de su servidor apache. Por lo tanto a la hora de actualizar losdocumentos debemos centrarnos en este paso. En el ejemplo sobre el que vamos atrabajar tiene la estructura: un directorio RRHH (que contiene Convenio_Actual.pdf) yotro Administración (que contiene Cierre_mensual.pdf)

La herramientas que utiliza este proyecto es gradle y para la creación del entorno webestático es JBake.

PreparaciónPara la creación de nuestra biblioteca debemos ejecutar los siguientes pasos:

• Bajarnos en el servidor que deseamos crear la biblioteca el proyecto static-documentspara posteriormente realizar las tareas de gradle:

git clone https://gitlab.com/jorge.aguilera/static-documents.git

• Con el proyecto bajado debemos copiar dentro del proyecto en el directoriosrc/documents/ la estructura de directorios(en nuestro caso RRHH y Administración )que deseamos encontrarnos en nuestra biblioteca , así como la información dentro decada directorio.

• Cuando tengamos recopilada toda la información, ejecutar las tareas de gradle paragenerar la imagen docker. Utilizaremos sshoogr [https://github.com/aestasit/sshoogr] ya queen este caso que vamos a ver la información estará en un servidor remoto.

Descripción del scriptA continuación vamos a pasar a describir el script encargado de realizar cada una de lasfunciones descritas anteriormente:

101 Groovy Script

92 |

Dependencias y configuración necesarias.

Estas son las que vamos a necesitar:

@Grab('com.aestasit.infrastructure.sshoogr:sshoogr:0.9.25')@GrabConfig(systemClassLoader=true)import static com.aestasit.infrastructure.ssh.DefaultSsh.*

options.trustUnknownHosts = true ①

① Desactivación de la comprobación estricta de la clave del host

Actualización de versión.

El primer paso es eliminar del fichero build.gradle del proyecto los campos version ytagVersion para impedir que en la creación de nuestra imagen tomen estas referencias ala hora de crear nuestra biblioteca. Nuestro script creará un fichero gradle.properties conla versión que hemos recibo por parametro y que será el tag de la imagen.

Utilizamos la función updateVersion que recibe por parámetro el servidor donde seencuentra el proyecto y la ruta del fichero gradle.properties que será actualizado.

  remoteSession("user:password@$server:22"){  remoteFile("$orig/gradle.properties").text = "version=$version"  }

Subida de nuevos documentos

Para este paso utilizaremos la función scpDocuments, a la cual le debemos indicar la rutadonde se encuentra la documentación por parámetro y la ruta en el servidor destino

  remoteSession("user:password@$server:22") {  scp{  from { localDir orig}  into { remoteDir dest}

  }  }

Creación de la imagen docuDockerYa solo nos queda ejecutar las tareas gradle build y pushDockerRegistry (para subirla anuestro repositorio). Para ello realizaremos:

101 Groovy Script

| 93

  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 puedeser descargada por los clientes para ver la última versión de los documentos

101 Groovy Script

94 |

Script

@Grab('com.aestasit.infrastructure.sshoogr:sshoogr:0.9.25')

@GrabConfig(systemClassLoader=true)

import static com.aestasit.infrastructure.ssh.DefaultSsh.*

options.trustUnknownHosts = true

String server = "src_server"

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

cli.with { (1)

  h(longOpt: 'help ','Usage Information \n', required: false)

  u(longOpt: 'Actualización ','-u version servidor ruta -> Ejemplo: -u v1 localhost', required:false)

  s(longOpt: 'Copiado de ficheros','-s servidor origen destino -> Ejemplo: -s localhost /tmp /home/docu',

required: false)

  c(longOpt: 'Crear Imagen ','-c servidor ruta -> Ejemplo: -c ruta', required: false)

}

def options = cli.parse(args)

if (!options) {

  return

}

if (options.h) {

  cli.usage()

  return

}

if (options.u) {

  if (options.arguments().size() < 3) {

  cli.usage()

  return

  }

  updateVersion(options.arguments()[0],options.arguments()[1],options.arguments()[2])

}

if (options.s) {

  if (options.arguments().size() < 3) {

  cli.usage()

  return

  }

  scpDocuments(options.arguments()[0],options.arguments()[1],options.arguments()[2])

}

if (options.c) {

  if (options.arguments().size() < 3) {

  cli.usage()

  return

  }

  createImage(options.arguments()[0],options.arguments()[1])

}

def updateVersion(version,server,orig){

  //tag::version[]

  remoteSession("user:password@$server:22"){

  remoteFile("$orig/gradle.properties").text = "version=$version"

  }

  //end::version[]

}

def createImage(server,src){

  //tag::create[]

  remoteSession("user:password@$server:22") {

  exec "cd $src; ./gradlew build"

  exec "cd $src; ./gradlew pushDockerRegistry"

  }

101 Groovy Script

| 95

  //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[]

}

101 Groovy Script

96 |

Tostando imágenesJorge Aguilera <[email protected] [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 deGroovy para Docker ejecute nuestros scripts en host donde no se encuentre instaladoGroovy. Así mismo esta imagen nos permite aprovechar todas las características deDocker como montar volúmenes, conexión entre contenedores, etc de tal forma quenuestros 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 propiasimágenes de tal forma que podamos subirlas a nuestro repositorio ( Hub Docker, nexusprivado, 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 mostrarpor pantalla el contenido de un fichero que le pasemos por parámetro y esperar 30segpara volver a repetir la acción (vamos lo que viene siendo a ser un bucle infinito de todala 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

DockerfileLo primero que vamos a necesitar es un fichero de instrucciones para que Docker puedacrear nuestra imagen. Este fichero suele tener el nombre de Dockerfile y para nuestroejemplo tiene esta pinta:

101 Groovy Script

| 97

FROM groovy:2.4.12-jre8-alpine

COPY WatchFile.groovy /home/groovy/

VOLUME ["/var/watchfile"]

ENTRYPOINT ["groovy", "WatchFile.groovy"]

CMD []

Mediante este fichero simplemente indicamos de qué imagen vamos a extender(groovy:2.4.12-jre8-alpine), copiamos nuestro script en la imagen e indicamos qué sedebe ejecutar al arrancar ( groovy WatchFile.groovy).

También preparamos nuestra imagen para que si no le proporcionamos parámetros deejecución el script lo detecte y nos pueda mostrar alguna ayuda de cómo ejecutarlo, orealizar una acción por defecto, (CMD [])

Así mismo vamos a montar un volumen particular para esta imagen en /var/watchfile, locual nos permitirá, por ejemplo, montar volúmenes de otros contenedores o del hostanfitrión para que nuestro script pueda "vigilarlo"

Generando la imagenDesde el directorio donde tengamos estos dos ficheros ejecutaremos:

docker build --rm -t jagedn/watchfile . ①

① jagedn/watchfile será el nombre de nuestra imagen. Fijate que el comando acaba en unpunto

Si todo va bien, en tu máquina existirá ahora una imagen jagedn/watchfile (o el nombreque le hayas dado)

Para comprobarlo puedes ejecutar

docker images

Ejecutando la imagenPara ejecutar la imagen simplemente haremos:

docker run --rm -it -v /un/path/cualquiera:/var/watchfile jagedn/watchfile /var/watchfile/mifichero.log ①

101 Groovy Script

98 |

Esto debe ejecutar un proceso que cada 30 segundos nos muestre el contenido demifichero.log

Como puedes ver, este ejemplo en sí no realiza nada interesante salvo que muestra lospasos a realizar para hacer que nuestro script se pueda distribuir y ejecutar en unentorno dockerizado por lo que es perfectamente integrable en pipelines de continuousdelivery por ejemplo.

Recuerda que para poder subir tu imagen a un repositorio simplemente deberás hacer unpush, por ejemplo:

docker push jagedn/watchfile

101 Groovy Script

| 99

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}

101 Groovy Script

100 |

De Excel a i18n y viceversaJorge Aguilera <[email protected] [mailto:jorge.aguilera@puravida-

software.com]> 2018-03-03

La mayoría de las aplicaciones Java utilizan la funcionalidad que ofrece para el manejo demensajes internacionalizados el cual se basa en una serie de ficheros properties quecomparten un nombre común más un sufijo que indica el idioma al que corresponden lastraducciones incluidas en el mismo.

Por ejemplo, supongamos que nuestra aplicación va a mostrar por defecto los mensajes eninglés pero necesitamos poder mostrarlos también en español y francés. Este caso en Javase corresponde con estos ficheros:

theapp.properties

login=Loginwelcome=Welcome

theapp_es.properties

login=Identificacionwelcome=Bienvenido

theapp_fr.properties

login=Identifierwelcome=Bienvenue

Cuando la aplicación crece, el número de mensajes a mostrar suele hacerlo también y nosvamos centrando en el fichero por defecto hasta tener la mayor cantidad posible deidentificadores. Si en este momento queremos realizar la traducción (o encargarsela aalguien) nos encontramos con una serie de ficheros incompletos e inconexos y aunqueexisten herramientas para facilitar la edición, estas no suelen ser del agrado de quientiene que traducirlos.

Mediante este script vamos a poder realizar dos procesos diferentes aunque relacionados:

• Partiendo de un conjunto de ficheros properties de traducciones incompletascrearemos un fichero Excel donde cada columna corresponderá a un idioma y cadafila a un elemento a traducir en cada idioma

• Partiendo de un excel con las traducciones completas crearemos un conjunto deficheros properties cada uno con las traducciones que le correspondan.

101 Groovy Script

| 101

Vamos a usar Apache POI para ambas situaciones pero para la escrituravamos a usar el DSL Groovy Excel Builder de James Kleeh parademostrar lo fácil que es escribir un excel con Groovy.

DependenciasDe 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 indicarsimplemente 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

ArgumentosComo hemos dicho el script podrá ejecutar dos acciones diferentes por lo que preparamosun CliBuilder que nos permita interpretar la línea de comandos y los argumentosproporcionados por el usuario:

101 Groovy Script

102 |

def cli = new CliBuilder(usage: 'groovy ExcelI18n.groovy -[h] -action generateProperties/generateExcel

filename.xls ')

cli.with { ①

  h(longOpt: 'help', 'Import/Export excel file to i18n properties files.', required: false)

  action('generateProperties generateExcel', required: true, args:1, argName:'action')

}

def options = cli.parse(args)

if (!options){

  return

}

if(options.h || options.arguments().size()==0) {

  cli.usage()

  return

}

if( options.action == 'generateProperties'){ ②

  return generateProperties(options.arguments()[0])

}

if( options.action == 'generateExcel'){ ③

  return generateExcel(options.arguments()[0])

}

cli.usage() ④

① preparamos las opciones disponibles

② de Excel a properties

③ de properties a Excel

④ si no hay acción correcta mostramos el uso

De i18n a ExcelPartiendo de una situación en la que tenemos un conjunto de traducciones incompletas loque pretendemos hacer es cargar todos estos ficheros en un Excel organizando por filas ycolumnas los códigos y los idiomas respectivamente.

Para determinar los idiomas que queremos manejar en nuestra aplicación hay que fijarseen que todos ellos siguen el patrón:

filename (_ i18code)? .properties , es decir un nombre de fichero común, un guión bajo yun código de idioma (por ejemplo es, fr, ca, etc) opcionales y una extension fija .properties

101 Groovy Script

| 103

void generateExcel(filename){

  File excelFile = new File(filename)

  String dir = excelFile.absolutePath.split(File.separator).dropRight(1).join(File.separator)

  String name = excelFile.name.split('\\.').dropRight(1).join('.')

  Properties defaultProperties = new Properties()

  defaultProperties.load( new File("${name}.properties").newInputStream() ) ①

  Map<String,Properties> propertiesMap = [:] ②

  new File(dir).eachFileMatch ~"${name}(_[A-Za-z]+)\\.properties", { ③

  def matcher = (it.name =~ "${name}(_[A-Za-z]+)\\.properties" )

  String lang = matcher[0][1]?.substring(1)

  Properties prp = new Properties()

  prp.load( it.newInputStream() )

  propertiesMap[ lang ] = prp ④

  }

  ExcelBuilder.output(new FileOutputStream(new File("${filename}"))) {

  sheet {

  row{

  cell("Code")

  cell("Default")

  propertiesMap.keySet().each { lang ->

  cell(lang.toUpperCase())

  }

  }

  defaultProperties.propertyNames().each{ String property-> ⑤

  row{

  cell(property)

  cell(defaultProperties[property])

  propertiesMap.keySet().each { lang ->

  cell(propertiesMap[lang][property])

  }

  }

  }

  }

  }

}

① cargar en un Properties el fichero por defecto (el cual contienen todos las keys atraducir)

② preparar un Map<String,Properties>

③ buscar los ficheros de traducciones particulares que cumplen el patrón explicado

④ cargar en el mapa el properties identificado por su código de idioma

⑤ crear un Excel donde cada key será una fila y cada idioma volcará su texto

De Excel a i18nUna vez completado el Excel con las traducciones adecuadas necesitarremos volver areescribir los ficheros properties

101 Groovy Script

104 |

void generateProperties(filename){

  File excelFile = new File(filename)

  String dir = excelFile.absolutePath.split(File.separator).dropRight(1).join(File.separator)

  String name = excelFile.name.split('\\.').dropRight(1).join('.')

  InputStream inp = new FileInputStream(excelFile)

  ①

  new File(dir).eachFileMatch ~"${name}(_[A-Za-z]+)?\\.properties", {

  it.delete()

  }

  List<String> languages = []

  Workbook wb = WorkbookFactory.create(inp)

  Sheet sheet = wb.getSheetAt(0)

  sheet.iterator().eachWithIndex{ Row row, int idx-> ②

  if( idx == 0){ ③

  languages = ['']+row.cellIterator().collect{ '_'+it.stringCellValue }.drop(2)*.toLowerCase()

  return

  }

  String code = row.getCell(0)

  languages.eachWithIndex{ String lang, int i -> ④

  String txt = row.getCell(i+1)?.stringCellValue

  if(txt)

  new File("${name}${lang}.properties") << "$code=$txt\n" ⑤

  }

  }

}

① limpiamos ficheros de traduccion si los hubiera

② iteramos por las filas del Excel

③ la primera fila nos indica los lenguajes que se contemplan además del por defecto

④ para cada lenguaje indicado en la primera fila buscamos si hay traducción en el excel

⑤ si tenemos traducción la concatenamos al fichero correspondiente al idioma

101 Groovy Script

| 105

Script

//tag::dependencies[]

@Grab('com.jameskleeh:excel-builder:0.4.2')

import org.apache.poi.ss.usermodel.*

import org.apache.poi.hssf.usermodel.*

import org.apache.poi.xssf.usermodel.*

import org.apache.poi.ss.util.*

import org.apache.poi.ss.usermodel.*

import com.jameskleeh.excel.ExcelBuilder

//end::dependencies[]

//tag::arguments[]

def cli = new CliBuilder(usage: 'groovy ExcelI18n.groovy -[h] -action generateProperties/generateExcel

filename.xls ')

cli.with { ①

  h(longOpt: 'help', 'Import/Export excel file to i18n properties files.', required: false)

  action('generateProperties generateExcel', required: true, args:1, argName:'action')

}

def options = cli.parse(args)

if (!options){

  return

}

if(options.h || options.arguments().size()==0) {

  cli.usage()

  return

}

if( options.action == 'generateProperties'){ ②

  return generateProperties(options.arguments()[0])

}

if( options.action == 'generateExcel'){ ③

  return generateExcel(options.arguments()[0])

}

cli.usage() ④

//end::arguments[]

//tag::generateProperties[]

void generateProperties(filename){

  File excelFile = new File(filename)

  String dir = excelFile.absolutePath.split(File.separator).dropRight(1).join(File.separator)

  String name = excelFile.name.split('\\.').dropRight(1).join('.')

  InputStream inp = new FileInputStream(excelFile)

  ①

  new File(dir).eachFileMatch ~"${name}(_[A-Za-z]+)?\\.properties", {

  it.delete()

  }

  List<String> languages = []

  Workbook wb = WorkbookFactory.create(inp)

  Sheet sheet = wb.getSheetAt(0)

  sheet.iterator().eachWithIndex{ Row row, int idx-> ②

  if( idx == 0){ ③

  languages = ['']+row.cellIterator().collect{ '_'+it.stringCellValue }.drop(2)*.toLowerCase()

101 Groovy Script

106 |

  return

  }

  String code = row.getCell(0)

  languages.eachWithIndex{ String lang, int i -> ④

  String txt = row.getCell(i+1)?.stringCellValue

  if(txt)

  new File("${name}${lang}.properties") << "$code=$txt\n" ⑤

  }

  }

}

//end::generateProperties[]

//tag::generateExcel[]

void generateExcel(filename){

  File excelFile = new File(filename)

  String dir = excelFile.absolutePath.split(File.separator).dropRight(1).join(File.separator)

  String name = excelFile.name.split('\\.').dropRight(1).join('.')

  Properties defaultProperties = new Properties()

  defaultProperties.load( new File("${name}.properties").newInputStream() ) ①

  Map<String,Properties> propertiesMap = [:] ②

  new File(dir).eachFileMatch ~"${name}(_[A-Za-z]+)\\.properties", { ③

  def matcher = (it.name =~ "${name}(_[A-Za-z]+)\\.properties" )

  String lang = matcher[0][1]?.substring(1)

  Properties prp = new Properties()

  prp.load( it.newInputStream() )

  propertiesMap[ lang ] = prp ④

  }

  ExcelBuilder.output(new FileOutputStream(new File("${filename}"))) {

  sheet {

  row{

  cell("Code")

  cell("Default")

  propertiesMap.keySet().each { lang ->

  cell(lang.toUpperCase())

  }

  }

  defaultProperties.propertyNames().each{ String property-> ⑤

  row{

  cell(property)

  cell(defaultProperties[property])

  propertiesMap.keySet().each { lang ->

  cell(propertiesMap[lang][property])

  }

  }

  }

  }

  }

}

//end::generateExcel[]

101 Groovy Script

| 107

Buscar el fichero de mayor tamañorecursivamenteMiguel Rueda <[email protected] [mailto:[email protected]]>2017-08-23

A veces nos surge que necesitamos saber qué fichero está consumiendo más espacio perodebido a que tenemos un número elevado de subdirectorios nos es dificil encontrarlo. Endeterminados sistemas operativos es "fácil" encontrarlo concatenando varios comandoscomo du grep etc.

Mediante este script encontraremos la ruta del fichero con el mayor tamaño sin importarel número de subdirectorios

max=null

void scanDir( File dir ){  dir.eachFile{ f->  max = max && max.size() >= f.size() ? max : f  }  dir.eachDir{ d ->  scanDir(d)  }}

scanDir(new File('.'))

println "El fichero mayor es: $max.path y ocupa ${max.size()} bytes"

A continuación pasamos a explicar de forma detallada el contenido de nuestro script degroovy que cumple la función de encontrar el fichero de mayor tamaño:

Definiremos una función llamada scanDir encargada de recorrer de manera recursivatodos los directorios y subdirectorios de la ruta indicada por parámetro. Comparará eltamaño de cada uno de los ficheros que van apareciendo y guadará el mayor.

Para recorrer cada uno de los directorios y subdirectorios de la ruta indicada porparámetro utilizamos:

  dir.eachFile{ f->

Para recorrer los ficheros del directorio/subdirectorio encontrado utilizamos:

  dir.eachDir{ d ->

101 Groovy Script

108 |

Realizaremos la comparación entre el tamaño de los diferentes ficheros utilizandof.size()(este método nos devuelve la dimensión del fichero):

  max = max && max.size() >= f.size() ? max : f

Para ejecutar este proceso simplemente llamaremos a la función scanDir indicando la rutaque queremos revisar (en este caso .):

scanDir(new File('.'))

Al final del proceso obtendremos el fichero del cual podemos obtener la ruta al mismo,tamaño, etc

println "El fichero mayor es: $max.path y ocupa ${max.size()} bytes"

101 Groovy Script

| 109

Script

max=null

void scanDir( File dir ){  dir.eachFile{ f->  max = max && max.size() >= f.size() ? max : f  }  dir.eachDir{ d ->  scanDir(d)  }}

scanDir(new File('.'))

println "El fichero mayor es: $max.path y ocupa ${max.size()} bytes"

101 Groovy Script

110 |

Buscar en fichero y volcar coincidentesJorge Aguilera <[email protected] [mailto:jorge.aguilera@puravida-

software.com]> 2017-08-23

En algunos sistemas operativos tenemos el comando grep o find que buscan una cadenaen los ficheros de un directorio y la vuelcan por consola. Con este script vamos a realizaralgo parecido para ver cómo podemos leer ficheros de gran tamaño y cómo escribir enotro fichero.

if( args.length != 3){  println "Hey necesito el fichero de entrada, el de salida y la cadena a buscar"  return}

inputFile = new File(args[0])outputFile = new File(args[1])search = args[2]

outputFile.newWriter().withWriter { writer-> ①  inputFile.eachLine { line, indx -> ②  if (line.indexOf(search) != -1)  writer << "$indx: $line\n" ③  }}

① Creamos o sobreescribimos un fichero de salida y usamos un writer para escribir en él

② readLine es indicada para leer ficheros de gran tamaño, teniendo otras formas de leerun fichero como .text que leerían todo el fichero en una cadena

③ utilizamos el operator leftshit para ir enviando cadenas al fichero

101 Groovy Script

| 111

Script

if( args.length != 3){  println "Hey necesito el fichero de entrada, el de salida y la cadena a buscar"  return}

inputFile = new File(args[0])outputFile = new File(args[1])search = args[2]

outputFile.newWriter().withWriter { writer-> ①  inputFile.eachLine { line, indx -> ②  if (line.indexOf(search) != -1)  writer << "$indx: $line\n" ③  }}

101 Groovy Script

112 |

Maven InventoryJorge Aguilera <[email protected] [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 deellas 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 ypuede suceder que se encuentren duplicadas en diferentes directorios o lo que es peorque tengamos para el mismo artefacto diferentes versiones y aparezcan problemas con elcargador de clases.

Con este pequeño script podremos hacer un scaneo rápido de estos artefactos y generarun informe con toda esta información que nos permita descubrir estos conflictos.

101 Groovy Script

| 113

import java.util.jar.JarEntry

import java.util.jar.JarFile

import groovy.json.JsonOutput

groups = [:]

path = args.length > 0 ? args[0] : System.properties["user.home"]+"/.m2/repository"; ①

new File(path).traverse(type: groovy.io.FileType.FILES) { it ->

  if( it.name.endsWith('.jar')){

  inspectJar(it)

  }

}

println JsonOutput.prettyPrint(new JsonOutput().toJson(groups))

void inspectJar(File f){

  final JarFile jarFile = new JarFile(f);

  Enumeration<JarEntry> enumOfJar = jarFile.entries()

  JarEntry pom = enumOfJar.find{it.name.endsWith("/pom.properties")}

  if( pom )

  inspectPom( jarFile, pom )

}

void inspectPom(JarFile jarFile, JarEntry jarEntry){

  InputStream input = jarFile.getInputStream(jarEntry);

  Properties prp = new Properties();

  prp.load(input);

  def group = groups[prp['groupId']] ?: [:]

  def artifact = group[prp['artifactId']] ?: [:]

  def version = artifact[prp['version']] ?: []

  version << jarFile.name

  artifact[prp['version']]=version

  group[prp['artifactId']]=artifact

  groups[prp['groupId']] = group

}

① si no se proporciona una ruta se usa la típica de maven

• Iteramos por todos los ficheros que terminan en .jar recursivamente

• Inspecionamos cada fichero buscando si contiene un pom.properties

• Actualizamos un Map (groups) de Map (artifacts) de List (versions) y añadimos elnombre del fichero a la lista de versiones. Si dos ficheros coinciden aparece unaentrada duplicada

AL final de escaneo volcamos en pantalla la información en formato JSON pero puedesusar cualquier otro formato de tu conveniencia.

101 Groovy Script

114 |

ResultadoEste 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"

  ]

  },

101 Groovy Script

| 115

Organizar fotografías por fechaJorge Aguilera <[email protected] [mailto:jorge.aguilera@puravida-

software.com]> 2018-08-12

Tengo una cámara nueva que hace unas fotos realmente fantásticas. Incluso puedoconectar mi teléfono y transferirme las fotos a este para compartirlas al momento. Sinembargo lo que no hace (o yo no he encontrado) es organizarme las fotos por carpetassegún la fecha en que la tomé.

Con este pequeño script vamos a poder acceder a la información EXIF que se guardadentro de la imagen para buscar la fecha en que fue tomada y usarla para organizar todaslas fotos de un directorio dado.

101 Groovy Script

116 |

@Grab("org.yaml:snakeyaml:1.16")@Grab("com.drewnoakes:metadata-extractor:2.11.0")

import static groovy.io.FileType.FILES

import com.drew.metadata.*import com.drew.imaging.*import com.drew.imaging.jpeg.*import com.drew.metadata.exif.*

dir = new File(args[0])

outdir = new File(args[1])outdir.mkdirs()

files = []dir.traverse(type: FILES, maxDepth: 0) { files << it }files = files.findAll{ file ->  String ext = file.name.toLowerCase().split("\\.").last()  ['jpg','png'].contains(ext)}.sort{ it.lastModified() }

if( !files.size() ){  println "No files"  return}

files.each{ file ->  String gname = new Date(file.lastModified()).format('yyyy-MM-dd')

  try {  Metadata metadata = ImageMetadataReader.readMetadata(file)  metadata.directories.find{ it.name=="Exif IFD0"}.tags.findAll{  it.tagName =="Date/Time"}.each{ tag ->  gname = tag.description[0..10].replaceAll(":","-")  }  }catch( e ){  println "error extracting metadata for $file.name : $e.message"  }

  File newdir = new File(outdir,gname)  newdir.mkdirs()println "Move $file.name to $newdir.name"  file.renameTo( new File(newdir, file.name) )}

El script es muy simple:

101 Groovy Script

| 117

Dado un directorio de origen y uno de destino, recorrerá todos los ficheros .jpg y .png delprimero y para cada uno de ellos extraerá la información meta que contiene. Estainformación es muy extensa y cada fabricante añade las suyas propias. De todas ellasbuscaremos el tag EXIF Date/Time el cual es usado para guardar la fecha en que se tomó lafoto.

Simplemente usaremos este tag como nombre del subdirectorio donde mover la foto.

101 Groovy Script

118 |

Script

@Grab("org.yaml:snakeyaml:1.16")@Grab("com.drewnoakes:metadata-extractor:2.11.0")

import static groovy.io.FileType.FILES

import com.drew.metadata.*import com.drew.imaging.*import com.drew.imaging.jpeg.*import com.drew.metadata.exif.*

dir = new File(args[0])

outdir = new File(args[1])outdir.mkdirs()

files = []dir.traverse(type: FILES, maxDepth: 0) { files << it }files = files.findAll{ file ->  String ext = file.name.toLowerCase().split("\\.").last()  ['jpg','png'].contains(ext)}.sort{ it.lastModified() }

if( !files.size() ){  println "No files"  return}

files.each{ file ->  String gname = new Date(file.lastModified()).format('yyyy-MM-dd')

  try {  Metadata metadata = ImageMetadataReader.readMetadata(file)  metadata.directories.find{ it.name=="Exif IFD0"}.tags.findAll{  it.tagName =="Date/Time"}.each{ tag ->  gname = tag.description[0..10].replaceAll(":","-")  }  }catch( e ){  println "error extracting metadata for $file.name : $e.message"  }

  File newdir = new File(outdir,gname)  newdir.mkdirs()println "Move $file.name to $newdir.name"  file.renameTo( new File(newdir, file.name) )}

101 Groovy Script

| 119

Eliminar lineas con errorJorge Aguilera <[email protected] [mailto:jorge.aguilera@puravida-

software.com]> 2018-02-01

A continuación vamos a ver un script realmente sencillo para un caso de uso frecuente:

Tenemos un fichero que contiene un elevado número de líneas y en otro fichero tenemosidentificadas 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 ybuscar en el fichero de origen aquellos que cumplan la codición reescribiendo una y otravez 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 untamaño que no exceda el de la memoria disponible). Después iremos eliminando de estalista aquellos registros que cumplan la condición específica, para lo que usaremos elmétodo removeIf de Groovy

origen=new File('dump.txt').text.split('\n') as List

new File('errors.txt').withReader{ reader ->  line=''  while( line=reader.readLine() ){  String id=line.split(' ')[0] ①  origen.removeIf{ it.startsWith(id) } ②  }}

new File('cleaned.txt').withWriter('ISO-8859-1',{  it.write origen.join('\n') ③})

① Suponemos que el primer campo de cada linea es el Id a buscar

② Eliminamos de la lista todos los registros que cumplan la condicion de comenzar por elID, por ejemplo.

③ Generamos un fichero nuevo uniendo la lista con retornos de carro

101 Groovy Script

120 |

Script

origen=new File('dump.txt').text.split('\n') as List

new File('errors.txt').withReader{ reader ->  line=''  while( line=reader.readLine() ){  String id=line.split(' ')[0] ①  origen.removeIf{ it.startsWith(id) } ②  }}

new File('cleaned.txt').withWriter('ISO-8859-1',{  it.write origen.join('\n') ③})

101 Groovy Script

| 121

Scan and ExecuteJorge Aguilera <[email protected] [mailto:jorge.aguilera@puravida-

software.com]> 2017-09-16

En algún momento de tu vida tendrás que recorrer un directorio de ficheros buscandoaquellos que cumplan una condición especial y ejecutar algún comando si lo encuentras.A veces la condición es simple, como buscar un texto dentro del fichero, que el nombredel fichero cumpla un patrón, etc, pero otras será algo más complicado.

Recientemente surgió la necesidad de subir todo el contenido de un repositorio maven enlocal (.m2) a un repositorio remoto Nexus3 por lo que un simple copy&paste no erasuficiente. En concreto las condiciones del proceso se resumían en:

• el repositorio Nexus3 está protegido por usuario/password

• buscar recursivamente en un directorio dado

• buscar si existe un fichero POM para el artefacto

• comprobar si existía un jar o un war con el mismo nombre del POM

• si la versión a la que apuntan es un snapshot deberán instalarse en un repo diferenteque si es una release

• ejecutar el comando mvn deploy:deploy-file con los parámetros correctos

Obviamente en tu caso los requisitos serán totalmente distintos yprobablemente podrás resolverlo con un BASH pero tal vez lascondiciones se vayan complicando y será cuando quieras hacerte unscript como este

101 Groovy Script

122 |

repoUrl=""

repoSnapUrl=""

void scanDir( dir ){

  dir.eachFile{ f->

  if( f.name.endsWith(".pom") ){ ①

  def jar = dir.listFiles().find{

  (it.name.endsWith(".jar") || it.name.endsWith(".war")) && ②

  it.name.split('\\.').dropRight(1).join('.') == f.name.split('\\.').dropRight(1).join

('.')

  }

  if( jar ){

  String url = jar.name.toLowerCase().contains("-snapshot.") ? repoSnapUrl : repoUrl ③

  """mvn deploy:deploy-file

  -Dfile=$jar.absolutePath

  -DpomFile=$f.absolutePath

  -Durl=$url

  -DrepositoryId=$repoId""".execute() ④

  }

  }

  }

  dir.eachDir{ d-> ⑤

  scanDir(d)

  }

}

repoId = args[1]

repoUrl = args[2]

repoSnapUrl = args.length > 3 ? args[3] : args[2]

scanDir( new File(args[0]) )

① Comprobamos si existe un fichero POM

② Comprobamos si existe un Jar o War con el mismo nombre (indica que pertenecen a lamisma version)

③ Configuramos repositorio para snapshot o para release

④ Construimos una cadena y la ejecutamos contra el sistema operativo

⑤ Recorremos recursivamente los subdirectorios

101 Groovy Script

| 123

Script

repoUrl=""

repoSnapUrl=""

void scanDir( dir ){

  dir.eachFile{ f->

  if( f.name.endsWith(".pom") ){ ①

  def jar = dir.listFiles().find{

  (it.name.endsWith(".jar") || it.name.endsWith(".war")) && ②

  it.name.split('\\.').dropRight(1).join('.') == f.name.split('\\.').dropRight(1).join

('.')

  }

  if( jar ){

  String url = jar.name.toLowerCase().contains("-snapshot.") ? repoSnapUrl : repoUrl ③

  """mvn deploy:deploy-file

  -Dfile=$jar.absolutePath

  -DpomFile=$f.absolutePath

  -Durl=$url

  -DrepositoryId=$repoId""".execute() ④

  }

  }

  }

  dir.eachDir{ d-> ⑤

  scanDir(d)

  }

}

repoId = args[1]

repoUrl = args[2]

repoSnapUrl = args.length > 3 ? args[3] : args[2]

scanDir( new File(args[0]) )

101 Groovy Script

124 |

A qué dedicas tu tiempo libre (GoogleCalendar)Jorge Aguilera <[email protected] [mailto:jorge.aguilera@puravida-

software.com]> 2017-12-3

Para este post vamos a necesitar los siguientes requisitos: una

cuenta en Google y eventos creados en el calendario. Por defecto usaremos

el calendario principal (pero se podría usar cualquier otro de los que

Google nos permite crear). Así mismo para poder acceder a las APIs de

Google deberemos obtener unas credenciales que autoricen a la aplicación

para acceder a nuestra cuenta.

En este post vamos a utilizar un GroovyScript para conectar el calendario de Googlerevisando todos los eventos que se encuentran creados, agrupándolos en función del díade la semana y el "tema" que le hayamos asignado.

Un evento del calendario de por sí NO incluye ningún campo del tipo"tema" por lo que para poder agruparlos podríamos usar, por ejemplo,algún identificador en el texto del mismo. En este ejemplo lo que vamos ausar es la capacidad de asignar un color a un evento, de tal forma que sien nuestro calendario asignamos el color "rojo" a los eventos de tipo"ocio", el color "azul" a los de "formación", etc. podremos de una formamuy fácil agrupar eventos dispares.

Consola GoogleEn primer lugar deberemos crear una aplicación en la consola de Google enhttps://console.developers.google.com

En esta aplicación habilitaremos (al menos) la API "Google Calendar API"

Así mismo deberemos crear unas credenciales de "Servicio" obteniendo la posibilidad dedescargarlas en un fichero JSON y que deberemos ubicar junto con el script.

GrooglePara facilitar la parte técnica de autentificación y creación de servicios he creado unproyecto 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 scriptssimplemente incluyendo su repositorio.

101 Groovy Script

| 125

@GrabResolver(name='puravida', root="https://dl.bintray.com/puravida-software/repo" )

@Grab(group='org.groovyfx',module='groovyfx',version='8.0.0',transitive=false, noExceptions=true)

@Grab(group = 'com.puravida.groogle', module = 'groogle-core', version = '1.0.0')

import com.puravida.groogle.GroogleScript

import com.google.api.services.calendar.CalendarScopes

Groogle nos permitirá realizar el login de nuestra aplicación y guardar la autorización ennuestro disco de tal forma que en ejecuciones posteriores no se requiera de autorizar denuevo la aplicación (mientras no borremos el fichero con la autorización generada)

InputStream clientSecret = new File('client_secret.json').newInputStream() ①

def groogle = new GroogleScript(applicationName:'101-scripts') ②

groogle.login(clientSecret,[CalendarScopes.CALENDAR_READONLY]) ③

① client-secret.json es el fichero que hemos descargado de la consola de Google y quecontiene las credenciales

② Groogle realiza el login y guarda la autorización en$HOME/.credentials/${applicationName}

③ En este post únicamente requerimos acceso a Calendar para lectura

PersonalizaciónComo los temas son personales cada uno deberá configurar los suyos según suspreferencias. Así mismo creará más o menos temas según el grado de detalle que sequiera 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ásejecutarlo primero y ver cuales te asigna.

BusinessLa lógica de negocio de este script consistirá básicamente en recorrer todos los eventosque 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 dondeir acumulando eventos. En caso de que no exista, simplemente lo inicializaremos a cero

101 Groovy Script

126 |

para ir acumulando sobre él.

def week = ['D','L','M','X','J','V','S']

def stadistics = [:]

groogle.calendarService.events().list(calendarId).execute().items.each{ event -> ①

  def events = !event.recurrentId ? [event] : ②

  groogle.calendarService.events().instances(calendarId, event.id).execute().items

  events.each{ item ->

  String colorId = item.colorId ?: '0'

  if( !stadistics."${colors[colorId]}"){ ③

  stadistics."${colors[colorId]}" =[:]

  (java.util.Calendar.SUNDAY..java.util.Calendar.SATURDAY).each{ day ->

  stadistics."${colors[colorId]}"."${week[day-1]}"= 0

  }

  }

  int day = new Date( (event.start.date ?: event.start.dateTime).value)[java.util.Calendar.DAY_OF_WEEK]

  stadistics."${colors[colorId]}"."${week[day-1]}" +=1 ④

  }

}

① Recorrer todos los eventos que nos devuelva Google

② Un evento puede ser puntual o recurrente. En este caso deberemos obtener todas lasinstancias del mismo

③ Buscar en el mapa el tema asociado al evento y si no existe inicializarlo

④ Incrementar en uno para el día de la semana del evento

VistaPara la vista usaremos un GroovyFX [http://groovyfx.org/] que nos muestre una gráfica debarras barChart tal que nos permite visualizar agrupados por temas los diferentescontadores de los días de la semana. Sin embargo este componente nos pide que leproporcionemos la serie de datos en una sucesión de 'clave', 'valor', 'clave', 'valor' en lugarde un mapa, por lo que lo primero que haremos será transformar nuestro mapa en unomás adecuado:

def flatten =[:]stadistics.each{  flatten."$it.key" = it.value.collect{ [it.key,it.value]}.flatten() ①}

Y ahora ya podemos construir la vista, generando las series de una forma dinámica.

101 Groovy Script

| 127

start {

  stage(visible:true,width:640,height:480) { ②

  scene{

  tilePane() {

  barChart(

  yAxis:new NumberAxis(label:'Ocupacion', tickLabelRotation:90),

  xAxis:new CategoryAxis(label:'Actividad'),

  barGap:3,

  categoryGap:20) {

  flatten.each{ ①

  series(name: it.key, data:it.value)

  }

  }

  }

  }

  }

}

① Creamos dinámicamente las series en función de los temas que tengamos inicialmente.

Mi calendario quedaría de esta forma:

101 Groovy Script

128 |

Script

//tag::dependencies[]

@GrabResolver(name='puravida', root="https://dl.bintray.com/puravida-software/repo" )

@Grab(group='org.groovyfx',module='groovyfx',version='8.0.0',transitive=false, noExceptions=true)

@Grab(group = 'com.puravida.groogle', module = 'groogle-core', version = '1.0.0')

import com.puravida.groogle.GroogleScript

import com.google.api.services.calendar.CalendarScopes

//end::dependencies[]

import static groovyx.javafx.GroovyFX.start

import javafx.scene.chart.CategoryAxis;

import javafx.scene.chart.NumberAxis;

import javafx.application.Platform

import javafx.scene.SnapshotParameters

import javax.imageio.ImageIO

import java.awt.image.BufferedImage

//tag::login[]

InputStream clientSecret = new File('client_secret.json').newInputStream() ①

def groogle = new GroogleScript(applicationName:'101-scripts') ②

groogle.login(clientSecret,[CalendarScopes.CALENDAR_READONLY]) ③

//end::login[]

String calendarId = 'primary'

/*

Si quieres saber todos tus calendarios simplemente ejecuta esta linea

println groogle.calendarService.calendarList().list().execute().items*.id

*/

//tag::temas[]

def colors = [

  '0':'Generico',

  '10':'Reuniones', ①

  '11':'Ocio'

]

//end::temas[]

//tag::business[]

def week = ['D','L','M','X','J','V','S']

def stadistics = [:]

groogle.calendarService.events().list(calendarId).execute().items.each{ event -> ①

  def events = !event.recurrentId ? [event] : ②

  groogle.calendarService.events().instances(calendarId, event.id).execute().items

  events.each{ item ->

  String colorId = item.colorId ?: '0'

  if( !stadistics."${colors[colorId]}"){ ③

  stadistics."${colors[colorId]}" =[:]

  (java.util.Calendar.SUNDAY..java.util.Calendar.SATURDAY).each{ day ->

  stadistics."${colors[colorId]}"."${week[day-1]}"= 0

  }

  }

  int day = new Date( (event.start.date ?: event.start.dateTime).value)[java.util.Calendar.DAY_OF_WEEK]

  stadistics."${colors[colorId]}"."${week[day-1]}" +=1 ④

  }

}

//end::business[]

//tag::flatten[]

101 Groovy Script

| 129

def flatten =[:]

stadistics.each{

  flatten."$it.key" = it.value.collect{ [it.key,it.value]}.flatten() ①

}

//end::flatten[]

//tag::vista[]

start {

  stage(visible:true,width:640,height:480) { ②

  scene{

  tilePane() {

  barChart(

  yAxis:new NumberAxis(label:'Ocupacion', tickLabelRotation:90),

  xAxis:new CategoryAxis(label:'Actividad'),

  barGap:3,

  categoryGap:20) {

  flatten.each{ ①

  series(name: it.key, data:it.value)

  }

  }

  }

  }

  }

}

//end::vista[]

101 Groovy Script

130 |

Import/Export de Google Sheet aDatabaseJorge Aguilera <[email protected] [mailto:jorge.aguilera@puravida-

software.com]> 2017-12-30

Para este post es necesario tener una cuenta en Google así como

tener al menos una Hoja de Cálculo de Google Drive (SpreadSheet). Así

mismo para poder acceder a las APIs de Google deberemos obtener unas

credenciales que autorizen a la aplicación a acceder a nuestra cuenta

habilitandolo para ello desde la consola developer de Google.

A veces el departamento de informática tiene que dar soporte a otros departamentos enlas tareas de exportar y/o importar datos de la base de daos usando formatos que a otrosdepartamentos les sea cómodo. Sin lugar a dudas, el formato elegido en la mayoría de loscasos es un fichero Excel de Microsoft. (Ver post en categoría bbdd y office para consultarscripts ya documentados)

Sin embargo muchas veces este Excel debe ser revisado por más de una persona antes devolver a ser enviado al departamento de informática para su importación en la base dedatos por lo que se produce un riesgo de disparidad de versiones en el fichero, infinidadde 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 dereferencia 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 deautorización:

• conectarse a la base de datos

• conectarse al SpreadSheet

• si la operación que se le indica es de exportar, recorrer todas las hojas del SpreadSheety volcar sobre cada una de ellas los datos de la tabla cuyo nombre coincida con la hoja

• si la operación es de importar, recorrer todas las hojas del SpreadSheet e importartodos las filas de la hoja en la tabla cuyo nombre coincida con la hoja.

101 Groovy Script

| 131

Id del SpreadSheet

En este post vamos a utilizar un GroovyScript para conectarse a nuestroSpreadSheet de Google para leer y/o escribir en el mismo por lo que necesitaremossaber su id. Este id se puede obtener de la propia URL que utiliza el navegadorcuando lo estamos editando. Por ejemplo, si la URL tiene este formato:

https://docs.google.com/spreadsheets/d/xxxxxxxxxtKhml999999999Icp123/edit#gid=0

Entonces xxxxxxxxxtKhml999999999Icp123 es el id que necesitamosproporcionar como argumento al script.

Consola GoogleEn primer lugar deberemos crear una aplicación en la consola de Google enhttps://console.developers.google.com

En esta aplicación habilitaremos (al menos) la API "Google Sheet API"

Así mismo deberemos crear unas credenciales de "Servicio" tras lo cual dispondremos dela posibilidad de descargar un fichero JSON con las mismas y que deberemos ubicar juntocon el script

GrooglePara facilitar la parte técnica de autentificación y creación de servicios he creado unproyecto 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 scriptssimplemente incluyendo su repositorio. Groogle se compone de varios componentes:

• groogle-core, contiene la parte de autentificación y autorización del usuario así comoproporcionar servicios básicos

• groogle-sheet, facilita la gestión de Sheets mediante un lenguaje específico (DSL)permitiendo la lectura y escritura de una hoja de calculo

• groogle-drive, facilita la gestión de ficheros en Drive (aún está en desarrollo)

• groogle-calendar, facilita la gestión de Eventos de un Calendario mediante un lenguajeespecífico

Dependencias

101 Groovy Script

132 |

@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.SheetsScopesimport com.puravida.groogle.GroogleScriptimport com.puravida.groogle.SheetScriptimport groovy.sql.Sql

AutorizaciónGroogle nos permitirá realizar el login de nuestra aplicación y guardar la autorización ennuestro disco de tal forma que ejecuciones posteriores no requieran de volver a autorizarla aplicación (mientras no borremos el fichero con la autorización generada)

clientSecret = new File('client_secret.json').newInputStream() ①

groogleScript = new GroogleScript('101-scripts', clientSecret,[SheetsScopes.SPREADSHEETS]) ②

sheetScript = new SheetScript(groogleScript: groogleScript) ③

① client-secret.json es el fichero que hemos descargado de la consola de Google y quecontiene las credenciales

② Groogle realiza el login y guarda la autorización en$HOME/.credentials/${applicationName}

③ SheetScript ofrece un DSL para la gestión de lectura/escritura del SpreadSheet

DatabasePara la gestión de la base de datos hemos creado una serie de métodos simples:

101 Groovy Script

| 133

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

"password", "com.mysql.jdbc.Driver")

List metadataTable(String table){ ①

  def ret = []

  sqlInstance.rows( "select * from $table limit 1".toString(),{ meta->

  (1..meta.columnCount).each{

  ret.add(meta.getColumnName(it))

  }

  })

  ret

}

void cleanTable(String table){ ②

  sqlInstance.execute "truncate table $table".toString()

}

void insertRowsIntoTable(List params, String table, List metadata){ ③

  String sql = "insert into $table (${metadata.join(',')}) values (${'?,'.multiply(metadata.size()-1)}?)"

  sqlInstance.withBatch(sql){ ps->

  params.each{ param ->

  ps.addBatch param

  }

  }

}

List moreRows(String table, int from, int size){ ④

  sqlInstance.rows( "select * from $table order by 1".toString(),from, size)*.values()

}

① Dado el nombre de una table, otener sus columnas mediante el metadata

② Borrado de una tabla. En nuestro caso y por ser MySQL hacemos un truncate

③ Insertado optimizado de un List<List<Object>> (lista de lista de valores)

④ Lectura de un lote de valores a partir de un registro dado

ArgumentosPara 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 aGoogle" lo cual lo representamos mediante los comandos g2d o d2g respectivamente

101 Groovy Script

134 |

cli = new CliBuilder(usage: '-i -s spreadSheetId g2d|d2g')

cli.with {

  h(longOpt: 'help', args:0,'Usage Information', required: false)

  s(longOpt: 'spreadSheet', args:1, argName:'spreadSheetId', 'El id de la hoja a usar', required: true)

}

options = cli.parse(args)

if (!options) return

if (options.h || options.arguments().size()!=1) {

  cli.usage()

  return

}

if( ['g2d','d2g'].contains(options.arguments()[0]) == false ){

  cli.usage()

  return

}

google2Database = options.arguments()[0] == 'g2d'

Así por ejemplo una posible ejecución del scritp sería:

De Google a Database

groovy ImportExportSheet.groovy -s xxxxxxxxxtKhml999999999Icp123 g2d

Comparando MetadatosEn primer lugar, el script validará que los esquemas de la hoja y de la base de datos secorresponden, de tal forma que ambos tienen las mismas columnas (para cada hoja dedatos que contenga el SpreadSheet).

Para ello lee la primera fila de cada hoja y la compara con el metadata de la tabla con elmismo nombre en la base de datos. Si no coinciden (por ejemplo la hoja contiene unacolumna más que la bbdd, o alguien ha cambiado el nombre de una columna) se aborta elscript mediante un assert

101 Groovy Script

| 135

sheetScript.withSpreadSheet options.spreadSheet, { spreadSheet -> ①

  sheets.each{ gSheet -> ②

  String hojaId = gSheet.properties.title

  println "Validando schema de $hojaId"

  withSheet hojaId, { sheet -> ③

  def sheetColumns = readRows("A1", "AA1").first() ④

  def tableColumns = metadataTable(gSheet.properties.title)

  assert sheetColumns.intersect(tableColumns).size() == sheetColumns.size(), """

  $gSheet.properties.title incompatible schemas:

  Sheet: $sheetColumns

  MySQL: $tableColumns

  """

  }

  }

}

① withSpreadSheet(id, closure), ejecuta la closure dentro de un contexto referenciado alSpreadSheet indicado

② el DSL nos permite acceder a todas las hojas mediante sheets

③ withSheet(id, closure) ejecuta la closure dentro de un contexto referenciado a la hojaindicada

④ readRows (rangoA, rangoB) realiza la lectura de un rango de celdas devolviendo unList<List<Object>>

De Google a DatabaseUna vez validados los metadatas de cada hoja el script puede realizar la importancióndesde las hojas a la base de datos (si el argumento de la ejecución es g2d). Para ellorecorrerá las filas de la hoja mediante readRows hasta que no haya datos (cuandoreadRows devuelva null) y para evitar un número excesivo de lecturas lo hará en bloquesde 100

101 Groovy Script

136 |

if( google2Database ) {  sheetScript.withSpreadSheet options.spreadSheet, { spreadSheet ->  sheets.each { gSheet ->  String hojaId = gSheet.properties.title  println "Importando hoja $hojaId"

  withSheet hojaId, { sheet ->  def sheetColumns = readRows("A1", "AA1").first()  cleanTable(hojaId)

  int idx = 2  def rows = readRows("A$idx", "AA${idx + 100}")  while (rows) {  insertRowsIntoTable(rows, hojaId, sheetColumns)  idx += rows.size()  rows = readRows("A$idx", "AA${idx + 100}")  }

  }

  }  }}

De Database a GoogleUna vez validados los metadatas de cada hoja el script puede realizar la importancióndesde la base de datos a las hojas (si el argumento de la ejecución es d2g). Para ellorecorrerá los registros de la base de datos en bloques de 100 y los insertará en la hojasimplemente llamando el método appendRows que admite un List<List<Object>>

Previamente cada hoja es inicializada mediante clearRange añadiendo también lostítulos mediante appendRow que requiere un List<Object>

101 Groovy Script

| 137

if( !google2Database ) {  sheetScript.withSpreadSheet options.spreadSheet, { spreadSheet ->  sheets.each{ gSheet ->  String hojaId = gSheet.properties.title  println "Exportando a hoja $hojaId"

  withSheet hojaId, { sheet ->  def sheetColumns = readRows("A1", "AA1").first()  clearRange('A','AA')

  appendRow(sheetColumns)

  int idx=0  def rows = moreRows(hojaId, idx, 100)  while( rows.size() ){

  appendRows(rows)  idx+=rows.size()+1  rows = moreRows(hojaId, idx, 100)  }  }

  }  }}

101 Groovy Script

138 |

Script

//tag::dependencies[]

@GrabResolver(name='puravida', root="https://dl.bintray.com/puravida-software/repo" )

@Grab(group = 'com.puravida.groogle', module = 'groogle-sheet', version = '1.1.1')

@Grab('mysql:mysql-connector-java:5.1.23')

@GrabConfig(systemClassLoader=true)

import com.google.api.services.sheets.v4.SheetsScopes

import com.puravida.groogle.GroogleScript

import com.puravida.groogle.SheetScript

import groovy.sql.Sql

//end::dependencies[]

//tag::cli[]

cli = new CliBuilder(usage: '-i -s spreadSheetId g2d|d2g')

cli.with {

  h(longOpt: 'help', args:0,'Usage Information', required: false)

  s(longOpt: 'spreadSheet', args:1, argName:'spreadSheetId', 'El id de la hoja a usar', required: true)

}

options = cli.parse(args)

if (!options) return

if (options.h || options.arguments().size()!=1) {

  cli.usage()

  return

}

if( ['g2d','d2g'].contains(options.arguments()[0]) == false ){

  cli.usage()

  return

}

google2Database = options.arguments()[0] == 'g2d'

//end::cli[]

//tag::database[]

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

"password", "com.mysql.jdbc.Driver")

List metadataTable(String table){ ①

  def ret = []

  sqlInstance.rows( "select * from $table limit 1".toString(),{ meta->

  (1..meta.columnCount).each{

  ret.add(meta.getColumnName(it))

  }

  })

  ret

}

void cleanTable(String table){ ②

  sqlInstance.execute "truncate table $table".toString()

}

void insertRowsIntoTable(List params, String table, List metadata){ ③

  String sql = "insert into $table (${metadata.join(',')}) values (${'?,'.multiply(metadata.size()-1)}?)"

  sqlInstance.withBatch(sql){ ps->

  params.each{ param ->

  ps.addBatch param

  }

  }

}

List moreRows(String table, int from, int size){ ④

  sqlInstance.rows( "select * from $table order by 1".toString(),from, size)*.values()

}

101 Groovy Script

| 139

//end::database[]

//tag::login[]

clientSecret = new File('client_secret.json').newInputStream() ①

groogleScript = new GroogleScript('101-scripts', clientSecret,[SheetsScopes.SPREADSHEETS]) ②

sheetScript = new SheetScript(groogleScript: groogleScript) ③

//end::login[]

//tag::schemas[]

sheetScript.withSpreadSheet options.spreadSheet, { spreadSheet -> ①

  sheets.each{ gSheet -> ②

  String hojaId = gSheet.properties.title

  println "Validando schema de $hojaId"

  withSheet hojaId, { sheet -> ③

  def sheetColumns = readRows("A1", "AA1").first() ④

  def tableColumns = metadataTable(gSheet.properties.title)

  assert sheetColumns.intersect(tableColumns).size() == sheetColumns.size(), """

  $gSheet.properties.title incompatible schemas:

  Sheet: $sheetColumns

  MySQL: $tableColumns

  """

  }

  }

}

//end::schemas[]

//tag::g2d[]

if( google2Database ) {

  sheetScript.withSpreadSheet options.spreadSheet, { spreadSheet ->

  sheets.each { gSheet ->

  String hojaId = gSheet.properties.title

  println "Importando hoja $hojaId"

  withSheet hojaId, { sheet ->

  def sheetColumns = readRows("A1", "AA1").first()

  cleanTable(hojaId)

  int idx = 2

  def rows = readRows("A$idx", "AA${idx + 100}")

  while (rows) {

  insertRowsIntoTable(rows, hojaId, sheetColumns)

  idx += rows.size()

  rows = readRows("A$idx", "AA${idx + 100}")

  }

  }

  }

  }

}

//end::g2d[]

//tag::d2g[]

if( !google2Database ) {

  sheetScript.withSpreadSheet options.spreadSheet, { spreadSheet ->

  sheets.each{ gSheet ->

  String hojaId = gSheet.properties.title

  println "Exportando a hoja $hojaId"

  withSheet hojaId, { sheet ->

101 Groovy Script

140 |

  def sheetColumns = readRows("A1", "AA1").first()

  clearRange('A','AA')

  appendRow(sheetColumns)

  int idx=0

  def rows = moreRows(hojaId, idx, 100)

  while( rows.size() ){

  appendRows(rows)

  idx+=rows.size()+1

  rows = moreRows(hojaId, idx, 100)

  }

  }

  }

  }

}

//end::d2g[]

101 Groovy Script

| 141

Gestionando permisos de Google DriveJorge Aguilera <[email protected] [mailto:jorge.aguilera@puravida-

software.com]> 2018-10-12

Para este post es necesario tener una cuenta en Google así como

tener al menos algunos ficheros en Google Drive. Así mismo para poder

acceder a las APIs de Google deberemos obtener unas credenciales que

autorizen a la aplicación a acceder a nuestra cuenta. Para obtener dichas

credenciales debes acceder a https://console.google.developers y crearlas

desde allí. Para este script el tipo de credenciales serán de tipo cuenta (es

decir el script se ejecutará en representación del usuario una vez que lo

autorize)

Hace unos días un usuario/amigo me planteaba una duda en Twitter sobre Groogle:

tengo un problema con mi Drive. Tengo cienes de carpetas y docscompartidos con URL y los quiero "descompartir" todos los que estándentro de una carpeta …

— Michael Gallego, https://twitter.com/micael_gallego/status/1049370562487865347

A partir de la versión 1.5.1 Groogle incluye un DSL específico para la gestión de permisosque analizaremos en este post.

DependenciasLa versión 1.5.1 no ha sido liberada todavía, así que utilizaremos la versión en previewdisponible 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 loginEl script acepta un parámetro para indicar el comando a ejecutar y si es necesario unsegundo argumento. Al ser un script de ejemplo no es la intención demostrar lasposibilidades de Groovy para trabajar con argumentos proporcionados por línea de

101 Groovy Script

142 |

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 unafunción login de utilidad

drive = DriveScript.instance ①

void login() {  drive.with {  login {  applicationName 'test-permissions'  withScopes DriveScopes.DRIVE  usingCredentials '/client_secret.json'  asService false ②  }  }}

login() ③

switch( args[0] ){ ④  case "dumpFile":  return dumpFile(args[1])  case "dumpFolder":  return dumpPermissions(args.length>1 ?args[1]:null)  case "removeFolder":  return removePermissions(args.length>1 ?args[1]:null)  case "removeFile":  return removeFilePermissions(args[1])  case "shareFile":  return shareFilePermissions(args[1],args[2])}

① referencia a DriveScript

② usaremos una cuenta de usuario para acceder a todos sus documentos

③ la primera vez abrirá un navegador para identificarnos. Las siguientes usará la caché

④ opciones que nos va a permitir el script

De forma resumida, tras hacer login, el script nos permitirá indicar para cada ejecución:

• si queremos volcar por pantalla los permisos de un fichero (necesitamos su id)

• si queremos volcar TODOS los ficheros de mi Drive (no pasamos argumento) o de unacarpeta (proporcionamos su id)

• si queremos revocar todos los permisos de un fichero (necesitamos su id)

• si queremos revocar TODOS los permisos excepto owner de TODOS los ficheros de miDrive (no pasamos argumento) o de una carpeta (proporcionamos su id)

101 Groovy Script

| 143

• compartir un fichero con un usuario proporcionado como segundo argumento

Existen más casos de uso, como revocar permisos sólo a ficheros en una carpeta,compartir con role de edición, etc que son fáciles de implementar siguiendo este post y ladocumentación de Groogle

dumpFile / dumpAllEstos métodos se ofrecen a nivel de comprobar los permisos que tiene un fichero/carpetaen un momento dado. Podemos ejecutarlo por ejemplo al principio para comprobar lasituación y al final para ver si se han aplicado los permisos que queríamos.

void dumpFile(fileId){  drive.with {  withFile fileId, {  println "$file.name ($file.id)"  file.permissions.each { permission ->  println "\t $permission"  }  }  }}void dumpPermissions(folderId) {  def self = this  drive.with {  withFiles {  if(folderId)  parentId folderId  eachFile { file, idx ->  println "$id: $file.name ($file.id)"  file.permissions.each { permission ->  println "\t $permission"  }  if( file.mimeType=='application/vnd.google-apps.folder') ①  self.dumpPermissions(file.id)  }  }  }}

① workarround para trabajar con las subcarpetas

Ambos comandos son parecidos diferenciandose en que uno actúa sobre un fichero dadoy el otro recorre todos los ficheros de una carpeta.

El script, una vez obtenido el fichero (OJO! no es un java.io.File sino uncom.google.api.services.drive.model.File) itera sobre sus permisos, por ejemplo paravolcarlos por pantalla obteniendo un resultado similar a:

101 Groovy Script

144 |

Documento con 2 permisos (owner y user)

Ejemplo1 (1HRnKA0o_xxxxxxxSSSSSSSSaaaaaaa)

  [deleted:false, displayName:Jorge Aguilera Gonzalez, emailAddress:jorge.aguilera@puravida-

software.com, id:12312, kind:drive#permission, photoLink:https://lh3.googleusercontent.com/photo.jpg,

role:reader, type:user]

  [deleted:false, displayName:groovy groogle, emailAddress:[email protected], 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 nuevospermisos, pero Groogle nos permite hacerlo mediante un DSL permissions

removeFile / removeAllAl igual que los métodos dump, ambos comandos son similares salvo que uno actúa sobreun fichero y el otro sobre todos. En ambos casos vamos a eliminar todos los permisos quepudiera tener el fichero excepto el de owner (que por otra parte Google no nos dejaríadando un error)

La misión del DSL es hacer coincidir los permisos existentes con los indicados en elmismo, permitiendo chequear diferentes tipos y roles. Por ejemplo:

• permitir a ciertos usuarios editar el documento

• permitir a ciertos usuarios leer el documento

• permitir a cualquiera con el enlace leer el documento

• permitir a los usuarios de un dominio editar el documento

• etc

Para ello provee diferentes métodos como usersAsReader '[email protected]' para indicarque queremos que un usuario dado pueda leer el documento.

Por defecto el DSL buscará aquellos que hayan sido especificados y NO existen en eldocumento y los creará.

Si además especificamos strick true el DSL se ejecutará en modo estricto y borraráaquellos que estén presentes en el fichero pero no se haya permitido en el DSL

Eliminar todos los permisos

  withFile fileId, { file ->  permissions {  strick true  }  }

En el ejemplo anterior estamos eliminando TODOS los permisos de acceso (excepto el deowner) que pudiera tener el documento.

101 Groovy Script

| 145

Si una vez ejecutado el comando para eliminar permisos volvieramos a ejecutar el dumpobtendríamos algo parecido a:

Documento con 1 permiso (owner)

Ejemplo1 (1HRnKA0o_xxxxxxxSSSSSSSSaaaaaaa)

  [deleted:false, displayName:groovy groogle, emailAddress:[email protected], id:111,

kind:drive#permission, photoLink:https://lh6.googleusercontent.com/s64/photo.jpg, role:owner, type:user]

shareFilePor ú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 modolectura al documento.

Share File

  withFile fileId, {  permissions {  usersAsReader args

  //others:  //usersAsWriter args  //domainAsReader args  //anyoneAsReader  //anyoneAsWriter

  strick true  }  }

En un mismo DSL permisssions podemos especificar diferentes casos como por ejemplo:

• usersAsWriter para especificar una lista de usuarios que pueden editar

• domainAsReader para especificar un dominio como lectura

• anyoneAsRead para indicar que cualquiera con el enlace puede leer

• etc

Documentación adicionalPara más info se puede consultar la documentación de Groogle:

https://puravida-software.gitlab.io/groogle/groogle-drive/

101 Groovy Script

146 |

Raffle (Google Sheet + Meetups)Jorge Aguilera <[email protected] [mailto:jorge.aguilera@puravida-

software.com]> 2019-04-9

Para este post vamos a necesitar los siguientes requisitos: una

cuenta en Google y una hoja de cálculo Sheet de Google. La estructura de

la hoja puede ser como quieras pero para este caso vamos a usar las 3

primeras columnas con los valores "nombre", "apellido" y "email"

En este post vamos a utilizar un GroovyScript para leer una hoja Sheet de Google dondelos asistentes a una charla se han registrado (tal vez con Google Form) para realizar unsorteo 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 GoogleEn primer lugar deberemos crear una aplicación en la consola de Google enhttps://console.developers.google.com

En esta aplicación habilitaremos (al menos) la API "Google Sheet API"

Así mismo deberemos crear unas credenciales de "usuario" obteniendo la posibilidad dedescargarlas en un fichero JSON y que deberemos ubicar junto con el script.

GrooglePara facilitar la parte técnica de autentificación y creación de servicios he creado unproyecto de código abierto, Groogle, disponible en https://groogle.gitlab.com/ el cualpublica un artefacto en Bintray y que podremos usar en nuestros scripts simplementeincluyendo su repositorio.

@Grab(group='com.puravida.groogle', module='groogle-sheet', version='2.0.0')import com.google.api.services.sheets.v4.SheetsScopesimport com.puravida.groogle.*import com.puravida.groogle.sheet.*import groovy.swing.SwingBuilder

import java.awt.BorderLayoutimport java.awt.BorderLayout as BLimport javax.swing.*

Groogle nos permitirá realizar el login de nuestra aplicación y guardar la autorización ennuestro disco de tal forma que en ejecuciones posteriores no se requiera de autorizar de

101 Groovy Script

| 147

nuevo la aplicación (mientras no borremos el fichero con la autorización generada)

  credentials {  applicationName 'raffle'  withScopes SheetsScopes.SPREADSHEETS  usingCredentials "client_secret.json"  asService false  }  service(SheetServiceBuilder.build(), SheetService)

① client-secret.json es el fichero que hemos descargado de la consola de Google y quecontiene las credenciales

② Groogle realiza el login y guarda la autorización en$HOME/.credentials/${applicationName}

③ En este post únicamente requerimos acceso a Sheet para lectura

Leyendo la hojaMediante un bucle iremos recuperando filas en bloques de 100 hasta que no haya máselementos 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íafácilmente adaptable para ser proporcionada por parámetro. En este ejemplo vamos ausar 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ónPara la interface de usuario vamos a utilizar SwingBuilder , un DSL de groovy, que permitecrear interfaces Swing de forma fácil.

Nuestro interface va a consistir básicamente en un JList con el array de asistentes y unbotón que realizará la acción de elegir uno de ellos aleatoriamente.

Tras mostrar el elegido, si el moderador lo desea puede indicar que el asistente no estápara repetir el sorteo sin él.

101 Groovy Script

148 |

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

101 Groovy Script

| 149

101 Groovy Script

150 |

Organizando eventosJorge Aguilera <[email protected] [mailto:jorge.aguilera@puravida-

software.com]> 2018-02-4

Para este post vamos a necesitar los siguientes requisitos: una

cuenta, una SpreadSheet y un Calendario todo ello de Google . Se

recomienda crear un calendario específico para este ejercicio puesto que

vamos a borrar y crear eventos en el mismo de forma genérica y si lo

hacemos sobre el principal podríamos perder eventos de nuestro interés.

Así mismo para poder acceder a las APIs de Google deberemos obtener

unas credenciales que autoricen a la aplicación para acceder a nuestra

cuenta.

Este post está inspirado en una iniciativa de Alba Roza (@Alba_Roza)https://twitter.com/Alba_Roza/status/957936830657257473

Consola GoogleEn primer lugar deberemos crear una aplicación en la consola de Google enhttps://console.developers.google.com

En esta aplicación habilitaremos las APIs de "Google Calendar API" y "Google Sheet API"

Así mismo deberemos crear unas credenciales de "Servicio" obteniendo la posibilidad dedescargarlas en un fichero JSON y que deberemos ubicar junto con el script.

GrooglePara facilitar la parte técnica de autentificación y creación de servicios he creado unproyecto 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 scriptssimplemente incluyendo su repositorio.

Este proyecto se divide a su vez en 4 subprojectos:

• groogle-core, contiene la parte de autentificación principalmente

• groogle-drive, para el manejo de ficheros y carpetas en Drive

• groogle-sheet, para la gestión específica de hojas de cálculo Sheet

• groogle-calendar, para la gestión específica de calendarios

101 Groovy Script

| 151

En nuestro caso vamos a utilizar estos dos últimos (el primero se autoincluye pordependencia 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.CalendarScopesimport com.google.api.services.sheets.v4.SheetsScopesimport com.puravida.groogle.GroogleScriptimport com.puravida.groogle.CalendarScriptimport com.puravida.groogle.SheetScript

Groogle nos permitirá realizar el login de nuestra aplicación y guardar la autorización ennuestro disco de tal forma que en ejecuciones posteriores no se requiera de autorizar denuevo la aplicación (mientras no borremos el fichero con la autorización generada)

GroogleScript.instance.applicationName='101-groovy'

clientSecret = this.class.getResource('client_secret.json').newInputStream()

GroogleScript.instance.login(clientSecret,[CalendarScopes.CALENDAR, SheetsScopes.SPREADSHEETS])

CalendarScript.instance.groogleScript=GroogleScript.instance

SheetScript.instance.groogleScript=GroogleScript.instance

IdentificadoresInicializamos (por comodidad) el id de la hoja de cálculo así como el del calendario

sheetId='xxxxxxxxxxxxxxxx_yyyyyyyyy-fMLg'calendarId='[email protected]'

SheetPara nuestro ejemplo la hoja Eventos a utilizar será muy sencilla y contendrá la siguienteestructura:

año mes día evento lugar

2018 1 1 Fiesta AñoNuevo

mi casa

2018 2 1 Codificandocomo locos

Mordor

Como los eventos que vamos a gestionar son eventos de todo el día vamos a formatear la

101 Groovy Script

152 |

fecha a YYYY-MM-DD lo cual nos permitirá posteriormente crear eventos en el calendariode forma muy cómoda. Si por ejemplo quisieramos tratar eventos "desde-hasta"simplemente modificaríamos la estructura de la hoja y no realizaríamos este formateo

events=[]

SheetScript.instance.withSpreadSheet sheetId, {

  withSheet 'Eventos',{

  int rowIdx=2

  def row = readRows("A$rowIdx","E$rowIdx")

  def calendarScript = CalendarScript.instance

  while( row ){

  def when =sprintf( '%1$04d-%2$02d-%3$02d',row.first()[0] as int,row.first()[1] as int,row.

first()[2] as int)

  def summary ="${row.first()[3]}"

  def description="${row.first()[4]}"

  println "$when $summary $description"

  events.add([when:when,summary:summary,description:description])

  rowIdx++

  row = readRows("A$rowIdx","E$rowIdx")

  }

  }

}

Recorremos todas las filas (excepto la primera) y recogemos en un mapa los detalles decada evento. Al finalizar tendremos una lista de eventos que podremos "limpiar", unificar,etc

CalendarUna vez que disponemos de los eventos realizaremos en primer lugar una limpieza delcalendario:

CalendarScript.instance.withCalendar( calendarId, {  eachEvent {  removeFromCalendar()  }})

Por último simplemente tenemos que recorrer la lista de los eventos e ir creandolos ennuestro calendario. Como ya hemos dicho nuestro ejemplo es para eventos todo el día porlo que usaremos el método allDay yyyy-MM-dd Si quisieramos gestionar eventos máscomplejos (varios días, desde una hora hasta otra, etc) utilizaríamos el método betweendateTime1 dateTime2

Así mismo podríamos añadir recordatorios a ciertos eventos, añadir lista de personasinteresadas etc.

101 Groovy Script

| 153

events.each{ evt ->  CalendarScript.instance.createEvent( calendarId,{  event.summary = evt.summary  event.description = evt.description  allDay evt.when  })}

101 Groovy Script

154 |

Script

//tag::dependencies[]

@GrabResolver(name='puravida', root="https://dl.bintray.com/puravida-software/repo" )

@Grab(group = 'com.puravida.groogle', module = 'groogle-core', version = '1.4.1')

@Grab(group = 'com.puravida.groogle', module = 'groogle-sheet', version = '1.4.1')

@Grab(group = 'com.puravida.groogle', module = 'groogle-calendar', version = '1.4.1')

@GrabConfig(systemClassLoader=true)

import com.google.api.services.calendar.CalendarScopes

import com.google.api.services.sheets.v4.SheetsScopes

import com.puravida.groogle.GroogleScript

import com.puravida.groogle.CalendarScript

import com.puravida.groogle.SheetScript

//end::dependencies[]

sheetId='1hf6q540q45gvhdHH9BOVv_Ij7Wk0KVPFCs42Xc-fMLg'

calendarId='puravida-software.com_n9tci5mfaqqdegi5askjetpa7s@group.calendar.google.com'

//tag::login[]

GroogleScript.instance.applicationName='101-groovy'

clientSecret = this.class.getResource('client_secret.json').newInputStream()

GroogleScript.instance.login(clientSecret,[CalendarScopes.CALENDAR, SheetsScopes.SPREADSHEETS])

CalendarScript.instance.groogleScript=GroogleScript.instance

SheetScript.instance.groogleScript=GroogleScript.instance

//end::login[]

//tag::readFromSheet[]

events=[]

SheetScript.instance.withSpreadSheet sheetId, {

  withSheet 'Eventos',{

  int rowIdx=2

  def row = readRows("A$rowIdx","E$rowIdx")

  def calendarScript = CalendarScript.instance

  while( row ){

  def when =sprintf( '%1$04d-%2$02d-%3$02d',row.first()[0] as int,row.first()[1] as int,row.

first()[2] as int)

  def summary ="${row.first()[3]}"

  def description="${row.first()[4]}"

  println "$when $summary $description"

  events.add([when:when,summary:summary,description:description])

  rowIdx++

  row = readRows("A$rowIdx","E$rowIdx")

  }

  }

}

//end::readFromSheet[]

//tag::removeFromCalendar[]

CalendarScript.instance.withCalendar( calendarId, {

  eachEvent {

  removeFromCalendar()

  }

})

//end::removeFromCalendar[]

//tag::writeToCalendar[]

events.each{ evt ->

  CalendarScript.instance.createEvent( calendarId,{

  event.summary = evt.summary

101 Groovy Script

| 155

  event.description = evt.description

  allDay evt.when

  })

}

//end::writeToCalendar[]

101 Groovy Script

156 |

Crea tu propio DSLJorge Aguilera <[email protected] [mailto:jorge.aguilera@puravida-

software.com]> 2018-02-4

En este artículo vamos a ver cómo puedes crear tu propia DSL

(domain specific language) con un ejemplo práctico donde tus usuarios

podrán generar gráficas de funciones usando una sintáxis específica

creada para ello. De esta forma podrás , por ejemplo, transformar las

complejidades técnicas a un lenguaje de negocio en aquellas situaciones

donde un simple paso de argumentos se queda corto.

Para este ejemplo vamos a utilizar la librería Exp4jhttps://www.objecthunter.net/exp4j/ la cual es capaz de parsear unafunción cualquiera y evaluarla cuando le proporcionamos los valores delas incógnitas.

Caso de usoQueremos crear un script capaz de dibujar una gráfica de una función proporcionada porel 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 ficherola gráfica genereda con el nombre que nos proporcione.

Como bola extra queremos darle la posibilidad de poder dibujar varias funciones en lamisma 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 optarpor una solución basada en el paso de parámetros, por lo que optaremos por crearnosnuestro propio lenguaje.

Exp4jAntes de introducirnos en el diseño de la DSL vamos a ver la parte técnica que usaremospara la generación del diagrama.

exp4j es una librería que permite parsear cualquier función (e incluso añadir las tuyas) yproporcionandole los valores de las incógnitas puede evaluarla y devolvernos el valor.

Este es un ejemplo básico:

101 Groovy Script

| 157

Expression e = new ExpressionBuilder("3 * sin(x) - 2 / (x - 2)")  .variables("x")  .build()  .setVariable("x", 2.3);double result = e.evaluate();

Así pues si somos capaces de proporcionarle la función que nos indique el usuariopodremos realizar un bucle en el rango especificado proporcionando valores a Expressiony 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 defunciones 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 DSLTras evaluar con nuestros usuarios las diferentes sintáxis que podemos crear y que leserían legibles en su lenguaje de negocio llegamos a un acuerdo como el siguiente:

101 Groovy Script

158 |

// Podemos comentar el fichero con dos barras

/*

  o un bloque con barra + asterisco

*/

// queremos dibujar una funcion usando unos rangos

plot "sin(x*e)", {

  // podemos usar from to e incrementing en el orden que le sea más cómodo  from (-4) to 6 incrementing 0.01

}

// podemos unir varios plots en el mismo diagrama con diferentes rangos

plot { //si no especificamos funcion lo podemos hacer dentro del DSL

  // "function" necesita una cadena con la funcion a dibujar

  // y "and" nos permite concatener funciones

  function "cos(sin(x))" and "x*cos(e*x)" and "t^4/x"

  // podemos especificar los rangos indicando from y to asi como la granularidad con incrementing

  from (-3) incrementing 0.01 to 3

  // no he sabido cómo usar directamente números negativos así que hay que recubrirlos con (-x)}

// queremos que nos genere la imagen en fichero

saveAs "test3.png"

Como se puede ver en el ejemplo, una vez aprendida la sintáxis, al usuario le es muy fácilcrear nuevos casos de uso.

Como resultado, el usuario obtendrá un fichero "test3.png" con la imagen, parecida a lasiguiente:

101 Groovy Script

| 159

DSLComo podemos ver nuestro DSL cuenta con 2 partes:

1. una lista de plot y el nombre de un fichero a generar

2. cada plot especifica un caso con

a. una o varias function que se pueden indicar de varias formas

b. un rango con from to e incrementing

PlotDSLPlotDSL será una clase embebida en el propio script que nos permitirá modelar la partemás interna de nuestro DSL. Implementará los métodos "from", "to", "and", "function", etcy 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 conestos valores genera las series de valores vista al principio aunque esto puede realizarseen alguna otra parte del script.

101 Groovy Script

160 |

  List<String> _functions;  PlotDSL(){ ①  _functions=[]  }  PlotDSL(List<String> functions){①  _functions = functions  }

  PlotDSL function(String f){ ①  _functions = [ f ]  this  }

  PlotDSL and(String f){ ②  _functions << f  this  }

  String _variable="x"  PlotDSL using(String variable){ ③  _variable = variable  this  }

  double _from  PlotDSL from(idx){  _from=idx as double  this  }

  double _to  PlotDSL to(idx){  _to=idx as double  this  }

  double _steps  PlotDSL incrementing(idx){  _steps=idx as double  this  }

  Map series(){ ④  calculateFunctions(_functions, _variable, _from, _to, _steps)  }

① 3 maneras de inicializar la función inicial

② cuando escribimos and "x*cos(e*x)" estamos ejecutando el metodo and(String func)

101 Groovy Script

| 161

pasando x*cos(e*x) como parámetro

③ using nos permite indicar a exp4j el nombre de la variable que usamos en la funcioni.e. t*cos(e*t) using "t"

④ método ajeno al DSL que realiza la lógica de negocio de convertir las funciones a seriesde datos

PlotsDSLPlotsDSL (ojo a la s) será la clase embebida encargada de construir los diferentes plot quepueda haber en el DSL.

Un PlotsDSL contiene un array de PlotDSL al que irá añadiendo elementos según seinvoque a su función plot

Esta función plot tiene dos variantes:

• con una closure como argumento

• con un string y una cloruse que corresponde

plot "sin(x*e)", {}

plot {}

Así pues PlotsDSL implementa las dos formas donde una de ellas es simplemente unacomodidad para invocar a la segunda. Lo importante en este punto es que la closure querecibe como argumento será invocada pero usando un PlotDSL como contexto

  PlotsDSL plot( Closure closure){  plot(null,closure)  }

  PlotsDSL plot( String function, Closure closure){  PlotDSL plot = function ? new PlotDSL( [function] ) : new PlotsDSL()  Closure cl = closure.clone()  cl.resolveStrategy = Closure.DELEGATE_ONLY ①  cl.delegate=plot ②  cl(plot) ③  plots << plot ④  this  }

① Usaremos una estrategia de delegación entre las varias que permite Groovy

② el código de la closure se ejecutará en el contexto del delegate

101 Groovy Script

162 |

③ invocamos a la closure (puede hacerse tambien como cl.call() )

④ una vez ejecutada la closure el objeto plotDSL ha sido inicializado

De texto a ClosureEl primer argumento a nuestro script será un fichero que contiene el DSL que queremosejecutar así que el script lo leerá y convertirá el texto a closure. Como "mejora" si elfichero no existe, el script interpretará que la unión de todos los argumentos como cadenaserá el DSL a ejecutar

Para evaluar un texto y convertirlo a Closure usaremos el método evaluate de GroovyShell.Simplemente tenemos que tener en cuenta que la cadena que espera este métodorepresente una closure por lo que recubrimos el texto del fichero mediante llaves { →$texto }

txt=args.join(' ')if( args.length == 1 && new File(args[0]).exists() ){  txt = new File(args[0]).text}

cl = new GroovyShell().evaluate("{ dsl-> $txt}")plotsDSL = new PlotsDSL()plotsDSL.parse(cl)

JavaFXPor último sólo resta que un PlotsDSL ejecute la closure en su contexto y si no se produceun error generar la vista con la lógica de negocio generada por aquel, para lo queusaremos GroovyFX (Groovy+JavaFX) y su propia DSL

101 Groovy Script

| 163

void saveScreenshot(saveNode, fileName){

  def snapshot = saveNode.snapshot(new SnapshotParameters(), null);

  BufferedImage bufferedImage = new BufferedImage(saveNode.width as int, saveNode.height as int,

BufferedImage.TYPE_INT_ARGB);

  BufferedImage image = javafx.embed.swing.SwingFXUtils.fromFXImage(snapshot, bufferedImage)

  File file = new File(fileName)

  ImageIO.write(image, "png", file )

}

start {

  stage(id:'st',title: "GrExp4j", x: 1024, y: 768, visible: true, style: "decorated", primary: true) {

  scene(id: "sc", width: 1024, height: 768) {

  saveNode=lineChart(animated:true, createSymbols:false, legendVisible:true, data: plotsDSL.

series())

  }

  }

  if( plotsDSL._screenshot ){

  saveScreenshot(saveNode,plotsDSL._screenshot)

  }

}

Simplemente crea una aplicación JavaFX y crea un lineChart proporcionandole las seriesde datos que construirá nuestro DSL. Si en el DSL se indica que queremos generar ungráfico usaremos la funcion saveScreenshot para ello.

101 Groovy Script

164 |

Script

@Grapes([

  @Grab(group='net.objecthunter', module='exp4j', version='0.4.8'),

  @Grab(group='org.groovyfx',module='groovyfx',version='8.0.0',transitive=false, noExceptions=true),

])

import net.objecthunter.exp4j.function.Function

import net.objecthunter.exp4j.operator.Operator

import net.objecthunter.exp4j.*

import static groovyx.javafx.GroovyFX.start

import javafx.application.Platform

import javafx.scene.SnapshotParameters

import javax.imageio.ImageIO

import java.awt.image.BufferedImage

import groovy.transform.ToString

class PlotDSL{

//tag::plotDSL[]

  List<String> _functions;

  PlotDSL(){ ①

  _functions=[]

  }

  PlotDSL(List<String> functions){①

  _functions = functions

  }

  PlotDSL function(String f){ ①

  _functions = [ f ]

  this

  }

  PlotDSL and(String f){ ②

  _functions << f

  this

  }

  String _variable="x"

  PlotDSL using(String variable){ ③

  _variable = variable

  this

  }

  double _from

  PlotDSL from(idx){

  _from=idx as double

  this

  }

  double _to

  PlotDSL to(idx){

  _to=idx as double

  this

  }

  double _steps

  PlotDSL incrementing(idx){

  _steps=idx as double

  this

  }

101 Groovy Script

| 165

  Map series(){ ④

  calculateFunctions(_functions, _variable, _from, _to, _steps)

  }

//end::plotDSL[]

  // tag::calculateFunction[]

  private List calculateFunction(String function, String variable, double ini, double end, double step){

  Expression e = new ExpressionBuilder(function).variables(variable).build()

  def data=[]

  for(double i=ini; i<end; i+=step){

  e.setVariable(variable,i)

  double v = e.evaluate()

  data << [i, Double.isNaN(v) ? 0 : v]

  }

  data.flatten()

  }

  // end::calculateFunction[]

  // tag::calculateFunctions[]

  private Map calculateFunctions(List functions, String variable, double ini, double end, double step){

  Map ret=[:]

  functions.each{func->

  ret[func] = calculateFunction(func, variable, ini, end, step)

  }

  ret

  }

  // end::calculateFunctions[]

}

class PlotsDSL{

  List<PlotDSL> plots = []

  PlotsDSL parse( Closure closure ){

  Closure cl = closure.clone()

  cl.resolveStrategy = Closure.DELEGATE_ONLY

  cl.delegate=this

  cl(this)

  }

  //tag::plotsConstructor[]

  PlotsDSL plot( Closure closure){

  plot(null,closure)

  }

  PlotsDSL plot( String function, Closure closure){

  PlotDSL plot = function ? new PlotDSL( [function] ) : new PlotsDSL()

  Closure cl = closure.clone()

  cl.resolveStrategy = Closure.DELEGATE_ONLY ①

  cl.delegate=plot ②

  cl(plot) ③

  plots << plot ④

  this

  }

  //end::plotsConstructor[]

  String _screenshot

  PlotsDSL saveAs(String screenshot){

  _screenshot=screenshot

  this

  }

  Map series(){

101 Groovy Script

166 |

  def ret=[:]

  plots.each{ plot->

  plot.series().each{ serie->

  ret[serie.key] = serie.value

  }

  }

  ret

  }

}

// tag::txtAClosure[]

txt=args.join(' ')

if( args.length == 1 && new File(args[0]).exists() ){

  txt = new File(args[0]).text

}

cl = new GroovyShell().evaluate("{ dsl-> $txt}")

plotsDSL = new PlotsDSL()

plotsDSL.parse(cl)

// end::txtAClosure[]

//tag::view[]

void saveScreenshot(saveNode, fileName){

  def snapshot = saveNode.snapshot(new SnapshotParameters(), null);

  BufferedImage bufferedImage = new BufferedImage(saveNode.width as int, saveNode.height as int,

BufferedImage.TYPE_INT_ARGB);

  BufferedImage image = javafx.embed.swing.SwingFXUtils.fromFXImage(snapshot, bufferedImage)

  File file = new File(fileName)

  ImageIO.write(image, "png", file )

}

start {

  stage(id:'st',title: "GrExp4j", x: 1024, y: 768, visible: true, style: "decorated", primary: true) {

  scene(id: "sc", width: 1024, height: 768) {

  saveNode=lineChart(animated:true, createSymbols:false, legendVisible:true, data: plotsDSL.

series())

  }

  }

  if( plotsDSL._screenshot ){

  saveScreenshot(saveNode,plotsDSL._screenshot)

  }

}

//end::view[]

101 Groovy Script

| 167

Xml a CalendarJorge Aguilera <[email protected] [mailto:jorge.aguilera@puravida-

software.com]> 2018-05-10

Para este post vamos a necesitar los siguientes requisitos: una

cuenta de Google y crear un Calendario de Google. Se recomienda crear un

calendario específico para este ejercicio puesto que vamos a borrar y crear

eventos en el mismo de forma genérica y si lo hacemos sobre el principal

podríamos perder eventos de nuestro interés. Así mismo para poder

acceder a las APIs de Google deberemos obtener unas credenciales que

autoricen a la aplicación para acceder a nuestra cuenta.

En este post vamos ver cómo consumir un XML con los eventos gratuitosque se van a realizar en las bibliotecas municipales de Madrid usando elservicio de Datos Abiertos. Nuestro script se va a ejecutar de formaperiódica, obteniendo del servicio remoto los eventos para los próximos60 días, parseando el Xml y creando los eventos en el calendario con lainformació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/

XmlEl endpoint que ofrece la web de Datos Abiertos de Madrid nos devuelve un array deelementos contenido tal como se muestra a continuación:

101 Groovy Script

168 |

<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 comoson TITULO-ACTIVIDAD, FECHA-EVENTO y LOCALIZACION, teniendo este último laparticularidad de que es a su vez un array de attributo

Para ello tendremos que ser capaces de buscar en los nodos aquellos que tengan elatributo nombre con el valor de interés

Consola GoogleEn primer lugar deberemos crear una aplicación en la consola de Google enhttps://console.developers.google.com

En esta aplicación habilitaremos al menos las APIs de "Google Calendar API"

101 Groovy Script

| 169

Así mismo deberemos crear unas credenciales de "OAuth" obteniendo la posibilidad dedescargarlas en un fichero JSON y que deberemos ubicar junto con el script.

GrooglePara facilitar la parte técnica de autentificación y creación de servicios he creado unproyecto 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 scriptssimplemente incluyendo su repositorio.

Este proyecto se divide a su vez en otros subprojectos, como por ejemplo:

• groogle-core, contiene la parte de autentificación principalmente

• groogle-drive, para el manejo de ficheros y carpetas en Drive

• groogle-sheet, para la gestión específica de hojas de cálculo Sheet

• groogle-calendar, para la gestión específica de calendarios

En nuestro caso vamos a utilizar el último (el primero se autoincluye por dependenciatransitiva)

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

@Grab(group='ch.qos.logback', module='logback-classic', version='1.2.3')

@Grab(group = 'com.puravida.groogle', module = 'groogle-core', version = '1.5.0-alpha2')

@Grab(group = 'com.puravida.groogle', module = 'groogle-calendar', version = '1.5.0-alpha2')

@GrabConfig(systemClassLoader=true)

import com.google.api.services.calendar.CalendarScopes

import com.puravida.groogle.GroogleScript

import com.puravida.groogle.CalendarScript

import groovyx.net.http.*

import static groovyx.net.http.HttpBuilder.configure

import static groovyx.net.http.ContentTypes.JSON

import static groovy.json.JsonOutput.prettyPrint

import static groovy.json.JsonOutput.toJson

Groogle nos permitirá realizar el login de nuestra aplicación y guardar la autorización ennuestro disco de tal forma que en ejecuciones posteriores no se requiera de autorizar denuevo la aplicación (mientras no borremos el fichero con la autorización generada)

  login {  applicationName 'groogle-example'  withScopes CalendarScopes.CALENDAR  usingCredentials new File('client_secret.json')  asService false  }

101 Groovy Script

170 |

Eliminando antiguosPara mantener el calendario "limpio" con sólo los eventos de interés vamos a realizar enprimer lugar un borrado de todos los eventos que existen en el mismo

  String groogleCalendarId = "[email protected]"  withCalendar groogleCalendarId, {  eachEvent{  removeFromCalendar()  }  }

Consumiendo XMLPara realizar la petición al servicio remoto usaremos HttpBuilder-Ng el cual nos permiterecuperar el contenido en formato Xml tal como se muestra a continuación:

XML = ["application/xml", "text/xml", "application/xhtml+xml", "application/atom+xml"]

xmlEncoder = NativeHandlers.Encoders.&xml

http = configure {

  request.uri = 'https://datos.madrid.es/egob/catalogo/206717-0-agenda-eventos-bibliotecas.xml'

  request.encoder XML, xmlEncoder

  request.contentType = 'application/xml'

}.get{

}

Una vez realizado el GET , el objeto http será un groovy.util.Node el cual nos permitiránavegar a través de sus nodos buscando la información de interés.

BusinessPor último sólo resta ir navegando por los nodos recuperados extrayendo la informaciónde interés y creando un evento en el calendario para cada una de ellas

101 Groovy Script

| 171

  http.contenido.each{

  String titulo = it.atributos.atributo.find{ it.'@nombre'=='TITULO-ACTIVIDAD' }.text()

  String descripcion = it.atributos.atributo.find{ it.'@nombre'=='DESCRIPCION' }.text()

  String inicio = it.atributos.atributo.find{ it.'@nombre'=='FECHA-EVENTO' }.text()

  String fin = it.atributos.atributo.find{ it.'@nombre'=='FECHA-FIN-EVENTO' }.text()

  String hora = it.atributos.atributo.find{ it.'@nombre'=='HORA-EVENTO' }.text()

  String where = it.atributos.atributo.find{ it.'@nombre'=='LOCALIZACION' }?.atributo.find{it

.'@nombre'=='NOMBRE-INSTALACION'}.text()

  Date dini = Date.parse('yyyy-MM-dd HH:mm:ss.S', inicio)

  Date dfin = Date.parse('yyyy-MM-dd HH:mm:ss.S', fin)

  if( descripcion.trim()=="" )

  return

  if( dini < fromDate )

  return

  createEvent groogleCalendarId, {

  it.event.summary = titulo

  it.event.description = "($hora) $descripcion"

  it.event.location=where

  from dini

  until dfin

  }

  }

101 Groovy Script

172 |

Script

//tag::dependencies[]

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

@Grab(group='ch.qos.logback', module='logback-classic', version='1.2.3')

@Grab(group = 'com.puravida.groogle', module = 'groogle-core', version = '1.5.0-alpha2')

@Grab(group = 'com.puravida.groogle', module = 'groogle-calendar', version = '1.5.0-alpha2')

@GrabConfig(systemClassLoader=true)

import com.google.api.services.calendar.CalendarScopes

import com.puravida.groogle.GroogleScript

import com.puravida.groogle.CalendarScript

import groovyx.net.http.*

import static groovyx.net.http.HttpBuilder.configure

import static groovyx.net.http.ContentTypes.JSON

import static groovy.json.JsonOutput.prettyPrint

import static groovy.json.JsonOutput.toJson

//end::dependencies[]

//tag::http[]

XML = ["application/xml", "text/xml", "application/xhtml+xml", "application/atom+xml"]

xmlEncoder = NativeHandlers.Encoders.&xml

http = configure {

  request.uri = 'https://datos.madrid.es/egob/catalogo/206717-0-agenda-eventos-bibliotecas.xml'

  request.encoder XML, xmlEncoder

  request.contentType = 'application/xml'

}.get{

}

//end::http[]

Date fromDate = new Date() - 10

CalendarScript.instance.with {

  //tag::login[]

  login {

  applicationName 'groogle-example'

  withScopes CalendarScopes.CALENDAR

  usingCredentials new File('client_secret.json')

  asService false

  }

  //end::login[]

  //tag::prepare[]

  String groogleCalendarId = "[email protected]"

  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

101 Groovy Script

| 173

.'@nombre'=='NOMBRE-INSTALACION'}.text()

  Date dini = Date.parse('yyyy-MM-dd HH:mm:ss.S', inicio)

  Date dfin = Date.parse('yyyy-MM-dd HH:mm:ss.S', fin)

  if( descripcion.trim()=="" )

  return

  if( dini < fromDate )

  return

  createEvent groogleCalendarId, {

  it.event.summary = titulo

  it.event.description = "($hora) $descripcion"

  it.event.location=where

  from dini

  until dfin

  }

  }

  //end::business[]

}

101 Groovy Script

174 |

OpenDataMadridJorge Aguilera <[email protected] [mailto:jorge.aguilera@puravida-

software.com]> 2018-2-17

En este post vamos a utilizar Gradle como entorno de ejecución denuestro script, en lugar del propio Groovy, y su ecosistema de plugins,para consumir una URL con datos abiertos sobre la ciudad de Madrid yenviar una notificación diaria a un canal de Telegram con un resumen delos próximos eventos que van a ocurrir en la ciudad

Gradle es un sistema de automatización de construcción de códigoabierto que construye sobre los conceptos de Apache Ant y ApacheMaven e introduce un lenguaje especifico del dominio (DSL) basado enGroovy en vez de la forma XML utilizada por Apache Maven paradeclarar la configuración de proyecto.

— Wikipedia, https://es.wikipedia.org/wiki/Gradle

PreparaciónEn primer lugar deberemos preparar nuestro entorno de desarrollo con Gradle. Para ellodisponemos de varios métodos:

• descargarlo, descomprimirlo y ajustar las variables de entorno indicadas

• usar sdkman e instalarlo con sdkman install gradle

• descargar este proyecto semilla https://gitlab.com/groovy-lang/gradle-seed y trabajarsobre él

Con los dos primeros métodos deberemos ejecutar en el directorio donde queramostrabajar el comando gradle init para que nos cree los ficheros necesarios. Si optamos porclonar el repo indicado en el tercer método esto ya se ha hecho y simplemente deberemostrabajar sobre el directorio donde hayamos descargado el repo

Scripts vs TaskGradle es una herramienta para automatizar la construcción de artefactos basada en elconcepto de task, los cuales los podemos ver como pequeños scripts, y que nos permitedefinir dependencias entre ellas. Así mismo proporciona en su DSL la capacidad dedefinir dependencias a los típicos artefactos externos (librerías) para incluir en laejecución del build.

De esta forma podemos usar Gradle como un entorno donde utilizar multitud de librerías

101 Groovy Script

| 175

existentes y mediante scripts interaccionar con el sistema no sólo para construirartefactos sino para ejecutar acciones variopintas como apagar un servidor, enviar uncorreo electrónico con el contenido de un fichero, etc

OpenDataMadridLa ciudad de Madrid, España, cuenta con un amplio catálogo de datos abiertos fácilmenteaccesible vía http en diferentes formatos como pueden ser xml, json, excel e incluso unAPI REST (https://datos.madrid.es/portal/site/egob)

En este post vamos a ver cómo interpretar el catálogo de actividades programadas de lasbibliotecas que se encuentran en la url https://datos.madrid.es/egob/catalogo/206717-0-agenda-eventos-bibliotecas.json

Si inspeccionamos este recurso veremos que las actividades están organizadas en unelemento @graph y cada uno de ellas indica la fecha de inicio y de fín así como el título,lugar, entre otros:

101 Groovy Script

176 |

"@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 todaslas 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 aquellashistorias que acaban de nacer porque para no tener pesadillas por la noche, nada como soñar despiertodurante 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 arealizar a dos (2) días vista y enviar un mensaje a un canal de Telegram con un resumende las mismas (en caso de que haya actividades)

Para el envío de Telegram usaremos un plugin de Gradle que nos permiteenviar a diferentes redes sociales como Twitter, Telegram o Slack,(https://puravida-gradle.gitlab.io/social-network/) lo cual nos servirá parademostrar la potencia de Gradle.

101 Groovy Script

| 177

DependenciasMientras que en un típico script usabamos @Grab para resolver las dependencias, conGradle usaremos su DSL buildSrc

Así mismo Gradle nos permite añadir otro tipo de dependencias no existentes con @Grabcomo son los plugins. Básicamente son artefactos orientados a implementar tareas que sepueden integrar en nuestro build tanto para ser ejecutadas directamente como paraextenderlas. En nuestro caso vamos a utilizar el plugin social-network y su tareatelegram

plugins {  id 'com.puravida.gradle.socialnetwork' version '0.1.1'}import groovy.json.JsonSlurper

ParseNuestra tarea de parseo va a ser una tarea básica que no extiende de ninguna previa yque va a generar, en caso de que haya eventos, un fichero build/telegram.txt con el textoa enviar en formato Markdown.

Utilizando la sintáxis de Gradle, nuestro script se ejecutará dentro de la closure doLast , enla cual descargamos el JSON y recorremos los elementos comentados anteriormente. Paracada elemento de interés iremos añadiendo a un array las líneas que queremos volcar alfichero (nótese que podíamos ir escribiendo directamente en el fichero, simplementeestamos mostrando otras formas de actualizar un fichero de texto).

101 Groovy Script

178 |

task eventosBibliotecas(){

  def msg = file("$buildDir/telegram.txt")

  outputs.files msg

  doLast{

  def arr = []

  def today = Calendar.instance.time

  arr.add "-".multiply(10)

  arr.add "\n"

  arr.add "*Hoy es ${today.format( 'dd/MM/yyyy' )}*"

  arr.add "\n"

  arr.add "\n"

  def json = new JsonSlurper().parseText('https://datos.madrid.es/egob/catalogo/206717-0-agenda-

eventos-bibliotecas.json'.toURL().text)

  json."@graph".each{ evt->

  use(groovy.time.TimeCategory) {

  def strstart = Date.parse('yyyy-MM-dd HH:mm:ss',evt.dtstart)

  def strend = Date.parse('yyyy-MM-dd HH:mm:ss',evt.dtend)

  def duration = strend - today

  switch( duration.days ){

  case 2:

  arr.add "Ἱ�️*${strstart.format( 'dd/MM/yyyy' )} al ${strend.format( 'dd/MM/yyyy'

)}*"

  arr.add "Dentro de *${duration.days}* días en ${evt.'event-location'}"  arr.add "$evt.title"

  arr.add "\n"

  }

  }

  }

  if( arr.size() )

  msg.text = arr.join('\n')

  }

}

TelegramComo hemos comentado usaremos un plugin de Gradle para el envío a un canal deTelegram al cual sólo le tenemos que configurar con las credenciales necesarias así comoel id del canal al que enviar el mensaje, mediante las propiedades telegram_token ytelegram_channel (consultar documentación del plugin)

Como bola extra podemos ver las funcionalidades de Gradle para hacer depender unastareas de otras (telegram de parse en nuestro caso), así como decidir si una tarea debe serejecutada o no en función de las condiciones que necesitemos

101 Groovy Script

| 179

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 manualPara ejecutar nuestro "script/tarea" simplemente ejecutaremos desde el directorioprincipal del proyecto:

si usamos Linux ejectaremos:

./gradlew -b OpenDataMadrid.gradle build -Ptelegram_token=123123ZZZZ "-Ptelegram_channel=@opendatamadrid" ①

① debido a que @ es un caracter especial en la linea de comandos, lo recubrimos entrecomillas

o si usamos windows

gradlew.bat -b OpenDataMadrid.gradle build -Ptelegram_token=123123ZZZZ -Ptelegram_channel=@opendatamadrid

Si todo va bien, y hay eventos, los usuarios suscritos al canal verían un mensaje parecidoa este:

101 Groovy Script

180 |

101 Groovy Script

| 181

Monitorea un servidor (o lo quequieras)Jorge Aguilera <[email protected] [mailto:jorge.aguilera@puravida-

software.com]> 2017-10-23

Qué es JavaFX

JavaFX es una familia de productos y tecnologías de OracleCorporation (inicialmente Sun Microsystems), para la creación deRich Internet Applications (RIAs), esto es, aplicaciones web que tienenlas características y capacidades de aplicaciones de escritorio,incluyendo aplicaciones multimedia interactivas.

Las tecnologías incluidas bajo la denominación JavaFX son JavaFXScript y JavaFX Mobile, aunque hay más productos JavaFX planeados

— Wikipedia, https://es.wikipedia.org/wiki/JavaFX

JavaFX puede verse como el sucesor a Swing en la medida en que nos permite crearaplicaciones 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 Scriptque permite desarrollar aplicaciones con menos código que con el stack tradicionalSwing. (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 lavida más fácil a la hora de definir nuestros elementos gráficos así como conseguir que loscambios que se producen en el modelo se transmitan a la vista de una forma realmentesencilla, el cual puedes encontrar en http://groovyfx.org/

Caso de UsoPara este ejemplo vamos a desarrollar un pequeño script que se va a conectar a unservidor web (parametrizable) y va a ir monitorizando el tiempo de respuesta, de talforma que si se encuentra caído en algún momento podamos verlo rápidamente. Tanto siestá caído como el tiempo que tarda en conectar lo vamos a mostrar en una ventanagráfica que se irá refrescando automáticamente.

Como el resto de scripts, este pretende ofrecerte las herramientas paraque puedas ajustarlo a tus propias necesidades. En este caso no es tanimportante la funcionalidad sino el cómo.

Más o menos esto es lo que veremos en pantalla:

101 Groovy Script

182 |

Modelo-Vista-ControladorNo. Tranquilo. No vamos a desarrollar ningún sistema MVC complejo (aunque si teinteresa 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 alnegocio 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 seproducen en el negocio sean reflejados en la vista (y viceversa aunque en este ejemplo novamos a tratar esta dirección). Para ello JavaFX proporciona unas clases similares a losBeans tanto en su funcionalidad como en su verbosidad, pero tranquilo que GroovyFxnos permite reducirla.

Como puedes ver a continuación la clase Business simplemente consta de un String paraguardar el server que queremos monitorizar y un FXBindable String el cual guarda eltiempo de respuesta a la última petición. Por lo demás simplemente un par de métodospara calcular cuanto tarda una conexión a un puerto mediante sockets, nada del otromundo (probablemente para tu caso específico será aquí donde el problema no sea tantrivial):

101 Groovy Script

| 183

class Business {  String server

  @FXBindable  String delay = '' ①

  void refresh() {  Date start = new Date()  if (!isReachable(10 * 1000)) { //10seg max  delay = "DOWN"  return  }  Date stop = new Date()  groovy.time.TimeDuration td = groovy.time.TimeCategory.minus(stop, start)  delay = "$td" ②  }

  boolean isReachable(int timeOutMillis, int port = 80) {  try {  Socket soc = new Socket()  soc.connect(new InetSocketAddress(server, port), timeOutMillis);  true  } catch (IOException ex) {  println ex.toString()  false;  }  }}

① FXBindable es propia del proyecto GroovyFx

② Al actualizar el valor como un String, los cambios se propagan

La segunda parte del script va a hacer uso del DSL que nos ofrece GroovyFX para poderdiseñar los elementos gráficos ( stage, scene, label, inputs, buttons, etc) así comopersonalizar 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 unirnuestro modelo con la vista, lo cual conseguimos mediante el método bind. Así en estescript le indicamos a un label que su texto NO es un texto fijo sino que está enlazado a unapropiedad delay del objecto business

101 Groovy Script

184 |

  stage(title: 'Latencia', visible: true) { ①

  scene(fill: BLACK, width: 800, height: 250) {

  hbox(padding: 60) {

  text(text: "Ping to $business.server :", font: '20pt sanserif') { ②

  fill linearGradient(endX: 0, stops: [PALEGREEN, SEAGREEN])

  }

  label(text: bind(business, 'delay'), font: '40pt sanserif') { ③

  fill linearGradient(endX: 0, stops: [CYAN, DODGERBLUE])

  effect dropShadow(color: DODGERBLUE, radius: 25, spread: 0.25)

  }

  }

  }

  }

① vamos creando los elementos gráficos

② bind crea un listener a la propiedad de business y actualiza el elemento gráfico enconsecuencia

TransicionesMediante JavaFX/GroovyFX no sólo tenemos aplicaciones estáticas que reaccionan anteeventos del usuario sino que podemos crear nuestras propias transiciones de una formacontínua, de tal forma que nuestra aplicación se puede ir refrescando ella sóla.

  sequentialTransition(cycleCount: INDEFINITE) { ①  pauseTransition(10.s) {  onFinished {  business.refresh() ②  }  }  }.playFromStart()

① Creamos un bucle infinito de transiciones

② Al terminar cada transicion refrescamos nuestro negocio

101 Groovy Script

| 185

Script

//tag::dependencies[]

@GrabConfig(systemClassLoader=true)

@Grab(group='org.groovyfx',module='groovyfx',version='8.0.0',transitive=false, noExceptions=true)

//end::dependencies[]

import groovyx.javafx.beans.FXBindable

import static groovyx.javafx.GroovyFX.start

//tag::business[]

class Business {

  String server

  @FXBindable

  String delay = '' ①

  void refresh() {

  Date start = new Date()

  if (!isReachable(10 * 1000)) { //10seg max

  delay = "DOWN"

  return

  }

  Date stop = new Date()

  groovy.time.TimeDuration td = groovy.time.TimeCategory.minus(stop, start)

  delay = "$td" ②

  }

  boolean isReachable(int timeOutMillis, int port = 80) {

  try {

  Socket soc = new Socket()

  soc.connect(new InetSocketAddress(server, port), timeOutMillis);

  true

  } catch (IOException ex) {

  println ex.toString()

  false;

  }

  }

}

//end::business[]

start {

  def business = new Business(server: args[0])

  //tag::vista[]

  stage(title: 'Latencia', visible: true) { ①

  scene(fill: BLACK, width: 800, height: 250) {

  hbox(padding: 60) {

  text(text: "Ping to $business.server :", font: '20pt sanserif') { ②

  fill linearGradient(endX: 0, stops: [PALEGREEN, SEAGREEN])

  }

  label(text: bind(business, 'delay'), font: '40pt sanserif') { ③

  fill linearGradient(endX: 0, stops: [CYAN, DODGERBLUE])

  effect dropShadow(color: DODGERBLUE, radius: 25, spread: 0.25)

  }

  }

  }

101 Groovy Script

186 |

  }

  //end::vista[]

  //tag::bucle[]

  sequentialTransition(cycleCount: INDEFINITE) { ①

  pauseTransition(10.s) {

  onFinished {

  business.refresh() ②

  }

  }

  }.playFromStart()

  //end::bucle[]

  business.refresh()

}

101 Groovy Script

| 187

TwitterFXJorge Aguilera <[email protected] [mailto:jorge.aguilera@puravida-

software.com]> 2017-11-23

Caso de UsoNuestra empresa ha entrado con ganas en el mundo de las redes sociales y quiere generarcontenido sobre nuestros productos. Hasta hemos contratado a una persona que se va aencargar de generarlos y publicarlos en Twitter.

Esta persona querría poder compartir de una forma sencilla los "Top Ventas" de nuestrosproductos 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 degenerar un gráfico de tartas, salvar la imagen a su disco, abrir Twitter, escribir unmensaje, adjuntar la imagen y twittearla. Fácil. Pesado. Laborioso.

Twitter AppPara poder publicar tweets de forma automática, el primer paso es crear una app enhttps://apps.twitter.com/ para que Twitter nos genere un conjunto de claves. Si sigues lospasos que ofrece la página al final del proceso obtendras 4 claves que deberás guardar enel fichero twitter4j.properties en el mismo directorio que resida nuestro script.

twitter4j.properties

oauth.consumerKey=XXXXXXXXXXoauth.consumerSecret=YYYYYYYYYYYYYYYYYYYYYYoauth.accessToken=527902906-asdfdsafassssssssssssssssoauth.accessTokenSecret=123mlkjdfd9sfldsjlkj

DependenciasNuestro script va a necesitar las siguientes dependencias agrupadas por funcionalidad:

101 Groovy Script

188 |

@GrabConfig(systemClassLoader=true)

@Grab('mysql:mysql-connector-java:5.1.6')

@Grab(group='org.groovyfx',module='groovyfx',version='8.0.0',transitive=false, noExceptions=true)

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

import groovy.sql.Sql

import static groovyx.javafx.GroovyFX.start

import javafx.application.Platform

import javafx.scene.SnapshotParameters

import javax.imageio.ImageIO

import java.awt.image.BufferedImage

import twitter4j.TwitterFactory;

import twitter4j.StatusUpdate

Básicamente:

• conexión a la base de datos

• librerías graficas JavaFX

• librería de twitter

CustomizePara poder ajustar nuestro script de forma cómoda vamos a utilizar las siguientesvariables:

String message ="""Estamos que nos salimos!!!Top Ventas de nuestro catálogo. Gracias a todos por elegirnos#groovy-script"""

String chartTitle="Top Ventas"

int width=height=400

Obtener Top SalesObtener los productos más vendidos puede ser desde una operación trivial hasta unaoperación compleja de agregados. Eso ya dependerá de tu negocio. Para nuestro casovamos a suponer que disponemos de una tabla MySQL donde ya se encuentran agregadoslas ventas por su descripción

101 Groovy Script

| 189

def data=[:] ①

Sql sql = Sql.newInstance( "jdbc:mysql://localhost:3306/scripts?jdbcCompliantTruncation=false",

  "root",

  "my-secret-pw",

  "com.mysql.jdbc.Driver")

sql.eachRow("select product_name, sales from sales order by sales limit 4"){

  data[it.product_name] = it.sales as double ②

}

① Usaremos un mapa "producto"=unidades para generar el gráfico de tartas

② Buscamos en la bbdd e insertamos en nuestro mapa

Generamos el gráfico de tartasEn esta parte podremos usar infinidad de técnicas y capacidades que nos ofrece JavaFX,como por ejemplo objetos 3D, rotaciones, hojas de estilo, etc. Por ahora a nosotros nosbastará con un gráfico de tartas donde se reflejen los productos obtenidos anteriormentey un título para el gráfico.

Una vez renderizado el gráfico nos interesará hacer una foto (snapshot) al nodo enconcreto que contiene el gráfico y volcar a fichero la imagen (en formato PNG).

start {  def saveNode ①  stage(visible: true) { ②  scene(fill: BLACK, width: width, height: height) {  saveNode = tilePane() {  pieChart(data: data, title: chartTitle) ③  }  }  }

① saveNode será una referencia al nodo que contiene el gráfico

② creamos un gráfico de tartas con los datos obtenidos en la bbdd y almacenados en elmapa

③ asignamos la referencia para poder usarla después

Una vez que la scene JavaFX se encuentra lista realizamos el volcado. Básicamentepediremos a JavaFX que nos vuelque en un objeto Image los pixeles y así podamosconvertirlos a fichero PNG

101 Groovy Script

190 |

  def snapshot = saveNode.snapshot(new SnapshotParameters(), null);

  BufferedImage bufferedImage = new BufferedImage(saveNode.width as int, saveNode.height as int,

BufferedImage.TYPE_INT_ARGB);

  BufferedImage image = javafx.embed.swing.SwingFXUtils.fromFXImage(snapshot, bufferedImage)

  File file = new File('screenshot.png')

  ImageIO.write(image, "png", file )

A TwitterGenerar un tweet mediante twitter4j es tan sencillo como crear un StatusUpdate,adjuntar un fichero si así lo deseamos y enviarlo:

  StatusUpdate status = new StatusUpdate(message) ①  status.media(file ) ②  TwitterFactory.singleton.updateStatus status

① El mensaje que definimos en la parte de customizacion al principio

② El fichero png screenshot que hemos generado en el apartado anterior

Aquí puedes ver cómo quedaría un tweet:

101 Groovy Script

| 191

Cerrar automáticamentePor último una vez cumplida su misión simplemente indicamos a JavaFX que cierre laventana automáticamente

  Platform.exit();  System.exit(0);

101 Groovy Script

192 |

Script

//tag::dependencies[]

@GrabConfig(systemClassLoader=true)

@Grab('mysql:mysql-connector-java:5.1.6')

@Grab(group='org.groovyfx',module='groovyfx',version='8.0.0',transitive=false, noExceptions=true)

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

import groovy.sql.Sql

import static groovyx.javafx.GroovyFX.start

import javafx.application.Platform

import javafx.scene.SnapshotParameters

import javax.imageio.ImageIO

import java.awt.image.BufferedImage

import twitter4j.TwitterFactory;

import twitter4j.StatusUpdate

//end::dependencies[]

//tag::customize[]

String message ="""

Estamos que nos salimos!!!

Top Ventas de nuestro catálogo. Gracias a todos por elegirnos#groovy-script

"""

String chartTitle="Top Ventas"

int width=height=400

//end::customize[]

//tag::business[]

def data=[:] ①

Sql sql = Sql.newInstance( "jdbc:mysql://localhost:3306/scripts?jdbcCompliantTruncation=false",

  "root",

  "my-secret-pw",

  "com.mysql.jdbc.Driver")

sql.eachRow("select product_name, sales from sales order by sales limit 4"){

  data[it.product_name] = it.sales as double ②

}

//end::business[]

//tag::vista[]

start {

  def saveNode ①

  stage(visible: true) { ②

  scene(fill: BLACK, width: width, height: height) {

  saveNode = tilePane() {

  pieChart(data: data, title: chartTitle) ③

  }

  }

  }

  //end::vista[]

  //tag::snapshot[]

  def snapshot = saveNode.snapshot(new SnapshotParameters(), null);

  BufferedImage bufferedImage = new BufferedImage(saveNode.width as int, saveNode.height as int,

BufferedImage.TYPE_INT_ARGB);

  BufferedImage image = javafx.embed.swing.SwingFXUtils.fromFXImage(snapshot, bufferedImage)

101 Groovy Script

| 193

  File file = new File('screenshot.png')

  ImageIO.write(image, "png", file )

  //end::snapshot[]

  //tag::twitter[]

  StatusUpdate status = new StatusUpdate(message) ①

  status.media(file ) ②

  TwitterFactory.singleton.updateStatus status

  //end::twitter[]

  //tag::exit[]

  Platform.exit();

  System.exit(0);

  //end::exit[]

}

101 Groovy Script

194 |

VisualSheet (Mejora la interface de tusscripts)Jorge Aguilera <[email protected] [mailto:jorge.aguilera@puravida-

software.com]> 2018-02-17

Caso de UsoEste script no tiene un caso de uso práctico como tal sino que pretende demostrar laforma de mejorar el interface gráfico de los mismos para conseguir una mejor interaccióncon el usuario.

En este caso vamos a mostrar un diálogo donde cargaremos en una tabla una hoja deGoogle y el usuario podrá interactuar con los valores recuperados.

DependenciasNuestro script va a necesitar las siguientes dependencias agrupadas por funcionalidad:

@GrabConfig(systemClassLoader=true)

@Grab(group='org.groovyfx',module='groovyfx',version='8.0.0',transitive=true, noExceptions=true)

@Grab(group = 'com.puravida.groogle', module = 'groogle-sheet', version = '1.4.1')

import groovy.transform.Canonical

import groovyx.javafx.beans.FXBindable

import javafx.collections.FXCollections

import javafx.collections.ObservableList

import static groovyx.javafx.GroovyFX.start

import com.google.api.services.sheets.v4.SheetsScopes

import com.puravida.groogle.GroogleScript

import com.puravida.groogle.SheetScript

Por un lado vamos a necesitar las dependencias de GroovyFX para la parte visual y porotro vamos a necesitar Groogle para la parte de interactuar con GoogleSheet

BusinessNuestro negocio va a ser realmente muy simple. Vamos a representar cada fila de la hojade 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 lainteracción del usuario con la fila en cuestión (derivate)

101 Groovy Script

| 195

enum Status {  ON, OFF}

@Canonicalclass 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 decálculo e inicializará el array de FxRows. Esta función será llamada únicamente cuando elusuario 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}")  }  }  }}

VistaLa vista se compone de un botón que activará una action y una tabla que mostrará deforma dinámica el array de FxRows. Así mismo podremos interactuar con los objetos enlas columnas name y status de tal forma que podríamos actualizar la hoja, calcularvalores o en definitiva cualquier acción que requiera el negocio.

101 Groovy Script

196 |

start {

  actions {

  fxaction(id: 'loadFile',onAction: {loadFile()})

  }

  stage(title: 'VisualSheet', height: 600, visible: true) {

  scene(fill: BLACK, width: 800, height: 250) {

  hbox(padding: 60) {

  button( loadFile, text:'Load')

  }

  hbox(padding: 60) {

  tableView(selectionMode: "single", cellSelectionEnabled: true, editable: true, items: bind

(self,'rows')) {

  tableColumn(editable: false, property: "coordinates", text: "Row", prefWidth: 50)

  tableColumn(editable: true, property: "name", text: "Name", prefWidth: 150,

  onEditCommit: { event ->

  FxRow item = event.tableView.items.get(event.tablePosition.row)

  item.name = event.newValue

  }

  )

  tableColumn(editable: true, property: "status", text: "Status", prefWidth: 150, type:

Status,

  onEditCommit: { event ->

  FxRow item = event.tableView.items.get(event.tablePosition.row)

  item.status = event.newValue

  item.derivate = event.newValue==Status.ON ? "Yes" : "NO"

  }

  )

  tableColumn(editable: false, property: "derivate", text: "Calculada", prefWidth: 150)

  }

  }

  }

  }

}

Esto es una imagen de cómo podría quedar la aplicación:

101 Groovy Script

| 197

101 Groovy Script

198 |

Script

//tag::dependencies[]

@GrabConfig(systemClassLoader=true)

@Grab(group='org.groovyfx',module='groovyfx',version='8.0.0',transitive=true, noExceptions=true)

@Grab(group = 'com.puravida.groogle', module = 'groogle-sheet', version = '1.4.1')

import groovy.transform.Canonical

import groovyx.javafx.beans.FXBindable

import javafx.collections.FXCollections

import javafx.collections.ObservableList

import static groovyx.javafx.GroovyFX.start

import com.google.api.services.sheets.v4.SheetsScopes

import com.puravida.groogle.GroogleScript

import com.puravida.groogle.SheetScript

//end::dependencies[]

//tag::business[]

enum Status {

  ON, OFF

}

@Canonical

class FxRow {

  @FXBindable String coordinates

  @FXBindable String name

  @FXBindable Status status

  @FXBindable String derivate

}

rows=FXCollections.observableArrayList([])

self = this

//end::business[]

//tag::load[]

def loadFile(){

  SheetScript.instance.withSpreadSheet args[0], { spreadSheet ->

  withSheet 'Hoja 1',{

  int idx = 2

  def googleRows = readRows("A$idx", "B${idx + 100}")

  while (googleRows ) {

  googleRows.eachWithIndex{ gRow, rIdx->

  self.rows << new FxRow(coordinates: "A${idx+rIdx}",

  name:gRow[0],

  status: gRow[1]=='on'?Status.ON:Status.OFF,

  derivate: ''

  )

  }

  idx += googleRows.size()

  googleRows = readRows("A$idx", "B${idx + 100}")

  }

  }

  }

}

//end::load[]

//tag::login[]

GroogleScript.instance.applicationName='101-scripts'

clientSecret = new File('../google/client_secret.json').newInputStream()

SheetScript.instance.groogleScript=GroogleScript.instance.login(clientSecret,[SheetsScopes.SPREADSHEETS])

//end::login[]

101 Groovy Script

| 199

//tag::view[]

start {

  actions {

  fxaction(id: 'loadFile',onAction: {loadFile()})

  }

  stage(title: 'VisualSheet', height: 600, visible: true) {

  scene(fill: BLACK, width: 800, height: 250) {

  hbox(padding: 60) {

  button( loadFile, text:'Load')

  }

  hbox(padding: 60) {

  tableView(selectionMode: "single", cellSelectionEnabled: true, editable: true, items: bind

(self,'rows')) {

  tableColumn(editable: false, property: "coordinates", text: "Row", prefWidth: 50)

  tableColumn(editable: true, property: "name", text: "Name", prefWidth: 150,

  onEditCommit: { event ->

  FxRow item = event.tableView.items.get(event.tablePosition.row)

  item.name = event.newValue

  }

  )

  tableColumn(editable: true, property: "status", text: "Status", prefWidth: 150, type:

Status,

  onEditCommit: { event ->

  FxRow item = event.tableView.items.get(event.tablePosition.row)

  item.status = event.newValue

  item.derivate = event.newValue==Status.ON ? "Yes" : "NO"

  }

  )

  tableColumn(editable: false, property: "derivate", text: "Calculada", prefWidth: 150)

  }

  }

  }

  }

}

//end::view[]

101 Groovy Script

200 |

Visualizar Extension de FicherosJorge Aguilera <[email protected] [mailto:jorge.aguilera@puravida-

software.com]> 2018-02-17

JavaFX (y GroovyFX) nos permiten visualizar datos de forma muy cómoda. En este casovamos a realizar un escaneo de nuestros ficheros y por cada extensión vamos a acumularel tamaño de los ficheros y su número, de tal forma que podamos visualizar ladistribución de los mismos en tres gráficas:

• distribución por tamaños (qué extensión ocupa más disco)

• distribución por número (qué extensión tiene más ficheros)

• relación entre ambos conceptos

CalculoDado un directorio como argumento, el script lo recorrera recursivamente generando 3mapas con la extensión como clave:

• por tamaño, bySize

• por contador, byNumber

• por ambos mediante un submapa con la unión de los 3 conceptos (extension, numeroy tamaño)

101 Groovy Script

| 201

bySize=[:]byNumber=[:]byBoth=[:]

void scanDir( File dir ){  dir.eachFile FileType.FILES,{ f->  split = f.name.split('\\.')  ext = split.size() ? split.last() : '.'

  bySize."$ext" = (bySize."$ext" ?: 0)+f.size()  byNumber."$ext" = (byNumber."$ext" ?: 0)+1

  both = byBoth."$ext" ?: [ext,0,0]  both[1] +=f.size()  both[2] +=1  byBoth."$ext" = both  }  dir.eachDir{ d ->  scanDir(d)  }}

scanDir(new File(args[0]))

bySize = bySize.sort{ a,b-> b.value<=>a.value }byNumber = byNumber.sort{ a,b-> b.value<=>a.value }

La gestión de Groovy con mapas es tan potente que nos permite crear elementos en elmapa de una forma realmente simple

VistaLa vista utilizará 3 gráficas, una para cada mapa calculado previamente

101 Groovy Script

202 |

  stage title: "My Files", visible: true, {  scene {  saveNode = stackPane {  scrollPane {  tilePane(padding: 10, prefTileWidth: 480, prefColumns: 2) {

  pieChart(data: bySize, title: "Size")

  pieChart(data: byNumber, title: "Number")

  scatterChart title:'Size vs Number', {  byBoth.values().each { bb->  series(name: bb[0], data: [bb[1],bb[2]])  }  }  }  }  }  }  }

EjemploA continuación se muestra un ejemplo de cómo se representaría un directorio, enconcreto el blog donde vemos que la extensión mayoritaria tanto en tamaño como ennúmero es SVG.

101 Groovy Script

| 203

101 Groovy Script

204 |

Script

//tag::dependencies[]

import groovy.io.FileType

@GrabConfig(systemClassLoader=true)

@Grab(group='org.groovyfx',module='groovyfx',version='8.0.0',transitive=false, noExceptions=true)

import static groovyx.javafx.GroovyFX.start

import javafx.application.Platform

import javafx.scene.SnapshotParameters

import javax.imageio.ImageIO

import java.awt.image.BufferedImage

//end::dependencies[]

//tag::calculating[]

bySize=[:]

byNumber=[:]

byBoth=[:]

void scanDir( File dir ){

  dir.eachFile FileType.FILES,{ f->

  split = f.name.split('\\.')

  ext = split.size() ? split.last() : '.'

  bySize."$ext" = (bySize."$ext" ?: 0)+f.size()

  byNumber."$ext" = (byNumber."$ext" ?: 0)+1

  both = byBoth."$ext" ?: [ext,0,0]

  both[1] +=f.size()

  both[2] +=1

  byBoth."$ext" = both

  }

  dir.eachDir{ d ->

  scanDir(d)

  }

}

scanDir(new File(args[0]))

bySize = bySize.sort{ a,b-> b.value<=>a.value }

byNumber = byNumber.sort{ a,b-> b.value<=>a.value }

//end::calculating[]

start { app ->

  //tag::view[]

  stage title: "My Files", visible: true, {

  scene {

  saveNode = stackPane {

  scrollPane {

  tilePane(padding: 10, prefTileWidth: 480, prefColumns: 2) {

  pieChart(data: bySize, title: "Size")

  pieChart(data: byNumber, title: "Number")

  scatterChart title:'Size vs Number', {

  byBoth.values().each { bb->

  series(name: bb[0], data: [bb[1],bb[2]])

  }

  }

  }

  }

101 Groovy Script

| 205

  }

  }

  }

  //end::view[]

  //tag::snapshot[]

  def snapshot = saveNode.snapshot(new SnapshotParameters(), null);

  BufferedImage bufferedImage = new BufferedImage(saveNode.width as int, saveNode.height as int,

BufferedImage.TYPE_INT_ARGB);

  BufferedImage image = javafx.embed.swing.SwingFXUtils.fromFXImage(snapshot, bufferedImage)

  File file = new File('myfiles.png')

  ImageIO.write(image, "png", file )

  //end::snapshot[]

}

101 Groovy Script

206 |

Configuración dinámica de ficheroremotoMiguel Rueda <[email protected] [mailto:[email protected]]>2017-10-13

El caso que vamos a tratar es la modificación del contenido de un fichero con el formatoclave:valor.

Si trabajamos con varias máquinas, seguramente nos hayamos encontrado con que cadauna de ellas tiene ficheros de configuración que indiquen la url de la base de datos a laque 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 todocuando hablamos de ficheros de configuración, son personalizados y en el caso de tenerque cambiar algún valor debemos de tener mucho cuidado ya que no son ficheros quepodamos copiar a todas nuestras maquinas de manera masiva porque dedemos respetarla configuración que tienen cada uno de ellos.

A continuación vamos a explicar como crear un script que modifique el usuario con elque hacemos la conexión a la base de datos en base a cierta condición propia de lamáquina.

Vamos a usar una herramienta que ya hemos utilizado en alguno denuestros scripts sshoogr [https://github.com/aestasit/sshoogr].

• Añadimos la dependencia de sshoogr

@Grab('com.aestasit.infrastructure.sshoogr:sshoogr:0.9.25')@GrabConfig(systemClassLoader=true)

• Definimos el nombre del servidor y el de nuestro fichero de configuración.

def server_name = "server_1"def file_config = '/tmp/file.properties'

• Creamos el método de lectura de nuestro fichero

101 Groovy Script

| 207

def getFile(file_config,server_name){  remoteSession("user:passwd@$server_name:22") {①  result = remoteFile(file_config).text ②  }  return result.readLines()③}

① Establecemos los parámtros de conexión usuario = user, contraseña = passwd

mientras que el servidor es variable.

② En la función remoteFile le pasamos por parámetro la ruta complete la fichero.

③ Con la función readLines obtenemos una lista con el contenido del fichero.

• Creamos el método para sobreescribir el fichero con los datos actualizados

def putFile(file_name,server_name,new_file){  remoteSession("user:passwd@$server_name:22") {①  remoteFile(dir).text = properties ②  }}

① Establecemos los parámtros de conexión usuario = user, contraseña = passwd

mientras que el servidor es variable.

② Actualizamos el contenido de nuestro fichero de configuración.

• Aplicamos la lógica de sustitución. En nuestro caso vamos a cambiar la variableuser.sql por el valor de una variable de entorno más una cadena fija _changeme

def read = getFile(file_config,server_name) ①

def properties = read.collect{  if (it.startWith("user.sql")){ ②  return "user.sql=${System.getenv('HOSTNAME')}_changeme"  }  return it}.join('\n')

putFile(file_config,server_name,properties)③

① Lo primero que hacemos es emplear nuestra función getFile para obtener encontenido del fichero.

② Si econtramos la clave en nuestro fichero que queremos actualizar la modificamosy si no saltamos a la siguiente linea.

③ Llamaremos a nuestro método putFile el cúal nos actualizará el contenido denuestro fichero de configuración.

101 Groovy Script

208 |

Si te da error al conectar con el host, prueba a desactivar lacomprobación estricta de la clave del host:

options.trustUnknownHosts = true

101 Groovy Script

| 209

Script

//tag::dependencies[]@Grab('com.aestasit.infrastructure.sshoogr:sshoogr:0.9.25')@GrabConfig(systemClassLoader=true)//end::dependencies[]

import static com.aestasit.infrastructure.ssh.DefaultSsh.*

options.trustUnknownHosts = true

//tag::config[]def server_name = "server_1"def file_config = '/tmp/file.properties'//end::config[]

//tag::main[]def read = getFile(file_config,server_name) ①

def properties = read.collect{  if (it.startWith("user.sql")){ ②  return "user.sql=${System.getenv('HOSTNAME')}_changeme"  }  return it}.join('\n')

putFile(file_config,server_name,properties)③//end::main[]

//tag::write[]def putFile(file_name,server_name,new_file){  remoteSession("user:passwd@$server_name:22") {①  remoteFile(dir).text = properties ②  }}//end::write[]

//tag::read[]def getFile(file_config,server_name){  remoteSession("user:passwd@$server_name:22") {①  result = remoteFile(file_config).text ②  }  return result.readLines()③}//end::read[]

101 Groovy Script

210 |

Envío de eventos con RabbitMQJorge Aguilera,<[email protected] [mailto:jorge.aguilera@puravida-

software.com]> 2018-01-27

En este artículo vamos a desarrollar una solución muy simple para monitorizar ficherosde logs en múltiples máquinas , enviando los cambios que se van produciendo en losmismos a un componente central el cual será el encargado de su tratamiento (persistenciaa base de datos, análisis de las lineas recibidas, alarmas, etc).

Para ello vamos a utilizar el mismo script en dos modos diferentes (proporcionando unargumento en su ejecución):

• producer , monitoriza un fichero y cada vez que se produzca un cambio en el mismolo notificará enviando la última línea que se haya escrito en el fichero. Este modo seráejecutado en varias máquinas de forma simultánea.

• consumer, recibe los eventos de los diferentes producer’S y los va mostrando porpantalla

Para ello usaremos un gestor de mensajería RabbitMQ, el cual será el encargado deproporcionar el canal de envío y recepción así como de la persistencia de los mensajeshasta su consumo.

RabbitMQRabbitMQ es un software destinado a la gestión del intercambio de mensajes, en su másamplia aceptación, entre aplicaciones. Un software cliente se subscribe al mismo pararecibir notificaciones de cuando otro software cliente envía un mensaje siendo el gestor elencargado 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 laimagen oficial Docker, pero no vamos a entrar en detalles de cómo hacer backups,seguridad etc.

Una vez que tengamos instalado Docker en alguna de nuestras máquinas ejecutaremos:

$docker run -d --hostname my-rabbit \  --name some-rabbit \  -v $(pwd)/rabbit://var/lib/rabbitmq/mnesia/rabbit \  -p 5672:5672 -p 15672:15672 \  rabbitmq:3-management

Básicamente creamos una instancia docker some-rabbit que va a usar un subdirectoriorabbit de la máquina donde se está ejecutando para la persistencia de los mensajes y porúltimo vamos a utlizar un par de puertos para poder acceder a dicha instancia (sólonecesitamos 5672 pero si quieres acceder a la consola de RabbitMQ vía web para poder

101 Groovy Script

| 211

inspeccionarlo usamos 15672)

ConexionLo primero que hará el script es establecer conexión con el gestor (en nuestro caso vamosa usar una conexión en localhost con las credenciales por defecto) y configurar en quéentorno publicará/recibirá los mensajes:

exchangeName="groovy-script"queueName="grabbit"routingKey='#'

factory = new ConnectionFactory()  factory.username='guest'  factory.password='guest'  factory.virtualHost='/'  factory.host= args[0] ?: 'localhost'  factory.port=5672conn = factory.newConnection()

channel = conn.createChannel()channel.exchangeDeclare(exchangeName, "direct", true)channel.queueDeclare(queueName, true, false, false, null)channel.queueBind(queueName, exchangeName, routingKey)

Por simplificar configuramos tanto el exchange, el routing y la cola en el mismo sitio sinimportar 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 losmensajes se guarden para asegurar su entrega.

Monitorizar fichero (producer)En el modo producer el script utilizará la librería de Apache VFS2 para monitorizar unfichero de tal forma que cada vez que se modifique este fichero recibiremos una llamadaen nuestro código (`void fileChanged(FileChangeEvent evt) throws Exception `)

101 Groovy Script

212 |

  monitor = args[2]

  if (new File(monitor).exists() == false)  new File(monitor).newPrintWriter().println "${new Date()}"

  FileSystemManager manager = VFS.getManager();  FileObject fobj = manager.resolveFile(new File(monitor).absolutePath)  DefaultFileMonitor fm = new DefaultFileMonitor(this as FileListener)  fm.delay = 500  fm.addFile(fobj)  fm.start()

Cada vez que recibimos el evento de que el fichero ha sido modificado, lo leeremos desdeel final hacia atrás buscando el último retorno de carro y de esta forma obtener la últimalínea del fichero. Obviamente no es una solución robusta para sistemas donde el cambioen el fichero puedan ser de varias líneas, en nuestro caso es un simple ejemplo sujeto deser modificado a situaciones más robustas

Por otra parte, la librería de RabbitMQ nos permite una gran granularidad en el envío delmensaje. En nuestro caso vamos a usar la forma más simple en la cual simplementeindicamos dónde queremos publicar el mensaje y lo enviamos como una simple cadenade texto (como bytes)

void fileChanged(FileChangeEvent evt) throws Exception {

  RandomAccessFile randomAccessFile = new RandomAccessFile(evt.file.localFile, "r"); ①

  long fileLength = evt.file.localFile.length() - 1;

  randomAccessFile.seek(fileLength);

  StringBuilder stringBuilder = new StringBuilder()

  for(long pointer = fileLength; pointer >= 0; pointer--){

  randomAccessFile.seek(pointer)

  char c = (char)randomAccessFile.read()

  if(c == '\n' && fileLength == pointer){

  continue

  }

  if(c == '\n' && fileLength != pointer){

  break

  }

  stringBuilder.append(c)

  }

  println "Soy el producer enviando ${stringBuilder.toString().reverse()}"

  channel.basicPublish(exchangeName, routingKey, null, stringBuilder.toString().reverse().bytes) ②

}

① Usamos un RandomAccess que nos permite movernos por el fichero en lugar de leerlosecuencial

② Publicamos en el sistema la linea leída

101 Groovy Script

| 213

Recibir eventos (consumer)La parte "consumer" del script simplemente va a conectarse al gestor de mensajes el cualle avisará cada vez que haya un mensaje que cumpla las condiciones de exchange/routingindicadas. En nuestro caso el script simplemente lo traceará en la consola

  boolean autoAck = true;  channel.basicConsume(queueName, autoAck, new DefaultConsumer(channel) {  public void handleDelivery(String consumerTag,  Envelope envelope,  AMQP.BasicProperties properties,  byte[] body)  throws IOException{  println "Soy el consumer recibiendo ${new String(body)}"  }  });

ResultadoEn este screenshot puedes ver un caso de ejecución. En la ventana inferior se encuentracorriendo el consumer a la espera de eventos de RabbitMQ, en la superior se encuentra elproducer esperando eventos de VFS y en la superior derecha se realiza un cambio en elfichero que se está monitorizando (volcando la fecha de ese momento).

El cambio en el fichero es detectado por el producer y propagado hasta el consumer. Eneste ejemplo todo se ejecuta en el mismo equipo pero como hemos dicho su interés radicaen que estos componentes pueden estar ejecutándose en otras máquinas e incluso redes

101 Groovy Script

214 |

DockerPara comprobar que la solución puede ser ejecutada en un entorno distribuido vamos acrear una "mini-red" docker con varios contenedores:

• rabbitmq-container, será el contenedor donde estará corriendo el servidor de colasRabbitMQ

• consumer, será el contenedor donde estará corriendo el script en modo consumer(mostrará por pantalla los eventos que reciba)

• producer1, será un contenedor observando un fichero

• producer2, será un contenedor observando otro fichero diferente

Para ello vamos a crear un fichero docker-compose.yml donde vamos a configurar todosestos containers:

docker-compose.yml

version: '2'

services:

  ①

  rabbitmq-container:

  image: rabbitmq:3-management

  hostname: my-rabbit

  volumes:

  - ./rabbit:/var/lib/rabbitmq/mnesia/rabbit

  ports:

  - 5672:5672

  - 15672:15672

  ②

  consumer:

  image: groovy:2.4-jdk8

  volumes:

  - ./:/home/groovy/

  command: groovy /home/groovy/grabbit.groovy rabbitmq-container consumer ③

  ④

  producer1:

  image: groovy:2.4-jdk8

  volumes:

  - ./:/home/groovy/

  command: groovy /home/groovy/grabbit.groovy rabbitmq-container producer /home/groovy/grabbit.log ⑤

  ④

  producer2:

  image: groovy:2.4-jdk8

  volumes:

  - ./:/home/groovy/

  command: groovy /home/groovy/grabbit.groovy rabbitmq-container producer /home/groovy/grabbit2.log ⑥

① rabbitmq-container es el "hostname" por el que el resto de containers lo puedenencontrar en su network

101 Groovy Script

| 215

② consumer es el nombre del container donde corre el script en modo lectura de la cola

③ indicamos el script a ejecutar así como el nombre del host a conectarse y el modo

④ tenemos dos producers (producer1 y producer2)

⑤ producer1 observará un fichero en lo que en su directorio de trabajo

⑥ producer2 observará un fichero diferente en lo que en su directorio de trabajo

Una vez definidos nuestros componentes levantamos el entorno:

$ docker-compose up

y actualizamos cualquiera de los dos ficheros que están siendo observados. En la consolade docker iremos viendo indentificados cada container en las trazas que generan

101 Groovy Script

216 |

Script

@Grapes([

  @Grab(group='com.rabbitmq', module='amqp-client', version='3.1.2'),

  @Grab(group='org.apache.commons', module='commons-vfs2', version='2.2')

])

import com.rabbitmq.client.*

import groovy.json.*

import org.apache.commons.vfs2.FileChangeEvent

import org.apache.commons.vfs2.FileListener

import org.apache.commons.vfs2.FileObject

import org.apache.commons.vfs2.FileSystemManager

import org.apache.commons.vfs2.VFS

import org.apache.commons.vfs2.impl.DefaultFileMonitor

//tag::conexion[]

exchangeName="groovy-script"

queueName="grabbit"

routingKey='#'

factory = new ConnectionFactory()

  factory.username='guest'

  factory.password='guest'

  factory.virtualHost='/'

  factory.host= args[0] ?: 'localhost'

  factory.port=5672

conn = factory.newConnection()

channel = conn.createChannel()

channel.exchangeDeclare(exchangeName, "direct", true)

channel.queueDeclare(queueName, true, false, false, null)

channel.queueBind(queueName, exchangeName, routingKey)

//end::conexion[]

if( args[1] == 'producer' ) {

  //tag::producer[]

  monitor = args[2]

  if (new File(monitor).exists() == false)

  new File(monitor).newPrintWriter().println "${new Date()}"

  FileSystemManager manager = VFS.getManager();

  FileObject fobj = manager.resolveFile(new File(monitor).absolutePath)

  DefaultFileMonitor fm = new DefaultFileMonitor(this as FileListener)

  fm.delay = 500

  fm.addFile(fobj)

  fm.start()

  //end::producer[]

}

if( args[1] == 'consumer' ) {

  //tag::consumer[]

  boolean autoAck = true;

  channel.basicConsume(queueName, autoAck, new DefaultConsumer(channel) {

  public void handleDelivery(String consumerTag,

  Envelope envelope,

101 Groovy Script

| 217

  AMQP.BasicProperties properties,

  byte[] body)

  throws IOException{

  println "Soy el consumer recibiendo ${new String(body)}"

  }

  });

  //end::consumer[]

}

//tag::filechanged[]

void fileChanged(FileChangeEvent evt) throws Exception {

  RandomAccessFile randomAccessFile = new RandomAccessFile(evt.file.localFile, "r"); ①

  long fileLength = evt.file.localFile.length() - 1;

  randomAccessFile.seek(fileLength);

  StringBuilder stringBuilder = new StringBuilder()

  for(long pointer = fileLength; pointer >= 0; pointer--){

  randomAccessFile.seek(pointer)

  char c = (char)randomAccessFile.read()

  if(c == '\n' && fileLength == pointer){

  continue

  }

  if(c == '\n' && fileLength != pointer){

  break

  }

  stringBuilder.append(c)

  }

  println "Soy el producer enviando ${stringBuilder.toString().reverse()}"

  channel.basicPublish(exchangeName, routingKey, null, stringBuilder.toString().reverse().bytes) ②

}

//end::filechanged[]

void fileCreated(FileChangeEvent arg0) throws Exception {

}

void fileDeleted(FileChangeEvent arg0) throws Exception {

}

101 Groovy Script

218 |

Crear un fichero de texto con todas lasoperaciones disponibles en el API delINEJesús J. Ballano <[email protected] [mailto:[email protected]]> 2018-02-13

En este script vamos a guardar en un fichero de texto todas las operaciones disponiblesque nos devuelve el INE (Instituto Nacional de Estadística de España) desde su endpointhttp://servicios.ine.es//wstempus/js/ES/OPERACIONES_DISPONIBLES.

Crear ficheros de texto en Groovy es fácil y acceder a cualquier API REST conHttpBuilderNG [https://github.com/http-builder-ng/http-builder-ng] también, así que los vamos acombinar.

HttpBuilderNG es una librería que nos facilita mucho el acceso a cualquier API y que nospermite elegir entre 3 posibles implementaciones: core, apache o okhttp. Para este ejemplovamos a usar la implementación core.

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

import groovyx.net.http.*

String baseUrl = "http://servicios.ine.es"

String path = "/wstempus/js/ES/OPERACIONES_DISPONIBLES"

def httpBin = HttpBuilder.configure { ②

  request.uri = baseUrl

}

def operations = httpBin.get { ③

  request.uri.path = path

}

File ineOperations = new File('/tmp/ineOperations.txt') ④

operations.each {

  ineOperations << "${it}\n" ⑤

}

① Recogemos la librería de HttpBuilderNG.

② Creamos la configuración con la uri base.

③ Ejecutamos la operación get.

④ Accedemos al fichero donde queremos guardar las distintas operaciones.

⑤ Por cada operación que nos devuelve el API, guardamos una línea en el fichero detexto.

101 Groovy Script

| 219

Script

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

import groovyx.net.http.*

String baseUrl = "http://servicios.ine.es"

String path = "/wstempus/js/ES/OPERACIONES_DISPONIBLES"

def httpBin = HttpBuilder.configure { ②

  request.uri = baseUrl

}

def operations = httpBin.get { ③

  request.uri.path = path

}

File ineOperations = new File('/tmp/ineOperations.txt') ④

operations.each {

  ineOperations << "${it}\n" ⑤

}

101 Groovy Script

220 |

Servidor web en 1 ficheroHolger Garcia <[email protected] [mailto:[email protected]]> 2018-04-07

En este script veremos como usando Groovy y Ratpack podemos crear un servidor webcompleto en un solo fichero.

Para un simple Hello world sería tan sencillo como:

@Grapes([  @Grab('io.ratpack:ratpack-groovy:1.5.2'),  @Grab('org.slf4j:slf4j-simple:1.7.25')])import static ratpack.groovy.Groovy.ratpack

ratpack {  handlers {  get {  render "Hello World!"  }  }}

Si guardamos este fichero con el nombre ratpack.groovy y lo ejecutamos con groovyratpack.groovy obtendremos una traza como la siguiente

[main] INFO ratpack.server.RatpackServer - Starting server...

[main] INFO ratpack.server.RatpackServer - Building registry...

[main] INFO ratpack.server.RatpackServer - Ratpack started (development) for http://localhost:5050

Voila!, hemos creado nuestro primer servidor en unas pocas líneas.

Ahora probemos con algo un poco mas interesante:

Por ejemplo podríamos servir ficheros locales usando

101 Groovy Script

| 221

@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 queramospreparar una respuesta sencilla con la que los desarrolladores frontend puedan comenzara 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.ratpackimport 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 conconexion a base datos, multiples integraciones y toda la logica de negocio que puedasdesear.

Para mas informacion recomiendo visitar la documentacion oficial de Ratpack[https://ratpack.io/manual/current/]

101 Groovy Script

222 |

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 "."  }  }}

101 Groovy Script

| 223

Leer un fichero desde un host remotoMiguel Rueda <[email protected] [mailto:[email protected]]>2017-10-12

Seguramente os haya surgido la necesidad de comprobar el contenido de unosdeterminados equipos remotos desde vuestro equipo como por ejemplo ver las trazas deun fichero .log que va dejando la aplicación que habéis instalado, comprobar versiones opropiedades guardadas en un fichero, modificar uno existente y querer comprobar queunos determinados ficheros de configuración apuntan correctamente, etc.

Cuando sólo tienes un equipo que administrar esta tarea no es muy compleja yprobablemente la realizas a mano, pero cuando el número de equipos a comprobar crecela tarea se vuelve muy repetitiva y cansada y es cuando piensas en crearte un script querealice 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 delas que vamos a ver en este post, como copiar archivos y directorios, ejecutar comandosremotos … os ánimo a que reviseis la documentación que seguro que os será de granayuda.

A continuación pasamos a explicar en funcionamiento de nuestro script paso a paso:

• Añadimos la dependencia de sshoogr:

@Grab('com.aestasit.infrastructure.sshoogr:sshoogr:0.9.25')@GrabConfig(systemClassLoader=true)import static com.aestasit.infrastructure.ssh.DefaultSsh.*

• Creamos la función para leer el fichero:

def getFile(dir,host){  remoteSession("user:passwd@$host:22") {①  result = remoteFile(dir).text ②  }  return result.readLines()③

}

① Establecemos los parámtros de conexión usuario = user, contraseña = passwd

mientras que el servidor es variable.

② En la función remoteFile le pasamos por parámetro la ruta complete la fichero.

101 Groovy Script

224 |

③ Con la función readLines obtenemos una lista con el contenido del fichero.

• Creamos la lista de hosts y el nombre del fichero que queremos leer:

def hosts = ['server_1','server_2','server_3','server_4','server_5']def file = '/var/log/www/access_log'

• recorremos los host y leemos el archivo en cuestion:

hosts.each{host->  getFile(file,host).each{line->  println line  }}

Si te da error al conectar con el host, prueba a desactivar lacomprobación estricta de la clave del host:

options.trustUnknownHosts = true

101 Groovy Script

| 225

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[]

101 Groovy Script

226 |

Contacts2QRCodeJorge Aguilera <[email protected] [mailto:jorge.aguilera@puravida-

software.com]> 2017-11-03

En ete script vamos a tratar las siguientes capacidades de Groovy:

• Leer todas las Hojas, Filas y Columnas de un Excel

• Escribir una celda en función de los valores leídos y/o lógica de negocio

• Invocar a un servicio remoto y volcar el resultado a un fichero

ObjetivoContacts2QRCode es un script que partiendo de un Excel donde se hayan guardado datosde contactos de una serie de personas, como por ejemplo los empleados de tuorganización, generar para cada uno de ellos un código QRCode para incluir en lastarjetas de visita.

Un código QR (del inglés Quick Response code, "código de respuestarápida") es la evolución del código de barras. Es un módulo paraalmacenar información en una matriz de puntos o en un código debarras bidimensional.

QRCode tiene numerosas utilidades y formatos. En este post trataremoscómo generar un VCard, formato destinado a especificar los datos decontacto de una persona, como nombre, puesto, dirección, teléfono, etc.

Para la generación del código QRCode utilizaremos el servicio gratuitozxing (http://zxing.org/)

ExcelEl excel con los datos de nuestros contactos contendrá la siguiente estructura, siendo laprimera fila destinada a contener la cabecera y por ello ignorada por el script:

Id id del empleado se utiliza como nombre defichero QR

Name nombre del empleado puede contener espacios yser compuesto con apellidos,etc

JobTitle puesto puede ser blanco

101 Groovy Script

| 227

Phone telefóno asegurate que el formato esuna cadena para que no setrate como numérico

eMail puede ser blanco

Address dirección en una sola línea

Organization

URL http://xxxx.yyy/zzzz si disponeis de páginapersonal para cadaempleado o en la empresa

QR esta columan será escritapor el script cada vez que seinvoque

DependenciasPara la lectura del Excel utilizaremos esta vez las librerías de Apache, POI

@Grab('org.apache.poi:poi:3.14')@Grab('org.apache.poi:poi-ooxml:3.14')

Parseo del ExcelDurante la primera pasada, el script leerá todas las hojas de cálculo y sus filas suponiendoque siguen el formato anteriormente explicado, generando para cada fila una URLpersonalizada:

WorkbookFactory.create(f,null,false).withCloseable { workbook -> ①  0.step workbook.getNumberOfSheets(), 1, { sheetNum -> ②  println "Working on ${workbook.getSheetName(sheetNum)}"  Sheet sheet = workbook.sheets[sheetNum]  for( Row row : sheet){  if( row.rowNum ){ ③  row.createCell(8).cellValue = generateQRLink(row) ④  }  }  }  bos = new ByteArrayOutputStream()  workbook.write(bos) ⑤}f << bos.toByteArray() ⑥

① Leemos el excel en modo escritura

101 Groovy Script

228 |

② Iteramos en todas las hojas del Excel. Podríamos buscar una en concreto si así loquisieramos

③ Iteramos por todas las filas ignorando la primera

④ Invocamos a la función que genera el link

⑤ Una vez terminado reescribimos el excel en memoria

⑥ Volcamos el buffer de memoria al fichero de origen

Dump de QRCodesSi se especifica un segundo parámetro, este indicará el directorio donde queremos que segeneren los QRCodes de cada empleado. Realizaremos el mismo bucle de lectura que en elapartado anterior pero esta vez buscaremos la columna que se actualizó. Dicha columnaes una URL que nos devuelve un PNG con el código generado, por lo que usaremos lasintaxis de Groovy para descargarlo directamente a un fichero:

if( args.length > 1 ){  File dumpFolder = new File(args[1])  dumpFolder.mkdirs()  WorkbookFactory.create(f,null,true).withCloseable { workbook -> ①  0.step workbook.getNumberOfSheets(), 1, { sheetNum ->  println "Dumping on ${workbook.getSheetName(sheetNum)}"  Sheet sheet = workbook.sheets[sheetNum]  for( Row row : sheet){  if( row.rowNum ){  new File(dumpFolder,"${row.getCell(0)}.png").withOutputStream {  it.bytes = "${row.getCell(8)}".toURL().bytes ②  }  }  }  }  }}

① Leemos el excel en modo lectura

② Gracias a Groovy toURL() genera una URL y bytes nos devuelve el contenido de laresponse como un array de bytes

generateQRLinkLa función generateQRLink simplemente lee los campos de la fila Excel y los concatena ala URL de Zxing devolviendo la cadena resultante

101 Groovy Script

| 229

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

}

GenerateVCardLa función generateVCard a la que se hace referencia en el código anterior es una simpleconcatenación de String en formato VCARD:

String generateVCard( name, title , tlf , email, addr, organization, url){  String getvcard = "BEGIN%3AVCARD%0AVERSION%3A3.0";  if(name) getvcard += "%0AN%3A"+URLEncoder.encode(name,"UTF-8") ①  if(organization) getvcard += "%0AORG%3A"+URLEncoder.encode(organization,"UTF-8")  if(title) getvcard += "%0ATITLE%3A"+URLEncoder.encode(title,"UTF-8")  if(tlf) getvcard += "%0ATEL%3A"+URLEncoder.encode(tlf,"UTF-8");  if(email) getvcard += "%0AEMAIL%3A"+URLEncoder.encode(email,"UTF-8");  if(addr) getvcard += "%0AADR%3A"+URLEncoder.encode(addr,"UTF-8")  if(url) getvcard += "%0AURL%3A"+URLEncoder.encode(url,"UTF-8");  getvcard += "%0AEND%3AVCARD";  return getvcard;}

① Utilizaremos URLEncoder para conseguir que los campos con espacio, acentos etc seanconvertidos correctamente

101 Groovy Script

230 |

Script

//tag::dependencies[]

@Grab('org.apache.poi:poi:3.14')

@Grab('org.apache.poi:poi-ooxml:3.14')

//end::dependencies[]

import org.apache.poi.ss.usermodel.*

if (args.length == 0) {

  println "Use:"

  println " groovy Constact2QRCode.groovy [excel-file]"

  return 1

}

urlRoot = "http://zxing.org/w/chart?cht=qr&chs=350x350&chld=H&choe=UTF-8&chl=";

File f = new File(args[0]);

ByteArrayOutputStream bos

//tag::readExcel[]

WorkbookFactory.create(f,null,false).withCloseable { workbook -> ①

  0.step workbook.getNumberOfSheets(), 1, { sheetNum -> ②

  println "Working on ${workbook.getSheetName(sheetNum)}"

  Sheet sheet = workbook.sheets[sheetNum]

  for( Row row : sheet){

  if( row.rowNum ){ ③

  row.createCell(8).cellValue = generateQRLink(row) ④

  }

  }

  }

  bos = new ByteArrayOutputStream()

  workbook.write(bos) ⑤

}

f << bos.toByteArray() ⑥

//end::readExcel[]

//tag::dumpQr[]

if( args.length > 1 ){

  File dumpFolder = new File(args[1])

  dumpFolder.mkdirs()

  WorkbookFactory.create(f,null,true).withCloseable { workbook -> ①

  0.step workbook.getNumberOfSheets(), 1, { sheetNum ->

  println "Dumping on ${workbook.getSheetName(sheetNum)}"

  Sheet sheet = workbook.sheets[sheetNum]

  for( Row row : sheet){

  if( row.rowNum ){

  new File(dumpFolder,"${row.getCell(0)}.png").withOutputStream {

  it.bytes = "${row.getCell(8)}".toURL().bytes ②

  }

  }

  }

  }

  }

}

//end::dumpQr[]

101 Groovy Script

| 231

//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[]

101 Groovy Script

232 |

Desmenuzando un PdfJorge Aguilera <[email protected] [mailto:jorge.aguilera@puravida-

software.com]> 2017-11-03

En ete script vamos a tratar las siguientes capacidades de Groovy:

• Incluir dependencias externas, en concreto PdfBox de Apache

• Realizar bucles anidados de una forma sencilla

• Generar mapas (key/value) al vuelo

ObjetivoMediante este script y usando la librería PdfBox de Apache, dividiremos las páginas de unPdf en áreas de tal forma que hagamos un "barrido" en cada una de ellas y podamosextraer el texto que se encuentre en las regiones. Al ir haciendo el "barrido" podremosidentificar cada línea leída con una posición dentro de la página

DependenciasPara 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.PDDocumentimport org.apache.pdfbox.text.*import java.awt.Rectangle

Script

101 Groovy Script

| 233

margenright = 10 ①

PDDocument document = PDDocument.load(new URL(args[0]).bytes)

def pages = [:]

document.documentCatalog.pages.eachWithIndex { page , pageIndex->

  def paragraphs = [:]

  PDFTextStripperByArea stripper = new PDFTextStripperByArea(sortByPosition:true)

  (0..42).each { ②

  stripper.addRegion("$it", new Rectangle( margenright, it * 20, 1500, 20)); ③

  }

  stripper.extractRegions(page)

  stripper.regions.eachWithIndex{ r, index->

  def str = stripper.getTextForRegion(r)

  if (str.trim().length() > 0)

  paragraphs["$index"] = str.trim() ④

  }

  pages["$pageIndex"] = paragraphs ⑤

}

println pages

① Puedes "jugar" a definir el margen derecho para afinar cuanto texto tomar desde laderecha.

② Definimos 42 regiones de 20 pixeles, suficientes para un A4

③ Definimos un área de 1500x20 suficientes para leer el ancho de un A4

④ Para cada region que encuentra con texto la añadimos dinámicamente a un mapacreado para la página

⑤ Para cada página añadimos las áreas encontradas en un mapa creado para todo eldocumento.

En una linea (más Grappe)@jmiguel en un afán minimalista nos comenta que el código anterior cumple su funciónen una sóla línea:

println new PDFTextStripper().getText(PDDocument.load(new URL(args[0]).bytes))①

<1>Si el primer argumento es una ruta local a un PDF, comienzalo con file://

101 Groovy Script

234 |

Script

//tag::dependencies[]

@Grapes(

  @Grab(group='org.apache.pdfbox', module='pdfbox', version='2.0.8')

)

import org.apache.pdfbox.pdmodel.PDDocument

import org.apache.pdfbox.text.*

import java.awt.Rectangle

//end::dependencies[]

//tag::sourceCode[]

margenright = 10 ①

PDDocument document = PDDocument.load(new URL(args[0]).bytes)

def pages = [:]

document.documentCatalog.pages.eachWithIndex { page , pageIndex->

  def paragraphs = [:]

  PDFTextStripperByArea stripper = new PDFTextStripperByArea(sortByPosition:true)

  (0..42).each { ②

  stripper.addRegion("$it", new Rectangle( margenright, it * 20, 1500, 20)); ③

  }

  stripper.extractRegions(page)

  stripper.regions.eachWithIndex{ r, index->

  def str = stripper.getTextForRegion(r)

  if (str.trim().length() > 0)

  paragraphs["$index"] = str.trim() ④

  }

  pages["$pageIndex"] = paragraphs ⑤

}

println pages

//end::sourceCode[]

101 Groovy Script

| 235

Importar datos de un excel a una tablaMiguel Rueda <[email protected] [mailto:[email protected]]>2017-10-08

Seguramente a lo largo de tu vida te habrás encontrado con algúncliente/compañero/amigo/no tan amigo al cual le has preguntado dónde guarda unadeterminada información sobre la que estáis trabajando y te ha contestado que en unExcel en su local. Tras la risa nerviosa habrás pasado por la irá, hasta acabar en la etapadel miedo. Bueno una vez la calma llegue podrás darle la opción de incluirlo en la base dedatos con la que trabajáis. Para ello pasamos a explicar cómo realizarlo.

Para trabajar con este script vamos a necesitar las siguientes librerías:

@Grab('org.apache.poi:poi:3.8')@Grab('org.apache.poi:poi-ooxml:3.8')@Grab('mysql:mysql-connector-java:5.1.6')

El siguiente paso que vamos a realizar será la conexión con la base de datos, en este casocontra nuestra máquina.

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

  "user",

  "password",

  "com.mysql.jdbc.Driver")

Las variables necesarias para este script las hemos definido dentro del mismo pero unaforma mejor de hacerlo sería recibir estar información por parámetro y comprobandoque cada uno de los datos son correctos, es decir, que el fichero existe, que la conexióncon la base de datos es correcta y por último que existe la tabla en la que queremos cargarlos datos. En este caso práctico nos vamos a centrar únicamente en la carga de datos porlo tanto vamos a dar valor a cada uno de los valores necesarios:

def table = "myTable"def path_file = "${System.properties['user.home']}/myExcel.xlsx"def list = parse(path_file)

Una vez tenemos los requisitos necesarios vamos a crear el método parse al cual lepasaremos por parámetro la ruta donde se encuentra nuestro Excel y él creará un arraycon los datos de nuestro fichero, en este caso un nombre fijado a myExcel.xlsx

101 Groovy Script

236 |

def parse(path) {  InputStream inp = new FileInputStream(path)  Workbook wb = WorkbookFactory.create(inp);  Sheet sheet = wb.getSheetAt(0);①

  Iterator<Row> rowIt = sheet.rowIterator() ②  Row row = rowIt.next()  def headers = getRowData(row) ③

  def rows = []  while(rowIt.hasNext()) {  row = rowIt.next()  rows << getRowData(row)  }  [headers, rows]}

① Con el objeto WorkbookFactory, accediendo al método create podemos acceder a nuestrofichero y con el método getSheetAt(0) del objeto Workbook podremos obtener la primerahoja (Sheet) de nuestro Excel.

② Para recorrer nuestra hoja seleccionada utilizaremos la función rowIterator() que nosva 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 nombredescriptivo de cada columna. Para obtener este mapa utilizamos nuestro métodogetRowData:

def getRowData(Row row) {  def data = []  for (Cell cell : row) {  getValue(row, cell, data)  }  data}

El cual recorre cada una las filas que tiene nuestro Excel convirtiendo cada valor eninformación con la que podamos trabajar, dependiendo del formato que se encuentre encada celda, es decir, si en una cadena lo formaterá a String, si es un valor entero a int…

Para este trabajo contamos con el método getValue:

101 Groovy Script

| 237

def getValue(Row row, Cell cell, List data) {  def rowIndex = row.getRowNum()  def colIndex = cell.getColumnIndex()  def value = ""  switch (cell.getCellType()) {  case Cell.CELL_TYPE_STRING:  value = cell.getRichStringCellValue().getString();  break;  case Cell.CELL_TYPE_NUMERIC:  if (DateUtil.isCellDateFormatted(cell)) {  value = cell.getDateCellValue();  } else {  value = cell.getNumericCellValue();  }  break;  case Cell.CELL_TYPE_BOOLEAN:  value = cell.getBooleanCellValue();  break;  case Cell.CELL_TYPE_FORMULA:  value = cell.getCellFormula();  break;  default:  value = ""  }  data[colIndex] = value  data}

En el cual podemos ver claramente como formatea cada unos de los valores. Como lorealizar 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 quepodemos trabajar. Nuestro siguiente paso será abordar nuestra tabla en este caso sobreuna base de datos mysql. Para ello vamos a utilizar:

101 Groovy Script

238 |

def insertValues(table,list,sql){

  int index = 0

  sql.rows(""" desc $table """.toString()).each{c-> ①

  keys = index == 0?(keys + ":$c.Field"):(keys + ",:$c.Field")

  index++

  }

  sql.withBatch(20, """ insert into $table values ( $keys ) """.toString()) { ps -> ②

  list.each { t->

  ps.addBatch(t)

  }

  }

}

① Obtenemos el esquema de la tabla recibida.

② A través de la herramienta withBatch vamos a recorrer la lista de valores e insertar de20 en 20 en nuestra base de datos.

101 Groovy Script

| 239

Script

//tag::grab[]

@Grab('org.apache.poi:poi:3.8')

@Grab('org.apache.poi:poi-ooxml:3.8')

@Grab('mysql:mysql-connector-java:5.1.6')

//end::grab[]

import org.apache.poi.ss.usermodel.*

import org.apache.poi.hssf.usermodel.*

import org.apache.poi.xssf.usermodel.*

import org.apache.poi.ss.util.*

import org.apache.poi.ss.usermodel.*

import java.io.*

import groovy.sql.Sql

//tag::mysql[]

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

  "user",

  "password",

  "com.mysql.jdbc.Driver")

//end::mysql[]

//tag::variables[]

def table = "myTable"

def path_file = "${System.properties['user.home']}/myExcel.xlsx"

def list = parse(path_file)

//end::variables[]

insertValues(table,list,sql)

//tag::insert[]

def insertValues(table,list,sql){

  int index = 0

  sql.rows(""" desc $table """.toString()).each{c-> ①

  keys = index == 0?(keys + ":$c.Field"):(keys + ",:$c.Field")

  index++

  }

  sql.withBatch(20, """ insert into $table values ( $keys ) """.toString()) { ps -> ②

  list.each { t->

  ps.addBatch(t)

  }

  }

}

//end::insert[]

//tag::parse[]

def parse(path) {

  InputStream inp = new FileInputStream(path)

  Workbook wb = WorkbookFactory.create(inp);

  Sheet sheet = wb.getSheetAt(0);①

  Iterator<Row> rowIt = sheet.rowIterator() ②

  Row row = rowIt.next()

  def headers = getRowData(row) ③

  def rows = []

  while(rowIt.hasNext()) {

  row = rowIt.next()

  rows << getRowData(row)

  }

101 Groovy Script

240 |

  [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[]

101 Groovy Script

| 241

Volcar datos de una tabla a ExcelMiguel Rueda <[email protected] [mailto:[email protected]]>2017-10-08

El caso práctico en el cuál este script nos puede resultar muy útil es cuando por ejemploexiste la necesidad de extraer información de una base de datos para crear una serie deinformes que nos solicita un cliente, como puede ser obtener información de unosdeterminados productos.

Muchos motores de bases de datos y/o herramientas son capaces de exportar estainformación a fichero CSV (fichero delimitado por comas, punto y coma, pipes, etc) yposteriormente importarlo en un Excel.

Supongamos que disponemos de una tabla de productos donde entre otra informaciónguardamos el código de producto y la descripción.

SKU DESCRIPTION

1 LAPICES

2 SACAPUNTAS

XXXXX UN PRODUCTO SUPERCOTIZADO

Extraer esta información es trivial con una simple sentencia SQL:

select concat(sku,'|', description, '|') from products order by sku

y redirigir la salida de esta sentencia a un fichero para ser abierto con Excel.

Pero con este script nos evitamos todo el proceso de conversión, generando un Exceldirectamente:

101 Groovy Script

242 |

@Grab('mysql:mysql-connector-java:5.1.6')①

@Grab('net.sourceforge.jexcelapi:jxl:2.6.12')

@GrabConfig(systemClassLoader=true)

import jxl.*

import jxl.write.*

import groovy.sql.Sql

filename = "informe.xls"

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

  "user",

  "password",

  "com.mysql.jdbc.Driver")

workbook = Workbook.createWorkbook(new File(filename))

sheet = workbook.createSheet("productos", 0)

first=true

i=0

sql.eachRow("select sku, description from products order by sku") { row -> ②

  if( first ){

  sheet.addCell (new Label (0,i,"Sku"))

  sheet.addCell ( new Label (1,i,"Product") ) ③

  first=false

  }

  sheet.addCell( new Label (0,i+1,"$l.sku") ) ④

  sheet.addCell ( new Label (1,i+1,l.description) )

  i++

}

workbook.write()

workbook.close()

① Cargamos dependencias (MySQL y JExcel)

② Para cada registro que cumpla la query generamos una fila nueva

③ Podemos generar cabeceras en la primera fila

④ Ejemplo de cómo crear dos celdas de tipo texto, pero existen otros tipos como Date oNumber

101 Groovy Script

| 243

Script

@Grab('mysql:mysql-connector-java:5.1.6')①

@Grab('net.sourceforge.jexcelapi:jxl:2.6.12')

@GrabConfig(systemClassLoader=true)

import jxl.*

import jxl.write.*

import groovy.sql.Sql

filename = "informe.xls"

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

  "user",

  "password",

  "com.mysql.jdbc.Driver")

workbook = Workbook.createWorkbook(new File(filename))

sheet = workbook.createSheet("productos", 0)

first=true

i=0

sql.eachRow("select sku, description from products order by sku") { row -> ②

  if( first ){

  sheet.addCell (new Label (0,i,"Sku"))

  sheet.addCell ( new Label (1,i,"Product") ) ③

  first=false

  }

  sheet.addCell( new Label (0,i+1,"$l.sku") ) ④

  sheet.addCell ( new Label (1,i+1,l.description) )

  i++

}

workbook.write()

workbook.close()

101 Groovy Script

244 |

MailMiguel Rueda <[email protected] [mailto:[email protected]]>2017-08-04

Creamos la clase Mail.groovy

import javax.mail.*import javax.mail.internet.*import java.util.Properties;import java.text.DecimalFormat;import javax.activation.DataHandler;import javax.activation.DataSource;import javax.activation.FileDataSource;import javax.mail.BodyPart;import javax.mail.Message;import javax.mail.MessagingException;import javax.mail.Multipart;import javax.mail.PasswordAuthentication;import javax.mail.Session;import javax.mail.Transport;import javax.mail.internet.InternetAddress;import javax.mail.internet.MimeBodyPart;import javax.mail.internet.MimeMessage;import javax.mail.internet.MimeMultipart;

class Mail{  def static sendEmail(subjetc,text,from,to){ ①

  def d_email = from,  d_uname = "",  d_password = "",  d_host = "smtp-relay.gmail.com",  d_port = "587", //465,587  m_to = to,  m_subject = subjetc,  m_text = text

  def props = new Properties() ②  props.put("mail.smtp.user", d_email)  props.put("mail.smtp.host", d_host)  props.put("mail.smtp.port", d_port)  props.put("mail.smtp.starttls.enable","true")  props.put("mail.smtp.debug", "false")  props.put("mail.smtp.auth", "false")  props.put("mail.protocol", "smtp")

101 Groovy Script

| 245

  def session = Session.getInstance(props)  session.setDebug(true);

  def msg = new MimeMessage(session)  msg.setText(m_text)  msg.setSubject(m_subject)  msg.setFrom(new InternetAddress(d_email))  msg.addRecipient(Message.RecipientType.TO, new InternetAddress(m_to))

  Transport.send( msg ) ③  }}

① Definimos el método sendEmail(subjetc,text,from,to) al cual mandamos comoargumento el asunto, el texto que que queremos incluir en el email, el emisor y elreceptor.

② Definimos el properties que utilizaremos para enviar el email.

③ Enviamos el email.

Llamada y envío del email

Abrimos una consola de groovy en la misma posición en la que se encuentre nuestoMail.groovy.

Mail.sendEmail("Email de test","Esto es una prueba de lo bien que lo

hacemos",'[email protected]','[email protected])

101 Groovy Script

246 |

Script

@Grab('mysql:mysql-connector-java:5.1.6')①

@Grab('net.sourceforge.jexcelapi:jxl:2.6.12')

@GrabConfig(systemClassLoader=true)

import jxl.*

import jxl.write.*

import groovy.sql.Sql

filename = "informe.xls"

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

  "user",

  "password",

  "com.mysql.jdbc.Driver")

workbook = Workbook.createWorkbook(new File(filename))

sheet = workbook.createSheet("productos", 0)

first=true

i=0

sql.eachRow("select sku, description from products order by sku") { row -> ②

  if( first ){

  sheet.addCell (new Label (0,i,"Sku"))

  sheet.addCell ( new Label (1,i,"Product") ) ③

  first=false

  }

  sheet.addCell( new Label (0,i+1,"$l.sku") ) ④

  sheet.addCell ( new Label (1,i+1,l.description) )

  i++

}

workbook.write()

workbook.close()

101 Groovy Script

| 247

Mail con documentos adjuntosMiguel Rueda <[email protected] [mailto:[email protected]]>2017-08-04

Partimos de la clase Mail creada en el apartado anterior:

import javax.mail.*import javax.mail.internet.*import java.util.Properties;import java.text.DecimalFormat;import javax.activation.DataHandler;import javax.activation.DataSource;import javax.activation.FileDataSource;import javax.mail.BodyPart;import javax.mail.Message;import javax.mail.MessagingException;import javax.mail.Multipart;import javax.mail.PasswordAuthentication;import javax.mail.Session;import javax.mail.Transport;import javax.mail.internet.InternetAddress;import javax.mail.internet.MimeBodyPart;import javax.mail.internet.MimeMessage;import javax.mail.internet.MimeMultipart;

class Mail{  def static sendEmail(path,subjetc,text,from,to){ ①

  def d_email = from,  d_uname = "",  d_password = "",  d_host = "smtp-relay.gmail.com",  d_port = "587", //465,587  m_to = to,  m_subject = subjetc,  m_text = text

  def props = new Properties()  props.put("mail.smtp.user", d_email)  props.put("mail.smtp.host", d_host)  props.put("mail.smtp.port", d_port)  props.put("mail.smtp.starttls.enable","true")  props.put("mail.smtp.debug", "false")  props.put("mail.smtp.auth", "false")  props.put("mail.protocol", "smtp")

101 Groovy Script

248 |

  def session = Session.getInstance(props)  session.setDebug(true);

  def msg = new MimeMessage(session)  msg.setText(m_text)  msg.setSubject(m_subject)  msg.setFrom(new InternetAddress(d_email))  msg.addRecipient(Message.RecipientType.TO, new InternetAddress(m_to))

  Multipart multipart = new MimeMultipart()  def messageBodyPart = new MimeBodyPart()  DataSource source = new FileDataSource(path) ②  messageBodyPart.setDataHandler(new DataHandler(source))  messageBodyPart.setFileName(filename)  multipart.addBodyPart(messageBodyPart)  msg.setContent(multipart)  Transport.send( msg )  }}

① Incluir la parámetro: path donde se encuentra nuestro fichero

② En esta sea crea el objeto DataSource.

101 Groovy Script

| 249

Script

@Grab('mysql:mysql-connector-java:5.1.6')①

@Grab('net.sourceforge.jexcelapi:jxl:2.6.12')

@GrabConfig(systemClassLoader=true)

import jxl.*

import jxl.write.*

import groovy.sql.Sql

filename = "informe.xls"

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

  "user",

  "password",

  "com.mysql.jdbc.Driver")

workbook = Workbook.createWorkbook(new File(filename))

sheet = workbook.createSheet("productos", 0)

first=true

i=0

sql.eachRow("select sku, description from products order by sku") { row -> ②

  if( first ){

  sheet.addCell (new Label (0,i,"Sku"))

  sheet.addCell ( new Label (1,i,"Product") ) ③

  first=false

  }

  sheet.addCell( new Label (0,i+1,"$l.sku") ) ④

  sheet.addCell ( new Label (1,i+1,l.description) )

  i++

}

workbook.write()

workbook.close()

101 Groovy Script

250 |

Memento en el TAEJorge Aguilera <[email protected] [mailto:jorge.aguilera@puravida-

software.com]> 2019-09-19

En ete script vamos a hacer uso de la anotación @Memoized de Groovy, útil parasituaciones de cálculo recursivo, mediante un ejemplo práctico para calcular el TAE(Tasa Anual Equivalente) de un préstamo.

No soy financiero por lo que algunas definiciones y/o explicaciones eneste ámbito pueden no ajustarse exactamente a la definición formal.

TAE

La Tasa Anual Equivalente es una referencia orientativa del coste orendimiento efectivo anual de un producto financiero,independientemente de su plazo. Su cáculo incluye la tasa de interésnominal, los gastos, comisiones y permite comparar de una manerahomogénea el rendimiento de productos financieros diferentes

— Wikipedia

Como se puede observar de la definición, el cálculo de la TAE es orientativo. Es decir, noexiste una fórmula matemática exacta que sirva para calcularla. Así mismo tiene encuenta los costes en que se puede incurrir durante la inversión, por ello es un conceptodiferente al interés de una inversión pues este no tiene en cuenta los gastos.

Digamos que queremos pedir un préstamo de 1000 US$ y queremos devolverlo en 10meses. Supongamos que este prestamo nos lo realiza un (buen) amigo y no nos poneningún interés, es decir, cada mes le devolveremos 10 US$ y transcurridos los 10 meseshabremos cancelado nuestra deuda, con un interés 0% y una TAE 0%

Volvemos a pedir este prestamo pero a un conocido que no nos quiere cobrar ningúninterés y que simplemente nos pide que le invitemos a unas cevezas que al final noscuesta 10 US$. El tipo de interés es 0% pero ahora el préstamo no nos sale gratis y vemosque tiene una TAE de cerca de un 2.21%. Si realizas el ejercicio con una calculadora TAEonline verás que puede variar algo dependiendo de la "granularidad" que se aplique.

Básicamente el cálculo es un proceso iterativo que partiendo de un tipo de interésdeterminado trata de buscar por aproximación el valor de la inversión. Si aplicando eseinterés, el valor de la inversión está por debajo del préstamo "real" lo subimos y si por elcontrario está por encima lo bajamos. Este proceso se repite el número de veces que sequiera hasta hallar un interés que aproxime ambos valores.

int closeTo( float a , float b){ ①

101 Groovy Script

| 251

  float diff = a-b  if( 0 == diff || Math.abs(diff) < 0.01){  return 0  }  return a < b ? -1 : 1}float calculaValorInversionPlazo(float cuota, float interes, int plazo){  counter++ ②  cuota/ Math.pow( (1+interes), plazo)}float calculaValorInversion(float cuota, float interes, int plazos){  (1..plazos).sum{n->  calculaValorInversionPlazo(cuota, interes, n)  }}float calculaTae( float prestamo, float comision, int plazos ){  boolean forward=true  float interes = 0.001  float step = 0.001  int deep=1

  float cuota = prestamo / plazos  float objetivo = prestamo - comision

  float lastVac=valorInversion( cuota, interes, plazos) ③

  for(int iter=0; iter<ITERACIONES; iter++){

  int close = closeTo(lastVac, objetivo)

  if( close == 0) { ④  float tae = ( Math.pow(1+interes, 12) -1 )*100  return tae  }

  // estamos por encima del objetivo, poco interes,  if( close > 0 ){  if( !forward ){  deep = deep * 10  forward = true  }  interes += (step/deep)  }

  // estamos por debajo del objetivo, mucho interes  if( close < 0 ){  // si hay que retrodecer cuando ibamos para adelante hay que profundizar  if( forward ){  deep = deep * 10  forward = false  }  interes -= (step/deep)  }

101 Groovy Script

252 |

  lastVac=valorInversion( cuota, interes, plazos) ②  }  return 0}

① Una forma simple de decidir si dos números son aproximados o uno es mayor que elotro

② counter nos servirá para conocer el número de veces que se ejecuta la función. Noforma parte del cálculo de la TAE

③ invocaremos a esta función numerosas veces con parámetros diferentes

④ hemos encontrado un tipo de interés aproximado para la inversión

Como ayuda para testear el código tenemos una función ajena al cálculo del TAE:

void assertTae( float prestamo, float comision, int plazos, float expected){ ②

  float tae=calculaTae(prestamo,comision,plazos)

  println "tae encontrada $tae%"

  if( closeTo(tae, expected) != 0 ){

  throw new RuntimeException(

  String.format("TAE de %f eur a %d meses y apertura %f debia ser %f pero dice %f",

  prestamo, plazos, comision, expected, tae))

  }

}

① utilidad para comprobar si una tae cumple con un valor esperado

Así pues nuestro script puede servir para dado un importe, unos gastos de comisión y unnúmero de meses para devolverlo calcular la TAE simplemente ejecutandocalculaTae(prestamo,comision,plazos)

StressComo hemos visto, buscar la TAE es un proceso intenso en cálculo y con la particularidadde que para una búsqueda pocas veces se llama a una misma función con los parámetrosvaliendo lo mismo pues por ejemplo el tipo de interés va cambiando en cada iteración. Sinembargo podemos imaginar situaciones donde este cálculo se tenga que realizar paracientos 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 asimular una serie de préstamos (6 casos diferentes) un número elevado de veces (10.000veces cada una). Para evaluar el rendimiento, esta función devolverá el tiempotranscurrido desde el inicio hasta completar todas las simulaciones.

Así mismo como se explicó al inicio dispondremos de un contador global que iremosincrementando cada vez que se llame a la función calculaValorInversionPlazo paradeterminar el número de veces que se ha ejecutado la misma

101 Groovy Script

| 253

def runExamples() {  examples = [  [1000, 10, 0, 0],  [1000, 10, 10, 2.21],  [500, 10, 10, 4.52],  [500, 20, 10, 9.37],  [647, 10, 10, 3.47],  [10000, 100, 24, 0.97],  ]

  Date start = new Date()  (0..10000).each {  examples.each { List item ->  assertTae(item[0], item[1], item[2], item[3])  }  }  Date end = new Date()  end-start}

Una vez ejecutadas dichas pruebas en mi máquina (i7, 16Gb memoria) tendremos unresultado similar a:

pasamos por counter 15221522 veces. Tardó 35.964 seconds

MemoizeComo podemos observar en la prueba de stress, algunas funciones son ejecutadasrepetidas veces y en muchos casos el valor de los parámetros han sido los mismos. Sipudieramos disponer de un cache donde asociar los parámetros con el resultadopodrí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í puessimplemente tenemos que añadir esta anotación a aquellas funciones que se vayan aejecutar múltiples veces con los mismos parámetros.

Para poder comparar las dos alternativas en un mismo script vamos a"duplicar" los métodos anotando uno de ellos con @Memoize y el otro no.El método anotado como @Memoize simplemente llamará al métodovisto anteriormente.

Así mismo vamos a usar una variable memoize que indique qué versióndel método queremos ejecutar, de tal forma que cuando esta variablevalga false el algoritmo invocará a la función no anotada con @Memoizey a la inversa

101 Groovy Script

254 |

@Memoizedint closeTo( float a , float b){ ①  float diff = a-b  if( 0 == diff || Math.abs(diff) < 0.01){  return 0  }  return a < b ? -1 : 1}@Memoizedfloat calculaValorInversionPlazoMemoized(float cuota, float interes, int plazo){  calculaValorInversionPlazo(cuota, interes, plazo)}@Memoizedfloat 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 elotro

② counter nos servirá para conocer el número de veces que se ejecuta la función. Noforma parte del cálculo de la TAE

En las mismas condiciones repetimos los test de stress y obtenemos:

pasamos por counter 1482 veces. Tardó 10.044 seconds

Comparativa

Versión Llamadas a función Tiempo ejecución

No memoize 15221522 36seg

Memoize 1482 10seg

101 Groovy Script

| 255

TweetReport (Persistencia con SQLite)Jorge Aguilera <[email protected] [mailto:jorge.aguilera@puravida-

software.com]> 2017-12-09

En este post vamos a analizar un caso de negocio que seguramente no ocurre muy amenudo pero que espero nos ayude a resolver situaciones parecidas. En este caso vamos adesarrollar un script que se podrá ejecutar repetidas veces a lo largo del tiempo (cadahora, varias veces al día, etc) cuya lógica de negocio queremos que se mantenga durantela ejecución de las mismas.

Nuestro Community Manager siente curiosidad por ciertos usuarios de Twitter que nossiguen en nuestra cuenta oficial y quiere hacer un pequeño estudio sobre los mismos queva a durar un cierto tiempo (dias/semanas/meses). Durante este tiempo nos iráproporcionando usuarios de Twitter de los que tendremos que obtener ciertos datospúblicos e ir guardándolos. Así mismo durante este período nos irá pidiendo que lehagamos un pequeño informe con los datos guardados.

En un escenario convencional, podríamos optar por escribir en un fichero plano lainformación de cada ejecución o si queremos algo más robusto instalaríamos un motor debase de datos tipo MySQL, Postgre, SQLServer, etc. Mientras que lo primero es muy simplede mantener resulta muy complejo de gestionar/programar. Por el contrario la segundaopción es mucho más robusta pero más compleja.

A medio de camino de ambas soluciones contamos con proyectos como SQLite que nospermitirán crear y acceder a los datos mediante un acceso JDBC (base de datos) pero sinlas complejidades de tener que instalar y mantener un motor de datos más completo.

DependenciasPara este scritp necesitaremos acceder a Twitter y a una base de datos SQLite por lo quedefinimos 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.Sqlimport twitter4j.Twitter;import twitter4j.TwitterFactory;import twitter4j.StatusUpdate

ArgumentosEl script admitirá la siguiente lista de argumentos:

101 Groovy Script

256 |

• --initialize, recrea la base de datos

• --username, busca un usuario proporcionado por parámetro y lo incluye en la base dedatos

• --all, recorre todos los usuarios existentes en la base de datos y los actualiza

• --report, genera un informe con los datos guardados hasta la fecha

def cli = new CliBuilder(usage: '-i -u username -r')

cli.with {

  h(longOpt: 'help', args:0,'Usage Information', required: false)

  i(longOpt: 'initialize',args:0,'Borrar todos los datos y crear la base de datos en limpio', required:

false)

  u(longOpt: 'username', args:1, argName:'username', 'El usuario a investigar', required: false)

  a(longOpt: 'all',args:0,'Actualiza la info de todos los usuarios registrados', required: false)

  r(longOpt: 'report',args:0,'Genera un report con los usuarios existentes', required: false)

}

def options = cli.parse(args)

if (options.h ) {

  cli.usage()

  return

}

ModeloPara ayudar en la encapsulación de los datos definimos una clase TwitterUser la cual seauto-actualiza si usamos el constructor que proporciona un id de Twitter:

class TwitterUser{  String id  String name  int tweets  int followers  int friends  String timeZone

  TwitterUser(){  }

  TwitterUser( String id){  def tuser = TwitterFactory.singleton.users().showUser(id)  this.id=id  this.name = tuser.name  this.tweets = tuser.statusesCount  this.followers = tuser.followersCount  this.friends = tuser.friendsCount  this.timeZone = tuser.timeZone  }}

101 Groovy Script

| 257

PrepareAntes de realizar ninguna acción contra la base de datos el propio script realiza unapreparación de la misma. Si se indicó el parámetro --initialize realizará un borrado de labase de datos. En cualquier caso creará la tabla si no existe

void prepareDatabase(boolean drop) {

  Sql sql = Sql.newInstance("jdbc:sqlite:tweetreport.db")

  if( drop )

  sql.execute "drop table if exists tweetreport"

  String sentence = """CREATE table if not EXISTS tweetreport

  (id varchar(20), name VARCHAR(40), tweets NUMBER(5), followers NUMBER(5), friends number(5), timezone

varchar(10))

  """

  sql.execute sentence

}

Update UserSi se indica un usuario, el script creará un objeto de la clase TwitterUser y mediante unasimple select por su id determinará si existe ya. Si existe se procederá a actualizar elregistro mientras que si no existe se insertará un registro nuevo

void updateUser(TwitterUser user){

  Sql sql = Sql.newInstance("jdbc:sqlite:tweetreport.db")

  def row = sql.firstRow("select * from tweetreport where id=?",[user.id])

  String sentence = row ?

  "update tweetreport set name=?.name, tweets=?.tweets, followers=?.followers, friends=?.friends,

timezone=?.timeZone where id=?.id"

  :

  "insert into tweetreport (id,name,tweets,followers,friends,timezone) values

(?.id,?.name,?.tweets,?.followers,?.friends,?.timeZone)"

  sql.execute sentence, user

}

Mediante la opción --all hacemos que el script recorra todos los usuarios guardados hastala fecha e invoque el método upate para cada uno de ellos

Report UserEn cualquier momento podemos solicitar que el script genere un informe con el estado dela base de datos, lo cual en nuestro ejemplo consistirá en recorrer los registros ymostrarlos por consola:

101 Groovy Script

258 |

void report(){  Sql sql = Sql.newInstance("jdbc:sqlite:tweetreport.db")  sql.eachRow"select * from tweetreport order by id",{ row->  println row  }}

PersistenciaAl ejecutar el script se creará un fichero tweetreport.db (indicado en la cadena deconexió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 basede datos sencilla y sin complejidades de instalación ni administración

101 Groovy Script

| 259

Script

//tag::dependencies[]

@GrabConfig(systemClassLoader=true)

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

@Grab(group='org.xerial', module='sqlite-jdbc', version='3.21.0')

import groovy.sql.Sql

import twitter4j.Twitter;

import twitter4j.TwitterFactory;

import twitter4j.StatusUpdate

//end::dependencies[]

//tag::model[]

class TwitterUser{

  String id

  String name

  int tweets

  int followers

  int friends

  String timeZone

  TwitterUser(){

  }

  TwitterUser( String id){

  def tuser = TwitterFactory.singleton.users().showUser(id)

  this.id=id

  this.name = tuser.name

  this.tweets = tuser.statusesCount

  this.followers = tuser.followersCount

  this.friends = tuser.friendsCount

  this.timeZone = tuser.timeZone

  }

}

//end::model[]

//tag::prepare[]

void prepareDatabase(boolean drop) {

  Sql sql = Sql.newInstance("jdbc:sqlite:tweetreport.db")

  if( drop )

  sql.execute "drop table if exists tweetreport"

  String sentence = """CREATE table if not EXISTS tweetreport

  (id varchar(20), name VARCHAR(40), tweets NUMBER(5), followers NUMBER(5), friends number(5), timezone

varchar(10))

  """

  sql.execute sentence

}

//end::prepare[]

//tag::update[]

void updateUser(TwitterUser user){

  Sql sql = Sql.newInstance("jdbc:sqlite:tweetreport.db")

  def row = sql.firstRow("select * from tweetreport where id=?",[user.id])

  String sentence = row ?

  "update tweetreport set name=?.name, tweets=?.tweets, followers=?.followers, friends=?.friends,

timezone=?.timeZone where id=?.id"

  :

  "insert into tweetreport (id,name,tweets,followers,friends,timezone) values

(?.id,?.name,?.tweets,?.followers,?.friends,?.timeZone)"

  sql.execute sentence, user

}

//end::update[]

List listUsers(){

101 Groovy Script

260 |

  Sql sql = Sql.newInstance("jdbc:sqlite:tweetreport.db")

  sql.rows("select id from tweetreport")*.id

}

//tag::report[]

void report(){

  Sql sql = Sql.newInstance("jdbc:sqlite:tweetreport.db")

  sql.eachRow"select * from tweetreport order by id",{ row->

  println row

  }

}

//end::report[]

//tag::cli[]

def cli = new CliBuilder(usage: '-i -u username -r')

cli.with {

  h(longOpt: 'help', args:0,'Usage Information', required: false)

  i(longOpt: 'initialize',args:0,'Borrar todos los datos y crear la base de datos en limpio', required:

false)

  u(longOpt: 'username', args:1, argName:'username', 'El usuario a investigar', required: false)

  a(longOpt: 'all',args:0,'Actualiza la info de todos los usuarios registrados', required: false)

  r(longOpt: 'report',args:0,'Genera un report con los usuarios existentes', required: false)

}

def options = cli.parse(args)

if (options.h ) {

  cli.usage()

  return

}

//end::cli[]

prepareDatabase(options.i )

if( options.username ) {

  TwitterUser user = new TwitterUser(options.username)

  updateUser(user)

}

if( options.all ){

  listUsers().each{ id->

  println "update $id"

  TwitterUser user = new TwitterUser(id)

  updateUser(user)

  }

}

if( options.report ){

  report()

}

101 Groovy Script

| 261

Buscando claves ssh en BitBucketJorge Aguilera <[email protected] [mailto:jorge.aguilera@puravida-

software.com]> 2018-03-09

Seguramente este caso de uso no será muy común pero espero que te aporte ideas sitrabajas con BitBucket

Hace unos días en B2Boost [http://www.b2boost.eu] estabamos intentando executar unpipeline de integración contínua para un proyecto que dependía de otros y necesitabaclonarlos primeramente para construir el producto.

Sin embargo surgió un problema cuando nos dimos cuenta que necesitabamos configurartodos los proyectos involucrados y añadirle una clave ssh común para poder clonarlos. Enun escenario ideal esto hubiera sido tan simple como ubicar esta clave a nivel deOrganizació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 organizacionesa la vez y por razones históricas nosotros la estabamos usando en algunos repositorios. Elproblema era que no sabíamos en cuales

Así pues, teníamos dos opciones:

• generar una nueva clave ssh desde nuestro sistema de CI y añadirla a nivel deorganización

• buscar qué proyectos usaban la clave y eliminarla

La primera opción probablemente era la más rápida pero se nos quedarían repositorioscon claves sin usar y además corríamos el riesgo de que otros proyectos se vieranafectados.

La segunda opción era fácil y limpia pero tediosa porque conllevaba navegar entre losrepos buscando la clave en los settings de cada uno … y eran más de 45

Rest al rescateBitBucket 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 tusrepositorios tras haber obtenido un token de autentificación.

Si consultas el manual verás que puedes usar herramientas de línea como curl peroimplementar toda la lógica que necesitamos en un bash puede convertirlo en dificil deentender, depurar y reutilizar. Pero con Groovy y HttpBuilder-ng lo puedes hacer en unscript de menos de 50 lineas

101 Groovy Script

262 |

OAuth ConsumerEn primer lugar necesitas crear un Oauth Consumer en tu cuenta de BitBucket (Profile,Settings, Oauth)

Después de crearlo obtendrás un Key y un Secret que se lo proporcionaremos al script víaargumentos de línea junto con el nombre de nuestra organización

DependenciasSólo vamos a necesitar http-builder y slf4j:

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

@Grab(group='ch.qos.logback', module='logback-classic', version='1.2.3')

import groovyx.net.http.*

import static groovyx.net.http.HttpBuilder.configure

import static groovyx.net.http.ContentTypes.JSON

import static groovy.json.JsonOutput.prettyPrint

import static groovy.json.JsonOutput.toJson

AutorizaciónEsta parte fue la más difícul, no por sus detalles técnicos sino porque el mecanismo deautentificació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 laprimera petición.

La solución pasa por construir nosotros la cabecera de forma manual codificando elusuario y la password en base64 (donde usuario y password son el Key`y el `Secret). Asímismo tenemos que enviar la petición en formato form porque no hemos conseguido queBitBucket reconozca esta petición como JSON

101 Groovy Script

| 263

organization = args[0] // organization

username = args[1] //secretId

password = args[2] //token

creds = "$username:$password".bytes.encodeBase64()

def http = configure {

  request.uri = 'https://bitbucket.org/'

  request.headers['Authorization'] = "Basic $creds"

  request.contentType = JSON[0]

}

def token = http.post{

  request.uri.path='/site/oauth2/access_token'

  request.body=[grant_type:'client_credentials'] ①

  request.contentType = 'application/x-www-form-urlencoded'

  request.encoder 'application/x-www-form-urlencoded', NativeHandlers.Encoders.&form

}.access_token

① send the grant_type as form because BitBucket doesn’t reconigze JSON in this request

BitBucketUna vez obtenido un token podemos configurar un objecto bitbucket que usaremos a lolargo del script con los parámetros comunes a todas las peticiones:

def bitbucket = configure {  request.uri = 'https://api.bitbucket.org/'  request.headers['Authorization'] = "Bearer $token"  request.accept=['application/json']  request.contentType = JSON[0]}

Listar repositoriosPodemos obtener los detalles de todos nuestros repos usando una petición paginada. Cadarepo obtenido contiene gran cantidad de información de la que nosotros vamos a usarúnicamente 'slug' como identificador del repo

101 Groovy Script

264 |

def page=1def repos=[]while( true ) {  def list = bitbucket.get{  request.uri.path="/2.0/repositories/$organization"  request.uri.query=[page:page]  }  repos.addAll list.values  if( repos.size() >= list.size )  break  page++}

Inspeccionar un repo

def keys=[:] ①repos.findAll{it.slug}.each{ repo->  def repokeys = bitbucket.get{  request.uri.path="/1.0/repositories/$organization/${repo.slug}/deploy-keys"  }  keys[repo.slug]=repokeys}

① construimos un mapa con valores tipo repo-name:json

ReportUna vez obtenidos todos los repos iteraremos por todos ellos haciendo una petición enbusca de las claves ssh que tiene y si alguna es el ID que buscamos

println "Total repos founded : ${repos.size()}"

println "Total keys founded : ${keys.size()}"

println prettyPrint(toJson(keys))

def remove = keys.findAll{ it.value.find{ it.key.indexOf('XXXXXXXXXXXXXXXXXXX')!=-1 } } ①

println "Remove: ${remove*.key}"

① filtrar claves que contienen nuestro ID

ConclusionTras ejecutar el script encontramos que sólo unos pocos repos estaban configurados conesta clave ssh pero sin el script probablemente hubieramos perdido una cantidad detiempo importante, navegando por todos ellos corriendo el riesgo de olvidar alguno lo quenos obligaría a volver a comenzar

101 Groovy Script

| 265

Script

//tag::dependencies[]

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

@Grab(group='ch.qos.logback', module='logback-classic', version='1.2.3')

import groovyx.net.http.*

import static groovyx.net.http.HttpBuilder.configure

import static groovyx.net.http.ContentTypes.JSON

import static groovy.json.JsonOutput.prettyPrint

import static groovy.json.JsonOutput.toJson

//end::dependencies[]

//tag::login[]

organization = args[0] // organization

username = args[1] //secretId

password = args[2] //token

creds = "$username:$password".bytes.encodeBase64()

def http = configure {

  request.uri = 'https://bitbucket.org/'

  request.headers['Authorization'] = "Basic $creds"

  request.contentType = JSON[0]

}

def token = http.post{

  request.uri.path='/site/oauth2/access_token'

  request.body=[grant_type:'client_credentials'] ①

  request.contentType = 'application/x-www-form-urlencoded'

  request.encoder 'application/x-www-form-urlencoded', NativeHandlers.Encoders.&form

}.access_token

//end::login[]

//tag::bitbucket[]

def bitbucket = configure {

  request.uri = 'https://api.bitbucket.org/'

  request.headers['Authorization'] = "Bearer $token"

  request.accept=['application/json']

  request.contentType = JSON[0]

}

//end::bitbucket[]

//tag::repos[]

def page=1

def repos=[]

while( true ) {

  def list = bitbucket.get{

  request.uri.path="/2.0/repositories/$organization"

  request.uri.query=[page:page]

  }

  repos.addAll list.values

  if( repos.size() >= list.size )

  break

  page++

}

101 Groovy Script

266 |

//end::repos[]

//tag::v10[]

def keys=[:] ①

repos.findAll{it.slug}.each{ repo->

  def repokeys = bitbucket.get{

  request.uri.path="/1.0/repositories/$organization/${repo.slug}/deploy-keys"

  }

  keys[repo.slug]=repokeys

}

//end::v10[]

//tag::report[]

println "Total repos founded : ${repos.size()}"

println "Total keys founded : ${keys.size()}"

println prettyPrint(toJson(keys))

def remove = keys.findAll{ it.value.find{ it.key.indexOf('XXXXXXXXXXXXXXXXXXX')!=-1 } } ①

println "Remove: ${remove*.key}"

//end::report[]

101 Groovy Script

| 267

Valida tu NIF o SOAP hecho fácilJorge Aguilera <[email protected] [mailto:jorge.aguilera@puravida-

software.com]> 2017-10-26

Qué es SOAP

SOAP (originalmente las siglas de Simple Object Access Protocol) esun protocolo estándar que define cómo dos objetos en diferentesprocesos pueden comunicarse por medio de intercambio de datosXML. Este protocolo deriva de un protocolo creado por Dave Winer en1998, llamado XML-RPC. SOAP fue creado por Microsoft, IBM y otros.Está actualmente bajo el auspicio de la W3C. Es uno de los protocolosutilizados en los servicios Web.

— Wikipedia, https://es.wikipedia.org/wiki/Simple_Object_Access_Protocol

Si como yo, siempre has creido que la S no era de Simple sino de Sufre, yla P de Pánico, este es tu post.

En este script vamos a explicar como consumir un servicio SOAP ofrecido por la AgenciaTributaria Española para consultas de calidad de datos identificativos, es decir, validar siun NIF corresponde a la persona que dice ser. Mediante este WebServices podemos enviaruna lista de NIFs junto con el nombre del titular y la respuesta nos indicará para cada unode ellos si según sus registros existe una correspondencia total, parcial o no existe. De estaforma 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 unabase de datos que será actualizada con la información de respuesta. Así pues deberemoscontar con una tabla nifes con la siguiente estructura:

Field Type Description

nif varchar(10) NIF proporcionado por elusuario

nombre varchar(200) Apellido y nombreproporcionado por elusuario

estado varchar(10) INCORRECTO /IDENTIFICADO /PARCIALMENTE-IDENTIFICADO

nombre_aeat varchar(200) Apellidos y Nombreproporcionado por AEAT

101 Groovy Script

268 |

Así pues el script primeramente creará una petición con los NIF/Nombres en estadoIncorrecto (o nulo) y parseará la respuesta actualizándolos con los datos obtenidos de laAEAT

SOAP y JavaSi nunca has tenido que consumir un servicio SOAP puedes echarle un ojo a este artículohttps://docs.oracle.com/javaee/5/tutorial/doc/bnayn.html de Oracle donde en apariencia noes 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 deintercambios de mensajes XML (y qué mensajes!!) Al ser HTTP no hace falta exponerpuertos especiales en nuestros sistemas (o si los abrimos podremos tratarlos comocualquier puerto que atienda este protocolo), así como aprovechar todo el stack deseguridad como puede ser el uso de SSL.

Por una parte diseñas el interface a exponer junto con sus parámetros tanto de entradacomo de salida y con la ayuda del veneno que prefieras (Axis, Spring, CXF, …) generas unpunto de entrada al mismo.

Con ayuda de tu veneno expones también el WSDL que es lo que necesitará cualquierprograma cliente que quiera consumirlo (como puedes imaginar, el XML no está pensadopara 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 agenerar la parte cliente y que de forma genérica te solicitarán una URL donde resida elWSDL (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 lasincluyas en tu proyecto y puedas así invocar al servicio de forma transparente.

SOAP y GroovyGroovy cuenta con el proyecto groovy-wslite [https://github.com/jwagenleitner/groovy-wslite] elcual hace realmente simple el consumir un servicio SOAP.

Como avisa el README del proyecto, esta librería asume que conoces elservicio que vas a consumir. Es decir, necesitas saber el "nombre" delmétodo que quieres ejecutar así como sus parámetros. En mi caso esto haocurrido en el 99.9999% de las veces, independiente de que un venenome generara el stub

Mediante esta librería tendremos control absoluto tanto de la Request como de laResponse, así como de todos los atributos que se necesiten enviar. Incluso en el peor delos casos, puedes construirte el XML mediante un String y enviarlo directamente.

La idea principal es que modelaremos nuestro intercambio mediante closures y lalibrería generará los mensajes al vuelo, sin necesidad de una fase inicial de

101 Groovy Script

| 269

convertir el WSDL a código:

def client = new SOAPClient('http://www.holidaywebservice.com/Holidays/US/Dates/USHolidayDates.asmx') ①

def response = client.send() {

  body {

  GetMothersDay('xmlns':'http://www.27seconds.com/Holidays/US/Dates/') { ②

  year(2011) ③

  }

  }

}

① Construimos un cliente SOAP indicando la ruta al servicio

② Invocamos la acción GetMothersDay pudiendo incluso cualificarla con su namespace

③ Pasamos un parámetro que nos pide el método llamado year

Así mismo la respuesta la podemos analizar sin necesidad de ningún stub:

println "${response.GetMothersDayResponse?.GetMothersDayResult}" ①

① Vamos "navegando" por la estructura retornada

Consulta de NIF válido según AEATSaber cuándo es el día de la madre de un año cualquiera está muy bien pero con pocautilidad, así que vamos a desarrollar un pequeño script que nos valide si un NIF es de lapersona que dice ser. Para ello la Agencia Tributaria ofrece un servicio SOAP de consulta(hasta 10K NIF en una sóla petición!!) donde nos confirma si un NIF corresponde con unnombre en base a sus registros.

Puedes encontrar la definición de este servicio en Información sobre Web Services deCalidad de Datos Identificativos[http://www.agenciatributaria.es/AEAT.internet/Inicio/Ayuda/Manuales__Folletos_y_Videos/Manuales_tecnicos/Web_service/Modelos_030__036__037/Informacion_sobre_Web_Services_de_Calidad_de_Datos_Identi

ficativos/Informacion_sobre_Web_Services_de_Calidad_de_Datos_Identificativos.shtml]

Para poder usar este servicio necesitarás un certificado digital que teidentifique. Puede ser de persona física, empleado público, FNMT oempresas. Según entiendo se requiere simplemente para evitar el abusodel servicio

A continuación un ejemplo de consulta y su respuesta extraidas del documento

101 Groovy Script

270 |

Ejemplo de consulta

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"

xmlns:vnif="http://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/burt/j

dit

/ws/VNifV2Ent.xsd"> ①

<soapenv:Header/>

<soapenv:Body>

<vnif:VNifV2Ent> ②

<vnif:Contribuyente> ③

<vnif:Nif>99999999R</vnif:Nif> ④

<vnif:Nombre>ESPAÑOL ESPAÑOL JUAN</vnif:Nombre></vnif:Contribuyente>

</vnif:VNifV2Ent>

</soapenv:Body>

</soapenv:Envelope>

① Schema "vnif" que hay que utilizar.

② Usando el schema "vnif" identificamos la funcion a ejecutar

③ Usando el schema "vnif" describimos una estructura de entrada

④ Usando el schema "vnif" indicamos un parametro

Ejemplo de respuesta

<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/"

xmlns:xsd="http://www.w3.org/2001/XMLSchema"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

 <env:Body>

 <VNifV2Sal:VNifV2Sal

xmlns:VNifV2Sal="http://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/b

urt/jdit/ws/VNifV2Sal.xsd"> ①

 <VNifV2Sal:Contribuyente> ②

 <VNifV2Sal:Nif>99999999R</VNifV2Sal:Nif>

 <VNifV2Sal:Nombre>ESPAÑOL ESPAÑOL JUAN</VNifV2Sal:Nombre> <VNifV2Sal:Resultado>Identificado</VNifV2Sal:Resultado>

 </VNifV2Sal:Contribuyente>

 </VNifV2Sal:VNifV2Sal>

 </env:Body>

</env:Envelope>

① Schema "VNIFV2Sal" a utilizar

② Estructura de respuesta

Consultar un NIFPara invocar ese servicio un script simple sería:

101 Groovy Script

| 271

@Grab('com.github.groovy-wslite:groovy-wslite:1.1.2')

def client = new SOAPClient('https://www1.agenciatributaria.gob.es/wlpl/BURT-JDIT/ws/VNifV2SOAP')

def response = client.send() {

  envelopeAttributes([

  "xmlns:vnif":

"http://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/burt/jdit/ws/VNif

V2Ent.xsd"

  ])

  body {

  'vnif:VNifV2Ent' {

  'vnif:Contribuyente'{

  'vnif:Nif'(0123456789X')

  'vnif:Nombre'('APELLIDO APELLIDO NOMBRE')

  }

  }

  }

}

println "${response.VNifV2Sal.Contribuyente.Resultado}"

Consultar hasta 10K NIFsPartiendo del ejemplo anterior donde consultamos un NIF vamos a desarrollar un scriptmás complejo donde la petición se construye dinámicamente en función de los valores dela base de datos. Así mismo trataremos una respuesta iterando por cada elemento deinterés.

dependencias

@Grab('com.github.groovy-wslite:groovy-wslite:1.1.2')@Grab('mysql:mysql-connector-java:5.1.6')@GrabConfig(systemClassLoader=true)

crear SOAP Client

  def client = new SOAPClient('https://www1.agenciatributaria.gob.es/wlpl/BURT-JDIT/ws/VNifV2SOAP') ①

① Proporcionamos directamente la URL al servicio

crear cabecera

  def response = client.send() {

  envelopeAttributes([

  "xmlns:vnif":

"http://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/burt/jdit/ws/VNif

V2Ent.xsd" ①

  ])

① Podemos ajustar la petición, incluyendo namespaces por ejemplo

101 Groovy Script

272 |

body dinámico y con un bucle

  body {

  sql.eachRow("select * from nifes where estado is null or estado <> 'IDENTIFICADO' "){ row-> ①

  'vnif:VNifV2Ent' { ②

  'vnif:Contribuyente'{ ③

  'vnif:Nif'(row.nif)

  'vnif:Nombre'(row.nombre.toUpperCase())

  }

  }

  }

① Recorremos la tabla y construimos elementos dinámicamente

② Cualificamos la función según el schema que corresponda si es necesario

③ Cualificamos los parametros según el schema que corresponda si es necesario

tratamiento respuesta dinámico

  sql.withBatch( batchSize, "update people set nombre_aeat=?,estado=? where nif=?"){ ps->

  response.VNifV2Sal.Contribuyente.each{ result -> ①

  ps.addBatch([

  "$result.Nombre".toString(), ②

  "$result.Resultado".toString(),

  "$result.Nif".toString()

  ])

  erroneos+= "$result.Resultado".equals('IDENTIFICADO') ? 0 : 1

  }

  }

① response.VNifV2Sal.Contribuyente nos devuelve un array de elementos Contribuyente

② Obtenemos el valor de un elemento mediante .text() o con .toString()

excepciones

} catch (SOAPFaultException sfe) { ①  println sfe.request?.contentAsString  println sfe.request.contentAsString} catch (SOAPClientException sce) { ②  // This indicates an error with underlying HTTP Client (i.e., 404 Not Found)  println sce.request?.contentAsString  println sce.response?.contentAsString}

① Excepción de "negocio"

② Excepción de "transporte"

101 Groovy Script

| 273

Ejecución y certificadoComo ya se ha comentado el servicio de la AEAT requiere que la conexión sea realizadacon un certificado por parte del cliente que sirva para identificarle. La forma más comúnes tener este certificado en un fichero .p12 protegido con contraseña de tal forma que alejecutar el script podamos indicarle a Groovy (en realidad a Java) donde encontrarlo.

Así pues la ejecución del script sería algo parecida a:

groovy -Djavax.net.ssl.keyStore=./test.p12 -Djavax.net.ssl.keyStorePassword=LAPWD

-Djavax.net.ssl.keyStoreTYpe=PKCS12 ConsultaNif.groovy USER PWD ①

① LAPWD correspondería a la password para abrir el keystore mientras que USER YPWD corresponderían al usuario de la base de datos

101 Groovy Script

274 |

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

101 Groovy Script

| 275

} catch (SOAPClientException sce) { ②

  // This indicates an error with underlying HTTP Client (i.e., 404 Not Found)

  println sce.request?.contentAsString

  println sce.response?.contentAsString

}

  //end::exceptions[]

101 Groovy Script

276 |