prefuse et TreeML
prefuse est une librairie Java de visualisation de graphes interactifs reposant sur Java2D pour le rendu ; très souple dans sa conception, celle-ci permet de réaliser des applications ou des applets Java présentant des graphes animés et interactifs avec vos données, et pour lesquels vous pouvez en définir précisément l’aspect et le comportement.
L’un des exemples en démonstration dans la galerie de graphes prefuse, treeview, présente des données en arborescence décrites dans le format TreeML ; il s’agit d’un format XML non officiel facile à appréhender : la structure de données est décrite au moyen de « branches » et de « feuilles », une branche pouvant contenir d’autres branches ou des feuilles, mais une feuille ne pouvant rien incorporer.
Par ailleurs, branches ou feuilles possèdent des propriétés appelées attributs.
Dans la structure XML d’élément document tree, « branches » et de « feuilles » correspondent respectivement aux éléments XML branch et leaf, tandis que les attributs sont définis par l’élément attribute ; l’ensemble des attributs utilisables doivent au préalable être « déclarés » en début de document avec l’élément XML attributeDecl, comme dans l’exemple ci-dessous :
<declarations>
<attributeDecl name='name' type='String' />
</declarations>
<branch>
<attribute name='name' value='Groovy' />
<branch>
<attribute name='name' value='Language' />
<branch>
<attribute name='name' value='Literals' />
<leaf>
<attribute name='name' value='Strings / RegEx' />
</leaf>
…
</branch>
…
</branch>
</branch>
</tree>
Dans cet exemple, un seul attribut a été déclaré.
Générer un document TreeML en langage Groovy
Un document TreeML peut être produit au moyen du builder MarkupBuilder du langage Groovy, qui permet de générer facilement tout type de document XML.
Les builders du langage Groovy sont une notion puissante permettant de produire des éléments d’informations en arborescence, avec une syntaxe concise et claire, ce qui donne un code plus lisible.
Pour n’en citer que quelques uns, nous avons le builder SwingBuilder (construction d’interfaces graphiques basées sur Swing), le builder AntBuilder (exécution de scripts Ant), le builder MarkupBuilder (cité plus haut), ou encore le builder GraphicsBuilder (génération d’images Java 2D).
Un builder peut également servir de support à l’implémentation d’un Domain Specific Language, ou DSL, qui est une sorte de mini langage dédié à un domaine très précis ; ce langage peut être utilisé par des spécialistes de ce domaine, sans demander de bagage technique dans un langage de programmation général (voir Writing Domain-Specific Languages).
Dans le cadre de notre article, voyons comment créer un nouveau builder, pour faciliter la création de documents TreeML.
L’approche que nous avons retenue pour notre classe builder, TreeMLBuilder, est celle présentée dans le billet Creating a Simple Builder with Groovy's methodMissing(),
d’organic thoughts.
Cette approche est basée sur la méta programmation de Groovy, par la définition de la méthode methodMissing.
Si cette méthode est implémentée pour une classe donnée, tout appel de méthode inconnue via un objet de cette classe, conduira à un appel à methodMissing avec comme arguments, le nom de la méthode et les arguments transmis à cette méthode.
En voici un exemple, que vous pouvez exécuter dans la console Groovy :
def methodMissing(String name, args) {
println "methodMissing appelée pour : $name, args : $args, length : $args.length"
}
}
def builder = new TreeMLBuilder()
builder.Salut('tout le monde !', 123, { new Date() })
L’appel à la méthode hypothétique Salut entraîne un appel à la méthode methodMissing, et par conséquent l’affichage ici des informations concernant l’appel : le nom de la méthode invoquée, la liste des arguments, et le nombre des arguments transmis.
Notez que dans notre exemple, le troisième argument est une Closure.
Dans le langage Groovy, si le dernier argument d’un appel de méthode est une Closure, nous pouvons également écrire :
Voici le code complet de la classe TreeMLBuilder (que vous pouvez récupérer comme pièce jointe à cet article), ainsi qu’un exemple de son utilisation :
def sb = new StringBuilder()
def declarations = [:]
def methodMissing(String name, args) {
println "methodMissing $name, args : $args, length : $args.length"
switch (name) {
case 'tree':
assert args.length == 1 && args[0] instanceof Closure
args[0].delegate = this
sb.append '<tree>'
args[0]()
sb.append '</tree>'
break
case 'declarations':
assert args.length == 1 && args[0] instanceof Closure
args[0].delegate = this
sb.append '<declarations>'
args[0]()
sb.append '</declarations>'
break
case 'attributeDecl':
assert args.length == 1 && args[0] instanceof HashMap && args[0].size() == 2
assert args.name && args.type
declarations."${args.name[0]}" = args.type[0]
sb.append "<attributeDecl name='${args.name[0]}' type='${args.type[0]}'/>"
break
case 'branch':
assert args.length == 1 && args[0] instanceof Closure
args[0].delegate = this
sb.append '<branch>'
args[0]()
sb.append '</branch>'
break
case 'leaf':
assert args.length == 1 && args[0] instanceof Closure
args[0].delegate = this
sb.append '<leaf>'
args[0]()
sb.append '</leaf>'
break
default:
// Dans le contexte d'une branche ou d'un noeud
println "default : $name"
if (declarations."$name") {
assert args.length == 1
sb.append "<attribute name='$name' value='${args[0]}'/>"
}
}
}
def build() {
sb.toString()
}
}
def builder = new TreeMLBuilder()
builder.tree {
declarations {
attributeDecl(name: 'nom', type: 'String')
}
branch {
nom 'Groovy'
branch {
nom 'Language'
branch {
nom 'Literals'
leaf {
nom 'Strings / RegEx'
}
leaf {
nom 'Lists / Maps / Ranges'
}
leaf {
nom 'Closures'
}
}
branch {
nom 'Every is an object'
leaf {
nom 'Object operators'
}
leaf {
nom 'Multimethods'
}
leaf {
nom 'GroovyBeans'
}
}
branch {
nom 'Meta Object Protocol'
leaf {
nom 'Method interception'
}
leaf {
nom 'Category use (Mixin)'
}
leaf {
nom 'Dynamic methods and properties'
}
}
branch {
nom 'Control Flow'
leaf {
nom 'Groovy Truth'
}
leaf {
nom 'Switch'
}
leaf {
nom 'Advanced operators'
}
leaf {
nom 'GPath'
}
leaf {
nom 'Iterations'
}
}
branch {
nom 'Execution'
leaf {
nom 'Scriptablility'
}
leaf {
nom 'Hot class reloading'
}
}
}
branch {
nom 'Library'
leaf {
nom 'SQL'
}
leaf {
nom 'XML'
}
leaf {
nom 'Swing'
}
leaf {
nom 'Ant'
}
leaf {
nom 'Templates'
}
leaf {
nom 'Groovlets'
}
leaf {
nom 'Stubs and Mocks'
}
}
branch {
nom 'GDK'
leaf {
nom 'Object Inspection'
}
branch {
nom 'Groovy-aware methods'
leaf {
nom 'Strings'
}
leaf {
nom 'Lists'
}
leaf {
nom 'Maps'
}
}
leaf {
nom 'Files and I/O'
}
leaf {
nom 'Threads and Processes'
}
}
}
}
builder.build()
Attention il manque du code de validation vérifiant, comme par exemple, de ne pas pouvoir inclure des éléments branch ou leaf, dans un élément leaf.
Vous pouvez constater que interceptons les appels aux méthodes tree, declarations, attributeDecl, branch, leaf, ainsi que les appels aux méthodes dont les noms sont des noms d’attributs déclarés (dans notre exemple c’est le cas des appels nom 'valeur') ; c’est ainsi que l’implémentation de la méthode methodMissing de la classe TreeMLBuilder peut construire dans un objet interne StringBuilder, les fragments de document XML correspondants.
Le seul point qui peut paraître obscur est l’affectation :
dans laquelle args[0] est une référence à une Closure, appelée ensuite par args[0]().
Cette écriture fait que tout appel effectué dans la Closure est délégué à l’objet builder this.
Par exemple :
nom 'Groovy'
}
entraîne l’appel à methodMissing pour les méthodes « branch » et « nom » : ici, branch est invoqué avec une Closure comme argument, et nom avec une chaîne de caractères.
Et sans l’affectation args[0].delegate = this, la méthode « nom » ne pourrait être considérée comme appartenant à TreeMLBuilder.
TreeMLGraphBuilder en action
Si vous avez lu l’excellent ouvrage Groovy In Action (GINA), vous avez peut être reconnu dans le format TreeML généré de notre exemple, le graphe présentant le langage Groovy en début de livre et prenant la forme d’une MindMap, comme celle-ci :
Vous pouvez voir ce graphe en action dans cette applet Java 6, avec la librairie prefuse, utilisant un graphe interactif de type treeview ; ce dernier charge le document TreeML GroovyTreeML.xml (identique à celui placé un pièce jointe à cet article) grâce à un paramètre – une URL - passé à l’applet.
Dans une application Grails, rien n’empêcherait de générer dynamiquement un document TreeML, au moment où l’applet prefuse récupère le document au travers d’une URL.
Que le document soit construit avec le builder MarkupBuilder ou notre builder TreeMLBuilder, on pourra y ajouter des données provenant d’objets métiers.
Nous avons là un moyen extraordinaire de présenter des données dans une application web !
| Fichier attaché | Taille |
|---|---|
| GroovyTreeML.xml.txt | 3.03 Ko |
| TreeMLBuilder.groovy.txt | 4.94 Ko |