I. Introduction : pourquoi les applications Web hors-ligne

Aujourd'hui la connectivité est une composante essentielle de notre vie sociale. Il faut être connecté. C'est donc tout naturellement et avec une grande diligence que nous déployons sur nos territoires des réseaux mobiles, marquant l'avènement des smartphones et de la 3G. Cependant malgré nos efforts, l'objectif n'est en fait pas réellement atteint si l'on considère que nous devons pouvoir être connectés en tout lieu et en tout temps.

Voici un ou deux exemples : je suis dans le train et je me dis :  »J'ai enfin le temps de lire l'article que je m'étais mis de côté, j'allume mon terminal et je vais sur le site en question… » Mais là, mauvaise surprise, le réseau est soit indisponible, soit trop lent, bref : je ne peux pas lire mon article.

Autre exemple : pour me rendre à mon rendez-vous, je comptais sur Google Maps pour m'indiquer mon chemin… Et encore une fois, juste à ce moment : pas de réseau ! Alors que toute cette technologie fonctionne parfaitement lorsque je n'en ai pas besoin ! Et pire : j'ai finalement réussi à me rendre sur place, et je souhaite pour me mettre dans le bain consulter l'email que m'avait envoyé ce client à propos de l'ordre du jour. Zut pas de réseau, impossible de lire cet email…

Le problème est donc le suivant : même avec des technologies nous donnant un accès au réseau disons 80% du temps, nous avons pris l'habitude de dépendre de la connectivité et nos activités en dépendent aussi, même dans les 20% du temps pendant lequel le réseau est indisponible.

Ce problème trouvera une solution logicielle dans les applications hors-ligne qui permettront :

  • de compenser les débits 3G parfois insuffisants (lecteur de blog offline par exemple) ;
  • de mitiger l'impact des déconnexions intempestives (emails) ;
  • de couvrir les besoins de déplacements hors zone de couverture (vente vrp…).

Nous avons vu fleurir ces dernières années le concept du cloud dont l'essence peut être comprise du point de vue de l'utilisateur par :

Peu importe l'infrastructure du service, pourvu que je sois connecté, je dois avoir accès à mes données, et je dois pouvoir y contribuer.

Nous pouvons désormais pousser le concept beaucoup plus loin avec les applications hors-ligne et déclarer :

Peu importe que je sois connecté ou non, je dois avoir accès à mes données, et je dois pouvoir y contribuer.

En fait l'expérience utilisateur ne devrait pas être liée au concept de connexion. Cette connexion est un concept technique, qu'il faut éliminer de la vie de l'utilisateur. Connecté ou pas, l'utilisateur exige la même expérience !

Les applications les plus connectées seront donc aussi des applications hors-ligne ! Nous aurons ainsi dépassé le concept du cloud.

I-A. Le Web est la plateforme

Outre le besoin des applications web de survivre à des déconnexions intempestives, on trouve aussi d'autres intérêts à la conception d'applications hors-ligne. En effet, le standard HTML5 permet de développer aujourd'hui des applicatifs réservés auparavant au monde « natif ». On trouve dans ce standard nombre de concepts historiquement liés aux systèmes d'exploitation et qui n'étaient jusque là pas accessibles aux applications web :

  • stockage de données local ;
  • stockage de l'application pour permettre son exécution hors-ligne ;
  • système de fichier ;
  • un modèle d'exécution enrichi avec les Worker threads ;
  • un modèle de connectivité également enrichi : Web sockets, WebRTC ;
  • l'accès au matériel : Web GL, Audio, etc ;
  • et aux périphériques (webcam, microphone, gps…).

Il y a donc une niche d'applications qui peuvent maintenant être implémentées sur la plateforme Web. Et ces applicatifs n'ont pas nécessairement l'utilité d'une connexion à Internet. L'avantage de la plateforme Web est son universalité et ses performances croissantes. Un seul code écrit pour cette plateforme peut s'exécuter sur 99% des terminaux : quel terminal aujourd'hui ne propose pas de navigateur Web moderne ?

Cette universalité permet de considérablement réduire les coûts de conception, de développement et de maintenance d'un logiciel, tout en ciblant un nombre toujours croissant d'utilisateurs, avec des performances largement acceptables dans la plupart des cas.

I-B. Attention au coût de développement !

Je tiens cependant à prévenir le concepteur et le développeur : implémenter ce nouveau paradigme d' applications hors-ligne aura un coût non négligeable au départ.

  • L'architecture du code sera différente de l'architecture client/serveur à laquelle nous sommes habitués.
  • De nouvelles problématiques (parfois complexes) feront surface et les résoudre sera également une difficulté car aujourd'hui aucun framework ne prend en charge l'intégralité de la pile applicative nécessaire au déploiement des applications hors-ligne.

Parmi les problématiques que l'on aura à traiter on trouvera : la tolérance aux pertes partielles ou totales de connexion, la synchronisation de référentiels de données, la gestion des conflits, etc. Ainsi il sera essentiel à qui veut se lancer dans le développement de telles applications de cerner le plus précisément possible le périmètre fonctionnel lié à la gestion de la vie hors-ligne de l'application afin de réduire les coûts et les risques au maximum en amont du développement.

Nous nous trouvons dans une étape transitoire et il y a fort à parier que les outils qu'il nous manque aujourd'hui seront développés par les pionniers des applications hors-ligne.

I-C. GWT : notre outil de développement

Au fil de cet article, l'outil de développement que nous utiliserons pour programmer la partie cliente de l'application sera Google Web Toolkit. Celui-ci nous permettra de développer des applications complexes en Java et de faire levier sur la plateforme HTML5 de façon puissante. Bien sûr les applications hors-ligne ne requièrent pas l'utilisation d'un outil spécifique et il est tout-à-fait possible de développer ces applications directement en Javascript. Voilà quelques éléments intéressants à noter à propos de GWT :

  • c'est un projet Open Source, initialement créé en 2006 chez Google, qui a depuis pris sont envol et dont un comité de pilotage indépendant intégrant les acteurs majeurs de GWT assure désormais la gouvernance ;
  • GWT est supporté par une communauté vaste et active ;
  • il a été téléchargé plus d'un million de fois ;
  • nous permet de partager du code entre client et serveur (dans le cas où le serveur est écrit en Java bien sûr). C'est un point crucial pour les applications hors-ligne, car une couche d'accès aux données sera implémentée sur le client ;
  • la plupart des API HTML 5 sont prises en charge ;
  • il intègre une bibliothèque nommée « Elemental » qui permet de prendre en compte rapidement et facilement les évolutions HTML5 ;
  • à l'instar de JNI sur la JVM, le JSNI nous permet de nous interfacer efficacement avec la plateforme sous-jacente à l'exécution du code Java : le monde Javascript.

I-C-1. La philosophie de GWT

L'idée de GWT est de permettre le développement d'applications dans un IDE évolué tel qu'Eclipse ou IntelliJ avec tous les bénéfices d'un langage fortement typé : débogage dans l'IDE, refactoring, etc…

Quelques principes de base sous-tendent cet outil :

  • tout doit être fait pour optimiser au maximum le code Javascript généré ainsi que les aller-retours réseau ;
  • un modèle de composants graphiques à la Swing ;
  • le développeur ne doit pas être un gourou de JavaScript, HTML ou CSS ;
  • gère les ressources de l'application (pour les icônes par exemple, le mécanisme de spriting est mis en place automatiquement) ;
  • génère de façon transparente le code JavaScript à partir de Java ;
  • multinavigateurs, le développeur ne se soucie pas spécificités de chaque navigateur.

Avec GWT, le Javascript correspond donc au bytecode du monde Java et le pendant de la JVM est le moteur d'exécution Javascript du navigateur.

Un seul code Java génère plusieurs scripts JavaScript optimisés spécifiquement pour chaque navigateur. Et au démarrage de l'application, le fichier Javascript approprié est chargé dans le navigateur. On évite ainsi l'effet du « qui peut le plus peut le moins », toutes les fonctionnalités inutilisées dans votre programme ne seront tout simplement pas chargées.

II. Les APIs HTML 5 pour l'Offline et leur intégration dans GWT

Maintenant que nous savons ce que nous voulons, et l'outil avec lequel nous allons réaliser notre objectif, il est temps de définir notre plan d'action. Nous examinerons dans un premier temps les APIs HTML5 dont nous aurons besoin et la manière de les mettre à profit dans notre code. Puis nous explorerons les problématiques spécifiquement liées aux applications hors-ligne.

II-A. Le cache d'application

Le cache d'application est la fonctionnalité qui permet de demander au navigateur de stocker certains fichiers en avance à partir du serveur afin de les rendre disponibles pour l'exécution de l'application lorsque la connexion au serveur est indisponible.

Ceci se fait par deux éléments.

  • L'ajout du tag « manifest » à la balise html de la page principale de l'application. La valeur de ce tag contiendra une référence au fichier décrit ci-dessous.
  • La mise à disposition de ce fichier manifest qui contient le nom des ressources que le navigateur doit mettre en cache. Ce fichier doit impérativement être décrit avec le type MIME « text/cache-manifest ».

Quand le navigateur rencontre le tag manifest, il commence à télécharger toutes les ressources décrites dans le fichier spécifié. Lorsque la connexion au réseau est indisponible, le navigateur se servira des ressources ainsi mises en cache pour présenter à l'utilisateur la page en question, et ceci sans qu'il ne se rende compte de rien. C'est-à-dire que pour l'utilisateur, l'application se charge exactement de la même façon, qu'Internet soit connecté ou pas.

Le navigateur gère également la vie du cache. Il vérifie périodiquement si une nouvelle version du fichier manifest est disponible et le cas échéant met à jour le contenu de son cache.

Cette API permet aussi de s'abonner aux événements liés à la gestion du cache - tels que sa mise à jour par exemple, ou encore la terminaison de celle-ci. Cette API permet donc une gestion assez fine de la dynamique du cache et il devient par exemple possible de prévenir l'utilisateur qu'une nouvelle version de l'application est disponible, ou bien de demander explicitement au navigateur de vérifier la présence d'une mise à jour.

Pour plus de détails sur l'API Application Cache, je vous conseille de vous reporter au lien suivant : http://www.html5rocks.com/en/tutorials/appcache/beginner

II-A-1. Cache d'application - GWT

Vous l'avez compris, le mécanisme du cache d'application est assez simple. Ce qui va être problématique à gérer et la gestion du contenu du fichier manifest. En effet ce fichier doit répertorier toutes les ressources utilisées par votre application : le ou les fichiers html, les css, les images, les scripts javascript, etc.

C'est ici que GWT va énormément nous aider, car cet outil intègre un mécanisme de gestion des ressources utilisées dans le code de l'application (sans rentrer dans le détail, GWT optimise toutes les images et autres css référencées dans votre application et génère des fichiers correspondants qu'utilisera ensuite le code javascript généré pour votre application).

Il va donc être possible de nous insérer dans le processus de compilation de notre application afin de générer le contenu du fichier manifest. Ceci passe par l'écriture d'un "linker" qui sera appelé par le compilateur et qui nous permettra de récupérer la liste de l'ensemble des ressources utilisées par notre application et de générer ainsi un fichier manifest. Toutes les ressources de l'application, y compris les images, css, etc sont intégrées automatiquement pour un fonctionnement hors ligne !

Ce linker ne fait pas encore parti de GWT en standard, mais il me semble qu'il sera intégré d'office dans les versions 2.3 ou ultérieures. Néanmoins, l'écriture de ce composant ne pose vraiment pas de problème, son code ressemble grosso-modo à cela :

 
Sélectionnez
  1. // S'exécute dans le contexte de la compilation   
  2. @Shardable   
  3. @LinkerOrder( Order.POST )   
  4. public class AppCacheLinker extends AbstractLinker {   
  5.  ...   
  6.  @Override   
  7.  public ArtifactSet link(LinkerContext context, … ) {   
  8.   for( Artifact artifact : artifacts )   
  9.    sb.append( artifact.getPartialPath() );   
  10.    
  11.   emitString( logger, sb.toString(), context.getModuleName() + ".appcache" );   
  12.  }   
  13. } 

II-A-2. Intégration de l'API du cache en GWT avec JSNI

Comme dit plus haut, l'API Application Cache permet de s'interfacer au niveau JavaScript avec le mécanisme de gestion du cache d'application du navigateur. Votre application est donc à même de recevoir toutes les notifications de changement d'état du cache, et donc de présenter à l'utilisateur les messages adéquates. Par exemple : « Une mise à jour de l'application est disponible, nous vous recommandons de cliquer sur ce bouton ou de recharger la page » peut être du plus bel effet.

L'interfaçage de votre application GWT avec l'API Javascript se fera nécessairement par le biais de la mécanique JSNI (pour Java Script Native Interface).

Voici à titre d'exemple une énumération Java représentant les différents états que peut prendre le cache, et une classe Java qui permet d'interroger l'état du cache d'application, et de le contrôler :

 
Sélectionnez
  1. public enum AppCacheEvent { 
  2.  // Application en cache 
  3.  CACHED,  
  4.  // Vérification en cours 
  5.  CHECKING,  
  6.  // Téléchargement en cours 
  7.  DOWNLOADING,  
  8.  // Une erreur s'est produite 
  9.  ERROR,  
  10.  // Pas de mise à jour 
  11.  NOUPDATE,  
  12.  // Le fichier manifest n'existe plus 
  13.  OBSOLETE,  
  14.  // Mise à jour en cours 
  15.  PROGRESS,  
  16.  // Mise à jour prête à être déployée 
  17.  UPDATEREADY; 
  18. } 
  19.  
  20. public final class AppCache extends JavaScriptObject { 
  21.  // call Javascript from Java 
  22.  public static final native AppCache get() /*-{ 
  23.   return $wnd.applicationCache; 
  24.  }-*/; 
  25.  
  26.  // launch an app cache update 
  27.  public final native void update() /*-{ 
  28.   this.update(); 
  29.  }-*/; 
  30.  
  31.  public interface Callback { 
  32.   void handleEvent( AppCacheEvent event ); 
  33.  } 
  34.  
  35.  // register to App Cache events 
  36.  public final native void registerEvents( Callback callback ) /*-{ 
  37.   this.addEventListener('cached', function( s ) { 
  38.    // Call Java from Javascript 
  39.    callback.@package.AppCache.Callback::handleEvent(Lpackage/AppCacheEvent;)(@package.AppCacheEvent::CACHED); 
  40.   } ); 
  41.  
  42.   // inscription aux autres événements... 
  43.  }-*/; 
  44. } 

Arrivée à ce point, notre application n'a plus besoin d'Internet pour se charger dans le navigateur de l'utilisateur. C'est l'effet « magique » du cache d'application : votre terminal est déconnecté, vous ouvrez le navigateur, tapez l'url de l'application et … celle-ci s'affiche ! Bluffant…

II-B. Stockage local de données

Non contents de pouvoir exécuter notre application sans connexion Internet, il va maintenant falloir que celle-ci puisse accéder à ses données localement (pourvu bien sûr qu'elles aient été rapatriées sur le poste client lors d'une connexion antérieure…). Les données de l'application seront bien sûr persistantes au-delà du rechargement de l'application, et du redémarrage du navigateur.

II-B-1. Conserver localement les données dans le navigateur de l'utilisateur

La problématique du stockage de données local a donné lieu à la création de différentes normes et techniques. Parmi ces normes, on compte : WebSQL, IndexedDB, FileSystem, LocalStorage… Malheureusement, un très petit nombre d'entre elles sont disponibles sur l'ensemble des navigateurs du marché. Il va donc falloir choisir celle que l'on utilisera en prenant en compte cette contrainte. Dans ce contexte, on se rendra très vite compte (et je vous conseille d'aller vérifier ceci sur www.caniuse.com) que notre choix sera presque imposé : pour une compatibilité maximale de notre application, il faudra utiliser Local Storage (il semblerait que depuis la rédaction de cet article, l'API FileSystem soit également assez répandue, vous pourrez donc l'utiliser - bien que vous le verrez ceci ne change rien au problème). Par compatibilité maximale, j'entends la prise en charge de tous les navigateurs modernes du marché, qu'ils soient exécutés sur une machine desktop ou sur un terminal mobile.

Hélas, cet espace de stockage est vraiment pauvre en terme de ses fonctionnalités : c'est un simple espace clé/valeur, les clés et les valeurs étant des chaînes de caractères… Mais ne déprimons pas sur ce point, vous allez découvrir dans la suite de cet article un outil qui nous permettra d'utiliser cet espace de stockage de façon très simple et qui pourtant nous offrira des fonctionnalités allant bien au-delà de ce que peut faire la meilleure norme de stockage HTML5 ! Je vous laisse lire la suite…

En attendant, restons concentrés sur le Local Storage et voyons comment nous pouvons l'utiliser à partir de notre code Java.

La bonne nouvelle est que cette API est prise en charge nativement par GWT. J'entends par là que vous avez à disposition des classes Java du framework vous permettant de stocker et lire des chaînes de caractères indexées par des clés (elles aussi sous forme de String). Il s'agit de la class Storage. Je vous invite à consulter sa documentation sur le site de GWT.

Un des cas d'utilisation classique de cette class Storage est de stocker les données au format JSON, ce qui nous permettra de sérialiser et désérialiser celles-ci de façon très performante (si les navigateurs savent faire très rapidement quelque chose, c'est bien la lecture/écriture en JSON…). Vous pouvez à cette fin vous reporter aux bibliothèques JSON intégrées à GWT.

Notons au passage ici l'existence d'une bibliothèque de sérialisation/désérialisation d'objets Java vers le Local Storage, développée par mon ami +Xi CHEN, qui repose sur la mécanique de sérialisation interne de la couche RPC de GWT.

Mais encore une fois, vous allez découvrir un outil extrêmement puissant dans la suite de cet article…

Juste une remarque avant de passer à la suite : la taille des données contenues dans le Local Storage ne doit pas excéder 5 Méga-octets. Même s'il est assez étrange que cette limitation soit définie dans la norme, elle nous laisse quand même un espace de stockage assez conséquent.

III. Problématiques Hors-ligne

Nous en avons fini avec l'aspect technique de la pile logicielle que nous sommes en train de mettre en place, et nous pouvons maintenant nous concentrer sur l'aspect fonctionnel. Récapitulons l'ensemble des fonctionnalités que nous voulons implémenter.

  • L'application doit pouvoir se lancer sans Internet, et l'utilisateur doit tout de même pouvoir s'authentifier.
  • L'utilisateur doit avoir à sa disposition l'ensemble des données dans son champ d'intérêt au moment de l'utilisation hors-ligne de l'application.
  • Il doit même pouvoir modifier ses données et en créer de nouvelles, elles seront ensuite livrées au serveur lors de la prochaine connexion.
  • Tout ceci doit se faire de la façon la plus transparente possible. L'idéal étant que l'utilisateur n'ai plus dans son esprit aucun concept lié à l'état "connecté" ou "déconnecté" de l'application.

Si l'on prend en compte l'aspect assez bas niveau de l'API Local Storage (souvenez-vous, ce n'est qu'un simple tableau associatif sans aucune structure, ni aucun typage), on s'aperçoit que développer une application complexe sur de telles fondations sera plus que fastidieux. Nous aimerions donc construire pour le développeur une pile applicative comportant des fonctionnalités locales évoluées de persistance et d'interrogation des données. Si vous êtes habitués au développement web Java EE, vous serez sans doute d'accord avec moi pour dire qu'une API de type JPA serait bienvenue dans le code côté client (celui qui s'exécute dans le navigateur une fois transformé en Javascript).

III-A. Authentification hors-ligne

J'aimerais illustrer grâce à la problématique de l'authentification un aspect très agréable de GWT. En préalable, je vous rappelle d'abord que l'espace de stockage local n'est absolument pas sécurisé. N'importe quelle personne équipée d'un outil de débogue du navigateur (Firebug par exemple) peut très facilement extraire les données que vous avez stockées pour l'utilisateur. Il en va donc naturellement que pour authentifier un utilisateur en mode déconnecté, vous aurez à stocker localement une donnée chiffrée (son mot de passe salé et chiffré en SHA-1 par exemple - l'algorithme de chiffrement dépendra bien sûr de la sensibilité des données et du niveau de protection que vous souhaitez fournir à l'utilisateur).

Partons pour l'exemple du principe que vous allez stocker le mot de passe de l'utilisateur chiffré par l'algorithme SHA-1. Il vous faut donc trouver une bibliothèque implémentant SHA-1, ou bien l'implémenter vous-même, ce qui n'est pas vraiment conseillé.

Et c'est ici que GWT trouve un réel avantage par rapport à Javascript : la quantité de bibliothèques open source disponibles en Java est énorme, et pour peu que vous trouviez celle qui vous plaît, il ne vous restera en général qu'à copier le code source en question dans votre projet. Il sera ensuite transformé en Javascript par le compilateur GWT de façon transparente. Attention tout de même, ceci n'est possible que si la bibliothèque en question n'utilise pas de fonctions "interdites" en GWT comme l'introspection, les threads, etc. Dans de tels cas, un effort de portage sera nécessaire. Lors de la réalisation d'un POC pour une application supportant le mode déconnecté, il m'a suffit d'à peine dix minutes pour trouver une librairie SHA-1 écrite en Java qui me semblait assez solide (merci ganymed-ssh-2), et pour l'intégrer dans mon projet GWT. J'avais alors à disposition côté client la méthode souhaitée, à savoir :

 
Sélectionnez
  1. String shaEncoded = sha.digest( String clearString ); 

III-B. Un SGBD dans le navigateur ?

Lorsque je parlais de fonctionnalités évoluées de stockage et d'interrogation des données requises pour le développement de notre pile applicative, je n'aurais pas rêvé mieux qu'un moteur SGBD relationnel SQL.

Partant de la constatation que seul le Local Storage était disponible pour la persistance des données, il fallait alors imaginer soit implémenter un moteur de base de données (quelques années de développement), soit se contenter d'une sérialisation basique et d'interrogations des données sous forme de parcours en « brute-force » de nos collections de données. Un moteur SQL disponible en quelques lignes de code. Vous allez voir, c'est exactement ce dont nous allons disposer !

Vous connaissez peut-être l'excellent JBoss ERRAI, un projet très intéressant et ambitieux qui vise (entre autres) à implémenter intégralement la norme JPA pour le framework GWT. Je vous invite à vous informer au sujet de ce projet, néanmoins, il reste encore peu mature (sur le sujet du stockage local), et nombre de requêtes (même des jointures simples) ne sont toujours pas implémentées (et ne le seront sans doute jamais, ce que l'on comprend si l'on s'intéresse à l'architecture de cette librairie - c'est mon avis).

La bibliothèque que je veux vous présenter s'appelle SQL.JS. De quoi s'agit-il exactement ? Et bien c'est tout simplement l'équivalent en Javascript de la très célèbre implémentation SQLite écrite en C. Si vous ne connaissez pas SQLite, sachez que vous l'utilisez régulièrement, cette implémentation étant intégrée à la plupart des navigateurs, à Android, à IOS, etc. En fait c'est tout simplement le moteur SGBD le plus déployé de la planète (environ 500 millions de déploiements). Cette information peut sembler être un pur argument marketing, mais c'est en fait un gage de qualité : cette bibliothèque est très fiable et son code très stable et performant.

Comment donc cette base de code en C peut-elle se retrouver "convertie" en Javascript ? Et bien, cela est possible grâce à un outil que j'adore : le compilateur emscripten, écrit par Alon Zakai. Pour ne pas rentrer dans les détails, je dirais simplement qu'emscripten est un cross-compilateur de C vers Javascript basé sur l'infrastructure de compilation LLVM, je vous invite à vous informer sur ces sujets.

Ce qui compte pour nous, c'est que nous avons à disposition un fichier Javascript qui contient le code de SQLite, avec une API très simple permettant d'exécuter n'importe quelle requête SQL, et de récupérer les résultats des requêtes sous forme de données JSON.

L'API de SQL.JS étant en Javascript, il va encore nous falloir écrire un peu de code JSNI pour interfacer notre code Java avec, ce qui sera très court :

 
Sélectionnez
  1. public class SQLite extends JavaScriptObject { 
  2.  // crée une instance SQLite initialisée à partir des données d'un fichier SQLite 
  3.  public static final native SQLite open( JsArrayInteger data ) 
  4.  /*-{ 
  5.   return $wnd.SQL.open(data); 
  6.  }-*/; 
  7.   
  8.  // exécute une requête SQL et renvoie son résultat sous forme d'un objet JSON 
  9.  public final JSONObject execute( String statement ) 
  10.  { 
  11.   JavaScriptObject jso = _execute( statement ); 
  12.   return new JSONObject( jso ); 
  13.  } 
  14.  
  15.  private final native JavaScriptObject _execute( String statement ) 
  16.  /*-{ 
  17.   return this.exec(statement); 
  18.  }-*/; 
  19.  
  20.  // exporte les données de la base sous forme de fichier SQLite 
  21.  public final native JsArrayInteger exportData() 
  22.  /*-{ 
  23.   return this.exportData(); 
  24.  }-*/; 
  25.  
  26.  // ferme l'instance de la base de données 
  27.  public final native void close() 
  28.  /*-{ 
  29.   this.close(); 
  30.  }-*/; 
  31. } 

Nous avons à dispositions toutes les méthodes dont nous avons besoin.

  • open( data ) permet d'initialiser une instance du moteur SQLite avec les données correspondant au contenu d'un fichier SQLite - une sauvegarde de la base de données que nous aurons stocké bien sûr, dans le Local Storage. Vous pourrez noter ici un point très important : les données qu'utilise SQL.JS sont en tout point identiques à celles utilisées par SQLite. C'est-à-dire qu'il sera tout à fait possible d'exporter une base de données SQL.JS pour la lire avec SQLite. Inversement, il est possible de lire avec SQL.JS une base de données SQLite. Une démonstration de ce point peut être trouvée ici.
  • exportData() nous renvoie un tableau d'entiers correspondant à la l'enchaînement d'octets que vous trouveriez dans un fichier SQLite classique. Il ne nous restera qu'à le stocker dans le Local Storage pour persister l'ensemble de nos données.
  • execute( statement ) nous permet d'exécuter nos requêtes SQL (et DDL) et d'obtenir les résultats correspondant sous forme de JSON.

Voilà, à partir de maintenant nous avons à disposition un pile applicative plus que correcte pour écrire nos applications hors-ligne. Allez, si vous n'aimez pas traiter avec le langage SQL directement (ce qui est compréhensible), quelques centaines de lignes de code suffiront à bâtir un mini-ORM vous permettant d'écrire par exemple :

 
Sélectionnez
  1. List articles = db.query( "select {Article:a} " +   
  2.  "from Article a " +   
  3.  "left join Marque m " +   
  4.  "where m.id=42" ).getResultList(); 

Ici nous obtenons une liste d'articles de la marque ayant pour identifiant 42 sous forme d'objets Java.

III-B-1. Des contrats de service homogènes

Si vous souhaitez proposer à l'utilisateur des fonctionnalités équivalentes que l'application soit en mode connecté ou déconnecté, vous savez que cette application va devoir effectuer en mode déconnecté elle-même les traitements métier qui étaient auparavant à la charge du serveur, une partie du code métier sera donc factorisable (si vous utilisez Java sur le serveur) entre le client et le serveur. Vous n'aurez donc qu'à abstraire la couche d'accès aux données (ce qui est une pratique courante) pour coder votre couche métier et ainsi vous éviter un développement en doublon (si vous me suivez, dans mon idée le client exécute une couche métier s'appuyant sur SQLite et le serveur exécute la même couche métier mais s'appuyant cette fois sur la norme JPA - par exemple).

III-C. Synchronisation des données

Nous allons maintenant entrer dans la problématique qui est pour moi la plus ardue de toutes celles que nous aurons couvertes ici, à savoir la synchronisation des données. C'est un problème extrêmement vaste, et auquel l'avènement des systèmes informatiques distribués et bases de données réparties (l'essence du "cloud" en fait) doivent leurs salut.

En gros la complexité du problème à résoudre dépendra du niveau d'autonomie que vous voulez accorder à l'utilisateur lorsque l'application est en mode déconnecté. Peut-il simplement lire les données stockées localement ? Peut-il ajouter des données ? Peut-il même modifier les données existantes ? En supprimer ?

Toute la difficulté tient au fait que si vous autorisez l'utilisateur à modifier en mode déconnecté les données issues du serveur, le référentiel des données locales et celui du serveur vont commencer à diverger. Et au moment de la reconnexion, il faudra réconcilier ces deux référentiels, tout en respectant les liens causaux reliant les informations.

Explorons ces différents niveaux de difficulté.

III-C-1. Synchronisation descendante continue (accès hors-ligne en lecture seule)

Ceci correspond à l'exemple d'une application permettant à l'utilisateur de consulter des articles en mode hors-ligne. Votre application doit simplement rapatrier ces articles lorsqu'une connexion est disponible, les stocker localement, et les mettre à disposition de l'utilisateur pour une utilisation hors-ligne.

Il s'agit donc de pouvoir interroger votre serveur quand c'est possible pour connaître l'ensemble des données qui ont été modifiées ou créées depuis la dernière connexion. L'utilisation de timestamp peut parfaitement faire l'affaire.

III-C-2. Envoi des ajouts au serveur (accès hors-ligne en lecture et ajout)

Si vous voulez permettre à vos utilisateurs de créer de nouvelles données sans modifier celles qui existent déjà dans le référentiel du serveur, vous tombez dans ce cas. Ici la difficulté consiste à pouvoir identifier les enregistrements de la base de données locale qui ont été créés en mode déconnecté, à les transmettre ensuite au serveur (qui les intégrera dans sa base de données avec des identifiant potentiellement différents), et à mettre à jour la base de données locale de façon à refléter de façon authentique les identifiants qui auront été générés par le serveur.

Une astuce que j'ai mis en place dans un projet d'application hors-ligne consistait à générer en mode déconnecté des enregistrements en base avec des identifiants négatifs. De cette façon, une simple requête SQL permettait de récupérer tous les enregistrements créés par l'utilisateur pendant sa déconnexion. A la reconnexion, le client envoyait les nouveaux enregistrements, le serveur les insérait en base et renvoyait au client les identifiants "officiels". Le client n'avait plus qu'à remplacer les identifiants négatifs par ceux générés par le serveur. Tout ceci fonctionne très bien et cela permet de réconcilier les référentiels serveur et client efficacement, en limitant la complexité des traitements.

III-C-3. Envoi des modifications au serveur (accès hors-ligne en lecture et écriture)

La volonté de donner à l'utilisateur une totale liberté quant à l'écriture et la modification de données existantes sur le serveur correspond à la situation la plus complexe à gérer. Les pires scénarios sont imaginables ! En effet imaginez une application de gestion de comptes clients permettant l'édition des informations client en mode déconnecté. Il se peut très bien que deux (ou plus) utilisateurs mettent à jour indépendamment la même fiche client. Comment gérer ces potentiels conflits et quelles techniques existent aujourd'hui à ce sujet ?

Je ne rentrerais pas dans les détails sur ces points mais me contenterais de vous donner quelques pointeurs qui vous seront utiles je l'espère. En fait, on peut distinguer deux problématiques complémentaires : la détection de conflits lors de la reconnexion d'un client, et la résolution de ces conflits.

III-C-3-a. Détection de conflits

La détection de conflit pose la question de savoir quels liens de causalité existent entre plusieurs versions de la même donnée (version S sur le serveur, version C1 sur le client 1 et version C2 sur le client 2 par exemple). Si il n'y a aucun conflit entre ces versions, nous pourrons retrouver un lien de causalité total entre ces différentes versions (par exemple, la version S est plus ancienne que C2 qui elle-même est plus ancienne que C1). Et s'il n'y a pas de conflit, la réconciliation des référentiels client et serveur lors de la reconnexion d'un client sera évidente voire triviale : il suffira de ne conserver que la version la plus récente.

Par contre, il peut se produire des situations dans lesquelles la version C1 n'est pas plus ancienne que la version C2, ni C2 plus ancienne que C1. Que s'est-il produit ? Généralement, les clients 1 et 2 ont obtenu du serveur la même version de l'information et l'ont ensuite modifiée indépendamment l'un de l'autre. Il n'y a pas de lien causal entre la version C1 et la version C2. Heureusement, de grands noms de l'informatique ont déjà planché sur ce sujet. Il s'agit entre autres de Leslie Lamport, qui a découvert l'algorithme connu sous le nom des Horloges de Lamport, dans les années 70. Il a recherché ces algorithmes dans le contexte de l'établissement de liens de causalité dans les systèmes informatiques distribués.

Cet algorithme est la base de celui qui nous intéresse : les Vecteurs de Version. Il s'agit d'un compteur vectoriel que nous associons à chaque donnée et qui permet justement la détection de conflits et l'ordonnancement des liens de causalité présents entre plusieurs versions d'une même donnée.

C'est cet algorithme que j'ai utilisé dans des projets d'applications hors-ligne qui est le plus puissant pour gérer la détection de conflit. Je vous conseille donc de vous informer à ce sujet. Il s'implémente assez facilement, par exemple en ajoutant des colonnes dédiées à sa gestion dans les tables de vos bases de données.

III-C-3-b. Résolution de conflits

Une fois le conflit détecté, il faudra le résoudre, c'est-à-dire choisir parmi les versions proposées laquelle doit être prise comme version "officielle", et qui sera ensuite dispatchée aux clients. Et là le problème est politique dans le sens où la façon de résoudre le conflit dépendra des tenants et aboutissants de votre application. On peut citer quelques politiques courantes :

  • la dernière version (dans le sens temporel) est gagnante ;
  • la version modifiée par l'utilisateur qui a été à l'origine de la création initiale de l'information gagne ;
  • certains utilisateurs gagnent toujours (les administrateurs par exemple) ;
  • merge automatique ou manuel des versions. Vous pouvez proposer à l'utilisateur : "un autre utilisateur a également modifié telle donnée, voici vos deux versions, que dois-je considérer comme version de référence ?" ;
  • choix arbitraire de la version officielle et log du conflit pour résolution ultérieure par un utilisateur habilité…

Toutes ces politiques ont leurs avantages et inconvénients. Le choix tiendra comme je le disais à l'importance de l'information en cours de résolution et aux spécificités de l'application que vous réalisez.

Il est assez intéressant de constater que les algorithmes utilisés pour écrire une application hors-ligne soient ceux-même qui permettent de mettre en œuvre les systèmes informatiques répartis, connus sous le nom de  »cloud » aujourd'hui.

Malheureusement, les architectures de type  »cloud » prennent en charge les incohérences et les fautes des serveurs internes qui les animent, mais pas les terminaux clients qui auraient pu bénéficier des avancées dans ce domaine afin de faciliter le développement d'applications hors-ligne. Il y a donc un travail de développement assez conséquent pour qui veut implémenter une application réellement hors-ligne, j'entends par là que l'utilisateur n'a plus à savoir si il est connecté ou pas.

IV. Démonstration ?

J'aurais aimé vous faire la démonstration que je fais à la fin de chaque conférence sur ce sujet, pour vous montrer "en live" la puissance de ce type d'application. Malheureusement, les moyens me manquent pour déployer et maintenir une application de démonstration. Il vous est cependant possible de regarder la démonstration que j'ai fait d'une telle application lors de la dernière conférence Devoxx (2013) à Paris, disponible sur le site Parleys.

V. Conclusion / Remerciements

Nous avions tout-à-l'heure émis un "cahier des charges" de notre application hors-ligne. Maintenant que nous avons passé en revue l'ensemble des problèmes auxquels nous allions être confrontés, que peut-on conclure ?

Tout d'abord, je pense qu'il est très important de noter que ce type d'application n'est absolument pas un vœux pieux ou un rêve, mais qu'il est tout-à-fait possible de réaliser une telle application. Si le temps manque on pourra moduler les libertés de l'utilisateur lorsqu'il se trouve hors-ligne, c'est-à-dire déconnecté du serveur. Il est de plus tout-à-fait envisageable de lisser et répartir l'effort de développement dans le sens où vous pouvez faire évoluer votre applicatif depuis un niveau de fonctionnalité hors-ligne faible jusqu'à être parfaitement iso-fonctionnel entre mode en-ligne et hors-ligne.

Nous avons vu que les API HTML5 nous permettaient de disposer d'un socle technique suffisamment puissant pour mettre en œuvre des applications hors-ligne.

Nous avons ensuite exploré les difficultés que nous allions rencontrer, et nous nous sommes agréablement surpris en constatant que nous aurions à disposition dans le client web un moteur SQL extrêmement puissant, ce qui nous permettra de construire une pile applicative donnant au développeur une productivité presque égale à celle du développement de la partie serveur.

Mais nous avons également pris conscience que la tâche n'était pas aussi simple et qu'un effort non négligeable sera à fournir. Cet effort sera récompensé car il procure à l'utilisateur une expérience hors du commun. J'ai moi-même était régulièrement bluffé lors du développement de telles applications, en ne sachant plus si mon serveur était disponible ou pas, et si j'étais bien connecté à Internet ou pas.

Tout ceci vous fera réaliser j'espère que la plateforme HTML5 est une superbe plateforme aux immenses possibilités, et qu'il est malgré tout dommage que nous ne l'utilisions pas encore au niveau qu'elle permet. Jamais je ne me lancerais dans le développement d'une application hors-ligne (et donc complexe) en Javascript car comme avec tout langage non typé, une base de code de taille conséquente sera très difficile à maintenir et à faire évoluer. Cependant grâce à GWT, j'écris mon code en Java et cela me permet de mieux garder la main dessus, et d'effectuer la maintenance et la refactorisation à moindre coût. Il ne s'agit pas là d'un argument prosélytique mais simplement d'une constatation que je partage avec vous.

Nous tenons à remercier djibril pour sa relecture orthographique attentive de cet article puis Mickael Baron pour la mise au gabarit.