Les addons, c'est encore sa Forge

GNU/Linux Magazine n° 189 | janvier 2016 | Jérôme Baton
Creative Commons
  • Actuellement 0 sur 5 étoiles
0
Merci d'avoir participé !
Vous avez déjà noté cette page, vous ne pouvez la noter qu'une fois !
Votre note a été changée, merci de votre participation !
Dans ce nouvel article, nous allons finir d'apprendre comment créer des addons pour Forge, le framework de création rapide d'applications Java EE édité par JBoss/Red Hat, et ainsi, l'étendre selon nos envies. Vous avez aimé la création de projet Java EE du premier article [1], alors vous allez aimer créer un addon pour manipuler des projets Android !.

Pour suivre cet article, il est préférable de connaître l'utilisation de JBoss Forge, de JBoss Developper Studio (JDS) bref, d'avoir lu les articles précédents consacrés à Forge [1][2].

Le but de cet article est de vous permettre de créer VOS propres outils pour VOS projets en abordant des points essentiels, mis en œuvre dans un contexte Android.
Ces outils vous permettront d'automatiser des tâches répétitives dans vos propres projets, quels qu'ils soient... donc moins d'ennuis au quotidien. Ô joie !

1. Le mode « moins simple mais mieux »

De façon « inception-nelle », le point de départ est d'utiliser Forge pour générer l'addon. Nous allons donc créer une nouvelle classe.

Ouvrez la fenêtre Forge pour lancer la commande Addon : New UI Command (voir figure 1).

Fig. 1 : Fenêtre de commande Forge.

Une fenêtre telle que celle présentée en figure 2 est affichée

Fig. 2 : Interface de génération d'un addon.

Le champ Type Name correspond au nom de la classe qui contiendra la commande, dont le nom est saisi dans le champ Command Name.

À l'identique de la façon précédente, il est possible de saisir plusieurs catégories. Ici, la catégorie reste Android. À la validation, le code généré est le suivant :

package org.wadael.forge.simple.android.commands;

 

public class I18NCommand extends AbstractUICommand{

 

@Override

public UICommandMetadata getMetadata(UIContext context){

return Metadata.forCommand(I18NCommand.class).name("addNewLanguage")

.category(Categories.create("Android"));

}

 

@Override

public void initializeUI(UIBuilder builder) throws Exception{

}

 

@Override

public Result execute(UIExecutionContext context) throws Exception{

return Results

.success("Command 'addNewLanguage' successfully executed!");

}

}

Cela reste simple, il n'y a qu'une classe qui possède trois méthodes.

Note

On ne peut donc avoir qu'une seule commande par classe.

La méthode getMetaData indique au moteur de Forge (nommé Furnace) le nom de la commande et sa catégorie. Le corps de la méthode indique ce qui a été saisi au préalable. On peut y ajouter une description pour être plus complet (voir code ci-dessous).

La seconde méthode, initializeUI, a pour rôle de mettre en place les composants qui constitueront l'interface utilisateur de cet « écran ».

La troisième méthode correspond aux instructions qui seront exécutées lorsque les saisies seront effectuées et que l'utilisateur cliquera sur Finish.

Une quatrième méthode, validate, est à ajouter afin de vérifier les saisies de l'utilisateur et renvoyer des messages d'erreur significatifs. Elle sera exécutée à chaque changement de valeur d'un champ.

Pour créer une commande qui dupliquera le fichier strings.xml pour créer un fichier pour une langue donnée (comme strings_fr pour le français), le code sera :

package org.wadael.forge.simple.android.commands;

 

public class I19NCommand extends AbstractUICommand {

public static List<String> PAYS ;

 

@Inject

private UIInput<String> folder;

 

@Inject

@WithAttributes(label="language",required=true, description="New language to support" )

private UISelectOne<String> language ;

 

public I19NCommand() {

PAYS = new ArrayList<String>();

PAYS.add("fr");

PAYS.add("de");

PAYS.add("ar");

PAYS.add("dg");

}

 

@Override

public UICommandMetadata getMetadata(UIContext context) {

return Metadata.forCommand(I19NCommand.class).name("addNewLanguage").category(Categories.create("Android")).description("Adds a new supported language to the application.");

}

 

@Override

public void initializeUI(UIBuilder builder) throws Exception {

language.setValueChoices(PAYS);
 // Ci-dessous, ces lignes pourraient remplacer l'annotation @WithAttributes

// language.setRequired(true);

// language.setDescription("New language to support");

folder.getFacet(HintsFacet.class).setInputType(InputType.DIRECTORY_PICKER);

folder.setRequired(true);

builder.add(folder);

builder.add(language);

}

 

@Override

public void validate(UIValidationContext validator) {

super.validate(validator);

// Rien d'autre à vérifier avec ces composants.

}

 

@Override

public Result execute(UIExecutionContext context) throws Exception {

File master = new File(folder.getValue() + File.separator + "res" + File.separator + "values" + File.separator + "strings.xml");

File dest = new File(folder.getValue() + File.separator + "res" + File.separator + "values" + File.separator + "strings_" + language.getValue() + ".xml");

 

FileUtils.copyFile(master, dest);

return Results.success("COPIE faite : fichier " + dest.getAbsolutePath() + " créé !");

}

}

 

À l'exécution, le résultat sera celui présenté en figure 3.

Fig. 3 : Sélection d'une variante de breton comme nouvelle langue pour une application

Et cela marche, le fichier strings.xml est dupliqué avec le suffixe choisi. Regardons le code de plus près. Les nouveautés par rapport au code précédent sont les attributs, composants graphiques, annotés avec @Inject. Il s'agit d'une annotation de la norme CDI. Le conteneur gère l'instanciation (selon le principe d'injection de dépendance).

Note

Notons le préfixe UI de ces classes, pratique pour rechercher avec <Ctrl> + <Shift> + <T>

La ligne suivante est non-triviale (et pire, introuvable avec seulement l'autocomplétion) :

folder.getFacet(HintsFacet.class).setInputType(InputType.DIRECTORY_PICKER);

Elle permet d'indiquer que le composant doit permettre de choisir un répertoire alors qu'il est déclaré comme stockant une chaîne :

private UIInput<String> folder;

Je vous invite à regarder les autres constantes définies dans InputType pour avoir la liste des types de données supportés par Forge.

Le caractère obligatoire d'une information est ajouté via la méthode setRequired.

Cette approche est déjà plus professionnelle, et grâce au choix des types de saisies et aux validations l'on peut commencer à partager ses addons avec des utilisateurs qui ne sont pas au fait de son implémentation.

L'inconvénient de cette approche est aussi son caractère mono-écran. On est toujours dans une vision simpliste : on saisit des valeurs, on valide et l'addon s’exécute. Impossible de faire faire une saisie à l'utilisateur sous condition de la valeur d'une autre saisie.

Un autre inconvénient, qui n'aura pas d'influence pour tous vos addons mais seulement pour ceux nécessitant le plus d'entrées par l'utilisateur, est que le nombre de widgets de saisie est limité par l'espace vertical de votre écran. Pour le test, j'ai créé une commande avec 10 paires de sélecteurs de répertoires et de listes déroulantes. Sur un écran 16/10 en 1080p, seules huit paires sont affichées et les deux dernières sont perdues car inaccessibles. Évidemment, faire saisir 20 informations à l'utilisateur ressemble (très) fortement à un problème de design mais, à mon humble avis, la limite est basse notamment pour les cas où l'interface serait créée à la volée à partir de métadonnées JDBC et où il faudrait proposer de modifier plusieurs informations (prérenseignées) par colonne (comme le type Java, le nom du champ, un commentaire, etc...). Une demande d'amélioration est ouverte sur ce point (pour que le conteneur soit scrollable) [3].

Comme pour chacun de ses outils, il est bon d'en connaître les limites. En voici une.

2. Collaboration des addons

L'illustration la plus simple de la collaboration entre addons est l'utilisation des composants d'interface utilisateur telle que UISelectOne qui appartient à l'addon UI.

Tout comme l'utilisation de composants est possible, il est possible de créer un addon qui partagera ses fonctionnalités avec d'autres addons. Pour cela, il faut lors de la création du projet, cocher la case Create API, Impl, SPI, Tests and Addons modules (voir figure 4).

Fig. 4 : La terrible case à cocher qui peut remplir votre workspace

La première conséquence sera la création de multiples projets dans votre workspace. Ce cas de figure est donc à réserver aux addons auxquels vous souhaiterez faire proposer des fonctionnalités réutilisables par d'autres addons.

Note

Cela va sans dire mais cela va mieux en le disant, un addon restant un projet de type Maven, on pourra y ajouter des dépendances qui ne sont pas des addons.

3. Autres addons

Pour utiliser d'autres addons, il faut soit les choisir à la création du projet soit les ajouter ultérieurement dans son fichier de configuration Maven (pom.xml), sans oublier la balise suivante au sein de la déclaration de dépendance :

<classifier>forge-addon</classifier>

 

Note

Mon astuce est, à la création du projet, de sélectionner tous les addons disponibles pour qu'ils soient référencés dans le fichier pom.xml, où je mets en commentaire ceux que je n'utilise pas (encore)

3.1 Alliance ou alliage ? Mixer les addons

Je reviens non pas sur ce groupe des années 90 mais sur l'addon Configuration, qui permet de mémoriser des informations de façon persistante, qui seront disponibles ad vitam æternam et pour tous les addons puisque stockées sur disque. Il permet donc de passer des informations d'une commande à l'autre (sur un même poste).

D'une certaine manière, il fond les addons entre eux, crée un lien entre eux.

Dans le cadre de mon job de jour, j'ai dû écrire plusieurs commandes qui utilisent les mêmes informations JDBC (url, username et mot de passe), aussi, plutôt que de les faire saisir en paramètre de chaque commande, j'ai créé des commandes pour faire saisir ces informations et les partager avec les véritables addons utilisateurs qui s'arrêtent en échec (avec return Results.fail(«message »)) si une des informations n'est pas saisie.

Le code ci-dessous décrit plusieurs commandes qui utilisent l'addon Configuration selon ce principe.
Ainsi, cette première classe l'utilise pour mémoriser une valeur métier :

public class MyConfiguration{

public static final String CLEF = "maClef_Valeur1";

 

@Inject

private Configuration conf;

 

@Command(value="set-valeur1",categories="metier")

public Result setLaValeurMetier(

@Option(value = "valeur", label = "Quelle valeur donner ?", required = true, type = "String", defaultValue = "8000", enabled = true)

String valeur){

conf.setProperty(CLEF, valeur);

return Results.success(CLEF +" vaut désormais " + conf.getString(CLEF));

}

 

public String getLaValeurMetier(){

return conf.getString(CLEF);

}

}

On pourrait multiplier les valeurs à renseigner dans cette classe et pour chacune, créer une méthode de valorisation (set) et de consultation (get). La deuxième classe est la classe utilisatrice dans laquelle on injecte la première (merci Furnace version CDI).

public class MyConfigurationUser {

@Inject

MyConfiguration conf ;

 

@Command(value="affiche-valeur1",categories="metier")

public Result afficheLaValeurMetier(){

String leopard = conf.getLaValeurMetier();

if(leopard==null) return Results.fail("Pas de valeur pour " + MyConfiguration.CLEF);

return Results.success(MyConfiguration.CLEF + " vaut " + leopard );

}

}

L'utilisation de ces commandes dans la console donne :

[SimplistAddon]$ set-valeur1 --valeur 2222

***SUCCESS*** maClef_Valeur1 vaut désormais 2222

[SimplistAddon]$ affiche-valeur1

***SUCCESS*** maClef_Valeur1 vaut 2222

Avertissement !

À savoir : les informations ainsi stockées le sont en clair dans le fichier ~/.forge/config.xml. Tout addon a accès à l'ensemble de ces informations. Oui, même ceux des autres !

3.2 Chers patrons

Parce que l'on ne peut pas générer tous les fichiers d'un projet avec des concaténations de String, il y a la possibilité d'utiliser des patrons (ou modèles, ou templates).

Basé sur le bien connu projet FreeMarker [4], dont l'utilisation est des plus simples, le principe est de créer un fichier modèle contenant des références puis que ces références soient remplacées par les valeurs fournies. La formule magique est : fichier modèle + données = fichier personnalisé

avec les données pouvant provenir d'objets personnalisés, de Maps ou de types simples.

Le code suivant lit un fichier contenu dans le jar de votre addon et l'affiche après avoir utilisé une Map comme source de données. Ce template Freemarker, nommé voila.txt a le contenu suivant :

Dennis dit :

${slogan}

 

Il est utilisé par le code suivant :

public class FileAddon extends AbstractUICommand {

@Inject

private TemplateFactory tfactory;

@Inject

private ResourceFactory rfactory;

 

@Override

public UICommandMetadata getMetadata(UIContext context) {

return Metadata.forCommand(FileAddon.class).name("readVoila")

.category(Categories.create("test"));

}

 

@Override

public void initializeUI(UIBuilder builder) throws Exception {

}

 

@Override

public Result execute(UIExecutionContext context) throws Exception {

URL voilaURL = getClass().getClassLoader().getResource("voila.txt");

Resource<URL> templateResource = rfactory.create(voilaURL);

Template template = tfactory.create(templateResource, FreemarkerTemplate.class);

Map<String, Object> params = new HashMap<String, Object>();

params.put("slogan", "JBoss Forge : c'est bon, mangez-en");

String output = template.process(params) ;

return Results.success(output);

}

}

Il s'agit de récupérer une référence au fichier (URL) puis de la transformer en template. Les paramètres donnés serviront à des remplacements de chaînes.

Pour insérer des listes, utiliser les balises #list, comme ceci :

<#list entries as entry>

${entry.name}

</#list>

Ici la liste a été ajoutée sous le nom entries.

3.3 Classes à connaître

Puisque que l'orientation principale des addons est de générer du code source, autant savoir se repérer dans le projet. Pour cela, la classe org.jboss.forge.addon.projects.ProjectFactory est essentielle. Obtenue par injection, elle permet d'obtenir le projet courant à une condition, qui est d'étendre non pas AbstractUICommand mais AbstractProjectCommand (quelques implémentations de méthodes à prévoir). Ensuite, un simple appel tel que DirectoryResource dr = (DirectoryResource) proj.getRoot(); avec Project proj = getSelectedProject(context); vous renvoie un objet de type DirectoryResource qui correspond au répertoire racine du projet. À vous de voir si vous devez laisser choisir le répertoire ou non.

La classe org.jboss.forge.addon.resource.ResourceFactorya pour intérêt de vous permettre d'accéder à des ressources (DirectoryResource, FileResource, JavaResource, …).

La classe org.jboss.forge.addon.templates.TemplateFactoryvue dans le code précédent permet de générer dynamiquement du contenu, que l'on insère par exemple dans une FileResource avec setContents(String).

Avec ces seules trois classes, vous voici prêt à générer du code source ! Si vous devez étudier un code source, intéressez-vous à l'addon parser-java.

4. Des astuces !

Par défaut, le contenu du répertoire src/main/resources n'est pas mis dans le jar de l’addon :( Or, je préfère ne pas mettre les templates dans l'arborescence des sources (src/main/java) et les mettre dans ressources. Une petite modification du fichier pom.xml s'impose. Au niveau de la balise <build>, ajouter la partie en rouge suivante :

<build>

  <resources>

    <resource >

       <directory>${basedir}/src/main/resources</directory>

     </resource>

  </resources>

...

Ayant eu besoin d'un driver JDBC, j'ai utilisé le traditionnel Class.forName(). Par réflexe acquis... sans résultats ! Imaginez mon désarroi ! C'est un peu plus compliqué mais la réponse se trouvait sur internet. Pour charger ce jar, j'ai utilisé cette méthode qui renvoie une connexion :

private Connection getConnection(Configuration configuration) {

if (!driverSetupDone) {

FileResource<?> resource = factory.create(FileResource.class, new File(JDBCConfig.getJDBCDriversFolder(configuration)));

if (resource == null) {

return null;

}

File file = (File) resource.getUnderlyingResourceObject();

if (resource != null && resource.exists()) {

try (JarFile jarFile = new JarFile(file);) {

URL[] urls = new URL[] { file.toURI().toURL() };

URLClassLoader classLoader = URLClassLoader.newInstance(urls);

Class<?> driverClass = classLoader.loadClass(Driver.class.getName());

Enumeration<JarEntry> iter = jarFile.entries();

while (iter.hasMoreElements()) {

JarEntry entry = iter.nextElement();

if (entry.getName().endsWith(".class")) {

String name = entry.getName();

name = name.substring(0, name.length() - 6);

name = name.replace('/', '.');

try {

Class<?> clazz = classLoader.loadClass(name);

} catch (ClassNotFoundException | NoClassDefFoundError err) {

// ignorons

}

}

}

driverClass = classLoader.loadClass(JDBCConfig.getJDBCDriver(configuration));

Driver d = (Driver) driverClass.newInstance();

DriverManager.registerDriver(d);

DriverManager.registerDriver(new DelegatingDriver(d));

} catch (Exception e) {

e.printStackTrace();

}

}

}

 

if (con == null)

try {

con = DriverManager.getConnection(JDBCConfig.getJDBCUrl(configuration), JDBCConfig.getJDBCUsername(configuration), JDBCConfig.getJDBCPassword(configuration));

} catch (SQLException e) {

e.printStackTrace();

}

return con;

}

 

Au secours, n'est-il pas ? Je vous conseille de vous créer un package d'utilitaires avec des classes pour rassembler des appels tels que cette méthode.

5. Des buts dingues !

Petite homophonie pour introduire cette activité passionnante de notre quotidien de programmeur : la chasse aux erreurs i.e. débugger toute la journée (et le lendemain aussi).
Parce qu'il est fort possible que votre code ne soit pas parfait à la première écriture, Dennis Ritchie a inventé le debugging (et si pour une fois ce n'est pas lui, il en était capable). Pour cela, la configuration la plus pratique à mon avis est d'écrire son code dans JBoss Developper Studio et d'avoir un shell où vous lancez Forge avec l'option de debug :

$ forge --debug

Ensuite, en complément, dans JBDS, sélectionner votre projet puis, par un clic droit, sélectionner Debug As > Remote Java Application.

Ce sera dans le shell que vous devrez lancer l'installation de votre addon (addon-build-and-install) et son exécution.

Si, en modifiant vos sources, le debugger perd la synchronisation (out of synch), alors il est plus simple de quitter et relancer Forge dans un shell que de relancer Eclipse.

Autre problème que je ne suis pas le seul à avoir rencontré, c'est la non-mise à jour de l'addon en phase de développement. Après un build, la commande indiquée comme réussie, la/les commandes correspondantes ne sont pas accessibles pour autant alors que tout semble s'être déroulé sans accrocs. Il se trouve que le fichier jar est locké par Forge, ce qui doit être la source de son non-remplacement. Fâcheux mais signalé, ce souci est voué à disparaître rapidement. Pour le moment, le contournement que j'utilise est de désinstaller son addon à la main, avec ces quelques étapes :

- quitter Forge ;

- éditer ~/.forge/addons/installed.xml pour y enlever la ligne correspondant à son addon ;

- supprimer le répertoire de son addon, dans ~/.forge/addons/ ;

- relancer Forge.

Note

Il est possible que ce problème soit réglé à l'heure où vous lirez ces lignes.

6. Nous avons vu...

Il y a énormément de raisons mais seulement trois manières de faire un addon Forge :

- la première est mono-écran et simpliste. C'est celle avec les annotations @Command. C'est la plus rapide, que l'on qualifierait de « quick and dirty ».

- la deuxième manière ajoute du choix dans les types de saisies proposées à l'utilisateur, ainsi que leur validation. Elle reste mono-écran, les types de saisies sont plus variés, des validations sont possibles.

- il en existe une troisième, non élaborée dans cet article, qui est multi-écran. Elle permet de créer des assistants avec un workflow. Il s'agit de la classe org.jboss.forge.addon.ui.wizard.UIWizard, non abordée ici mais vous avez maintenant les connaissances pour vous y atteler par vous-même.

Nous avons aussi vu qu'il est possible de réutiliser d'autres addons, notamment ceux du noyau (core, UI), dans ses propres addons.

En fonction de votre besoin, à vous de décider quelle approche employer pour vos développements, et créer les outils qui vous simplifieront le travail.

Conclusion

J'ai pour opinion que la génération de code sera dans un avenir proche aussi naturelle que l'utilisation de librairies tierces de nos jours. N'utilisons-nous pas déjà de nombreux assistants également ? Des IDEs sophistiqués ?
Malgré tout, les outils disponibles sur le marché ne peuvent pas couvrir tous nos besoins, aussi avons-nous besoin d'une boîte à outils pour créer nos outils.

Issu de Red Hat, un grand contributeur de l'écosystème Open Source Java et géré par une équipe de qualité, JBoss Forge est un outil sur lequel il est raisonnable d'investir du temps et sur lequel baser vos sources.

À vous de battre le fer pendant qu'il est chaud et d'écrire vos addons dès aujourd'hui. Je vous laisse fer, souffler sur les braises de votre créativité et forger vos addons, vous avez les outils pour le faire.
Ensuite, ayez la gentillesse de répondre à mon sondage à l'adresse suivante http://goo.gl/uRUQmf.

Puis, rejoignez-moi dans la création d'un addon plus ambitieux dédié à Android [5] !

Références

[1] BATON J., « Les addons, c'est sa forge (1re partie)», GNU/Linux Magazine n°186, octobre 2015, p.72 à 77.

[2] BATON J., « JBoss Forge2, Java EE facile, très facile », GNU/Linux Magazine n°180, mars 2015, p. 72 à 82.

[3] Bug report pour un container scrollable : https://issues.jboss.org/browse/FORGE-2175

[4] FreeMarker : http://fr.wikipedia.org/wiki/Freemarker

[5] Mon Github : http://www.github.com/wadael

Note

Je remercie Fabien Dubail, Koen Aers, Horacio Gonzalez et Philippe Charrière pour leur aimable travail de relecture de cet article, ainsi qu'Antonio Goncalves pour m'avoir fait découvrir l'extensibilité de cet outil, George Gastaldi pour son aide, ma société : DRiMS, qui m'a permis de travailler avec cet outil (8 h/j >> 3 h/nuit) et bien sûr, Sylvie qui s'occupe de nos enfants quand je travaille sur un article !