JBang, Script en java

Contexte

Pour le besoin d’un projet, j’avais besoin d’extraire un certificat au format PEM à partir d’un ensemble de clés JWKS (JSON Web Key Sets). Je savais que je pouvais le faire très facilement en Java. Cependant, créer un projet Java jetable n’est pas très motivant pour simplement quelques lignes de code. D’où l’idée d’écrire un script pour cela.

En effet, depuis le JDK 11, il est possible de réaliser un script en Java (JEP 330: Launch Single-File Source-Code Programs). Malheureusement, cela est limité aux classes fournies par le JDK.

Depuis quelques temps, j’entends parler de jbang. Il permettrait de lever cette contrainte. C’est l’occasion de le mettre à l’épreuve.

Installation

La procédure d’installation est très simple sur la page téléchargement du site du projet. La page décrit les opérations selon les différents systèmes d’exploitation.

Pour ma part, n’ayant pas de package Debian, j’ai choisi la solution manuelle en m’assurant que le programme soit accessible via la variable $PATH.

astuce Bien que je possède plusieurs versions de JVM, il est intéressant de savoir que l’installation du JDK au préalable n’est pas nécessaire.

Premier lancement

L’outil me propose d’initialiser le script comme il faut avec l’option init. Laissons-nous guider pour commencer.

jbang init Hello.java

Un fichier Hello.java est généré. C’est l’occasion de voir son anatomie.

///usr/bin/env jbang "$0" "$@" ; exit $?
// //DEPS <dependency1> <dependency2>

import static java.lang.System.*;

public class Hello {

    public static void main(String... args) {
        out.println("Hello World");
    }
}

La première ligne est le fameux shebang. C’est l’information qui permet de savoir quelle programme doit être exécuté.

avertissement L’usage du style // au lieu du classique #! est une astuce (bash, zsh, etc…​) qui permet l’exécution comme un script tout en ayant un code Java valide.

Sur la deuxième ligne, nous aurons l’occasion de revenir dessus.

Le reste, c’est du code Java classique. Une classe Hello ayant une méthode main.

Passons à l’exécution, il suffit de taper la commande suivante :

jbang  Hello.java

Nous obtenons l’affichage suivant :

[jbang] Building jar...
Hello World

Grâce au Shebang, nous pouvons aussi appeler directement le script

./Hello.java

avertissement Si besoin, il faut donner les droits d’exécution avec la commande suivante : chmod u+x Hello.java.

Nous obtenons l’affichage suivant :

Hello World

astuce Le script n’a pas été modifié. Donc, il n’a pas été nécessaire de reconstruire l’archive.

Patron ("template")

Il est possible d’utiliser des patrons. Un certain nombre existe déjà comme la possibilité d’avoir ce qui faut pour écrire un outil en ligne de commande.

jbang init --template=cli Cli.java

ou en utilisant les options courtes :

jbang init -t cli Cli.java

De là, nous pouvons exécuter directement le script

./Cli.sh
[jbang] Building jar...
Hello World!

Cela n’a rien d’étonnant par rapport à tout à l’heure. Sauf que comme tout outil en ligne de commande qui se respecte, nous avons une aide à notre disposition sans effort de notre part.

./Cli.java --help
Usage: Cli [-hV] <greeting>
Cli made with jbang
      <greeting>   The greeting to print
  -h, --help       Show this help message and exit.
  -V, --version    Print version information and exit.

Je vois que j’ai un argument qui permet de préciser la salutation. De ce pas, j’exécute la commande suivante :

./Cli.java Lilian
Hello Lilian

Passons au code,

///usr/bin/env jbang "$0" "$@" ; exit $?
//DEPS info.picocli:picocli:4.5.0

import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Parameters;

import java.util.concurrent.Callable;

@Command(name = "Cli", mixinStandardHelpOptions = true, version = "Cli 0.1",
        description = "Cli made with jbang")
class Cli implements Callable<Integer> {

    @Parameters(index = "0", description = "The greeting to print", defaultValue = "World!")
    private String greeting;

    public static void main(String... args) {
        int exitCode = new CommandLine(new Cli()).execute(args);
        System.exit(exitCode);
    }

    @Override
    public Integer call() throws Exception { // your business logic goes here...
        System.out.println("Hello " + greeting);
        return 0;
    }
}

Nous voyons au niveau du code, que c’est la librairie picocli qui est utilisée afin de simplifier les gestions des arguments.

Mais le plus intéressant, c’est la seconde ligne.

//DEPS info.picocli:picocli:4.5.0

Par cette ligne, nous exprimons le fait que le script a besoin de la librairie picocli. Nous retrouvons le schéma classique groupId:artifactId:version.

Mise en pratique

J’initialise mon script

jbang init -t cli GetKey.java

Je précise la librairie que je souhaite utiliser, en l’occurrence jose4j

//DEPS org.bitbucket.b_c:jose4j:0.7.6

Je suis un aficionados de vi. Mais effectivement, cela n’est pas top pour le développement Java par rapport à un IDE.(Gestion des imports, autocomplétion, validation syntaxique, etc..)

Là encore, jbang vient à notre rescousse en proposant un mode édition.

jbang edit --open=vscode GetKey.java

Votre IDE s’ouvre : jbang vscode

Je précise mon paramètre de ligne de commande :

@Parameters(index = "0", description = "URL vers JSON Web Key Sets (JWKS)")
private String url;

J’écris le code pour récupérer le certificat et l’afficher au format PEM

@Override
public Integer call() throws Exception {
    HttpsJwks jwks = new HttpsJwks(url);
    List<JsonWebKey> liste = jwks.getJsonWebKeys();
    for (JsonWebKey jsonWebKey : liste) {
        String cert = RsaKeyUtil.pemEncode(jsonWebKey.getPublicKey());
        System.out.println(cert);
    }
    return 0;
}

il me reste plus qu’à l’éxecuter

./GetKey https://MON.SITE/auth/realms/OMS-Intra/protocol/openid-connect/certs
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqlWybNYyj0hxoE8AcpGT
...
...
oU1MWOIhL/9ex7UB+6C3Uj2Mg3uoHQZB2PaeHHTb0fm957ufzWHGUyFd0q3UjUTQ
2N7mvALKLPsakuppHzRMeXHt7Zhu8kUfhPhQDUYc+l5bB1HhUqvZ9rjVTDRP7R+T
uQIDAQAB
-----END PUBLIC KEY-----

Et voilà, le tour est joué. A votre tour !