Dans ce billet nous allons voir comment réaliser simplement une API REST dynamiquement générée à partir du modèle de données en utilisant Generic System dans un environnement non-JEE avec AngularJS et vert.x.
Configuration Environnement
Maven
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.genericsystem</groupId> <artifactId>genericsystem2015</artifactId> <version>4.0-SNAPSHOT</version> </parent> <artifactId>gs-example-angular</artifactId> <name>Generic System Example Angular</name> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>org.genericsystem</groupId> <artifactId>gs-kernel</artifactId> <version>4.0-SNAPSHOT</version> </dependency> <dependency> <groupId>io.vertx</groupId> <artifactId>vertx-core</artifactId> <version>${vertx.version}</version> </dependency> <dependency> <groupId>io.vertx</groupId> <artifactId>vertx-web</artifactId> <version>${vertx.version}</version> </dependency> </dependencies> <repositories> <repository> <id>middlewarefactory</id> <url>http://middlewarefactory.com/repository</url> <releases> <enabled>true</enabled> <updatePolicy>daily</updatePolicy> </releases> <snapshots> <enabled>true</enabled> <updatePolicy>daily</updatePolicy> </snapshots> </repository> <repository> <id>sonatype-nexus-snapshots</id> <name>Sonatype Nexus Snapshots</name> <url>https://oss.sonatype.org/content/repositories/snapshots/</url> <releases> <enabled>false</enabled> </releases> <snapshots> <enabled>true</enabled> </snapshots> </repository> </repositories> </project> |
Modèle de données
Comme dans l’exemple JSF, nous utilisons la configuration statique de Generic System pour mettre en place notre modèle de données. Pour cela, nous créons deux classes : Car et Power.
Car.java
Ici Car est un type, afin de le spécifier à GS il nous suffit d’ajouter l’annotation @SystemGeneric.
L’annotation @Table permet d’associer une chaîne de caractères au type concerné.
1 2 3 4 |
@SystemGeneric @StringValue("car") @Table("car") public class Car { } |
Power.java
Ici Power est une propriété de Car qui prend comme valeur des entiers. Pour le spécifier à GS nous utiliserons les annotations :
- @Components qui permet de définir notre composant Car ;
- @PropertyConstraint qui permet de spécifier que c’est une propriété ;
- @InstanceValueClassConstraint qui permet de restreindre la classe des valeurs (ici des entiers) ;
- @Column permet d’associer une chaîne de caractères à l’attribut concerné.
1 2 3 4 5 6 7 |
@SystemGeneric @Components(Car.class) @PropertyConstraint @InstanceValueClassConstraint(Integer.class) @StringValue("power") @Column("power") public class Power { } |
Serveur vert.x
Nous allons ensuite créer un serveur avec vert.x.
Pour cela, nous créons une classe Server qui est un Verticle, l’unité de travail de vert.x. Dans cette classe nous utilisons la méthode
start() au sein de laquelle nous allons créer notre API REST.
Nous déclarons un router permettant de gérer les requêtes http.
Ayant besoin d’un cache pour travailler avec GS, nous créons une session vert.x dans laquelle nous déposons le cache GS.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
public class Server extends AbstractVerticle { private static Engine engine = new Engine(System.getenv("HOME") + "/genericsytem/carsAngular", Car.class, Power.class); public static void main(String[] args) { String dir = "gs-example-angular/src/main/java/" + Server.class.getPackage().getName().replace(".", "/"); try { // We need to use the canonical file. Without the file name is . File current = new File(".").getCanonicalFile(); if (dir.startsWith(current.getName()) && !dir.equals(current.getName())) { dir = dir.substring(current.getName().length() + 1); } } catch (IOException e) { // Ignore it. } System.setProperty("vertx.cwd", dir); Consumer<Vertx> runner = vertx -> { try { vertx.deployVerticle(Server.class.getName()); } catch (Throwable t) { t.printStackTrace(); } }; Vertx vertx = Vertx.vertx(new VertxOptions().setClustered(false)); runner.accept(vertx); } JsonArray jsonArray = getGSJsonCRUD(); @Override public void start() throws Exception { Router router = Router.router(vertx); router.route().handler(BodyHandler.create()); router.route().handler(CookieHandler.create()); router.route().handler(SessionHandler.create(LocalSessionStore.create(vertx))); router.route().handler(ctx -> { Session session = ctx.session(); Cache cache = session.get("cache"); if (cache == null) session.put("cache", cache = engine.newCache()); cache.start(); ctx.next(); }); router.route().handler(StaticHandler.create()); vertx.createHttpServer().requestHandler(router::accept).listen(8080); } } |
Afficher les données
Nous allons afficher les données de notre modèle sur des pages HTML grâce à AngularJS.
Tout d’abord nous relions la vue avec le module angular dans un index.html.
Depuis la version 1.1 d’angularJS, le fichier angular-route.js doit être intégré indépendamment d’angular.js :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<div class="container"></div> <!DOCTYPE html> <html> <head> <meta charset='utf-8'> <title>AngularJs</title> <link href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css" rel="stylesheet"> <body ng-app="CrudApp"> <div class="container"> <div ng-view></div> </div> <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.7/angular.js"></script> <script type ="text/javascript" src="https://code.angularjs.org/1.5.7/angular-route.js"></script> <script type="text/javascript" src="/js/app.js"></script> </body> </html> |
Voici notre module angular dans lequel nous indiquons quel “controller” utiliser suivant la page HTML affichée :
Il est à noter que depuis la version 1.1, ngRoute doit être placé en paramètre.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
angular.module('CrudApp', [ngRoute]).config(['$routeProvider', function ($routeProvider) { $routeProvider. when('/', {templateUrl: '/tpl/home.html', controller: IndexCtrl}). when('/list', {templateUrl: '/tpl/lists.html', controller: ListCtrl}). when('/add-inst', {templateUrl: '/tpl/add-new.html', controller: AddCtrl}). when('/edit/:id', {templateUrl: '/tpl/edit.html', controller: EditCtrl}). otherwise({redirectTo: '/'}); }]); function IndexCtrl($scope, $http, $location){ $http.get('/api/types').success(function(data){ $scope.choices = data; $scope.select = function(choice){ path = choice.tableName; columns = choice.columns; $scope.activePath = $location.path('/list'); }; }); } |
Au lancement de l’application, c’est le “controller” IndexCtrl qui est invoqué. Celui-ci réalise une requête de type GET qui va nous permettre de récupérer les types d’objets disponibles et de les afficher dans la page home.html.
Nous devons donc rajouter dans notre serveur le handler correspondant.
Etant donné que nous allons travailler avec angular, les données seront partagées sous forme de JSON. Ainsi, grâce à la méthode getGSJsonCRUD(), nous construisons en premier lieu un tableau d’objets JSON contenant les informations disponibles dans le modèle : les types d’objets disponibles ainsi que leurs attributs éventuels :
1 2 3 4 5 6 |
[...] JsonArray jsonArray = getGSJsonCRUD(); router.get("/api/types").handler(ctx -> { ctx.response().end(jsonArray.encode()); }); [...] |
Celui-ci renvoie le tableau d’objets JSON généré au lancement du serveur.
le home.html :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<h2>Home, choose a type</h2> <div> <table class="table table-condensed"> <thead> <tr> <th>Type</th> </tr> </thead> <tbody> <tr ng-repeat="choice in choices"> <td>{{choice.tableName}}</td> <td><button class="btn btn-primary" id="type-choice" ng-click="select(choice)"> Select<span class="glyphicon glyphicon-hand-up" /> </button></td> </tr> </tbody> </table> </div> |
Voilà le résultat obtenu :
En sélectionnant le type qui nous intéresse, nous allons alors afficher la liste des instances associées. Nous pourront alors comme dans tout CRUD qui se respecte en créer de nouvelles, les modifier, les effacer.
Nous modifions en conséquence notre javascript en rajoutant les “controller” nécessaires :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
[...] function ListCtrl($scope, $http) { $scope.type = path; $scope.names = columns; $http.get('/api/'+path).success(function (data) { $scope.instances = data; }); } function AddCtrl($scope, $http, $location) { $scope.master = {}; $scope.activePath = null; $scope.type = path; $scope.names = columns; $scope.add_new = function (instance, AddNewForm) { $http.post('/api/'+path+'/', instance).success(function () { $scope.reset(); $scope.activePath = $location.path('/list'); }); $scope.reset = function () { $scope.instance = angular.copy($scope.master); }; $scope.reset(); }; } function EditCtrl($scope, $http, $location, $routeParams) { var id = $routeParams.id; $scope.activePath = null; $scope.type = path; $scope.names = columns; $http.get('/api/'+path+'/' + id).success(function (data) { $scope.instance = data; }); $scope.update = function (instance) { $http.put('/api/'+path+'/' + id, instance).success(function (data) { $scope.instance = data; $scope.activePath = $location.path('/list'); }); }; $scope.delete = function (instance) { $http.delete('/api/'+path+'/' + instance.id).success(function (data){ $scope.activePath = $location.path('/list')}); }; } [...] |
et dans notre serveur en rajoutant les handler requis. Comme nous pouvons avoir plusieurs types disponibles, nous générons notre API REST pour chaque type :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
[...] for (int j = 0; j < jsonArray.size(); j++) { String typeName = jsonArray.getJsonObject(j).getString("tableName"); router.get("/api/" + typeName).handler(ctx -> { Generic type = engine.getInstance(typeName); final JsonArray json = new JsonArray(); type.getInstances().stream().forEach(i -> json.add(getJson(i, getAttributes(type)))); ctx.response().end(json.encode()); }); router.get("/api/" + typeName + "/:id").handler(ctx -> { Generic type = engine.getInstance(typeName); Generic instance = getInstanceById(type, Long.valueOf(ctx.request().getParam("id"))); JsonObject json = getJson(instance, getAttributes(type)); ctx.response().end(json.encode()); }); router.post("/api/" + typeName).handler(ctx -> { Generic type = engine.getInstance(typeName); JsonObject newInst = ctx.getBodyAsJson(); Generic instance = type.setInstance(newInst.getString("value")); for (Generic attribute : getAttributes(type)) instance.setHolder(attribute, convert(attribute, newInst.getString(getColumnName(attribute)))); ctx.response().end(newInst.encode()); }); router.put("/api/" + typeName + "/:id").handler(ctx -> { Generic type = engine.getInstance(typeName); Generic instance = getInstanceById(type, Long.valueOf(ctx.request().getParam("id"))); JsonObject update = ctx.getBodyAsJson(); instance = instance.updateValue(update.getString("value")); for (Generic attribute : getAttributes(type)) { instance.getHolder(attribute).updateValue(convert(attribute, update.getString(getColumnName(attribute)))); } JsonObject json = getJson(instance, getAttributes(type)); ctx.response().end(json.encode()); }); router.delete("/api/" + typeName + "/:id").handler(ctx -> { Generic type = engine.getInstance(typeName); Generic instance = getInstanceById(type, Long.valueOf(ctx.request().getParam("id"))); instance.remove(); ctx.response().end(); }); } [...] |
Nous affichons les instances du type sélectionné grâce à notre page list.html :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<h2>{{type}}</h2> <div class="col-md-6"> <table class="table table-condensed"> <thead> <tr> <th>{{type}}</th> <th ng-repeat="name in names">{{name.columnName}}</th> </tr> <tbody> <tr ng-repeat="instance in instances"> <td>{{instance.value}}</td> <td ng-repeat="name in names" >{{instance[name.columnName]}}</td> <td><a href="#/edit/{{instance.id}}"class="btn btn-default btn-sm"><i class="glyphicon glyphicon-pencil"></i></a></td> </tr> </tbody> </table> <a class="btn btn-primary" href="#/add-inst">Add New {{type}}</a></div> |
Notre formulaire add-new.html nous permettant d’ajouter de nouvelles instances :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<h2 >Add new {{type}}</h2> <div class="row"> <div class="col-md-6"> <form novalidate name="AddNewForm" id="add-new-form" method="post" action=""> <div> <label>{{type}}</label> <input class="form-control" type="text" ng-model="instance.value" required /> </div> <div class="form-group" ng-repeat="name in names"> <label>{{name.columnName}}</label> <input class="form-control" type="text" ng-model="instance[name.columnName]" required /> </div> <button class="btn btn-primary" ng-disabled="AddNewForm.$invalid || isUnchanged(instance)" id="add-new-btn" ng-click="add_new(instance)">Save!</button> <a href="#/list" class="btn">Cancel</a> </form> </div> </div> |
Et le edit.html qui permet de modifier les instances existantes :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
<h2 >Edit {{type}}</h2> <div class="row"> <div class="col-md-6"> <form novalidate name="EditForm" id="edit-form" method="post" action=""> <div > <label>{{type}}</label> <input class="form-control" type="text" ng-model="instance.value" value="{{instance.value}}" required /> </div> <div class="form-group" ng-repeat="name in names"> <label>{{name.columnName}}</label> <input class="form-control" type="text" ng-model="instance[name.columnName]" value="{{instance[name.columnName]}}" required /> </div> <button class="btn btn-primary" ng-disabled="AddNewForm.$invalid || isUnchanged(instance)" id="add-new-btn" ng-click="update(instance)"> Update <span class="glyphicon glyphicon-floppy-disk" /> </button> <button class="btn btn-danger" ng-click="delete(instance)"> Delete <span class="glyphicon glyphicon-trash" /> </button> <a href="#/list" class="btn">Cancel</a> </form> </div> </div> |
Voilà le résultat obtenu pour la liste des instances :
Pour remplir cette liste il nous suffit d’ajouter de nouvelles instances :
Nous pouvons modifier les instances créées :
Enregistrement et annulation
Comme précisé dans l’exemple JSF, l’enregistrement ne se fait actuellement que sur le cache, il faut donc ajouter un bouton pour flusher les modifications et un autre pour les annuler.
Si plusieurs personnes travaillent sur le même graphe, chacun possède son propre cache qui est une image du graphe à un instant donné, et ne voit pas les modifications apportées par les autres. Cependant, il est possible de visualiser tout changement persisté en effectuant un “décalage temporel” sur son cache. Nous allons donc ajouter un bouton correspondant à cette fonction.
Nous ajoutons nos boutons à notre list.html :
1 2 3 4 5 6 7 8 9 10 11 |
<button class="btn btn-success"> Save </button> <button class="btn btn-warning"> Shift </button> <button class="btn btn-danger"> Cancel </button> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
router.put("/api/" + typeName).handler(ctx -> { engine.getCurrentCache().flush(); ctx.response().end(); }); router.delete("/api/" + typeName + "/shift").handler(ctx -> { engine.getCurrentCache().shiftTs(); ctx.response().end(); }); router.delete("/api/" + typeName + "/clear").handler(ctx -> { engine.getCurrentCache().clear(); ctx.response().end(); }); |
1 2 3 4 5 6 7 8 9 10 11 |
$scope.commit = function(){ $http.put('/api/'+path+'/commit/'); }; $scope.shift = function () { $http.post('/api/'+path+'/shift/'); $route.reload(); }; $scope.clear = function (instance) { $http.delete('/api/'+path+'/clear/'); $route.reload(); }; |