Fedora-Fr - Communauté francophone Fedora - Linux

Planet de Fedora-Fr : la communauté francophone autour de la distribution Linux Fedora

A propos

Cette page est actualisée toutes les heures.

Cette page est une sélection de blogs autour de Fedora. Fedora-Fr.org décline toute responsabilité au sujet des propos tenus par les auteurs des blogs de ce planet. Leurs propos sont leur entière responsabilité.

Le contenu de ce planet appartient à leurs auteurs respectifs. Merci de consulter leur blogs pour obtenir les licences respectives.

Mot-clefs : plugins

QtScript : exemple d'utilisation

Fabien Nicoleau

J'ai essayé dans mon billet précédent de démystifier QtScript et de montrer au travers de différents exemples quelques unes des possibilités offertes par ce module de la bibliothèque Qt. Après ces premiers pas, je vous propose ici d'utiliser QtScript dans un exemple concret. Il s'agira de créer une calculatrice pour laquelle chaque type d'opération possible sera un plugin développé en JavaScript. Il sera ainsi possible "d'étendre" cette calculatrice comme bon vous semble en codant de nouveaux types d'opérations. Cependant je m'en tiendrai dans cet exemple aux simples addition, soustraction, multiplication et division. J'ai appelé ce petit projet QtExCalc pour Qt Extendable Calculator.

Le projet

Pour commencer, assurez vous que le package qt-devel est installé sur votre poste. Si ce n'est pas le cas, sous Fedora, tapez :

# yum install qt-devel

Il faudra ensuite télécharger les sources du projet, dont vous trouverez l'archive jointe à ce billet. Une fois cette dernière décompressée, déplacez vous dans le dossier qtexcalc puis tapez :

$ qmake-qt4
$ make

Le projet est alors compilé. Vous pouvez l'exécuter :

$ ./qtexcalc

L'application se lance :

capture1.jpg

Elle est très simple. Choisissez un type d'opération proposé dans la liste, saisissez deux nombre, puis cliquez sur le bouton pour afficher le résultat. Tout l'intérêt de ce programme est donc qu'il est modulable, grâce à des scripts JS. A son lancement, elle charge des plugins placés dans le sous-répertoire plugins. Chaque plugin fourni trois informations :

  • Le libellé de son opération, qui sera affiché dans la liste ("Addition" par exemple)
  • Le signe de son opération, qui sera affiché sur le bouton ("+" par exemple)
  • Une procédure de traitement, prenant deux chiffres en paramètre, et renvoyant un résultat. Le traitement effectué est évidemment propre à chaque plugin

Le code

Les sources sont composées d'une classe Dialog comprennant la fenêtre principale, ainsi que les différents traitements associés aux actions de l'utilisateur, une classe Plugin, dont chaque instance "représentera" un des plugins, et qui sera chargée de dialoguer avec. Enfin, un fichier main, simplement chargé de créer la fenêtre principale et de l'afficher. Passons au code.

main.cpp

C'est le seul fichier dont je copierai intégralement le contenu ici, tant il est petit :

#include <QtGui/QApplication>
#include "dialog.h"

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    Dialog w;
    w.show();
    return a.exec();
}

Comme expliqué plus tôt, on se contente ici d'afficher la fenêtre du projet.

Classe Plugin

Cette classe a en charge de dialoguer avec le plugin. Elle contient trois méthodes dont voici l'implémentation :

Plugin::Plugin(QScriptEngine* eng)
{
    this->engine = eng;
    this->sign =this->engine->globalObject().property("getOperationSign").call(QScriptValue()).toString() ;
}

QString Plugin::getSign() {
    return this->sign;
}

double Plugin::compute(double num1, double num2) {
    return this->engine->globalObject().property("compute").call(QScriptValue(),QScriptValueList() << num1<<num2).toNumber();
}

Le constructeur prend en paramètre un pointeur sur un moteur de script, qui permettra de dialoguer avec le plugin, et récupère ensuite le signe de l'opération du plugin en appelant sa fonction getOperationSign()

La méthode getSign() renvoie le signe de l'opération du plugin qui a été stocké dans le constructeur.

Enfin, la méthode compute() se charge d'effectuer l'opération. Elle attend deux paramètres qui seront passés à la fonction compute() du plugin. Elle renvoie ensuite le résultat de l'opération.

Classe Dialog

Voici la principale classe du projet. Je laisse de coté ici tout ce qui concerne la gui, pour ne se concentrer que sur QtScript. Commençons par la méthode loadPlugins() qui est appelée lors de la construction de l'objet et qui permet de "charger" les différents plugins :

void Dialog::loadPlugins() {
    QDir dirPlugins("plugins");
    QString fName;
    QScriptEngine * engine;
    QComboBox * cbPlugins = this->ui->cbChoosePlugin;
    cbPlugins->addItem(QString::fromUtf8 ("Choisissez une opération ..."),"");
    foreach(fName,dirPlugins.entryList(QStringList() << "*.qs")) {
        engine = new QScriptEngine();
        QFile file("plugins/"+fName);
        file.open(QIODevice::ReadOnly);
        engine->evaluate(file.readAll());
        file.close();
        cbPlugins->addItem(engine->globalObject().property("getOperationName").call(QScriptValue()).toString(),fName);
        this->plugins[fName] = new Plugin(engine) ;
    }
}

Tous les fichiers dont l'extension est qs (pour QtScript, mais on peut bien sûr utiliser ce qu'on veut) sont listés grâce à la méthode entryList(). Pour chacun des fichiers, on créé un moteur de script puis on évalue le contenu. Ensuite, on ajoute à la liste (addItem()) le nom de l'opération, en appelant la fonction getOperationName du plugin, et on donne le nom du fichier (fName) comme data. Enfin, on construit un nouvel objet Plugin auquel on passe en paramètre le moteur et que l'on stocke dans une QMap, avec le nom de fichier (fName toujours) comme clé. Ainsi, lorsqu'une opération sera choisie dans la liste, on récupèrera le data (correspondant au nom de fichier) grâce auquel on pourra accéder à l'objet correspondant de la QMap.

Passons maintenant au slot choosedPlugin appelé à chaque fois que l'on sélectionne un item dans la liste des opérations :

void Dialog::choosedPlugin(int index) {
    if(index>0) {
        this->ui->bProcess->setText(this->plugins[this->ui->cbChoosePlugin->itemData(index).toString()]->getSign());
        this->ui->bProcess->setEnabled(true);
    }
    else {
        this->ui->bProcess->setText("");
        this->ui->bProcess->setEnabled(false);
    }
}

On vérifie tout d'abord que l'on a pas sélectionné le premier élément, qui est celui demandant de choisir une opération. Si on a bien sélectionné une opération, on récupère la donnée de l'élément (itemData()) sélectionné, qui nous permet d'accéder à l'objet Plugin correspondant, depuis lequel on appelle la méthode getSign(). Le signe de l'opération est appliqué au bouton, qui est ensuite activé.

Passons au dernier slot, compute, appelé lorsque l'utilisateur clique sur le bouton :

void Dialog::compute() {
    if(!this->ui->leNumber1->text().isEmpty() && ! this->ui->leNumber2->text().isEmpty() ) {
        Plugin * selectedPlugin = this->plugins[this->ui->cbChoosePlugin->itemData(this->ui->cbChoosePlugin->currentIndex()).toString()];
        this->ui->lResult->setText(QString::number(selectedPlugin->compute(this->ui->leNumber1->text().toDouble(),this->ui->leNumber2->text().toDouble())));
    }
}

Si les deux champs de saisie ont été remplis, on récupère l'objet Plugin de l'opération sélectionnée. On appelle la méthode compute() du Plugin, en lui passant en paramètre la valeur des champs de saisie, qui nous retourne le résultat de l'opération, résultat que l'on affiche dans le libellé correspondant.

Voici pour terminer, le code du plugin "addition", je ne donne pas le code des 3 autres, qui se devine facilement, et qui est dispo dans les sources :

function getOperationName() {
    return "Addition";
}

function getOperationSign() {
    return "+";
}

function compute(num1,num2) {
    return num1+num2;
}

On y voit donc les trois fonctions nécessaires, donnant le libellé de l'opération, son signe, et enfin le résultat de l'opération.

Voilà pour l'explication de ce petit projet, qui j'espère vous permettra de voir de quelle façon on peut facilement avec Qt gérer des plugins écrits en JavaScript. Si vous avez des questions sur le code, n'hésitez pas à les poser dans les commentaires.


Bon dev'

Fabien (eponyme)

QtScript : exemple d'utilisation

Fabien Nicoleau

J'ai essayé dans mon billet précédent de démystifier QtScript et de montrer au travers de différents exemples quelques unes des possibilités offertes par ce module de la bibliothèque Qt. Après ces premiers pas, je vous propose ici d'utiliser QtScript dans un exemple concret. Il s'agira de créer une calculatrice pour laquelle chaque type d'opération possible sera un plugin développé en JavaScript. Il sera ainsi possible "d'étendre" cette calculatrice comme bon vous semble en codant de nouveaux types d'opérations. Cependant je m'en tiendrai dans cet exemple aux simples addition, soustraction, multiplication et division. J'ai appelé ce petit projet QtExCalc pour Qt Extendable Calculator.

Le projet

Pour commencer, assurez vous que le package qt-devel est installé sur votre poste. Si ce n'est pas le cas, sous Fedora, tapez :

# yum install qt-devel

Il faudra ensuite télécharger les sources du projet, dont vous trouverez l'archive jointe à ce billet. Une fois cette dernière décompressée, déplacez vous dans le dossier qtexcalc puis tapez :

$ qmake-qt4
$ make

Le projet est alors compilé. Vous pouvez l'exécuter :

$ ./qtexcalc

L'application se lance :

capture1.jpg

Elle est très simple. Choisissez un type d'opération proposé dans la liste, saisissez deux nombre, puis cliquez sur le bouton pour afficher le résultat. Tout l'intérêt de ce programme est donc qu'il est modulable, grâce à des scripts JS. A son lancement, elle charge des plugins placés dans le sous-répertoire plugins. Chaque plugin fourni trois informations :

  • Le libellé de son opération, qui sera affiché dans la liste ("Addition" par exemple)
  • Le signe de son opération, qui sera affiché sur le bouton ("+" par exemple)
  • Une procédure de traitement, prenant deux chiffres en paramètre, et renvoyant un résultat. Le traitement effectué est évidemment propre à chaque plugin

Le code

Les sources sont composées d'une classe Dialog comprennant la fenêtre principale, ainsi que les différents traitements associés aux actions de l'utilisateur, une classe Plugin, dont chaque instance "représentera" un des plugins, et qui sera chargée de dialoguer avec. Enfin, un fichier main, simplement chargé de créer la fenêtre principale et de l'afficher. Passons au code.

main.cpp

C'est le seul fichier dont je copierai intégralement le contenu ici, tant il est petit :

#include <QtGui/QApplication>
#include "dialog.h"

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    Dialog w;
    w.show();
    return a.exec();
}

Comme expliqué plus tôt, on se contente ici d'afficher la fenêtre du projet.

Classe Plugin

Cette classe a en charge de dialoguer avec le plugin. Elle contient trois méthodes dont voici l'implémentation :

Plugin::Plugin(QScriptEngine* eng)
{
    this->engine = eng;
    this->sign =this->engine->globalObject().property("getOperationSign").call(QScriptValue()).toString() ;
}

QString Plugin::getSign() {
    return this->sign;
}

double Plugin::compute(double num1, double num2) {
    return this->engine->globalObject().property("compute").call(QScriptValue(),QScriptValueList() << num1<<num2).toNumber();
}

Le constructeur prend en paramètre un pointeur sur un moteur de script, qui permettra de dialoguer avec le plugin, et récupère ensuite le signe de l'opération du plugin en appelant sa fonction getOperationSign()

La méthode getSign() renvoie le signe de l'opération du plugin qui a été stocké dans le constructeur.

Enfin, la méthode compute() se charge d'effectuer l'opération. Elle attend deux paramètres qui seront passés à la fonction compute() du plugin. Elle renvoie ensuite le résultat de l'opération.

Classe Dialog

Voici la principale classe du projet. Je laisse de coté ici tout ce qui concerne la gui, pour ne se concentrer que sur QtScript. Commençons par la méthode loadPlugins() qui est appelée lors de la construction de l'objet et qui permet de "charger" les différents plugins :

void Dialog::loadPlugins() {
    QDir dirPlugins("plugins");
    QString fName;
    QScriptEngine * engine;
    QComboBox * cbPlugins = this->ui->cbChoosePlugin;
    cbPlugins->addItem(QString::fromUtf8 ("Choisissez une opération ..."),"");
    foreach(fName,dirPlugins.entryList(QStringList() << "*.qs")) {
        engine = new QScriptEngine();
        QFile file("plugins/"+fName);
        file.open(QIODevice::ReadOnly);
        engine->evaluate(file.readAll());
        file.close();
        cbPlugins->addItem(engine->globalObject().property("getOperationName").call(QScriptValue()).toString(),fName);
        this->plugins[fName] = new Plugin(engine) ;
    }
}

Tous les fichiers dont l'extension est qs (pour QtScript, mais on peut bien sûr utiliser ce qu'on veut) sont listés grâce à la méthode entryList(). Pour chacun des fichiers, on créé un moteur de script puis on évalue le contenu. Ensuite, on ajoute à la liste (addItem()) le nom de l'opération, en appelant la fonction getOperationName du plugin, et on donne le nom du fichier (fName) comme data. Enfin, on construit un nouvel objet Plugin auquel on passe en paramètre le moteur et que l'on stocke dans une QMap, avec le nom de fichier (fName toujours) comme clé. Ainsi, lorsqu'une opération sera choisie dans la liste, on récupèrera le data (correspondant au nom de fichier) grâce auquel on pourra accéder à l'objet correspondant de la QMap.

Passons maintenant au slot choosedPlugin appelé à chaque fois que l'on sélectionne un item dans la liste des opérations :

void Dialog::choosedPlugin(int index) {
    if(index>0) {
        this->ui->bProcess->setText(this->plugins[this->ui->cbChoosePlugin->itemData(index).toString()]->getSign());
        this->ui->bProcess->setEnabled(true);
    }
    else {
        this->ui->bProcess->setText("");
        this->ui->bProcess->setEnabled(false);
    }
}

On vérifie tout d'abord que l'on a pas sélectionné le premier élément, qui est celui demandant de choisir une opération. Si on a bien sélectionné une opération, on récupère la donnée de l'élément (itemData()) sélectionné, qui nous permet d'accéder à l'objet Plugin correspondant, depuis lequel on appelle la méthode getSign(). Le signe de l'opération est appliqué au bouton, qui est ensuite activé.

Passons au dernier slot, compute, appelé lorsque l'utilisateur clique sur le bouton :

void Dialog::compute() {
    if(!this->ui->leNumber1->text().isEmpty() && ! this->ui->leNumber2->text().isEmpty() ) {
        Plugin * selectedPlugin = this->plugins[this->ui->cbChoosePlugin->itemData(this->ui->cbChoosePlugin->currentIndex()).toString()];
        this->ui->lResult->setText(QString::number(selectedPlugin->compute(this->ui->leNumber1->text().toDouble(),this->ui->leNumber2->text().toDouble())));
    }
}

Si les deux champs de saisie ont été remplis, on récupère l'objet Plugin de l'opération sélectionnée. On appelle la méthode compute() du Plugin, en lui passant en paramètre la valeur des champs de saisie, qui nous retourne le résultat de l'opération, résultat que l'on affiche dans le libellé correspondant.

Voici pour terminer, le code du plugin "addition", je ne donne pas le code des 3 autres, qui se devine facilement, et qui est dispo dans les sources :

function getOperationName() {
    return "Addition";
}

function getOperationSign() {
    return "+";
}

function compute(num1,num2) {
    return num1+num2;
}

On y voit donc les trois fonctions nécessaires, donnant le libellé de l'opération, son signe, et enfin le résultat de l'opération.

Voilà pour l'explication de ce petit projet, qui j'espère vous permettra de voir de quelle façon on peut facilement avec Qt gérer des plugins écrits en JavaScript. Si vous avez des questions sur le code, n'hésitez pas à les poser dans les commentaires.


Bon dev'

Fabien (eponyme)

Premiers pas avec QtScript

Fabien Nicoleau

Dans un précédent billet, j'avais parlé de la possibilité d'intégrer très facilement dans vos applications Java des scripts écrits en JavaScript, et de la facilité avec laquelle vos applications peuvent interagir avec les scripts. J'avais ensuite proposé un exemple d'utilisation avec un robot IRC fonctionnant avec des plugins écrits en JavaScript.

La bibliothèque Qt permet elle aussi d'intégrer des scripts JavaScript dans vos applications. Je propose ici de découvrir comment le faire, à travers quelques exemples simples.

Avant de commencer

Avant d'entrer dans le vif du sujet, préparons d'abord le terrain. Je proposerai pour chaque exemple le code dans ce billet, mais je vous conseille, si vous voulez tester, de télécharger l'archive jointe à ce billet contenant les sources des exemples. Une fois décompressée, déplacez vous dans le dossier qtscripting, puis ouvrez une console, et tapez :

qmake-qt4
make

Tous les exemples sont alors compilés. Bien sûr, pour que la compilation fonctionne, ils vous faut installer le paquet qt-devel. Sous Fedora, tapez :

# yum install qt-devel

Une fois les exemples compilés, vous pouvez vous déplacer dans les dossiers, et lancer les exécutables.

Notez que pour ajouter le support des scripts au projet, il a simplement suffit d'ajouter script à la directive QT (voir le QT += script dans le fichier common.pri), et d'include QtScript dans les sources.

Exemple 1 : 1 + 1

Pour le premier exemple, faisons simple ! Il s'agit ici simplement de voir comment exécuter un code JavaScript depuis du code C++, et de récupérer le résultat.

On commence direct avec le code :

#include <QtScript>
#include <QApplication>
int main(int argc, char **argv)
{
    QApplication app(argc, argv);
    QScriptEngine engine;
    QScriptValue resultat = engine.evaluate("1+1");
    qDebug() << resultat.toString() ;
    return 0;
}

Rien de complexe. On créé d'abord un objet de type QScriptEngine, qui sera notre moteur de script, indispensable. On demande ensuite à notre moteur d'évaluer le code "1+1". Le résultat de l'évaluation est stocké dans un objet de type QScriptValue. Enfin on affiche le résultat. En parcourant la doc sur QScriptValue, vous pourrez vous rendre compte qu'il est possible d'effectuer de nombreuses vérifications afin de s'assurer que le résultat est exploitable, notamment en contrôlant son type.

Voilà pour un début, il n'y a rien d'exceptionnel bien sûr, mais les base sont posées : notre programme a affiché le résultat d'un code externe (en l'occurrence du JavaScript, même si ce n'est qu'une addition, et que le code est "en dur" dans le programme). Pour les plus sceptiques, j'affiche la sortie du programme :

[eponyme@localhost exemple1]$ ./exemple1
"2"
[eponyme@localhost exemple1]$

Surpris ? :D

Passons à la suite.

Exemple 2 : appel d'une fonction

Nous allons dans cette exemple appeler une fonction et lui passer un paramètre, puis afficher ce qu'elle nous renvoie. Le code :

#include <QtScript>
#include <QApplication>
int main(int argc, char **argv)
{
    QApplication app(argc, argv);
    QScriptEngine engine;
    engine.evaluate("function disMoiBonjour(prenom) { return \"Bonjour \"+prenom; }");    
    QScriptValue disBonjour = engine.globalObject().property("disMoiBonjour");
    qDebug() << disBonjour.call(QScriptValue(),QScriptValueList() << "Fabien").toString() ;
    return 0;
}

Comme dans le premier exemple, on créé un moteur, mais cette fois ci au lieu de lui faire évaluer une addition, nous demandons d'évaluer la définition d'une fonction, appelée disMoiBonjour. Cette fonction retourne une phrase "disant bonjour" à la personne dont le prénom est passé en paramètre. La ligne suivante nous permet de récupérer parmi les objets du script (globalObject())  notre fonction disMoiBonjour (property()), conservée dans la variable disBonjour. Il ne nous reste plus qu'à l'appeler grâce à la méthode call(). Notez le second paramètre, qui est une liste de valeurs qui sont passées en arguments à la fonction appelée. Ici, on envoie juste la chaîne "Fabien".

Voici la sortie du programme :

[eponyme@localhost exemple2]$ ./exemple2
"Bonjour Fabien"
[eponyme@localhost exemple2]$

Cet article ayant juste pour but de découvrir des choses simples et basiques possibles avec QtScript, je ne rentre pas plus dans les le détail concernant les différentes méthodes utilisées ici, mais la documentation vous fera découvrir que l'on peut aller beaucoup plus loin.

Au passage, on voit déjà que du code JavaScript dans une chaine de caractères, ça n'est pas très pratique. Vivement qu'on puisse le mettre dans un fichier !

Exemple 3 : code JS dans un fichier et objet global

Pour le moment, nos scripts étaient "en dur" dans le code C++, ce qui n'a que très peu (aucun ?) intérêt. Voyons ici comment externaliser ce code et le placer dans un fichier. La méthode evaluate() prenant en paramètre une chaine de caractères, il sera très simple (surtout avec Qt) de récupérer le contenu d'un fichier texte pour le faire évaluer. Je profite aussi de cet exemple pour montrer comment exporter depuis le code C++ un objet global dans le script. Comme d'habitude, on attaque directement avec le code :

#include <QtScript>
#include <QApplication>
int main(int argc, char **argv)
{
    QApplication app(argc, argv);
    QScriptEngine engine;
    QFile file("script.qs");
    file.open(QIODevice::ReadOnly);
    engine.globalObject().setProperty("name","Fabien");
    engine.evaluate(file.readAll());   
    file.close();
    return 0;
}

Comme on pouvait s'y attendre, la chose est très simple à réaliser grâce à l'objet QFile. On ouvre en lecture seule un fichier nommé script.qs, et on passe son contenu à la méthode evaluate() via la méthode readAll(). Le contenu du fichier est ainsi entièrement évalué. L'autre nouveauté ici est que l'on "envoie" dans le script l'objet "name", dont la valeur est "Fabien", avec la méthode setProperty(). Ainsi, à n'importe quel endroit du script, il sera possible d'accéder à cet objet.

Voyons d'ailleurs le contenu du script (fichier script.qs) :

print('Bonjour '+name);

Ce petit script se contentera donc d'afficher dans la console un bonjour à la personne dont le prénom est stocké dans l'objet name (et assigné depuis le code C++).

Voyons ce que ca donne à l'exécution :

[eponyme@localhost exemple3]$ ./exemple3
Bonjour Fabien
[eponyme@localhost exemple3]$

La encore, pas de grande surprise ! Mais désormais, vous pouvez stocker votre code JS dans un fichier, ce qui ouvre un grand nombre de possibilités, notamment celui de stocker différents scripts afin de charger différentes fonctions, pour un système de plugins.

Exemple 4 : envoi d'un QObject dans le script

L'exemple 3 nous a montré qu'il est possible d'envoyer un objet avec une valeur dans le script, et ce depuis le code C++. Cependant cet objet ne peut prendre qu'une valeur simple. Nous allons voir ici comment envoyer dans le script un QObject (et donc un de ses objets hérités, ce qui en fait beaucoup).

Pour illustrer cet exemple, je vais utiliser un QLabel, habituellement utilisé pour afficher un libellé dans une interface graphique. Voici le code :

#include <QtScript>
#include <QApplication>
#include <QLabel>
int main(int argc, char **argv)
{
    QApplication app(argc, argv);
    QLabel lbl;
    QScriptEngine engine;
    QFile file("script.qs");
    file.open(QIODevice::ReadOnly);
    lbl.setText("UN");
    QScriptValue object = engine.newQObject(&lbl);
    engine.globalObject().setProperty("label",object);   
    engine.evaluate(file.readAll());
    file.close();
    qDebug() <<"Dans le code : " <<  lbl.text();
    return 0;
}

Le QLabel ne sera pas utilisé ici dans une interface, mais on utilise ses méthodes pour le manipuler. Après l'habituelle création du moteur, on créé donc un objet QLabel, pour lequel on modifie le libellé en le passant à "UN", grâce au slot setText(). Ensuite, la méthode newQObject() du moteur est appelée, prenant en paramètre un pointeur sur notre objet de type QLabel. C'est la valeur renvoyée par newQObject qui est utilisée pour le setProperty() de la ligne suivante, déjà utilisé dans l'exemple 3. Ainsi, dans notre script, on pourra utiliser notre objet QLabel n'importe quand grâce à la variable label, dont le nom est passé en premier argument à setProperty(). Comme dans les autres exemples, on évalue le script, puis on ferme le fichier. Enfin, on affiche la valeur de notre QLabel grâce à sa méthode text(). En toute logique, elle devrait renvoyer "UN", à moins que la valeur ait été changée depuis le script ... Voyons justement le code de ce dernier (fichier script.qs) :

print("Dans le script : "+label.text);
label.setText("DEUX");

Deux lignes, très simples. La première affiche la valeur du QLabel accessible grâce à la variable label. La valeur du QLabel est obtenue grâce à la propriété text de ce dernier. Sur la seconde ligne, on change la valeur du QLabel à "DEUX", grâce à setText().

Voyons ce que donne l'exécution :

[eponyme@localhost exemple4]$ ./exemple4
Dans le script : UN
Dans le code :  "DEUX"
[eponyme@localhost exemple4]$

On voit ici l'évolution de notre QObject ! La première valeur affichée ("UN") depuis le script (avec print()) est celle qui avait été fixée dans le code C++. Après avoir affiché la valeur, le script JS a changé la valeur du QLabel, et depuis le code, la valeur affichée est bien "DEUX".

Nous pouvons donc de façon très simple passer à notre script JS des QObject et lui permettre d'en modifier les propriétés.

Exemple 5 : gestion des erreurs

Lorsque l'on débute, ou lorsque l'on commence à faire des choses compliquées, il arrive souvent que l'on bloque car le code JS n'est pas évalué. Dans la plupart des cas, il s'agit juste d'une erreur de syntaxe. Mais il est parfois difficile de savoir pourquoi est ce que notre script n'est pas évalué, et donc qu'il est impossible d'interagir avec lui ensuite. On peut aussi être parfois sûr de son code, et ne pas penser qu'il y a une erreur. Heureusement il est possible, lorsqu'une erreur est rencontrée, d'afficher un message. Voici un code d'exemple :

#include <QtScript>
#include <QApplication>
int main(int argc, char **argv)
{
    QApplication app(argc, argv);
    QScriptEngine engine;
    QScriptValue result = engine.evaluate("print('Bonjour !'");
    if (result.isError()) {
        qDebug() << result.property("message").toString();
    }
    return 0;
}

Dans ce code, on demande au moteur d'évaluer du JavaScript devant afficher "Bonjour !", mais la parenthèse fermante est volontairement oubliée. On se sert de la valeur renvoyée par l'évaluation pour vérifier s'il y a eu une erreur avec isError(). Si c'est le cas, on peut afficher un message explicatif en récupérant la propriété message du résultat d'évaluation.

Voyons l'exécution du programme :

[eponyme@localhost exemple5]$ ./exemple5
"Parse error"
[eponyme@localhost exemple5]$

Notre code n'est pas évalué, et le moteur nous indique une Parse error. On sait au moins ou chercher. Cette vérification vous permettra de gagner un peu de temps si vous n'obtenez pas le résultat espéré.

Exemple 6 : signaux et slots

Les signaux et slots sont l'un des avantages du développement avec la bibliothèque Qt, et n'ont pas été oubliés avec QtScript. Dans ce dernier exemple, nous allons créer une fenêtre basique comprenant deux boutons. Le premier, appelé "Bonjour", devra afficher "Bonjour !" dans la console lorsque l'on clique dessus, et le second, "Au revoir", devra afficher ... "Au revoir !" (et oui !)  lorsque l'on clique dessus. La particularité est que ces affichages dans la console se feront depuis un script JS, et non pas directement depuis des slots dans le code C++. L'intérêt ? Ici, aucun. Il s'agira simplement de montrer que l'on peut connecter un signal à un slot dont l'implémentation est faite dans un script JS, ce qui permet de personnaliser facilement une application en changeant le comportement du slot dans les scripts. Pour l'exemple, nous verrons deux façons différentes de connecter un signal à un slot implémenté dans un script, la première sera faite depuis le code C++, pour le bouton "Bonjour", et la seconde directement dans le script JS, pour le bouton "Au revoir".

Passons au code :

#include <QtScript>
#include <QApplication>
#include <QtGui>
int main(int argc, char **argv)
{
    QApplication app(argc, argv);
    QWidget fenetre;
    QVBoxLayout vbox(&fenetre);
    QPushButton bBonjour("Bonjour");
    QPushButton bAuRevoir("Au revoir");
    vbox.addWidget(&bBonjour);
    vbox.addWidget(&bAuRevoir);

    QScriptEngine engine;
    QFile file("script.qs");
    file.open(QIODevice::ReadOnly);
    QScriptValue objBonjour = engine.newQObject(&bBonjour);
    QScriptValue objAuRevoir = engine.newQObject(&bAuRevoir);
    engine.globalObject().setProperty("objAuRevoir",objAuRevoir);
    engine.evaluate(file.readAll());
    file.close();
    QScriptValue slot = engine.globalObject().property("bonjourHandler");
    qScriptConnect(&bBonjour, SIGNAL(clicked()), objBonjour, slot);

    fenetre.show();
    return app.exec();
}

C'est un peu plus long que d'habitude, certes, mais le premier pavé ne concerne que l'interface graphique, ce qui n'est pas le sujet de ce billet. On y créé les deux boutons et on les place sur la fenêtre.

Ensuite, comme d'habitude, on créé un moteur, qui évaluera du code JS placé dans un fichier. Deux "objets QtScript" sont créés à partir des deux boutons, grâce à newQObject() : objBonjour et objAuRevoir. Ils nous serviront à réaliser les connexions avec les slots du script. L'objet objAuRevoir est exporté dans le script car nous en aurons besoin pour réaliser la connexion au slot

Une fois l'évaluation du script faite, on récupère la propriété bonjourHander, qui est une fonction dans le script JS, que l'on stocke dans la variable slot. Cette variable est utilisée ensuite pour réaliser la connexion entre le signal clicked du bouton "Bonjour" et le slot bonjourHandler, grâce à la fonction qScriptConnect().

Voyons maintenant le contenu du script (fichier script.qs) :

function bonjourHandler () {
    print('Bonjour !');
}
function auRevoirHandler () {
    print('Au revoir !');
}
objAuRevoir.clicked.connect(this.auRevoirHandler);

On y trouve les deux fonctions qui seront appelées lors des clics sur les boutons. bonjourHandler est déjà connectée au bouton "Bonjour" via l'appel à qScriptConnect() dans le code C++. La fonction auRevoirHandler est elle connectée au bouton "Au revoir" à la dernière ligne du script, qui utilise l'objet objAuRevoir exporté dans le programme. Ainsi nos deux fonctions sont connectées à leur bouton respectif, l'une depuis le code C++, l'autre depuis le script JS. A vous de voir ce que vous préférez !

Une petite image pour l'exécution du programme :

tuto_qtscript_exemple6.jpg


C'est sur cet exemple que ce termine cette initiation. Je n'ai pas la prétention avec cet article de donner une vue d'ensemble des possibilités offertes par QtScript. J'en suis d'ailleurs encore au stade de la découverte. Mais ces exemples pourrons vous permettre de vous lancer, et de démystifier un peu les choses ! Je proposerai dans un prochain article un exemple concret d'utilisation de QtScript pour un système de plugins.

Hormis les liens disséminés un peu partout dans l'article, les deux sources qui m'ont le plus aidé sont :

Bon dev' !


Fabien (eponyme)

Premiers pas avec QtScript

Fabien Nicoleau

Dans un précédent billet, j'avais parlé de la possibilité d'intégrer très facilement dans vos applications Java des scripts écrits en JavaScript, et de la facilité avec laquelle vos applications peuvent interagir avec les scripts. J'avais ensuite proposé un exemple d'utilisation avec un robot IRC fonctionnant avec des plugins écrits en JavaScript.

La bibliothèque Qt permet elle aussi d'intégrer des scripts JavaScript dans vos applications. Je propose ici de découvrir comment le faire, à travers quelques exemples simples.

Avant de commencer

Avant d'entrer dans le vif du sujet, préparons d'abord le terrain. Je proposerai pour chaque exemple le code dans ce billet, mais je vous conseille, si vous voulez tester, de télécharger l'archive jointe à ce billet contenant les sources des exemples. Une fois décompressée, déplacez vous dans le dossier qtscripting, puis ouvrez une console, et tapez :

qmake-qt4
make

Tous les exemples sont alors compilés. Bien sûr, pour que la compilation fonctionne, ils vous faut installer le paquet qt-devel. Sous Fedora, tapez :

# yum install qt-devel

Une fois les exemples compilés, vous pouvez vous déplacer dans les dossiers, et lancer les exécutables.

Notez que pour ajouter le support des scripts au projet, il a simplement suffit d'ajouter script à la directive QT (voir le QT += script dans le fichier common.pri), et d'include QtScript dans les sources.

Exemple 1 : 1 + 1

Pour le premier exemple, faisons simple ! Il s'agit ici simplement de voir comment exécuter un code JavaScript depuis du code C++, et de récupérer le résultat.

On commence direct avec le code :

#include <QtScript>
#include <QApplication>
int main(int argc, char **argv)
{
    QApplication app(argc, argv);
    QScriptEngine engine;
    QScriptValue resultat = engine.evaluate("1+1");
    qDebug() << resultat.toString() ;
    return 0;
}

Rien de complexe. On créé d'abord un objet de type QScriptEngine, qui sera notre moteur de script, indispensable. On demande ensuite à notre moteur d'évaluer le code "1+1". Le résultat de l'évaluation est stocké dans un objet de type QScriptValue. Enfin on affiche le résultat. En parcourant la doc sur QScriptValue, vous pourrez vous rendre compte qu'il est possible d'effectuer de nombreuses vérifications afin de s'assurer que le résultat est exploitable, notamment en contrôlant son type.

Voilà pour un début, il n'y a rien d'exceptionnel bien sûr, mais les base sont posées : notre programme a affiché le résultat d'un code externe (en l'occurrence du JavaScript, même si ce n'est qu'une addition, et que le code est "en dur" dans le programme). Pour les plus sceptiques, j'affiche la sortie du programme :

[eponyme@localhost exemple1]$ ./exemple1
"2"
[eponyme@localhost exemple1]$

Surpris ? :D

Passons à la suite.

Exemple 2 : appel d'une fonction

Nous allons dans cette exemple appeler une fonction et lui passer un paramètre, puis afficher ce qu'elle nous renvoie. Le code :

#include <QtScript>
#include <QApplication>
int main(int argc, char **argv)
{
    QApplication app(argc, argv);
    QScriptEngine engine;
    engine.evaluate("function disMoiBonjour(prenom) { return \"Bonjour \"+prenom; }");    
    QScriptValue disBonjour = engine.globalObject().property("disMoiBonjour");
    qDebug() << disBonjour.call(QScriptValue(),QScriptValueList() << "Fabien").toString() ;
    return 0;
}

Comme dans le premier exemple, on créé un moteur, mais cette fois ci au lieu de lui faire évaluer une addition, nous demandons d'évaluer la définition d'une fonction, appelée disMoiBonjour. Cette fonction retourne une phrase "disant bonjour" à la personne dont le prénom est passé en paramètre. La ligne suivante nous permet de récupérer parmi les objets du script (globalObject())  notre fonction disMoiBonjour (property()), conservée dans la variable disBonjour. Il ne nous reste plus qu'à l'appeler grâce à la méthode call(). Notez le second paramètre, qui est une liste de valeurs qui sont passées en arguments à la fonction appelée. Ici, on envoie juste la chaîne "Fabien".

Voici la sortie du programme :

[eponyme@localhost exemple2]$ ./exemple2
"Bonjour Fabien"
[eponyme@localhost exemple2]$

Cet article ayant juste pour but de découvrir des choses simples et basiques possibles avec QtScript, je ne rentre pas plus dans les le détail concernant les différentes méthodes utilisées ici, mais la documentation vous fera découvrir que l'on peut aller beaucoup plus loin.

Au passage, on voit déjà que du code JavaScript dans une chaine de caractères, ça n'est pas très pratique. Vivement qu'on puisse le mettre dans un fichier !

Exemple 3 : code JS dans un fichier et objet global

Pour le moment, nos scripts étaient "en dur" dans le code C++, ce qui n'a que très peu (aucun ?) intérêt. Voyons ici comment externaliser ce code et le placer dans un fichier. La méthode evaluate() prenant en paramètre une chaine de caractères, il sera très simple (surtout avec Qt) de récupérer le contenu d'un fichier texte pour le faire évaluer. Je profite aussi de cet exemple pour montrer comment exporter depuis le code C++ un objet global dans le script. Comme d'habitude, on attaque directement avec le code :

#include <QtScript>
#include <QApplication>
int main(int argc, char **argv)
{
    QApplication app(argc, argv);
    QScriptEngine engine;
    QFile file("script.qs");
    file.open(QIODevice::ReadOnly);
    engine.globalObject().setProperty("name","Fabien");
    engine.evaluate(file.readAll());   
    file.close();
    return 0;
}

Comme on pouvait s'y attendre, la chose est très simple à réaliser grâce à l'objet QFile. On ouvre en lecture seule un fichier nommé script.qs, et on passe son contenu à la méthode evaluate() via la méthode readAll(). Le contenu du fichier est ainsi entièrement évalué. L'autre nouveauté ici est que l'on "envoie" dans le script l'objet "name", dont la valeur est "Fabien", avec la méthode setProperty(). Ainsi, à n'importe quel endroit du script, il sera possible d'accéder à cet objet.

Voyons d'ailleurs le contenu du script (fichier script.qs) :

print('Bonjour '+name);

Ce petit script se contentera donc d'afficher dans la console un bonjour à la personne dont le prénom est stocké dans l'objet name (et assigné depuis le code C++).

Voyons ce que ca donne à l'exécution :

[eponyme@localhost exemple3]$ ./exemple3
Bonjour Fabien
[eponyme@localhost exemple3]$

La encore, pas de grande surprise ! Mais désormais, vous pouvez stocker votre code JS dans un fichier, ce qui ouvre un grand nombre de possibilités, notamment celui de stocker différents scripts afin de charger différentes fonctions, pour un système de plugins.

Exemple 4 : envoi d'un QObject dans le script

L'exemple 3 nous a montré qu'il est possible d'envoyer un objet avec une valeur dans le script, et ce depuis le code C++. Cependant cet objet ne peut prendre qu'une valeur simple. Nous allons voir ici comment envoyer dans le script un QObject (et donc un de ses objets hérités, ce qui en fait beaucoup).

Pour illustrer cet exemple, je vais utiliser un QLabel, habituellement utilisé pour afficher un libellé dans une interface graphique. Voici le code :

#include <QtScript>
#include <QApplication>
#include <QLabel>
int main(int argc, char **argv)
{
    QApplication app(argc, argv);
    QLabel lbl;
    QScriptEngine engine;
    QFile file("script.qs");
    file.open(QIODevice::ReadOnly);
    lbl.setText("UN");
    QScriptValue object = engine.newQObject(&lbl);
    engine.globalObject().setProperty("label",object);   
    engine.evaluate(file.readAll());
    file.close();
    qDebug() <<"Dans le code : " <<  lbl.text();
    return 0;
}

Le QLabel ne sera pas utilisé ici dans une interface, mais on utilise ses méthodes pour le manipuler. Après l'habituelle création du moteur, on créé donc un objet QLabel, pour lequel on modifie le libellé en le passant à "UN", grâce au slot setText(). Ensuite, la méthode newQObject() du moteur est appelée, prenant en paramètre un pointeur sur notre objet de type QLabel. C'est la valeur renvoyée par newQObject qui est utilisée pour le setProperty() de la ligne suivante, déjà utilisé dans l'exemple 3. Ainsi, dans notre script, on pourra utiliser notre objet QLabel n'importe quand grâce à la variable label, dont le nom est passé en premier argument à setProperty(). Comme dans les autres exemples, on évalue le script, puis on ferme le fichier. Enfin, on affiche la valeur de notre QLabel grâce à sa méthode text(). En toute logique, elle devrait renvoyer "UN", à moins que la valeur ait été changée depuis le script ... Voyons justement le code de ce dernier (fichier script.qs) :

print("Dans le script : "+label.text);
label.setText("DEUX");

Deux lignes, très simples. La première affiche la valeur du QLabel accessible grâce à la variable label. La valeur du QLabel est obtenue grâce à la propriété text de ce dernier. Sur la seconde ligne, on change la valeur du QLabel à "DEUX", grâce à setText().

Voyons ce que donne l'exécution :

[eponyme@localhost exemple4]$ ./exemple4
Dans le script : UN
Dans le code :  "DEUX"
[eponyme@localhost exemple4]$

On voit ici l'évolution de notre QObject ! La première valeur affichée ("UN") depuis le script (avec print()) est celle qui avait été fixée dans le code C++. Après avoir affiché la valeur, le script JS a changé la valeur du QLabel, et depuis le code, la valeur affichée est bien "DEUX".

Nous pouvons donc de façon très simple passer à notre script JS des QObject et lui permettre d'en modifier les propriétés.

Exemple 5 : gestion des erreurs

Lorsque l'on débute, ou lorsque l'on commence à faire des choses compliquées, il arrive souvent que l'on bloque car le code JS n'est pas évalué. Dans la plupart des cas, il s'agit juste d'une erreur de syntaxe. Mais il est parfois difficile de savoir pourquoi est ce que notre script n'est pas évalué, et donc qu'il est impossible d'interagir avec lui ensuite. On peut aussi être parfois sûr de son code, et ne pas penser qu'il y a une erreur. Heureusement il est possible, lorsqu'une erreur est rencontrée, d'afficher un message. Voici un code d'exemple :

#include <QtScript>
#include <QApplication>
int main(int argc, char **argv)
{
    QApplication app(argc, argv);
    QScriptEngine engine;
    QScriptValue result = engine.evaluate("print('Bonjour !'");
    if (result.isError()) {
        qDebug() << result.property("message").toString();
    }
    return 0;
}

Dans ce code, on demande au moteur d'évaluer du JavaScript devant afficher "Bonjour !", mais la parenthèse fermante est volontairement oubliée. On se sert de la valeur renvoyée par l'évaluation pour vérifier s'il y a eu une erreur avec isError(). Si c'est le cas, on peut afficher un message explicatif en récupérant la propriété message du résultat d'évaluation.

Voyons l'exécution du programme :

[eponyme@localhost exemple5]$ ./exemple5
"Parse error"
[eponyme@localhost exemple5]$

Notre code n'est pas évalué, et le moteur nous indique une Parse error. On sait au moins ou chercher. Cette vérification vous permettra de gagner un peu de temps si vous n'obtenez pas le résultat espéré.

Exemple 6 : signaux et slots

Les signaux et slots sont l'un des avantages du développement avec la bibliothèque Qt, et n'ont pas été oubliés avec QtScript. Dans ce dernier exemple, nous allons créer une fenêtre basique comprenant deux boutons. Le premier, appelé "Bonjour", devra afficher "Bonjour !" dans la console lorsque l'on clique dessus, et le second, "Au revoir", devra afficher ... "Au revoir !" (et oui !)  lorsque l'on clique dessus. La particularité est que ces affichages dans la console se feront depuis un script JS, et non pas directement depuis des slots dans le code C++. L'intérêt ? Ici, aucun. Il s'agira simplement de montrer que l'on peut connecter un signal à un slot dont l'implémentation est faite dans un script JS, ce qui permet de personnaliser facilement une application en changeant le comportement du slot dans les scripts. Pour l'exemple, nous verrons deux façons différentes de connecter un signal à un slot implémenté dans un script, la première sera faite depuis le code C++, pour le bouton "Bonjour", et la seconde directement dans le script JS, pour le bouton "Au revoir".

Passons au code :

#include <QtScript>
#include <QApplication>
#include <QtGui>
int main(int argc, char **argv)
{
    QApplication app(argc, argv);
    QWidget fenetre;
    QVBoxLayout vbox(&fenetre);
    QPushButton bBonjour("Bonjour");
    QPushButton bAuRevoir("Au revoir");
    vbox.addWidget(&bBonjour);
    vbox.addWidget(&bAuRevoir);

    QScriptEngine engine;
    QFile file("script.qs");
    file.open(QIODevice::ReadOnly);
    QScriptValue objBonjour = engine.newQObject(&bBonjour);
    QScriptValue objAuRevoir = engine.newQObject(&bAuRevoir);
    engine.globalObject().setProperty("objAuRevoir",objAuRevoir);
    engine.evaluate(file.readAll());
    file.close();
    QScriptValue slot = engine.globalObject().property("bonjourHandler");
    qScriptConnect(&bBonjour, SIGNAL(clicked()), objBonjour, slot);

    fenetre.show();
    return app.exec();
}

C'est un peu plus long que d'habitude, certes, mais le premier pavé ne concerne que l'interface graphique, ce qui n'est pas le sujet de ce billet. On y créé les deux boutons et on les place sur la fenêtre.

Ensuite, comme d'habitude, on créé un moteur, qui évaluera du code JS placé dans un fichier. Deux "objets QtScript" sont créés à partir des deux boutons, grâce à newQObject() : objBonjour et objAuRevoir. Ils nous serviront à réaliser les connexions avec les slots du script. L'objet objAuRevoir est exporté dans le script car nous en aurons besoin pour réaliser la connexion au slot

Une fois l'évaluation du script faite, on récupère la propriété bonjourHander, qui est une fonction dans le script JS, que l'on stocke dans la variable slot. Cette variable est utilisée ensuite pour réaliser la connexion entre le signal clicked du bouton "Bonjour" et le slot bonjourHandler, grâce à la fonction qScriptConnect().

Voyons maintenant le contenu du script (fichier script.qs) :

function bonjourHandler () {
    print('Bonjour !');
}
function auRevoirHandler () {
    print('Au revoir !');
}
objAuRevoir.clicked.connect(this.auRevoirHandler);

On y trouve les deux fonctions qui seront appelées lors des clics sur les boutons. bonjourHandler est déjà connectée au bouton "Bonjour" via l'appel à qScriptConnect() dans le code C++. La fonction auRevoirHandler est elle connectée au bouton "Au revoir" à la dernière ligne du script, qui utilise l'objet objAuRevoir exporté dans le programme. Ainsi nos deux fonctions sont connectées à leur bouton respectif, l'une depuis le code C++, l'autre depuis le script JS. A vous de voir ce que vous préférez !

Une petite image pour l'exécution du programme :

tuto_qtscript_exemple6.jpg


C'est sur cet exemple que ce termine cette initiation. Je n'ai pas la prétention avec cet article de donner une vue d'ensemble des possibilités offertes par QtScript. J'en suis d'ailleurs encore au stade de la découverte. Mais ces exemples pourrons vous permettre de vous lancer, et de démystifier un peu les choses ! Je proposerai dans un prochain article un exemple concret d'utilisation de QtScript pour un système de plugins.

Hormis les liens disséminés un peu partout dans l'article, les deux sources qui m'ont le plus aidé sont :

Bon dev' !


Fabien (eponyme)

Scripts JS avec Java et Rhino : mise en œuvre avec un bot IRC

Fabien Nicoleau

Je montrais dans mon billet précédent que grâce à Rhino il est possible d'exécuter du code JavaScript depuis un programme Java, et comment il possible de se servir de ce système pour gérer des plugins. Je propose dans ce billet de mettre en œuvre ces fonctionnalités et de les appliquer à un projet simple : un robot IRC modulaire. Le principe est d'avoir un robot écrit en Java et capable de se connecter à un serveur IRC, puis de communiquer avec lui (envoi et réception de messages). Nous n'ajouterons rien à ce programme Java, aucun savoir faire pour ce robot, à une exception prêt : la possibilité de stocker des plugins qui lui permettront d'avoir de nouvelles fonctionnalités. Attention, le sujet de ce billet n'étant pas le codage d'un robot IRC (il y en a de meilleurs pour ca ^^), celui présenté ici est ultra simple, et se contente du minimum.

Pour comprendre ce programme, il faut avoir quelques notions sur Rhino/Java, des notions sur les expressions régulières, et notamment les groups qui seront bien utiles ici, et savoir en gros ce qu'est un client IRC.

Le décor

Le programme sera donc codé en Java. Une classe JIrcRobot se chargera de se connecter au serveur IRC, et de communiquer avec lui. Une interface permettra de définir les méthodes à implémenter par les plugins. Elle sera originalement nommée Plugin. La classe JIrcRobot chargera les plugins disponibles, c'est à dire ceux contenus dans le dossier originalement nommé plugins et dont l'extension est .js, puisqu'ils contiennent du code JavaScript. Cela permettra aussi de pouvoir désactiver le chargement d'un plugin en modifiant simplement son extension. Une fois les plugins chargés, le programme attendra des messages du serveur. A chaque message reçu, il interrogera tout ses plugins pour savoir si l'un d'eux est concerné par le message. Si c'est le cas, le plugin sera alors exécuté et pourra alors réagir en envoyant des messages au serveur IRC.

Le test

Pour ceux qui préfèrent voir les choses fonctionner tout de suite, et voir comment ca marche, je vous propose de tester tout de suite le programme. Il vous faut récupérer tous les fichiers en annexe de ce programme : le fichier JIrcRobot.java ainsi que tous les fichiers en .js, qui seront à mettre dans un dossier nommé plugins. JIrcRobot.java sera à mettre au même niveau d'arborescence que le dossier plugins. Vous pouvez sinon juste prendre le fichier jircrobot.tar.gz et le décompresser.

Ouvrez ensuite le fichier JIrcRobot.java et éditez les paramètres de connexion, situés sous le commentaire "CONFIGURATION DE LA CONNEXION AU SERVEUR" pour donner un pseudonyme au robot, lui indiquer sur quel serveur il doit se connecter, et quels canaux il doit rejoindre. Sauvegardez les modifications et compilez le programme :

javac JIrcRobot.java

Vous pouvez maintenant l'exécuter :

java JIrcRobot

Le robot charge les plugins puis se connecte au serveur. En l'état actuel, le robot est capable de répondre aux PING du serveur, afin qu'il ne coupe pas la connexion, de rejoindre des canaux de discussion, de se présenter quand il rejoint un canal et souhaiter la bienvenue à ceux qui arrivent ensuite, de se couper si quelqu'un tape "!QUIT" et enfin de crier "COCORICO" si une phrase contient "france". Ces fonctionnalités sont toutes apportées par les plugins. Le programme Java en lui même ne sait rien faire de tout ca.

La classe JIrcRobot

Il est maintenant temps de comprendre comment tout cela fonctionne. Je passe rapidement sur les fonctionnalités liées au protocole IRC. Sans entrer dans le code, il faut juste savoir que la classe propose ces méthodes :

connect() : se connecte au serveur IRC
getNickName() : renvoie le pseudonyme du robot
joinChannels() : envoie les commandes au serveur IRC pour rejoindre des canaux de discussion
loadPlugins() : charge tous les plugins disponibles dans un dossier
readLine() : lis un message provenant du serveur, s'il y en a un d'arrivé
run() : boucle conservant la connexion avec le serveur, et exécutant les plugins si besoin
setStayConnected() : permet d'indiquer si l'on souhaite rester connecté au serveur ou non
write() : envoi un message au serveur IRC


Les méthodes importantes seront détaillées ensuite : loadPlugins et run.

Enfin la classe contient une interface privée, Plugin, qui permet de définir les méthodes que devront implémenter les plugins :

getPattern() : retournera l'expression régulière qui si elle est validée, indiquera que le plugin doit réagir au message arrivé
patternMatch() : cette méthode sera appelée si l'expression régulière du plugin match avec le message arrivé

Nos plugins devront donc implémenter ces deux méthodes.

Le programme Java

Après avoir vu les méthodes permettant de dialoguer avec le serveur, intéressons nous, dans le détail cette fois-ci, au code des deux principales. Avant cela, un rapide coup d'œil à la méthode main du programme :

public static void main(String[] args) {
    JIrcRobot jir = new JIrcRobot();
    jir.loadPlugins("plugins");
    if (jir.connect()) {
        jir.run();
    }
}

Rien de complexe : on créé un objet JIrcRobot, on charge les plugins, on connecte le robot au serveur IRC, et enfin on se met en attente de réception des messages avec la méthode run(). Il est maintenant temps de voir comment sont chargés les plugins, avec la méthode loadPlugins ;

public void loadPlugins(String folder) {
    ScriptEngine engine;
    Invocable inv;
    File dir = new File(folder);
    for (File file : dir.listFiles()) {
        if (file.isFile() && file.getName().toLowerCase().endsWith(".js")) {
            try {
                engine = new ScriptEngineManager().getEngineByName("JavaScript");
                engine.put("robot", this);
                engine.eval(new FileReader(file.getAbsolutePath()));
                inv = (Invocable) engine;
                this.plugins.add(inv.getInterface(Plugin.class));
                System.out.println(file.getAbsolutePath() + " loaded.");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

Cette méthode va lister (listFiles) le dossier dont le nom lui est passé en paramètre (folder). Pour chaque fichier dont l'extension est .js (endsWith), elle créé un moteur de JavaScript (getEngineByName) et place dans l'environnement du script une variable nommée robot pointant sur l'objet JIrcRobot (put). Ensuite elle lit le contenu du fichier et stocke dans le tableau plugins une instanciation de l'interface Plugin provenant du fichier (getInterface).

Le chargement des plugins consiste donc à stocker des objets implémentant l'interface Plugin et dont le contenu provient des fichiers JavaScript.

Maintenant, la méthode run :

public void run() {
    String buffer;
    Matcher matcher;
    while (this.stayConnected) {
        buffer = this.readLine();
        if (!buffer.equals("")) {
            System.out.println(buffer);
            for (Plugin p : this.plugins) {
                matcher = Pattern.compile(p.getPattern()).matcher(buffer);
                if (matcher.find()) {
                    p.patternMatch(matcher);
                }
            }
        }
    }
}

Cette méthode contient une boucle qui attend un message du serveur IRC (readLine). Une fois qu'un message est reçu, elle boucle sur tous les plugins chargés. Pour chacun d'eux, elle teste si le pattern du plugin (p.getPattern()) match avec le message reçu (buffer). Si c'est le cas (find), la méthode patterMatch() du plugin est appellée et on lui passe en paramètre le matcher, ce qui permettra au plugin d'analyser les groupes capturés grâce à l'expression régulière. C'est assez pratique car le plugin reçoit ainsi seulement ce qui l'intéresse, et qu'il a lui-même spécifié dans la regex.

C'est ici que s'arrête le détail ! Nous avons un système qui stocke des plugins et un autre qui communique avec eux pour savoir s'il faut les solliciter (grâce à un système de regex ici, mais ca pourrait être autrement) et qui qui les exécute si besoin. Ce robot IRC est donc ainsi fait d'un noyau, disposant de peu de fonctionnalités, mais extensible à volonté.

Les plugins

Notre programme étant prêt, concentrons nous maintenant sur les plugins. Nous créerons un fichier par plugin

ping.js

Le serveur IRC envoie régulièrement un message PING auquel le client doit répondre. Si la réponse n'est pas envoyée dans un certain laps de temps, le serveur rompt la connexion. Ce plugin est donc vital. Le message est du type "PING :msg". Le client doit répondre "PONG :msg". msg est parfois le nom du serveur, ou parfois un numéro aléatoire. Pour un PING reçu, on récupèrera donc la partie droite, et on s'en servira pour envoyer le PONG. Voici le contenu de ping.js :

importClass(java.util.regex.Matcher);

function getPattern() {
    return 'PING\\s:(.*)';
}

function patternMatch(matcher) {
    robot.write("PONG :" + matcher.group(1));
}

On commence par importer la classe Java Matcher. On implémente ensuite les deux méthodes nécessaires de l'interface Plugin. Le regex renvoyé par getPattern contient un groupe qui capturera la partie à renvoyer par le PONG. La fonction patternMatch qui est appelée si le regex a matché envoie le PONG suivi du groupe capturé dans le PING. Pour cela elle utilise la variable robot représentant l'objet JIrcRobot pour envoyer un message au serveur.

endOfMOTD.js

Le Message Of The Day (MOTD) est un texte envoyé par le serveur au client lorsqu'il se connecte. Il contient souvent des informations sur le serveur. Une fois le MOTD envoyé, le serveur envoie un message indiquant la fin du MOTD du type : ":calvino.freenode.net 376 jircrobot :End of /MOTD command.". Il est important de réagir à ce message car certains serveurs n'autorisent pas le client à rejoindre des canaux tant que le MOTD n'a pas été envoyé. En attendant la réception de ce message pour rejoindre des canaux, on est sûr qu'on ne sera pas bloqué. Ce plugin réagira donc à ce message, et une fois reçu, appellera la méthode joinChannels de la classe JIrcRobot :

importClass(java.util.regex.Matcher);

function getPattern() {
    return ':.*\\s376\\s.*';
}

function patternMatch(matcher) {
    robot.joinChannels();
}

C'est assez simple ici encore. On donne la regex pour capturer la fin du MOTD et lorsque que le message est arrivé, on utilise la variable robot pour se connecter aux canaux.

quit.js

Ce plugin permettra de stopper le bot. Lorsqu'une personne tapera !QUIT sur un canal, le robot se stoppera. Biensûr dans le cadre d'un "vrai" bot, il faudrait s'assurer que la personne voulant couper le robot en a le droit, en se basant sur son nom d'hôte ou son pseudonyme. Le message envoyé par le serveur sera du type ":someone!~someident@some-host.com PRIVMSG #channel1 :!QUIT". Voici le contenu du plugin :

importClass(java.util.regex.Matcher);

function getPattern() {
    return ':.*\\sPRIVMSG\\s[^ ]+\\s:!QUIT';
}

function patternMatch(matcher) {
    robot.setStayConnected(false);
}

La regex ne capture rien ici car on ne gère pas de droits. Pour quelque chose de plus sécurisé, il faudrait capturer au moins le pseudonyme de la personne. La fonction appelée utilisera la méthode setStayConnected avec false en paramètre, ce qui aura pour effet de stopper la méthode run du robot.

join.js

Afin de rendre notre robot un minimum sociable, ce plugin aura pour tâche de le faire se présenter lorsqu'il rejoint un canal et de souhaiter la bienvenue aux nouveaux arrivants. Pour cela, il suffira de capturer les messages de type JOIN provenant du serveur, qui sont envoyés quand une personne rejoint un canal, ou quand le bot lui même le fait. Le message est du type ":nick!~ident@hostname.fr JOIN :#channel". Il suffit alors de comparer "nick" avec le propre pseudonyme du robot. S'il sont équivalents, c'est que c'est lui qui entre sur un canal, il doit donc se présenter. Si ce n'est pas le sien, alors il souhaite la bienvenue au nouvel arrivant. Il faudra donc capturer le pseudonyme et le nom du canal afin de s'en servir pour envoyer le message :

importClass(java.util.regex.Matcher);

function getPattern() {
    return ':([^!]+).*\\sJOIN\\s:([^ ]+)';
}

function patternMatch(matcher) {
    // Si c'est le robot qui JOIN
    if(matcher.group(1) == robot.getNickname()) {
        robot.write("PRIVMSG "+matcher.group(2)+" :Salut tout le monde, je suis "+matcher.group(1));
    }
    else { // si c'est un nouvel arrivant
        robot.write("PRIVMSG "+matcher.group(2)+" :Salut "+matcher.group(1));
    }
}

On voit donc la regex avec les deux captures (nick + canal). Ces informations sont ensuite utilisées pour le message envoyé.

patriote.js

Un plugin beaucoup moins utile, qui permettra simplement de montrer comment réagir à un texte particulier. Nous souhaitons ici que le bot montre qu'il est fier d'être français. A chaque fois que le mot France (peu importe la casse) sera écrit, il réagira. Le message reçu sera du type ":someone!~someident@some-host.com PRIVMSG #channel :Vive la France". Voyons le code du plugin :

importClass(java.util.regex.Matcher);

function getPattern() {
    return '(?i):[^!]+.*\\sPRIVMSG\\s([^ ]+)\\s:.*france.*';
}

function patternMatch(matcher) {
    robot.write("PRIVMSG "+matcher.group(1)+" :COCORICO !!!");
}

La regex indique que la recherche sera insensible à la casse. On capture simplement le nom du canal d'où le texte est envoyé pour pouvoir répondre.


Voilà pour quelques exemples de plugins. Le but ici est de montrer que presque tout est possible, mais que surtout ce genre de programme, modulable, auquel on peut ajouter des fonctionnalités, est facilement réalisable. Pour les plus courageux qui auront lu jusque ici (j'espère que number80 en fait partie !), n'hésitez pas à poser des questions dans les commentaires. Il n'est pas facile de synthétiser tout cela en un seul billet, il y a donc probablement des passages obscurs !


Fabien (eponyme)

Scripts JS avec Java et Rhino : mise en œuvre avec un bot IRC

Fabien Nicoleau

Je montrais dans mon billet précédent que grâce à Rhino il est possible d'exécuter du code JavaScript depuis un programme Java, et comment il possible de se servir de ce système pour gérer des plugins. Je propose dans ce billet de mettre en œuvre ces fonctionnalités et de les appliquer à un projet simple : un robot IRC modulaire. Le principe est d'avoir un robot écrit en Java et capable de se connecter à un serveur IRC, puis de communiquer avec lui (envoi et réception de messages). Nous n'ajouterons rien à ce programme Java, aucun savoir faire pour ce robot, à une exception prêt : la possibilité de stocker des plugins qui lui permettront d'avoir de nouvelles fonctionnalités. Attention, le sujet de ce billet n'étant pas le codage d'un robot IRC (il y en a de meilleurs pour ca ^^), celui présenté ici est ultra simple, et se contente du minimum.

Pour comprendre ce programme, il faut avoir quelques notions sur Rhino/Java, des notions sur les expressions régulières, et notamment les groups qui seront bien utiles ici, et savoir en gros ce qu'est un client IRC.

Le décor

Le programme sera donc codé en Java. Une classe JIrcRobot se chargera de se connecter au serveur IRC, et de communiquer avec lui. Une interface permettra de définir les méthodes à implémenter par les plugins. Elle sera originalement nommée Plugin. La classe JIrcRobot chargera les plugins disponibles, c'est à dire ceux contenus dans le dossier originalement nommé plugins et dont l'extension est .js, puisqu'ils contiennent du code JavaScript. Cela permettra aussi de pouvoir désactiver le chargement d'un plugin en modifiant simplement son extension. Une fois les plugins chargés, le programme attendra des messages du serveur. A chaque message reçu, il interrogera tout ses plugins pour savoir si l'un d'eux est concerné par le message. Si c'est le cas, le plugin sera alors exécuté et pourra alors réagir en envoyant des messages au serveur IRC.

Le test

Pour ceux qui préfèrent voir les choses fonctionner tout de suite, et voir comment ca marche, je vous propose de tester tout de suite le programme. Il vous faut récupérer tous les fichiers en annexe de ce programme : le fichier JIrcRobot.java ainsi que tous les fichiers en .js, qui seront à mettre dans un dossier nommé plugins. JIrcRobot.java sera à mettre au même niveau d'arborescence que le dossier plugins. Vous pouvez sinon juste prendre le fichier jircrobot.tar.gz et le décompresser.

Ouvrez ensuite le fichier JIrcRobot.java et éditez les paramètres de connexion, situés sous le commentaire "CONFIGURATION DE LA CONNEXION AU SERVEUR" pour donner un pseudonyme au robot, lui indiquer sur quel serveur il doit se connecter, et quels canaux il doit rejoindre. Sauvegardez les modifications et compilez le programme :

javac JIrcRobot.java

Vous pouvez maintenant l'exécuter :

java JIrcRobot

Le robot charge les plugins puis se connecte au serveur. En l'état actuel, le robot est capable de répondre aux PING du serveur, afin qu'il ne coupe pas la connexion, de rejoindre des canaux de discussion, de se présenter quand il rejoint un canal et souhaiter la bienvenue à ceux qui arrivent ensuite, de se couper si quelqu'un tape "!QUIT" et enfin de crier "COCORICO" si une phrase contient "france". Ces fonctionnalités sont toutes apportées par les plugins. Le programme Java en lui même ne sait rien faire de tout ca.

La classe JIrcRobot

Il est maintenant temps de comprendre comment tout cela fonctionne. Je passe rapidement sur les fonctionnalités liées au protocole IRC. Sans entrer dans le code, il faut juste savoir que la classe propose ces méthodes :

connect() : se connecte au serveur IRC
getNickName() : renvoie le pseudonyme du robot
joinChannels() : envoie les commandes au serveur IRC pour rejoindre des canaux de discussion
loadPlugins() : charge tous les plugins disponibles dans un dossier
readLine() : lis un message provenant du serveur, s'il y en a un d'arrivé
run() : boucle conservant la connexion avec le serveur, et exécutant les plugins si besoin
setStayConnected() : permet d'indiquer si l'on souhaite rester connecté au serveur ou non
write() : envoi un message au serveur IRC


Les méthodes importantes seront détaillées ensuite : loadPlugins et run.

Enfin la classe contient une interface privée, Plugin, qui permet de définir les méthodes que devront implémenter les plugins :

getPattern() : retournera l'expression régulière qui si elle est validée, indiquera que le plugin doit réagir au message arrivé
patternMatch() : cette méthode sera appelée si l'expression régulière du plugin match avec le message arrivé

Nos plugins devront donc implémenter ces deux méthodes.

Le programme Java

Après avoir vu les méthodes permettant de dialoguer avec le serveur, intéressons nous, dans le détail cette fois-ci, au code des deux principales. Avant cela, un rapide coup d'œil à la méthode main du programme :

public static void main(String[] args) {
    JIrcRobot jir = new JIrcRobot();
    jir.loadPlugins("plugins");
    if (jir.connect()) {
        jir.run();
    }
}

Rien de complexe : on créé un objet JIrcRobot, on charge les plugins, on connecte le robot au serveur IRC, et enfin on se met en attente de réception des messages avec la méthode run(). Il est maintenant temps de voir comment sont chargés les plugins, avec la méthode loadPlugins ;

public void loadPlugins(String folder) {
    ScriptEngine engine;
    Invocable inv;
    File dir = new File(folder);
    for (File file : dir.listFiles()) {
        if (file.isFile() && file.getName().toLowerCase().endsWith(".js")) {
            try {
                engine = new ScriptEngineManager().getEngineByName("JavaScript");
                engine.put("robot", this);
                engine.eval(new FileReader(file.getAbsolutePath()));
                inv = (Invocable) engine;
                this.plugins.add(inv.getInterface(Plugin.class));
                System.out.println(file.getAbsolutePath() + " loaded.");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

Cette méthode va lister (listFiles) le dossier dont le nom lui est passé en paramètre (folder). Pour chaque fichier dont l'extension est .js (endsWith), elle créé un moteur de JavaScript (getEngineByName) et place dans l'environnement du script une variable nommée robot pointant sur l'objet JIrcRobot (put). Ensuite elle lit le contenu du fichier et stocke dans le tableau plugins une instanciation de l'interface Plugin provenant du fichier (getInterface).

Le chargement des plugins consiste donc à stocker des objets implémentant l'interface Plugin et dont le contenu provient des fichiers JavaScript.

Maintenant, la méthode run :

public void run() {
    String buffer;
    Matcher matcher;
    while (this.stayConnected) {
        buffer = this.readLine();
        if (!buffer.equals("")) {
            System.out.println(buffer);
            for (Plugin p : this.plugins) {
                matcher = Pattern.compile(p.getPattern()).matcher(buffer);
                if (matcher.find()) {
                    p.patternMatch(matcher);
                }
            }
        }
    }
}

Cette méthode contient une boucle qui attend un message du serveur IRC (readLine). Une fois qu'un message est reçu, elle boucle sur tous les plugins chargés. Pour chacun d'eux, elle teste si le pattern du plugin (p.getPattern()) match avec le message reçu (buffer). Si c'est le cas (find), la méthode patterMatch() du plugin est appellée et on lui passe en paramètre le matcher, ce qui permettra au plugin d'analyser les groupes capturés grâce à l'expression régulière. C'est assez pratique car le plugin reçoit ainsi seulement ce qui l'intéresse, et qu'il a lui-même spécifié dans la regex.

C'est ici que s'arrête le détail ! Nous avons un système qui stocke des plugins et un autre qui communique avec eux pour savoir s'il faut les solliciter (grâce à un système de regex ici, mais ca pourrait être autrement) et qui qui les exécute si besoin. Ce robot IRC est donc ainsi fait d'un noyau, disposant de peu de fonctionnalités, mais extensible à volonté.

Les plugins

Notre programme étant prêt, concentrons nous maintenant sur les plugins. Nous créerons un fichier par plugin

ping.js

Le serveur IRC envoie régulièrement un message PING auquel le client doit répondre. Si la réponse n'est pas envoyée dans un certain laps de temps, le serveur rompt la connexion. Ce plugin est donc vital. Le message est du type "PING :msg". Le client doit répondre "PONG :msg". msg est parfois le nom du serveur, ou parfois un numéro aléatoire. Pour un PING reçu, on récupèrera donc la partie droite, et on s'en servira pour envoyer le PONG. Voici le contenu de ping.js :

importClass(java.util.regex.Matcher);

function getPattern() {
    return 'PING\\s:(.*)';
}

function patternMatch(matcher) {
    robot.write("PONG :" + matcher.group(1));
}

On commence par importer la classe Java Matcher. On implémente ensuite les deux méthodes nécessaires de l'interface Plugin. Le regex renvoyé par getPattern contient un groupe qui capturera la partie à renvoyer par le PONG. La fonction patternMatch qui est appelée si le regex a matché envoie le PONG suivi du groupe capturé dans le PING. Pour cela elle utilise la variable robot représentant l'objet JIrcRobot pour envoyer un message au serveur.

endOfMOTD.js

Le Message Of The Day (MOTD) est un texte envoyé par le serveur au client lorsqu'il se connecte. Il contient souvent des informations sur le serveur. Une fois le MOTD envoyé, le serveur envoie un message indiquant la fin du MOTD du type : ":calvino.freenode.net 376 jircrobot :End of /MOTD command.". Il est important de réagir à ce message car certains serveurs n'autorisent pas le client à rejoindre des canaux tant que le MOTD n'a pas été envoyé. En attendant la réception de ce message pour rejoindre des canaux, on est sûr qu'on ne sera pas bloqué. Ce plugin réagira donc à ce message, et une fois reçu, appellera la méthode joinChannels de la classe JIrcRobot :

importClass(java.util.regex.Matcher);

function getPattern() {
    return ':.*\\s376\\s.*';
}

function patternMatch(matcher) {
    robot.joinChannels();
}

C'est assez simple ici encore. On donne la regex pour capturer la fin du MOTD et lorsque que le message est arrivé, on utilise la variable robot pour se connecter aux canaux.

quit.js

Ce plugin permettra de stopper le bot. Lorsqu'une personne tapera !QUIT sur un canal, le robot se stoppera. Biensûr dans le cadre d'un "vrai" bot, il faudrait s'assurer que la personne voulant couper le robot en a le droit, en se basant sur son nom d'hôte ou son pseudonyme. Le message envoyé par le serveur sera du type ":someone!~someident@some-host.com PRIVMSG #channel1 :!QUIT". Voici le contenu du plugin :

importClass(java.util.regex.Matcher);

function getPattern() {
    return ':.*\\sPRIVMSG\\s[^ ]+\\s:!QUIT';
}

function patternMatch(matcher) {
    robot.setStayConnected(false);
}

La regex ne capture rien ici car on ne gère pas de droits. Pour quelque chose de plus sécurisé, il faudrait capturer au moins le pseudonyme de la personne. La fonction appelée utilisera la méthode setStayConnected avec false en paramètre, ce qui aura pour effet de stopper la méthode run du robot.

join.js

Afin de rendre notre robot un minimum sociable, ce plugin aura pour tâche de le faire se présenter lorsqu'il rejoint un canal et de souhaiter la bienvenue aux nouveaux arrivants. Pour cela, il suffira de capturer les messages de type JOIN provenant du serveur, qui sont envoyés quand une personne rejoint un canal, ou quand le bot lui même le fait. Le message est du type ":nick!~ident@hostname.fr JOIN :#channel". Il suffit alors de comparer "nick" avec le propre pseudonyme du robot. S'il sont équivalents, c'est que c'est lui qui entre sur un canal, il doit donc se présenter. Si ce n'est pas le sien, alors il souhaite la bienvenue au nouvel arrivant. Il faudra donc capturer le pseudonyme et le nom du canal afin de s'en servir pour envoyer le message :

importClass(java.util.regex.Matcher);

function getPattern() {
    return ':([^!]+).*\\sJOIN\\s:([^ ]+)';
}

function patternMatch(matcher) {
    // Si c'est le robot qui JOIN
    if(matcher.group(1) == robot.getNickname()) {
        robot.write("PRIVMSG "+matcher.group(2)+" :Salut tout le monde, je suis "+matcher.group(1));
    }
    else { // si c'est un nouvel arrivant
        robot.write("PRIVMSG "+matcher.group(2)+" :Salut "+matcher.group(1));
    }
}

On voit donc la regex avec les deux captures (nick + canal). Ces informations sont ensuite utilisées pour le message envoyé.

patriote.js

Un plugin beaucoup moins utile, qui permettra simplement de montrer comment réagir à un texte particulier. Nous souhaitons ici que le bot montre qu'il est fier d'être français. A chaque fois que le mot France (peu importe la casse) sera écrit, il réagira. Le message reçu sera du type ":someone!~someident@some-host.com PRIVMSG #channel :Vive la France". Voyons le code du plugin :

importClass(java.util.regex.Matcher);

function getPattern() {
    return '(?i):[^!]+.*\\sPRIVMSG\\s([^ ]+)\\s:.*france.*';
}

function patternMatch(matcher) {
    robot.write("PRIVMSG "+matcher.group(1)+" :COCORICO !!!");
}

La regex indique que la recherche sera insensible à la casse. On capture simplement le nom du canal d'où le texte est envoyé pour pouvoir répondre.


Voilà pour quelques exemples de plugins. Le but ici est de montrer que presque tout est possible, mais que surtout ce genre de programme, modulable, auquel on peut ajouter des fonctionnalités, est facilement réalisable. Pour les plus courageux qui auront lu jusque ici (j'espère que number80 en fait partie !), n'hésitez pas à poser des questions dans les commentaires. Il n'est pas facile de synthétiser tout cela en un seul billet, il y a donc probablement des passages obscurs !


Fabien (eponyme)

Java : gérer facilement des plugins JavaScript

Fabien Nicoleau

Dans une application, il peut être parfois intéressant d'extraire une partie de l'intelligence afin de répondre à un besoins sépcifique. Un exemple courant est une application fournie à différents clients. Elle comporte un socle commun, puis les spécificités sont développées dans des plugins afin de ne pas avoir à toucher au code de l'application pour une demande particulière. L'autre intérêt est de pouvoir charger dans une application des fonctionnalités supplémentaires, permettant de réduire le poids de l'application en retirant des fonctionnalités inutilisées. Le fait d'avoir du code externe permet aussi d'en facilité les mises à jour.

En Java, il est possible de créer des plugins de différentes façons : avec des classes externes, chargées grâce à un ClassLoader, grâce à groovy, très populaire, ou encore jython, permettant d'exécuter du code Python depuis un programme Java. Il existe de nombreuses autres solutions, comme celle d'utiliser le moteur JavaScript de la fondation Mozilla : Rhino. Sa particularité est qu'il est fourni par défaut avec Java depuis la version 6.

En ayant eu besoin pour un projet, et ayant trouvé peu d'aide en français, je propose ici quelques exemples très basiques afin de mettre en place un système de plugins JavaScript avec Java. 90% de l'aide dont j'ai eu besoin a été trouvée sur cette page de documentation. Je reprends donc ici quelques exemples, commentés, afin de comprendre le principe et d'imaginer les possibilités qu'offre Rhino. N'hésitez pas à poser des questions dans les commentaires si vous avez besoins de précisions.

Tout d'abord, je vous indique ici les import dont vous aurez besoin et qui seront à utiliser tout au long des exemples (mais pas utiles pour tous). Il ne seront pas réécris ensuite, pour plus de lisibilité :

import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import java.io.FileReader;

Toujours pour simplifier le code affiché, les exceptions ne seront pas gérées, mais renvoyées par le main. Il vous faut évidemment avoir quelques bases de Java et de JavaScript pour pouvoir suivre ces exemples. Dans chacun d'eux, j'utiliserai des fonctions JavaScript. Si vous préférez utiliser du code orienté objet, il vous sera facile de l'adapter grâce aux exemples de la page Oracle citée en préambule.

Enfin, pour chaque exemple, je n'afficherai que la fonction main, qu'il faudra évidemment que vous incluiez dans une classe pour que le tout fonctionne, cependant les sources de chaque exemples sont disponibles en annexe de ce billet.

Exemple 1 : un simple affichage

Pour le premier essai, nous allons exécuter une simple ligne de code JavaScript dans du code Java :

public static void main(String[] args) throws Exception {
     ScriptEngineManager manager = new ScriptEngineManager();
     ScriptEngine engine = manager.getEngineByName("JavaScript");
     engine.eval("println('Mon premier script !');");
}

Rien de plus ! C'est en fait assez simple. Nous commençons par instancier un ScriptEngineManager qui nous permettra de charger le moteur de script souhaité. Nous l'utilisons sur la deuxième ligne : nous chargeons le moteur JavaScript. Aucune installation de bibliothèque nécessaire, car ce moteur est fourni avec Java 6 ! Ces deux premières lignes seront utilisées dans tous nos exemples. Nous terminons en demandant à notre moteur d'évaluer une ligne de code, ici l'affichage du texte "Mon premier script !".

Après compilation et exécution, la ligne s'affiche bien. Rien de transcendant ici, mais nous voyons que nous allons facilement pouvoir exécuter du code JavaScript.

Exemple 2 : interagir avec un script externe

Dans l'optique d'utiliser un système de plugins, il nous faut d'abord pouvoir sortir le code JavaScript du code Java, et le placer dans un fichier externe. De plus il nous faut pouvoir appeler une fonction en lui passant des paramètres, puis pouvoir récupérer le résultat obtenu.

Pour illustrer l'exemple, nous allons imaginer un programme Java qui passe à une fonction nommée compute() deux chiffres qu'elle traitera. Le code Java récupèrera le résultat du traitement effectué par compute() et l'affichera. Enfin, nous allons créer deux plugins JavaScript qui implémenteront tous les deux la fonction compute(). L'un fera une addition (plus.js) des deux chiffres donnés, l'autre une soustraction (moins.js). On commence par le code Java :

public static void main(String[] args) throws Exception {
     ScriptEngineManager manager = new ScriptEngineManager();
     ScriptEngine engine = manager.getEngineByName("JavaScript");
     engine.eval(new FileReader("plus.js"));
     Invocable inv = (Invocable) engine;
     System.out.println(inv.invokeFunction("compute",3,2));
}

Comme dans le premier exemple, nous récupérons le moteur JavaScript. Ensuite plutôt que directement évaluer du code, nous passons un fichier à la fonction eval(), qui contiendra notre code JavaScript. Nous commençons ici par plus.js, qui fera une addition. Vous pourrez ensuite utiliser moins.js. Enfin, nous invoquons la fonction compute() et lui passons deux arguments (3 et 2). Nous récupérons le résultat du traitement, qui est un Object, et qui contiendra ici un chiffre (un Double à vrai dire), nous l'affichons directement.

Voici le contenu de plus.js :

function compute(a,b) {
   return a+b;
}

Celui de moin.js :

function compute(a,b) {
   return a-b;
}

Aucun commentaire nécessaire ! Exécutez le programme avec plus.js, puis moins.js, les résultats seront 5.0 puis 1.0.

Nous voyons ici que nous pouvons très facilement avoir du code Java qui exécute une fonction dont il ne connait pas le contenu. Il en récupère le résultat sous forme d'un objet et peut le traiter. D'un autre coté, nous avons des fichiers contenant du code JavaScript qui traitent de façon différente deux arguments identiques, et renvoient le résultat. Dans une application gérant des plugins, il suffit alors que le nom du fichier JavaScript soit une variable, ou provienne d'un choix de l'utilisateur, et le tour est joué ! Attention tout de même, pour un système robuste, il est important de gérer les exceptions afin de réagir aux problèmes qui pourraient survenir.

Exemple 3 : Exporter des variables dans le script externe

Lorsque l'on mets en place un système de plugins, on souhaite que ces derniers puissent avoir accès aux informations de l'application, souvent par l'intermédiaire d'un noyau depuis lequel on accède à des variables. De cette manière le plugin agit de façon directe avec l'application, en consultant des informations, et en les mettant à jour. Nous allons dans cet exemple voir qu'il est tout à fait possible de faire cela avec Java et Rhino. Nous allons créer une classe Afficheur contenant la méthode affiche() qui se contente d'afficher dans la console une ligne de texte. Ensuite, nous allons instancier un objet de cette classe et l'exporter dans notre script JavaScript. Ce dernier utilisera dans son code l'objet exporté et appellera la méthode affiche() :

private static class Afficheur {
   public void affiche() {
       System.out.println("J'affiche !");
    }
}
public static void main(String[] args) throws Exception {
    ScriptEngineManager manager = new ScriptEngineManager();
    ScriptEngine engine = manager.getEngineByName("JavaScript");
    engine.eval(new FileReader("afficheur.js"));
    Afficheur aff = new Afficheur();
    engine.put("monAfficheur",aff);
    Invocable inv = (Invocable) engine;
    inv.invokeFunction("affiche");
}

On voit ici qu'une fois l'objet Afficheur instancié, on l'exporte dans le script grace à la méthode put(). On indique que le nom de variable par laquelle l'objet sera accessible dans le script sera "monAfficheur". Enfin on appelle la fonction affiche() implémentée dans le script afficheur.js dont voici le contenu :

function affiche() {
   monAfficheur.affiche();
}

La fonction affiche() utilise donc la variable monAfficheur disponnible grace à l'export fait depuis le code Java, et appelle la méthode affiche(). Voilà notre liaison réalisée ! Depuis notre code JavaScript, nous accédons à un objet de notre application Java et pouvons l'utiliser. Ici nous ne réalisons qu'un simple affichage, mais tout est possible. Notez qu'il est possible de définir différent scope lors de l'export des variables permettant ainsi de leur donner des valeurs différentes selon le contexte utilisé lors de l'évaluation. Plus de renseignement dans la documentation.

Exemple 4 : Implémenter une interface

Nous progressons dans les fonctionnalités proposées pour réaliser une gestion de plugins. Mais il est possible d'aller plus loin, et de définir plus précisément les fonctionnalités offertes par les plugins. Pour cela, nous pouvons définir en Java une interface, et l'implémenter depuis un plugin. Une fois le code du plugin évalué, nous appelons une méthode depuis le code Java, dont l'implémentation a été réalisée depuis le code JavaScript ! Il devient alors possible de définir proprement en Java les méthodes qui seront à implémenter par les plugins, sans en connaitre le traitement évidemment, et d'utiliser ensuite dans le code des objets instanciant cette interface. Mieux que du blabla, le code : 

private interface Afficheur2 {
    void affiche2();
}
public static void main(String[] args) throws Exception {
    ScriptEngineManager manager = new ScriptEngineManager();
    ScriptEngine engine = manager.getEngineByName("JavaScript");
    engine.eval("function affiche2() { println('affiche (2)'); }");
    Invocable inv = (Invocable) engine;
    Afficheur2 aff = inv.getInterface(Afficheur2.class);
    aff.affiche2();
}

Pour l'exemple, je n'utilise pas de fichier externe, mais directement une ligne de code JavaScript. Ce code implémente la méthode affiche2() de l'interface Afficheur2 en faisant un simple affichage de ligne. Nous créons un objet de type Afficheur2 puis récupérons l'instanciation de l'interface grâce à la méthode getInterface().

Enfin, nous utilisons ce nouvel objet et appelons la méthode affiche2() qui effectuera ce qui a été défini dans le code JavaScript, c'est à dire afficher une ligne de texte.

Nous voyons ici qu'il nous est donc possible d'écrire du code générique et de l'utiliser. C'est grâce aux plugins utilisés que instanciation est faite et nous pouvons ainsi pour un même code Java obtenir des comportement totalement différents.

Exemple 5 : Utilisation des classes Java depuis le code JavaScript

Comme dernier exemple et pour montrer toute la puissance de Rhino, nous créons un code Java qui se contentera d'évaluer un script :

public static void main(String[] args) throws Exception {
    ScriptEngineManager manager = new ScriptEngineManager();
    ScriptEngine engine = manager.getEngineByName("JavaScript");
    engine.eval(new FileReader("frame.js"));
}

Le script en revanche est particulier, car il importe directement des classes Java et utilise les objets instanciés. Le script frame.js va créer une JFrame et l'afficher. Il affichera aussi le titre de la fenêtre, qu'il récupère directement depuis la propriété de la JFrame :

importClass(javax.swing.JFrame);
importClass(java.awt.Dimension);

var frame = new JFrame("hello");
frame.setSize(new Dimension(250,250));
frame.setVisible(true);
println(frame.title);

Exécutez le programme, une fenêtre apparait !

JFrame depuis un JavaScript


Voilà pour cette petite initiation au scripting avec Java et son application avec l'utilisation de plugins. Il y aurait surement beaucoup plus à dire, mais vous disposez maintenant d'une base pour débuter, et de l'alliance du Java et du JavaScript pour vos applications ! Ce sujet aura aussi été pour moi l'occasion de reprendre la plume sur ce blog, chose qui n'avait pas été faite depuis trop longtemps !


Fabien (eponyme)

Java : gérer facilement des plugins JavaScript

Fabien Nicoleau

Dans une application, il peut être parfois intéressant d'extraire une partie de l'intelligence afin de répondre à un besoins sépcifique. Un exemple courant est une application fournie à différents clients. Elle comporte un socle commun, puis les spécificités sont développées dans des plugins afin de ne pas avoir à toucher au code de l'application pour une demande particulière. L'autre intérêt est de pouvoir charger dans une application des fonctionnalités supplémentaires, permettant de réduire le poids de l'application en retirant des fonctionnalités inutilisées. Le fait d'avoir du code externe permet aussi d'en facilité les mises à jour.

En Java, il est possible de créer des plugins de différentes façons : avec des classes externes, chargées grâce à un ClassLoader, grâce à groovy, très populaire, ou encore jython, permettant d'exécuter du code Python depuis un programme Java. Il existe de nombreuses autres solutions, comme celle d'utiliser le moteur JavaScript de la fondation Mozilla : Rhino. Sa particularité est qu'il est fourni par défaut avec Java depuis la version 6.

En ayant eu besoin pour un projet, et ayant trouvé peu d'aide en français, je propose ici quelques exemples très basiques afin de mettre en place un système de plugins JavaScript avec Java. 90% de l'aide dont j'ai eu besoin a été trouvée sur cette page de documentation. Je reprends donc ici quelques exemples, commentés, afin de comprendre le principe et d'imaginer les possibilités qu'offre Rhino. N'hésitez pas à poser des questions dans les commentaires si vous avez besoins de précisions.

Tout d'abord, je vous indique ici les import dont vous aurez besoin et qui seront à utiliser tout au long des exemples (mais pas utiles pour tous). Il ne seront pas réécris ensuite, pour plus de lisibilité :

import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import java.io.FileReader;

Toujours pour simplifier le code affiché, les exceptions ne seront pas gérées, mais renvoyées par le main. Il vous faut évidemment avoir quelques bases de Java et de JavaScript pour pouvoir suivre ces exemples. Dans chacun d'eux, j'utiliserai des fonctions JavaScript. Si vous préférez utiliser du code orienté objet, il vous sera facile de l'adapter grâce aux exemples de la page Oracle citée en préambule.

Enfin, pour chaque exemple, je n'afficherai que la fonction main, qu'il faudra évidemment que vous incluiez dans une classe pour que le tout fonctionne, cependant les sources de chaque exemples sont disponibles en annexe de ce billet.

Exemple 1 : un simple affichage

Pour le premier essai, nous allons exécuter une simple ligne de code JavaScript dans du code Java :

public static void main(String[] args) throws Exception {
     ScriptEngineManager manager = new ScriptEngineManager();
     ScriptEngine engine = manager.getEngineByName("JavaScript");
     engine.eval("println('Mon premier script !');");
}

Rien de plus ! C'est en fait assez simple. Nous commençons par instancier un ScriptEngineManager qui nous permettra de charger le moteur de script souhaité. Nous l'utilisons sur la deuxième ligne : nous chargeons le moteur JavaScript. Aucune installation de bibliothèque nécessaire, car ce moteur est fourni avec Java 6 ! Ces deux premières lignes seront utilisées dans tous nos exemples. Nous terminons en demandant à notre moteur d'évaluer une ligne de code, ici l'affichage du texte "Mon premier script !".

Après compilation et exécution, la ligne s'affiche bien. Rien de transcendant ici, mais nous voyons que nous allons facilement pouvoir exécuter du code JavaScript.

Exemple 2 : interagir avec un script externe

Dans l'optique d'utiliser un système de plugins, il nous faut d'abord pouvoir sortir le code JavaScript du code Java, et le placer dans un fichier externe. De plus il nous faut pouvoir appeler une fonction en lui passant des paramètres, puis pouvoir récupérer le résultat obtenu.

Pour illustrer l'exemple, nous allons imaginer un programme Java qui passe à une fonction nommée compute() deux chiffres qu'elle traitera. Le code Java récupèrera le résultat du traitement effectué par compute() et l'affichera. Enfin, nous allons créer deux plugins JavaScript qui implémenteront tous les deux la fonction compute(). L'un fera une addition (plus.js) des deux chiffres donnés, l'autre une soustraction (moins.js). On commence par le code Java :

public static void main(String[] args) throws Exception {
     ScriptEngineManager manager = new ScriptEngineManager();
     ScriptEngine engine = manager.getEngineByName("JavaScript");
     engine.eval(new FileReader("plus.js"));
     Invocable inv = (Invocable) engine;
     System.out.println(inv.invokeFunction("compute",3,2));
}

Comme dans le premier exemple, nous récupérons le moteur JavaScript. Ensuite plutôt que directement évaluer du code, nous passons un fichier à la fonction eval(), qui contiendra notre code JavaScript. Nous commençons ici par plus.js, qui fera une addition. Vous pourrez ensuite utiliser moins.js. Enfin, nous invoquons la fonction compute() et lui passons deux arguments (3 et 2). Nous récupérons le résultat du traitement, qui est un Object, et qui contiendra ici un chiffre (un Double à vrai dire), nous l'affichons directement.

Voici le contenu de plus.js :

function compute(a,b) {
   return a+b;
}

Celui de moin.js :

function compute(a,b) {
   return a-b;
}

Aucun commentaire nécessaire ! Exécutez le programme avec plus.js, puis moins.js, les résultats seront 5.0 puis 1.0.

Nous voyons ici que nous pouvons très facilement avoir du code Java qui exécute une fonction dont il ne connait pas le contenu. Il en récupère le résultat sous forme d'un objet et peut le traiter. D'un autre coté, nous avons des fichiers contenant du code JavaScript qui traitent de façon différente deux arguments identiques, et renvoient le résultat. Dans une application gérant des plugins, il suffit alors que le nom du fichier JavaScript soit une variable, ou provienne d'un choix de l'utilisateur, et le tour est joué ! Attention tout de même, pour un système robuste, il est important de gérer les exceptions afin de réagir aux problèmes qui pourraient survenir.

Exemple 3 : Exporter des variables dans le script externe

Lorsque l'on mets en place un système de plugins, on souhaite que ces derniers puissent avoir accès aux informations de l'application, souvent par l'intermédiaire d'un noyau depuis lequel on accède à des variables. De cette manière le plugin agit de façon directe avec l'application, en consultant des informations, et en les mettant à jour. Nous allons dans cet exemple voir qu'il est tout à fait possible de faire cela avec Java et Rhino. Nous allons créer une classe Afficheur contenant la méthode affiche() qui se contente d'afficher dans la console une ligne de texte. Ensuite, nous allons instancier un objet de cette classe et l'exporter dans notre script JavaScript. Ce dernier utilisera dans son code l'objet exporté et appellera la méthode affiche() :

private static class Afficheur {
   public void affiche() {
       System.out.println("J'affiche !");
    }
}
public static void main(String[] args) throws Exception {
    ScriptEngineManager manager = new ScriptEngineManager();
    ScriptEngine engine = manager.getEngineByName("JavaScript");
    engine.eval(new FileReader("afficheur.js"));
    Afficheur aff = new Afficheur();
    engine.put("monAfficheur",aff);
    Invocable inv = (Invocable) engine;
    inv.invokeFunction("affiche");
}

On voit ici qu'une fois l'objet Afficheur instancié, on l'exporte dans le script grace à la méthode put(). On indique que le nom de variable par laquelle l'objet sera accessible dans le script sera "monAfficheur". Enfin on appelle la fonction affiche() implémentée dans le script afficheur.js dont voici le contenu :

function affiche() {
   monAfficheur.affiche();
}

La fonction affiche() utilise donc la variable monAfficheur disponnible grace à l'export fait depuis le code Java, et appelle la méthode affiche(). Voilà notre liaison réalisée ! Depuis notre code JavaScript, nous accédons à un objet de notre application Java et pouvons l'utiliser. Ici nous ne réalisons qu'un simple affichage, mais tout est possible. Notez qu'il est possible de définir différent scope lors de l'export des variables permettant ainsi de leur donner des valeurs différentes selon le contexte utilisé lors de l'évaluation. Plus de renseignement dans la documentation.

Exemple 4 : Implémenter une interface

Nous progressons dans les fonctionnalités proposées pour réaliser une gestion de plugins. Mais il est possible d'aller plus loin, et de définir plus précisément les fonctionnalités offertes par les plugins. Pour cela, nous pouvons définir en Java une interface, et l'implémenter depuis un plugin. Une fois le code du plugin évalué, nous appelons une méthode depuis le code Java, dont l'implémentation a été réalisée depuis le code JavaScript ! Il devient alors possible de définir proprement en Java les méthodes qui seront à implémenter par les plugins, sans en connaitre le traitement évidemment, et d'utiliser ensuite dans le code des objets instanciant cette interface. Mieux que du blabla, le code : 

private interface Afficheur2 {
    void affiche2();
}
public static void main(String[] args) throws Exception {
    ScriptEngineManager manager = new ScriptEngineManager();
    ScriptEngine engine = manager.getEngineByName("JavaScript");
    engine.eval("function affiche2() { println('affiche (2)'); }");
    Invocable inv = (Invocable) engine;
    Afficheur2 aff = inv.getInterface(Afficheur2.class);
    aff.affiche2();
}

Pour l'exemple, je n'utilise pas de fichier externe, mais directement une ligne de code JavaScript. Ce code implémente la méthode affiche2() de l'interface Afficheur2 en faisant un simple affichage de ligne. Nous créons un objet de type Afficheur2 puis récupérons l'instanciation de l'interface grâce à la méthode getInterface().

Enfin, nous utilisons ce nouvel objet et appelons la méthode affiche2() qui effectuera ce qui a été défini dans le code JavaScript, c'est à dire afficher une ligne de texte.

Nous voyons ici qu'il nous est donc possible d'écrire du code générique et de l'utiliser. C'est grâce aux plugins utilisés que instanciation est faite et nous pouvons ainsi pour un même code Java obtenir des comportement totalement différents.

Exemple 5 : Utilisation des classes Java depuis le code JavaScript

Comme dernier exemple et pour montrer toute la puissance de Rhino, nous créons un code Java qui se contentera d'évaluer un script :

public static void main(String[] args) throws Exception {
    ScriptEngineManager manager = new ScriptEngineManager();
    ScriptEngine engine = manager.getEngineByName("JavaScript");
    engine.eval(new FileReader("frame.js"));
}

Le script en revanche est particulier, car il importe directement des classes Java et utilise les objets instanciés. Le script frame.js va créer une JFrame et l'afficher. Il affichera aussi le titre de la fenêtre, qu'il récupère directement depuis la propriété de la JFrame :

importClass(javax.swing.JFrame);
importClass(java.awt.Dimension);

var frame = new JFrame("hello");
frame.setSize(new Dimension(250,250));
frame.setVisible(true);
println(frame.title);

Exécutez le programme, une fenêtre apparait !

JFrame depuis un JavaScript


Voilà pour cette petite initiation au scripting avec Java et son application avec l'utilisation de plugins. Il y aurait surement beaucoup plus à dire, mais vous disposez maintenant d'une base pour débuter, et de l'alliance du Java et du JavaScript pour vos applications ! Ce sujet aura aussi été pour moi l'occasion de reprendre la plume sur ce blog, chose qui n'avait pas été faite depuis trop longtemps !


Fabien (eponyme)