[Update] - Ecriture d'un DSL groovy pour exoplatform Web Content Management (WCM)
Introduction
Un DSL (Domain Specific Language) est un langage spécifique à un domaine particulier, limité et utilisant une syntaxe naturelle.
En informatique, celui-ci facilite la communication entre les experts métiers et les développeurs.
Quelques exemples de DSL du monde informatique :
- SQL,
- HTML,
- RegExp,
- Syntaxe Cron (unix)...
Un DSL ajoute un niveau d’abstraction à la réalité du métier afin de rendre le langage plus léger à utiliser et manipulable par des personnes qui ne connaissent pas forcément l’implémentation réelle.
Je travaille actuellement sur un site internet ou des personnes vont contribuer à son contenu (actualités, faq, articles...).
Pour ce faire j’utilise «eXoplatform Web Content Management (WCM)»:http://www.exoplatform.com/
Chaque type de contenu (actualités, faq) est stocké dans le JCR (Java Content Repository), et pour permettre l’ajout, édition et visualisation de chaque contenu, je dois écrire un formulaire permettant de modifier celui-ci.
Le language (WebUI) est proposé par eXo pour écrire ces formulaires, mais l'écriture d’un formulaire avec celui-ci donne un mélange de code html/groovy/js/css qui est difficile à écrire et encore plus à maintenir. J’ai écrit un DSL permettant de simplifier l'écriture de ces formulaires. La méthode utilisée est présentée dans la suite de cet article.
Cahier des charges
Implémenter les composants suivants :
- form
- inputText (mode text, textarea, calendar, wysiwyg)
- Boutons de soumission/reset du formulaire
La syntaxe doit etre proche de celle habituellement utilisée pour coder des ihm en json
ex :
monFormulaire{
maTextBox(nom : "firstName", required: true)
}
Une validation stricte du contenu du DSL doit etre effectuée, si celui-ci n’est pas valide, des erreurs lisibles doivent etre remontées à l’utilisateur (exceptions, logs).
Simplifications apportées par l’utilisation du DSL
Un formulaire d'édition de noeud avec webui (dialog) ou le DSL doit effectuer :
- Récupérer le noeud courant
- Ouvrir et fermer un formulaire
- Ajouter au formulaire n composants de saisie
- Ajouter au formulaire des boutons de validation/annulation
- Ajouter/générer le code html nécessaire à la mise en page du formulaire
Récupération du noeud courant
Quelque soit l’utilisation, DSL ou webui, le noeud courant est disponible via la variable uicomponent.
Ouverture et fermeture du formulaire
Ouvrir un formulaire avec webui
Nous voyons ci dessous que nous devons mélanger syntaxe groovy et code html
<% uiform.begin() %>
<div class="HorizontalLayout">
...
<table class="UIFormGrid">
</table>
</div>
<% uiform.end() %>
Ouvrir un formulaire avec le DSL
Avec le DSL, le code est beaucoup plus succint :
form{
}
Ajouter au formulaire n composants de saisie
version webui
Avec le code ci dessous, nous voyons que pour chaque entrée de notre formulaire, nous devrons créer une nouvelle ligne dans le tableau englobant le formulaire, récupérer le label dans le bundle et ensuite ajouter à uicomponent (le noeud correspondant à notre formulaire,) le composant webui a utiliser
<tr>
<td class="FieldLabel"><%=_ctx.appRes("Article.dialog.label.title")></td>
<td class="FieldComponent">
<%
String[] fieldTitle = ["jcrPath=/node/exo:title", "validate=empty"] ;
uicomponent.addTextField("title", fieldTitle) ;
%>
</td>
</tr>
Version DSL
N’ayant pas à nous soucier du code html, nous obtenons également quelque chose de plus concis :
formItem ( name: 'title', label : 'title', type: 'text', validate: 'empty' , jcrPath: "/node/exo:title")
Ajouter au formulaire des boutons de validation/annulation
version webui
Le code ci dessous va servir à générer un bouton valider et annuler.
<div class="UIAction">
<table class="ActionContainer">
<tbody><tr>
<td>
<% for(action in uicomponent.getActions()) {
String actionLabel = _ctx.appRes(uicomponent.getName() + ".action." + action);
String link = uicomponent.event(action) ;
%>
<div onclick="$link" class="ActionButton LightBlueStyle">
<div class="ButtonLeft">
<div class="ButtonRight">
<div class="ButtonMiddle">
<a href="javascript:void(0);">$actionLabel</a>
</div>
</div>
</div>
</div>
<%}%>
</td>
</tr>
</tbody></table>
Version DSL
Avec le DSL, pas besoin d’ajouter les boutons, un formulaire contient systèmatiquement ces boutons, le DSL ajouter ces boutons automatiquement à la fin du formulaire.
Génération du code html
Le but du DSL est de simplifier la travail des développeurs en simplifiant, voir en simplifiant toutes les étapes qui sont inutiles et redondantes. C’est à ce titre que j’ai souhaité que le builder génère le code html
Création du DSL avec groovy
Le DSL est le vocabulaire manipulé par l’utilisateur. Pour implémenter un DSL nous devons utiliser en groovy un builder.
Il existe différentes possibilités pour créer un builder. Pour définir la syntaxe, il faut :
- Tout faire à la main et récupérer les appels au méthodes et propriétés manquantes en utilisant methodMissing() et propertyMissing().
- Etendre la classe BuilderSupport, et implémenter les méthodes suivantes selon le contenu du DSL :
- createNode(Object name, Map attributes, Object value)
- createNode(Object name, Map attributes)
- createNode(Object name)
- Dernière méthode, utiliser le metaBuilder.
Présentation du metaBuilder et utilisation
Le metabuilder]http://groovytools.sourceforge.net/builder/ est un builder de builder.
Afin d'écrire un builder avec notre metabuilder, la première étape est d'écrire le schéma de notre DSL
form(factory: Form) {
properties{
}
//Form must contain at least one item of formItem
collections {
formItemList(min: 1) {
//form contains an object list formItem, schema defined bellow
formItem(schema: 'formItem')
}
}
}
Dans le code ci-dessus, nous déclarons que notre élément principal est le form. La factory, ou classe chargée de son implémentation sera la classe Form.
Notre form devra au moins contenir une collection de 1 éléménent de formItemList.
Le schéma de formItem est défini ci-dessous :
formItem(factory: FormItem) {
properties {
name(req: true)
label(req: true)
type(req: true, check: ['text','textarea', 'password', 'calendar', 'wysiwyg'])
validate(req: false, check: ['not-empty', 'name', 'email', 'number', 'empty', 'datetime'])
jcrPath(req: false)
visible(req: false, check: [true, false], def: true)
editable(req: false, check: [true, false, 'if-not-null', 'if-null'], def: true)
}
}
}
La factory de notre formItem est FormItem.
Cet élement contiendra les attributs suivants :
- name (obligatoire),
- label (obligatoire),
- type (obligatoire), les valeurs possibles sont : text, textarea, calendar, password ou calendar
- jcrPath (non obligatoire),
- visible (non obligatoire), les valeurs possibles sont true ou false
- editable (non obligatoire), les valeurs possibles sont true, false, if-not-null, if-null
Si le vocabulaire utilisé par l’utlisateur du DSL ne repecte pas ce schéma, des erreurs seront remontées à l’utilisateur (le développeur).
Nous utiliserons le DSL de la facon suivante :
import org.webuibuilder.*
WebUIBuilder wb = new WebUIBuilder(uicomponent)
wb.build{
form{
formItem ( name: 'title', label : 'title', type: 'text', validate: 'empty' , jcrPath: "/node/exo:title")
formItem ( name: 'summary', label : 'summary', type: 'text' , jcrPath: "/node/exo:summary")
formItem ( name: 'content', label : 'content', type: 'wysiwyg' , jcrPath: "/node/exo:text")
}
}
Ce code remplace le contenu du template groovy d'édition du node type exo:article.
Le template groovy se trouve dans : Drive «administration de WCM», path : /exo:ecm/templates/exo:article/dialogs/dialog1.gtmpl
uicomponent est l'élément englobant le formulaire webui, celui-ci est valorisé par le portail. Il sera passé au constructeur de notre builder.
Nous appelons ensuite la méthode build de notre builder dont le code est le suivant
def build(Closure closure){
MetaBuilder mb
Form f = mb.build(closure)
f.processRender out, uiForm
}
Nous passons le code saisi par notre développeur, à l’aide du DSL, à la méthode build de notre metaBuilder.
Notre metabuilder apelle les factory nécessaires à la constitution de notre formulaire (Form et FormItem) autant de fois que nécessaire.
Nous obtenons finalement une instance de Form valorisée, contenant une collection de FormItem.
Nous appelons ensuite la méthode processRender (JSF like :-) ) en lui passant en paramètre out (printerWriter chargé de renvoyé le code html généré au client web, et uiform qui est notre instance de formulaire.
Dans le code du form, nous appelerons de la meme facon la méthode processRender sur les enfants.
Le code de la factory Form est le suivant :
void processRender(WriterPrinter out, uicomponent){
out.write """<div class="UIForm">"""
uicomponent.begin()
out.write """
<div class="HorizontalLayout">
"""
org.webuibuilder.FormItem.processTimestamp (out, uicomponent)
this.formItemList.each{
it.processRender(out, uicomponent, it)
}
out.write """
<table class="UIFormGrid">
</table>"""
//Rendu du bouton
Button.processRender(out, uicomponent)
out.write """</div>"""
uicomponent.end()
out.write "</div>"
}
Le code de la factory FormItem est la suivante :
void processRender(WriterPrinter out, uiForm, FormItem formItem){
List<string> args = new ArrayList<string>();
formItem.metaPropertyValues.each {
if (it.value!=null){
switch (it.name){
//Liste des proprietes prise en compte pour le moment
case ["jcrPath","validate","editable", ]:
args.add("$it.name=$it.value")
break
}
}
}
out.write """
$formItem.label
"""
switch (formItem.type){
case "text":
uiForm.addTextField(formItem.name,formItem.label,args as String[])
break
case "textarea" :
uiForm.addTextAreaField(formItem.name,formItem.label,args as String[])
break
case "calendar":
uiForm.addCalendarField(formItem.name, formItem.label, args as String[])
break
case "wysiwyg":
uiForm.addWYSIWYGField(formItem.name, formItem.label, args as String[])
break
}
out.write """
"""
}
Pour voir le code dans son ensemble, je vous invite à aller voir sur «github»:http://github.com/dgouyette/webuibuilder , des tests unitaires sont fournis avec, ainsi qu’un classe de mock, cela vous aidera a mieux comprendre comment le tout fonctionne ensemble.
Avant
Voici un exemple de code nécessaire pour le formulaire d’un nodetype (exo:article doté de 3 propriétés (title, summary, content)
<% uicomponent.addInterceptor("ecm-explorer/interceptor/PreNodeSaveInterceptor.groovy", "prev") ; %>
<div class="UIForm FormLayout">
<% uiform.begin() %>
<div class="HorizontalLayout">
<table class="UIFormGrid">
<tbody><tr>
<td class="FieldLabel"><%=_ctx.appRes("Article.dialog.label.name")%></td>
<td class="FieldComponent">
<%
String[] fieldName = ["jcrPath=/node", "mixintype=mix:votable,mix:commentable,mix:i18n", "editable=if-null", "validate=empty,name"] ;
uicomponent.addTextField("name", fieldName) ;
%>
</td>
</tr>
<tr>
<td class="FieldLabel"><%=_ctx.appRes("Article.dialog.label.title")%></td>
<td class="FieldComponent">
<%
String[] fieldTitle = ["jcrPath=/node/exo:title", "validate=empty"] ;
uicomponent.addTextField("title", fieldTitle) ;
%>
</td>
</tr>
<tr>
<td class="FieldLabel"><%=_ctx.appRes("Article.dialog.label.summary")%></td>
<td class="FieldComponent">
<div class="UIFCKEditor">
<%
String[] fieldSummary = ["jcrPath=/node/exo:summary", "options=basic", ""] ;
uicomponent.addWYSIWYGField("summary", fieldSummary) ;
%>
</div>
</td>
</tr>
<tr>
<td class="FieldLabel"><%=_ctx.appRes("Article.dialog.label.content")%></td>
<td class="FieldComponent">
<div class="UIFCKEditor">
<%
String[] fieldContent = ["jcrPath=/node/exo:text", "options=default", ""] ;
uicomponent.addWYSIWYGField("content", fieldContent) ;
%>
</div>
</td>
</tr>
</tbody></table>
<%/* start render action*/%>
<div class="UIAction">
<table class="ActionContainer">
<tbody><tr>
<td>
<% for(action in uicomponent.getActions()) {
String actionLabel = _ctx.appRes(uicomponent.getName() + ".action." + action);
String link = uicomponent.event(action) ;
%>
<div onclick="$link" class="ActionButton LightBlueStyle">
<div class="ButtonLeft">
<div class="ButtonRight">
<div class="ButtonMiddle">
<a href="javascript:void(0);">$actionLabel</a>
</div>
</div>
</div>
</div>
<%}%>
</td>
</tr>
</tbody></table>
</div>
<%/* end render action*/%>
</div>
<%uiform.end()%>
</div>
Après
import org.webuibuilder.*
WebUIBuilder wb = new WebUIBuilder(uicomponent)
wb.build{
form{
formItem ( name: 'title', label : 'title', type: 'text', validate: 'empty' , jcrPath: "/node/exo:title")
formItem ( name: 'summary', label : 'summary', type: 'text' , jcrPath: "/node/exo:summary")
formItem ( name: 'content', label : 'content', type: 'wysiwyg' , jcrPath: "/node/exo:text")
}
}
Besoin de feedback
La version actuelle est une milestone 1. Ce qui signifie que c’est un premier jet et qu’elle ne contient pas encore l’ensemble des fonctionnalités de la version finale.
Merci de tester ce DSL et de me remonter vos besoins
- ajout des mixins,
- ajouter le nodeType au formulaire...
Merci également de me remonter les bugs rencontrés (idéalement avec un TU permettant de reproduire le pb).
Mise en place du DSL sur un serveur d’application
La mise en place sur tomcat et jboss est détaillée sur github et dans le fichier README présent dans le code source.