Angular JS, épisode 3 : il fait le REST (ou comment marier Angular et CXF)

Commençons par un résumé des épisodes précédents : après une présentation succincte d’Angular, nous avons développé une petite application de type « grille et détail » permettant d’éditer des objets téléphones.

Dans cet article, Henri Darmet, toujours lui, vous propose de connecter cette mini-réalisation à un véritable back-end en utilisant la technologie de communication phare du RIA : les services REST.

Angular JSLe tutoriel officiel d’Angular expose deux solutions, une « basique », à usage général, juste REST, et l’autre, plus spécialisée dans l’édition de données (la solution RESTful). L’ambition,  à moyen terme, étant de faire une application CRUD, intéressons nous tout de suite à la seconde, plus adaptée.

L’idée derrière tout ça ? Mesurer l’effort nécessaire pour avoir une chaine qui fonctionne de bout en bout dans un vrai environnement, c’est-à-dire avec de vrais services REST servis par un vrai serveur Java WEB. Et comme on pouvait s’y attendre, c’est bien plus délicat que d’exécuter les exemples du tutoriel, affutés pour gommer les difficultés.

Phase 1 :  développer un service RESTful

C’est là que CXF entre en jeu.

Bien sûr, je commence à faire le tour des articles proposés par Google à la recherche d’un exemple concis et complet sur la manière d’intégrer Angular et CXF. Chou Blanc. Donc voici un (le premier ?) article pratique sur le mariage Angular-CXF. J’espère qu’il vous sera utile.

Le corps de mon service est très simple : pas de Spring, pas d’Hibernate, ce n’est pas le sujet. Il me suffit d’être sûr que du code Java a été exécuté à la demande d’Angular et que les entrées comme les sorties de mes services sont plausibles. Je simule la base de données par une simple HashMap.

Cela donne :

=========== rest.PhoneService.java
package rest;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;

@Consumes("application/json")
@Produces("application/json")
public class PhoneService {
static int ID=0;
Map phones = new HashMap();
public PhoneService() {
phones.put("nexus", new Phone("nexus", "Nexus", "Nexus snippet"));
phones.put("iphone", new Phone("iphone", "IPhone", "IPhone snippet"));
}
@GET
@Path("/phones")
public Collection getPhones() {
return new ArrayList(phones.values());
}
}
=================================

Quelques annotations parsèment le code. Notez la présence de @Consumes(« application/json ») et @Produces(« application/json ») qui indiquent que l’on va échanger uniquement du JSON (et non du XML). Notez bien car c’est ici que se noue le drame que nous verrons en seconde partie de cet article.

La méthode de service elle-même est précédée par @GET et @Path(« /phones ») qui indiquent respectivement que cette méthode sera appelée à l’aide de la méthode Http GET et activée sur détection de l’URL http://<monserveur>/<monappli>/rest/services/phones. Rien que du très classique.

Ce n’est pas tout à fait suffisant : il faut aussi définir ce qu’est en Java, un objet téléphone.

Manque vite réparé par la classe suivante :

=========== rest.PhoneService.java
package rest;

import java.io.Serializable;
import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement
public class Phone implements Serializable {
private static final long serialVersionUID = 1L;
String id;
String name;
String snippet;
public Phone() {}
public Phone(String id, String name, String snippet) {
super();
this.id = id;
this.name = name;
this.snippet = snippet;
}

public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getSnippet() {
return snippet;
}

public void setSnippet(String snippet) {
this.snippet = snippet;
}
}
=================================

On est dans le JavaBean pur et dur. Le seul trait remarquable est l’utilisation de l’annotation XmlRootElement indispensable à JAXB pour comprendre que ce que l’on veut sérialiser/désérialiser, ce sont les objets téléphones. Je ne peux m’empêcher de penser que JAXB aurait pu le deviner tout seul, mais bon.

Phase 2 : installer le WebService

Comme je n’ai pas installé Spring, je me rabats sur la solution « Sans spring » qui est aussi mal documentée que possible sur le site Apache. Manifestement, Google pourrait donner de précieux conseils aux contributeurs documentaires d’Apache CXF. Bref, je finis par comprendre qu’il suffit d’inclure dans le web.xml, les lignes suivantes :

======== web.xml
<servlet>
<display-name>CXFNonSpringJaxrsServlet</display-name>
<servlet-name>CXFNonSpringJaxrsServlet</servlet-name>
<servlet-class>org.apache.cxf.jaxrs.servlet.CXFNonSpringJaxrsServlet
</servlet-class>
<init-param>
<param-name>jaxrs.serviceClasses</param-name>
<param-value>rest.PhoneService</param-value>
</init-param>
<init-param>
<param-name>jaxrs.address</param-name>
<param-value>/services</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>CXFNonSpringJaxrsServlet</servlet-name>
<url-pattern>/rest/*</url-pattern>
</servlet-mapping>
=================================

Notez la référence à ma classe « rest.PhoneService » dans le paramètre jaxrs.serviceClass.

Phase 3 : implémenter l’appel au service au sein d’Angular

C’est l’intérêt principal de cet article !

Un très bon chapitre du tutoriel (vers la fin) explique comment faire. Je résume ici :

  • écrire un composant service qui implémente l’appel
  • importer ce service dans l’application
  • réaliser l’appel au service dans le controller PhoneListCtrl

Pour le premier point, je copie/colle/simplifie l’exemple du tutoriel, ce qui donne, dans un nouveau fichier script nommé services.js :

angular.module('phonecatServices', ['ngResource'])
.factory('Phone', function($resource){
return $resource('rest/services/phones', {}, {
query: {method:'GET', isArray:true}
});
});

C’est court, mais c’est abscons. J’explique. Nous créons un nouveau module (un peu l’équivalent du « package » java) qui a comme vocation de contenir tous nos appels de service. Ce module s’appelle « phonecatServices ». Il ne s’agit pas d’un module indépendant : il va utiliser des ressources définies dans un autre module du core Angular, nommé « ngResource », en l’occurrence la méthode $resource qui donne accès aux services RESTful.

Nous ajoutons à notre module, une fonction de construction (factory) d’objets de type « Phone ». Ainsi, chaque fois qu’un composant aura besoin qu’on lui injecte un objet nommé « Phone », Angular appellera cette méthode pour construire cet objet. La création de l’objet final « Phone » est finalement délégué à l’objet $resource. Pour cela, on lui passe quelques paramètres :

  • L’url d’appel est « rest/services/phones » et elle ne doit pas être, au préalable, complétée par des paramètres (d’où l’objet {} qui suit l’expression de l’URL).
  • On veut faire appel à une méthode que nous appellerons query() et cette méthode retournera un objet de type tableau d’entités (ici de téléphones).

Cette façon de faire peut sembler un peu déroutante : il faut un objet Phone pour obtenir une liste d’objets Phone. Eh oui, c’est ainsi que fonctionne l’objet $resource d’Angular. « Alors quel objet Phone utiliser quand on n’en a pas d’autre ? » Eh bien un objet Phone qui ne correspond à aucun objet téléphone réel, généré par la factory, sans nom, ni snippet, juste un objet « point de départ ». Phone est donc à la fois l’entité et le DAO. Cela ne vous rappelle rien ? Eh oui, il s’agit d’une implémentation du pattern « active records ».

Pour importer ce service, il nous faut définir un objet module propre à l’application en général et indiquer que cet objet module dépend du module des services. Créons pour cela un autre script, appelé « app.js » et ne contenant qu’une seule ligne :

angular.module('phonecat', ['phonecatServices']);

Le module de notre  application se nomme donc « phonecat ». Comment notre page HTML va-t-elle le savoir ? Très simplement, cette information est accrochée à la déclaration « ng-app » :

<html ng-app='phonecat'>
<head>
…

Bien sûr, il ne faut pas oublier,  dans le header, le chargement de nos nouveaux scripts :

<script src="libjs/angular/angular.js"></script>
<script src="libjs/angular/angular-resource.js"></script>

<script src="js/controllers.js"></script>
<script src="js/app.js"></script>
<script src="js/services.js"></script>

Tiens ? Qu’est-ce que cette ligne faisant référence à angular-resource.js ? C’est le script ou est défini le module ngResource. Que se passe-t-il si on l’oublie ? Comme d’habitude, plus rien ne marche sans dire pourquoi et on perd du temps à comprendre pourquoi.

Phase 4 : appeler notre service à l’intérieur de notre controller

function PhoneListCtrl($scope, Phone) {
$scope.phones = Phone.query();
$scope.select = function(phone) {
$scope.editedPhone = phone;
};
}

On a simplement remplacé le tableau « en dur » par un appel au service REST. On lance l’application et … ça ne marche pas ! Une puce apparait dans notre liste, juste une, avec rien derrière : ni nom de téléphone, ni snippet.

Grrr….

On cherche… on essaie de comprendre pourquoi juste une ligne, pourquoi, il n’y a ni nom, ni snippet, on ne comprend pas. On pose des points d’arrêt dans le code java et on constate qu’il a été effectivement appelé et qu’il a bien renvoyé deux téléphones dument renseignés. Alors, on a l’idée qui débloque tout : on ajoute {{phones}} comme « trace » dans notre écran, afin de voir ce qui est effectivement chargé. Et là, que lit-on ?

{phone:[{name: ‘nexus’, snippet :’nexus snippet’}, {name: ‘iphone’, snippet :’iphone snippet’}]}

En gros, CXF a « entouré » notre tableau de téléphones par « {phone:[…]} ». Conséquence, Angular au retour constate qu’il n’a pas de tableau mais un unique objet (d’où la puce unique), que cet objet n’a qu’une propriété, nommée « phone » et non deux, nommées « name » et « snippet ». D’où le nom et la snippet qui restent désespérément vides.

Argggh !

« Pourtant le service java retourne une liste d’objets, pas un objet unique ! » Ouais. Mais JAXB décline notre liste ainsi. « Ah bon. Peut-on y faire quelque chose ? » Non. « Angular sait-il prendre en compte le fait qu’il faut enlever la coque du message ? » Non plus. « Ah. » On est pris en sandwich entre Angular qui exige un format particulier et JAXB qui ne sait qu’en fournir un autre.

Que faire ?

C’est à la fois simple et irritant : il faut changer de provider JAXB. Le comportement que nous venons de constater est celui de l’implémentation JAXB par défaut appelée Jettison. Il en existe d’autres, en particulier une, appelée Jackson, qui elle ne présente pas ce défaut. Une demi-journée de recherches Googliennes, d’interrogation des uns et des autres vient de passer, et on prie, on espère qu’en effet, Jackson fera l’affaire. On télécharge, on installe. Ça, c’est simple : il suffit de rajouter dans web.xml un second paramètre à notre servlet :

<init-param>
<param-name>jaxrs.providers</param-name>
<param-value>org.codehaus.jackson.jaxrs.JacksonJaxbJsonProvider</param-value>
</init-param>

On allume un cierge. On lance… Ça marche ! Ouf…

[Mode troll on] Cela fait beaucoup de soucis pour utiliser un des protocoles les plus simples qui ait été imaginé : JSON se lit et se comprend d’un coup d’œil et son analyse syntaxique constitue, au pire, un solide exercice de Travaux Pratique en école d’ingénieur. Pourtant, CXF utilise les grands moyens : JAXB. Painfull. L’utilisation de JAXB me fait l’impression d’un voyage de chez moi jusqu’à la boulangerie la plus proche (100m) en passant par Saturne, Jupiter et Pluton. [Mode troll off]

Phase 5 : sauvegarder les mises à jour apportées à l’objet en cours d’édition

Il faut pour cela :

  1. Munir notre objet téléphone d’une méthode save(),
  2. Ajouter un service REST qui effectue cette mise à jour.
  3. Ajouter, dans la page HTML un bouton déclenchant le réflexe de sauvegarde du téléphone courant.
  4. Ajouter ce reflexe dans le controller.

Allons-y. La première étape est simple :

angular.module('phonecatServices', ['ngResource'])
.factory('Phone', function($resource){
return $resource('rest/services/phones', {}, {
query: {method:'GET', isArray:true},
save: {method:'POST'}
});
});

« Save » utilise la méthode http POST. C’est comme ça, d’ailleurs que le service REST saura que l’on demande la mise à jour d’un téléphone et non une liste de téléphones (car l’URL d’appel, elle,  est identique…). La description du téléphone est contenue, au format JSON, dans le corps de la requête. Le service effectuera la mise à jour et renverra la description du téléphone, toujours au format JSON dans le corps de la réponse. Pourquoi ? Pour signaler d’éventuelles mises à jour apportées par le serveur (comme l’affectation d’un identifiant, la modification d’une estampille temporelle, etc.) L’objet, au retour, est automatiquement mis à jour par Angular. Il n’y a rien à faire. Rien du tout. Appréciable. La « finition » du framework par Google est vraiment étonnante. Respect.

Le comportement (factice) de notre méthode REST s’écrira ainsi :

public class PhoneService {
…

@POST
@Path("/phones")

public Phone savePhone(Phone phone) {
phones.put(phone.id, phone);
System.out.println("Phone : "+phone.id+" sauvegardé !");
return phone;
}

…
}

Le contenu de cette méthode a peu de valeur en soi (puisqu’il est factice). Les éléments d’interface (@POST, @Path, la déclaration de la méthode), si. Car dans la vraie vie, ils seront identiques à cela.

Dans la page HTML, il faut juste rajouter un bouton :

<html ng-app="phonecat">
<head>
<script src="libjs/angular/angular.js"></script>
<script src="libjs/angular/angular-resource.js"></script>

<script src="js/controllers1.js"></script>
<script src="js/app1.js"></script>
<script src="js/services.js"></script>
</head>
<body ng-controller="PhoneListCtrl">
<ul>
<li ng-repeat="phone in phones" ng-click="select(phone)">
{{phone.name}}
<p>{{phone.snippet}}</p>
</li>
</ul>
<div>
<div>
Nom : <input type="text" ng-model="editedPhone.name"></input>
</div>
<div>
Snippet :<textarea ng-model="editedPhone.snippet"></textarea>
</div>
<div>
<button ng-click="sauver()">Save</button>
</div>
</div>
</body>
</html>

L’ajout du réflexe dans le controller est encore plus trivial :

function PhoneListCtrl($scope, Phone) {
$scope.phones = Phone.query();
$scope.select = function(phone) {
$scope.editedPhone = phone;
};
$scope.sauver = function() {
$scope.editedPhone.$save();
};
}

On exécute, ça marche (après avoir bien vérifié qu’on n’a rien oublié et qu’on a vidé le cache du navigateur…). Je le répèterai à l’occasion afin que vous n’oubliiez jamais. Ctrl shift suppr, Ctrl shift suppr, Ctrl shift suppr.

Notez que la méthode $save est ajoutée directement sur les objets Phone, par la magie de notre ami, l’objet $resource, qui a instrumentalisé la « classe » javascript Phone.

Il nous reste à implémenter les autres services CRUD : création et suppression. Facile ? On pourrait le penser au vu de la facilité qu’on a eu à implémenter save. On a tort, comme le montrera le prochain épisode.

3 réflexions au sujet de « Angular JS, épisode 3 : il fait le REST (ou comment marier Angular et CXF) »

  • Ping : Premiers pas avec Angular JS | Blog Objet Direct

  • 10 février 2014 à 13 h 27 min
    Permalink

    Bonjour,

    Je tiens tout d’abord à vous féliciter pour cette série de bons articles sur AngularJS, qui plus est en français !

    Cependant, et même s’il est bien traité, le sujet me semble avoir été abordé avec pas mal de préjugés sur la techno de Google et sur JavaScript en particulier, et certains points me paraissent approximatifs, voire erronés.

    Phase 1 :
    – Avec Jackson, il n’y a plus besoin d’instrumentaliser (polluer ?) son POJO / Bean avec des annotations type @XmlRootElement

    Phase 3 :
    – Une erreur s’est glissé dans le bout de code de la phase 3 (des balises visiblement générées par la coloration syntaxique qui ne passent pas)
    – La création du module pour les « services » n’est pas obligatoire ; on peut gérer tous ses controllers, services, directives, filtres au sein d’un même module. Il est d’ailleurs plutôt conseillé de définir des modules fonctionnels plutôt que techniques.
    – $ngResource (http://docs.angularjs.org/api/ngResource.$resource) fournit de base les méthodes GET et POST et notamment la fameuse méthode « query », qu’il n’est donc pas nécessaire de redéfinir
    – Il est d’ailleurs préférable dans ce cas, de passer par la méthode $module.service plutôt que $module.factory : cf. documentation (http://docs.angularjs.org/guide/module, section « Recommended Setup » ) ou encore ce thread http://stackoverflow.com/a/13763886/2120773 ;
    – Il aurait été mieux de nommer le service « PhoneService » plutôt que « Phone », qui peut prêter à confusion ; l’objet retourné / injecté n’est pas un « Phone », c’est un objet de type $resource : http://docs.angularjs.org/api/ngResource.$resource
    – L’API semble bancale si l’on s’en tient à une architecture RESTful : quid d’un paramètre d’URL {id} ?

    Phase 4 :
    – L’auteur semble ne pas maîtriser le débogage JS autant que son homologue Java. Avec des outils tels que les Chrome Dev Tools (notamment le debugger pas-à-pas ou le parcours des sources récupérés), ou le plugin AngularJS pour Chrome, il n’est plus si difficile de remonter et comprendre les erreurs JS. Autre bon réflexe : vérifier les en-têtes HTTP des flux transmises/récupérées.
    – Pour les problèmes de CXF, je plussoie. Perso, j’ai résolu le problème en faisant passant aussi à Jackson, mais surtout en faisant des tests automatisés (Jetty + contrôles des flux HTTP). Quoiqu’il en soit, ce n’est pas un problème de JavaScript ou d’AngularJS.

    Phase 5 :
    – Il y a de nouveau une coquille dans le code JavaScript lié à la coloration syntaxique.

    Pour améliorer l’article :
    – je retravaillerais les points ci-dessus
    – je corrigerais les erreurs de code du plugin de coloration syntaxique du blog
    – je mettrais un lien vers le code dans GitHub / BitBucket
    – je parlerais d’industrialisation (avec Yeoman / Yo / Grunt / Bower)
    – je parlerais d’initiatives comme JHipster (http://jhipster.github.io/) ou JRocket (http://jrocket.io)

    Dans tous les cas, encore merci pour l’initiative et bon courage pour les suivants.

    PS : je permets une dernière remarque : c’est bien dommage que l’auteur (note ami Henri ^^) ne publie pas lui-même les articles. Cela confère une impression très négative et très préjudiciable (« l’auteur ne prend même pas la peine ni le temps de poster lui-même la nourriture qu’il nous fait avaler », ou « l’auteur veut nous enseigner des choses complexes mais il ne maîtrise même pas WordPress »), alors même que le contenu des articles est vraiment d’une grande richesse.

  • 10 février 2014 à 14 h 54 min
    Permalink

    Les erreurs générées par la coloration syntaxique ont été corrigées. Pour le reste, je laisse « notre ami Henri » répondre…

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *