Graalvanizando un script de Groovy
Tags: java, groovy, graalvm, linkedin
Published: 2023-04-22 22:48:13

---
title: Graalvanizando un script de Groovy
published: true
date: 2023-04-21 00:00:00 UTC
tags: java,groovy,graalvm,linkedin
canonical_url: https://jorge.aguilera.soy/blog/2023/groovy-graalvm.html
---

En este post voy a publicar el código y pasos a seguir para convertir un script de Groovy en un binario de tal forma que para su ejecución no se necesita ni Groovy ni Java instalado. Además, aunque es simple, la velocidad de ejecución es sensiblemente mejor

En concreto el aplicativo va a subir un post a Linkedin (por ahora texto, luego veré de adjuntar imágenes) usando el nuevo api REST de esta plataforma. Para ello pedirá el `author` y el `accessToken` para poder publicar

## Requisitos

Para completar todos los pasos se requiere tener instado:

- sdkman, [https://sdkman.io/install](https://sdkman.io/install)

Una vez instalado le indicaremos que nos instale las versiones de Java y Groovy necesarias para generar el binario:

`sdk use groovy 4.0.11`

`sdk use java 22.3.r19-grl`

## Configuración

Para generar una imagen nativa de un script de groovy es necesario que este sea compilado en modo estático (lo que resta mucho de la expresividad de Groovy, pero …​)

compiler.config

```
withConfig(configuration) {
    ast(groovy.transform.CompileStatic)
    ast(groovy.transform.TypeChecked)
}
```

## GroovyScript

Para mejorar la legibilidad del código he dividido el script en 2 ficheros .groovy, uno de ellos con métodos estáticos orientado a hacer las peticiones HTTP get y post y el otro que será el que contenga la "logica de negocio"

HttpUtil.groovy

```
import groovy.json.JsonOutput
import groovy.json.JsonSlurper

import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse

class HttpUtil{

    static Map postJson(Map map, String url, String token) {
        println "-> ${JsonOutput.prettyPrint(JsonOutput.toJson(map).toString())}"
        def request = HttpRequest.newBuilder(new URI(url))
                .headers(
                        "Content-Type", "application/json",
                        "X-Restli-Protocol-Version", "2.0.0",
                        "LinkedIn-Version", "202301",
                        "Authorization", "Bearer $token"
                )
                .POST(HttpRequest.BodyPublishers.ofString(JsonOutput.toJson(map).toString()))
                .build()
        def response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofInputStream())
        if( response.statusCode() > 299 ){
            println response
            return null
        }
        def result = response.body().text
        println "<-${result}"
        if( !result ){
            return null
        }
        new JsonSlurper().parseText(result) as Map
    }

    static Map getJson(String url, String token){
        def request = HttpRequest.newBuilder(new URI(url))
                .headers(
                        "Content-Type", "application/json",
                        "X-Restli-Protocol-Version", "2.0.0",
                        "LinkedIn-Version", "202301",
                        "Authorization", "Bearer $token"
                )
                .GET()
                .build()
        def response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString())

        def result = response.body()
        println "<-${result}"
        new JsonSlurper().parseText(result) as Map
    }
}
```

Aunque parece tener mucho código en realidad es una implementación básica para usar las clases Http de Java y poder hacer un get y un post con unas cabeceras determinadas así como enviar y recibir Mapas como si fueran Json.

PostContent.groovy

```
@GrabConfig(systemClassLoader = true)
@Grab('info.picocli:picocli-groovy:4.7.3')

@picocli.groovy.PicocliScript2

import groovy.transform.Field
import static picocli.CommandLine.*

@Option(names = ["-a", "--author"], description = "nickname", required=true)
@Field String author = ''

@Option(names = ["-k", "--token"], description = "token", required=true)
@Field String token = ''

@Option(names = ["-t", "--test"], description = "run as test")
@Field boolean test = false

@Parameters
@Field List<String> content = []

String postURL = "https://api.linkedin.com/rest/posts"

if( test ){
        postURL="https://httpbin.org/delay/0"
}

Map post = [
        "author" : "urn:li:person:${author}",
        "commentary" : content.join(' '),
        "visibility" : "PUBLIC",
        "distribution" : [
                "feedDistribution" : "MAIN_FEED",
                "targetEntities" : [],
                "thirdPartyDistributionChannels": []
        ],
        "lifecycleState" : "PUBLISHED",
        "isReshareDisabledByAuthor": false
]

HttpUtil.postJson(post, postURL, token)
```

Este script usa Picocli para facilitar el parseo de comandos. Simplemente necesita un author y un token y además podemos indicar que se ejecute en modo test (-t) con lo que usará el servidor de httpbin como backend para testearlo

Por lo demás es un "simple" post con el formato que Linkedin espera para publicar un post

## Graalvm

Las fases para convertir este script en binario las he separado en:

- Pasar de Groovy a Java usando groovyc

- Ejecutarlo con Java en modo test para que el agente genere la configuracion de Graalvm

- Ejecutar el native-image para generar el binario

compile.sh

```
#!/bin/bash
set -e

# sdk use groovy 4.0.11
# sdk use java 22.3.r19-grl

echo compiling script
groovyc --configscript=compiler.groovy -d out PostContent.groovy

CP="$CP:./out"
CP="$CP:$HOME/.sdkman/candidates/groovy/4.0.11/lib/groovy-4.0.11.jar"
CP="$CP:$HOME/.sdkman/candidates/groovy/4.0.11/lib/groovy-json-4.0.11.jar"
CP="$CP:$HOME/.sdkman/candidates/groovy/4.0.11/lib/groovy-cli-picocli-4.0.11.jar"
CP="$CP:$HOME/.groovy/grapes/info.picocli/picocli/jars/picocli-4.7.3.jar"
CP="$CP:$HOME/.groovy/grapes/info.picocli/picocli-groovy/jars/picocli-groovy-4.7.3.jar"

echo generating graalvm configuration
java -Dgroovy.grape.enable=false -agentlib:native-image-agent=config-output-dir=conf/ \
    -cp "$CP" \
    PostContent -t -a test -k test test

echo building native image
native-image -Dgroovy.grape.enable=false \
    --no-server \
    --no-fallback \
    --report-unsupported-elements-at-runtime \
    --initialize-at-build-time \
    --initialize-at-run-time=org.codehaus.groovy.control.XStreamUtils,groovy.grape.GrapeIvy \
    -H:ConfigurationFileDirectories=out/conf/ \
    --enable-url-protocols=http,https \
    -cp "$CP" \
    -H:ConfigurationFileDirectories=conf/ \
    PostContent
```

Si todo va bien al final del proceso (poco más de 1 minuto) tendremos un binario `postcontent` que podremos ejecutar

`./postcontent -a NH123123 -k A_BEARER_TOKEN_YOU_CAN_USE_MY_PREVIOUS_POST Hi this is a post from groovy script`

<dl>
<dt>INFO</dt>
<dd>
<p>Para obtener el token te remito a otro de mis post donde te cuento como generarlo, o si lo prefieres usar el servicio
que he publicado.</p>
</dd>
</dl>

## Conclusion

Las nuevas versiones de Groovy y Graalvm permiten (con un poco de esfuerzo lo admito) poder crear comandos de consola binarios, lo que para mí abre la puerta a poder distribuir utilidades usando mi lenguaje favorito

Queda pendiente para un futuro post el poder adjuntar imágenes, que con el nuevo api Linkedin lo ha complicado un poco más.