AngularJS – Java & Moi https://javaetmoi.com Développeur Java, Spring & co, et fier de l'être Sun, 21 Apr 2024 17:49:09 +0000 fr-FR hourly 1 https://wordpress.org/?v=6.9.4 https://javaetmoi.com/wp-content/uploads/2022/05/cropped-java-icon-32x32.png AngularJS – Java & Moi https://javaetmoi.com 32 32 Découvrez les forks de Spring Petclinic https://javaetmoi.com/2016/12/les-forks-de-spring-petclinic/ https://javaetmoi.com/2016/12/les-forks-de-spring-petclinic/#comments Wed, 21 Dec 2016 16:20:08 +0000 http://javaetmoi.com/?p=1657 Continuer la lecture de Découvrez les forks de Spring Petclinic ]]> L’application démo Spring Petclinic a été conçue pour montrer comment le framework Spring peut être utilisé pour développer une application web secondée par une base de données relationnelle. En somme, rien de révolutionnaire. Mais c’est ce qui fait tout son intérêt : présenter une architecture logicielle respectant l’état de l’art d’une application conçue avec Spring.

Avec plus de 2000 forks sur GitHub, la communauté a créé de nombreux forks de Spring Petclinic : Angular, React, REST, Spring Cloud … Afin de fédérer cet engouement, l’organisation GitHub Spring Petclinic a été créé sur GitHub en novembre 2016. La version de référence de Spring Petclinic reste sur https://github.com/spring-projects/spring-petclinic. Les branches et les forks ont basculé sur https://github.com/spring-petclinic.

Ce billet a pour objectif de vous présenter cette récente initiative puis de vous présenter les différents forks d’ores et déjà disponible dans l’organisation Spring Petclinic. Mais avant cela, remontons le temps.

Les origines

D’après une vieille documentation encore en ligne, Spring Petclinic a été initialement développé par Ken Krebs en 2003. A cette époque, la version 1.0 de Spring Framework n’était pas encore releasée (il a fallu attendre mars 2004). La Javadoc @author démontre que le co-fondateur du framework Spring, Juergen Hoeller en personne, a activement contribué à Petclinic.
Les années passèrent. L’application bénéficia des montées de version du framework Spring.
En 2007, Spring Petclinic était distribué avec Spring Framework 2.5 en tant qu’application d’exemple.
Ensuite, pendant 5 ans, l’application n’a plus évolué.

En 2013, Michael Isvy, ex-responsable formation Spring chez Pivotal, Keith Donald et Costin Leau ont fait revivre l’application en la déplaçant sur GitHub et en la migrant vers Spring 3.

A partir de juin 2015, j’ai eu l’honneur de reprendre la coordination technique du projet. Mes contributions principales auront été de proposer une configuration full Java, une version Spring Boot et de migrer l’IHM vers le thème Bootstrap 3 de Pivotal.

Le mois dernier, j’ai passé la main à Dave Syer, qui n’est autre que le papa de Spring Batch, Spring Cloud et de Spring Boot.

L’application Petclinic de référence

Reprenant les rennes, Dave Syer a tout de suite mis sa griffe sur le repo spring-projects/spring-petclinic :

  1. La version Spring Boot est désormais celle de référence. La version Spring Framework est « archivée » dans un fork présenté plus loin.
  2. Java 8 minimum
  3. La couche présentation en JSP est réécrite en Thymeleaf. Le WAR auto-exécutable devient un JAR.
  4. L’architecture applicative est modernisée. La Pull Request #200 « Modularize and migrate to aggregate-oriented domain » présente les changements. La couche service est retirée. Les Controllers dialoguent directement directement avec la couche Repository qui assure désormais la gestion des transactions. L’organisation des packages passe d’un découpage technique (model, repository, service et web) à un découpage métier (owner, vet, visit).

Ce dernier changement d’architecture est le fait le plus marquant. Quelle rupture avec 15 ans de découpage Contrôleur -> Service -> DAO. Afin d’éviter des dépendances circulaires entre packages Java, la conception objet est en quelque sorte dénormalisée. La classe Visit ne référence plus la classe Pet, mais seulement son identifiant.

Maintenue par l’équipe Pivotal, cette version « canonique » de Spring Petclinic est celle à partir desquels les forks pourront être créés. Notons enfin que c’est la version Spring Boot qui est mise en avant. Cela implique qu’une nouvelle application Spring doit donc partir dans la majorité des cas sur du Spring Boot.

Spring Framework Petclinic

L’application spring-petclinic/spring-framework-petclinic  a pour objectif de maintenir une version de Spring Petclinic sans Spring Boot, à l’ancienne, avec de la configuration Spring, de bonnes vielles pages JSP et une architecture 3-tiers.

Comparée à son aînée, cette version présente de nombreux points d’intérêts :

  1. La configuration Spring en XML (branche master) ou en full Java (branch javaconfig) de l’ensemble des couches d’une application web : présentation (Spring MVC, ressources statiques, dépendances JavaScript récupérées avec webjar), service (cache et transaction) et persistance.
  2. 3 implémentations de la couche de persistance : Spring JDBC, Hibernate et Spring Data JPA. Le choix se fait au démarrage de l’application web par l’usage d’un profile Spring.
  3. Des templates de pages et des composants graphiques avec JSP.
  4. L’usage de l’AOP avec l’aspect CallMonitoringAspect
  5. Un support de PostreSQL en plus de MySQL et HSQLDB.

Le fichier README.MD donne les points d’entrée vers les fichiers  de configurer et les classes Java les plus intéressantes.

Spring Petclinic AngularJS

Le fork spring-petclinic/spring-petclinic-angular1 a été créé à partir de la branche angular de l’application de référence, juste avant que celle-ci ne soit supprimée.
Liu Dapeng, Michael Isvy et moi-même en sont les principaux contributeurs.

L’intérêt principal de ce fork est de disposer d’un front-end full JavaScript. Le code Angular JS 1.5 dialogue avec le backend à l’aide d’une API REST propulsée par Spring MVC.
L’intérêt secondaire est de prouver que les mondes JavaScript et Java peuvent parfaitement cohabiter. Le téléchargement et l’exécution des outils front-end gulp, bower, npm et node sont pilotés par Maven à l’aide du frontend-maven-plugin.

L’application est décomposée en 2 modules Maven, un client front-end et une partie serveur Spring Boot :

  1. spring-petclinic-client : ressources statiques (fichiers JavaScript Angular, images, fonts, css) packagées sous forme d’un webjar
  2. spring-petclinic-server : API REST de Spring Petclinic et la page index.html (template Thymeleaf) permettant de référencer les ressources statiques du webjar

Côté serveur, afin d’exposer une API REST au front-end Angular, les contrôleurs Spring MVC ont été convertis en @RestController, renommés pour les besoins de l’API REST et simplifiés car une partie de la logique est désormais traitée dans le navigateur (ex : OwnerResource.java).

La partie front n’a rien à voir avec l’originale en JSP / Java. Elle bascule complètement dans le monde JavaScript (à l’exception d’un peu de Maven).
Le spring-petclinic-client/pom.xml est configuré pour installer Node JS et NPM, récupérer les librairies tierces JS avec bower (cf. bower.json) puis lancer la phase de build avec Gulp (cf. gulpfile.js). Le répertoire target/dist construit par Gulp et les librairies JS sont enfin packagés par Maven sous forme de webjar. A noter que Gulp est configuré pour générer des CSS à partir des fichiers LESS de Petclinic et à minifier JS et CSS.
Le code AngularJS est centralisé dans le répertoire spring-petclinic-client/src/scripts/. L’application Angular est bootstrapée dans le fichier app.js. Le module externe ui-router est chargé de la navigation entre vues. L’organisation de chaque vue se fait sur le même modèle. Voici en exemple celle listant les vétérinaires :

  1. vet-list.component.js : déclaration du composant vetList, du template vet-list.template.html et du contrôleur VetListController
  2. vet-list.controller.js : définition du contrôleur VetListController chargé de faire un appel REST pour récupérer l’objet JSON représentant la liste des vétérinaires.
  3. vet-list.js : configuration ui-router faisant le lien entre l’URL /vets et le template vet-list
  4. vet-list.template.js: template HTML comportant des directives Angular

Spring Petclinic AngularJS montre également l’usage des DevTools. Introduits dans Spring Boot 1.3, ils peuvent remplacer l’usage d’outils comme JRebel ou Spring Loaded.
Le module spring-boot-devtools a été configuré de manière à ce que :

  • la recompilation d’une classe Java déclenche le rechargement du contexte applicatif Spring (qui dure à peine 2 secondes sur mon macbook)
  • une modification de ressources statiques déclenche un rafraichissement de la page dans le navigateur (le plugin LiveReload doit préalablement être installé)

Cette configuration n’est active que pendant la phase de développement. Elle est localisée dans le fichier application-dev.properties qui n’est chargé par Spring Boot que lorsque le profile Spring dev est actif. Dans votre IDE, ajouter l’option ci-dessous au démarrage de la JVM : -Dspring.profiles.active=dev

Spring Petclinic ReactJS

Le projet spring-petclinic/spring-petclinic-reactjs est le 2nd portage de l’application Spring Petclinic vers un front-end full JavaScript de type SPA (Single Page Application), en l’occurrence basé ici sur ReactJS (un framework MVC JavaScript développé par Facebook) et TypeScript (un sur-ensemble de ES6 développé par Microsoft).
Ce fork a été développé par Nils Hartmann, co-auteur d’un livre en allemand sur React et pro Spring Boot. Nils est parti de la version Spring Boot de Spring Petclinic. Pour designer l’API REST, il a récupéré certaines classes de la version AngularJS.

Comparé au fork AngularJS, front-end et backend disposent de leur propre serveur : l’un tournant sur Node.JS et l’autre sous Spring Boot.

La partie front-end est localisée dans le sous-répertoire client. Outre le code TypeScript (TS) et les ressources statiques, on retrouve la configuration d’un certain nombres d’outils JavaScript :

  • NPM tire les dépendances
  • Webpack permet de modulariser le code JavaScript
  • Pendant le développement, Babel transpile à chaud le code TS en JS
  • TSlint est utilisé pour vérifier la qualité du code TS
  • Téléchargement de définitions TS avec Typings

Chaque page de l’application web a été décomposée en composants et sous-composants React. A titre d’exemple, la page OwnersPage.tsx affichant le détail d’un propriétaire est découpée en 2 composants : OwnerInformation.tsx et PetsTable.tsx. La majeure partie du code applicatif se retrouve ainsi dans le répertoire client/src/components dédié aux composants.

La partie serveur se rapproche de celle d’AngularJS. Seule différence majeure : les données envoyées par le client sont validées. Se référer aux classes InvalidRequestException, ApiExceptionHandler, ErrorResource et FieldErrorResource. Bien conçue, cette couche de validation pourra être reportée sur la version AngularJS (cf. issue 7).

Spring Petclinic Microservices

Fondée par Maciej Szarliński, la version microservices de Spring Petclinic est mon coup de cœur du moment : spring-petclinic/spring-petclinic-microservices. Un grand nombre de modules de la stack Spring Cloud y sont mis en œuvre.

Ce fork de la version AngularJS de Spring Petclinic a été décomposée en 3 micro-services fonctionnels : customers, vets et visits. Autonomes, ces micro-services ne communiquent pas ensemble. Au démarrage, ils vont chercher leur configuration auprès du serveur de config (module spring-petclinic-config-server). Par défaut, le serveur de config récupère la configuration depuis le repo GitHub spring-petclinic-microservices-config. Il est possible d’utiliser un repo Git local : -Dspring.profiles.active=local -DGIT_REPO=/projects/spring-petclinic-microservices-config

Les 3 micro-services vont s’enregistrer auprès de l’annuaire de Service (module spring-petclinic-discovery-server) basé sur Eureka. Ils peuvent ainsi être accédés à partir de leur nom de service (ex : http://customers-service/owners/{ownerId}). Leur nom est paramétré dans le fichier bootstrap.yml, au côté de l’URL du serveur de config.

Le front-end Angular n’attaque pas directement les 3 micro-services. Il passe par une API Gateway dont le mécanisme de routage est assuré par Zuul (module spring-petclinic-api-gateway). Cette gateway n’est pas réduite à un simple passe-plat. Elle s’occupe de :

  1. Agréger les réponses renvoyées par plusieurs micro-services avant de les retourner au client (se référer à la classe ApiGatewayController)
  2. Load-balancer les requêtes entre plusieurs instances du même micro-services (annotation @LoadBalanced dans la classe ApiGatewayApplication).

Afin de pouvoir suivre les requêtes HTTP entre plusieurs microservices, un mécanisme de traces distribuées a été mis en œuvre avec Spring Cloud Sleuth. L’interface graphique du serveur Zipkin permet de les consulter.

Enfin, avec pour objectif de simplifier le démarrage de l’ensemble de ces applications (3 microservices + 4 composants d’infra), un fichier docker-compose.yml sera bientôt mis à disposition.

Tableau de synthèse

Le tableau ci-dessous dresse une liste des différentes versions de Spring Petclinic présentant à mes yeux un intérêt majeur :

Appellation Description
Spring Petclinic Version de référence de Spring Petclinic.
Implémentée avec Spring Boot et Thymeleaf.
Spring Framework Petclinic Configuration XML et Java de Spring Framework.
Front-end implémenté en JSP.
3 technologies de persistance : JDBC, JPA et Spring Data JPA.
Spring Petclinic AngularJS Front-end Angular 1 embarqué dans un webjar.
Usage de DevTools.
Spring Petclinic ReactJS Front-end ReactJS délivré par un serveur NodeJS et attaquant l’API REST du back-end implémenté en Spring Boot.
Spring Petclinic Microservices Version distribuée de Spring Petclinic implémentée à l’aide de Spring Cloud : serveur Spring Config, annuaire de services avec Eureka, gestion des logs avec Zipkin et Sleuth, API Gateway avec Zuul, Docker compose …

Conclusion

Dans cet article, j’ai commencé par retracer l’historique de l’application de référence Spring Petclinic qui a fêté son 13ième anniversaire et qui comptabilise plus de 2000 forks. Parmi ces forks, une poignée a intégré la nouvelle organisation Spring Petclinic. On y retrouve des versions front-end basées sur AngularJS et ReactJS, une version distribuée avec des micro-services et du Spring Cloud, une version plus legacy n’utilisant pas Spring Boot mais de la configuration XML ou Java (au choix).

L’organisation Spring Petclinic demande à s’élargir, soit en proposant un nouveau fork (ex : sur Angular 2) soit en contribuant à ceux existants. Toute personne intéressée peut en faire la demande via l’issue Spring Petclinic Organization. Alors : à vos claviers !!

Resources :

]]>
https://javaetmoi.com/2016/12/les-forks-de-spring-petclinic/feed/ 2
Introduction à Angular JS https://javaetmoi.com/2015/05/introduction-a-angularjs/ https://javaetmoi.com/2015/05/introduction-a-angularjs/#respond Wed, 20 May 2015 17:36:21 +0000 http://javaetmoi.com/?p=1316 Continuer la lecture de Introduction à Angular JS ]]> Si vous pensez encore que le data-binding, l’inversion de dépendances, le pattern MVC ou bien encore la gestion de la navigation sont réservés au code Java des applications web modernes, courrez visionner cette présentation.
AngularJS, le dernier framework JavaScript de chez Google, devrait vous surprendre.

Basée sur l’université  AngularJS, ou le futur du développement Web présentée lors de Devoxx France 2013, cette présentation a pour objectif de vous initier à AngularJS.
Les concepts fondamentaux seront mis en en action au travers de l’application Game Store.
Côté industrialisation, nous verrons que tests unitaires, tests fonctionnels et infrastructure de build ne sont pas non plus délaissés.

A l’heure où la couche présentation des applications web est de plus en plus déportée côté client, ce workshop a pour objectif de démystifier ce type de framework permettant de structurer une application JavaScript.

]]>
https://javaetmoi.com/2015/05/introduction-a-angularjs/feed/ 0
14 prises de notes à Devoxx France 2014 https://javaetmoi.com/2014/04/14-prises-de-notes-a-devoxx-france-2014/ https://javaetmoi.com/2014/04/14-prises-de-notes-a-devoxx-france-2014/#respond Wed, 23 Apr 2014 15:28:37 +0000 http://javaetmoi.com/?p=1082 Continuer la lecture de 14 prises de notes à Devoxx France 2014 ]]> En attendant que les vidéos des différentes conférences de l’édition 2014 de Devoxx France soient mises en ligne sur Parleys et en complément des supports déjà mis en ligne par certains Speakers, je mets librement à votre disposition les différentes notes que j’ai pu prendre sur mon laptop.
Les sujets sont variés : de Docker à Angular JS, en passant par Java 8. Certaines pourront être lues de manière autonome ; je pense par exemple au quickie Outils pour manager une équipe et à la conférence 33 things your want to do better. Pour être exploitables en l’état, d’autres notes demanderont à ce que vous ayez assisté à la conférence ou que vous ayez pu récupérer les supports de présentation.

devoxx-france-2014-les-cast-codeurs

Sans plus attendre, voici donc mes 14 notes triées par ordre chronologique :

  1. Java 8 – Streams et Collectors
  2. Créons un web component avec Polymer
  3. JBoss Forge in Action
  4. Un PaaS Java Docker en 30 minutes
  5. Les Applications Réactives
  6. Au secours mon code Angular est pourri
  7. Spring4TW et Boot
  8. Do you really get Classloaders
  9. Outils pour manager une équipe (de développement)
  10. RxJava et Java 8
  11. Google App Engine, çà bouge beaucoup
  12. Deux années de Continuous Delivery au pays des Traders
  13. Bootstrap your productivity with Spring Boot
  14. 33 things your want to do better

Bonne lecture, découverte ou redécouverte et, je l’espère, à l’année prochaine pour Devoxx France 2015 !!

]]>
https://javaetmoi.com/2014/04/14-prises-de-notes-a-devoxx-france-2014/feed/ 0
Comprendre AngularJS en le recodant à Devoxx France 2014 https://javaetmoi.com/2014/04/lab-angularjs-from-scratch-devoxx-france-2014/ https://javaetmoi.com/2014/04/lab-angularjs-from-scratch-devoxx-france-2014/#comments Sat, 19 Apr 2014 08:41:24 +0000 http://javaetmoi.com/?p=1068 Continuer la lecture de Comprendre AngularJS en le recodant à Devoxx France 2014 ]]> Lors de Devoxx France 2013, je découvrais AngularJS lors de l’Université sur AngularJS animée par Thierry Chatel. Enthousiasmé par ce framework, je vous faisais ici même une restitution de cette Université. Depuis un an, j’ai poursuivi mon initiation en codant un front-end pour Elasticsearch avec Angular. Lorsque j’ai découvert que Matthieu Lux et Olivier Huber proposaient le Hand’s-on-Lab « Angular JS from scratch : comprendre Angular en le refaisant de zéro » à Devoxx France 2014, j’y ai vu l’occasion ou jamais d’approfondir mes connaissances et de découvrir les mécanismes se cachant derrière la magie d’Angular.

angular-from-scratch-devoxx-france-2014

Ce workshop a eu un beau succès : une salle comble 10 minutes avant son début et une place sur le podium des meilleures sessions de la matinée.
Pour coder les différents exercices sans avoir à se tourner régulièrement vers les solutions, de solides connaissances en JavaScript étaient nécessaires : héritage par prototype, constructeur, portée du this, couteau suisse underscore (each, clone, isEqual) …
Par ailleurs, pour apprécier la démarche, une connaissance minimaliste d’Angular me paraissait également indispensable.
Durant les 3 heures du Lab, nous avons pu implémenter 11 des 12 étapes prévues initialement (la dernière étant en bonus). Timing parfaitement respecté. Si vous n’avez pas eu la chance d’assister à cette présentation et si vous disposez de 3 heures devant vous, je vous conseille de tenter de le réaliser chez vous.
Les slides du workshop, le code source de départ, les solutions et les tests unitaires sous Jasmine sont disponibles dans le repo Github angular-from-scratch de Zenika.

L’objectif de ce billet est de vous accompagner dans la réalisation du Lab. Je me focaliserai sur les mécanismes qui permettent de recoder le traditionnel « Hello Word » d’Angular. Vous y trouverez donc une version édulcorée du code du Lab. Garde-fous contre les boucles infinies et comparaisons par valeur n’y seront pas abordés.
Afin de mieux comprendre où s’inscrivent les différentes étapes qui permettent de réimplémenter Angular, je m’appuierai régulièrement sur le schéma présenté par Olivier au début du Lab :

angular-from-scratch-devoxx-france-2014-schema

Le code complet de ce billet est disponible dans jsfiddle et également sous forme de gist.
Afin de pouvoir plus facilement se référer au code source d’Angular, le nom des méthodes et des objets utilisés dans ce Lab reprend volontairement ceux d’Angular.

Etape 1 : le $scope

Connu de tout développeur Angular, le scope est l’objet central du framework. Il permet de mettre à disposition des vues et des contrôleurs le modèle de données de l’application. Contrairement à d’autres frameworks comme Backbone, vous pouvez y placer des objets JavaScript standard (POJSO).
La particularité du scope est de pouvoir être observé. A l’instar du pattern Observer, des watchers peuvent s’enregistrer et être à l’écoute de tout changement sur le modèle, dans sa globalité ou sur une partie donnée. Les watchers sont tout simplement matérialisés par un tableau JavaScript :

function Scope() {
    this.$$watchers = [];
}

En général, Angular instancie pour vous le scope des différentes vues composant une page.
Pour les besoins du Lab, nous l’instancions manuellement :

var scope = new Scope();

Nous y définissons un objet labs comportant 2 propriétés :

scope.labs = {
    titre: "AngularJS from scratch",
    date: new Date()
}

La page HTML référence le titre  objet de l’objet labs :

<h1 class="page-header" ng-bind="labs.titre">AngularJS from scratch</h1>
<input type="text" ng-model="labs.titre"/>

Nous reviendrons sur les directives ng-bind et ng-model lors des étapes 10 et 11.

Etape 2 : Scope.$watch

Comme vu dans l’étape précédente, le scope contient un tableau de watchers. Le but de cette étape est d’implémenter une fonction $watch permettant d’ajouter un watcher dans le tableau $$watchers du scope. Quiconque le souhaite pourra alors surveiller une donnée du scope.

Scope.prototype.$watch = function (watcherFn, listenerFn) {
    var watcher = {
        watcherFn: watcherFn,
        listenerFn: listenerFn,
        last: undefined
    };
    this.$$watchers.push(watcher);
}

La fonction $watch est ajoutée dans le prototype du Scope. Toute instance de Scope hérite ainsi de cette fonction. La ligne this.$$watchers.push(watcher);  ne pose aucune difficulté.

Un watcher est caractérisé par 3 éléments :

  1. une fonction watcherFn indiquant quelle donnée du modèle l’appelant souhaite observer,
  2. une fonction de rappel listenerFn appelée lorsqu’un changement sera détecté
  3. une variable interne last permettant de sauvegarder la précédente valeur du modèle et de réaliser le dirty checking.

Voici un exemple d’appel à la fonction $watch :

scope.$watch(function (scope) {
    return scope.labs.titre;
}, function (newValue, oldValue, scope) {
    console.log("La titre a changé de", oldValue, "à", newValue);
});

En pratique, un développeur Angular fait rarement appel explicitement à cette méthode.

Etape 3 et 5 : Scope.$digest et digest loop

La fonction $digest est au cœur d’Angular. Sur le schéma ci-dessus, elle représente la digest loop. Comme son nom l’indique, son algorithme principal consiste à boucler sur le tableau de watchers jusqu’à ce que tous les évènements aient été traités. Par évènement, on entend un changement dans le modèle.
Voici un exemple d’implémentation :

Scope.prototype.$digest = function () {
    var dirty;
    do {
        dirty = false;
        _.each(this.$$watchers, function (watcher) {
            var newValue = watcher.watcherFn(this);
            if (watcher.last !== newValue) {
                watcher.listenerFn(newValue, watcher.last, this);
                watcher.last = newValue;
                dirty = true;
            }
        }.bind(this));
    } while (dirty);
}

Quelques explications peuvent être nécessaires à la compréhension de ce code :

  • L’itération sur le tableau $$watchers est réalisée par la méthode each d’Underscore
  • La méthode watcherFn accepte comme argument le scope à observer. Ici, un this est passé en paramètre. Sans l’utilisation du bind(this), ce serait le this de l’inner fonction qui aurait été  passé à watcherFn et non le scope sur lequel la méthode $digest est appelée. bind(this) est une technique native JavaScript que je ne connaissais pas. Elle permet de forcer le this. Une technique plus repandue est l’utilisation d’un var self=this; avant la déclaration de l’inner fonction. Underscore aurait également pu être utilisé pour gérer cette problématique récurrente en JavaScript.
  • Lorsqu’un changement est détecté, la fonction de rappel listenerFn est appelée avec la nouvelle valeur, l’ancienne valeur et le scope.

A chaque fois qu’un $digest est appelé, la fonction watcherFn de tous les watchers est appelée. Cela a un coût. Et c’est pourquoi les auteurs d’Angular encouragent à garder cette fonction la plus légère possible. Appels réseaux et algorithmes complexes y sont à proscrire.

Lors du Lab, nous avons ajouté 3 améliorations :

  1. Un premier garde-fou permettant d’éviter un appel infini en levant une erreur après 10 itérations
  2. Un second garde-fou permettant d’éviter des appels récursifs à la méthode $digest (étape 7).
  3. La possibilité d’effectuer des comparaisons par valeur et non pas uniquement par référence. La comparaison de tableaux ou de grappes d’objets devient alors possible (étape 6).

Etape 4 : Scope.$apply

La fonction $apply exécute une expression passée en argument puis lance quoi qu’il arrive un $digest :

Scope.prototype.$apply = function (exprFn) {
    try {
        exprFn();
    } finally {
        this.$digest();
    }
}

Cette méthode est appelée en interne par Angular lorsqu’il a besoin de binder une donnée, par exemple lors de l’utilisation de la directive ng-bind dans les templates. Tous les composants Angular y font appels.

En dehors d’un contexte Angular, cette méthode doit explicitement être  appelée par le développeur. C’est typiquement le cas depuis une callback jQuery.
L’étape 5 ayant été traitée en même temps que l’étape 3 et les étapes 6 et 7 étant facultatives pour l’objectif fixé initialement, nous enchaînons directement à l’étape 8.

Etape 8 : place aux directives

Dans le fragment HTML présenté au début du billet, 2 directives viennent enrichir le HTML sous forme d’attributs : ng-model et ng-bind. Dans Angular, une directive n’est rien d’autre qu’une fonction ou un objet ayant des propriétés bien définies. Pour les besoins du Lab, nous resterons sur le cas simple : la fonction. L’objet $$directives doit permettre d’enregistrer les fonctions associées à ces directives. Pour rappel, un objet JavaScript peut être utilisé de la même manière qu’un tableau associatif (une Map en Java) : à partir de la clé (chaine ‘ng-bind’) on récupère la valeur (fonction ng-bind).
La fonction $directive permet quant à elle d’ajouter une directive et de lire une directive depuis l’objet $$directives.

var $$directives = {};
var $directive = function (name, directiveFn) {
    if (directiveFn) {
        $$directives[name] = directiveFn;
    }
    return $$directives[name];
}

La fonction $directive fait à la fois getter et setter pour les $$directives. L’implémentation utilise une technique répandue en JavaScript : lorsque seul le nom d’une directive est passé en paramètre, la fonction agit comme un getter. Lorsque le nom et le code d’une directive sont passés en paramètre, la fonction enregistre la fonction avant de la retourner.

A noter que les développeurs Angular ne manipulent jamais directement ces 2 objets. Ils sont utilisés par le moteur d’injection de dépendance d’Angular.

Etape 9 : $compile le DOM

Cette étape consiste à écrire la fonction $compile chargée de parcourir récursivement les éléments du DOM. Les attributs de chaque élément sont également parcourus. Lorsqu’un attribut correspond au nom d’une directive, la fonction implémentant la directive est exécutée.
Le code est compréhensif :

var $compile = function(element, scope) {
    _.each(element.children, function (child) {
       $compile(child, scope);
    });
    _.each(element.attributes, function(attribute) {
        var directiveFn = $directive(attribute.name);
        if (directiveFn) {
            directiveFn(scope, element, element.attributes);
        }
    });
}

Deux remarques à propos du code :

  1. Contrairement à ce que l’on pouvait s’attendre, tous les attributs de l’élément sur lequel est apposée la directive sont passés en paramètre de la fonction directiveFn.
  2. La récursion sur les éléments enfants est lancée avant le parcours des attributs de l’élément. Angular offre le choix avec les propriétés prelink et postlink. De manière générale, postlink est à privilégier.

Pour demander à notre framework maison de parcourir l’intégralité du DOM, une unique ligne de code est nécessaire :

$compile(document.body, scope);

A présent que le framework sait découvrir des directives dans le DOM et appeler la fonction correspondante, il est temps d’implémenter une première directive.

Etape 10 : ng-bind

La directive ng-bind se met à l’écoute sur la donnée pointée par la valeur de l’attribut ng-bind. Lors d’un changement de valeur, l’élément du DOM (dans notre exemple <h1>) est modifié avec la nouvelle valeur.

$directive('ng-bind', function (scope, element, attributes) {
    scope.$watch(function(scope) {
        return eval('scope.' + attributes['ng-bind'].value);    // 'scope. labs.titre'
    }, function(newValue) {
        element.innerHTML = newValue;
    });
});

Bien que controversée, l’utilisation de la fonction eval simplifie ici le code. Au cours du Lab, Matthieu nous a donné son équivalent fonctionnel. Basé sur les fonctions split et reduce, le code devient illisible pour les développeurs ne pratiquant pas ce paradigme.

Etape 11 : ng-model

ng-model est une directive gérant le data-binding bidirectionnel. Appliquée à la balise <input/>, elle permet d’y afficher le contenu du modèle même lorsque celui-ci change et de mettre à jour le modèle lorsque l’utilisateur saisit des données dans le champ de saisie.
S’agissant d’une version ++ de la directive ng-bind, le début de leur implémentation correspondant au premier sens de binding se ressemble :

$directive('ng-model', function(scope, element, attributes) {
    scope.$watch(function() {
        return eval('scope.' + attributes['ng-model'].value);
    }, function(newValue) {
        element.value = newValue;
    });
    element.addEventListener('keyup', function() {
        scope.$apply(function() {
            eval('scope.' + attributes['ng-model'].value + ' = \"' + element.value + '\"');
        });
    });
});

La directive ng-model ajoute un listener d’évènements JavaScript. Lorsque l’évènement ‘keyup’ survient, le modèle est mis à jour à l’intérieur de la fonction $apply. Cette dernière déclenche la digest loop qui notifie la balise ng-bind. C’est par ce mécanisme que lorsque l’utilisateur saisi du texte dans l’input, le titre <h1> est mis à jour en conséquence.
La fonction eval est là encore utilisée. Angular n’y fait pas appel car il possède son propre parseur.

Hello World

Une fois ces 2 directives enregistrées, un changement du titre du labs met simultanément à jour le titre <h1> et le champs de saisie <input> :

scope.$apply(function () {
    scope.labs.titre = "Hello World";
})

angular-hello-world

Conclusion

92 lignes de JavaScript auront été nécessaires pour ré-implémenter une version minimaliste du cœur d’AngularJS. Vous pouvez tester : le code fonctionne sous IE 11, Firefox 28 et Chrome 34. Underscore aura permis de gagner en clarté ainsi que quelques lignes de code.
N’ayant que quelques dizaines d’heures d’Angular à mon actif, j’espère que ce que je vous aurais restitué sera exempt d’erreurs. Dans le cas contraire, je compte sur les speakers et vous pour me rectifier.

]]>
https://javaetmoi.com/2014/04/lab-angularjs-from-scratch-devoxx-france-2014/feed/ 4
Développer et industrialiser une web app avec AngularJS https://javaetmoi.com/2014/02/developper-industrialiser-web-app-recherche-angularjs/ Sun, 09 Feb 2014 19:13:56 +0000 http://javaetmoi.com/?p=898 Continuer la lecture de Développer et industrialiser une web app avec AngularJS ]]> Au travers du billet Elastifiez la base MusicBrainz sur OpenShift, je vous ai expliqué comment indexer dans Elasticsearch et avec Spring Batch l’encyclopédie musicale MusicBrainz. L’index avait ensuite été déployé sur le Cloud OpenShift de RedHat.
Une application HTML 5 était mise à disposition pour consulter les albums de musique ainsi indexés. Pour m’y aider, Lucian Precup m’avait autorisé à adapter l’application qu’il avait mise au point pour l’atelier Construisons un moteur de recherche  de la conférence Scrum Day 2013.
Afin d’approfondir mes connaissances de l’écosystème JavaScript, je me suis amusé à recoder cette application front-end en partant de zéro. Ce fut l’occasion d’adopter les meilleures pratiques en vigueur : framework JavaScript MV*, outils de builds, tests,  qualité du code, packaging …
Au travers de ce article, je vous présenterai comment :

  1. Mettre en place un projet Anguler à l’aise d’Angular Seed, Node.js et Bower
  2. Développer en full AngularJS et Angular UI Bootstrap
  3. Utiliser le framework elasticsearch-js
  4. Internationaliser une application Angular
  5. Tester unitairement et fonctionnellement une application JS avec Jasmine et Karma
  6. Analyser du code source JavaScript avec jshint
  7. Packager avec Grunt le livrable à déployer
  8. Utiliser l’usine de développement JavaScript disponible sur le Cloud : Travis CI, Coversall.io et David

Le code source de l’application est bien entendu disponible sur GitHub et testable en ligne.

Angular MusicBrainz web app screenshot

Démarrer un projet avec Angular Seed

Hébergé sur GitHub et maintenu par les auteurs d’Angular, le projet angular-seed permet de démarrer  rapidement une application Angular. Outre le squelette applicatif, ce projet propose :

  • des exemples de tests unitaires et de tests dits end-to-end,
  • des scripts .sh ou .bat permettant d’exécuter ces différents types de tests
  • un script JS permettant de démarrer un serveur web sous NodeJS

Le README.MD explique de manière approfondie l’organisation du projet et la nature de chaque fichier.
Une fois ce repository cloné sous GitHub ou bien dézippé en local, il est possible de le personnaliser à sa guise.

Une alternative à angular-seed serait d’utiliser Yo pour générer le squelette de l’application, sur un principe similaire aux archetypes maven.

Exécuter l’application

L’application blanche fournie dans Angular Seed étant une application full HTML, il n’est pas nécessaire de la déployer dans un serveur d’application JEE ou un conteneur web. Un  simple serveur web comme Apache ou Nginx est nécessaire.
Les utilisateurs de Firefox pourront même se passer de serveur web et ouvrir directement le fichier app/index.html à partir de leur disque.
Chrome n’ayant pas cette faculté (les requêtes Ajax chargées de récupérer un fichier sur disque sont bloquées), vous pouvez utiliser le serveur web installé sur votre poste de développement.

En guise de serveur web, vous pourrez utiliser le script scripts\web-server.js pour en démarrer un en full JavaScript. Le seul pré-requis est l’installation de NodeJS qui intègre le moteur JavaScript V8 de Google. A noter que NodeJS et son gestionnaire de paquets npm seront nécessaires dans la suite de cet article pour installer les outils, construire l’application, monter de version les dépendances ou bien encore exécuter les tests.

Une fois NodeJS installé et ajouté au PATH du système, exécuter la commande suivante pour démarrer le serveur:

D:\Dev\angular-musicbrainz>node scripts\web-server.js
Http Server running at http://localhost:8000/

Saisir l’URL http://localhost:8000/app/index.html dans le navigateur de votre choix. Les requêtes HTTP apparaissent sur la console :

GET /app/index.html Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.63 Safari/537.36
GET /app/lib/bootstrap/dist/css/bootstrap.css Mozilla/5.0d (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.63 Safari/537.36
GET /app/lib/bootstrap/dist/css/bootstrap-theme.css Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.63 Safari/53
GET /app/lib/angular-resource/angular-resource.js Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.63 Safari/537.
…

 Structuration du projet

La structure des répertoires du projet reprend celle d’Angular Seed.  Le tableau ci-dessous liste les librairies tierces utilisées par l’application.

Répertoires / fichiersDescription
app/Code source et ressources de l’application
        css/Feuilles de styles CSS
        i18n/Fichiers JSON de traduction de l’application
        img/Images et icône
        index.htmlPage principale de la Single Page Application
        js/Fichiers JavaScript spécifiques à l’application
                app.jsDéclaration des modules et démarrage de l’application
                controllers.jsContrôleurs Angular spécifiques à l’application
                directives.jsDirectives Angular spécifiques à l’application
                filters.jsFiltres / formateurs Angular spécifiques à l’application
                routes.jsConfiguration des routes
                services.jsServices spécifiques à l’application
        lib/Librairies tierces déclarées dans bower.json
                angular/Module principal d’Angular
                angular-i18n/                Fichiers de traductions fournis par Angular
                angular-mocks/                Mocks permettant de bouchonner des services Angular
                angular-resource/                Accès REST aux ressources serveur
                angular-route/Routage des vues en fonction de l’URL
                angular-sanitize/Filtres standards
                angular-scenario/DSL des scénarios de tests end-to-end
                angular-ui-boostrap-bower/Widgets Angular basés sur Bootstrap
                bootstrap/Mise en page et charte graphique
                elasticsearch-js/Client JavaScript pour Elasticsearch
                jquery/Manipulation de DOM
        partials/        Template HTML des vues de l’application
                directives/Template HTML des directives de l’application
        conf/Configuration Karma des tests unitaires et e2e
        dist/Répertoire de destination du livrable de production
        node_modules/Modules NodeJS utilisées par Grunt
        scripts/Scripts shell, JS et batch permettant de lancer les tests et de démarrer un serveur web
        test/Code source des tests
                e2e/Tests end-to-end
                unit/Tests unitaires

Automatisation avec Grunt

Grunt peut être comparé au Gradle du monde JavaScript. Il permet d’exécuter des tâches sous NodeJS et est particulièrement utile pour automatiser certaines tâches de développement. Son installation nécessite une seule commande :
npm install -g grunt-cli

Le gestionnaire npm permet ensuite de télécharger et d’installer les modules NodeJS nécessaires au fonctionnement du script Grunt. Commande à exécuter à la racine du répertoire, au même niveau que le fichier package.json :
npm install

Le sous-répertoire node_modules est alimenté par les modules déclarés dans le fichier package.json.

Angular Seed ne configurant pas Grunt, je me suis inspiré de différents exemples pour mettre au point le script Gruntfile.js.

Voici quelques commandes utiles :

  • grunt test : lance successivement les tests unitaires et les tests end-to-end
  • grunt server : démarre un serveur web, ouvre la page dans l’application et, à l’instar de JRebel, recharge à chaud le code modifié depuis votre IDE.
  • grunt jshint : vérifie que la qualité du code de production JavaScript
  • grunt build : construit le livrable à installer sur le serveur web. Les fichiers générés sont mis à disposition dans le sous-répertoire dist.
  • grunt karma:coverage : génère le taux de couverture des tests unitaires. Au format HTML, le rapport est accessible depuis le sous-répertoire coverage\PhantomJS 1.9.2 (Windows 7)\lcov-report

Pendant le développement de l’application web, à des fins de débogage, le code JavaScript est non minifié et séparé dans plusieurs fichiers, de même pour les feuilles de style CSS.
Sur un principe similaire à ce que propose Jawr dans le monde Java, l’étape de build va permettre d’obtenir un livrable le plus léger possible. Voici les opérations effectuées :

  1. Le code JavaScript et les feuilles de styles CSS sont concaténées puis minifiés. Exemple de directive à placer dans le code HTML :
    Directive build:js sur la page index.html

  2. Les images bitmaps et vectorielles sont réduites.
  3. Les pages HTML référençant ces ressources statiques sont mises à jour en conséquences.

Dépendances Bower

Comme vu au paragraphe précédent, les modules nécessaires au fonctionnement de l’infrastructure de build basé sur Grunt sont récupérés à l’aide de npm et du fichier package.json.

Les librairies nécessaires au développement de l’application (Angular, Bootstrap) et de ses tests sont gérées par un second système de dépendances, à savoir Bower. Cet outil permet de rechercher des librairies via la commande  bower search <nom-librairie>. L’instruction bower install <nom- librairie> –save(-dev) permet de la télécharger et de la référencer dans le fichier bower.json. Les librairies sont installées dans le répertoire app/lib configuré dans le fichier .bowerrc.

S’appuyant sur le fichier bower.json, la commande bower install permet de récupérer toutes les dépendances utilisées par l’application. Elle permet également de mettre à jour les dépendances lors d’une montée de version.

En théorie, il n’est pas nécessaire d’archiver ces dépendances dans le gestionnaire de code source (ici GitHub). En pratique, c’est utile de pouvoir exécuter l’application sans avoir à installer ni NodeJS ni Bower.

Architecture applicative

En avril 2013, je découvrais Angular à Devoxx France. Je vous en présentais ici même les mécanismes et les fonctionnalités clés. Après m’être auto-formé et avoir animé un workshop sur ce framework, j’étais impatient de le mettre en œuvre sur un projet personnel (en attendant un futur projet pro). Et autant vous l’annoncer dès à présent, je n’ai pas été déçu : j’ai pris un réel plaisir à développer le front end de mon application de recherche angular-musicbrainz.

D’un point de vue de l’architecture applicative, cette single page application est composée de 3 fichiers HTML principaux :

  1. index.html : bootstrape l’application en initiant le gabarit HTML (essentiellement le menu) et en téléchargeant les fichiers JavaScript, HTML et CSS nécessaires à son fonctionnement
  2. partials\search.html : vue de recherche et de restitution des résultats. C’est l’implémentation de cette vue que nous allons présenter dans la suite de se billet.
  3. partials\info.html : vue référençant quelques URLs utiles.

Le code JavaScript de l’application est découpé techniquement. A une couche technique (contrôleurs, services, filtres et directives) correspond un fichier JavaScript et un module Angular.
Simple,  cette organisation est pratique pour de petites applications. Sur des applications plus conséquences, un découpage fonctionnel est à privilégier.

Services

L’application repose sur 4 services implémentés dans le fichier services.js :

Nom du serviceFonctionnalités
esCrée et configure un client Elasticsearch. S’appuie sur la fabrique mise à disposition par la librairie elasticsearch-js.
searchServiceCode métier regroupant les requêtes JSON de recherche Elasticsearch. 2 méthodes sont disponibles : l’une pour la recherche full text et l’autre pour l’auto-complétion.
userLanguagePermet d’accéder à la locale de l’utilisateur.
translationRécupère le fichier de traduction de l’application.

Filtres

Les templates HTML reposent sur 5 filtres personnalisés

Nom du filtreUtilisationRendu
interpolate{ ‘v%VERSION%’ | interpolate }V1.0
joinBy{{hit._source.tags | joinBy:’ – ‘}}pop – rock – blues
reverseng-repeat= »rating in facets.rating.entries | reverse » angular-musicbrainz-rating
artistTypeLabel{{type.term | artistTypeLabel}}Artiste (fr) ou Artist (en)
yearFormat{{range | yearFormat}}Avant 1970 (fr)

Pour les détails d’implémentation, se référer au code source filters.js et aux tests unitaires qui les documentent filterSpecs.js.

Directives

2 widgets graphiques ont été réalisés à l’aide de directives :

DirectiveUtilisationRendu
Cover<cover album-id= »hit._source.id »> </cover> angular-musicbrainz-cover
Rank<rank score= »hit._source.rating.score »> </rank> angular-musicbrainz-rank

A noter que la directive rating d’Angular Bootsrap offre une alternative à la directive rank. Cette dernière reprend la CSS de MusicBrainz permettant d’ajuster le dégradé des étoiles au pixel près.

Afin de rendre le code des directives plus lisible et maintenable, les templates HTML de ces 2 directives ont été externalisés dans des fichiers html dédiés.
Exemple rating.xml :

<span class="inline-rating">
  <span class="star-rating small-star">
     <span style="width:{{score+ceil}}%;" class="current-rating">{{score}}</span>
  </span>
</span>

La configuration des tests unitaires Karma a dû être ajustée en conséquences :

// generate js files from html templates to expose them during testing.
   preprocessors : {
        'app/partials/directives/**/*.html': ['ng-html2js']
    },

Contrôleurs

A  chaque vue de l’application, correspond un contrôleur. Le fichier controller.js en défini donc deux : SearchCtrl et InfoCtrl.
Trivial, le contrôleur InfoCtrl met à disposition dans le scope de le vue info les 2 URLs affichées et formatées  à l’aide de la directive Angular ngLinky 

Contrôleur :

$scope.demoUrl = 'http://angular-musicbrainz.javaetmoi.com/';

Template :

<li>Online Demo: <span ng-bind-html="demoUrl | linky"/></li>

Le contrôleur SearchCtrl embarque toute la logique applicative de l’application. Il offre à la fois des fonctions réagissant aux actions utilisateurs et les données utilisées par Angular lors du rendu de la vue search. En voici les principales :

PropriétéTypeDescription
fullTextSearchfonctionExécute une recherche full text lors du clic sur le bouton « Recherche MusicBrainz».
autocompletefonctionExécute une requête d’auto-complétion à chaque frappe de l’utilisateur dans le zone de recherche.
selectPageFonctionPermet à l’utilisateur de sélectionner une plage de résultats. Exécute une recherche Elasticsearch sur la plage indiquée.
searchRespDonnéeRésultats Elasticsearch d’une recherche fulltext.
pageSizeDonnéeNombre de résultats à afficher à l’écran.
currentPageDonnéePlage de résultats actuellement affichée.
pageSizesDonnéeTailles de plages que l’utilisateur peut choisir.

Routes

Par rapport au template angular-seed, la configuration des routes a été externalisée dans un fichier dédié routes.js.

Application

Le 6ième et dernier module Angular correspond  au module applicatif musicAlbumApp déclaré dans le fichier app.js. Outre la déclaration des modules Angular nécessaires au fonctionnement de l’application, ce module est chargé de déterminer la langue dans laquelle l’interface doit s’afficher puis charger les données adéquates. Nous y reviendrons dans la suite de cet article.

Utilisation d’elasticsearch-js

Pour interroger le cluster Elasticsearch depuis le navigateur, j’ai étudié 3 possibilités : le service natif $http d’Angular, la librairie elastic.js et la librairie elasticsearch-js. Sortie en décembre 2013 et mise en avant par Elasticsearch.org, j’ai choisi d’utiliser cette dernière. Via la création de tickets GitHub, j’ai eu la chance de pouvoir  contribuer à l’amélioration de cette jeune librairie déjà très mature.

Le module esFactory permet de déclarer en quelques lignes un client JavaScript Elasticsearch. Voici les paramètres renseignés :

angular.module('musicAlbumApp.services', ['ngResource'])
    .value('version', '1.0')
    // elasticsearch.angular.js creates an elasticsearch
    // module, which provides an esFactory
    .service('es', ['esFactory', function (esFactory) {
        return esFactory({
            hosts: [
                // you may use localhost:9200 with a local Elasticsearch cluster
                'es.javaetmoi.com:80'
            ],
            log: 'trace',
            sniffOnStart: false
        });
    }])

J’ai volontairement désactivé la fonctionnalité de sniffOnStart. En effet, j’ai configuré le reverse proxy Nginx pour ne laisser passer que les requêtes de _search. Les requêtes HTTP de type HEAD envoyées par le client pour déterminer la disponibilité des différents nœuds du cluster étaient donc rejetées.

L’appel au service de recherche Elasticsearch est également très simple. Dans l’attribut body de la fonction search proposée par l’API, est utilisé le formalisme standard de déclaration des requêtes au format JSON. En complément, les attributs index et type permettent respectivement d’indiquer sur quel index Elasticsearch et sur quel type de document lancer la recherche. Voici un exemple d’appel :

Extrait méthode fullTextSearch

.factory('searchService', ['es', function (es) {
        return {
            'fullTextSearch': function (from, size, text) {
                return es.search({
                    index: 'musicalbum',
                    type: 'album',
                    body: {
                        'from': from,
                        'size': size,
                        'query': {
                            'bool': {
                                'must': [
                                    {
                                        'fuzzy_like_this': {
                                            'fields': [
                                                'name',
                                                'artist.name',
                                                'year.string'
                                            ],
                                            'like_text': text,
                                            'min_similarity': 0.7,
                                            'prefix_length': 1
                                        }
                                    }
                                ]
                            }
                        },
                        'facets': {
                            'artist_type': {
                                'terms': {
                                    'field': 'artist.type_id'
                                }
                            },
                            'album_rating': {
                                'histogram': {
                                    'key_field': 'rating.score',
                                    'interval': 21
                                }
                            },
                            'album_year': {
                                'range': {
                                    'field': 'year',
                                    'ranges': [
                                        { 'to': 1970},
                                        {  'from': 1970, 'to': 1980},
                                        {  'from': 1980, 'to': 1990},
                                        {  'from': 1990, 'to': 2000},
                                        {  'from': 2000, 'to': 2010},
                                        {  'from': 2010 }
                                    ]
                                }
                            }
                        }
                    }
                });
            },

La fonction search renvoie une promesse de réponse. Pour récupérer la réponse retournée par Elasticsearch, la méthode then peut être utilisée :

searchService.fullTextSearch(from, $scope.pageSize.count, text).then(
                function (resp) {
                    $scope.searchResp = resp;
                    $scope.totalItems = resp.hits.total;
searchService.fullTextSearch(from, $scope.pageSize.count, text).then(
    function (resp) {
        $scope.searchResp = resp;
        $scope.totalItems = resp.hits.total;
    }
);

Localisation

Le service $locale d’Angular permet de formater les nombres et les dates en fonction des préférences linguistiques de l’utilisateur. Il existe autant de fichiers JavaScript que de combinaisons langue / pays (exemples : angular-locale_fr-fr.js, angular-locale_en-us.js).

Pour charger le fichier adéquat, l’application doit détecter le langage défini par l’utilisateur dans son Navigateur. A première vue, les variables du DOM window.navigator.userLanguage et window.navigator.language auraient dû apporter cette information. Il en aurait été trop simple. L’article Detecting a Browser’s Language in Javascript explique précisément pourquoi.
Le header HTPP Accept-Language ne peut être lue que côté serveur web. Or, l’application était jusque-là full JavaScript. Convertir la page index.html en une page PHP ou JSP aurait été simple. Néanmoins, j’ai préféré m’affranchir de toute installation côté serveur. J’ai donc utilisé le service http://ajaxhttpheaders.appspot.com mis à disposition sur Google App Engine et dont voici un exemple d’utilisation :

$http.jsonp('http://ajaxhttpheaders.appspot.com?callback=JSON_CALLBACK').
success(function (data) {
    var acceptLang = data['Accept-Language'];
    langRange = userLanguage.getFirstLanguageRange(acceptLang);
    language = userLanguage.getLanguage(langRange);
    if (sessionStorage) {
        sessionStorage.setItem('userLanguageRange', langRange);
    }
}).
finally(function () {
    loadI18nResources();
});

Une fois la langue de l’utilisateur connue, la fonction $.getScript de JQuery permet de charger dynamiquement le fichier JavaScript Angular correspondant.
Afin d’éviter des appels intempestifs au service http://ajaxhttpheaders.appspot.com, la langue est conservée dans le sessionStorage du navigateur.
Dans un souci d’internationalisation, l’application angular-musicbrainz a été traduite en 2 langues : le français et l’anglais. Les libellés affichés à l’écran dépendent donc des préférences utilisateurs. Le système mis en œuvre s’inspire de ce que proposent les articles Creating multilingual support using AngularJS et Traduction des libellés dans les vues AngularJS. Un objet translation contenant la traduction de tous les libellés d’une langue est chargé à partir d’un fichier JSON puis ai mis dans le scope parent ($rootScope). Cet objet peut être accédé à la fois dans les templates HTML que côté JavaScript :

<label id="search-input-label" class="col-sm-3 control-label" ng-bind="translation.SEARCH_LABEL">Searching a music album</label>
$scope.pageSizes = [
            {count: 5, label: '5 ' + $scope.translation.SEARCH_PAGE_RESULT},

Widgets graphiques

Le projet UI Bootstrap propose une douzaine de directives Angular basées sur Boostrap : sélection de date avec calendrier, accordéon, onglets, barres de progression, fenêtre popup, collapse, carrousel d’images …

Notre web app de recherche utilise 3  de ses directives :

DirectiveVisuelExemple d’utilisation dans les templates HTML
typeahead angular-musicbrainz-typeahead
<input type="text" 
class="form-control" 
ng-model="searchText" 
typeahead="album for album 
in autocomplete($viewValue) 
| filter:$viewValue" />
pagination angular-musicbrainz-pagination
<pagination total-items="totalItems" 
page="currentPage"
max-size="maxSize"
num-pages="numPages" 
items-per-page="pageSize.count" 
on-select-page="selectPage(page)">
</pagination>
pagerangular-musicbrainz-pager
<pager total-items="totalItems"
page="currentPage"
on-select-page="selectPage(page)">
</pager>

Comme le montre les exemples ci-dessus, les directives UI Boostrap permettent d’étendre le HTML, soit par de nouveaux tags (ex: <pagination/> , soit par des attributs enrichissants des tags standards (ex : typeahead sur <input/> ).

Tests unitaires

Avec son découpage en modules, la possibilité de créer des mocks et l’indépendance du code JavaScript au regard du DOM, Angular permet de tester unitairement chaque contrôleur, service, filtre, route et directive. Qui plus est, l’application blanche angular-seed  vient avec toute l’infrastructure de tests : spécifications Jasmine à compléter, configuration Karma, scripts batch et shell permettant d’exécuter les tests. Autant dire, le développeur n’a aucune excuse pour ne pas tester unitairement son application.
L’application angular-musicbrainz comptabilise 36 tests unitaires, couvrant ainsi 65% du code source. Avant de pouvoir exécuter les tests unitaires, il est nécessaire d’installer les quatre modules Karma suivants :

npm install -g karma karma-junit-reporter karma-ng-html2js-preprocessor karma-coverage

Les tests unitaires peuvent être exécutés de 2 manières :

  1. Par la commande grunt : grunt karma
  2. Ou par un script : scripts\test.bat

Karma exécute les tests puis se met en attente de changements. En effet, tel infinitest, Karma relance les tests à chaque modification du code source ou des tests. Cela s’avère très pratique pour lever au plus tôt toute régression ou bien travailler en TDD.

Autre aspect de Karma : il permet de faire tourner les tests simultanément dans un ou plusieurs navigateurs. Dans le fichier de configuration karma.conf.js, Google Chrome et le navigateur headless PhantomJS ont été retenus.

Une fois la structuration d’un cas de test prise en main (mots clés describe, beforeEach et it), l’écriture du code de tests est plus ou moins simple. La difficulté principale vient de la lourdeur de la configuration nécessaire à mettre en place pour bouchonner les adhérences. Voici par exemple comment tester la fonction fullTextSearch du contrôleur SearchCtrl :

it('fullTextSearch should put the searchResp variable into the scope', function () {

            expect(scope.searchResp).toBeUndefined();
            expect(scope.isAvailableResults()).toBeFalsy();
            expect(scope.isAtLeastOneResult()).toBeFalsy();

            scope.fullTextSearch('U2', 1);
it('fullTextSearch should put the searchResp variable into the scope', function () {

    expect(scope.searchResp).toBeUndefined();
    expect(scope.isAvailableResults()).toBeFalsy();
    expect(scope.isAtLeastOneResult()).toBeFalsy();

    scope.fullTextSearch('U2', 1);

    // scope.$digest() will fire watchers on current scope,
    // in short will run the callback function in the controller that will call anotherService.doSomething
    scope.$digest();

    expect(scope.searchResp).toBeDefined();
    expect(scope.totalItems).toBeDefined();
    expect(scope.isAvailableResults()).toBeTruthy();
    expect(scope.isAtLeastOneResult()).toBeTruthy();
});

Le contrôleur SearchCtrl  s’appuie sur le service searchService dont la fonction fullTextSearch a dû être bouchonnée. Au final, le développeur écrit plus de code de test que de code testé.
Espérons que le duo Karma / Jasmine gagnera en maturité avec le temps. En Java, l’utilisation des annotations @Mock et @InjectInto permet en effet de réduire drastiquement ce type code.

Non des moindre, le dernier point à connaître lors de l’écriture des tests concerne les assertions. Venant avec un nombre de matchers clés en mains, Jasmine permet d’écrire ses propres matchers.

Tests end-to-end

A l’instar de ce que propose Selenium dans le monde Java, Karma permet d’écrire et d’exécuter des scénarios fonctionnels. Un prérequis à leur exécution est que l’application web doit être démarrée.

Là encore, l’application blanche angular-seed  vient avec toute l’infrastructure de tests e2e nécessaire. Comme prérequis, le module karma-ng-scenario doit être installé via npm :

npm install -g karma-ng-scenario

Les tests e2e peuvent être exécutés en ligne de commande: scripts\e2e-test.bat

Pour l’écriture des scénarios de tests, le module angular-scenario fournit un DSL permettant de sélectionner des éléments du DOM et de simuler des évènements utilisateurs. A noter que le framework Protactor doit remplacer à termes ce module.
Comme le montre l’extrait de code ci-dessous, le code reste lisible :

describe('search', function () {

    beforeEach(function () {
        browser().navigateTo('#/search');
    });

    it('should render search when user navigates to /search', function () {
        expect(element('#search-input-label').text()).
        toContain('music');
    });

    it('U2 album search', function () {
        input('searchText').enter('U2');
        element(':button').click();
        expect(element('#result-number').text()).
        toContain('22');

    });

});

Créé par l’un des développeurs d’Angular, Karma a l’avantage de connaître le fonctionnement interne d’Angular. Cette faculté lui permet de résoudre les problèmes de requêtes Ajax souvent rencontrés dans les tests Selenium. Adieux les tempos ou autre  waitForElement.

angular-musicbrainz-e2e

Exécution des tests end-to-end dans Chrome :

Contrôle qualité avec JSHint

Fork actif de jslint, JSHint s’apparente au Checkstyle du monde Java. Cet outil Open Source effectue plusieurs types de vérifications sur les fichiers JavaScript :

  • Conventions de nommage
  • Règles de formatage
  • Bonnes pratiques permettant d’éviter de potentiels bugs

Le fichier de configuration .jshintrc permet d’activer chacune des dizaines de règles proposées par JSHint. Activée sur notre projet, la règle curly vérifie par exemple s’il ne manque pas des accolades dans les boucles et les conditions.

La vérification des fichiers JavaScript peut ensuite se faire, soit en ligne de commande :

D:\Dev\angular-musicbrainz>grunt jshint
Running "jshint:all" (jshint) task
Linting app/js/services.js ...ERROR
[L140:C17] W116: Expected '{' and instead saw 'return'.
return undefined;

Soit directement depuis IntelliJ IDEA après configuration :

angular-musicbrainz-jshint
angular-musicbrainz-jshint2

JSHint a toute sa place sur un projet de grande taille sur lesquels de nombreux développeurs travaillent puis se relaieront pour sa maintenance. Sur de plus modestes applications comme angular-musicbainz , il a le mérite de former et de mettre en garde des développeurs JavaScript Junior.

Usine de dév JavaScript

Dans le billet Ma petite usine logicielle, je vous expliquais comment utiliser CloudBees et GitHub pour industrialiser vos projets Java. Vous l’aviez compris, l’intégration continue et l’automatisation des tâches me tiennent à cœur. J’ai donc naturellement regardé ce qui existait dans le monde JavaScript. Ce dernier n’est pas en reste. Voici ce que j’ai mis en place sur angular-musicbrainz.

Déjà mis en place sur mes projets Java avec Maven, Travis CI est une plateforme d’intégration continue mise à disposition gratuitement pour les projets Open Source. Cette plateforme présente l’avantage de supporter NodeJS et peut donc intégrer des applications JavaScript.

La configuration du build Travis se trouve dans le fichier .travis.yml :

language: node_js
node_js:
  - 0.10

before_script:
  - npm install -g grunt-cli

script:
  - grunt karma:ci

after_success:
  - grunt coverage

On demande à Travis d’installer le client Grunt avant d’exécuter les tests unitaires et de publier la couverture de code. A chaque commit dans le repo GitHub, le build est lancé. La sortie console s’affiche en temps réel :

angular-musicbrainz-travis


En cas d’échec du build, vous pouvez être notifiés par email , IRC, webhook …

Outre la génération d’un rapport de couverture de code testé, la commande grunt coverage  envoie ce rapport au service Coveralls. Ce dernier historise le taux de couverture et offre une IHM permettant de naviguer parmi les fichiers analysés.

L’historique des builds du projet angular-musicbrainz est accessible en ligne :

angular-musicbrainz-coveralls1


Le taux de couverture du build n°494581 est également consultable en ligne :

angular-musicbrainz-coveralls1


Autre service en ligne intéressant : pouvoir vérifier rapidement que les dépendances d’une application sont à jour. C’est ce que propose David. Voici visuellement la synthèse proposée par David pour les dépendances de dev d’angular-musicbainz :

angular-musicbrainz-david

A noter qu’un service similaire pour les dépendances utilisées par Bower serait intéressant.

Chacun de ces services propose un badge dynamique. Pratique, ces badges peuvent être affichés dans le README.MD :

angular-musicbrainz-build-status
Conclusion

Ce long billet m’aura permis de vous faire découvrir les différentes facettes du monde JavaScript dont j’ai fait connaissance tout au long du développement de cette petite application web de recherche.
L’utilisation d’Angular est plaisante et me réconcilie avec le développement JavaScript que je ne trouvais jusque-là pas assez industrialisé. Diminuant le nombre de lignes de code JavaScript au profit du HTML, ce framework permet de structurer proprement le code JavaScript. Ceux qui ont connus des applications où chaque page comporte des centaines de lignes jQuery non organisées apprécieront sans aucun doute.

En quelques années, je constate avec plaisir que l’écosystème JavaScript a rattrapé son retard sur celui de Java : intégration continue, outils de builds, tests unitaires, tests fonctionnels, qualimétrie,  gestion des dépendances, MVC, data-binding, POJO, templating, injection de dépendances, modularisation, nombre grandissant de frameworks, moteur d’exécution optimisé, support IDE … Chapeau bas.

]]>
Elastifiez la base MusicBrainz sur OpenShift https://javaetmoi.com/2013/11/musicbrainz-elasticsearch-angularjs-openshift/ https://javaetmoi.com/2013/11/musicbrainz-elasticsearch-angularjs-openshift/#comments Fri, 15 Nov 2013 19:35:01 +0000 http://javaetmoi.com/?p=782 Continuer la lecture de Elastifiez la base MusicBrainz sur OpenShift ]]>
logo-musicbrainz

Pour les besoins d’un workshop sur Elasticsearch, je me suis amusé à indexer une encyclopédie musicale et à mettre en ligne une petite application HTML 5 permettant de réaliser des recherches.

Comme source de données musicale, j’ai opté pour MusicBrainz qui est une plateforme ouverte collectant des méta-données sur les artistes, leurs albums et leurs chansons puis les mettant à disposition du publique.

Pour indexer les données depuis une base PostgreSQL, j’ai privilégié Spring Batch au détriment d’une river. Pour l’IHM, j’ai adapté un prototype basé sur AngularJS, jQuery et Bootstrap qu’avait réalisé Lucian Precup pour la Scrum Day 2013. La mise en ligne de l’index Elasticsearch m’aura permis de tester  la plateforme Cloud  OpenShift de Redhat.

Cet article a pour objectif de décrire les différentes étapes qui m’ont été nécessaires pour réaliser ma démo et d’expliquer ce que j’ai librement rendu accessible sur GitHub et Internet.

Vue d’ensemble

Le diagramme suivant présente l’architecture mise en place.

batch-indexation-musicbrainz

Un batch d’indexation se connecte via JDBC à la base de données de MusicBrainz et indexe les albums de musique dans Elasticsearch. Une application HTML 5 permet d’interroger l’index Elasticsearch.

Base de données MusicBrainz

A l’instar d’IMDb pour le cinéma, MusicBrainz est une base de données dédiée à la musique. Artistes, groupes de musiques, albums, pochettes et chansons issus du monde entier y sont référencés.  Outre la base de données musicale, MusicBrainz propose également une interface graphique permettant d’effectuer des recherches, de consulter les données et de participer à l’enrichissement de la base. Last.fm, The Guardian ou bien encore la BBC s’interfacent avec MusicBrainz.
Parce que la base PostgreSQL du sites MusicBrainz.org n’est pas accessible depuis Internet mais également dans le souci de pouvoir réaliser ma démo déconnecté du réseau, j’ai cherché à pouvoir installer la base de données en locale. MusicBrainz propose 2 solutions :

  1. Télécharger l’image d’une machine virtuelle du serveur MusicBrainz ou
  2. Télécharger la dernière archive de la base PostgreSQL est l’installer en suivant les instructions du INSTALL.md

Pour ma part, j’ai opté pour la solution la plus simple : installer une VM. Disponible au format OVA, elle peut être déployée aussi bien dans VirtualBox ou que dans VMWare. Le guide d’installation de la VM terminé, 2 étapes seront ensuite nécessaires pour que le host puisse accéder à la base PostgreSQL :

  1. 2013-11-virtualbox-musicbrainz-natConfigurer la redirection de port : VirtualBox permet de rediriger les connexions TCP établies sur un port de l’host vers un autre port de la VM. La base PostgreSQL écoutant sur le port 5432, la règle suivante peut être ajoutée via l’interface de VirtualBox : PostgreSQL database – TCP – host : 5432 / guest : 5432
  2. Configurer PostgreSQL : par mesure de sécurité, la base PostgreSQL ne permet pas d’accès distant. Pour que le batch exécuté depuis l’OS hôte puisse s’y connecter, ces instructions doivent être suivies. Démarrer la VM, s’y connecter (login : vm / musicbrainz) et éditer les 2 fichiers de configuration ph_hba.conf et postgresql.conf.

Depuis l’hôte, il est à présent possible de se connecter à la base à partir de n’importe quel client SQL (SQuireL, pgAdmin …). Utiliser les paramètres de connexion suivants :

  • URL : jdbc:postgresql://localhost:5432/musicbrainz
  • Login : musicbrainz / musicbrainz

Le batch Java est désormais capable de récupérer les données à indexer.

Serveur Elasticsearch en local

Le batch se connecte à un cluster Elasticsearch. L’installation d’un cluster est donc nécessaire, que ce soit sur votre poste de développement ou sur une autre machine. Installer un serveur Elasticsearch est on ne peut plus simple. Quelques lignes de commandes suffisent. Pour davantage d’explications, je vous renvoie à l’article Premiers pas avec ElasticSearch de Tanguy Leroux.  Au vu de la volumétrie des données et de la faible charge, un seul nœud suffit amplement.

Le batch d’indexation

Le batch n’indexe pas toute la base de données MusicBrainz. Il se cantonne aux albums de musique qui sont un sous ensemble des release groups.  Seuls les albums « principaux » sont indexés. Single, EP, Compilation, Live ou autre Remix ne sont pas indexés.

Le batch d’indexation est composé d’un seul job Spring Batch. La configuration des beans d’infrastructure sur lesquels s’appuie le batch est répartie dans les fichiers applicationContext-datasource.xml, applicationContext-elasticsearch.xml et applicationContext-batch.xml. Y sont déclarés :

  • la source de données MusicBrainz et son gestionnaire de transaction,
  • un client Elasticsearch déclaré via la fabrique de beans Spring mise à disposition par David Pilato dans le projet spring-elasticsearch,
  • un JobRepository en mémoire et un JobLauncher Spring Batch.

Déclaré dans le fichier applicationContext-job.xml, le job musicAlbumJob  est décomposé en 4 étapes successives :

  1. Suppression d’un éventuel précédent index
  2. Création de l’index musicalbum
  3. Définition du type de document album
  4. Indexation dans Elasticsearch

La définition du job ne comporte aucune difficulté :

  <job id="musicAlbumJob" xmlns="http://www.springframework.org/schema/batch">
    <step id="deleteIndexIfExists" next="createIndexSettings">
      <tasklet ref="deleteIndexTasklet" />
    </step>
    <step id="createIndexSettings" next="createIndexMapping">
      <tasklet ref="createIndexSettingsTasklet" />
    </step>
    <step id="createIndexMapping" next="indexMusicAlbum">
      <tasklet ref="createIndexMappingTasklet" />
    </step>
    <step id="indexMusicAlbum">
      <!-- Executes partition steps locally in separate threads of execution -->
      <batch:partition step="indexMusicAlbumPartition" partitioner="partitionerMusicAlbum">
        <batch:handler grid-size="${batch.partition}" task-executor="batchTaskExecutor" />
      </batch:partition>
    </step>
  </job> 

A noter ligne 31 que le batch profite du mécanisme de partitionnement présenté dans le précédent billet Parallélisation de traitements batchs. Chacun des beans référencés par les steps sont définis dans le même fichier de configuration Spring. Les 3 premières étapes sont implémentés à l’aide de tasklets :

  <bean id="deleteIndexTasklet" class="com.javaetmoi.core.batch.tasklet.DeleteElasticIndexTasklet"
    p:esClient-ref="esClient" p:indexName="${es.index}" />

  <bean id="createIndexSettingsTasklet" class="com.javaetmoi.core.batch.tasklet.CreateElasticIndexSettingsTasklet"
    p:esClient-ref="esClient" p:indexName="${es.index}" p:indexSettings="${es.settings.filename}" />

  <bean id="createIndexMappingTasklet" class="com.javaetmoi.core.batch.tasklet.CreateElasticIndexMappingTasklet"
    p:esClient-ref="esClient" p:indexName="${es.index}" p:indexMapping="${es.mapping.filename}" p:mappingType="${es.mapping.type}" /> 

Utilisant l’API Java d’Elasticsearch, ces tasklets sont assez génériques pour être réutilisées sur d’autres projets. En attendant d’apporter qui sait ma contribution au projet spring-batch-elasticsearch d’Olivier Bazoud, je les ai mis à disposition dans la version 0.2 du projet spring-batch-toolkit.

A titre d’exemple, voici un extrait de la tasklet CreateElasticIndexSettingsTasklet :

public class CreateElasticIndexSettingsTasklet implements Tasklet {

    private static final Logger LOG = LoggerFactory.getLogger(CreateElasticIndexSettingsTasklet.class);

    private Client              esClient;

    private String              indexName;

    private Resource            indexSettings;
    
    @PostConstruct
    public void afterPropertiesSet() {
        Assert.notNull(esClient, "esClient must not be null");
        Assert.notNull(indexName, "indexName must not be null");
        Assert.notNull(indexSettings, "indexSettings must not be null");
    }    

    @Override
    public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
        LOG.debug("Creating the index {} settings", indexName);
        
        String source = IOUtils.toString(indexSettings.getInputStream(), "UTF-8");
        CreateIndexRequestBuilder createIndexReq = esClient.admin().indices().prepareCreate(indexName);
        createIndexReq.setSettings(source);
        CreateIndexResponse response = createIndexReq.execute().actionGet();
        if (!response.isAcknowledged()) {
            throw new RuntimeException("The index settings has not been acknowledged");
        }

        esClient.admin().indices().refresh(new RefreshRequest(indexName)).actionGet();

        LOG.info("Index {} settings created", indexName);
        return RepeatStatus.FINISHED;
    }

    /**
     * Sets the Elasticsearch client used to defined index settings.
     * 
     * @param esClient
     *            Elasticsearch client
     */
    public void setEsClient(Client esClient) {
        this.esClient = esClient;
    }

    /**
     * Sets the name of the index where documents will be stored
     * 
     * @param indexName
     *            name of the Elasticsearch index
     */
    public void setIndexName(String indexName) {
        this.indexName = indexName;
    }

    
    /**
     * Sets the JSON resource defining index settings.
     * 
     * @param indexSettings
     *            Spring resource descriptor, such as a file or class path resource.
     */    
    public void setIndexSettings(Resource indexSettings) {
        this.indexSettings = indexSettings;
    }
} 

Le bean de partition indexMusicAlbumPartition s’appuie quant à lui sur un chunk Spring Batch composé d’un reader, d’un writer et d’un processor composite :

  <!-- Read music albums from database then index them into ElasticSearch -->
  <batch:step id="indexMusicAlbumPartition">
    <tasklet transaction-manager="musicBrainzTransactionManager">
      <chunk reader="musicAlbumReader" processor="musicAlbumProcessor" writer="musicAlbumWriter"
        commit-interval="${batch.commit.interval}" retry-limit="3">
        <retryable-exception-classes>
          <include class="org.elasticsearch.client.transport.NoNodeAvailableException" />
          <include class="org.elasticsearch.transport.ReceiveTimeoutTransportException" />
        </retryable-exception-classes>
      </chunk>
      <listeners>
        <listener>
            <bean class="com.javaetmoi.core.batch.listener.LogStepListener" scope="step"
                p:commitInterval="${batch.commit.interval}"/>
        </listener>
      </listeners>
    </tasklet>
  </batch:step> 

Dans le fichier properties de configuration du batch, la taille des lots (commit-interval) est fixé à 5000 albums.

Le bean musicAlbumReader utilise la classe JdbcCursorItemReader de Spring Batch pour exécuter la requête SQL chargée de lire les albums. Cette requête effectue une jointure entre 10 tables et filtre sur des critères permettant de ramener un ResultSet dans lequel un album ne correspond qu’à une seule ligne. Aucune agrégation de lignes n’est donc à réaliser par le reader. L’enrichissement de l’album avec des données multi-valuées (ex : tags) est réalisé dans la phase de traitement.
Pour comprendre la requête, le modèle physique de données de MusicBrainz est consultable en ligne.

  <bean id="musicAlbumReader" class="org.springframework.batch.item.database.JdbcCursorItemReader" scope="step"
    p:dataSource-ref="musicBrainzDataSource" p:rowMapper-ref="albumRowMapper">
    <property name="sql">
      <value><![CDATA[
            SELECT 
                release_group.id AS albumId,                
                release_group.gid AS albumGid,
                release_group.type AS albumPrimaryTypeId,
                release_name.name AS albumName,
                artist_name.name AS artistName,
                artist.gid AS artistGid,
                artist.type as artistTypeId,    
                artist.begin_date_year artistBeginDateYear,
                artist.gender as artistGenderId,
                area.name as artistCountryName,
                artist_meta.rating artistRatingScore,
                artist_meta.rating_count artistRatingCount,
                release_group_meta.first_release_date_year albumYear,
                release_group_meta.rating albumRatingScore,
                release_group_meta.rating_count albumRatingCount 
            FROM
                artist
             INNER JOIN artist_credit_name
                ON artist_credit_name.artist = artist.id
             INNER JOIN artist_credit
                ON artist_credit.id = artist_credit_name.artist_credit
             INNER JOIN release_group
                ON release_group.artist_credit = artist_credit.id
             INNER JOIN release_name
                ON release_name.id = release_group.name
             INNER JOIN artist_name 
               ON artist.name = artist_name.id
             INNER JOIN area
               ON artist.area = area.id
             LEFT OUTER JOIN release_group_secondary_type_join
                ON release_group_secondary_type_join.release_group = release_group.id
             LEFT OUTER JOIN artist_meta
                ON artist.id = artist_meta.id    
             LEFT OUTER JOIN release_group_meta
                ON release_group_meta.id = release_group.id 
            WHERE
                release_group.type = '1'
                  AND artist_credit.artist_count = 1
                AND release_group_secondary_type_join.secondary_type IS NULL
                AND release_group.id >= ? and release_group.id <= ?
            ]]></value>
    </property>
    <property name="preparedStatementSetter">
      <bean class="org.springframework.batch.core.resource.ListPreparedStatementSetter">
        <property name="parameters">
          <list>
            <!-- SPeL parameters order is important because it referes to "where album_id >= ? and album_id <= ?" -->
            <value>#{stepExecutionContext[minValue]}</value>
            <value>#{stepExecutionContext[maxValue]}</value>
          </list>
        </property>
      </bean>
    </property>
  </bean> 

Le ResultSet est mappé à l’aide de la classe AlbumRowMapper implémentant l’interface RowMapper de Spring JDBC. Une instance de a classe Album est retournée en sortie du reader.

public class Album {
	
	private Integer id;
	
	private String gid;
	
	private String name;
	
	private ReleaseGroupPrimaryType type;
	
	private Integer year;
	
	private Rating rating = new Rating();
	
	private Artist artist = new Artist();
	
	private List<String> tags;  

A ce stade, la liste des tags utilisés dans MusicBrainz pour qualifier le genre musical d’un album est vide.

Le bean musicAlbumProcessor est composé de 2 traitements successifs matérialisés par 2 classes : EnhanceAlbumProcessor et MusicAlbumDocumentProcessor. La première exécute une requête JDBC pour charger les tags de l’album. Le 2nd transforme la classe Album en un document indexable dans Elasticsearch.

  <bean id="musicAlbumProcessor" class="org.springframework.batch.item.support.CompositeItemProcessor"> 
    <property name="delegates">
        <list>
            <bean class="com.javaetmoi.elasticsearch.musicbrainz.batch.item.EnhanceAlbumProcessor" />
            <bean class="com.javaetmoi.elasticsearch.musicbrainz.batch.item.MusicAlbumDocumentProcessor"
                p:documentType="${es.mapping.type}" />
        </list>
    </property>
  </bean> 

La classe MusicAlbumDocumentProcessor implémente indirectement l’interface ItemProcessor de Spring Batch. Elle prend en entrée un Album et le transforme EsDocument. La classe EsDocument modélise un document indexable dans Elasticsearch. Elle comporte un identifiant, un type, un contenu et éventuellement une version. Cette classe est suffisamment générique pour avoir été factorisé dans le projet spring-batch-toolkit.

public class EsDocument {

    private String          id;

    private String          type;

    private Long            version;

    private XContentBuilder contentBuilder;

    /**
     * EsDocument constructor.
     * 
     * @param type
     *            type of the Elasticsearch document
     * @param contentBuilder
     *            Elasticsearch helper to generate JSON content.
     */
    public EsDocument(String type, XContentBuilder contentBuilder) {
        this.type = type;
        this.contentBuilder = contentBuilder;
    }

    protected String getId() {
        return id;
    }

    /**
     * Set the ID of a document which identifies a document.
     * 
     * @param id
     *            ID of a document (may be <code>null</code>)
     */
    public void setId(String id) {
        this.id = id;
    }

    protected XContentBuilder getContentBuilder() {
        return contentBuilder;
    }

    protected String getType() {
        return type;
    }

    /**
     * Sets the version, which will cause the index operation to only be performed if a matching
     * version exists and no changes happened on the doc since then.
     * 
     * @param version
     *            version of a document
     * @see http://www.elasticsearch.org/blog/versioning/
     */
    protected void setVersion(Long version) {
        this.version = version;
    }
    
    protected boolean isVersioned() {
        return version !=null;
    }

    public Long getVersion() {
        return version;
    }

} 

Le type XContentBuilder fait partie de l’API Java d’Elasticsearch. Il permet de construire en mémoire la représentation d’un objet JSON. La classe abstraite EsDocumentProcessor  dont hérite MusicAlbumDocumentProcessor implémente le pattern template method et pilote la création du EsDocument. La construction de l’objet JSON a été réalisée manuellement en utilisant les méthodes startObject, field, array et endObject exposées par le XContentBuilder. Comme alternative, Jackson aurait  pu être utilisé pour sérialiser la classe Album en JSON.

Le bean musicAlbumWriter termine le traitement batch. Il utilise la fonctionnalité de requêtes en masse (bulk request) d’Elasticsearch pour indexer simultanément tous les documents lus dans un chunk (soit ici 5000). Factorisée elle aussi dans le projet spring-batch-toolkit, la classe EsDocumentWriter concentre le code :

/**
 * Index several documents in a single bulk request.
 */
public class EsDocumentWriter implements ItemWriter<EsDocument> {

    private static final Logger LOG = LoggerFactory.getLogger(EsDocumentWriter.class);

    private Client              esClient;

    private String              indexName;

    private Long                timeout;

    @PostConstruct
    public void afterPropertiesSet() {
        Assert.notNull(esClient, "esClient must not be null");
        Assert.notNull(indexName, "indexName must not be null");
    }

    @Override
    public final void write(List<? extends EsDocument> documents) throws Exception {
        BulkRequestBuilder bulkRequest = esClient.prepareBulk();
        for (EsDocument doc : documents) {
            IndexRequestBuilder request = esClient.prepareIndex(indexName, doc.getType()).setSource(
                    doc.getContentBuilder());
            request.setId(doc.getId());
            if (doc.isVersioned()) {
                request.setVersion(doc.getVersion());
            }
            bulkRequest.add(request);
        }
        BulkResponse response;
        if (timeout != null) {
            response = bulkRequest.execute().actionGet(timeout);
        } else {
            response = bulkRequest.execute().actionGet();
        }
        processResponse(response);
    }

    private void processResponse(BulkResponse response) {
        if (response.hasFailures()) {
            String failureMessage = response.getItems()[0].getFailureMessage();
            throw new ElasticSearchException("Bulk request failed. First failure message: " + failureMessage);
        }
        LOG.info("{} documents indexed into ElasticSearch in {} ms", response.getItems().length,
                response.getTookInMillis());
    }

    /**
     * Sets the Elasticsearch client used for bulk request.
     * 
     * @param esClient
     *            Elasticsearch client
     */
    public void setEsClient(Client esClient) {
        this.esClient = esClient;
    }

    /**
     * Sets the name of the index where documents will be stored.
     * 
     * @param indexName
     *            name of the Elasticsearch index
     */
    public void setIndexName(String indexName) {
        this.indexName = indexName;
    }

    /**
     * Waits if necessary for at most the given time for the computation to complete, and then
     * retrieves its result, if available.
     * 
     * @param timeout
     *            the maximum time in milliseconds to wait
     */
    public void setTimeout(Long timeout) {
        this.timeout = timeout;
    }

} 

En sortie, voici un exemple du document JSON représentant l’album “Achtung Baby” du groupe U2 :

Mapping Elasticsearch

Comme expliqué précédemment, le batch est chargé de créer l’index musicalbum. Outre le nombre de shards et de réplicas, le fichier es-index-settings.json déclare les filtres et les analyseurs utilisés pour indexer puis rechercher des albums.
Le filtre myEdgeNGram et l’analyseur myPartialNameAnalyzer sont par exemple utilisés par l’autosuggestion des résultats de recherche :

 "analysis": {
          "filter": {
            "myEdgeNGram": {
              "side": "front",
              "max_gram": 10,
              "min_gram": 1,
              "type": "edgeNGram"
            }
          },
          "analyzer": {
            "myStandardAnalyzer": {
              "filter": [
                "standard",
                "lowercase",
                "asciifolding"
              ],
              "type": "custom",
              "tokenizer": "standard"
            },
            "myPartialNameAnalyzer": {
              "filter": [
                "standard",
                "lowercase",
                "asciifolding",
                "myEdgeNGram"
              ],
              "type": "custom",
              "tokenizer": "standard"
            },

Le fichier es-index-mappings.json  précise à Elasticsearch comment indexer les différents champs de l’EsDocument construit à partir d’un Album. Ce sont les usages de recherche qui guident la réalisation du fichier de mapping. Par exemple, le nom d’un album sera indexé de 2 manières à l’aide d’une propriété de type multi_field : l’une pour la recherche fulltext et l’autre pour l’autosuggestion.

 {
  "_source": {
    "enabled": "true",
    "compress": "true"
  },
  "properties": {
    "id": {
      "type": "string",
      "analyzer": "myIdAnalyzer"
    },
    "name": {
      "type": "multi_field",
      "fields": {
        "name": {
          "type": "string",
          "analyzer": "myStandardAnalyzer"
        },
        "start": {
          "search_analyzer": "myStandardAnalyzer",
          "index_analyzer": "myPartialNameAnalyzer",
          "type": "string"
        }
      }
    },
    "year": {
      "type": "multi_field",
      "fields": {
        "year": {
          "type": "date",
          "format": "year"
        },
        "string": {
          "type": "string",
          "analyzer": "myBasicAnalyzer"
        }
      }
    },
    "rating": {
      "type": "object",
      "properties": {
        "score": {
          "type": "integer"
        },
        "count": {
          "type": "integer"
        }
      }
    },
    "tags": {
      "type": "string",
      "index_name": "tag"
    },
    "artist": {
      "type": "object",
      "properties": {
        "name": {
          "type": "multi_field",
          "fields": {
            "name": {
              "type": "string",
              "analyzer": "myStandardAnalyzer"
            },
            "start": {
              "search_analyzer": "myStandardAnalyzer",
              "index_analyzer": "myPartialNameAnalyzer",
              "type": "string"
            }
          }
        },
        "type_id": {
          "type": "integer"
        },
        "type_name": {
          "type": "string"
        },
        "begin_date_year": {
          "type": "date",
          "format": "year"
        },
        "country_name": {
          "type": "string",
          "analyzer": "myStandardAnalyzer"
        },
        "gender": {
          "type": "string",
          "analyzer": "myBasicAnalyzer"
        },
        "rating": {
          "type": "object",
          "properties": {
            "score": {
              "type": "integer"
            },
            "count": {
              "type": "integer"
            }
          }
        }
      }
    }
  }
}

Tests unitaires

Avant d’exécuter le batch sur la base de données MusicBrainz, le test unitaire TestMusicAlbumJob m’aura permis d’éprouver le code. La structure du schéma de la base MusicBrainz est reproduite dans une base de données en mémoire H2. Elle est alimentée avec la discographie de U2. Pour se faire, la librairie open source DbSetup a été mise une nouvelle fois à contribution. Une instance Elasticsearch embarquée est démarrée par le test. Le batch est exécuté. Le test vérifie simplement que le nombre de documents indexés correspond au nombre d’albums de U2. En complément, l’exécution d’une requête de recherche aurait permis de valider le mapping.

Exécution du batch

Comme son nom l’indique, la classe IndexBatchMain fournit la méthode main permettant d’exécuter le batch en ligne de commande. Quelques étapes suffisent :

  1. Démarrer un serveur Elasticsearch
  2. Démarrer la base de données MusicBrainz database ou la VM l’hébergeant
  3. git clone https://github.com/arey/musicbrainz-elasticsearch.git
  4. Personnaliser si besoin le fichier es-musicbrainz-batch.properties
  5. mvn install
  6. mvn exec:java

Quelques minutes plus tard, quelques 265 169 albums sont indexés.  

Démo

Pour exploiter l’index nouvellement créé, rien de tel qu’une petite interface en HTML 5. Pour se faire, Lucian Precup m’a autorisé à adapter une page qu’il avait mis au point dans le cadre de l’atelier Construisons un moteur de recherche tenu lors de la Scrum Day 2013. Réalisée en AngularJS, jQuery et Boostrap, cette page propose une zone de recherche full-text, offre de l’autosuggestion et affiche le résultat de recherche de manière paginée. Quelques filtres et directives Angular ont été ajoutés pour, par exemple, gérer les appréciations des mélomanes.  La capture d’écran  ci-dessous donne un aperçu du rendu graphique :

workshop-demo-screenshot
Déployée sur OVH, l’application Angular est accessible à l’adresse http://musicsearch.javaetmoi.com/ 

Requêtes de recherche

La recherche utilisée pour l’autosuggestion repose sur une query_string analysant le nom de l’album, le nom de l’artiste et la date de sortie de l’album. Pour les noms, elle utilise 2 champs : celui pour la recherche exacte (ex: artist.name) et celui pour la recherche de type « commence par » (ex : artist.name.start). La surbrillance est activée sur les 3 critères.
Le gist  7436834 propose la commande curl équivalente :

curl -XPOST 'http://es.javaetmoi.com/musicalbum/album/_search?pretty' -d ' 
{
  "fields": [
    "artist.name",
    "id",
    "name",
    "year.string"
  ],
  "query": {
    "query_string": {
      "fields": [
        "name",
        "name.start",
        "year.string",
        "artist.name",
        "artist.name.start"
      ],
      "query": "U2",
      "use_dis_max": false,
      "auto_generate_phrase_queries": true,
      "default_operator": "OR"
    }
  },
  "highlight": {
    "number_of_fragments": 0,
    "pre_tags": [
      "<b>"
    ],
    "post_tags": [
      "</b>"
    ],
    "fields": {
      "artist.name": {},
      "name": {},
      "year.string": {}
    }
  }
}' 

Voici un extrait du résultat retourné par Elasticsearch:

{
  "took" : 13,
  "timed_out" : false,
  "_shards" : { "total" : 1,  "successful" : 1,  "failed" : 0  },
  "hits" : {
    "total" : 22,
    "max_score" : 9.11103,
    "hits" : [ {
      "_index" : "musicalbum",
      "_type" : "album",
      "_id" : "c6b36664-7e60-3b3e-a24d-d096c67a11e9",
      "_score" : 9.11103,
      "fields" : {
        "id" : "c6b36664-7e60-3b3e-a24d-d096c67a11e9",
        "artist.name" : "U2",
        "name" : "War"
      },
      "highlight" : {
        "artist.name" : [ "U2" ]
      }
    }, …

La recherche fulltext utilise quant à elle le type de recherche fuzzy_like_this permettant une recherche approximative sur le  nom de l’album, le nom de l’artiste et la date de sortie de l’album. Trois facettes de types différents permettent d’afficher la répartition du nombre de résultats en fonction du type d’artiste (terms facet), des appréciations (histogram facet) et de la décennie (range facet).
Le gist  7436893 présente la commande curl équivalente :

curl -XPOST 'http://es.javaetmoi.com/musicalbum/album/_search?pretty' -d ' 
{
  "from": 0,
  "size": 10,
  "query": {
    "bool": {
      "must": [
        {
          "fuzzy_like_this": {
            "fields": [
              "name",
              "artist.name",
              "year.string"
            ],
            "like_text": "U2 war",
            "min_similarity": 0.7,
            "prefix_length": 1
          }
        }
      ]
    }
  },
  "facets": {
    "artist_type": {
      "terms": {
        "field": "artist.type_id"
      }
    },
    "album_rating": {
      "histogram": {
        "key_field": "rating.score",
        "interval": 20
      }
    },   
    "album_year": {
      "range": {
        "field": "year",
        "ranges": [
          { "to": 1970},
          {  "from": 1970, "to": 1980},
          {  "from": 1980, "to": 1990},
          {  "from": 1990, "to": 2000},
          {  "from": 2000, "to": 2010},
          {  "from": 2010 }
        ]
      }
    }
  }
}' 

 Voici un extrait du résultat retourné par Elasticsearch :

{
  "took" : 57,
  "timed_out" : false,
  "_shards" : { "total" : 1, "successful" : 1, "failed" : 0 },
  "hits" : {
    "total" : 539,
    "max_score" : 5.985128,
    "hits" : [ {
      "_index" : "musicalbum",
      "_type" : "album",
      "_id" : "c6b36664-7e60-3b3e-a24d-d096c67a11e9",
      "_score" : 5.985128, "_source" : {"id":"c6b36664-7e60-3b3e-a24d-d096c67a11e9","name":"War","year":1983,"tags":["rock","album rock","alternative pop/rock","classic pop and rock","pop/rock"],"rating":{"score":79,"count":9},"artist":{"name":"U2","id":"a3cb23fc-acd3-4ce0-8f36-1e5aa6a18432","type_id":2,"type_name":"Group","begin_date_year":"1976","country_name":"Ireland","rating":{"score":87,"count":21}}}
    }, ... ]
  },
  "facets" : {
    "artist_type" : {
      "_type" : "terms",
      "missing" : 6, "total" : 533, "other" : 0,
      "terms" : [ {
        "term" : 2,
        "count" : 407
      }, {
        "term" : 1,
        "count" : 120
      }, {
        "term" : 3,
        "count" : 6
      } ]
    },... ]
    }
  }
}

Dans le Cloud avec OpenShift

A la recherche d’un hébergeur me permettant d’installer mon index en ligne, je suis tombé sur le billet Searching with ElasticSearch on OpenShift de Marek Jelen, évangéliste OpenShift. C’était l’occasion de découvrir l’offre Cloud de RedHat, et cela sans sortir ma carte bancaire. En effet, OpenShift offre 3 Gems limitées à 512 Mo de RAM et de 1 Go d’espace disque. Avec un index de 160 Mo, c’était amplement suffisant.
Elasticsearch et Nginx sur OpenShift

Les explications du billet sont claires. Parti du cartouche Do-It-Yourself 0.1 contenant une simple distribution Linux, l’installation d’Elasticsearch se fait classiquement. Des variables systèmes prédéfinies doivent être utilisées pour spécifier l’adresse IP (OPENSHIFT_DIY_IP), le port HTTP (OPENSHIFT_DIY_PORT) et le répertoire d’installation (OPENSHIFT_DATA_DIR).
Si vous le souhaitez, l’installation des plugins eshead et bigdesk est possible.

Afin de résoudre l’exception BindException[Address already in use] au démarrage d’Elasticsearch, j’ai suivi les préconisations postées dans un commentaire par ewindsor.

Une fois Elasticsearch démarré, seul le port HTTP est accessible depuis Internet. C’est le port utilisé par l’IHM de recherche. Le port utilisé par le client TCP Elastisearch n’est quant à lui pas accessible. Le Batch d’indexation s’exécutant en local ne peut donc pas alimenter directement le cluster Elasticsearch. Par facilité, je me suis contenté d’uploader par SFTP mon index local (répertoire data\musicbrainz)  sur le serveur OpenShift.
Un redémarrage d’Elasticsearch et l’index est visible via Eshead :

Index musicbrainz vu dans Eshead

Le plugin Jetty pour Elasticsearch et le cartouche Nginx pour OpenShift permettent de sécuriser l’accès au serveur Elasticsearch, rendant possible la configuration d’un reverse proxy avec authentification basic HTTP.

Pour terminer, OpenShift permet d’associer un nom de domaine à une Gem.  Ainsi, le nom de domaine es.javaetmoi.com pointe sur le serveur Nginx.

L’application Angular http://musicsearch.javaetmoi.com/  fait appel à l’API REST d’Angular exposée sur l’URL http://es.javaetmoi.com/musicalbum/album/_search

Conclusion

Ce billet nous aura permis d’aborder de bout en bout la mise en ligne d’une application basée sur Elasticsearch : de l’indexation des données par batch à leur consultation dans votre  navigateur.
En moins d’une heure, l’index Elasticsearch aura été mis en ligne sur OpenShift, le PaaS / IaaS de Redhat.  La disponibilité d’un cartdrige OpenShift pour Elasticsearch permettrait d’accélérer son déploiement. A noter que mon cluster Elasticsearch n’est formé que d’un seul nœud. Je n’ai pas vérifié s’il était possible d’installer un cluster Elasticsearch sur plusieurs serveurs.

Vous l’aurez remarqué, l’index musicalbum créé par le batch est figé. Pour aller plus loin, il aurait été intéressant d’automatiser sa mise à jour régulière. La base de données Musicbrainz est capable de se synchroniser toutes les heures avec la base principale. Il serait donc possible de reconstruire périodiquement l’index en utilisant, par exemple, un mécanisme d’alias pour ne pas interrompre le service de recherche. La base répliquée et le batch aurait pu être installés sur une 3ième Gem OpenShift. Resterait alors à régler la communication entre le batch et le serveur Elasticsearch. RedHat a dû prévoir la possibilité d’ouvrir un port entre 2 Gems. Dans le cas contraire, un client Java utilisant l’API REST d’indexation permettrait de contourner le blocage du port utilisé pour la communication TCP d’Elasticsearch.

]]>
https://javaetmoi.com/2013/11/musicbrainz-elasticsearch-angularjs-openshift/feed/ 1
AngularJS à Devoxx France 2013 https://javaetmoi.com/2013/04/angularjs-devoxx-france-2013/ https://javaetmoi.com/2013/04/angularjs-devoxx-france-2013/#respond Tue, 02 Apr 2013 16:44:53 +0000 http://javaetmoi.com/?p=667 Continuer la lecture de AngularJS à Devoxx France 2013 ]]> Université AngularJS ou le futur du développement Web à devoxxx France 2013

A deux semaines de sa première formation en entreprise sur AngularJS, répétition générale pour Thierry Chatel devant 200 développeurs avides d’en apprendre un peu plus sur le dernier né des frameworks JavaScript de Google. Développeur Java / Swing chez IBM au début des années 2000, Thierry s’est ensuite dirigé vers du conseil en architecture avant de découvrir AngularJS durant l’été 2011. Depuis, il y consacre beaucoup d’énergie et anime notamment le site FrAngular.com, premier blog francophone dédié à ce framework.
Assez parlé de sa personne, lui-même n’en serait que trop gêné.

Comme de nombreux développeurs venus assister à cette conférence, j’étais curieux de découvrir à mon tour le framework qui avait fait autant parlé de lui lors de Devoxx World 2012. Et autant vous l’annoncer dès maintenant : je n’ai pas été déçu.

D’une durée de 3h, cette Université intitulée AngularJS, ou le futur du développement Web s’est décomposée en une première partie théorique suivie d’une seconde plus pratique basée sur différents types d’applications : Last Tweets, directive Google Maps et Game Store. Live coding et démos furent au rendez-vous.
Pour la petite anecdote, les slides de la présentation sont écrits avec la syntaxe Markdown et sont interprétés par l’outil Angular Showoff reposant, vous l’aurez deviné, sur Angular. L’intérêt majeur est qu’ils peuvent embarquer du code Angular, pratique pour les démos in-slides telles que :

Your name: 
Hello {{me}}!

La fonctionnalité clé

La fonctionnalité phare que recherchait notre speaker dans un framework JavaScript était le data binding bi-directionnel entre le modèle et les vues. C’est donc ce qu’il a particulièrement apprécié dans Angular et mis en avant dès le début de sa présentation.
Par data binding bi-directionnel, on entend qu’une mise à jour du modèle JavaScript soit répercutée sur l’IHM et, inversement, qu’une interaction de l’utilisateur (ex : saisie dans un champ texte) soit aussitôt reflétée dans le modèle. C’est le comportement que l’on retrouve typiquement côté serveur avec des technologies telles JSF entre la vue xhtml et les beans managés.
La majorité de ses concurrents JavaScript n’assurent le binding que dans un seul sens : des données du modèle vers la vue HTML. En général, l’opération inverse doit être implémentée manuellement en JavaScript en s’abonnement aux évènements du DOM, par exemple à l’aide de JQuery.
Angular permet de gérer nativement ce binding qui, jusque-là, faisait défaut au HTML. Concrètement, cela se traduit par drastiquement moins de code JavaScript de manipulation du DOM et, implicitement, une compréhension du code et une maintenance facilitée. Je confirme que, dans les applications présentées lors de ce show, nous n’avons pas vu une ligne de ce type de code.
Autre aspect en faveur d’Angular : le modèle JavaScript ainsi que les différents services JS écrits par le développeur ne requièrent aucune adhérence avec Angular. On retrouve le côté POJO de Java et des beans Spring. C’est ce qui donne son petit côté magique au framework.

Le mécanisme

Le mécanisme sous-jacent au data binding est introduit par Thierry au travers du slogan d’Angular « HTML enhanced for webapps ». En effet, le parti pris d’AngularJS est d’étendre le HTML, de l’enrichir avec des directives Angular, à savoir des tags et des attributs supplémentaires ainsi que des expressions entre {{double accolades}}. La page HTML fait elle-même office de Template.
Au démarrage de l’application, lors d’une phase de compilation, le framework parcourt le DOM à la recherche de ces balises puis instrumente le code HTML afin d’assurer le binding.  A partir de ce point, modèles et vues sont synchronisés en continue.
La force d’Angular réside dans le fait qu’il n’est nul besoin d’utiliser JavaScript pour déclarer un template. C’est la page HTML qui est étendue pour jouer ce rôle.

Sous le capot d’Angular, la détection des changements côtés vue s’appuie sur des évènements du DOM (ex : onclick, onkeyup) ou extérieurs (ex : timeout, réponse Ajax).
Par contre, les modifications du modèle sont détectées par un mécanisme bien spécifique : le dirty checking. A ce que j’en ai compris, il permet de surveiller tout objet JavaScript, de détecter qu’une valeur a changé puis d’exécuter du code JS. J’entrevoie une similitude avec le fonctionnement d’Hibernate qui surveille les changements opérés sur les objets attachés à sa session, ceci pour les refléter en base de données.

Le dirty checking s’appuie sur des watches, des listeners à l’écoute des changements. Lors de la compilation du template, Angular positionne automatiquement des watches sur les expressions et les tags HTML bindés avec un modèle. Pour des problématiques de performance, Thierry nous alerte sur le fait que l’expression surveillée ne doit pas être trop couteuse à évaluer.
Pour les objets « non managés » par Angular (issus par exemple d’API tierces), le développeur peut déclarer des watches de manière impérative.  Cette possibilité a été illustrée lors du développement d’une directive Angular utilisant l’API Google Maps. Le but était de surveiller le déplacement du centre de la carte via la souris de l’utilisateur et, le cas échéants, de mettre à jour les 2 champs de saisie correspondant à la latitude et la longitude, champs par ailleurs modifiables au clavier.

Syntaxe utilisée pour ajouter un watcher :

scope.$watches('center', function () { map.setCenter(scope.center); }, true);

L’objet center possédant 2 propriétés, le paramètre booléen true précise à Angular qu’il doit surveiller l’objet en profondeur.

Pour terminer sur ce sujet, notre orateur nous laisse rêveur en nous annonçant que ce mécanisme de dirty-checking est à l’étude au sein du W3C sous le nom de Object.observe() et que certaines versions avant-gardistes de navigateurs telles Chrome Canari ou Chronium l’implémentent déjà nativement, multipliant ainsi par 20 les performances actuelles du dirty-checking d’Angular.

Les autres concepts

Bien entendu, la force d’Angular ne se limite pas au seul data binding bi-directionnel. Le framework met à disposition de nombreux concepts bien connus des développeurs Java :

  • MVC : le pattern Modèle Vue Contrôleur fait partie intégrante d’Angular. Le HTML enrichi représente la vue. Des actions positionnées sur la vue permettent de faire appel à des contrôleurs JavaScript. Ces derniers exécutent le code applicatif puis mettent les données à disposition de la vue par l’intermédiaire d’un contexte. Thierry nous explique que les contrôleurs peuvent fonctionner sans la vue, ce qui est intéressant pour les tests. Une mauvaise pratique est donc de manipuler le DOM dans le contrôleur. Il nous rappelle aussi que le pattern MVC permet d’avoir plusieurs vues pour les mêmes contrôleurs / données ; une vue pouvant ainsi être dédiée aux appareils mobiles.
  • Injection de dépendance : les amateurs de Spring, de Guice ou de CDI retrouveront avec plaisir ce pattern dans Angular. Lors de l’appel d’une méthode d’un contrôleur, Angular lui injecte en paramètre les services dont il a besoin. Ces services peuvent être fournis par Angular (ex : $http pour des appels REST) ou par le développer qui les aura préalablement configurés à l’aide d’un provider. Pour les curieux, Thierry rentre dans le détail de l’implémentation en expliquant qu’Angular se base sur la signature des méthodes pour déterminer le service à injecter. Le nom des paramètres est interprété comme le nom du service à injecter. Cette approche pose problème lorsque le code JS est minifié. Il devient alors nécessaire de déclarer explicitement le nom des services à injecter en passant par un tableau de chaînes de caractères.
  • Service : tout objet JS peut être mis à disposition du reste de l’application  en tant que service. Outre le fait de pouvoir mutualiser du code faisant, par exemple, appel à des services REST du backend, les services permettent de conserver des données entre 2 contrôleurs. Thierry prend l’exemple d’un panier d’achat issu de l’application Game Store.
  • Navigation : la navigation entre les différentes vues s’appuie sur le mécanisme de routes bien connu des développeurs PlayFramework. Boutons précédents, suivants et marque-pages sont gérés nativement.
  • Scope : en Java, que ce soit en JSF ou avec Spring, les beans ont une portée (un scope). Dans Angular, cette notion prend un autre sens. Il faut le voir comme un contexte d’exécution qui peut être propre à une directive Angular. Les scopes sont utilisés lors de l’évaluation des  {{ expressions }}. Lorsque l’objet référencé n’est pas présent dans le contexte courant, Angular va le chercher dans le contexte englobant, et ainsi de suite, en remontant la hiérarchie dans le DOM. Cela me fait penser aux EL de l’API Servlet qui utilisent dans l’ordre les portées page, request, session et application.
  • Filtre : dans Angular, les filtres ne sont pas à rapprocher des filtres JavaEE ou Spring Security, mais aux pipes Unix. Thierry a utilisé un filtre pour parser le texte d’un tweet et y ajouter une balise <a> lorsqu’une URL est détectée. Angular semble disposer d’un ensemble de filtres prêts à l’emploi.

Les tests

Lors de cette présentation, l’outillage concernant les tests n’a pas été oublié.
Pour les tests fonctionnels, Karma (ex-Testacular) permet d’exécuter des tests dans les navigateurs connectés à l’outil. Une démo a été réalisée sous nos yeux avec Chrome, Firefox, IE et depuis un Smartphone. Karma présente l’avantage d’utiliser le moteur JavaScript des différents navigateurs connectés.
Tel infinitest, Karma relance les tests à chaque sauvegarde du code.
Créé par Vojta Jína, développeur principal d’Angular, Karma connait le fonctionnement d’AngularJS. Cela lui permet de résoudre de manière transparente le problème des requêtes Ajax souvent rencontré dans les tests Selenium.

Du point de vue des tests unitaires, Angular permet de tester des contrôleurs et des services JS nécessitant de faire appel à un serveur. Pour se faire, Angular fourni le mock object _$httpBackend_ qui permet de simuler un appel au serveur et de retourner le jeu de données nécessaire au test (ex : au format JSON).

Enfin, dans le cadre d’un débogage, le plugin Batarang pour Chrome facilite l’inspection du DOM et des scopes.

La cible

D’après notre speaker, Angular cible avant tout les applications de gestion qui font habituellement recourt à de multiples champs de saisie.
Dédié aux applications mono-pages (SPA), il ne le recommande pas pour les sites web, d’autant plus qu’ils ne seraient pas indexables dans Google. Un paradoxe.
A noter qu’Angular est un framework qui s’utilise uniquement côté client, à savoir dans le navigateur, et qui n’a donc aucun sens à être embarqué côté serveur (ex : dans Node.JS).

Encore jeune, très peu d’applications visibles sur le Net reposent sur Angular. Thierry nous cite tout de même quelques exemples :

  • le nouveau manager d’OVH,
  • le site de Google « Doubleclick for publisher » dédié aux professionnels de la publicité,
  • et le site Youtube de la PS3.

Les limites

Thierry nous parle de la limite des 2000 watches. Limite que l’on peut facilement dépasser si l’on n’y prête pas attention et que l’on dispose d’une machine puissante. Ce seuil est d’autant plus valable sur les mobiles. Une astuce consiste à ne surveiller que les données visibles par l’utilisateur et à désactiver les watches sur les expressions qui ne varieront jamais.

Le framework Angular ne dispose pas de composants UI. Il permet de réutiliser des composants JQuery UI, mais un adaptateur est nécessaire. Des librairies externes telles qu’AngularUI ou AngularStrap commencent à voir le jour.

Par ailleurs, en tant que framework, Angular est particulièrement structurant. Simple librairie, JQuery l’est beaucoup moins. Une application basée sur Angular doit donc adhérer complètement à sa philosophie ; il n’est pas intéressant de n’utiliser que certaines briques. A noter tout de même qu’il est possible de n’utiliser Angular que sur une partie de son application web et de le positionner, par exemple, que sur un simple div.

Lors de la série des questions/réponses, un développeur de l’assistance fait remarquer qu’Angular demande une syntaxe spécifique pour fonctionner sous IE 7. Son fonctionnement sur d’anciens navigateurs semble donc nécessiter quelques adaptations.

Lors de cette session, certaines fonctionnalités indispensables à toute IHM n’ont pas été abordées. C’est par exemple le cas de la validation des données saisies dans les formulaires.

Conclusion

Malgré sa jeunesse, AngularJS semble chambouler le foisonnant écosystème des frameworks JavaScript (Backbone.js, Knockout, Ember.js). Certains le voient déjà comme l’inspirateur d’ HTML 6. Avec DART, GWT et Angular, Google possède plusieurs poulains. Reste à savoir lequel sera le plus endurant ?

Pour ma part, outre l’omniprésent JQuery, je n’avais encore jamais pris le temps d’étudier l’un de ces frameworks JavaScript qui détrônent progressivement les technologies côté serveur.
Cette université m’aura conforté dans mon choix de regarder de plus près ce que propose AngularJS.
D’après Thierry, apprendre correctement le JavaScript est un pré-requis. L’apprécier en est un second. Avant d’aborder les tutoriaux sur Angular, je vais donc commencer pas ressortir le livre JavaScript, gardez le meilleur de Douglas Crockford.

Références :

]]>
https://javaetmoi.com/2013/04/angularjs-devoxx-france-2013/feed/ 0