Créer un DSL facilement avec Xtext

Exemple d'un langage de description de sites web

Olivier Le Goaer

Après avoir rappelé brièvement ce que sont les DSL et leur intérêt pour le développement logiciel, ce tutoriel illustre la création d'un langage minimaliste de description de site web, baptisé « WDL ». L'environnement utilisé est Eclipse Modeling Framework (EMF), qui est devenu le standard de-facto pour l'ingénierie dirigée par les modèles. Les tenants et les aboutissants ce tutoriel se trouvent dans mon cours.

- Titre : Créer un DSL facilement avec Xtext

- Auteur : Olivier Le Goaer

- Parution : 5 octobre 2012

- Mise à jour : 4 octobre 2016

- Public visé :

- Licence : Le contenu de cet article est rédigé par Olivier Le Goaer et est mis à disposition selon les termes de la Licence Creative Commons Attribution 3.0 non transposé. Les logos Developpez.com, en-tête, pied de page, css, et look & feel de l'article sont Copyright ® 2013 Developpez.com.

Sommaire

- I. Introduction

- II. WDL : Website Description Language

- III. Une histoire de syntaxe…

- III-A. Syntaxe abstraite avec Ecore

- III-A-1. Création d'un fichier Ecore

- III-A-2. Métamodélisation via l'éditeur graphique

- III-A-3. Métamodélisation via l'éditeur textuel

- III-A-4. Génération des classes d'implémentation

- III-B. Syntaxe concrète avec Xtext

- III-B-1. Création d'un fichier Xtext

- III-B-2. Définition de la grammaire

- III-B-3. Génération du plugin

- IV. Aller plus loin : portée et validation

- IV-A. Mécanisme de portée

- IV-B. Mécanisme de validation

- V. Projection vers les langages du web

- V-A. Transformation

- V-B. Code de transformation

- VI. Remerciements

I - Introduction

On appelle DSL la famille de langages dédiés à un domaine particulier, par opposition aux GPL qui peuvent potentiellement traiter tous les domaines. Par nature, les GPL sont des langages assez stables et éprouvés, comme c'est le cas en programmation avec Java et C, ou en modélisation avec UML. À l'inverse, on peut s'attendre à une prolifération de langages dédiés, pour des usages ponctuels et bien ciblés, et bénéficiant naturellement d'un processus de validation moins rigide.

L'IDM promeut cette philosophie selon laquelle chacun est libre de créer un langage pour un besoin précis. Là où le bât blesse, c'est qu'il faut être capable d'outiller correctement ce tout nouveau langage, avec notamment :

1. le compilateur/interpréteur associé ;

2. un environnement adapté (éditeur de code avec coloration syntaxique, autocomplétion, etc).

Fort heureusement, il existe des solutions moins chronophages que de tout redévelopper. Le point (1) se contourne en projetant le code vers un langage cible qui, lui, dispose d'un compilateur/interpréteur bien établi. Pour le point (2), des technologies comme Xtext nous aident à produire des éditeurs de code sur-mesure, pour l'IDE Eclipse.

II - WDL : Website Description Language

L'idée du langage WDL serait de décrire les différentes pages qui composent un site web, mobile ou non, en renseignant quelques caractéristiques de base, et notamment les menus de navigation entre celles-ci. Les commentaires (monolignes et multilignes) seraient également supportés.

Comme souvent pour la création d'un DSL, le plus simple est d'imaginer à quoi ressemblera notre futur langage, sobrement baptisé WDL (Website Description Language). Nous optons pour une syntaxe textuelle exploitant des mots-clés anglais, moins verbeux que le français. Au passage, l'extension de nos futurs fichiers textuels est toute trouvée : .wdl

Exemple de fichier .wdl

mobile website {

copyright : "Le Goaer Corporation"

page home {
title : "Bienvenue"
menu : about, findUs
}

page about {
title : "A propos..."
}

//Page qui contiendra une map google
page findUs {
title : "Où nous trouver ?"
menu : home
}
}

Le vocabulaire utilisé par WDL (les mots-clés en quelque sorte) relève bien du domaine des sites web, et est donc parfaitement accessible à un non-informaticien. Dès lors, on peut imaginer un webmaster/webdesigner ou même un client final exprimer ses besoins dans le langage WDL, sans connaissances particulières des technologies du web.

III - Une histoire de syntaxe…

L'approche préconisée dans ce tutoriel est de commencer par définir la syntaxe abstraite de notre langage WDL à travers la définition d'un métamodèle, en introduisant des concepts (i.e. le vocabulaire du domaine des sites web) et les relations entre ces concepts. Pour autant, notre langage ne disposera pas encore de syntaxe concrète (aussi appelée syntaxe de surface), qui est pourtant l'incarnation la plus naturelle d'une langue, quelle qu'elle soit. Cette tâche se fait dans un second temps par la mise en correspondance du métamodèle et d'une grammaire, dans le cas d'une syntaxe textuelle comme c'est le cas ici. Voyons comment réaliser ces 2 grandes étapes sous EMF.

III-A - Syntaxe abstraite avec Ecore

Dans l'univers EMF, les métamodèles sont eux-mêmes décrits dans un langage spécialisé : le langage ECORE. Ce langage de métamodélisation repose sur des notions issues de l'orienté-objet et ne devrait donc pas dérouter les ingénieurs habitués à la programmation par objet et à UML. Pour en savoir plus, consultez le tutoriel sur la métamodélisation en Ecore par Mickael Baron.

III-A-1 - Création d'un fichier Ecore

Commencez par créer un nouveau projet EMF vide (File>New>Other>EMF>Empty EMF Project) puis dotez-le d'une nature Xtext (Clic droit sur le projet > Configure > Add Xtext Nature/Convert to Xtext Project). Dans le dossier model, créez un fichier Ecore (New>Other>EMF>Ecore model), nommé Wdl.ecore. N'oubliez surtout pas de renseigner les propriétés de votre langage, du genre name:website, nsPrefix:website et nsUri:http://WDL/1.0. Désormais, cet URI servira d'identifiant unique à chaque version de votre langage à travers la plateforme EMF.

A partir de là, vous avez le choix entre une édition graphique de votre fichier Ecore, ou bien une édition textuelle. Chacun possède ses avantages et inconvénients. Personnellement, je prend le meilleur des deux mondes : je dégrossis le travail avec l'éditeur graphique puis j'attaque les finitions avec l'éditeur textuel. Veillez juste à ne pas être contradictoire entre ces deux points de vues sur le même fichier Ecore.

III-A-2 - Métamodélisation via l'éditeur graphique

Lancez l'éditeur graphique de diagramme Ecore (clic droit sur le fichier > Initialize Ecore Diagram). En fin de processus, vous obtenez un fichier Wdl.aird. Double cliquez dessus et vous verrez apparaître une palette d'outils « à la UML ». La métamodélisation graphique du domaine des sites web est alors triviale :

Wdl.ecore (vue graphique)

Un Website est l'élément racine. Il est décrit par deux attributs (copyright et isMobileFriendly) et par une composition d'une ou plusieurs pages. Une page est décrite par deux attributs (son nom et son titre), ainsi que par des références à d'autres pages.

III-A-3 - Métamodélisation via l'éditeur textuel

L'éditeur textuel nécessite l'installation au préalable d'un composant supplémentaire : OCL Tools (Help > Install Modeling Components). Une fois installé, lancez l'éditeur (clic droit sur le fichier > Open with > OCLinEcore Editor) et complétez le code comme suit :

Wdl.ecore (vue textuelle)

package website : website = 'http://WDL/1.0'
{
class Website
{
attribute copyright : String[?];
attribute isMobileFriendly : Boolean[1];
property pages : Page[+] { ordered composes };
invariant SingletonPage: self.pages->forAll(p1, p2 | p1 <> p2 implies p1.name<>p2.name);
}
class Page
{
attribute name : String[1];
attribute title : String[1];
property targets : Page[*] { ordered !unique };
invariant AvoidSelfReference: not self.targets->includes(self);
invariant AvoidDuplicates: self.targets->forAll ( t1 | self.targets -> one ( t2 | t2 = t1));
}
}

Le gros avantage de cette vue textuelle est de pouvoir exprimer des contraintes OCL directement au niveau du métamodèle. Ici par exemple, j'ai contraint chaque page à avoir un nom unique, à ne pas s'auto-référencer ni contenir des références à d'autres pages en double.

III-A-4 - Génération des classes d'implémentation

Pour terminer, vous devez obtenir les classes java d'implémentation correspondant à ce modèle du domaine. Cela se réalise très facilement à l'aide d'un générateur. Dans le dossier model, créez un fichier Wdl.genmodel (New>Other>EMF>EMF Generator model). Ouvrez ce fichier dans l'éditeur arborescent et faites un clic droit sur l'élément racine, puis choisissez uniquement 'Generate Model Code'. Vous verrez apparaître dans le dossier src/ de votre projet, les implémentations Java du métamodèle WDL, adhérant aux conventions particulières du framework EMF. Ces implémentations seront nécessaires au fonctionnement du parseur ANTLR…

III-B - Syntaxe concrète avec Xtext

Xtext est un langage dédié(1) à la description d'une grammaire EBNF, en lien avec des implémentations Ecore. L'exploit de cette technologie est de générer automatiquement un parseur ANTLR ainsi qu'un plugin Eclipse formant un éditeur de code complet avec coloration, complétion, folding, détection d'erreurs, quick fix, etc, propre à votre langage. Vous trouverez sur developpez.com deux tutoriels détaillés sur le fonctionnement de Xtext, l'un par Georges KEMAYO et l'autre par Alain Bernard.

III-B-1 - Création d'un fichier Xtext

Commencez par créer un nouveau projet Xtext basé sur un métamodèle existant (File>New>Other>Xtext>Xtext Projet From Existing Ecore Models). L'assistant vous demandera de sélectionner le genmodel (Wdl.genmodel) créé à l'étape précédente, ainsi que d'autres informations, notamment le paquetage (disons com.developpez.WDL) et l'extension des futurs fichiers WDL (.wdl). Vérifiez bien que la règle d'entrée est bien positionnée sur 'Website' et non pas 'Page'.

III-B-2 - Définition de la grammaire

Vous le constaterez, Xtext génère pour vous une grammaire par défaut dans le fichier Wdl.xtext, situé dans le répertoire /src de votre projet. Elle est inférée à partir de la structure du métamodèle, mais convient rarement en pratique. Aussi, à l'aide de l'éditeur de code Xtext, nous allons la modifier à notre convenance. Le résultat est présenté ci-dessous.

Wdl.xtext

grammar com.developpez.WDL with org.eclipse.xtext.common.Terminals

import "http://WDL/1.0"
import "http://www.eclipse.org/emf/2002/Ecore" as ecore

WebsiteRule returns Website:
(isMobileFriendly?='mobile')?
'website'
'{'
('copyright :' copyright=STRING)?
(pages+=PageRule)+
'}';

PageRule returns Page:
'page'
name = ID
'{'
'title : ' title = STRING
('menu :' targets += [Page|ID] ( "," targets+=[Page|ID])* )?
'}';

L'élément racine de modélisation étant le Website, la règle de production correspondante WebsiteRule est l'axiome de la grammaire. Une autre règle de production est nécessaire : celle qui décrit une page. Veuillez noter que j'ai choisi le mot-clé 'menu' plutôt que 'targets' pour bien montrer l'indépendance entre syntaxe abstraite et syntaxe concrète. Puisque les pages cibles dans les menus doivent exister par ailleurs, nous utilisons des « cross-references » ([Page|ID]).

Xtext offre déjà un ensemble de terminaux prédéfinis (org.eclipse.xtext.common.Terminals), dont les identificateurs (ID) et les types de données de base (STRING, INT, etc.). Les commentaires sont déjà gérés par défaut, mais ne sont pas incorporés à l'arbre de syntaxe abstraite produit lors du parsing ; ce sont des « hidden terminals ».

III-B-3 - Génération du plugin

Lancez le script MWE2 (Run As>MWE2 Workflow) situé dans le répertoire /src de votre projet Xtext, et le framework Xtext générera des implémentations à travers quatre projets interdépendants qui forment un plugin eclipse prévu pour prendre en charge les fichiers WDL :

- com.developpez.website

- com.developpez.website.sdk

- com.developpez.website.tests

- com.developpez.website.ui

Pour vous en convaincre, lancez une autre instance d'eclipse (Run As>Eclipse Application) à partir de ce projet. Il peut arriver que vous manquiez d'espace mémoire pour cette opération. Dans ce cas, au niveau de la configuration de lancement de cette instance (Run As > Run Configurations), rajoutez cet argument à la VM Java : -XX:MaxPermSize=1024m

Dès lors, créez un workspace, puis un projet neutre et enfin un fichier d'extension .wdl. La plate-forme va reconnaître cette extension de fichier et proposer d'ajouter la nature Xtext au projet. Acceptez. Nous avons ainsi la confirmation que le plugin est chargé et parfaitement fonctionnel dans cette instance d'éclipse.

Pour la distribution de votre éditeur WDL aux utilisateurs finaux, soit vous allez jusqu'au bout de la procédure d'exportation de plugin Eclipse, soit vous en faite une application exécutable autonome.

IV - Aller plus loin : portée et validation

Ce n'est pas parce que le plugin est généré automatiquent qu'il n'y a plus rien à faire. En effet, vous serez peut-être amené à vouloir compléter son code en mettant les mains dans le cambouis. Attention, votre code doit être écrit en xtend : c'est un dialecte de Java qui est ensuite traduit en pur Java dans le répertoire /xtend-gen. Il vous suffira simplement de relancer l'instance du plugin pour constater vos modifications.

IV-A - Mécanisme de portée

On l'a vu, grâce aux règles Xtext de type « cross-references », les menus peuvent faire référence à des pages définies ailleurs dans le fichier WDL. L'autocomplétion (Ctrl + Espace) de l'éditeur WDL tient d'ailleurs compte de cela (voir Figure).

Mais parfois, l'éditeur vous signalera une erreur « Couldn't resolve reference to … ». Ce problème est inhérent au mécanisme de portée (ou scoping). Par exemple, tel que le modèle du domaine WDL est défini, il existe 2 portées distinctes : la portée Website et la portée Page. Cela signifie que ce qui est défini au niveau du Website est possiblement invisible depuis la portée de la page, et vice-versa. Dans ce cas, il faudra corriger le problème dans le package scoping.

Ce problème de portée ne se produira pas si vous respectez une convention Xtext simple mais contraignante : l'attribut servant aux cross-references doit impérativement s'appeler 'name'.

IV-B - Mécanisme de validation

Le gros bénéfice d'avoir intégré des contraintes OCL en amont dans le métamodèle est qu'elles se retrouvent nativement gérées par le plugin, en plus d'être simples à écrire. Pour le reste, il vous faut les coder manuellement dans le paquetage validation. Ci-dessous, j'ai choisi de présenter un avertissement (warning) lorsque le titre d'une page fait moins de 4 caractères. Selon le niveau de sévérité du problème, on pourrait également choisir error ou info.

Méthode de validation du titre des pages web

import org.eclipse.xtext.validation.Check
import website.*

class WDLValidator extends AbstractWDLValidator {

public static val SHORT_TITLE = 'short_title'

@Check
def checkTitleLength(Page p) {
if (p.title.length < 4) {
warning('Ce titre est un peu court pour une page web',  WebsitePackage.Literals.PAGE__TITLE,  SHORT_TITLE)
}
}

}

Pour que l'expérience utilisateur soit optimale, une suite possible à une violation d'une contrainte consiste à proposer un correctif automatique (appelé « Quick Fix » dans le jargon). Ceci dépasse le cadre de ce tutoriel, mais sachez que tout se passe dans le paquetage ui.quickfix.

V - Projection vers les langages du web

Comme cela a été évoqué en introduction, l'étape finale pour donner un réel intérêt à votre DSL consiste à projeter (i.e. transformer) un code WDL vers les langages reconnus du web, à savoir HTML/CSS/JS.

Il existe des technologies pour faciliter cette transformation, et qui s'intègrent parfaitement à EMF, comme Acceleo ou JET. Si vous êtes courageux, vous pouvez tout à fait écrire cette transformation directement en pur Java EMF, c'est-à-dire parcourir les nœuds de l'AST et écrire dans des fichiers de sorties (*.html, *.css). Mais le plus rapide reste d'utiliser la fonctionnalité de génération à la volée intégrée à Xtext. Dans ce cas, votre code de transformation doit être écrit en Xtend.

V-A - Transformation

Cette étape est brièvement abordée dans ce tutoriel. Mais comprenez que chaque transformation que vous allez écrire embarque des choix importants de conception. Pour notre exemple, j'avais dans l'idée que :

- Le mot-clé mobile implique la génération d'un unique document HTML multipages exploitant le framework jQuery Mobile alors que son absence implique la génération d'un document html pour chaque page, et d'une feuille de style css par défaut ;

- Le mot-clé copyright implique la génération d'un pied de page auquel sera automatiquement ajouté le sigle © et l'année en cours ;

- Dans le cas d'un site non-mobile, l'attribut 'Id' des pages donnera son nom aux fichiers html générés, et l'attribut title donnera leurs titres (balise <title>). Dans le cas d'un site jQuery Mobile, l'attribut 'Id' donnera les identifiants des blocs (<div data-role=''page''>) tandis que l'attribut 'title' donnera le texte des entêtes de ces blocs ;

- Le mot-clé menu implique la génération d'une barre de navigation en jQuery Mobile pour la navigation entre les pages, ou un menu horizontal avec quelques effets plaisants (css+javascript) dans le cas d'un site web non mobile ;

- Des textes fictifs (les fameux « Lorem Ipsum ») viendront temporairement combler certaines parties.

V-B - Code de transformation

Le paquetage generator permet de gérer la transformation écrite en Xtend. Vous avez à votre disposition d'un coté la ressource en entrée (c-a-d l'AST) et de l'autre un accès au système de fichier pour la sortie (c-a-d les fichiers générés). Voici un extrait de code de génération dans le cas d'un site web non mobile.

Extrait du code de génération

package com.univpau.generator

import org.eclipse.emf.ecore.resource.Resource
import org.eclipse.xtext.generator.IGenerator
import org.eclipse.xtext.generator.IFileSystemAccess
import website.Website
import website.Page
import java.util.Calendar
import java.util.GregorianCalendar

class WDLGenerator implements IGenerator {

override void doGenerate(Resource resource, IFileSystemAccess fsa) {

val websiteObject = resource.allContents.toIterable.filter(typeof(Website)).head

val fullCopyright = new GregorianCalendar().get(Calendar.YEAR) + ''' &copy; ''' + websiteObject.copyright
val isMobile = websiteObject.isMobileFriendly

if (!isMobile) {

fsa.generateFile("screen.css", css)

for(Page p : resource.allContents.toIterable.filter(typeof(Page))) {
fsa.generateFile(p.name + ".html", '''
<html>
<head>
<link rel="stylesheet" type="text/css" href="screen.css">
<title>''' + p.title + '''</title></head>
<body>''' + p.menu + '''<p>''' + lorem + '''</p>
<hr/>
<footer>''' + fullCopyright + '''</footer>
</body>
</html>''')
}
} else {
// code de génération jQuery Mobile
}
}

def getMenu(Page p)
'''<ul id="menu_horizontal">
«FOR cible : p.targets »
<li><a href="./«cible.name».html"> «cible.title» </a></li>
«ENDFOR»
</ul>
'''

def css() '''/* Je sélectionne les <li> du menu horizontal */
ul#menu_horizontal li {
display : inline;
padding : 0 0.5em; /* Pour espacer les boutons entre eux */
}
a:hover { color:red; }
ul#menu_horizontal {
list-style-type : none; /* Car sinon les puces se placent n'importe où */
}
'''

def lorem() '''Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum'''

}

La génération est effectuée automatiquement lorsque l'utilisateur final enregistre son fichier .wdl après y avoir apporté des modifications. Les fichiers générés apparaissent dans le répertoire par défaut /src-gen du projet courant de l'utilisateur.

VI - Remerciements

Je tiens à remercier Gaëtan Deltombe pour son aide sur la partie scoping, Alain Bernard pour ses remarques pertinentes sur les cross-references, Mickael Baron pour sa relecture générale et enfin Cédric Duprez pour la relecture orthographique.

(1)Xtext est donc lui-même un DSL