Groovy par la pratique

A propos de cette série

Groovy est un langage de programmation moderne qui s'exécute sur la plateforme Java. Il offre une intégration cohérente avec le code Java existant tout en apportant de nouvelles fonctionnalités spectaculaires telles que les femetures et la métaprogrammation. Pour simplifier, Groovy est ce à quoi le langage Java aurait ressemblé s'il avait été conçu au 21ème siècle.

La clé pour incorporer un nouvel outil à votre boite à outils de développeur est de savoir quand l'utiliser et quand le laisser de côté. Groovy peut être extrêmement puissant, mais seulement s'il est appliqué aux scénarios appropriés. Pour cette raison, la série "Groovy par la pratique" explore les usages de Groovy, vous aidant à apprendre quand et comment les réutiliser avec succès.

Groovy: un DSL pour les programmeurs Java

Article d'origine: 

Ecrire moins de code et être plus productif avec Groovy

Résumé: 

L'expert Groovy Scott Davis relance la série Groovy par la pratique, en sommeil depuis 2006. Ce volet initial vous met au courant de l'histoire récente de Groovy et de sa situation actuelle. Puis vous verrez à quel point il est facile de se mettre à Groovy en 2009.

Andrew Glover a commencé à écrire sur Groovy pour developerWorks en 2004, commençant par l'article d'introduction "Feeling Groovy" dans la série alt.lang.jre, et continuant avec la série de longue haleine "Groovy par la pratique". C'était avant qu'aucun livre soit publié sur Groovy (il en existe plus d'une douzaine maintenant), et des années avant la version 1.0 de Groovy en janvier 2007. Il y a eu bien des changements depuis que le dernier volet de Groovy par la pratique a été publié fin 2006.

De nos jours, Groovy est téléchargé en moyenne à peu près 35000 fois par mois. Des sociétés relativement conservatrices comme la Mutuelle d'Omaha ont plus de 70000 lignes de code Groovy en production. Groovy a une des listes de diffusion les plus actives de Codehaus.org, qui héberge le projet (voir les Ressources). Le seul projet ayant plus de téléchargements et une liste de diffusion plus active est Grails, un framework Web populaire implémenté en Groovy (voir les Ressources).

Exécuter d'autres langages que Java sur une JVM n'est plus simplement commun, cela fait partie du coeur de la stratégie de Sun pour la JVM. Groovy rejoint à d'autres langages supportés par Sun comme JavaScript, JavaFX, JRuby et Jython dans la valse des alternatives. Ce qui était expérimental en 2004 constitue désormais l'état de l'art.

Ecrire sur Groovy en 2009 est, à bien des égards, assez semblable à ce que c'était lorsqu'Andy a commencé. La syntaxe s'est stabilisée en 2005 et reste la même aujourd'hui. Des fonctionnalités nouvelles et fascinantes sont ajoutées à chaque version, mais préserver la compatibilité descendante est une priorité pour les responsables du projet. Cette fondation solide fait de Groovy un choix privilégié pour les sociétés développant en Java et ayant l'habitude de se fier à des technologies qui seront encore sur le marché aussi longtemps que leurs applications seront en production.

Cet article a pour but de transformer rapidement des développeurs Java expérimentés en développeurs Groovy. Ne soyez pas dupes de son côté introductif. Cette série, comme son nom l'indique, est entièrement consacrée aux utilisations pratiques des savoir-faire Groovy. Une fois que vous aurez dit "Hello, World" dans cette introduction en douceur, préparez-vous à entrer très vite dans le monde réel.

Installer Groovy

Si vous n'avez jamais travaillé avec Groovy auparavant, la première chose à faire est de l'installer. L'installation est relativement facile. Il s'agit de la même procédure que pour installer d'autre applications Java courant comme Ant et Tomcat, ainsi que la plateforme Java elle-même:

  1. Télécharger l'archive (ZIP ou tar) de Groovy la plus récente
  2. Dézipper l'archive dans le répertoire de votre choix. (Il vaut mieux éviter les noms de répertoire contenant un espace)
  3. Créer une variable d'environnement GROOVY_HOME
  4. Ajouter GROOVY_HOME/bin à la variable PATH

Groovy préfère Java 5 ou 6. Entrez java -version à l'invite de commande pour vérifier que vous êtes à jour. Puis entrez groovy -version pour vous assurer que Groovy est bien installé.

Les principaux IDEs (Eclipse, IntelliJ et Netbeans) ont un plugin Groovy qui incorpore des fonctionnalités comme l'autocomplétion et le débuggage pas à pas. Bien qu'un bon IDE soit presque une nécessité pour écrire du code Java de nos jours, ce n'est pas forcément vrai pour ce qui est de Groovy. Grâce à la concision du langage Groovy, de nombreuses personnes choisissent d'utiliser un simple éditeur de texte. Des éditeurs Open Source tels que vi et Emacs supportent Groovy, de même que des éditeurs de texte commerciaux très peu couteux comme Textpad (pour Windows®) et TextMate (pour Max OS X). (Voir les ressources pour plus d'informations.)

Comme vous le verrez plus loin dans l'article, il est facile d'intégrer Groovy dans un projet Java existant. Tout ce que vous avez à faire est d'ajouter au classpath un JAR Groovy qui se trouve dans GROOVY_HOME/embeddable et d'inclure la tache javac existante dans une tache groovyc. (Maven offre un support similaire)

Mais avant que nous n'allions trop loin, nous allons commencer par l'indispensable "Hello World".

Hello Groovy World

Vous savez ce qu'un exemple de type "Hello Wold" est censé démontrer - le programme le plus simple qu'il soit possible d'écrire dans un langage donné. Ce qui est intéressant avec "Hello World" en Java, figurant dans le Listing 1, c'est la somme de connaissance du langage qui est nécessaire pour comprendre complètement ce qui se passe:

Listing 1. Un exemple de "Hello World" en Java

public class HelloJavaWorld {
  public static void main(String[] args){
    System.out.println("Hello Java World");
  }
}

Il faut commencer par créer un fichier nommé HelloJavaWorld.java et y saisir public class HelloJavaWorld. La première leçon délicate qu'apprennent beaucoup de développeurs Java est que si le nom de la classe et celui du fichier ne sont pas exactement les mêmes (y compris pour ce qui est de la casse), la classe ne compilera pas. De plus, les étudiants curieux vont d'ores et déjà se poser des questions sur les modificateurs d'accès comme public et private.

La ligne suivante - public static void main(String[] args) - déchaine généralement une avalanche de questions sur des détails d'implémentation: Que veut dire static ? Que veut dire void ? Pourquoi la méthode doit-elle être nommée main ? Qu'est-ce qu'un tableau de String ? Et finalement, essayez d'expliquer à un développeur Java novice que out est une instance public, static, final d'un objet PrintStream de la classe System. Je n'oublierai jamais l'étudiant qui a dit "Oh la la ! Tout ce que je voulais était dire 'Hello.'"

Mettez ceci en contraste avec "Hello world" en Groovy. Créez un fichier nommé HelloGroovyWorld.groovy et tapez la ligne indiquée dans le Listing 2:

Listing 2. Un exemple de "Hello World" en Groovy

println "Hello Groovy World"

Oui, il s'agit de l'équivalent en Groovy de l'exemple Java du Listing 1. Ici, tous les détails d'implémentation - le "bagage" qui ne contribue pas immédiatement à la résolution du problème en question - s'évanouit dans la nature, vous laissant simplement avec le code qui dit "Hello.". Tapez groovy HelloGroovyWorld pour confirmer que cela fonctionne.

Cet exemple trivial illustre ce que Groovy propose: réduire de manière spectaculaire les lignes de code qu'il faut écrire tout en préservant la sémantique de l'équivalent Java. Dans la section suivante, nous explorerons davantage cette idée.

Creuser davantage Hello World

Les programmeurs Java expérimentés savent qu'il faut compiler son code avant qu'il soit exécuté dans une JVM. Pourtant, il ne semble exister nulle part de fichier class pour le script Groovy. Cela signifie-t-il que l'on peut exécuter du code Groovy directement ? La réponse est "Pas vraiment, mais ça y ressemble pour de bon, n'est-ce-pas ?"

L'interpréteur Groovy compile le code source en mémoire avant de le passer à la JVM. Vous pouvez effectuer cette étape manuellement en tapant groovyc HelloGroovyWorld.groovy. Cependant, si vous essayez d'exécuter la classe résultante en utilisant java, vous tomberez sur l'exception du Listing 3:

Listing 3: Essai d'exécution d'une classe Groovy compilée sans le JAR de Groovy dans le CLASSPATH

$ java HelloGroovyWorld
Exception in thread "main" java.lang.NoClassDefFoundError: groovy/lang/Script

Comme je l'ai signalé précédemment, le JAR de Groovy doit être inclus dans le CLASSPATH. Essayez à nouveau, cette fois en passant l'argument -classpath à java, comme indiqué dans le Listing 4:

Listing 4: Exécuter avec succès une classe Groovy compilée avec la commande java

//Pour UNIX, Linux, et Mac OS X
$ java -classpath $GROOVY_HOME/embeddable/groovy-all-x.y.z.jar:.
        HelloGroovyWorld
Hello Groovy World

//Pour Windows
$ java -classpath %GROOVY_HOME%/embeddable/groovy-all-x.y.z.jar;.
        HelloGroovyWorld
Hello Groovy World

On commence à avancer. Mais afin de prouver que le script Groovy préserve réellement la sémantique de l'exemple Java, vous devez creuser davantage le bytecode. Pour commencer, tapez javap HelloJavaWorld, comme indiqué dans le Listing 5:

Listing 5. Examen du bytecode Java

$ javap HelloJavaWorld
Compiled from "HelloJavaWorld.java"
public class HelloJavaWorld extends java.lang.Object {
  public HelloJavaWorld();
  public static void main(java.lang.String[]);
}

Il ne devrait pas y avoir trop de surprises ici, à part quelques subtilités que le compilateur javac a ajouté pour vous. Vous n'avez pas eu besoin de taper extends java.lang.Object explicitement ou de fournir un constructeur par défaut pour la classe.

Maintenant, tapez javap HelloGroovyWorld, comme dans le Listing 6:

Listing 6. Examen du bytecode Groovy

$ javap HelloGroovyWorld
Compiled from "HelloGroovyWorld.groovy"
public class HelloGroovyWorld extends groovy.lang.Script {
  ...
  public static void main(java.lang.String[]);
  ...
}

Ici, vous constatez que le compilateur groovyc a créé une classe du même nom que votre fichier source. (Le fait que la classe étende groovy.lang.Script au lieu de java.lang.Object devrait vous aider à comprendre pourquoi essayer d'exécuter le fichier sans le JAR de Groovy dans le CLASSPATH lance une exception NoClassDefFoundError). Parmi toutes les méthodes ajoutées par le compilateur, vous devriez trouver une bonne vieille méthode public static void main(String[] args). Le compilateur groovyc a encapsulé les lignes de votre script dans cette méthode pour préserver la sémantique Java. Ceci signifie que Groovy vous permet de tirer parti de toutes vos connaissances Java existantes.

Par exemple, voici comment vous pouvez utiliser des paramètres en ligne de commande dans un script Groovy. Créez un nouveau fichier Hello.groovy et ajoutez la ligne du Listing 7:

Listing 7. Un script Groovy qui prend une entrée en ligne de commande

println "Hello, " + args[0]

Maintenant tapez groovy Hello Jane en ligne de commande. Le tableau de String args est là, comme tout développeur Java s'y attendrait. Utiliser args ici n'est peut-être pas compréhensible au non-initié, mais cela se tient parfaitement pour les développeurs Java confirmés.

Groovy réduit le code Java à sa plus simple expression. Le script Groovy que vous venez d'écrire ressemble beaucoup à du pseudocode exécutable. Il est d'apparence assez simple pour être compris par des novices, mais il n'écarte pas la puissance sous-jacente du langage Java pour les développeurs expérimentés. C'est pour cette raison que je dis que Groovy est un langage dédié (Domain Specific Language ou DSL) pour la plateforme Java. (Voir la section "Qu'est-ce-qu'un DSL ?").

Qu'est-ce-qu'un DSL ?

Martin Fowler a popularisé la notion de langage dédié (voir les Ressources). Il définit un DSL comme un "langage de programmation d'une expressivité limitée destiné à un domaine particulier". Par "expressivité limitée", il ne veut pas dire utilité limitée, mais plutôt que le langage dispose uniquement du vocabulaire nécessaire pour le rendre approprié à un "domaine particulier". Un DSL est un langage limité ayant un but spécifique, par opposition à un langage généraliste comme le langage Java.

SQL est un bon exemple de DSL. On ne pourrait pas écrire de système d'exploitation avec SQL, mais il est idéalement adapté au domaine restreint de la prise en charge des bases de données relationnelles. De manière très semblable, Groovy est un DSL pour la plateforme Java en ce qu'il est idéalement adapté au domaine restreint du développement Java. Mon utilisation du terme DSL ici a pour but d'illustrer mon propos sans être tout a fait exacte. Peut-être qu'en disant que Groovy est un DSL interne pour les idiomes Java courants, je m'attirerais moins les inévitables foudres des puristes des DSL.

Dave Thomas clarifie davantage la notion de DSL (voir les Ressources). Il écrit : "Quand des experts d'un domaine communiquent ... ils utilisent un jargon, un langage spécialisé qu'ils ont inventé à la manière d'un raccourci pour communiquer efficacement avec leurs pairs.". Nous nous approchons peut-être davantage de l'explication de la relation entre Groovy et Java en disant que Groovy est un "Java raccourci". La section suivante de l'article nous en donne un autre exemple.

Plain Old Groovy Objects (ndT: Bons vieux objets Groovy)

Les Javabeans - ou plus familièrement les POJOs (Plain Old Java Objects ndT: Bons vieux objets Java) - constituent un des éléments de base du développement Java. Il faut remplir un certain nombre de critères bien définis quand on crée un POJO pour représenter un objet métier. La classe devrait être publique, et les champs devraient être privés avec un jeu de méthodes get et set publiques. Le Listing 8 montre un POJO typique:

Listing 8. Un POJO

public class JavaPerson {
  private String firstName;
  private String lastName;

  public String getFirstName(){ return firstName; }
  public void setFirstName(String firstName){ this.firstName = firstName; }

  public String getLastName(){ return lastName; }
  public void setLastName(String lastName){ this.lastName = lastName; }
}

Les Plain Old Groovy Objects (POGOs) sont un équivalent des POJOs. Ils gardent tout à fait la même sémantique que les POJOs tout en réduisant spectaculairement la quantité de code à écrire. Le Listing 9 montre une classe écrite en Groovy représentant une personne "raccourcie":

Listing 9. Un POGO

class GroovyPerson {
  String firstName
  String lastName
}

Toutes les classes Groovy sont publiques par défaut. Toutes les propriétés sont privées, et toutes les méthodes sont publiques. Le compilateur génère un jeu de méthodes get et set publiques pour chaque propriété automatiquement. Compilez JavaPerson avec javac et GroovyPerson avec groovyc. Ensuite exécutez javap sur chacune d'elles pour confirmer que l'exemple Groovy fait tout ce que fait l'exemple Java, y compris étendre java.lang.Object. (On n'avait pas spécifié de classe dans l'exemple HelloGroovyWorld précédent, donc Groovy a créé une classe qui étend groovy.lang.Script à la place.)

Tout ceci signifie que vous pouvez immédiatement utiliser des POGOs à la place de vos POJOs. La classe Groovy est la classe Java réduite à sa plus simple expression. Une fois la classe Groovy compilée, les autres classes Java peuvent l'utiliser aussi facilement que si elle avait été écrite en Java. Pour le prouver, créer un fichier nommé JavaTest.java et ajoutez-y le code du Listing 10:

Listing 10: Appel de classes Groovy à partir de code Java

public class JavaTest {
  public static void main(String[] args){
    JavaPerson jp = new JavaPerson();
    jp.setFirstName("John");
    jp.setLastName("Doe");
    System.out.println("Hello " + jp.getFirstName());

    GroovyPerson gp = new GroovyPerson();
    gp.setFirstName("Jane");
    gp.setLastName("Smith");
    System.out.println("Hello " + gp.getFirstName());
  }
}

Même si les getters et les setters n'apparaissent pas dans le code source Groovy, ce test prouve qu'elles sont bien là et pleinement fonctionnelles dans la classe Groovy compilée. Mais cet exemple ne serait pas complet si je ne vous montrais pas le test correspondant en Groovy. Créez un fichier nommé TestGroovy.groovy et ajoutez-y le code du Listing 11:

Listing 11. Appel de classes Java à partir de code Groovy

JavaPerson jp = new JavaPerson(firstName:"John", lastName:"Doe")
println "Greetings, " + jp.getFirstName() + ". 
   It is a pleasure to make your acquaintance."

GroovyPerson gp = new GroovyPerson(lastName:"Smith", firstName:"Jane")
println "Howdy, ${gp.firstName}. How the heck are you?"

La première chose que vous avez probablement noté est le nouveau constructeur offert par Groovy, qui vous permet de nommer les champs et de les spécifier dans l'ordre que vous désirez. Encore plus intéressant, vous pouvez utiliser ce constructeur soit dans des classes Java, soit dans des classes Groovy. Comment est-ce possible ? En réalité, Groovy appelle le constructeur sans arguments par défaut et appelle ensuite les setters et getters appropriés pour chaque champ. Vous pourriez obtenir un comportement similaire avec Java, mais du fait que Java n'a pas d'arguments nommés et que les deux champs sont des Strings, il est impossible de passer les champs nom et prénom dans n'importe quel ordre.

Ensuite, notez que Groovy supporte la manière Java traditionnelle pour concaténer des chaines, de même que la manière Groovy d'embarquer du code entouré par ${} directement dans un String. (Ces derniers sont appelés GStrings, abréviation de Groovy Strings.)

Enfin, vous avez un aperçu des améliorations syntaxiques de Groovy à votre disposition lors de l'appel de getters d'une classe. Au lieu d'utiliser le plus verbeux gp.getFirstName(), vous pouvez appeler simplement gp.firstName. Cela peut ressembler à un accès direct du champ, mais en réalité il s'agit simplement d'un appel caché du getter correspondant. Les setters fonctionnent sur le même principe: gp.setLastName("Jones") et gp.lastName = "Jones" sont équivalents, ce dernier effectuant un appel au premier en arrière-plan.

J'espère que vous conviendrez qu'à chaque fois, la version Groovy ressemble à un raccourci de la version Java - ce que les "experts du domaine" utiliseraient pour "communiquer efficacement avec leurs pairs", ou quoique ce soit s'apparentant à un gentil petit badinage entre vieux amis.

Le code Groovy n'est au bout du compte rien d'autre que du code Java

Un des aspects les plus sous-estimés de Groovy est qu'il supporte entièrement la syntaxe Java. Comme je l'ai mentionné auparavant, vous n'êtes pas obligé de désapprendre si peu que ce soit Java quand vous passez à Groovy. Lorsque vous débutez en Groovy, le plus gros de votre code ressemble beaucoup à du Java classique. Mais au fur et a mesure que vous vous accoutumerez à la nouvelle syntaxe, le code évoluera progressivement pour adopter le style plus concis et expressif de Groovy.

Pour prouver que le code Groovy peut être exactement le même que le code Java, copiez JavaTest.java dans JavaTestInGroovy.groovy, et tapez groovy JavaTestInGroovy. Vous devriez obtenir une sortie identique, mais notez que vous n'avez pas eu à compiler la classe Groovy avant son exécution.

Ceci démontre que Groovy devrait être un choix presque évident pour les développeurs Java expérimentés. Du fait que la syntaxe Java est une syntaxe Groovy valide, la courbe d'apprentissage initiale n'existe pratiquement pas. Vous pouvez utiliser votre propre version de Java avec Groovy, votre propre IDE et votre propre environnement de production. Votre routine journalière devrait donc être très peu perturbée. Tout ce que vous avez à faire est vous assurer que le JAR Groovy est dans le CLASSPATH et modifier les scripts de compilation pour que les classes Groovy soient compilées avec les classes Java. La section suivante montre comment ajouter la tache groovyc à un fichier build.xml de Ant.

Compiler du code Groovy avec Ant

Si javac était un compilateur modulaire, on pourrait lui dire de compiler à la fois les fichiers Groovy et Java en même temps. Parce qu'il ne l'est pas, on inclut simplement la tache Ant javac dans une tache groovyc. De cette manière, groovyc compile le code source Groovy et javac le code source Java comme il l'a toujours fait.

Bien sûr, groovyc peut compiler des fichiers Java ainsi que des fichiers Groovy, mais vous rappelez-vous les méthodes utilitaires supplémentaires que groovyc avait ajouté à HelloGroovyWorld et à GroovyPerson ? Ces méthodes supplémentaires seraient également ajoutées aux classes Java. Il vaut mieux laisser groovyc compiler les fichiers Groovy et javac les fichiers Java.

 

Pour faire appel à groovyc depuis Ant, définissez la tache au moyen de taskdef puis utilisez la tache groovyc comme vous utiliseriez normalement la tache javac (voir les Ressources pour plus d'informations). Le Listing 12 montre le script de construction Ant:

Listing 12. Compiler du code Groovy et du code Java avec Ant

<taskdef name="groovyc"
        classname="org.codehaus.groovy.ant.Groovyc"
        classpathref="my.classpath"/ >
  <groovyc srcdir="${testSourceDirectory}" destdir="${testClassesDirectory}" >
    <classpath >
      <pathelement path="${mainClassesDirectory}"/ >
      <pathelement path="${testClassesDirectory}"/ >
      <path refid="testPath"/ >
    </classpath >
  <javac debug="on" / >
</groovyc >

Au fait, ces chaines contenant ${} ressemblent étrangement à des GStrings, n'est-ce-pas ? Groovy est un langage qui garde le meilleur, empruntant sans vergogne la syntaxe et les fonctionnalités d'autres langages et librairies. Ce n'est pas la dernière fois que vous verrez quelque chose écrit en Groovy qui vous fait penser : "Hum, est-ce-que je n'ai pas déjà vu çà quelque part ?"

Conclusion

Cela a été un voyage éclair dans Groovy. Vous avez quelque peu appris d'où Groovy est parti et où il en est actuellement. Vous avez installé Groovy sur votre système, et au travers de quelques exemples simples vous avez eu un aperçu de la puissance que Groovy offre aux développeurs Java.

Groovy n'est pas le seul langage alternatif à s'exécuter sur la JVM. JRuby est une excellente solution pour les développeurs Java qui connaissent déjà Ruby. Jython est une excellente solution pour les développeurs Java qui connaissent déjà Python. Mais comme vous l'avez vu, Groovy est une excellente solution pour les développeurs Java qui connaissent déjà Java. Le fait que Groovy offre une syntaxe concise à la Java qui préserve aussi la sémantique est assez fascinant. De plus, un nouveau langage dont l'adoption n'implique pas de del *.* ou de rm -Rf * change de manière agréable, n'est-ce-pas ?

La prochaine fois, vous en saurez plus sur les itérations en Groovy. Quand on code, on a fréquemment besoin de parcourir des choses morceau par morceau, qu'il s'agisse d'une liste, d'un fichier ou d'un document XML. Vous verrez de plus près la fermeture each que l'on retrouve partout. En attendant, j'espère que vous trouverez beaucoup d'utilisations pratiques de Groovy.

Enrichir for each

Article d'origine: 

Adopter une approche de type "Moindre surprise" de l'itération

Résumé: 

Dans ce volet de Groovy par la pratique, Scott Davis donne une liste étourdissante de manières d'itérer dans des ... listes. Et des tableaux. Et des fichiers. Et des URLs. Et ainsi de suite. Ce qui est le plus impressionnant est que Groovy fournit une mécanisme consistant pour parcourir toutes ces collections, et d'autres encore.

L'itération est une des bases de la programmation. Vous êtes constamment confronté à des choses - un objet List, File, un ResulSet JDBC - qu'il faut parcourir élément par élément. Le langage Java vous donne presque toujours les possibilités de parcours dont vous avez besoin, mais il est frustrant de ne pas avoir de moyen standardisé de le faire. L'approche Groovy de l'itération est l'un des aspects éminemment pratiques par lesquels la programmation Groovy se distingue de la programmation Java. A travers une série d'exemples (tous disponibles en téléchargement), vous apprendrez dans cet article de quelle manière la versatilité de la méthode Groovy each() vous permet de laisser les singularités des itérations Java derrière vous.

Stratégies d'itération Java

Supposez que vous avez une java.util.List de langages de programmation. Le Listing 1 montre la manière programmatique de dire "Je connais chacun de ceux-là" en Java:

Listing 1: Itération dans une liste en Java

import java.util.*;

public class ListTest{
    public static void main(String[] args){
        List<String> list = new ArrayList<String>();
	list.add("Java");
	list.add("Groovy");
	list.add("JavaScript");

	for(Iterator<String> i = list.iterator(); i.hasNext();){
	    String language = i.next();
	    System.out.println("I know " + language);
        }
    }
}

Grâce à l'interface java.lang.Iterable qui est partagée par la plupart des classes de collection, vous pouvez itérer sur java.util.Set ou java.util.Queue de la même manière.

A présent supposez que les langages sont stockés dans une java.util.Map. Essayer d'obtenir un itérateur sur une Map échoue à la compilation - Les Maps n'implémentent pas l'interface Iterable. Heureusement, vous pouvez appeler map.KeySet pour récupérer un Set, ce qui vous permet de continuer. Cette différence mineure vous a ralentit un peu, sans être insurmontable. Il fallait simplement vous rappeler que les classes List, Set et Queue implémentent Iterable, mais pas la classe Map, même si elles appartiennent toutes au même package .

Maintenant supposez que les langages sont stockés dans un tableau de String. Un tableau est une structure de données, pas une classe. Vous ne pouvez pas appeler .iterator() sur une tableau de String, et vous êtes obligé d'utiliser une stratégie d'itération légèrement différente. A nouveau, vous êtes gêné mais il existe des solutions, comme vous pouvez le voir dans le Listing 2:

Listing 2. Itération dans un tableau Java

public class ArrayTest{
    public static void main(String[] args){
        String[] list = {"Java", "Groovy", "JavaScript"};
    
        for(int i = 0; i < list.length; i++) {
            String language = list[i];
            System.out.println("I know " + language);            
        }
    }
}

Mais un instant - que devient la syntaxe for-each introduite par Java 5 (voir les Ressources) ? Cela fonctionne pour toute classe implémentant Iterable, ainsi que pour les tableaux, comme le montre le Listing 3:

Listing 3. La boucle for-each du langage Java

import java.util.*;

public class MixedTest{
    public static void main(String[] args){
        List<String> list = new ArrayList<String>();
        list.add("Java");
        list.add("Groovy");
        list.add("JavaScript");
    
        for(String language: list){
             System.out.println("I know " + language);      
        }

        String[] list2 = {"Java", "Groovy", "JavaScript"};
        for(String language: list2){
            System.out.println("I know " + language);      
        }
    }
}

Ainsi désormais il est possible d'itérer sur les tableaux et les collections (à l'exception des Maps) de la même façon. Mais qu'en serait-il si les langages étaient stockés dans un java.io.File ? Ou dans un ResultSet JDBC ? Un document XML ? Un java.util.StringTokenizer ? Dans chaque cas, vous devez utiliser une stratégie d'itération un peu différente. Cette approche n'était pas intentionnelle - les différentes API ont été développées à des époques différentes par différents développeurs - mais le fait est qu'il vous faut connaitre une demi-douzaine de stratégies d'itération Java et, plus important, les cas spécifiques qui mènent à leur utilisation.

Eric S. Raymond à écrit sur le sujet de "La Règle de Moindre Surprise" dans son livre "L'Art de la programmation Unix" (voir les Ressources, le livre est disponible en français). Il dit: "Pour concevoir des interfaces utilisables, il est préférable quand c'est possible de ne pas concevoir un modèle entièrement nouveau d'interface. La nouveauté est un obstacle à l'appropriation; cela met une charge d'apprentissage sur les épaules de l'utilisateur, aussi minimisez-la.". La position de Groovy vis-à-vis de l'itération suit le conseil de Raymond. Avec Groovy, qu'il s'agisse de parcourir presque n'importe quelle structure, tout ce dont vous avez besoin de vous rappeler est each().

Itération de List en Groovy

Pour commencer, je vais transformer l'exemple à base de List du Listing 3 en Groovy. Au lieu de manipuler la List dans une boucle for (ce qui, finalement, n"a pas l'air très orienté objet, n'est-ce-pas ?), on appelle simplement la méthode each() de la liste et on passe le résultat à une fermeture.

Créez un fichier nommé listTest.groovy et ajoutez-y le code du Listing 4:

Listing 4. Itération sur une liste en Groovy

def list = ["Java", "Groovy", "JavaScript"]
list.each{language->
  println language
}

La première ligne du Listing 4 est la syntaxe Groovy raccourcie pour construire une java.util.ArrayList. Vous pouvez ajouter println list.class au script pour le vérifier. Ensuite, il suffit d'appeler each() sur la liste et d'afficher la variable language dans le corps de la fermeture. On a donné un nom à la variable language au début de la fermeture avec l'instruction language->. Si vous ne fournissez pas de nom de variable, Groovy l'appelle par défaut it. Entrez groovy listTest en ligne de commande pour exécuter listTest.groovy.

Le Listing 5 donne deux versions à chaque fois plus courtes du code du Listing 4:

Listing 5. Itération utilisant la variable it de Groovy

// plus court, en utilisant la variable it
def list = ["Java", "Groovy", "JavaScript"]
list.each{ println it }

// Encore plus court, en utilisant une liste anonyme
["Java", "Groovy", "JavaScript"].each{ println it }

Groovy permet d'utiliser la méthode each() sur les tableaux et les Lists indifféremment. Pour changer ArrayList en tableau de String, ajoutez as String[] à la fin de la ligne, comme le montre le Listing 6:

Listing 6. Itération sur un tableau en Groovy

def list = ["Java", "Groovy", "JavaScript"] as String[]
list.each{println it}

Le fait que la méthode each() soit présente partout en Groovy, associé à la syntaxe raccourcie pour les getters (getClass() et class donnent le même résultat), permet d'écrire du code tout à la fois concis et expressif. Par exemple, disons que vous voulez afficher toutes les méthodes publiques d'une classe donnée en utilisant la réflexion. Le Listing 7 donne un exemple:

Listing 7. Réflexion avec Groovy

def s = "Hello World"
println s
println s.class
s.class.methods.each{println it}

//sortie:
$ groovy reflectionTest.groovy 
Hello World
class java.lang.String
public int java.lang.String.hashCode()
public volatile int java.lang.String.compareTo(java.lang.Object)
public int java.lang.String.compareTo(java.lang.String)
public boolean java.lang.String.equals(java.lang.Object)

La dernière ligne du script appelle la méthode getClass(). Une instance de java.lang.Class possède une méthode getMethods qui retourne un tableau. En chainant tout cela et en appelant each() sur le tableau de Methods retourné, vous faites pas mal de choses en une seule ligne de code.

Mais l'omniprésente méthode each() n'est pas limitée aux Lists et aux tableaux, comme l'est la structure for-each de Java. En Java, l'histoire s'arrête là. En Groovy, on commence juste à s'échauffer.

Itération sur une Map

Comme nous l'avons vu précédemment, en Java on ne peut pas itérer directement sur une Map. En Groovy, ce n'est pas un problème, comme le Listing 8 le montre:

Listing 8. Itération sur une Map en Groovy

def map = ["Java":"server", "Groovy":"server", "JavaScript":"web"]
map.each{ println it }

Pour accéder au contenu des paires nom/valeur, vous pouvez soit utiliser les méthodes implicites getKey() et getValue(), soit nommer explicitement les variables au début de la fermeture, comme le montre le Listing 9:

Listing 9. Récupérer clé et valeur d'une Map

def map = ["Java":"server", "Groovy":"server", "JavaScript":"web"]
map.each{ 
  println it.key
  println it.value 
}

map.each{k,v->
  println k
  println v
}

Comme vous pouvez le voir, itérer sur une Map est aussi naturel qu'itérer sur toute autre collection.

Avant que nous ne continuions avec l'exemple d'itération suivant, il est préférable que vous connaissiez une autre facilité syntaxique concernant les Maps en Groovy. Au lieu d'appeler map.get("Java") comme vous le feriez en Java, vous pouvez le raccourcir en map.Java, comme le montre le Listing 10:

Listing 10. Récupérer les valeurs d'une Map

def map = ["Java":"server", "Groovy":"server", "JavaScript":"web"]

//Résultats identiques
println map.get("Java")
println map.Java

Bien que ce raccourci syntaxique pour les Maps soit indéniablement sympathique, il est aussi la cause d'erreurs courantes lors de l'utilisation de la réflexion sur les Maps. Un appel à list.class retourne un java.util.ArrayList, tandis que l'appel map.class retourne null. Ceci vient du fait que le raccourci qui permet de récupérer un élément de la Map surcharge l'appel du get classique. Aucun élément de la Map n'a pour clé "class", donc l'appel retourne null, comme dans l'exemple du Listing 11:

Listing 11. Les Maps en Groovy et null

def list = ["Java", "Groovy", "JavaScript"]
println list.class
// java.util.ArrayList

def map = ["Java":"server", "Groovy":"server", "JavaScript":"web"]
println map.class
// null

map.class = "I am a map element"
println map.class
// I am a map element

println map.getClass()
// class java.util.LinkedHashMap

C'est l'un des rares cas où Groovy viole la Règle de Moindre Surprise, mais du fait que récupérer les éléments d'une Map est bien plus courant qu'utiliser la réflexion, c'est une exception avec laquelle on peut composer.

Itération dans une String

Maintenant que vous commencez à vous habituer à la méthode each(), nous allons voir qu'elle se retrouve dans toutes sortes d'endroits intéressants. Supposez que vous souhaitiez itérer dans une String, lettre par lettre. La méthode each() est là pour vous aider, comme le montre le Listing 12:

Listing 12: Itération dans une String

def name = "Jane Smith"
name.each{letter->
  println letter
}

Cela ouvre toutes sortes de possibilités, par exemple remplacer les espaces avec des soulignés, comme dans le Listing 13:

Listing 13. Remplacer les espaces par des soulignés

def name = "Jane Smith"
println "replace spaces"
name.each{
  if(it == " "){
    print "_"
  }else{
    print it
  }
}

// sortie
Jane_Smith

Bien sûr, pour ce qui est de remplacer une seule lettre, Groovy offre une méthode replace plus concise. Vous pourriez résumer tout le code du Listing 13 en une seule ligne: "Jane Smith".replace(" ", "_"). Mais pour des manipulations plus compliquées, la méthode each() est parfaite.

Itération dans un Range (ndT: intervalle)

Groovy offre un type natif Range qui ne demande qu'à être itéré. Tout ce qui est séparé par deux points (comme 1..10) est un Range. Le Listing 14 donne un exemple:

Listing 14. Itération dans un Range

def range = 5..10
range.each{
  println it
}

//sortie:
5
6
7
8
9
10

Les Ranges ne sont pas limités aux Integers. Voyez le code du Listing 15, qui itère sur un Range de Dates:

Listing 15. Itération sur des Date

def today = new Date()
def nextWeek = today + 7
(today..nextWeek).each{
  println it
}

//sortie:
Thu Mar 12 04:49:35 MDT 2009
Fri Mar 13 04:49:35 MDT 2009
Sat Mar 14 04:49:35 MDT 2009
Sun Mar 15 04:49:35 MDT 2009
Mon Mar 16 04:49:35 MDT 2009
Tue Mar 17 04:49:35 MDT 2009
Wed Mar 18 04:49:35 MDT 2009
Thu Mar 19 04:49:35 MDT 2009

Comme vous le constatez, la méthode each() apparait exactement là où vous l'attendriez. Le langage Java n'a pas de type natif Range, mais il existe un concept similaire sous la forme d'enums. Et, sans surprise, each() vous y attend aussi.

Iteration dans des enums

Une enum Java est un ensemble arbitraire de valeurs stockées dans un ordre spécifique. Le Listing 16 montre comme la méthode each() s'associe naturellement aux enums, comme elle le fait avec l'opérateur Range:

Listing 16. Itération sur une enum

enum DAY{
  MONDAY, TUESDAY, WEDNESDAY, THURSDAY,
    FRIDAY, SATURDAY, SUNDAY
}

DAY.each{
  println it
}

(DAY.MONDAY..DAY.FRIDAY).each{
  println it
}

Il existe des cas particuliers en Groovy où le nom de la méthode each() n'est pas assez expressif. Dans les exemples suivants, vous verrez la méthode each() décorée d'une façon appropriée à son contexte d'utilisation. La méthode Groovy eachRow() en est un exemple parfait.

Itération SQL

Quand on a affaire à des tables de bases de données relationnelles, il est courant de dire: "J'ai un traitement spécifique à effectuer sur chacune des lignes de la table.". Mettez celà en contraste avec les exemples précédents, où vous êtiez plus susceptible de dire, "J'ai un traitement spécifique à effectuer sur chacun des langages de la liste.". Dans cet esprit, un objet groovy.sql.Sql offre une méthode eachRow(), comme le montre le Listing 17:

Listing 17. Itération dans un ResultSet

import groovy.sql.*

def sql = Sql.newInstance(
   "jdbc:derby://localhost:1527/MyDbTest;create=true",
   "username",
   "password",
   "org.apache.derby.jdbc.ClientDriver")

println("grab a specific field")
sql.eachRow("select name from languages"){ row ->
    println row.name
}

println("grab all fields")
sql.eachRow("select * from languages"){ row ->
    println("Name: ${row.name}")
    println("Version: ${row.version}")
    println("URL: ${row.url}\n")
}

La première ligne du script instancie un nouvel objet Sql en passant une chaine de connexion JDBC, un nom d'utilisateur, un mot de passe, et un driver JDBC. Ensuite, vous pouvez appeler la méthode eachRow(), en lui passant en argument l'instruction SQL select. Dans la fermeture, vous pouvez référencer les noms de colonnes (name, version, url) comme s'il existait réellement des methodes getName(), getVersion() et getUrl().

C'est considérablement plus propre que l'équivalent Java où vous devez jongler avec les DriverManagers, les Connections, les Statements, et les ResultSets JDBC, pour ensuite nettoyer tout cà dans un océan de blocs try/catch/finally imbriqués.

Dans le cas de l'objet Sql, vous pourriez objecter que les noms each() ou eachRow() sont tout aussi valables. Mais dans les exemples suivants, je pense que vous serez d'accord pour dire que le nom each() est loin d'être suffisamment expressif.

Itération sur un fichier

Je rêve rarement d'itérer sur un java.io.File ligne par ligne en pur Java. Quand on en a fini avec tous les BufferedReaders et FileReaders imbriqués, sans compter la gestion des exceptions en fin de processus, on a eu le temps d'oublier ce qu'on voulait faire au départ.

Le Listing 18 montre le traitement complet en Java:

Listing 18. Itération dans un fichier en Java

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;

public class WalkFile {
   public static void main(String[] args) {
      BufferedReader br = null;
      try {
         br = new BufferedReader(new FileReader("languages.txt"));
         String line = null;
         while((line = br.readLine()) != null) {
            System.out.println("I know " + line);
         }
      }
      catch(FileNotFoundException e) {
         e.printStackTrace();
      }
      catch(IOException e) {
         e.printStackTrace();
      }
      finally {
         if(br != null) {
            try {
               br.close();
            }
            catch(IOException e) {
               e.printStackTrace();
            }
         }
      }
   }
}

Le Listing 19 montre le traitement équivalent en Groovy:

Listing 19. Itération dans un fichier en Groovy

def f = new File("languages.txt")
f.eachLine{language->
  println "I know ${language}"
}

C'est là où la concision de Groovy est vraiment remarquable. J'espère que vous voyez maintenant pourquoi j'appelle Groovy "un DSL pour les programmeurs Java".

Gardez présent à l'esprit que nous manipulons la même classe java.io.File en Groovy et en Java. Si le fichier n'existe pas, le code Groovy lancera la même FileNotFoundException que le code Java. La différence est qu'en Groovy il n'y a pas d'exceptions vérifiées. C'est ma prérogative - et non une contrainte du langage - d'envelopper la structure eachLine() dans un bloc try/catch/finally. Pour un simple script en ligne de commande, je donne la priorité à la brièveté du code comme dans le Listing 19. Si la même itération doit s'exécuter au sein d'un serveur d'application, je ne serais pas aussi cavalier en ne traitant pas les exceptions. J'envelopperais dans ce cas le bloc eachLine() dans le même bloc try/catch que la version Java.

La classe File offre plusieurs variantes de la méthode each(). L'une d'elles est splitEachLine(String separateur, Closure fermeture). Ceci signifie que vous pouvez non seulement itérer dans le fichier ligne par ligne, mais aussi découper les lignes en même temps. Le Listing 20 montre un exemple:

Listing 20. Découper les lignes d'un fichier

// languages.txt
// Notez l'espace entre le langage et la version
Java 1.5
Groovy 1.6
JavaScript 1.x 

// splitTest.groovy
def f = new File("languages.txt")
f.splitEachLine(" "){words->
  words.each{ println it }
}

// sortie
Java
1.5
Groovy
1.6
JavaScript
1.x

Si vous traitez des fichiers binaires, Groovy possède aussi une méthode eachByte().

Bien entendu, un File du langage Java n'est pas toujours un fichier - parfois c'est un répertoire. Groovy offre plusieurs variantes de each() pour manipuler également les sous-répertoires.

Itération dans un répertoire

Il est commode d'utiliser Groovy comme un langage de script shell (ou script de traitement par lots), grâce à la facilité avec laquelle on peut interagir avec le système de fichiers. Pour avoir la liste des contenus du répertoire courant, voyez le Listing 21:

Listing 21. Itération dans un répertoire

def dir = new File(".")
dir.eachFile{file->
  println file
}

La méthode eachFile() retourne à la fois les fichiers et les sous-répertoires. En utilisant les méthodes isFile() et isDirectory() de Java, on peut commencer à faire des choses plus sophistiquées. Le Listing 22 donne un exemple:

Listing 22. Distinguer les fichiers des répertoires

def dir = new File(".")
dir.eachFile{file->
  if(file.isFile()){
    println "FILE: ${file}"    
  }else if(file.isDirectory()){
    println "DIR:  ${file}"
  }else{
    println "Uh, I'm not sure what it is..."
  }
}

Du fait que les méthodes Java retournent de simples booléens, vous pouvez raccourcir ce code avec un opérateur ternaire Java. Le Listing 23 donne un exemple:

Listing 23. Opérateur ternaire

def dir = new File(".")
dir.eachFile{file->
  println file.isDirectory() ? "DIR:  ${file}" : "FILE: ${file}"
}

Si on ne veut que les répertoires, on peut utiliser eachDir() au lieu de eachFile(). Il existe aussi des méthodes eachDirMatch() et eachDirRecurse().

Comme vous le voyez, la seule méthode each() manquerait d'expressivité pour File. La sémantique de la méthode each() typique est conservée sur les Files, mais les noms de méthodes sont plus parlants pour fournir des informations supplémentaires sur les fonctionnalités étendues.

Itération dans une URL

Une fois que vous avez compris comment itérer dans un File, vous pouvez utiliser les mêmes principles pour itérer dans la réponse d'une requête HTTP. Groovy fournit une méthode eachLine() pratique (et familière) sur java.net.URL.

Par exemple, le Listing 24 parcourt le code HTML de la page d'accueil de ibm.com, ligne par ligne:

Listing 24. Itération dans une URL

def url = new URL("http://www.ibm.com")
url.eachLine{line->
  println line
}

Itération XML

Vous avez vu comment utiliser la méthode eachLine() sur les fichiers et les URLs. XML présente un problème un peu différent: vous êtes probablement moins intéressé par un parcours ligne à ligne du document que par un parcours élément par élément.

Par exemple, disons que la liste des langages est stockée dans un fichier nommé languages.xml, présenté dans le Listing 25:

Listing 25. Le fichier languages.xml

<langs>
  <language>Java</language>
  <language>Groovy</language>
  <language>JavaScript</language>
</langs>

Groovy fournit une méthode each() que vous pouvez utiliser, mais au prix d'une petite entorse. Si vous analysez le XML au moyen d'une classe native Groovy nommée XMLSlurper, vous pouvez parcourir les éléments avec each(). Regardez le Listing 26, par exemple:

Listing 26. Itération XML

def langs = new XmlSlurper().parse("languages.xml")
langs.language.each{
  println it
}

//sortie
Java
Groovy
JavaScript

L'instruction langs.language.each ramène tous les éléments nommés <language> sous l'élément <langs>. S'il s'y trouvait aussi des éléments <format> et <server>, ils n'apparaitraient pas dans le résultat de la méthode each().

Et si cela n'est pas assez impressionnant, maintenant imaginez que ce XML soit accessible par le biais d'un service Web de type Rest et non pas par un fichier du système. Remplacez ce chemin vers le fichier par une URL, et ne changez rien d'autre au code, comme le montre le Listing 27:

Listing 27. Itération sur du XML provenant d'un appel à un service Web

def langs = new XmlSlurper().parse("http://somewhere.com/languages")
langs.language.each{
  println it
}

C'est une utilisation assez concise et élégante de la méthode each(), n'est ce pas ?

Conclusion

Le plus beau de tout cet exercice fondé sur each(), c'est qu'il nous permet de beaucoup avancer dans notre connaissance de Groovy avec peu d'efforts. Il n'y a plus beaucoup de nouveautés sur le sujet des itérations Groovy une fois que vous connaissez la méthode each(). Et comme Raymond le dirait, c'est justement ce qui est intéressant. A partir du moment où vous savez itérer dans une List, vous savez en même temps itérer dans un tableau, une Map, une String, un Range, une enum, un ResultSet SQL, un File, un répertoire, une URL, et même sur les éléments d'un document XML.

Les derniers exemples de cet article ont effleuré l'analyse d'un XML avec XmlSlurper. La prochaine fois, je reviendrai sur ce sujet pour vous montrer comme il est facile d'analyser du XML avec Groovy. Vous verrez à la fois XmlParser et XmlSlurper à l'action, et vous vous ferez une meilleure idée de la raison pour laquelle Groovy possède deux classes similaires et pourtant distinctes ayant pour objectif principal d'analyser du  XML. En attendant, j'espère que vous trouverez beaucoup d'utilisations pratiques de Groovy.

Générer, analyser et ingurgiter du XML

Article d'origine: 

Manipulation de XML sans peine

Résumé: 

Voyez comme il est facile de manipuler dans tous les sens du XML avec Groovy. Dans ce volet de Groovy par la pratique, l'auteur Scott Davis démontre que, soit que vous vouliez créer du XML avec MarkupBuilder et StreamingMarkupBuilder, soit que vous vouliez analyser du XML avec XmlParser et XmlSlurper, Groovy offre des outils convaincants pour traiter ce format de données très courant.

On a l'impression que XML a toujours existé. En fait, XML a célébré son dixième anniversaire en 2008 (voir les Ressources). Comme le langage Java™ précède XML de quelques années seulement, on peut dire que pour les développeurs Java, XML a toujours existé.

Sun Microsystems, l'inventeur du langage Java, a été un fervent partisan de XML depuis le tout début. Après tout, la promesse faite par XML de permettre l'indépendance de la plateforme est bien en phase avec le slogan du langage Java "écrit une fois, exécuté partout". Etant donné que les deux technologies partagent la même sensibilité, on pourrait penser que le langage Java et XML cohabiteraient mieux qu'ils ne le font. En fait, analyser et générer du XML en Java parait bizarrement étranger et alambiqué.

Heureusement, Groovy introduit de meilleurs moyens pour créer et traiter du XML. A l'aide de quelques exemples (tous disponibles en téléchargement), cet article vous montrera de quelle manière Groovy rend l'écriture et l'analyse de XML d'une simplicité rafraichissante.

Comparer l'analyse XML avec Java et avec Groovy

A la fin de l'article "Enrichir for each", j'ai introduit un document XML simple rappelé au Listing 1. (J'ai ajouté l'attribut type cette fois pour rendre les choses un peu plus intéressantes).

Listing 1. Un document XML listant les langages que je connais

<langs type="current">
  <language>Java</language>
  <language>Groovy</language>
  <language>JavaScript</language>
</langs>

L'analyse de ce document XML trivial est incontestablement complexe en Java, comme vous le voyez dans le Listing 2. Il faut 30 lignes de code pour analyser un fichier XML de cinq lignes.

Listing 2. Analyser un fichier XML en Java

import org.xml.sax.SAXException;
import org.w3c.dom.*;
import javax.xml.parsers.*;
import java.io.IOException;

public class ParseXml {
  public static void main(String[] args) {
    DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
    try {
      DocumentBuilder db = dbf.newDocumentBuilder();
      Document doc = db.parse("src/languages.xml");

      //afficher l'attribut "type"
      Element langs = doc.getDocumentElement();
      System.out.println("type = " + langs.getAttribute("type"));

      //afficher les éléments "language"
      NodeList list = langs.getElementsByTagName("language");
      for(int i = 0 ; i < list.getLength();i++) {
        Element language = (Element) list.item(i);
        System.out.println(language.getTextContent());
      }
    }catch(ParserConfigurationException pce) {
      pce.printStackTrace();
    }catch(SAXException se) {
      se.printStackTrace();
    }catch(IOException ioe) {
      ioe.printStackTrace();
    }
  }
}

Comparez cela avec le code Groovy correspondant du Listing 3:

Listing 3. Analyser un fichier XML en Groovy

def langs = new XmlParser().parse("languages.xml")
println "type = ${langs.attribute("type")}"
langs.language.each{
  println it.text()
}

//sortie:
type = current
Java
Groovy
JavaScript

Le plus intéressant avec ce code Groovy n'est pas qu'il soit significativement plus court que son équivalent Java - bien que ses cinq lignes pour analyser cinq lignes de XML parlent clairement en sa faveur. Ce que j'apprécie le plus dans ce code Groovy est qu'il est de loin plus expressif. Quand j'écris langs.language.each, c'est comme si je travaillais directement avec le XML. Dans la version Java, le XML a complètement disparu.

Variables de type String et XML

Le bénéfice du travail avec XML en Groovy devient encore plus évident quand on stocke le XML dans une variable de type String au lieu d'un fichier. Les triples quotes de Groovy (couramment appelées HereDoc dans d'autres langages) permettent un stockage sans douleur du XML en interne, comme le montre le Listing 4. Le seul changement à apporter à l'exemple Groovy du Listing 3 est de modifier l'appel de la méthode parse() de XmlParser (qui s'occupe des Files, InputStreamss, Readers, et des URIs) en celui de parseText().

Listing 4. Stocker du XML en interne avec Groovy

def xml = """
<langs type="current">
  <language>Java</language>
  <language>Groovy</language>
  <language>JavaScript</language>
</langs>
"""

def langs = new XmlParser().parseText(xml)
println "type = ${langs.attribute("type")}"
langs.language.each{
  println it.text()
}

Notez que les triples quotes gèrent le document XML multilignes sans problème. La variable xml est réellement une java.lang.String classique - vous pouvez ajouter println xml.class pour le vérifier vous-même. Les triples quotes gèrent également les doubles quotes internes de type="current" sans vous forcer à les échapper manuellement avec un caractère antislash comme vous le feriez en Java.

Mettez en contraste l'élégance du code Groovy du Listing 4 avec le Java correspondant au Listing 5:

Listing 5. Stocker du XML en interne avec Java

import org.xml.sax.SAXException;
import org.w3c.dom.*;
import javax.xml.parsers.*;
import java.io.*;

public class ParseXmlFromString {
  public static void main(String[] args) {
    String xml = "<langs type=\"current\">\n" +
            "  <language>Java</language>\n" +
            "  <language>Groovy</language>\n" +
            "  <language>JavaScript</language>\n" +
            "</langs>";

    byte[] xmlBytes = xml.getBytes();
    InputStream is = new ByteArrayInputStream(xmlBytes);

    DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
    try {
      DocumentBuilder db = dbf.newDocumentBuilder();
      Document doc = db.parse(is);

      //afficher l'attribut "type"
      Element langs = doc.getDocumentElement();
      System.out.println("type = " + langs.getAttribute("type"));

      
      //afficher les éléments "language"
      NodeList list = langs.getElementsByTagName("language");
      for(int i = 0 ; i < list.getLength();i++) {
        Element language = (Element) list.item(i);
        System.out.println(language.getTextContent());
      }
    }catch(ParserConfigurationException pce) {
      pce.printStackTrace();
    }catch(SAXException se) {
      se.printStackTrace();
    }catch(IOException ioe) {
      ioe.printStackTrace();
    }
  }
}

Remarquez que la variable xml est polluée avec des caractères d'échappement pour les quotes internes et les sauts de ligne. Cependant, le plus gênant est d'avoir à convertir la String en tableau de byte, puis de devoir passer cela à un ByteArrayInputStream, avant qu'elle puisse être analysée. Curieusement, le DocumentBuilder ne fournit pas de méthode directe pour analyser une simple String en tant que XML.

Générer du XML avec MarkupBuilder

La plus large victoire de Groovy sur Java arrive quand vous voulez créer un document XML. Le Listing 6 montre les 50 lignes de code Java nécessaires à la création d'un fichier XML de cing lignes:

Listing 5. Générer du XML avec Java

import org.w3c.dom.*;
import javax.xml.parsers.*;
import javax.xml.transform.*;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.StringWriter;

public class CreateXml {
  public static void main(String[] args) {
    DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
    try {
      DocumentBuilder db = dbf.newDocumentBuilder();
      Document doc = db.newDocument();

      Element langs = doc.createElement("langs");
      langs.setAttribute("type", "current");
      doc.appendChild(langs);

      Element language1 = doc.createElement("language");
      Text text1 = doc.createTextNode("Java");
      language1.appendChild(text1);
      langs.appendChild(language1);

      Element language2 = doc.createElement("language");
      Text text2 = doc.createTextNode("Groovy");
      language2.appendChild(text2);
      langs.appendChild(language2);

      Element language3 = doc.createElement("language");
      Text text3 = doc.createTextNode("JavaScript");
      language3.appendChild(text3);
      langs.appendChild(language3);

      // Output the XML
      TransformerFactory tf = TransformerFactory.newInstance();
      Transformer transformer = tf.newTransformer();
      transformer.setOutputProperty(OutputKeys.INDENT, "yes");
      StringWriter sw = new StringWriter();
      StreamResult sr = new StreamResult(sw);
      DOMSource source = new DOMSource(doc);
      transformer.transform(source, sr);
      String xmlString = sw.toString();
      System.out.println(xmlString);
    }catch(ParserConfigurationException pce) {
      pce.printStackTrace();
    } catch (TransformerConfigurationException e) {
      e.printStackTrace();
    } catch (TransformerException e) {
      e.printStackTrace();
    }
  }
}

Je sais que certains d'entre vous vont crier "Erreur !" à cet instant précis. Beaucoup de librairies tierces peuvent rendre ce code plus lisible - JDOM et dom4j en étant deux exemples populaires. Mais aucune des librairies Java ne s'approche de la simplicité d'utilisation du MarkupBuilder de Groovy, comme le démontre le Listing 7:

Listing 7. Générer du XML avec Groovy

def xml = new groovy.xml.MarkupBuilder()
xml.langs(type:"current"){
  language("Java")
  language("Groovy")
  language("JavaScript")
}

Notez que nous revenons à peu près à un ratio 1:1 entre le code et le XML. Plus important, je peux visualiser à nouveau le XML. Bien sûr, les caractères "<" et ">" sont remplacés par des accolades, et les attributs utilisent le double-point (la notation Groovy pour les HashMap) au lieu du signe égal, mais la structure de base est reconnaissable que ce soit en Groovy ou en XML. C'est presque comme un DSL destiné à générer du XML, n'est-ce-pas ?

Le Builder peut réaliser ce genre de choses magiques parce que Groovy est un langage dynamique. Le langage Java, par contre, est statique: le compilateur Java s'assure que toutes les méthodes existent avant que vous ne les appeliez. (Le code Java ne compilera même pas, sans parler d'exécution, si vous essayez d'appeler une méthode inexistante). Mais le Builder de Groovy démontre que ce qui est un bug pour un langage est une fonctionnalité pour un autre. Si vous examinez la documentation des APIs de MarkupBuilder, vous ne trouverez pas de méthode langs(), de méthode language(), ou tout autre nom d'élément. Heureusement, Groovy peut traiter ces appels à des méthodes qui n'existent pas et faire quelque chose d'utile avec. Dans le cas d'un MarkupBuilder, il prend les appels à des méthodes fantômes et génère du XML bien formé.

Le Listing 8 développe l'exemple basique de MarkupBuilder que nous venons de voir. Si vous voulez récupérer le XML généré dans une String, passez un StringWriter au constructeur du MarkupBuilder. Si vous voulez ajouter d'autres attributs à langs, passez les simplement en paramètres, séparés par des virgules. Notez que le contenu de l'élément language est défini par une valeur sans nom. Vous pouvez ajouter des attributs et un contenu par le biais d'une liste dont le séparateur est la virgule également.

Listing 8. Un exemple plus poussé de MarkupBuilder

def sw = new StringWriter()
def xml = new groovy.xml.MarkupBuilder(sw)
xml.langs(type:"current", count:3, mainstream:true){
  language(flavor:"static", version:"1.5", "Java")
  language(flavor:"dynamic", version:"1.6.0", "Groovy")
  language(flavor:"dynamic", version:"1.9", "JavaScript")
}
println sw

//sortie:
<langs type='current' count='3' mainstream='true'>
  <language flavor='static' version='1.5'>Java</language>
  <language flavor='dynamic' version='1.6.0'>Groovy</language>
  <language flavor='dynamic' version='1.9'>JavaScript</language>
</langs>

Avec ces quelques trucs sur MarkupBuilde en poche, on peut commencer à faire des choses intéressantes. Par exemple, vous pouvez rapidement générer un document HTML bien formé et l'écrire dans un fichier. Le Listing 9 montre le code:

Listing 9. Générer du HTML avec un MarkupBuilder

def sw = new StringWriter()
def html = new groovy.xml.MarkupBuilder(sw)
html.html{
  head{
    title("Links")
  }
  body{
    h1("Here are my HTML bookmarks")
    table(border:1){
      tr{
        th("what")
        th("where")
      }
      tr{
        td("Groovy Articles")
        td{
          a(href:"http://ibm.com/developerworks", "DeveloperWorks")
        }
      }
    }
  }
}

def f = new File("index.html")
f.write(sw.toString())

//sortie:
<html>
  <head>
    <title>Links</title>
  </head>
  <body>
    <h1>Here are my HTML bookmarks</h1>
    <table border='1'>
      <tr>
        <th>what</th>
        <th>where</th>
      </tr>
      <tr>
        <td>Groovy Articles</td>
        <td>
          <a href='http://ibm.com/developerworks'>DeveloperWorks</a>
        </td>
      </tr>
    </table>
  </body>
</html>

La Figure 1 montre l'affichage par un navigateur du HTML généré par le Listing 9:

Figure 1. Rendu du HTML

Rendu du HTML

Génération de XML avec StreamingMarkupBuilder

MarkupBuilder fonctionne très bien pour générer des documents XML de manière synchrone. Pour créer des documents XML plus avancés, Groovy offre StreamingMarkupBuilder. Il permet de générer toutes sortes de fonctionnalités de XML, comme les instructions de traitement, les espaces de nom et le texte sans échappement (parfait pour les sections CDATA) avec l'aide de l'objet mkp. Le Listing 10 donne un aperçu des fonctionnalités intéressantes de StreamingMarkupBuilder:

Listing 10. Générer du XML avec StreamingMarkupBuilder

def comment = "<![CDATA[<!-- address is new to this release -->]]>"
def builder = new groovy.xml.StreamingMarkupBuilder()
builder.encoding = "UTF-8"
def person = {
  mkp.xmlDeclaration()
  mkp.pi("xml-stylesheet": "type='text/xsl' href='myfile.xslt'" )
  mkp.declareNamespace('':'http://myDefaultNamespace')
  mkp.declareNamespace('location':'http://someOtherNamespace')
  person(id:100){
    firstname("Jane")
    lastname("Doe")
    mkp.yieldUnescaped(comment)
    location.address("123 Main")
  }
}
def writer = new FileWriter("person.xml")
writer << builder.bind(person)

//sortie:
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type='text/xsl' href='myfile.xslt'?>
<person id='100'
        xmlns='http://myDefaultNamespace'
        xmlns:location='http://someOtherNamespace'>
  <firstname>Jane</firstname>
  <lastname>Doe</lastname>
  <![CDATA[<!-- address is new to this release -->]]>
  <location:address>123 Main</location:address>
</person>

Notez que StreamingMarkupBuilder ne produit pas de XML jusqu'à ce que la méthode bind() soit appelée, en passant en paramètre la fermeture qui contient le balisage et toutes les instructions. Ceci permet de construire les différentes parties du document XML de manière asynchrone et de tout générer d'un seul coup. (Voir les Ressources pour plus d'informations).

Comprendre XmlParser

Groovy vous donne deux moyens de produire du XML - MarkupBuilder et StreamingMarkupBuilder - ayant chacun ses propres atouts. Ceci est vrai aussi pour l'analyse du XML. Vou pouvez utiliser soit XmlParser soit XmlSlurper.

XmlParser offre une vision plus orientée programmeur du document XML. Si vous n'avez aucun mal à imaginer le document en termes de Lists et de Maps (respectivement pour les Elements et les Attributes), alors vous devriez être à l'aise avec XmlParser. Le Listing 11 dévoile un peu plus les rouages de XmlParser:

Listing 11. Examen plus poussé de XmlParser

def xml = """
<langs type='current' count='3' mainstream='true'>
  <language flavor='static' version='1.5'>Java</language>
  <language flavor='dynamic' version='1.6.0'>Groovy</language>
  <language flavor='dynamic' version='1.9'>JavaScript</language>
</langs>
"""

def langs = new XmlParser().parseText(xml)
println langs.getClass()
// class groovy.util.Node

println langs
/*
langs[attributes={type=current, count=3, mainstream=true};
      value=[language[attributes={flavor=static, version=1.5};
                      value=[Java]],
             language[attributes={flavor=dynamic, version=1.6.0};
                      value=[Groovy]],
             language[attributes={flavor=dynamic, version=1.9};
                      value=[JavaScript]]
            ]
]
*/

Remarquez que la méthode XmlParser.parseText() retourne un groovy.util.Node - dans ce cas, le noeud racine du document XML. Quand on exécute println langs , la méthode Node.toString() est appelée, retournant une sortie de débuggage. Pour récupérer les données réelles, il faudrait appeler soit Node.attribute() soit Node.text().

Récupérer les attributs avec XmlParser

Comme vous l'avez vu précédemment, vous pouvez récupérer la valeur d'un attribut particulier en appelant Node.attribute("key"). Si vous appelez Node.attributes(), vous obtiendrez une HashMap  qui contient les valeurs de tous les attributs. En utilisant la fermeture each que vous avez découvert dans "Enrichir for each", parcourir les attributs n'est pas compliqué. Voyez le Listing 12 pour en avoir un exemple. (Consultez les Ressources pour avoir l'API de groovy.util.Node).

Listing 12. XmlParser traite les attributs comme une HashMap

def langs = new XmlParser().parseText(xml)

println langs.attribute("count")
// 3

langs.attributes().each{k,v->
  println "-" * 15
  println k
  println v
}

//output:
---------------
type
current
---------------
count
3
---------------
mainstream
true

Aussi agréable le travail avec les attributs soit-il, XmlParser fournit un aide encore plus efficace pour travailler avec les éléments.

Accéder aux éléments avec XmlParser

Avec XmlParser, il est possible de requêter les éléments de manière intuitive au moyen des GPath (implémentation Groovy de XPath). Par exemple, le Listing 13 démontre que la structure langs.language que j'ai utilisée précédemment retourne une groovy.util.NodeList contenant le résultat de la requête. NodeList étend java.util.ArrayList, c'est donc à la base une List qui aurait reçu les super pouvoirs de GPath.

Listing 13. Requêtage avec GPath et XmlParser

def langs = new XmlParser().parseText(xml)

// Syntaxe de requêtage raccourcie
// sur une NodeList anonyme
langs.language.each{
  println it.text()
}

// Séparation de la requête
// et de la fermeture each
def list = langs.language
list.each{
  println it.text()
}

println list.getClass()
// groovy.util.NodeList

GPath est bien sûr le pendant de MarkupBuilder. Il utilise la même astuce en appelant des méthodes sans existence réelle, mais cette fois ce mécanisme est utilisé pour requêter le XML existant au lieu de le générer à la volée.

Sachant que le résultat d'une requête GPath est une List, on peut rendre le code encore plus concis. Groovy possède l'opérateur spread-dot (ndT: *.). En une seule ligne de code, il itère dans la liste, exécute l'appel de méthode sur chaque élément et retourne une List de résultats. Par exemple, si votre seul objectif est d'appeler la méthode Node.text() sur chaque élément du résultat de la requête, le Listing 14 vous montre comment le faire en une seule ligne de code:

Listing 14. Combiner l'opérateur spread-dot avec GPath

// la manière longue d'obtenir un résultat
def results = []
langs.language.each{
  results << it.text()
}

// la manière courte utilisant l'opérateur spread-dot
def values = langs.language*.text()
// [Java, Groovy, JavaScript]

// obtenir rapidement toutes les valeurs de l'attribut version
def versions = langs.language*.attribute("version")
// [1.5, 1.6.0, 1.9]

Mais si XmlParser est à la fois élégant et puissant, XmlSlurper se situe encore un cran au-dessus.

Analyse XML avec XMLSlurper

Quand j'ai introduit le Listing 1, j'ai dit que Groovy me donnait l'impression de travailler directement avec le XML. XmlParser a beaucoup de qualités, mais il faut tout de même traiter le XML par programmation. On vous retourne des Lists de Nodes et des HashMaps d' Attributes, et vous êtes obligés d'appeler des méthodes telles que Node.attribute() et Node.text() pour accéder aux données de base. XmlSlurperefface les derniers vestiges d'appels de méthodes, vous laissant avec l'agréable illusion que vous êtes en prise directe avec le XML.

Pour parler technique, XmlParser retourne des Nodes et des NodeLists, tandis que XmlSlurper retourne un groovy.util.slurpersupport.GPathResult. Mais maintenant que vous êtes au courant, je veux que vous oubliiez que j'ai jamais mentionné les détails d'implémentation de XmlSlurper. Vous apprécierez bien davantage la magie si vous ne gâchez pas l'effet en regardant derrière le rideau.

Le Listing 15 montre XmlParser et XmlSlurper côte-à-côte:

Listing 15. XmlParser et XmlSlurper

def xml = """
<langs type='current' count='3' mainstream='true'>
  <language flavor='static' version='1.5'>Java</language>
  <language flavor='dynamic' version='1.6.0'>Groovy</language>
  <language flavor='dynamic' version='1.9'>JavaScript</language>
</langs>
"""

def langs = new XmlParser().parseText(xml)
println langs.attribute("count")
langs.language.each{
  println it.text()
}

langs = new XmlSlurper().parseText(xml)
println langs.@count
langs.language.each{
  println it
}

Notez que XmlSlurper élimine toute notion d'appel de méthode. Au lieu d'appeler langs.attribute("count"), vous appelez langs.@count. Le signe @ est emprunté à XPath, mais le résultat donne l'illusion que l'on travaille directement avec l'attribut, impression que ne produit pas un appel à la méthode attribute(). Plutôt que d'appeler it.text(), vous appelez simplement it. On part de l'hypothèse que vous voulez travailler directement avec le contenu de l'élément.

XMLSlurper dans le monde réel

Pour aller plus loin que langs et language, voici un exemple tiré du monde réel montrant XmlSlurper en action. Yahoo! met à disposition sous forme de flux RSS la météo actuelle pour un code ZIP donné. RSS est évidemment un dialecte spécialisé. Entrez http://weather.yahooapis.com/forecastrss?p=80020 dans un navigateur web. Sentez-vous libre de modifier le code ZIP de Broomfield dans le Colorado pour mettre le vôtre (ndT: pour ma ville, le code est par exemple FRXX0110). Le Listing 16 montre une version simplifiée du flux RSS résultant:

Listing 16. Flux RSS de Yahoo! pour avoir la météo actuelle

<rss version="2.0"
     xmlns:yweather="http://xml.weather.yahoo.com/ns/rss/1.0"
     xmlns:geo="http://www.w3.org/2003/01/geo/wgs84_pos#">
  <channel>
    <title>Yahoo! Weather - Broomfield, CO</title>
    <yweather:location city="Broomfield" region="CO"   country="US"/>
    <yweather:astronomy sunrise="6:36 am"   sunset="5:50 pm"/>

    <item>
      <title>Conditions for Broomfield, CO at 7:47 am MST</title>
      <pubDate>Fri, 27 Feb 2009 7:47 am MST</pubDate>
      <yweather:condition text="Partly Cloudy"
                          code="30"  temp="25"
                          date="Fri, 27 Feb 2009 7:47 am MST" />
    </item>
  </channel>
</rss>

La première chose à faire est de mettre la main sur ce RSS par programmation. Créez un fichier nommé weather.groovy et ajoutez-y le code du Listing 17:

Listing 17. Accéder au RSS par programmation

def baseUrl = "http://weather.yahooapis.com/forecastrss"

if(args){
  def zip = args[0]
  def url = baseUrl + "?p=" + zip
  def xml = url.toURL().text
  println xml
}else{
  println "USAGE: weather zipcode"
}

Entrez  en ligne de commande pour vérifier que vous pouvez visualiser le RSS brut.

La partie la plus importante de ce script est url.toURL().text. la variable url est une String bien formée. Toutes les Strings se voient ajouter par Groovy la méthode toURL() qui les transforme en java.net.URL. En contrepartie, Groovy ajoute également à toutes les URLs une méthode getText() qui exécute une requête HTTP GET et retourne le résultat sous forme de String.

Maintenant que l'on dispose du RSS stocké dans la variable xml, ajoutons une pincée de XmlSlurper pour sélectionner les portions intéressantes, comme dans le Listing 18:

Listing 18. Utiliser XmlSlurper pour analyser du RSS

def baseUrl = "http://weather.yahooapis.com/forecastrss"

if(args){
  def zip = args[0]
  def url = baseUrl + "?p=" + zip
  def xml = url.toURL().text

  def rss = new XmlSlurper().parseText(xml)
  println rss.channel.title
  println "Sunrise: ${rss.channel.astronomy.@sunrise}"
  println "Sunset: ${rss.channel.astronomy.@sunset}"
  println "Currently:"
  println "\t" + rss.channel.item.condition.@date
  println "\t" + rss.channel.item.condition.@temp
  println "\t" + rss.channel.item.condition.@text
}else{
  println "USAGE: weather zipcode"
}

//sortie:
Yahoo! Weather - Broomfield, CO
Sunrise: 6:36 am
Sunset: 5:50 pm
Currently:
   Fri, 27 Feb 2009 7:47 am MST
   25
   Partly Cloudy

Vous avez vu comme il parait naturel de travailler avec XML en utilisant XmlSlurper ? Vous affichez l'élément <title> en y faisant directement référence - rss.channel.title. Vous traitez l'attribut temp avec un simple rss.channel.item.condition.@temp. On n'a pas l'impression de programmer, mais de travailler directement avec le XML.

Avez-vous remarqué que XmlSlurper ignore même les espaces de noms ? On peut activer la prise en compte des espaces de noms dans le constructeur, mais je le fais rarement. Par défaut, XmlSlurper permet de manipuler XML avec une facilité déconcertante.

Conclusion

Pour être un développeur efficace à notre époque, il est nécessaire d'avoir des outils qui rendent aisée l'utilisation de XML. Les MarkupBuilder et StreamingMarkupBuilder de Groovy font que créer du XML à la volée est une sinécure. XmlParser est parfait pour manipuler des Lists d'Elements et des HashMaps d'Attributes, et XmlSlurper fait disparaitre complètement le code, vous laissant l'agréable illusion que vous travaillez directement sur le XML.

Une bonne partie de cette puissance dans le traitement du XML serait impossible sans les possibilités dynamiques de Groovy. Dans le prochain article, nous explorerons plus en détail la nature dynamique de Groovy. Vous apprendrez comment fonctionne la métaprogrammation avec Groovy, depuis les sympathiques méthodes ajoutées aux classes standard du JDK (comme String.toURL() et List.each()) jusqu'aux méthodes personnalisées que vous ajouterez vous-mêmes. En attendant, j'espère que vous trouverez beaucoup d'utilisations pratiques de Groovy.

Métaprogrammation avec les fermetures, ExpandoMetaClass, et les catégories

Article d'origine: 

Ajoutez des methodes où et quand vous le voulez

Résumé: 

Entrez dans le monde de la métaprogrammation façon Groovy. La possibilité d'ajouter de nouvelles méthodes aux classes dynamiquement à l'exécution - même aux classes Java™, et même aux classes Java final - est incroyablement puissante. Que ce soit pour du code de production, des tests unitaires, ou quoi que ce soit dans l'intervalle, les capacités de la métaprogrammation Groovy devraient piquer la curiosité de n'importe quel développeur Java, même le plus blasé.

Depuis des années, on entend dire que Groovy est un langage de programmation dynamique pour la JVM. Mais qu'est-ce-que ça signifie réellement ? Dans ce volet de Groovy par la pratique, vous en apprendrez davantage sur la capacité qu'a Groovy d'ajouter des méthodes aux classes dynamiquement à l'exécution. Cette souplesse va bien au delà de ce que le langage Java standard peut proposer. A travers une série d'exemples de code (tous disponibles en téléchargement), vous verrez que la métaprogrammation est des aspects les plus pratiques et puissants de Groovy.

Modéliser ce qui nous entoure

Notre travail de programmeurs est de modéliser le monde réel dans nos logiciels. Quand le monde réel est assez gentil pour exposer un domaine simple - les animaux qui ont des écailles (ndT: scaly) ou des plumes (ndT: feathery) pondent des oeufs, et les animaux à pelage (ndT: furry) donnent naissance à des petits complètement formés - il est facile d'abstraire des comportements dans le logiciel, comme dans le Listing 1:

Listing 1. Modéliser des animaux avec Groovy

class ScalyOrFeatheryAnimal{
  ScalyOrFeatheryAnimal layEgg(){
    return new ScalyOrFeatheryAnimal()
  }
}

class FurryAnimal{
  FurryAnimal giveBirth(){
    return new FurryAnimal()
  }
}

Malheureusement, le monde réel abonde en exceptions et en cas aux limites - les ornithorynques (ndT: platypuses) ont un pelage et pondent des oeufs. C'est presque comme si nos abstractions si soigneusement conçues étaient la cible privilégiée de toute une équipe de contradicteurs patentés.

Si le langage que vous utilisez pour modéliser le domaine est trop rigide pour s'accomoder des exceptions qui arriveront inévitablement, vous pourriez bien finir par ressembler à un fonctionnaire têtu empêtré dans une bureaucratie mesquine - "Je suis désolé madame Ornithorynque, mais il faudra accoucher d'un petit complètement formé si vous voulez bénécier de notre système."

A l'opposé, un langage dynamique comme Groovy vous donne la flexibilité nécessaire pour permettre à votre logiciel de modéliser la réalite avec plus de précision, plutôt que de vouloir présomptueusement (et futilement) que ce soit la réalité qui fasse des concessions. Si la class Platypus a besoin d'une méthode layEgg(), Groovy le permet, comme le montre le Listing 2:

Listing 2. Ajouter une méthode layEgg() dynamiquement

Platypus.metaClass.layEgg = {->
  return new FurryAnimal()
}

def baby = new Platypus().layEgg()

Si tous ces discours sur les animaux à pelage et les oeufs semblent frivoles, parlons donc de la rigidité d'une des classes les plus couramment utilisées de Java: String.

Les méthodes ajoutées par Groovy à java.lang.String

Un des aspects sympathiques du travail avec Groovy est qu'il ajoute beaucoup de méthodes à java.lang.String. Des méthodes comme padRight() et reverse() effectuent de simples transformations de String, comme le montre le Listing 3. (Pour un lien vers la liste de toutes les méthodes ajoutées par le GDK à String, voir les Ressources. Comme la page d'accueil du GDK le dit avec enthousiasme, "Ce document décrit les méthodes ajoutées au JDK pour le rendre plus fun (ndT: groovy en V.O.)").

Listing 3. Méthodes ajoutées à String par Groovy

println "Introduction".padRight(15, ".")
println "Introduction".reverse()

//sortie
Introduction...
noitcudortnI

Mais les ajouts à String ne se limitent pas à ces petites gâteries. Si la String est en fait une URL bien formée, en une seule ligne vous pouvez la transformer en une java.net.URL et obtenir le résultat d'une requête HTTP GET, comme dans le Listing 4:

Listing 4. Effectuer une requête HTTP GET

println "http://thirstyhead.com".toURL().text

//sortie
<html>
  <head>
    <title>ThirstyHead: Training done right.</title>
<!-- snip -->

Pour donner un autre exemple, exécuter une commande shell locale est tout aussi simple qu'effectuer un appel réseau distant. Habituellement, je taperais ifconfig en0 en ligne de commande pour vérifier les paramètres TCP/IP de ma carte réseau. (Si vous utilisez Windows® et non Mac OS X ou Linux®, essayez ipconfig). Avec Groovy, je peux faire la même chose par programmation, comme vous le voyez dans le Listing 5:

Listing 5. Exécuter une commande shell avec Groovy

println "ifconfig en0".execute().text

//sortie
en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
	ether 00:17:f2:cb:bc:6b
	media: autoselect status: inactive
  //snip

Je ne veux pas dire que Groovy apporte un plus parce que vous ne pouvez pas faire la même chose avec Java. Vous le pouvez. Le plus vient du fait que ces méthodes ont apparemment été ajoutées à la classe String - ce qui n'est pas une mince affaire, vu qu'elle est final. (Nous en dirons plus sur ce sujet dans un instant). Le Listing 6 montre l'équivalent Java de String.execute().text:

Listing 6. Exécuter une commande shell avec Java

Process p = new ProcessBuilder("ifconfig", "en0").start();
BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()));
String line = br.readLine();
while(line != null){
  System.out.println(line);
  line = br.readLine();
}

On a un peu l'impression d'être renvoyé d'un guichet de l'administration des impôts (ndT: Department of Motor Vehicles en V.O., mais en France on préfère taper sur les impôts !)  à un autre, n'est-ce-pas ? "Je suis désolé monsieur, mais pour obtenir la String que vous avez demandé, vous devez d'abord faire la queue là-bas pour avoir un BufferedReader."

Evidemment, vous pouvez créer des méthodes et des classes utilitaires pour faire abstraction de toute cette laideur, mais toutes les solutions du type com.mycompany.StringUtil du monde ne sont qu'un pâle substitut à l'ajout direct d'une méthode là où l'on en a besoin: la classe String. (Cela rappelle Platypus.layEgg() !)

Alors, comment Groovy fait-il exactement pour attacher de nouvelles méthodes à des classes qui ne peuvent être ni étendues ni modifiées directement ? Pour le comprendre, il faut connaitre la notion de fermeture et la classe ExpandoMetaClass.

Les fermetures et ExpandoMetaClass

Groovy possède une fonctionnalité à l'air inoffensif mais en réalité extrêmement puissante - les fermetures - sans laquelle l'Ornithorynque ne serait jamais en mesure de pondre un oeuf. Pour faire court, une fermeture est un morceau de code nommé. C'est une méthode n'appartenant à aucune classe. Le Listing 7 montre une fermeture simple:

Listing 7. Une fermeture simple

def shout = {src->
  return src.toUpperCase()
}

println shout("Hello World")

//sortie
HELLO WORLD

Avoir des méthodes autonomes est assez sympathique, mais pas autant, loin s'en faut, que la possibilité d'attacher ces méthodes à des classes existantes. Examinez le code du Listing 8 qui, au lieu de créer une méthode qui prend une String en paramètre, ajoute la méthode directement à la classe String:

Listing 8. Ajout de la méthode shout à la classe String

String.metaClass.shout = {->
  return delegate.toUpperCase()
}

println "Hello MetaProgramming".shout()

//sortie
HELLO METAPROGRAMMING

 

La fermeture sans argument shout() est ajoutée à l'ExpandoMetaClass (EMC) de String. Toute classe - qu'elle soit Java ou Groovy - est enveloppée dans une EMC qui intercepte les appels de ses méthodes. Cela veut dire que même si String est final, on peut ajouter des méthodes à son EMC. Pour un observateur non averti, le résultat est que String semble posséder une méthode shout().

Comme ce type de relation n'existe pas dans le langage Java, Groovy a introduit un nouveau concept: les délégués. Le delegate est la classe que l'EMC enveloppe.

Sachant que les appels de méthodes atteignent l'EMC d'abord, et le delegate ensuite, on peut faire toutes sortes de choses intéressantes. Par exemple, notez de quelle manière on redéfinit la méthode toUpperCase() de String dans le Listing 9:

Listing 9. Redéfinition de la méthode toUpperCase()

String.metaClass.shout = {->
  return delegate.toUpperCase()
}

String.metaClass.toUpperCase = {->
  return delegate.toLowerCase()
}

println "Hello MetaProgramming".shout()

//sortie
hello metaprogramming

Encore une fois, tout çà peut sembler futile (et même dangereux !). Dans le monde réel, peu de gens ont besoin de changer le comportement de la méthode toUpperCase(), mais pouvez-vous imaginer quelle bénédiction cela se révèle être pour écrire les tests unitaires de votre code ?

La métaprogrammation offre un moyen simple et facile de rendre déterministe un comportement potentiellement aléatoire. Pour l'illustrer, le Listing 10 redéfinit la méthode statique random() de la classe Math:

Listing 10. Redéfinition de la méthode Math.random()

println "Before metaprogramming"
3.times{
  println Math.random()
}

Math.metaClass.static.random = {->
  return 0.5
}

println "After metaprogramming"
3.times{
  println Math.random()
}

//sortie
Before metaprogramming
0.3452
0.9412
0.2932
After metaprogramming
0.5
0.5
0.5

Maintenant imaginez que vous essayez de tester unitairement une classe qui réalise des appels SOAP coûteux. Nul besoin de créer une interface et d'écrire un bouchon qui l'implémente - vous pouvez redéfinir cette méthode pour qu'elle retourne une simple réponse simulée. (Vous verrez des exemples d'utilisation de Groovy pour les tests unitaires et la création de simulacres dans la prochaine section).

La métaprogrammation de Groovy est un phénomène qui se passe à l'exécution - il dure aussi longtemps que le programme s'exécute. Mais comment faire si vous voulez que votre métaprogrammation aie une durée de vie plus limitée (ce qui est tout spécialement important lors de l'écriture de tests unitaires) ? Dans la prochaine section, vous apprendrez comment définir une portée pour la magie de la métaprogrammation.

Définir la portée de la métaprogrammation

Le Listing 11 place le code dont je me suis servi en exemple dans un GroovyTestCase , pour me permettre de tester un peu plus rigoureusement. (Voir l'article "Practically Groovy: Unit test your Java code faster with Groovy" pour plus de détails sur l'utilisation de GroovyTestCase).

Listing 11. Exploration de la métaprogrammation avec un test unitaire

class MetaTest extends GroovyTestCase{

  void testExpandoMetaClass(){
    String message = "Hello"
    shouldFail(groovy.lang.MissingMethodException){
      message.shout()
    }

    String.metaClass.shout = {->
      delegate.toUpperCase()
    }

    assertEquals "HELLO", message.shout()

    String.metaClass = null
    shouldFail{
      message.shout()
    }
  }
}

Tapez groovy MetaTest en ligne de commande pour exécuter ce test.

Notez que vous pouvez annuler la métaprogrammation en positionnant tout simplement String.metaClass à null.

Mais comment faire si vous ne souhaitez pas que la méthode shout() apparaisse pour toutes les instances de Strings ? Eh bien, vous pouvez simplement faire modifier l'EMC d'une instance particulière de la classe, comme le montre le Listing 12:

Listing 12. Métaprogrammation d'une seule instance

void testInstance(){
  String message = "Hola"
  message.metaClass.shout = {->
    delegate.toUpperCase()
  }

  assertEquals "HOLA", message.shout()
  shouldFail{
    "Adios".shout()
  }
}

Si vous voulez ajouter ou redéfinir plusieurs méthodes à la fois, le Listing 13 montre comment définir d'un seul coup plusieurs méthodes:

Listing 13. Métaprogrammation de plusieurs méthodes à la fois

void testFile(){
  File f = new File("nonexistent.file")
  f.metaClass{
    exists{-> true}
    getAbsolutePath{-> "/opt/some/dir/${delegate.name}"}
    isFile{-> true}
    getText{-> "This is the text of my file."}
  }

  assertTrue f.exists()
  assertTrue f.isFile()
  assertEquals "/opt/some/dir/nonexistent.file", f.absolutePath
  assertTrue f.text.startsWith("This is")
}

Remarquez que je ne me soucie plus de savoir si le fichier existe réellement dans le système. Je peux le passer aux autres classes dans le test unitaire, et il se comportera comme un vrai fichier. Une fois que la variable f n'est plus définie à la fin du test, le comportement personnalisé disparait également.

Même si ExpandoMetaClass est indéniablement puissante, Groovy propose une seconde approche de la métaprogrammation avec ses capacités propres: les catégories.

Les catégories et le bloc use

Le meilleur moyen d'expliquer ce qu'est une catégorie est d'en voir une en action. Le Listing 14 montre l'utilisation d'une catégorie pour ajouter une méthode shout() à String:

Listing 14. Utilisation d'une catégorie pour la métaprogrammation

class MetaTest extends GroovyTestCase{
  void testCategory(){
    String message = "Hello"
    use(StringHelper){
      assertEquals "HELLO", message.shout()
      assertEquals "GOODBYE", "goodbye".shout()
    }

    shouldFail{
      message.shout()
      "foo".shout()
    }
  }
}

class StringHelper{
  static String shout(String self){
    return self.toUpperCase()
  }
}

Si vous avez déjà développé en Objective-C, cette technique doit vous paraitre familière. La catégorie StringHelper est une classe normale - elle n'étend aucune classe parent spécifique et n'implémente pas spécialement d'interface. Pour ajouter de nouvelles méthodes à un type T, il faut juste définir des méthodes statiques qui prennent le type T comme premier paramètre. Puisque shout() est une méthode statique qui prend une String comme premier paramètre, toutes les Strings faisant partie d'un bloc use reçoivent une méthode shout().

Alors, quand faut-il préférer une catégorie à une EMC ? Comme vous pouvez le voir, définir une catégorie permet d'ajouter des méthodes à certaines instances - uniquement les instances qui se trouvent à l'intérieur du bloc use.

Alors qu'une EMC vous permet de définir de nouveaux comportements à la volée, une catégorie vous permet d'isoler le comportement dans un fichier classe séparé. Cela implique que vous pouvez l'utiliser dans des circonstances bien différentes: test unitaires, code de production, et ainsi de suite. Le travail supplémentaire requis pour définir des classes distinctes est contrebalancé en termes de réutilisabilité.

Le Listing 15 montre l'utilisation de StringHelper et d'une autre catégorie FileHelper dans le même bloc use:

Listing 15. Utilisation de plusieurs catégories dans un bloc use

class MetaTest extends GroovyTestCase{
  void testFileWithCategory(){
    File f = new File("iDoNotExist.txt")
    use(FileHelper, StringHelper){
      assertTrue f.exists()
      assertTrue f.isFile()
      assertEquals "/opt/some/dir/iDoNotExist.txt", f.absolutePath
      assertTrue f.text.startsWith("This is")

      assertTrue f.text.shout().startsWith("THIS IS")
    }

    assertFalse f.exists()
    shouldFail(java.io.FileNotFoundException){
      f.text
    }
  }
}


class StringHelper{
  static String shout(String self){
    return self.toUpperCase()
  }
}


class FileHelper{
 static boolean exists(File f){
   return true
 }

 static String getAbsolutePath(File f){
   return "/opt/some/dir/${f.name}"
 }

 static boolean isFile(File f){
   return true
 }

 static String getText(File f){
   return "This is the text of my file."
 }
}

Cependant l'aspect le plus intéressant des catégories est leur implémentation. Les EMCs requièrent l'utilisation de fermetures, ce qui a pour conséquence que vous ne pouvez les implémenter qu'en Groovy. Comme les catégories ne sont rien d'autre que des classes avec des méthodes statiques, elles peuvent être définies en Java. En réalité vous pouvez réutiliser des classes Java existantes - qui n'ont jamais été conçues expressément pour la métaprogrammation - avec Groovy.

Le Listing 16 donne un exemple d'utilisation du package Jakarta Commons Lang (voir les Ressources) pour la métaprogrammation. Par coïncidence, toutes les méthodes qui se trouvent dans org.apache.commons.lang.StringUtils suivent le modèle défini pour les catégories - des méthodes statiques qui prennent une String en premier paramètre. Cela veut dire que l'on peut utiliser telle quelle la classe StringUtils comme une catégorie.

Listing 16. Utilisation d'une classe Java pour la métaprogrammation

import org.apache.commons.lang.StringUtils

class CommonsTest extends GroovyTestCase{
  void testStringUtils(){
    def word = "Introduction"

    word.metaClass.whisper = {->
      delegate.toLowerCase()
    }

    use(StringUtils, StringHelper){
      //from org.apache.commons.lang.StringUtils
      assertEquals "Intro...", word.abbreviate(8)

      //from the StringHelper Category
      assertEquals "INTRODUCTION", word.shout()

      //from the word.metaClass
      assertEquals "introduction", word.whisper()
    }
  }
}

class StringHelper{
  static String shout(String self){
    return self.toUpperCase()
  }
}

Tapez groovy -cp /jars/commons-lang-2.4.jar:. CommonsTest.groovy pour exécuter le test. (Bien sûr, vous devez modifier le chemin pour refléter l'endroit où se trouve le JAR dans votre système).

Métaprogrammation et REST

Juste afin de m'assurer que je ne vous laisse pas avec l'impression erronnée que la métaprogrammation est utile seulement pour les tests unitaires, voici un dernier exemple. Rappelez-vous l'exemple du service web de type REST fourni par Yahoo! afin d'avoir les conditions météorologiques actuelles, que nous avons utilisé dans le volet "Générer, analyser et ingurgiter du XML". Combinez les compétences concernant XmlSlurper que vous avez acquises dans cet article avec les compétences en métaprogrammation acquises venant de celui-ci, et vous pouvez consulter la météo pour n'importe quel code ZIP en 10 lignes de code, comme dans le Listing 17:

Listing 17. Ajout d'une méthode weather

String.metaClass.weather={->
  if(!delegate.isInteger()){
    return "The weather() method only works with zip codes like '90201'"
  }
  def addr = "http://weather.yahooapis.com/forecastrss?p=${delegate}"
  def rss = new XmlSlurper().parse(addr)
  def results = rss.channel.item.title
  results << "\n" + rss.channel.item.condition.@text
  results << "\nTemp: " + rss.channel.item.condition.@temp
}

println "80020".weather()


//sortie
Conditions for Broomfield, CO at 1:57 pm MDT
Mostly Cloudy
Temp: 72

Comme vous le voyez, la métaprogrammation apporte une souplesse extrême. Vous pouvez utiliser une ou plusieurs des techniques mises en valeurs dans l'article pour ajouter aisément des méthodes à une, plusieurs ou toutes les classes que vous voulez.

Conclusion

Il est tout simplement irréaliste d'attendre de la réalité qu'elle se conforme aux limitations arbitraires de votre langage. La modélisation du monde réel dans un logiciel nécessite un outil assez souple pour gérer tous les cas limite. Heureusement, avec les fermetures de Groovy, les ExpandoMetaClasses, et les catégories, vous disposez d'un ensemble d'outils bien affûté pour ajouter des comportements où vous en avez besoin, quand vous en avez besoin.

La prochaine fois, je m'arrêterai sur la puissance de Groovy pour les tests unitaires. Il y a de réels bénéfices à écrire vos tests en Groovy, que ce soit un GroovyTestCase ou un test JUnit 4.x avec des annotations. Vous verrez également GMock à l'oeuvre, un framework de simulacres écrit en Groovy. En attendant, j'espère que vous trouverez beaucoup d'utilisations pratiques de Groovy.

L'annotation @Delegate

Article d'origine: 

Exploration des limites du duck typing dans un langage à typage statique

Résumé: 

Scott Davis continue sa présentation de la métaprogrammation Groovy avec une étude approfondie de l'annotation @Delegate, qui gomme les distinctions entre type de données et comportement d'une part, et entre typage statique et dynamique d'autre part.

Dans les articles précédents de Groovy par la pratique, vous avez vu que des caractéristiques de Groovy telles que les fermetures et la métaprogrammation apportent du dynamisme au développement Java™. Cet article va encore plus loin. Vous verrez que l'annotation @Delegate est une variante de l'utilisation du délégué par ExpandoMetaClass. Vous verrez également (une fois de plus) pourquoi les possibilités dynamiques de Groovy en font un langage idéal pour les tests unitaires.

Dans l'article "Métaprogrammation avec les fermetures, ExpandoMetaClass, et les catégories", nous avons introduit la notion de délégué. Par l'ajout d'une méthode shout() à l'ExpandoMetaClass de java.lang.String, nous avons utilisé le délégué pour définir la relation entre les deux classes, comme le montre le Listing 1:

Listing 1. Utilisation d'un délégué pour accéder à String.toUpperCase()

String.metaClass.shout = {->
  return delegate.toUpperCase()
}

println "Hello MetaProgramming".shout()

//sortie
HELLO METAPROGRAMMING

On ne peut pas utiliser this.toUpperCase(), parce que ExpandoMetaClass n'a pas de méthode toUpperCase(). De même, on ne peut pas utiliser super.toUpperCase(), parce que ExpandoMetaClass n'étend pas String. (En fait, elle ne pourrait pas étendre String, puisque String est une classe finale). Le langage Java n'a tout simplement pas le vocabulaire nécessaire pour exprimer la relation symbiotique entre les deux classes. C'est pourquoi Groovy a introduit le concept de délégué.

L'annotation @Delegate a été ajoutée au langage dans Groovy 1.6. (Voir les Ressources pour voir la liste des nouvelles annotations ajoutées dans Groovy 1.6). Cette annotation permet d'ajouter un ou plusieurs délégués à n'importe quelle classe, pas seulement ExpandoMetaClass.

Pour apprécier pleinement la puissance de l'annotation @Delegate, considérons une difficulté très courante en programmation Java: la création d'une nouvelle classe basée sur une classe finale.

Le modèle de conception Composite et les classes finales

Supposons que vous vouliez créer une classe AllCapsString qui se comporte comme java.lang.String, excepté que - comme son nom l'indique - la valeur retournée est toujours en majuscules. String est une classe finale - l'équivalent Java d'une impasse évolutionnaire. Le Listing 2 prouve que vous ne pouvez pas étendre String directement:

Listing 2. Impossibilité d'étendre une classe finale

class AllCapsString extends String{
}

$ groovyc AllCapsString.groovy

org.codehaus.groovy.control.MultipleCompilationErrorsException: 
startup failed, AllCapsString.groovy: 1: You are not allowed to 
overwrite the final class 'java.lang.String'.
 @ line 1, column 1.
   class AllCapsString extends String{
   ^

1 error

Ca n'a pas marché, si bien que l'idée suivante est d'utiliser le modèle de conception Composite, comme le montre le Listing 3 (pour plus de détails sur le modèle de conception Composite, voir les Ressources):

Listing 3. Utilisation de la composition pour créer un nouveau type de classe String

class AllCapsString{
  final String body
  
  AllCapsString(String body){
    this.body = body.toUpperCase()
  }
  
  String toString(){
    body
  }
    
  //Maintenant il faut implémenter les 72 méthodes de String
  char charAt(int index){
    return body.charAt(index)
  }
  
  //une de faite, plus que 71...
}

Ainsi, la classe AllCapsString a une String, mais elle ne se comportera pas comme une String à moins que vous ne lui rajoutiez les 72 méthodes de String. Pour voir les méthodes qu'il faut ajouter, vous pouvez vous référer à la Javadoc de String, ou exécuter le code du Listing 4:

Listing 4. Affichage des méthodes de la classe String

String.class.methods.eachWithIndex{method, i-> 
  println "${i} ${method}"
}

//output
0 public boolean java.lang.String.contentEquals(java.lang.CharSequence)
1 public boolean java.lang.String.contentEquals(java.lang.StringBuffer)
2 public boolean java.lang.String.contains(java.lang.CharSequence)
...

L'ajout manuel de 72 méthodes à AllCapsString n'est pas une utilisation judicieuse de votre précieux temps de développeur. C'est là que l'annotation @Delegate se révèle pratique.

Comprendre @Delegate

@Delegate est une annotation évaluée à la compilation qui demande au compilateur de pousser toutes les méthodes du délégué, ainsi que les interfaces qu'il implémente, vers la classe externe.

Avant d'ajouter l'annotation @Delegate à body, compilez AllCapsString et utilisez javap pour vérifier que la plupart des méthodes de String sont manquantes, comme le montre le Listing 5:

Listing 5. AllCapsString avant l'utilisation de @Delegate

$ groovyc AllCapsString.groovy 
$ javap AllCapsString
Compiled from "AllCapsString.groovy"
public class AllCapsString extends java.lang.Object 
       implements groovy.lang.GroovyObject{
    public AllCapsString(java.lang.String);
    public java.lang.String toString();
    public final java.lang.String getBody();
    //snip...

Maintenant, ajoutez l'annotation @Delegate à body comme dans le Listing 6. Repétez les commandes groovyc et javap, et voilà (ndT: en français dans le texte), AllCapsString possède les mêmes méthodes et implémente les mêmes interfaces que java.lang.String.

Listing 6. Utilisation de l'annotation @Delegate pour pousser toutes les méthodes de String vers la classe englobante

class AllCapsString{
  @Delegate final String body

  AllCapsString(String body){
    this.body = body.toUpperCase()
  }
  
  String toString(){
    body
  } 
}

$ groovyc AllCapsString.groovy 
$ javap AllCapsString
Compiled from "AllCapsString.groovy"
public class AllCapsString extends java.lang.Object 
       implements java.lang.CharSequence, java.lang.Comparable,
       java.io.Serializable,groovy.lang.GroovyObject{
       
    //NOTE: méthodes de AllCapsString:   
    public AllCapsString(java.lang.String);
    public java.lang.String toString();
    public final java.lang.String getBody();    
    
    //NOTE: méthodes de java.lang.String:
    public boolean contains(java.lang.CharSequence);
    public int compareTo(java.lang.Object);
    public java.lang.String toUpperCase();
    //snip...

Remarquez, cependant, qu'il est toujours possible d'appeler getBody() et ainsi de passer outre les méthodes poussées vers la classe englobante AllCapsString. En ajoutant private à la déclaration du champ - @Delegate final private String body - on peut empêcher le couple getter / setter usuel d'apparaitre. Ceci achève la transformation: AllCapsString fournit intégralement le comportement d'une String, vous permettant de redéfinir les méthodes natives de String comme vous le souhaitez.

Les limites du duck typing dans un langage statique

Bien que AllCapsString se comporte désormais exactement comme une String, ce n'est toujours pas vraiment une String. Dans le code Java, vous ne pouvez pas utiliser AllCapsString à la place de String, parce que ça n'est pas vraiment un canard, ça cancane seulement. (Les langages dynamiques utilisent le duck typing, le langage Java utilise le typage statique. Voir les Ressources pour en savoir plus sur la différence entre les deux). Autrement dit, parce que AllCapsString n'étend pas réellement String (et n'implémente pas non plus l'interface Stringable, qui n'existe pas), vous ne pouvez pas l'utiliser de manière interchangeable avec String dans votre code Java. Le Listing 7 montre qu'un cast de AllCapsString en String échoue en Java:

Listing 7. Le typage statique de Java empêche AllCapsString d'être utilisé en lieu et place de String

public class JavaExample{
  public static void main(String[] args){
    String s = new AllCapsString("Hello");
  }
}

$ javac JavaExample.java 
JavaExample.java:5: incompatible types
found   : AllCapsString
required: java.lang.String
    String s = new AllCapsString("Hello");
               ^
1 error

Comme on le voit, tout en ne corrompant pas vraiment la signification du mot-clé final de Java en vous permettant d'étendre une classe pour laquelle le développeur original avait expressément interdit l'extension, @Delegate vous donne autant de puissance que possible sans franchir la limite.

Et rappelez vous qu'une classe peut avoir plus d'un délégué. Supposez que vous vouliez créer une classe RemoteFile qui aie les caractéristiques à la fois de java.io.File et de java.net.URL. Le langage Java ne permet pas l'héritage multiple, mais vous pouvez vous en approcher terriblement avec une paire de @Delegates, comme le montre le Listing 8. La classe RemoteFile n'est ni un File ni une URL, mais elle a les comportements des deux.

Listing 8. Plusieurs @Delegates, offrant le comportement de l'héritage multiple

class RemoteFile{
  @Delegate File file
  @Delegate URL url
}

Si @Delegate peut uniquement changer le comportement de votre classe - pas le type - cela veut-il dire qu'elle est inutile aux développeurs Java ? Loin de là. Même les langages typés statiquement comme Java fournissent une forme limitée de duck typing appelée polymorphisme.

Cancaner comme un canard polymorphique

Le polymorphisme - un mot dérivé du Grec signifiant "plusieurs formes" - signifie que si des classes partagent explicitement le même comportement en implémentant la même interface, elle peuvent être utilisées de manière interchangeable. Dans d'autres termes, si vous définissez une variable de type Duck (en supposant que Duck (ndT: canard) est une interface qui expose les méthodes quack() (ndT: cancaner) et waddle() (ndT: se dandiner)), vous pouvez lui affecter un Mallard() (ndT: col-vert), un GreenWingedTeal() (ndT: sarcelle de Caroline), ou (mon préféré) un PekingWithHoisinSauce() (ndT: canard laqué à la Pékinoise).

L'annotation @Delegate fournit un support au polymorphisme en promouvant non seulement les méthodes de la classe déléguée vers la classe externe, mais aussi les interfaces du délégué. Cela a pour conséquence que si la classe déléguée implémente une interface, vous pouvez en créer un substitut.

@Delegate et l'interface List

Supposons que l'on veuille créer une nouvelle classe nommée FixedList. Elle doit se comporter comme une java.util.ArrayList avec une distinction importante: on doit pouvoir définir une limite supérieure au nombre d'éléments que l'on peut y ajouter. Cela vous permet de créer une variable sportsCar qui peut embarquer deux passagers mais pas plus, une variable restaurantTable qui peut accueillir 4 convives au maximum, etc.

La classe ArrayList implémente l'interface List. Cela vous donne plusieurs possibilités. Vous pourriez également implémenter l'interface List dans la classe FixedList, mais alors il faudrait effectuer le travail fastidieux de fournir une implémentation pour chacune des méthodes de List. Puisque ArrayList n'est pas une classe finale, une autre possibilité est d'étendre simplement ArrayList dans FixedList. C'est une solution parfaitement valide, mais dans le cas (hypothétique) où ArrayList serait finale, l'annotation @Delegate fournirait une troisième option: en faisant de ArrayList un délégué de FixedList, vous pourriez obtenir tous les comportements ArrayList de  tout en implémentant automatiquement l'interface List.

Pour commencer, créez la classe FixedList avec un délégué ArrayList, comme dans le Listing 9. Effectuez notre combinaison préférée groovyc / javap pour vérifier que FixedList offre non seulement les mêmes méthodes que ArrayList, mais qu'elle implémente aussi les mêmes interfaces.

Listing 9. Première étape de la création de la classe FixedList

class FixedList{
  @Delegate private List list = new ArrayList()
  final int sizeLimit
  
  /**
    * NOTE: This constructor limits the max size of the list,
    *  not just the initial capacity like an ArrayList.
    */
  FixedList(int sizeLimit){
    this.sizeLimit = sizeLimit
  }
}  

$ groovyc FixedList.groovy
$ javap FixedList
Compiled from "FixedList.groovy"
public class FixedList extends java.lang.Object 
             implements java.util.List,java.lang.Iterable,
             java.util.Collection,groovy.lang.GroovyObject{
    public FixedList(int);
    public java.lang.Object[] toArray(java.lang.Object[]);
    //snip..

Vous n'avez encore rien fait pour contraindre la taille de FixedList, mais c'est un bon début. Comment pourriez-vous vous assurer que la taille d'une FixedList n'est pas, eh bien, fixée à cet instant ? Vous pourriez écrire un bout de code jetable, mais si FixedList est destinée à passer en production, vous seriez mieux inspiré d'écrire quelques cas de test.

Tester @Delegate avec GroovyTestCase

Pour commencer à tester @Delegate, écrivez un test unitaire qui prouve que l'on peut ajouter plus d'éléments à une FixedList que ce qui est permis (ndT: vous connaissez les Trois Stooges ?). Le Listing 10 montre ce test:

Listing 10. Ecriture d'un test qui échoue tout d'abord

class FixedListTest extends GroovyTestCase{
  
  void testAdd(){
    List threeStooges = new FixedList(3)
    threeStooges.add("Moe")    
    threeStooges.add("Larry")
    threeStooges.add("Curly")
    threeStooges.add("Shemp")
    assertEquals threeStooges.sizeLimit, threeStooges.size()
  }
}

$ groovy FixedListTest.groovy 

There was 1 failure:
1) testAdd(FixedListTest)junit.framework.AssertionFailedError: 
   expected:<3> but was:<4>

Il semble que vous devriez redéfinir la méthode add() dans FixedList, comme dans le Listing 11. Une nouvelle exécution du test échoue encore, mais cette fois à cause de l'exception qui est lancée.

Listing 11. Rédéfinition de la méthode add() de ArrayList

class FixedList{
  @Delegate private List list = new ArrayList()
  final int sizeLimit
  
  //snip...
  
  boolean add(Object element){
    if(list.size() < sizeLimit){
      return list.add(element)
    }else{
      throw new UnsupportedOperationException("Error adding ${element}:" +
                 " the size of this FixedList is limited to ${sizeLimit}.")
    }
  }
}

$ groovy FixedListTest.groovy 

There was 1 error:
1) testAdd(FixedListTest)java.lang.UnsupportedOperationException: 
   Error adding Shemp: the size of this FixedList is limited to 3.

Grâce à la méthode shouldFail de GroovyTestCase, qui se révèle bien pratique, vous pouvez récupérer l'exception comme dans l'exemple du Listing 12, et fêter votre premier test couronné de succès:

Listing 12. La méthode shouldFail gère l'exception attendue

class FixedListTest extends GroovyTestCase{
  void testAdd(){
    List threeStooges = new FixedList(3)
    threeStooges.add("Moe")    
    threeStooges.add("Larry")
    threeStooges.add("Curly")
    assertEquals threeStooges.sizeLimit, threeStooges.size()
    shouldFail(java.lang.UnsupportedOperationException){
      threeStooges.add("Shemp")      
    }
  }
}  

Test de la surcharge d'opérateur

Dans l'article "Smooth Operators" (ndT: non traduit), vous avez vu que Groovy supporte la surcharge d'opérateur. Avec les Lists, on peut aussi bien utiliser << pour ajouter des éléments que la traditionnelle méthode add(). Ecrivez un petit test unitaire comme celui du Listing 13 pour vérifier que l'utilisation de << ne casse pas par hasard le fonctionnement de FixedList:

Listing 13. Test de la surcharge d'opérateur

class FixedListTest extends GroovyTestCase{
 
  void testOperatorOverloading(){
    List oneList = new FixedList(1)
    oneList << "one"
    shouldFail(java.lang.UnsupportedOperationException){
      oneList << "two"
    }    
  }
}

Voir le test passer devrait vous apporter une certaine sérénité.

Vous pouvez aussi tester les cas pathologiques. Le Listing 14, par exemple, teste ce qui se passe si vous créez une FixedList avec un nombre d'éléments négatif:

Listing 14. Test des cas aux limites

class FixedListTest extends GroovyTestCase{
  void testNegativeSize(){
    List badList = new FixedList(-1)
    shouldFail(java.lang.UnsupportedOperationException){
      badList << "will this work?"
    }    
  }
}

Test de l'insertion d'un élément en milieu de liste

Maintenant que vous êtes convaincu que la méthode add() redéfinie fonctionne, l'étape suivante est d'implémenter la surcharge de la méthode add() qui prend un index et un élément, comme le montre le Listing 15:

Listing 15. Ajout d'éléments avec index

class FixedList{
  @Delegate private List list = new ArrayList()
  final int sizeLimit
   
  void add(int index, Object element){
    list.add(index, element)
    trimToSize()
  }
  
  private void trimToSize(){
    if(list.size() > sizeLimit){
      (sizeLimit..<list.size()).each{
        list.pop()
      }
    }
  }
}

Notez que vous pouvez (et devriez) vous en remettre aux fonctionnalités natives du délégué à chaque fois que c'est possible - après tout, c'est bien pour cette raison que vous avez choisi d'en faire un délégué. Dans notre cas, nous laissons ArrayList s'occuper de l'ajout, et nous élaguons les éléments en surnombre par rapport à la taille de FixedList. (Le fait de déclencher ou non une UnsupportedOperationException dans notre méthode add() comme dans la méthode sans paramètre est un choix de conception qui vous appartient).

La méthode trimToSize() contient un peu de sucre syntaxique de Groovy digne que l'on s'y intéresse. Tout d'abord, la méthode pop() est un ajout par métaprogrammation de Groovy à toutes les Lists. Il supprime le dernier élément de la List, en mode LIFO (dernier entré, premier servi).

De plus, on peut remarquer l'utilisation d'un range Groovy dans la boucle each. Pour mieux comprendre ce comportement, il peut être utile de remplacer les variables par des vrais nombres. Supposons que la FixedList a pour sizeLimit 3, et qu'après l'ajout de nouveaux éléments elle atteigne une size() de 5. L'expression serait donc évaluée comme (3..5).each{}. Mais comme les Lists utilisent 0 comme indice de départ, aucun élément de la liste n'a 5 pour indice. En spécifiant (3..<5).each{}, on exclut le dernier nombre de l'intervalle.

Ecrivez quelques tests, à l'exemple du Listing 16, pour vérifier que la nouvelle méthode add() surchargée se comporte comme prévu:

Listing 16. Tester l'ajout d'éléments au milieu de FixedList

class FixedListTest extends GroovyTestCase{
  void testAddWithIndex(){
    List threeStooges = new FixedList(3)
    threeStooges.add("Moe")    
    threeStooges.add("Larry")
    threeStooges.add("Curly")
    threeStooges.add(2,"Shemp")
    assertEquals 3, threeStooges.size()
    assertFalse threeStooges.contains("Curly")
  }

  void testAddWithIndexOnALessThanFullList(){
    List threeStooges = new FixedList(3)
    threeStooges.add("Curly")
    assertEquals 1, threeStooges.size()

    threeStooges.add(0, "Larry")
    assertEquals 2, threeStooges.size()
    assertEquals "Larry", threeStooges[0]

    threeStooges.add(0, "Moe")
    assertEquals 3, threeStooges.size()
    assertEquals "Moe", threeStooges[0]
    assertEquals "Larry", threeStooges[1]
    assertEquals "Curly", threeStooges[2]
  }
}

Avez-vous remarqué que vous avez écrit plus de code de test que de code de production ? C'est bien ! Je dis souvent que le code de test doit peser au moins le double du code de production.

Implémenter addAll()

Pour terminer l'écriture de la classe FixedList, redéfinissez les méthodes addAll() de ArrayList, comme le montre le Listing 17:

Listing 17. Implémenter les méthodes addAll()

class FixedList{
  @Delegate private List list = new ArrayList()
  final int sizeLimit
  
  boolean addAll(Collection collection){
    def returnValue = list.addAll(collection)
    trimToSize()
    return returnValue
  }

  boolean addAll(int index, Collection collection){
    def returnValue = list.addAll(index, collection)
    trimToSize()
    return returnValue
  }
}

Maintenant écrivez les tests unitaires correspondants,  comme dans le Listing 18:

Listing 18. Tester les méthodes addAll()

class FixedListTest extends GroovyTestCase{
  void testAddAll(){
    def quartet = ["John", "Paul", "George", "Ringo"]
    def trio = new FixedList(3)
    trio.addAll(quartet)
    assertEquals 3, trio.size()
    assertFalse trio.contains("Ringo")
  }

  void testAddAllWithIndex(){
    def quartet = new FixedList(4)
    quartet << "John"
    quartet << "Ringo"
    quartet.addAll(1, ["Paul", "George"])
    assertEquals "John", quartet[0]
    assertEquals "Paul", quartet[1]
    assertEquals "George", quartet[2]
    assertEquals "Ringo", quartet[3]
  }
}

Et voilà le résultat. Grâce à la puissance de l'annotation @Delegate, vous avez été en mesure de créer une classe FixedList avec à peu près 50 lignes de code. Grâce à la puissance de GroovyTestCase, vous l'avez testé, ce qui vous permettra de mettre ce code en production avec l'assurance qu'il se comporte comme prévu. Le Listing 19 montre la totalité de la classe FixedList:

Listing 19. La classe FixedList complète

class FixedList{
  @Delegate private List list = new ArrayList()
  final int sizeLimit
  
  /**
    * NOTE: This constructor limits the max size of the list,
    *  not just the initial capacity like an ArrayList.
    */
  FixedList(int sizeLimit){
    this.sizeLimit = sizeLimit
  }
  
  boolean add(Object element){
    if(list.size() < sizeLimit){
      return list.add(element)
    }else{
      throw new UnsupportedOperationException("Error adding ${element}:" + 
        " the size of this FixedList is limited to ${sizeLimit}.")
    }
  }
  
  void add(int index, Object element){
    list.add(index, element)
    trimToSize()
  }
  
  private void trimToSize(){
    if(list.size() > sizeLimit){
      (sizeLimit..<list.size()).each{
        list.pop()
      }
    }
  }

  boolean addAll(Collection collection){
    def returnValue = list.addAll(collection)
    trimToSize()
    return returnValue
  }
  
  boolean addAll(int index, Collection collection){
    def returnValue = list.addAll(index, collection)
    trimToSize()
    return returnValue
  }
  
  String toString(){
    return "FixedList size: ${sizeLimit}\n" + "${list}"
  }
}

Conclusion

En se concentrant sur la capacité d'ajouter de nouveaux comportements aux classes au lieu de changer leur type, la métaprogrammation Groovy ouvre sur tout un ensemble de possibilités dynamiques sans violer les règles du système de typage statique de Java. Avec ExpandoMetaClass (avec laquelle vous pouvez ajouter arbitrairement de nouvelles méthodes à des classes existantes en les enveloppant) et @Delegate (avec laquelle vous pouvez exposer les fonctionnalités des classes composites internes dans la classe d'enveloppe externe), Groovy permet à la JVM de se dandiner et de cancaner comme jamais.

Dans le prochain article, je parlerai d'une technologie ancienne qui bénéficie d'un second souffle grâce à la syntaxe flexible de Groovy: Swing. Oui, la complexité de Swing s'évanouit complètement avec le SwingBuilder de Groovy. Il rend le développement d'interfaces graphiques - oserai-je le dire ? - amusant et facile. En attendant, j'espère que vous trouverez beaucoup d'utilisations pratiques de Groovy.

SwingBuilder et l'API de Twitter, 1ère partie

Article d'origine: 

Construire des IHM avec Swing n'a jamais été aussi facile

Résumé: 

Dans ce volet de Groovy par la pratique, Scott Davis s'attaque à un sujet qui plonge dans la terreur la plupart des développeurs Java habitués au côté serveur: Swing. Comme vous le verrez, le SwingBuilder de Groovy rend moins pénible l'utilisation de ce framework d'IHM puissant mais complexe.

Récemment, j'ai interviewé (voir les Ressources) Ted Neward, l'auteur de la série d'articles The busy Java developer's guide to Scala pour IBM developerWorks. Nous avons discuté d'une librairie pour Twitter qu'il développe dans sa série, et qui est intéressante, appelée Scitter (Scala + Twitter). Scitter met en exergue les possibilités de Scala dans le domaine des services Web et de l'analyse XML, mais Ted a admis qu'il ne développerait probablement jamais de frontal pour cette API. Ceci m'a bien sûr fait imaginer ce à quoi une IHM pour Twitter écrite en Groovy pourrait ressembler. Est-ce que Gwitter (Groovy + Twitter) sonnerait bien pour nommer ce projet ?

Je ne m'attaquerai pas à l'intégration Scala + Groovy dans cet article,bien qu'il y ait beaucoup de synergies potentielles entre les deux langages. Je m'intéresserai plutôt à une portion de l'écosystème Java souvent sous-estimée par les développeurs Java: Swing. Mais avant cela, voyons comment le XmlSlurper de Groovy traite aisément le flux Atom de Twitter.

L'API de recherche de Twitter

En parcourant la documentation en ligne de l'API de recherche de Twitter (voir les Ressources), on s'aperçoit qu'il est possible de faire une simple requête HTTP GET pour effectuer une recherche Twitter. La requête est passée par le biais d'un paramètre nommé q dans la chaine de requête, et les résultats peuvent être retournés soit sous forme Atom (un format XML de syndication) ou JSON (JavaScript Object Notation). Par exemple pour obtenir un flux Atom contenant toutes les occurences de thirstyhead, il faut faire la requête HTTP GET suivante: http://search.twitter.com/search.atom?q=thirstyhead.

Comme le montre le Listing 1, les résultats sont retournés comme une série d'éléments <entry> imbriqués dans un élément <feed>:

Listing 1. Les résultats d'une recherche Twitter sous forme Atom

<feed xml:lang="en-US" xmlns="http://www.w3.org/2005/Atom">
<entry>
    <title>thirstyhead: New series from Andrew Glover: Java Development 2.0 
           http://bit.ly/bJX5i</title>
    <content type="html">thirstyhead: New series from Andrew Glover: Java 
                         Development 2.0 http://bit.ly/bJX5i</content>
    <id>tag:twitter.com,2007:
        http://twitter.com/thirstyhead/statuses/3419507135</id>
    <published>2009-08-20T02:54:54+00:00</published>
    <updated>2009-08-20T02:54:54+00:00</updated>
    <link type="text/html" rel="alternate" 
          href="http://twitter.com/thirstyhead/statuses/3419507135"/>
    <link type="image/jpeg" rel="image" 
          href="http://s3.amazonaws.com/twitter_production/profile_images/
          73550313/flame_normal.jpg"/>
    <author>
      <name>ThirstyHead.com</name>
      <uri>http://www.thirstyhead.com</uri>
    </author>
  </entry>

  <entry>...</entry>
  <entry>...</entry>  
  <!-- snip -->
</feed>

Dans "Générer, analyser et ingurgiter du XML", vous avez vu combien il est facile d'expédier l'analyse du XML en utilisant XmlSlurper. Maintenant que vous savez à quoi le résultat de la requête va ressembler, créez un fichier nommé searchCli.groovy, comme dans le Listing 2:

Listing 2. Un script Groovy pour analyser les résultats Atom

if(args){
 def username = args[0]
 def addr = "http://search.twitter.com/search.atom?q=${username}"
 def feed = new XmlSlurper().parse(addr)
 feed.entry.each{
   println it.author.name
   println it.published
   println it.title
   println "-"*20
 }  
}else{
 println "USAGE: groovy searchCli <query>"
}

Entrez groovy searchCli thirstyhead en ligne de commande (ndT: cette recherche ne retourne plus aucun résultat maintenant, essayez autre chose !), et prenez plaisir à votre maitrise en 13 lignes des résultats Atom, comme le montre le Listing 3:

Listing 3. Exécution du script searchCli.groovy

$ groovy searchCli thirstyhead

thirstyhead (ThirstyHead.com)
2009-08-20T02:54:54Z
New series from Andrew Glover: 
Java Development 2.0 http://bit.ly/bJX5i
--------------------
kung_foo (kung_foo)
2009-08-18T12:33:32Z
ThirstyHead interviews Venkat Subramaniam: 
http://blip.tv/file/2484840 "Groovy and Scala are good friends..." 
(via @mittie). very good.

//snip

Création des classes initiales de Gwitter

Même si les scripts Groovy vont très bien pour les petits utilitaires informels et les expérimentations de preuves de concept, écrire des classes Groovy n'est pas beaucoup plus difficile. Et en guise de récompense, vous pouvez compiler les classes Groovy et les appeler dans du code Java.

Par exemple, créez Tweet.groovy à l'exemple du Listing 4:

Listing 4. Tweet.groovy

class Tweet{
  String content
  String published
  String author
  
  String toString(){
    return "${author}: ${content}"
  }
}

Comme vous le savez déjà, ce POGO (Plain Old Groovy Object) est un substitut du POJO (Plain Old Java Object) significativement plus verbeux.

Maintenant, convertissez le script de recherche du Listing 2 en la classe Search.groovy, comme le montre le Listing 5:

Listing 5. Search.groovy

class Search{
  static final String addr = "http://search.twitter.com/search.atom?q="
  
  static Object[] byKeyword(String query){
    def results = []
    def feed = new XmlSlurper().parse(addr + query)
    feed.entry.each{entry->
      def tweet = new Tweet()
      tweet.author = entry.author.name
      tweet.published = entry.published
      tweet.content = entry.title
      results << tweet
    }
    return results as Object[]    
  }
}

En temps normal, je me contenterais du résulat sous forme de java.util.ArrayList. Mais la javax.swing.JList que vous utiliserez plus loin dans l'article a besoin d'un Object[], considérez donc cela comme une toute petite avance sur la suite.

Notez que j'ai ôté la méthode main() de Search.groovy. Comment interagir avec cette classe maintenant ? Eh bien, avec un test unitaire, bien sûr ! Créez SearchTest.groovy, comme le montre le Listing 6:

Listing 6. SearchTest.groovy

class SearchTest extends GroovyTestCase{
  void testSearchByKeyword(){
    def results = Search.byKeyword("thirstyhead")
    results.each{
      assertTrue it.content.toLowerCase().contains("thirstyhead") ||
                 it.author.toLowerCase().contains("thirstyhead")
    }    
  }
}

Si vous tapez groovy SearchTest en ligne de commande et que vous voyez  (voir le Listing 7), vous avez converti avec succès votre simple script de recherche en un ensemble de classes réutilisables:

Listing 7. Résultat d'une exécution réussie du test

$ groovy SearchTest
.
Time: 4.64

OK (1 test)

Maintenant que la structure sous-jacente est en place, la prochaine étape est de commencer à lui faire un joli visage.

Introduction à SwingBuilder

Swing est une boite à outils IHM incroyablement puissante. Malheureusement parfois la puissance est cachée par la complexité. Si vous découvrez Swing, vous pourriez avoir la sensation d'apprendre à piloter un Boeing 747 quand tout ce dont vous avez vraiment besoin est un Cessna monomoteur - ou un deltaplane.

Le SwingBuilder de Groovy ne diminue pas la complexité intrinsèque de tâches telles que choisir le bon LayoutManager ou gérer correctement les problèmes de processus. C'est la complexité syntaxique qui est réduite. Comme vous le verrez dans un instant, Groovy offre des constructeurs avec arguments nommés et en nombre variable qui conviennent parfaitement pour les différents JComponents qu'il est nécessaire d'instancier puis de configurer par une série de setters. (Pour plus de détail sur SwingBuilder, voir les Ressources).

Mais l'utilisation des fermetures de Groovy est tout aussi appréciable. Mon problème depuis toujours avec Swing est qu'une arborescence naturelle semble disparaitre parmi les détails d'implémentation. Avec Java, vous finissez avec un ensemble décousu de composants sans aucune véritable intuition de qui appartient à qui. Vous pouvez déclarer une JFrame, un JPanel, et un JLabel dans n'importe quel ordre. Dans le code, on dirait qu'ils sont d'égale importance, alors qu'en fait la JFrame contient le JPanel, qui à son tour contient le JLabel. Regardez le Listing 8 pour avoir un exemple:

Listing 8. HelloJavaSwing.java

import javax.swing.*;

public class HelloJavaSwing {
  public static void main(String[] args) {
    JPanel panel = new JPanel();
    JLabel label = new JLabel("Hello Java Swing");

    JFrame frame = new JFrame("Hello Java Swing");
    panel.add(label);
    frame.add(panel);
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    frame.setSize(200,300);
    frame.setVisible(true);
  }
}

La compilation (javac HelloJavaSwing.java) et l'exécution (java HelloJava) de ce code devraient conduire à l'application illustrée dans la Figure 1:

Figure 1. HelloJavaSwing

Hello Java Swing

Le Listing 9 montre la même application écrite en Groovy. Comme vous le voyez, l'utilisation judicieuse que SwingBuilder fait des fermetures vous permet de voir clairement la chaine de composition dans du code déclaratif.

Listing 9. HelloGroovySwing.groovy

import groovy.swing.SwingBuilder
import javax.swing.*

def swingBuilder = new SwingBuilder()
swingBuilder.frame(title:"Hello Groovy Swing", 
                   defaultCloseOperation:JFrame.EXIT_ON_CLOSE, 
                   size:[200,300],
                   show:true) {
  panel(){
    label("Hello Groovy Swing")    
  }
}

Entrez groovy HelloGroovySwing pour voir l'application de la Figure 2:

Figure 2. HelloGroovySwing

Hello Groovy Swing

Remarquez dans le Listing 9 que les J devant les noms de composants n'existent plus - un peu comme les get et set superflus sont ôtés des noms de méthodes. De plus , notez les arguments nommés du constructeur de frame. En coulisse, Groovy appelle le constructeur sans argument par défaut puis les setters - sans aucune différence avec l'exemple Java précédent. Mais le fait que tout soit rassemblé dans le constructeur fait que les choses sont bien nettes et ordonnées, et le retrait du préfixe set ainsi que des parenthèses finales réduit considérablement le bruit visuel.

Si vous ne connaissiez pas encore Swing, tout çà semble encore un peu compliqué. Mais si vous avez ne serait-ce qu'un peu d'expérience de Swing, cela ressemble certainement à du Swing réduit à son essence même: propre, clair, et efficace.

Comme vous l'avez fait dans la section précédente, prenez ce que vous avez appris avec ce script et convertissez-le en une classe. Créez un fichier nommé Gwitter.groovy, comme dans le Listing 10. Ce sont là les modestes débuts de l'interface utilisateur de votre client Groovy + Twitter.

Listing 10. Squelette de l'interface utilisateur de Gwitter

import groovy.swing.SwingBuilder
import javax.swing.*
import java.awt.*

class Gwitter{   
  static void main(String[] args){
    def gwitter = new Gwitter()
    gwitter.show()
  }
    
  void show(){
    def swingBuilder = new SwingBuilder()  
    swingBuilder.frame(title:"Gwitter", 
                       defaultCloseOperation:JFrame.EXIT_ON_CLOSE, 
                       size:[400,500],
                       show:true) {
    }    
  }  
} 

Tapez groovy Gwitter pour vérifier qu'une fenêtre vide s'affiche. Si tout se passe comme prévu, la prochaine étape est d'ajouter un menu simple à l'application.

Ajouter une barre de menu

La création de menus avec Swing donne un autre exemple de composants qui sont naturellement hiérarchisés. Vous créez une JMenuBar qui contient un ou plusieurs JMenus, qui à leur tour contiennent un ou plusieurs JMenuItems.

Pour créer un menu File avec un item Exit, ajoutez le code du Listing 11 à Gwitter.groovy:

Listing 11. Ajout d'un menu File à Gwitter

import groovy.swing.SwingBuilder
import javax.swing.*
import java.awt.*

class Gwitter{   
  static void main(String[] args){
    def gwitter = new Gwitter()
    gwitter.show()
  }
    
  void show(){
    def swingBuilder = new SwingBuilder()  
    
    def customMenuBar = {
      swingBuilder.menuBar{
        menu(text: "File", mnemonic: 'F') {
          menuItem(text: "Exit", mnemonic: 'X', actionPerformed: { dispose() })
        }
      }  
    }    
    
    swingBuilder.frame(title:"Gwitter", 
                       defaultCloseOperation:JFrame.EXIT_ON_CLOSE, 
                       size:[400,500],
                       show:true) {
      customMenuBar()                         
    }    
  }  
}

Notez la hiérarche d'imbrications de la fermeture customMenuBar. Je l'ai placé séparément pour des raisons de lisibilité, mais j'aurais aussi bien pu la définir sur place, au milieu de la définition de la fenêtre. Une fois que la fermeture est définie, je l'appelle à l'intérieur de la fermeture frame. Entrez groovy Gwitter une fois de plus pour vérifier que le menu File apparait, comme le montre la Figure 3. Sélectionnez File > Exit pour sortir de l'application.

Figure 3. Le menu File de Gwitter

Le menu File de Gwitter

Regardez de nouveau le Listing 11. Notez que le gestionnaire actionPerformed est défini dans une fermeture et non dans une classe anonyme. Est-ce que ça ne rend pas le code propre et facile à lire par comparaison à l'alternative Java ?

Désormais il est temps d'ajouter quelques éléments de formulaire pour permettre la recherche.

Ajout du panneau de recherche

Les développeurs Swing expérimentés sont adeptes de l'assemblage de JPanels distincts pour former une application. Ces conteneurs permettent de grouper facilement des composants dont les fonctions sont apparentées.

Par exemple, Gwitter a besoin d'un JTextField pour permettre aux utilisateurs de saisir un critère de recherche, et d'un JButton pour soumettre la requête. Ca semble une bonne idée de regrouper ces deux objets dans une fermeture searchPanel, comme dans le Listing 12:

Listing 12. Ajout de searchPanel

import groovy.swing.SwingBuilder
import javax.swing.*
import java.awt.*

class Gwitter{   
  def searchField
  
  static void main(String[] args){
    def gwitter = new Gwitter()
    gwitter.show()
  }
    
  void show(){
    def swingBuilder = new SwingBuilder()  
    
    def customMenuBar = {
      swingBuilder.menuBar{
        menu(text: "File", mnemonic: 'F') {
          menuItem(text: "Exit", mnemonic: 'X', actionPerformed: {dispose() })
        }
      }  
    }    

    def searchPanel = {
      swingBuilder.panel(constraints: BorderLayout.NORTH){
        searchField = textField(columns:15)
        button(text:"Search", actionPerformed:{ /* TODO */ } )
      }
    }
    
    swingBuilder.frame(title:"Gwitter", 
                       defaultCloseOperation:JFrame.EXIT_ON_CLOSE, 
                       size:[400,500],
                       show:true) {
      customMenuBar()                         
      searchPanel()
    }    
  }  
}

Une fois que vous commencez à travailler avec les panneaux, le problème du choix d'un LayoutManager approprié ne tarde pas à venir. Par défaut, un JPanel utilise un FlowLayout. Cela veut dire que le textField et le button seront placés horizontalement, l'un après l'autre.

Le contentPane d'une JFrame est légèrement différent - il utilise un BorderLayout par défaut. Quand vous ajoutez le searchPanel à la fenêtre, vous devez spécifier dans quelle zone vous voudriez le voir apparaitre: NORTH, SOUTH, EAST, WEST, ou CENTER. (Pour les nuls en géographie, il est aussi possible d'utiliser PAGE_START, PAGE_END, LINE_START, LINE_END, et CENTER). Pour en savoir plus sur les différents LayoutManagers disponibles dans Swing, voir les Ressources.

Notez que la variable searchField est déclarée au niveau de la classe. De cette manière les autres composants comme le bouton peuvent y accéder. Le reste des composants est anonyme. Un coup d'oeil rapide à la liste des attributs de classe donne une indication de l'importance relative des composants.

Vous avez probablement remarqué que l'écouteur actionPerformed du bouton ne fait rien... pour l'instant. Il ne saurait pas quelle tâche effectuer à cet instant précis. Avant de pouvoir l'implémenter, il faut ajouter une autre panneau à l'application: celui qui permettra d'afficher le résultat de la recherche.

Ajout du panneau d'affichage des résultats

Comme le montre le Listing 13, vous définissez un resultsPanel dans une fermeture imbriquée comme vous l'avez fait avec searchPanel. Seulement cette fois, vous ajoutez un conteneur à l'intérieur d'un panneau: un JScrollPane. Ce composant permet à des barres de défilement horizontales et verticales d'apparaitre et de disparaitre en fonction du besoin. Les résultats de la méthode Search.byKeyword() sont affichés dans une JList nommée resultsList. (Les méthodes JList.setListData() prennent en paramètre un Object[] - rappelez-vous que c'est justement ce que vous avez fait en sorte que la méthode Search.byKeyword() retourne.)

Listing 13. Ajout de resultsPanel

import groovy.swing.SwingBuilder
import javax.swing.*
import java.awt.*

class Gwitter{   
  def searchField
  def resultsList
  
  static void main(String[] args){
    def gwitter = new Gwitter()
    gwitter.show()
  }
    
  void show(){
    def swingBuilder = new SwingBuilder()  
    
    def customMenuBar = {
      swingBuilder.menuBar{
        menu(text: "File", mnemonic: 'F') {
          menuItem(text: "Exit", mnemonic: 'X', actionPerformed: {dispose() })
        }
      }  
    }    

    def searchPanel = {
      swingBuilder.panel(constraints: BorderLayout.NORTH){
        searchField = textField(columns:15)
        button(text:"Search", actionPerformed:{ 
          resultsList.listData = Search.byKeyword(searchField.text) } )
      }
    }
    
    def resultsPanel = {
      swingBuilder.scrollPane(constraints: BorderLayout.CENTER){
        resultsList = list()
      }
    }    
    
    swingBuilder.frame(title:"Gwitter", 
                       defaultCloseOperation:JFrame.EXIT_ON_CLOSE, 
                       size:[400,500],
                       show:true) {
      customMenuBar()                         
      searchPanel()
      resultsPanel()
    }    
  }  
}

Notez que la variable resultsList, comme searchField, est définie au niveau de la classe. Les deux variables sont utilisées dans le gestionnaire actionPerformed du bouton se trouvant dans le searchPanel.

Avec l'ajout du resultsPanel, Gwitter est désormais fonctionnel. Entrez en ligne de commande et vérifiez que ça fonctionne comme prévu. Chercher devrait renvoyer des résultats similaires à ceux de la Figure 4:

Figure 4. Première mouture des résultats de recherche

Première mouture des résultats de recherche

Vous pourriez arrêter là et crier victoire, mais j'aimerais attirer votre attention sur deux problèmes avant que vous n'effectuiez votre tour d'honneur. Le premier concerne la gestion des processus qui peut potentiellement virer au cauchemar, telle qu'elle est gérée par l' du bouton de recherche. Le second est la rusticité générale de l'application. Ces deux points sont résolus dans les deux prochaines sections.

Le processus de distribution des événements

Ce qui est cruellement ironique avec Swing, c'est qu'il attend des concepteurs d'IHM qu'ils soient à l'aise avec les problèmes de gestion de multiples processus plutôt destinés à des ingénieurs en développement, ou bien des ingénieurs en développement qu'ils comprennent les subtilités de la conception d'IHM et les problèmes d'utilisabilté.

Il est impossible de couvrir un sujet aussi complexe que les problèmes de gestion des processus dans une application Swing en quelques paragraphes. Il nous suffira de dire que les applications Swing sont fondamentalement mono-processus à la base. Tout se passe dans le processus de distribution des événements (EDT: Event Dispatch Thread). Lorsque les utilisateurs grognent a propos des temps de réponse de leur application Swing, c'est généralement parce qu'un développeur débutant a effectué une requête longue et coûteuse sur une base de données, ou bien a appelé des services Web, dans l'EDT - le même processus qui gère le rafraichissement de l'écran, les clics dans les menus, etc. Je vous ai fait faire involontairement ce genre de choses dans le gestionnaire actionPerformed du bouton de recherche. (Vous voyez comme il est facile de faire cette erreur courante).

Heureusement, la classe javax.swing.SwingUtilities propose quelques méthodes utilitaires bien nommées - invokeAndWait() et invokeLater() - qui aident à résoudre ces problèmes de gestion des processus. Les deux méthodes permettent d'agir de manière synchrone ou asynchrone sur l'EDT. (Voir les Ressources pour en savoir davantage sur la classe SwingUtilities). SwingBuilder facilite l'appel de ces deux méthodes, et vous donne une troisième option: la possibilité de créer sans effort un nouveau processus pour exécuter des actions potentiellement coûteuses.

Pour effectuer un appel synchrone sur l'EDT (SwingUtilities.invokeAndWait()), vous pouvez envelopper l'appel dans la fermeture edt{}. Pour effectuer un appel asynchrone sur l'EDT (SwingUtilities.invokeLater()), enveloppez l'appel dans une fermeture doLater{}. Mais je vais vous faire pratiquer la troisième possibilité: monter un nouveau processus pour gérer l'appel de méthode Search.byKeyword(). Pour ce faire, enveloppez le code dans une fermeture doOutside{}, à l'exemple du Listing 14:

Listing 14. Utilisation de la fermeture doOutside

def searchPanel = {
  swingBuilder.panel(constraints: BorderLayout.NORTH){
    searchField = textField(columns:15)
    button(text:"Search", actionPerformed:{ 
      doOutside{ 
        resultsList.listData = Search.byKeyword(searchField.text)
      }
    } )
  }
}

Pour une application aussi simple que Gwitter, il est probable que vous ne ressentiriez jamais vraiment comme il est pénible d'appeler le service Web dans l'EDT. Mais à la seconde où vous montreriez votre code à des experts Swing, ils vous regarderaient avec le mépris que l'on réserve d'habitude à ceux qui conduisent lentement sur la voie de gauche ou qui se garent sur les emplacements réservés aux personnes handicapées sur les parkings des magasins. Comme SwingBuilder rend faciles les bonnes pratiques concernant la gestion des processus, il n'y a pas de raison de s'en priver.

Maintenant que vous avez résolu ce problème de processus, la ligne suivante sur notre liste de choses à faire est de faire en sorte que cette application fasse moins mal aux yeux.

Ajouter des zébrures à la liste

Je n'ai pas peur d'admettre que Gwitter est assez laid pour l'instant. Je vais faire deux choses très simples pour améliorer l'apparence avec un peu de HTML. Par bonheur, les JLabels sont capables d'interpréter du HTML rudimentaire. Modifiez la méthode toString() de Tweet.groovy comme indiqué dans le Listing 15. La méthode toString() est appelée par JList pour afficher les résultats.

Listing 15. Renvoyer du HTML dans la méthode toString()

class Tweet{
  String content
  String published
  String author
  
  String toString(){
    //return "${author}: ${content}"

    return """<html>
         <body>
           <p><b><i>${author}:</i></b></p>
           <p>${content}</p>
         </body>
       </html>"""
  }
}

La modification suivante est un peu plus subtile. Une astuce courante dans les IHM est de zébrer les listes ou les tables longues. En alternant les couleurs sur les lignes paires et impaires, la liste est plus facile à parcourir. J'ai cherché JList stripes dans un moteur de recherche et j'ai suivi le conseil du premier article que j'ai trouvé. L'auteur recommende de créer un DefaultListCellRenderer personnalisé. J'ai hoché la tête avec un air de profonde sagesse en lisant chaque paragraphe, et ensuite j'ai volé sans aucune honte son exemple de code (voir les Ressources pour l'article complet).

Comme la syntaxe Groovy est un sur-ensemble de la syntaxe Java, j'ai pu copier et coller le code Java dans un fichier Groovy sans le modifier. Si j'avais disposé d'un système de construction complet pour compiler à la fois du code Groovy et du code Java, j'aurais même pu le laisser dans un fichier Java. Mais en lui donnant l'extension .groovy, je peux exécuter tout le code de Gwitter sans le compiler. Encore une fois, je tire parti de l'intégration sans faille entre Java et Groovy. Vous pouvez utiliser n'importe quelle solution Java dans une application Groovy sans changement.

Créez un fichier nommé StripeRenderer.groovy et mettez-y le code du Listing 16:

Listing 16. Création d'un CellRenderer pour les bandes

import java.awt.*;
import javax.swing.*;

class StripeRenderer extends DefaultListCellRenderer {
    public Component getListCellRendererComponent(JList list, Object value,
            int index, boolean isSelected, boolean cellHasFocus) {
        JLabel label = (JLabel) super.getListCellRendererComponent(list, value,
                index, isSelected, cellHasFocus);
       
        if(index%2 == 0) {
            label.setBackground(new Color(230,230,255));
        }
        
        label.setVerticalAlignment(SwingConstants.TOP);
        return label;
    }
}

Maintenant que la classe StripeRenderer est en place, la dernière chose à faire est de permettre à la JList d'en bénéficier. Modifiez resultsPanel comme le montre le Listing 17:

Listing 17. Ajout du CellRenderer personnalisé à la pour les bandes

def resultsPanel = {
  swingBuilder.scrollPane(constraints: BorderLayout.CENTER){
    //resultsList = list()
    resultsList = 
       list(fixedCellWidth: 380, fixedCellHeight: 75, cellRenderer:new StripeRenderer())
  }
}

Entrez groovy Gwitter en ligne de commande une dernière fois. Chercher thirstyhead devrait vous renvoyer des résultats similaires à ceux de la Figure 5:

Figure 5. Résultats avec les zébrures

Résultats avec les zébrures

Je pourrais passer beaucoup plus de temps à polir l'interface de Gwitter, mais j'espère que vous êtes impressionnés par ce que 50 lignes de code Swing (sans compter les classes de support, bien sûr) ont accompli.

Conclusion

Comme vous l'avez appris dans cet article, Groovy ne réduit pas la complexité intrinsèque de Swing, mais il diminue spectaculairement sa complexité syntaxique. Cela veut libère pour combattre d'autres batailles, plus importantes.

Si j'ai piqué votre curiosité à propos de Groovy et Swing avec cet article, vous devriez vous intéresser au projet Griffon (voir les Ressources). Il offre le même outillage et utilise le même principe de convention avant configuration que Grails, mais il est bâti sur les fondements que sont SwingBuilder et Groovy plutôt que sur Spring MVC et Hibernate. Le projet en est encore à ses débuts - au moment de l'écriture de cet article, la version la plus récente est la 0.2 (ndT: au 01/05/2010 c'est la 0.3) - mais il a été assez robuste pour gagner la Scripting Bowl pour Groovy à JavaOne 2009. Et un des projets d'exemple qu'il fournit est Greet: un client Twitter complet, implémenté en Groovy.

La prochaine fois, vous ajouterez de nouvelles fonctions à Gwitter. Vous apprendrez comment gérer les bases de l'authentification HTTP, tirerez profit d'un cousin de XmlSlurper appelé ConfigSlurper, utiliserez Gwitter pour requêter et analyser les fils Twitter de vos amis et ajouterez des onglets. Dans la troisième partie, vous ajouterez la touche finale à Gwitter en lui ajoutant la capacité d'émettre des POST HTTP de manière à envoyer vos propres tweets. En attendant, j'espère que vous trouverez beaucoup d'utilisations pratiques de Groovy.