Des portlets à la sauce Ember.js

Le revival du Javascript

ember-logoIl y a une poignée d’années, prononcer le mot Javascript face à un développeur JavaEE, c’était comme parler de la valeur du ticket restaurant lors d’un premier entretien d’embauche… Juste un truc pas complètement à côté de la plaque, mais plutôt à éviter, un aspect de la réalisation qu’il fallait mieux cacher comme la poussière qu’on pousse sous le tapis…

Et puis, progressivement, une révolution a eu lieu : le HTML5 s’est précisé, l’outillage s’est raffiné (merci Firebug et Chrome !) et surtout des frameworks de plus en plus puissants et faciles à utiliser ont fleuri (en commençant par JQuery), permettant d’exploiter à fond les capacités impressionnantes de ce langage. Et petit à petit, les technologies Front ont retrouvé grâce auprès des développeurs JavaEE.

Dernier acte à ce jour, la parution de plusieurs solutions dites de MVC javascript, telles que BackBone.js (qui fait déjà figure d’ancêtre !), Angular.js et Ember.js (pour ne citer que les plus populaires).

Ember.js : mon petit préféré…

Dans ce post, nous nous intéresserons uniquement à Ember.js, qui est à mon goût, la solution la plus complète et la plus “élégante” (en tout cas de celles que j’ai pu aborder…)

Ember est un framework Javascript issu du projet SproutCore, grandement utilisé par Apple notamment pour ses applications Web. Contrairement à Backbone.js, Ember est directement conçu pour être couplé à Handlebars pour les aspects templating, et offre immédiatement des possibilités de binding bi-directionnelles. Il est dépendant de JQuery.

Son apprentissage est relativement rapide, grâce à un site Web plutôt didactique et à une bonne documentation de l’API (en tout cas, tant qu’on ne touche pas à Ember-data qui fera sûrement l’objet d’un autre post…). Je recommande cependant l’excellent article d’Andy Matthews (http://www.adobe.com/devnet/html5/articles/flame-on-a-beginners-guide-to-emberjs.html) pour une première plongée. C’est clairement cet article qui m’a permis d’écrire le mien ! Il y a également un très bon tutoriel vidéo directement disponible sur le site.

Étant un amateur de portail (j’ai prononcé un gros mot ?), je me suis dit qu’il serait intéressant de voir dans quelle mesure il pourrait être possible d’utiliser Ember dans le cadre particulier d’une portlet. Il n’y a rien d’évident à cela, sachant que ce type de framework est généralement conçu pour manipuler une page Web, alors même qu’une portlet ne produira qu’un fragment de la page… La cible retenue est l’inusable Liferay.

Maven et Liferay : c’est possible !

Depuis la version 6.1 du portail Liferay, il est possible d’abandonner le plugin sdk de l’éditeur pour une structure plus classique de projet Maven. Liferay publie en effet à la fois les artefacts du portail et les plugins de construction ou de déploiement sur des repositories publics.

Pour aller plus loin, vous aurez donc besoin :

Pour plus de simplicité, il est préférable de définir au préalable un profile spécifique à l’installation locale de son portail Liferay dans le fichier settings.xml du répertoire .m2. Ce profile est du type :

<profile> 
    <id>emberlf</id> 
    <properties> 
        <liferay.version>6.1.1</liferay.version>
        <liferay.auto.deploy.dir>%path to liferay bundle%/Liferay6.1/liferay-portal-6.1.1-ce-ga2/deploy</liferay.auto.deploy.dir> 
    </properties>
</profile>

Démarrer votre portail et lancer la création du squelette de projet de portlet par la commande suivante :

mvn archetype:generate -DarchetypeArtifactId=liferay-portlet-archetype 
    -DarchetypeGroupId=com.liferay.maven.archetypes 
    -DarchetypeVersion=6.1.0 -DgroupId=fr.ippon.liferay.ember 
    -DartifactId=ember-portlet -Dversion=1.0-SNAPSHOT 
    -DinteractiveMode=false

Charger le projet dans votre éditeur, et vous avez alors une magnifique portlet qui ne fait rien… On peut même la déployer sur le portail à l’aide d’une commande :

 mvn -Pemberlf package liferay:deploy

Ah si, elle affiche “This is the ember-portlet.”.

Le reste de cet article va vous montrer comment faire rapidement bien mieux !

Intégration d’Ember à la portlet

Avant toute chose, il faut bien entendu récupérer la distribution d’Ember.js et les frameworks associés. Cet article se base sur la version 1.0.0-rc3.

La première chose à faire est de recopier les ressources ember dans le projet

  • ember-1.0.0-rc.3.js, handlebars-1.0.0-rc3.js et jquery-1.9.1.min.js dans le répertoire ember-portlet/src/main/webapp/js

Une fois ces ressources copiées, il convient de les déclarer à la portlet. Cette déclaration s’effectue au travers du fichier liferay-portlet.xml en y ajoutant les lignes :

 <footer-portlet-javascript>/js/jquery-1.9.1.min.js</footer-portlet-javascript>
 <footer-portlet-javascript>/js/handlebars.js</footer-portlet-javascript>
 <footer-portlet-javascript>/js/ember-1.0.0-rc.1.js</footer-portlet-javascript>

On va également ajouter dans le fichier main.js déjà présent (créé par défaut par Liferay), la déclaration de l’application Ember.js, plus quelques petites subtilités permettant de valider le fonctionnement d’Ember.js :

 App = Ember.Application.create({ 
     rootElement: $('section#portlet_emberportlet_WAR_emberportlet') });
 App.ApplicationView = Ember.View.extend({ 
     click: function(evt) { 
         alert("Well done ! En plein sur la View Ember de la portlet !"); 
         } 
  });
  App.ApplicationController = Ember.Controller.extend({ 
      templateName: 'Ember portlet', 
      people: [ {firstName: "Kitien", lastName: "Laroutte"}, 
      {firstName: "Sophie", lastName: "Fonsec"}, 
      {firstName: "Igor", lastName: "Saherseize"} ] 
   });

Le point le plus important (et peu documenté…) est que l’on initialise l’application en lui passant un ‘rootElement’ correspondant au contenu de la portlet. Cela permet de limiter et de positionner les actions sur le DOM effectuées par le framework, qui est ainsi restreint au fragment de page servi par la portlet.

Le reste est assez standard :

  • On crée une vue, dans laquelle on définit un événement sur le clic,
  • On crée un contrôleur qui se contente pour le moment de définir des données pour affichage dans la JSP…

Rien de très dynamique pour le moment.

vous pouvez alors modifier votre fichier view.jsp de la façon suivante :

 <%@ taglib uri="http://java.sun.com/portlet_2_0" prefix="portlet" %> 
 <portlet:defineObjects /> 
 <script type="text/x-handlebars"> 
 <h3>{{templateName}} </h3> 
 <ul> 
 {{#each people}} 
     <li>Hello {{firstName}}</li> 
 {{/each}}
 </ul> 
 </script>

Après déploiement de la portlet par un mvn -Pember-liferay clean package liferay:deploy et placement dans une page du portail, on obtient la vue de la liste des personnes déclarées précédemment.

Et un clic sur le fragment de page affichant la portlet produit l’affichage d’une boîte d’alerte.

Il est maintenant temps d’aller un cran plus loin !

Service Liferay et JSON

Liferay est un portail offrant une architecture de services particulièrement ouverte. On bénéficie ainsi de nombreux services SOAP ou REST/JSon permettant de manipuler les principaux objets créés au travers du “service builder”.

Pour consulter (et tester !) ces services JSON, il suffit de se rendre sur la page suivante de son portail local :

http://localhost:8080/api/jsonws

ember-logo

L’appel suivant, une fois les valeurs de repositoryId et de token d’authentification fixés permet par exemple de récupérer les fichiers stockés dans le répertoire de la bibliothèque :

http://localhost:8080/api/secure/jsonws/dlapp/get-file-entries?repositoryId=&folderId=0&p_auth=

On va donc mettre à profit cette capacité pour construire une petite interface permettant de lister les répertoires de la bibliothèque Liferay et les fichiers qu’ils contiennent.

C’est beau la mécanique Ember !

Dans toute application Ember.js, l’essentiel de l’interface et de la cinématique applicative est réparti entre les fichiers HTML et Javascript. Les deux sont intimement liés et doivent normalement s’aborder simultanément, ce qui est assez difficile dans le cas d’un post de Blog, vous en conviendrez… On va donc commencer par s’intéresser à la partir JSP de notre portlet. Pour cela, il convient de remplacer le contenu de view.jsp par le code suivant :

 <%@ page import="com.liferay.portlet.documentlibrary.model.DLFolderConstants" %> 
 <%@ page import="com.liferay.portal.kernel.util.WebKeys" %> 
 <%@ page import="com.liferay.portal.theme.ThemeDisplay" %> 
 <%@ page import="com.liferay.portal.security.auth.AuthTokenUtil" %> 
 <%@ taglib uri="http://java.sun.com/portlet_2_0" prefix="portlet" %> 
 <%@ taglib prefix="liferay-portlet" uri="http://liferay.com/tld/portlet" %> 
 <portlet:defineObjects /> 
 
 <% String auth= AuthTokenUtil.getToken(request); 
 ThemeDisplay themeDisplay = (ThemeDisplay) request.getAttribute(WebKeys.THEME_DISPLAY); long defaultRepoId = DLFolderConstants.getDataRepositoryId(themeDisplay.getScopeGroupId(), DLFolderConstants.DEFAULT_PARENT_FOLDER_ID); %> 
 
 <script language="javascript"> 
     var repoId = <%=defaultRepoId%>; 
 </script> 
 <script type="text/x-handlebars"> 
     <div id="selector"> 
         <div id="file-entries-frm"> 
             <b>Select File Directory: </b> {{view Ember.Select contentBinding="App.fileDirectoriesSelector.content" optionLabelPath="content.folderName" optionValuePath="content.id" valueBinding="App.fileDirectoriesSelector.selectedFolderId" prompt="Select a folder :" }} 
         </div> 
     </div> 
     <hr/> 
         <div id="filelist"> {{view App.FileListView}} </div> 
</script> 
<script type="text/x-handlebars" data-template-name="filelist"> 
    <div id="file-entries-content"> 
        <div id="file-entries"> 
            <ul> {{#each App.fileEntriesController}} <li> <span>{{size}} octets</span> <h3>{{name}}-{{title}}</h3> <p>{{description}}</p> </li> {{/each}} </ul> 
        </div> 
    </div>
</script>

Évidemment, ce code, même assez concis mérite quelques explications. Explorons le ligne par ligne :

  • Lignes 11 à 19 : Un premier point à résoudre, purement Liferay, consiste à récupérer l’identifiant du repository par défaut du portail.On doit pouvoir faire mieux, mais cette partie de la JSP n’existe que pour cela…
  • Lignes 21 : On pose le template d’affichage de la vue Ember. Ce template ne dispose pas d’attribut data_template_name (on peut aussi fixer cette attribut à “application”).
  • Ligne 25 : Introduction d’un sélecteur Ember avec toute la magie de ce framework ! Ainsi on binde non seulement les valeurs affichées sur une variable Javascript (contentBinding), mais également le résultat d’une action de sélection (valueBinding). Tout cela sera plus clair en analysant le fichier Javascript
  • Ligne 35 : On intègre la vue définie par App.FileListView. On verra que cette vue s’appuie sur le template défini en ligne 40
  • Ligne 45 : On utilise les capacités de boucle du framework pour afficher autant de blocs HTML qu’il y a de fichiers présents dans le répertoire sélectionné. Libre à vous d’afficher d’autres informations, liées à l’objet App.FileEntry

Comme déjà précisé plus haut, le seul fichier JSP est largement insuffisant pour comprendre la mécanique Ember, l’essentiel de l’application étant dans le fichier javascript. Pour cela, on va modifier le fichier main.js pour qu’il contienne le code suivant :

 /***************************Application**************************
 / var App; App = Ember.Application.create({ rootElement: $('section#portlet_emberportlet_WAR_emberportlet'), LOG_TRANSITIONS: true });
 /***************************Models**************************/ 
 App.FileEntry = Em.Object.extend(
     { companyId: null, createDate: null, custom1ImageId: null, custom2ImageId: null, description: null, extension: null, extraSettings: null, fileEntryId: null, fileEntryTypeId: null, folderId: null, groupId: null, largeImageId: null, mimeType: null, modifiedDate: null, name: null, readCount: null, repositoryId: null, size: null, smallImageId: null, title: null, userId: null, userName: null, uuid: null, version: null, versionUserId: null, versionUserName: null }
     ); 
/************************** * Views **************************/ 
App.FileListView = Ember.View.extend(
{ templateName: 'filelist', classNames: ['filelist'] }
); 
App.FileDirectoriesSelector = Ember.Object.extend({ selectedFolderName: "", selectedFolderId: -1, selectedFolderIdChanged: function(){ App.fileEntriesController.loadFileEntriesBySelect();}.observes('selectedFolderId') }); App.fileDirectoriesSelector = App.FileDirectoriesSelector.create({ content: getFolders() }); function getFolders() { var url = 'http://localhost:8080/api/secure/jsonws/dlapp/get-folders/repository-id/%@/parent-folder-id/0?p_auth=%@'.fmt(repoId,Liferay.authToken); // Push Default folder by default folders = Ember.ArrayProxy.create({content:[Ember.Object.create({folderName: "Root Folder", id: 0})]}); $.getJSON(url,function(data){ $(data).each(function(index,value){ folders.addObject(Ember.Object.create({folderName: value.name, id: value.folderId})); }) }); return folders; } 
/************************** * Controllers **************************/ 
App.fileEntriesController = Em.ArrayController.create({ content: [], loadFileEntriesBySelect: function() { var me = this; if (App.fileDirectoriesSelector.selectedFolderId != null) { var url = 'http://localhost:8080/api/secure/jsonws/dlapp/get-file-entries?repositoryId=%@'.fmt(repoId); url += '&folderId=%@&p_auth=%@'.fmt(App.fileDirectoriesSelector.selectedFolderId, Liferay.authToken); $.getJSON(url,function(data){ me.set('content', []); $(data).each(function(index,value){ var t = App.FileEntry.create({ description: value.description, extension: value.extension, largeImageId: value.largeImageId, mimeType: value.mimeType, modifiedDate: value.modifiedDate, name: value.name, readCount: value.readCount, size: value.size, smallImageId: value.smallImageId, title: value.title, userId: value.userId, userName: value.userName }); me.pushObject(t); }) }); } } });

Voici quelques explications concernant le contenu du fichier javascript :

  • Lignes 6 : On déclare l’application Ember. Le premier paramètre permet de restreindre la portée du framework à la section définissant la portlet, le second est optionnel et permet de logguer les transitions dans la console Javascript (peu utile dans notre cas où l’unique transition amène sur la page par défaut).
  • Ligne 14 : On définit un objet Ember.js qui est le symétrique de l’objet manipulé par Liferay pour décrire les FileEntry. J’y ai mis tous les attributs, même si la plupart d’entre eux ne sont pas utilisés ici.
  • Ligne 46 : La vue intégrée dans le template par défaut est définie et pointe vers le template du même nom. Ember permet de faire plus court en utilisant des conventions de nommage. Mais pour être plus clair, j’ai préféré faire de la sorte.
  • Ligne 51 à 58 : On déclare ici un objet qui va jouer un rôle prépondérant dans le fonctionnement de l’application. Il s’agit en effet de la vue javascript du sélecteur de répertoire affiché dans le template. Cette déclaration se fait en deux temps : par extend() puis par create, car on utilise un ‘observateur’ sur la variable selectedFolderId qui ne peut porter que sur un objet étendu (Attention, cette information est très mal documentée et n’est valable que sur les distributions récentes d’Ember.js). A noter également que la variable content s’appuie sur le retour d’une fonction
  • Ligne 62 : Cette fonction s’appuie sur les services REST de Liferay et sur JQuery pour construire la liste des répertoires présents dans la bibliothèque Liferay de premier niveau.
  • Ligne 71 : On crée un contrôleur Ember.js qui va contenir le résultat de l’appel aux services REST Liferay listant les fichiers présents dans le répertoire passé en argument. Ce contrôleur pourra ensuite être exploité dans le template d’affichage filelist

En synthèse, selectedFolderId est directement lié à la valeur du sélecteur HTML et toute modification de sa valeur provoquera un appel à la fonction liée à la propriété selecteFolderIdChanged. Elégant, non ?

Pour teminer et uniquement pour des aspects purement esthétiques, on va utiliser le fichier main.css suivant, qui va décorer les boites correspondant à chacun des fichiers trouvés :

#p_p_id_emberportlet_WAR_emberportlet_ { font-family: 'tahoma', sans-serif; background: #eeeeee; } #file-entries-frm { margin: 0 auto 15px auto; text-align: center; } #file-entries-content { border: 1px solid blue; margin: 0 auto; } #file-entries ul { padding: 0; border: 1px solid #999999; } #file-entries ul li { text-align: left; margin: 0; padding: 10px; list-style: none; border-bottom: 1px solid #999999; min-height: 50px; background: #eeeeee; /* Old browsers */ background: -moz-linear-gradient(top, #eeeeee 0%, #dddddd 100%); /* FF3.6+ */ background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#eeeeee), color-stop(100%,#dddddd)); /* Chrome,Safari4+ */ background: -webkit-linear-gradient(top, #eeeeee 0%,#dddddd 100%); /* Chrome10+,Safari5.1+ */ background: -o-linear-gradient(top, #eeeeee 0%,#dddddd 100%); /* Opera 11.10+ */ background: -ms-linear-gradient(top, #eeeeee 0%,#dddddd 100%); /* IE10+ */ background: linear-gradient(top, #eeeeee 0%,#dddddd 100%); /* W3C */ filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#eeeeee', endColorstr='#dddddd',GradientType=0 ); /* IE6-9 */ } #file-entries ul li:hover { background: #cfe7fa; /* Old browsers */ background: -moz-linear-gradient(top, #cfe7fa 0%, #6393c1 100%); /* FF3.6+ */ background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#cfe7fa), color-stop(100%,#6393c1)); /* Chrome,Safari4+ */ background: -webkit-linear-gradient(top, #cfe7fa 0%,#6393c1 100%); /* Chrome10+,Safari5.1+ */ background: -o-linear-gradient(top, #cfe7fa 0%,#6393c1 100%); /* Opera 11.10+ */ background: -ms-linear-gradient(top, #cfe7fa 0%,#6393c1 100%); /* IE10+ */ background: linear-gradient(top, #cfe7fa 0%,#6393c1 100%); /* W3C */ filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#cfe7fa', endColorstr='#6393c1',GradientType=0 ); /* IE6-9 */ } #file-entries ul li span { float: right; font-size: 8pt; color: #888888; } #file-entries ul li img { float: left; margin: 0 10px 0 0; width: 48px; height: 48px; } #file-entries ul li h3 { font-size: 11pt; margin: 0; } #file-entries ul li p { margin: 0; font-size: 10pt; }

Lancement de la compilation et du déploiement

Reste maintenant à compiler et déployer le tout, par une commande du type :

mvn -Pemberlf package liferay:deploy

ATTENTION, la portlet est relativement simpliste et ne fonctionne que pour un utilisateur loggué. Donc, placez la sur une page secondaire et identifiez-vous avant d’y accéder.
Pour que la démonstration soit pertinente, il faut également que la bibliothèque contiennent des répertoires et des documents, histoire d’avoir des choses à afficher !

Vous devez dorénavant obtenir une portlet du type :
ember-logo

Pour information, voici la vue Back-office standard de Liferay sur ma bibliothèque de document, pour le même répertoire :
ember-logo

En moins de 200 lignes de code et avec une lisibilité très correct, on arrive donc à mettre en place un fonctionnement AJAX évolué sans gros effort. C’est beau le Javascript, non ?