Supprimer les accents d’une phrase avec Java6

Au cours d’un développement, il arrive qu’il soit nécessaire de supprimer les accents d’une chaine de caractère. S’il faut développer soit-même la solution autant dire que la tâche peut se révéler complexe si on souhaite faire les choses bien.

Heureusement Java propose une solution élégante directement disponible dans le JDK depuis la version 6. Il s’agit de la classe java.text.Normalizer.

La classe Normalizer permet de décomposer les caractères composites en caractères unicode. En clair, cela signifie qu’un caractère accentué est divisé en 2 caractères: le 1er non accentué, puis un second correspondant à l’accent. Le caractère: ‘ê’ sera ainsi décomposé comme suit: ‘e’, ‘^’.

Le premier exemple ci-dessous permet de normaliser une chaîne de caractère selon la norme définie à l’adresse suivante: http://www.unicode.org/reports/tr15/tr15-23.html#Decomposition

public static String normalize(String input) {
  return Normalizer.normalize(input, Normalizer.Form.NFD);
}

Le deuxième exemple, ci-dessous, permet de normaliser la chaîne de caractères, puis de supprimer l’ensemble des caractères représentant les accents des caractères décomposés:

public static String stripAccents(String input) {
  return Normalizer.normalize(input, Normalizer.Form.NFD).replaceAll("\\p{InCombiningDiacriticalMarks}+", "");
}

Comment souventen Java, mieux vaut ne pas essayer de refaire la roue, le JDK offre souvent une solution efficace, et de nombreuses librairies permettent souvent de répondre à vos besoins permettant ainsi de se concentrer un peu plus sur l’objectif de votre développement et d’éviter de perdre du temps sur des détails techniques.

QCon London – The Guardian, Simplifying Development


Lors de la dernière journée de QCon Londres, dans le cadre du track The rise of Scala & Functional Programming, les équipes du journal The Guardian nous proposaient un second retour d’expérience via leur architecte Graham Tackley. L’objectif de cette session était de présenter comment les équipes sont passées à Scala et comment ce choix a simplifié les développements.

Tout d’abord, il faut savoir qu’entre 2006 et 2008, l’architecture applicative du site web du journal reposait sur un socle composé des technos Spring, Hibernate et Velocity. Assez rapidement, ce socle commença à poser des problèmes de maintenabilité aux équipes. En premier lieu, la cause incombait à la verbosité des technologies utilisées. Pour se faire une idée, il suffit de jeter un œil aux métriques et en particulier au cumul des lignes de code:

  • les classes Java représentaientnt 185.000 lignes,
  • les fichiers XML : 35.000 lignes correspondant en grande partie au code Spring,
  • les fichiers de templating (Velocity) : 72.000 lignes.

Ces totaux peuvent vous sembler importants, mais il faut se rappeler que Java n’est pas un langage appliquant particulièrement les principes DRY. De plus, les configurations XML des applications basées sur Spring étaient particulièrement verbeuses ce qui est moins le cas aujourd’hui depuis la généralisation de l’usage des annotations. Il faut cependant relativiser s’il faut ramener ces chiffres à la verbosité des tests unitaires qui représentaient à l’époque, accrochez-vous, 248.000 lignes de code.

Graham nous explique qu’un cycle de développement de deux semaines pour une application web est bien trop long. De plus, cela ne répond pas aux exigences du monde du web. Cependant, cela est trop rapide pour les équipes qui ne peuvent répondre aux cadences exigées par le monde du web avec un applicatif aussi lourd à faire évoluer.

Un autre aspect des problématiques de maintenabilité et de charge semblait venir de la partie ORM de l’application. Non pas qu’Hibernate, utilisé à l’époque, soit remis en cause, mais plutôt les concepts associés.

Une des premières initiatives prises pour remédier aux problèmes de cycles de développement a été de scinder l’application monolithique en différentes micros applications indépendantes avec des  cycles de développement très courts et dont la maintenabilité est facilitée par leur faible taille et leur découplage.

Rapidement, les équipes ont commencé à créer des micros applications dans différentes technologies telles que Python avec Django. Ces outils ont rapidement permis aux équipes de se focaliser sur des aspects métiers plutôt que sur des aspects techniques. Cependant, ces nouveaux environnements semblaient poser des problèmes d’exploitation puisque les environnements d’exécution associés impliquaient de nouvelles acquisitions de compétences. Cela avait pour effet de générer des problématiques d’exploitation qui jusqu’ici n’existaient pas. Comme Graham le rappelle, il vaut mieux privilégier les évolutions plutôt que les révolutions.

Afin de retrouver un environnement proche du monde Java, les équipes ont cherché de nouvelles solutions et se sont orientées vers le développement de composants basés sur Scala. L’objectif recherché étant d’avoir accès aux avantages que peuvent apporter la JVM et l’environnement Java : même infrastructures, même outils (Maven notamment), même bibliothèques, mêmes runtimes, etc. Le compilateur Scala génère du bytecode et un war, c’est à dire tout ce qu’il y a des plus standard dans le monde java. Scala étant opérable avec Java, il est possible de mixer les deux langages dans une même application et de pouvoir reprendre certaines parties de code de l’un ou de l’autre.

Scala est connu pour sa concision, ce qui est réellement important aux yeux de Graham vis-à-vis des problématiques engendrées par la taille de la base de code historique.

En novembre 2009, les équipes sont parties sur un socle technique basé sur Java + Guice + Guice Servlet + Apache Solr et 3 à 4 développeurs, pour switcher en février 2010 de Java à Scala. Graham explique qu’Il n’aura fallu que trois mois à l’équipe pour passer le projet en Scala et le porter en production. Par la suite, il insiste sur le fait que la mise en production s’est passée sans incident, et ce, même avec un changement aussi impactant.

Enfin en juillet 2010, les équipes sont passées à SBT afin de disposer d’un outils plus puissant et mieux adapté aux besoins des projets Scala. SBT permet en particulier d’outrepasser les problématiques de lenteur du compilateur Scala.

La stack actuellement exploitée par le site est composé de Scala + Lift + Apache Solr. On pourra s’interroger sur ces changements de technologies incessants. Mais comme le répète Graham, mieux vaut privilégier les évolutions que les révolutions.

Afin de faire taire certaines idées persistantes à propos de la supposée difficulté d’apprendre Scala, Graham prend les devants en expliquant que ce choix a été plus bénéfique que prévu pour les équipes. Les membres ont pris beaucoup de plaisir à apprendre ce langage, ce qui a favorisé une forte collaboration dans cet apprentissage. Cela a pris entre un et trois mois pour que chaque développeur devienne productif avec le langage. Graham insiste sur le fait que ses équipes n’ont pas rencontré tous ces problèmes énoncés dans différents articles qui ont enflammé la communauté Scala ces dernièrs mois.

Graham voit au final dans Scala plusieurs avantages pour ceux qui veulent l’utiliser ou bien encore l’apprendre :

  • Scala tend à favoriser l’immutabilité dans les développements,
  • La console Scala encourage la vision du code comme une entité vivante (qui évolue),
  • Les frameworks de tests sont d’une extrême simplicité,
  • Scala ne pousse pas à l’intégrer de frameworks complexes à tous les étages.

Parmi les inconvénients imputables au langage, Graham met en avant :

  • La relation de type « je t’aime, moi non plus » des développeurs (en général) avec SBT. On pourrait dire la même chose avec la communauté Java avec Maven…
  • Le support encore immature des outils,
  • Un compilateur lent,
  • Le rythme des évolutions du langage.

Enfin, Graham dit à demi-mot que le framework Play!2 semble très prometteur et qu’il faut garder un œil dessus dans les prochains mois.

Liens utiles:

QCon London – The Guardian, Architecting for failure


La conférence QCon de Londres a été l’occasion pour les équipes du journal « The Guardian » de proposer des retours d’expériences extrêmement enrichissants sur l’architecture de leur site web et l’organisation de leurs équipes de développement. Michael Bruton-Spall, advocate pour le Guardian, a présenté le second jour de la conférence la session: Architecting for Failing.

Le pitch de la session est le suivant: « Your systems are going to fail, it might not be today, it might not be tomorrow, but sometime soon, probably at 2am, your systems are going to fail in new and exiting ways ».

Ce principe a été abordé au cours de plusieurs sessions et fait parti des sujets chauds cette année. La question n’est pas de savoir si votre système tombera, mais plutôt comment vous réagirez lorsque cela arrivera.

Avec la complexité croissante des systèmes informatiques et l’avènement du cloud, les causes de plantage deviennent plus variées et plus nombreuses, et vous ne maîtrisez plus forcement la chaîne d’exploitation de bout en bout. Il est donc nécessaire de monitorer vos systèmes, et d’être préparé à réagir lorsque les choses tournent mal.

Historiquement, le site web du journal The Guardian avait une architecture monolithique. Avec une base de code devenue difficilement maintenable, les équipes ont décidé de revoir le design de leur application et de passer à une architecture à base de micros applications. Le principe est de construire les pages web du site à partir d’un template qui sera complété par l’inclusion de morceaux de code générés par des applications indépendantes. L’idée peut être comparée aux « Servers Side Includes » de l’époque. 

Aujourd’hui, ce concept prend un sens nouveau puisqu’il permet de décomposer une application monolithique en de multiples applications indépendantes les unes des autres. Les implications de ce changement sont une maintenabilité bouleversée puisqu’il s’agit de maintenir des multiples applications ayant une complexité bien plus faible et une base de code unitaire réduite. Il devient ainsi plus simple d’appliquer un changement dans une seule des applications et de ne mettre en production qu’une partie de l’ensemble applicatif. Il n’est plus nécessaire d’attendre une release globale pour mettre en production une nouvelle fonctionnalité.

Cette diversification des applications a toutefois un coût puisqu’il est nécessaire de fournir un support et une maintenance pour chacune d’elles. Afin de simplifier les choses à ce niveau, les équipes de développement ont décidé que chaque nouvelle micro-application serait basée sur des technologies tournant sur la JVM.

L’architecture étant basée sur HTTP, il est possible de mettre en cache les données générées par les différentes micro-applications, et de gérer finement les état des différents caches utilisés. Il est ainsi possible de gérer simplement les pages en erreur au niveau du cache, et de ne pas écraser des données de cache valides par des données en erreurs (« stale-if-error » géré par Varnish).

L’architecture à base de micros-applications permet également de tester, releaser, déployer, et gérer un cycle de vie totalement différent pour chacune des applications. Cela permet de simplifier de façon drastique la maintenance de l’application et de  faciliter les évolutions. Ce changement d’architecture a toutefois impliqué la mise en place d’un cache systématique afin de limiter des coûts de latence élevés. 

Rétrospectivement, le choix d’éclater une application monolithique en de multiples applications indépendantes pouvait paraître risqué puisque cette refonte nécessitait une réécriture en profondeur des applications ainsi qu’une complexification importante de l’architecture, mais ce choix fut vraisemblablement le bon puisque ce changement a permis à l’équipe d’améliorer de façon drastique la maintenabilité de l’application et raccourcir les délais de livraison de nouvelles fonctionnalités.

Par la suite, Michael explique que les équipes ont prévu dans le système différents mécanismes permettant de gérer les imprévus. Les équipes ont ainsi mis en place un mode d’urgence permettant de gérer les pics de traffic: les pages dynamiques étant coûteuses en CPU ainsi qu’en mémoire, il est nécessaire de mettre en place des mécanismes d’urgence permettant de désactiver les traitements coûteux et de privilégier la vitesse. Par ailleurs, les caches de leurs applicatifs n’expirent pas en fonction du temps, mais sur une base de critères variés permettant d’éviter la regénération de contenus lors de pics d’affluence.

Michael explique qu’il est nécessaire de tout cacher, vraiment tout. Les caches in-memory des micros-applications ne sont pas suffisants dans certaines situations: il est nécessaire de cacher des pages entières afin de répondre plus rapidement. Les pages générées sont stockées sur disque et servies en tant que fichiers statiques. Ceci permet de servir plus de 1000 pages par secondes par serveur. Ce mécanisme de mise en cache de pages complètes a pour avantage de ne pas affecter les micros-applications par design.

Le monitoring applicatif est également un aspect essentiel, car il aide à trouver l’origine des problèmes. Il permet de savoir ce qui n’a pas été, quand un problème est survenu, et quel changement a déclenché le problème.

L’aggregation de statistiques dans cette architecture est faite à différents niveaux, et toutes les données monitorables le sont que ce soit des informations de CPU ou bien des informations provenant des micros-applications. Des switchs automatiques permettent d’activer / désactiver des valves, de passer en mode urgence, ou bien de gérer le cas d’une base de données inaccessible. Les valeurs d’activation des switchs correspondent à des valeurs plancher telles que le temps de réponse. Les tendances de ces switchs sont mesurées via leurs outils de monitoring et peuvent être analysées à tout moment.

Michael insiste sur l’importance d’avoir une plateforme applicative « facilement » monitorable. La surveillance des logs doit en être un élément central du système de monitoring. Les logs doivent être parsables, datés, et renseigner sur le code affecté. Enfin, la maîtrise d’outils Unix tels que ‘grep’, ‘cut’, ‘uniq’, ‘sort’, ‘sed’, ou bien encore ‘awk’ doit faire partie de la boîte à outils de chaque développeurs afin de faciliter l’analyse de logs applicatifs.

En fin de session, Michael explique qu’il existe des cas de défaillance prédictibles (Disque dur Fill, CPU à 100%, …), tout comme des cas de défaillance non prédictibles (MTBF, MTBR). Si  vous être en mesure d’anticiper ces défaillances du système, vous serez en mesure de récupérer plus rapidement et d’amoindrir l’impact sur vos utilisateurs. La Keynote du dernier jour de la conférence nous confirme d’ailleurs qu’il préférable privilégier le Mean Time To Recovery plutôt que le MTBR. Michael rappelle également qu’il ne faut jamais dépendre trop fortement de services ‘third parties’, car ils failliront toujours au moment le pire et laisseront vos équipes seules face à leurs problèmes. Michael conclut la session sur l’importance d’architecturer les applications de façon à les rendre résiliantes face à d’éventuelles défaillances de composants ou dépendances dont vous n’avez pas toujours la maîtrise complète.

Liens utiles:

QCon London – Developers Have a Mental Disorder


La keynote de fermeture de cette première journée de conférence à la QCon de Londres est présentée par Greg Young, co-fondateur et CTO d’IMIS, cabinet d’analyse de marchés boursiers à Vancouver BC. Greg Young est ce qu’on peut appeler un agitateur doublé d’un showman. Le moins qu’on puisse dire c’est qu’il sait réveiller une assemblée un peu fatiguée après plus de 8 heures de sessions cloud, mobilité, hardcore java, et j’en passe.

Greg Young met d’entrée de jeu les pieds dans le plat en expliquant que les développeurs aiment résoudre des problèmes que personne n’a. Il met pour cela en avant le concept de réutilisation, qui est trop souvent avancé à tord et à travers pour justifier le développement d’architectures ou d’applications bien trop complexes pour avoir une chance d’être réutilisées par une autre application du SI. Ce problème vient du fait que, le plus souvent, les développeurs aiment construire de l’abstraction autour de concepts simples. Nous voulons abstraire tout ce qui nous passe sous la main mais en tirons rarement bénéfice. 

Il vaut mieux parfois réécrire le même code 2 ou 3 fois, plutôt qu’apporter du couplage dans le logiciel : abstraire les idées apporte de la complexité.

Pour argumenter le concept, il part d’un exemple qu’on rencontre souvent sur des projets informatiques qui démarrent. Lorsqu’on forme une équipe de développement pour un nouveau projet, l’équipe aura tendance à choisir les technologies avant même de connaître réellement le besoin (Tomcat, Spring, Hibernate et MySQL à tout hasard?). Oui, mais seulement en a-t-on vraiment besoin et est-ce seulement adapté à la problématique ?

Greg Young va plus loin en expliquant qu’il vaut mieux parfois « A piece of crap » réalisée en 2 semaines, plutôt qu’une vraie application bien codée réalisée en plusieurs mois (oui, c’est un brin provocateur). Une application réalisée rapidement amènera plus rapidement un feedback et suffira parfois à répondre au besoin.

La question n’est donc pas de savoir si l’application répond à l’état de l’art, mais surtout si elle répond aux besoins réels du client. Inutile de dire que les ORM, qui représentent l’abstraction et la surcomplexité par excellence, en ont pris pour leur grade. Si vous en doutez, posez vous la question de savoir sur quoi repose JPA :

  • JPA est un socle commun à différentes implémentations d’ORM
  • Les ORM sont eux-même des abstractions de la base de données
  • Les bases de données reposent elles-même sur un langage normalisé depuis les années 90, le SQL.

Faut-il en déduire que nous avons un tout petit rien de complexité inutile ? Oui ! Avez-vous déjà travaillé sur des projets pour lequels vous avez déjà eu à changer de base de données ? Si oui, combien de temps aurait coûté une adaptation du code quand bien même il y aurait des changements de requêtage SQL à faire ? En conclusion: oui, nous employons inutilement trop d’abstractions de concepts.

Et pour ôter tout doute de l’esprit, Greg Young prend un exemple frappant, mais ô combien révélateur : si on vous demandait de réaliser un blog, l’idée de proposer une stack de développement à base de Java, Spring, Hibernate vous viendrait-elle à l’esprit, alors que des solutions bien plus adaptées existent ? De mémoire, il existe bien des projets de blogs implémentés avec ces technologies, mais n’est-ce pas sortir un bazooka pour tuer une mouche ?

La keynote de Greg Young, volontairement provocatrice, a pour objectif premier de nous rappeler d’être pragmatique dans notre approche du développement et de garder une vision simple des choses. Nous devons toujours prendre du recul sur les différents choix qui s’offrent à nous. C’est notre travail d’artisan de l’informatique d’adapter nos outils aux besoins du client.

Une de mes phrases préférées de la keynote est la suivante : « Developers love to solve problems that nobody have ». Autant dire que lorsque la salle a été sondée à main levée, on pouvait voir qu’une très grande majorité se reconnaissait dans cette description! Oui, nous, développeurs, avons tendance à trouver des problèmes là où il n’y en a pas. A nous de rester pragmatique et simple pour éviter de tomber dans ce piège classique, et ainsi « éviter de coder un CMS pour gérer une flotte de camions à ordures », dixit Greg Young.

QCon London – Keynote d’ouverture – The Data Panorama

Chaque journée de la QCon de Londres est inaugurée par une keynote. Le premier jour de la conférence, ce sont Martin Fowler et Rebecca Parsons de ThoughtWorks qui se sont prêtés au rituel. Ils ont présenté une keynote d’ouverture nommée : « The Data Panorama ».

Pour eux, le sujet brûlant en 2012, c’est BigData. La donnée est partout et au centre de tout projet informatique. Plus exactement, la donnée est : distribuée, précieuse, connectée, et urgente.

La gestion de la donnée a évolué ces dernières années, notamment en terme de volume. Cependant, il existe aujourd’hui des solutions efficaces pour traiter cet afflux de données dans nos SI en passant par différents outils tels que les algorithmes Map/Reduce basés sur le principe de Divide & Conquer : il est plus intéressant de diviser en de multiples unités réduites de stockages et de traitements, plutôt que d’avoir une unité importante dont le coût d’acquisition peut être élevé. Le concept se décline aujourd’hui de façon optimale avec le cloud qui permet de payer uniquement des coûts d’exploitation correspondant à l’usage.

BigData n’est pas nécessairement un terme à définir. Il s’agirait plutôt d’une notion qui possède des caractéristiques. Le BigData est non relationel, open sourcecluster friendly, schemaless, et adapté aux besoins modernes du web et des entreprises.

Il existe différents types de bases de données NoSQL adaptés aux différents besoins de l’informatique : clé/valeur (Redis, …), document (CouchDB, MongoDB, …), colonne (HBase, Cassandra), et graph (Neo4j). En avril 2010, Michaël Figuière présentait ces différents types de bases NoSQL sur le blog de Xebia : NoSQL Europe: Tour d’horizon des bases de données. Martin Fowler estime toutefois que la base de données « classique » n’est pas à jeter aux oubliettes, mais plutôt à garder intégrée dans le SI en tant que partie de la solution globale à apporter à la problématique de gestion de la donnée.

Traditionnellement, les données en entreprise sont agrégées dans une base d’intégration (données de facturation, d’inventaire, etc.). Or, Martin Fowler met en avant le besoin de changer ce principe pour aller vers un nouveau design de stockage des données au niveau de chacune des applications. La donnée doit être alimentée par de l’Event Sourcing, ce qui revient à dire en des termes simplifiés que les systèmes informatiques doivent capturer les changements d’état des applications comme une séquence d’évènements. Ceci permet d’ouvrir de nombreuses perspectives telles que la reconstruction de données, ou bien la réexécution de scénarios rencontrés en production.

Le cloud est une des clés de la mise en œuvre de systèmes modernes de gestion des données. Martin Fowler le définit comme un outil disponible à la demande en self-service avec de fortes capacités d’élasticité accessibles rapidement. De plus, les outils Cloud doivent être monitorables, et agir en tant que pools de ressources.

Les aspects analytiques ont également changé pour évoluer depuis des besoins de tendance et de variance, vers des besoins de data mining, d’analyse de relations ou bien encore de reconnaissance de motifs. Hadoop, une implémentation de type Map/Reduce, est un outil qui permet de répondre à ces nouveaux besoins en distribuant les jobs de traitement de données sur différentes machines pour en extraire des données importantes et les grouper de façon à être exploitables. 

Martin Fowler aborde en fin de keynote l’importance de la visualisation des données. Il fait référence à un tableau périodique des méthodes de visualisation, qu’il est possible de trouver à l’adresse suivante: http://www.visual-literacy.org/periodic_table/periodic_table.html. Ce tableau permet de comprendre les différents types de données, de poser un visuel de celles-ci et rend compte de la complexité des données avec lesquelles nous travaillons. D’une certaine manière les informaticiens modernes deviennent des sortes des scientifiques ou plutôt des journalistes de la donnée : ils doivent être en mesure d’en extraire les informations pertinentes, afin de mieux anticiper et donc d’être plus réactifs face aux changements. Selon Martin Fowler, Il est de la responsabilité des développeurs de s’assurer qu’elles soient justes et qu’elle ne reflètent pas une visions déformée des informations.

Liens utiles:

Retours QCon Londres 2012

Xebia m’a récemment donné la chance de me rendre à la conférence QCon 2012 de Londres. A travers différents articles rédigés pour le blog de Xebia, que vous pourrez retrouver sur le blog d’ici peu, je vous proposerai de découvrir un peu de cette fantastique conférence! En attendant, n’hésitez pas à visiter le site de la conférence pour en découvrir un peu plus ;)

Introduction aux nouveautés de Groovy 1.8, et Grails 1.4

Ce mardi 7 juin a eu lieu le Paris Groovy & Grail User Group (GUG) dans les locaux de VMware à la Défense. Nous avons le droit à une présentation de Guillaume Laforge introduisant les nouveautés de Groovy 1.8, ainsi que les fonctionnalités à venir dans Groovy 1.9. Cette présentation très intéressante a déjà été jouée lors de la Gr8Conf 2011 de Copenhague et au S2G Forum 2011 de Londres. Cependant si vous l’avez ratée, et que vous souhaitez la voir, vous pouvez vous rattraper en visionnant les slides de la présentation sur sur SlideShare à l’adresse suivante:

Nous avons également eu le droit à une présentation animée par Stéphane Maldini en seconde partie introduisant les nouvelles fonctionnalités de Grails 1.4, et il faut bien avouer que cette nouvelle version va envoyer du lourd au niveau killer features. Grails dans sa version 1.4 va proposer de nombreuses fonctions avancées particulièrement tournées vers la productivité.

Quelques liens intéressants concernant Grails 1.4 :

Lancement du PaaS « Cloud Foundry » par SpringSource

Vous n’avez peut-être pas suivi l’actualité ces derniers jours, mais SpringSource/WMware vient de lancer une version toute nouvelle de son offre Cloud via le site CloudFoundry.com. Cette offre de type PaaS est basée sur le projet Cloude Foundry, qui n’est autre qu’un projet open-source sous licence Apache 2.0 hébergé sur GitHub.

A l’occasion de cette sortie, j’ai rédigé un billet de présentation de Cloud Foundry sur le blog de Xebia. Si vous être curieux de ce qui peut se faire aujourd’hui en terme d’offre PaaS, je vous invite à aller lire mon article à l’adresse suivante:

En vrac, et pour teaser, SpringSource/VMware propose ni plus ni moins qu’une plateforme Cloud multi-langage: Java, Ruby, JavaScript, et autre langages de la JVM. Dans la foulée la plateforme propose différents supports de stockage de données: Redis (Base Clés/valeurs), MongoDB (Base Document), MySql (Base relationnelle). Il est par exemple possible de créer son application Web avec des frameworks tels que Rails, Grails ou bien encore avec Node.JS.

Tout cela, est bien alléchant et donne envie d’aller essayer cette offre. A côté de cela, il ne faut pas oublier que SpringSource/VMware propose également en preview une offre Dev at Cloud nommée Code2Cloud (Git, BugTracker, Hudson, …), ainsi qu’une suite logicielle basée sur Eclipse nommée STS (Spring Tool Suite) permettant d’exploiter parfaitement tous les produit SpringSource/VMware.

Aucun prix n’a pour le moment été communiqué, l’essai de la beta de la plateforme est pour le moment gratuit, mais l’attente d’activation de son compte semble longue. Vous pouvez toujours vous lancer dans le développer d’une application basée sur l’offre de SpringSource/VMware, en démarrant un projet avec STS. Une belle introduction est proposée aux URL suivantes:

Créer un composant Apache Camel de connexion à l’APNS – 3 sur 3

Nous avons vu dans un premier article comment initier le développement d’un composant Apache Camel, puis dans un second comment implémenter ses différentes classes.

A ce stade de notre développement nous sommes déjà en mesure d’utiliser pleinement notre composant, mais nous ne pouvons pas encore en assurer sa qualité. Pour cela, il est nécessaire d’ajouter à notre composant différentes classes de test. Bien que la testabilité de frameworks d’intégration puisse parfois paraître difficile, le projet Apache Camel fournit tous les outils nécessaires permettant de répondre à ce besoin. Nous verrons donc dans cet article comment tester le composant que nous avons développé.

Pour finir, nous verrons comment intégrer notre développement à un projet Camel, ainsi que les limites de notre composant et les solutions pour résoudre ces limitations.

Tester la classe ApnsConsumer

Bien que le projet Apache Camel permette de travailler naturellement avec Spring, nous nous attarderons dans un premier temps sur les méthodes permettant de tester notre composant sur un mode standalone. Pour cela, nous étendrons la classe CamelTestSupport. Cette dernière fournit les méthodes nécessaires pour effectuer différentes assertions sur les valeurs attendues en fin de traitement, en particulier sur des endpoints de type MockEndpoint.

Nous devons tout d’abord implémenter les 2 méthodes suivantes:

Méthode Description
createCamelContext() Permet de créer le contexte Camel qui sera utilisé par le test
createRouteBuilder() Permet de construire la route qui sera appelée par le test. Idéalement, la route doit se terminer par l’appel d’un MockEpoint

Avant l’exécution de nos tests, un serveur APNS bouchonné doit être lancé. Il enverra sur le flux feedback des données factices.

L’objectif de ce test est de valider que le endpoint apns:consumer est capable de consommer les informations de test renvoyées par le flux feedback du serveur bouchonné. Le endpoint Camel mock:result nous permet de valider les assertions nécessaires sur les messages reçus par ce endpoint.

Le endpoint apns:consumer est configuré pour poller le flux feedback de l’APNS toutes les 500ms. Le test ne doit donc pas durer raisonnablement plus de 5 secondes en tenant compte du temps d’attente d’une seconde avant les assertions et du temps de démarrage du serveur bouchon APNS. Un timeout de 5 secondes est donc placé sur le test pour s’assurer que ce dernier tombe en erreur si un événement bloquant inattendu empêche le test de se terminer.

public class ApnsConsumerTest extends CamelTestSupport {

    ApnsServerStub server;

    public ApnsConsumerTest() {
    	super();
    }


    /**
     * Démarrage du serveur bouchonné simulant l'APNS
     */
    @Before
    public void startup() throws InterruptedException {
        server = ApnsServerStub.prepareAndStartServer(FixedCertificates.TEST_GATEWAY_PORT, FixedCertificates.TEST_FEEDBACK_PORT);
    }

    /**
     * Arrêt du serveur bouchonné simulant l'APNS
     */
    @After
    public void stop() {
        server.stop();
    }


    /**
     * Test de l'ApnsConsumer. L'objectif est de vérifier que le flux feedback est
     * bien consommé par la route Camel. Pour cela on vérifie après consommation
     * que le endpoint MockEndpoint a bien reçu les messages.
     */
    @Test(timeout=5000)
    public void testConsumer() throws Exception {

    	byte[] deviceTokenBytes = ApnsUtils.createRandomDeviceTokenBytes();
        String deviceToken = ApnsUtils.encodeHexToken(deviceTokenBytes);

        MockEndpoint mock = getMockEndpoint("mock:result");
        mock.expectedMessageCount(1);
        mock.message(0).body().isInstanceOf(InactiveDevice.class);

        byte[] feedBackBytes = ApnsUtils.generateFeedbackBytes(deviceTokenBytes);
        server.toSend.write(feedBackBytes);

        Thread.sleep(1000);

        assertMockEndpointsSatisfied();

        InactiveDevice inactiveDevice = (InactiveDevice)mock.getExchanges().get(0).getIn().getBody();
        assertNotNull(inactiveDevice);
        assertNotNull(inactiveDevice.getDate());
        assertNotNull(inactiveDevice.getDeviceToken());
        assertEquals(deviceToken, inactiveDevice.getDeviceToken());
    }

    /**
     * Création et configuration du contexte Camel avec une configuration de test.
     * Enregistrement du composant APNS, et configuration de l'ApnsService
     */
    protected CamelContext createCamelContext() throws Exception {
        CamelContext camelContext = super.createCamelContext();

        ApnsServiceFactory apnsServiceFactory = ApnsUtils.createDefaultTestConfiguration();
        ApnsService apnsService = apnsServiceFactory.getApnsService();

        ApnsComponent apnsComponent = new ApnsComponent(apnsService);

        camelContext.addComponent("apns", apnsComponent);

        return camelContext;
    }

    /**
     * Création de la route Camel permettant de simuler la consommation du flux
     * feedback de l'APNS
     */
    protected RouteBuilder createRouteBuilder() throws Exception {
        return new RouteBuilder() {
            public void configure() throws Exception {
                from("apns:consumer?initialDelay=500&delay=500&timeUnit=MILLISECONDS")
                	.to("log:com.apache.camel.component.apns?showAll=true&multiline=true")
                	.to("mock:result");
            }
        };
    }

}

Tester la classe ApnsProducer

De la même façon que nous nous y sommes pris pour la classe de test ApnsConsumerTest, nous allons étendre la classe CamelTestSupport pour tester notre classe ApnsProducer. L’objectif est de vérifier que le serveur bouchonné reçoit bien les messages produits.

public class ApnsProducerTest extends CamelTestSupport {

    private ApnsServerStub server;
    private String FAKE_TOKEN = "19308314834701ACD8313AEBD92AEFDE192120371FE13982392831701318B943";

    public ApnsProducerTest() {
    	super();
    }


    /**
     * Démarrage du serveur bouchonné simulant l'APNS
     */
    @Before
    public void startup() {
        server = ApnsServerStub.prepareAndStartServer(FixedCertificates.TEST_GATEWAY_PORT, FixedCertificates.TEST_FEEDBACK_PORT);
    }

    /**
     * Arrêt du serveur bouchonné simulant l'APNS
     */
    @After
    public void stop() {
        server.stop();
    }


    /**
     * Test de l'ApnsProducer. L'objectif est de vérifier que le serveur APNS a
     * bien reçu les notifications envoyées. 
     * Pour cela on vérifie après production des notifications que le serveur bouchonné
     * a bien reçu les messages: On s'assure que les tableaux de bytes de la notification
     * produite et du contenu reçu par le serveur sont bien égaux.
     */
    @Test(timeout=2000)
    public void testProducer() throws Exception {
    	String message = "Hello World";
    	String messagePayload = APNS.newPayload().alertBody(message).build();

        ApnsNotification apnsNotification = new ApnsNotification(FAKE_TOKEN, messagePayload);
        server.stopAt(apnsNotification.length());

        template.sendBody("direct:test", message);

        server.messages.acquire();
        assertArrayEquals(apnsNotification.marshall(), server.received.toByteArray());
    }

    /**
     * Création et configuration du contexte Camel avec une configuration de test.
     * Enregistrement du composant APNS, et configuration de l'ApnsService
     */
    protected CamelContext createCamelContext() throws Exception {
        CamelContext camelContext = super.createCamelContext();

        ApnsServiceFactory apnsServiceFactory = ApnsUtils.createDefaultTestConfiguration();
        ApnsService apnsService = apnsServiceFactory.getApnsService();

        ApnsComponent apnsComponent = new ApnsComponent(apnsService);
        camelContext.addComponent("apns", apnsComponent);

        return camelContext;
    }

    /**
     * Création de la route Camel permettant de simuler la production de messages
     * à destination des serveurs APNS.
     */
    protected RouteBuilder createRouteBuilder() throws Exception {
        return new RouteBuilder() {
            public void configure() throws Exception {
                from("direct:test").
                setHeader(ApnsConstants.HEADER_TOKENS, constant(FAKE_TOKEN)).
                to("apns:notify");
            }
        };
    }
}

Tester l’intégration d’un composant Camel avec Spring

Les configurations Camel sont souvent déclarées par des fichiers de configuration Spring. C’est pourquoi il est nécessaire de tester l’intégration du composant dans une configuration Spring. Un bon moyen d’automatiser ce type de test est d’utiliser les classes de support JUnit fournies par Spring ( _AbstractJUnit4SpringContextTests_ ).

L’ensemble de la configuration est déclarée dans le fichier SpringApnsConsumerTest-context.xml associé au test unitaire. Les différents beans utilisés par la classe de test sont injectés via les annotations @Autowired et @EndpointInject(uri = « mock:result »).

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:camel="http://camel.apache.org/schema/spring"
       xsi:schemaLocation="
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
        http://camel.apache.org/schema/spring http://camel.apache.org/schema/spring/camel-spring.xsd">

	<bean id="apnsServiceFactory" class="org.apache.camel.component.apns.factory.ApnsServiceFactory">
		<property name="feedbackHost" value="localhost" />
		<property name="feedbackPort" value="7843" />
		<property name="gatewayHost" value="localhost" />
		<property name="gatewayPort" value="7654" />
		<property name="sslContext" ref="sslContext" />
	</bean>

	<!-- Déclaration de l'ApnsService, utilisé par l'ApnsComponent -->
	<bean id="apnsService" factory-bean="apnsServiceFactory" factory-method="getApnsService" />


	<bean id="sslContext" class="org.apache.camel.component.apns.util.FixedCertificates" factory-method="clientContext"/>

	<!-- Déclaration de l'ApnsComponent. Le scheme utilisé pour la création des endpoints -->
	<!-- sera l'Id utilisé pour la création du bean -->
	<bean id="apns" class="org.apache.camel.component.apns.ApnsComponent">
		<property name="apnsService" ref="apnsService" />
	</bean>

	<!-- Déclaration du context Camel, et de la route de test: 'apns-test' -->
	<!-- Les message reçus par le flux feedback seront envoyés vers le endpoint mock:result -->
	<camelcontext id="camel-apns-test" xmlns="http://camel.apache.org/schema/spring">

		<route id="apns-test">
			<from uri="apns:consumer?initialDelay=500&amp;amp;amp;amp;delay=500&amp;amp;amp;amp;timeUnit=MILLISECONDS" />
			<to uri="log:org.apache.camel.component.apns?showAll=true&amp;amp;amp;amp;multiline=true" />
			<to uri="mock:result" />
		</route>

	</camelcontext>

</beans>

Une fois la déclaration du composant Camel effectuée via le fichier de configuration Spring, il ne reste plus qu’à injecter les différents beans configurés dans les propriétés de la classe de test. Le contenu du test en lui-même reste identique, cependant la partie configuration a disparu ce qui rend le test plus lisible. Afin de faire disparaître du test ce qui peut encore en parasiter sa lisibilité, une classe de test parent peut être créée pour contenir les méthodes d’initialisation et d’arrêt du serveur APNS de test.

@ContextConfiguration
public class SpringApnsConsumerTest extends AbstractJUnit4SpringContextTests {

    ApnsServerStub server;

    /**
     * Injection du context Camel déclaré via Spring
     */
    @Autowired
    protected CamelContext camelContext;

    /**
     * Injection du endpoint Camel "mock:result" déclaré via Spring
     */
    @EndpointInject(uri = "mock:result")
    protected MockEndpoint mock;

    public SpringApnsConsumerTest() {
    	super();
    }


    /**
     * Démarrage du serveur bouchonné simulant l'APNS
     */
    @Before
    public void startup() throws InterruptedException {
        server = ApnsServerStub.prepareAndStartServer(FixedCertificates.TEST_GATEWAY_PORT, FixedCertificates.TEST_FEEDBACK_PORT);
    }

    /**
     * Arrêt du serveur bouchonné simulant l'APNS
     */
    @After
    public void stop() {
        server.stop();
    }


    /**
     * Test du consommateur
     */
    @Test(timeout=5000)
    public void testConsumer() throws Exception {

    	byte[] deviceTokenBytes = ApnsUtils.createRandomDeviceTokenBytes();
        String deviceToken = ApnsUtils.encodeHexToken(deviceTokenBytes);

        mock.expectedMessageCount(1);
        mock.message(0).body().isInstanceOf(InactiveDevice.class);

        byte[] feedBackBytes = ApnsUtils.generateFeedbackBytes(deviceTokenBytes);
        server.toSend.write(feedBackBytes);

        Thread.sleep(1000);

        mock.assertIsSatisfied();

        InactiveDevice inactiveDevice = (InactiveDevice)mock.getExchanges().get(0).getIn().getBody();
        Assert.assertNotNull(inactiveDevice);
        Assert.assertNotNull(inactiveDevice.getDate());
        Assert.assertNotNull(inactiveDevice.getDeviceToken());
        Assert.assertEquals(deviceToken, inactiveDevice.getDeviceToken());
    }

}

Limitations du composant actuel

Dans un soucis de simplicité, le composant présenté est limité fonctionnellement et ne propose qu’un ensemble réduit de fonctionnalités. L’implémentation proposée dans ce billet nous oblige à déclarer les tokens à notifier directement au niveau de la route. Cependant une implémentation plus complète pourrait permettre d’exploiter des en-têtes par exemple pour rendre la sélection de token à notifier plus dynamique.

Heureusement, la richesse du framework Apache Camel nous permet de passer outre cette restriction et d’obtenir l’aspect dynamique souhaité en utilisant le pattern Recipient List, qui permet par exemple l’extraction des URIs de destination depuis un en-tête de message produit au préalable.

RouteBuilder builder = new RouteBuilder() {
    public void configure() {
        from("direct:a").recipientList(
        header("recipientListHeader").tokenize(","));
    }
};

Dans cet exemple, l’en-tête recipientListHeader contiendrait les différentes URIs séparées par des virgules.

Utilisation du composant dans un projet

Pour utiliser le composant développé dans cette série d’articles, il suffit de l’importer en tant que dépendance Maven dans votre projet Camel et de déclarer le repository qui permettra de le charger à défaut de l’avoir déjà dans son répository local.

Ce qui donne les lignes suivantes à ajouter dans le pom de votre projet:

  • La dépendance Maven:
<dependency>
    <groupid>org.apache.camel</groupid>
    <artifactid>camel-apns</artifactid>
    <version>2.4.0</version>
</dependency>
  • Le repository Maven qui met à disposition le composant:
<repositories>
    <repository>
        <id>camel-apns.repo-release</id>
        <url>http://camel-apns.googlecode.com/svn/maven/public/repository/release/</url>
    </repository>
</repositories>

Exemples d’utilisation

Afin de donner une vision synthétique de l’usage de notre composant, voici un condensé de quelques exemples d’utilisation:

  • Consommation du flux feedback avec une configuration Java
      from("apns:consumer")
            .to("log:com.apache.camel.component.apns?showAll=true&amp;amp;amp;amp;multiline=true")
            .to("mock:result");

  • Envoi de notifications avec une configuration Java
      from("direct:test")
            .setHeader(ApnsConstants.HEADER_TOKENS, constant(FAKE_TOKEN))
            to("apns:notify");
  • Envoi d’une notification avec une configuration Spring
<camelcontext id="camel-apns-test" xmlns="http://camel.apache.org/schema/spring">

	<route id="apns-test">
		<from uri="apns:consumer" />
		<to uri="log:org.apache.camel.component.apns?showAll=true&amp;amp;amp;amp;multiline=true" />
		<to uri="mock:result" />
	</route>

</camelcontext>

Conclusion

Nous avons vu dans cet article comment créer et tester un composant Camel qui communique avec les serveurs de notifications d’Apple. Les API du framework ont été pensées pour faciliter le développement de nouveaux composants, et vous permettront d’implémenter et de tester facilement les composants dont vous avez besoin. Vous pouvez consulter le code source du composant camel-apns présenté dans cette série d’article sur sa page Google Code.

Liens utiles

Le site google code du composant ‘camel-apns’ présenté dans le billet:

D’autres liens utiles sur le développement de composants Camel:

Précédentes parties de l’article:

Créer un composant Apache Camel de connexion à l’APNS – 2 sur 3

Nous avons vu dans un premier article comment initier le développement d’un composant Apache Camel. Cependant, nous n’avons pas encore abordé son développement à proprement parler et notre composant ne permet pas encore de communiquer avec les serveurs Apple. Nous allons donc voir dans cet article comment implémenter les différentes classes nécessaires au bon fonctionnement de notre composant.
Pour rappel l’objectif est de développer un composant capable de communiquer avec l’Apple Push Notification Service, qui permet d’envoyer des notifications aux appareils mobiles d’Apple (iPad, iPhone, iPod Touch).

Implémentation du composant

Les quatre interfaces suivantes doivent être implémentées pour développer un composant Camel permettant à la fois de consommer et produire des messages :

Interfaces à implémenter Description
Component Elle permet de créer les endpoints relatifs au composant.
Endpoint Elle permet la création des Consumers et Producers relatifs à un endpoint défini par son URI.
Consumer Elle correspond à l’implémentation des Consumers relatifs au endpoint d’un composant.
Producer Elle correspond à l’implémentation des Producers relatifs au endpoint d’un composant.

La classe ApnsService

Pour communiquer avec les serveurs Apple, nous devons instancier un objet ApnsService. Le framework java-apns fournit pour cela la classe ApnsServiceBuilder. Celle-ci permet de construire facilement un objet ApnsService en spécifiant au builder la configuration souhaitée.

Le builder est obtenu grâce à la classe utilitaire Apns :

        ApnsServiceBuilder builder = APNS.newService();

Ensuite, il suffit d’appeler les différentes méthodes de configuration du builder. Dans le cadre d’une configuration simple, le code source suivant est suffisant :


// Instanciation du builder
ApnsServiceBuilder builder = APNS.newService();


InputStream certificateInputStream = null;
try {
    // Ouverture de l'inputStream correspondant au certificat
	certificateInputStream = ResourceUtils.getInputStream("certificate_classpath");
	// Configuration du certificat utilisé pour communiquer avec l'APNS
	builder.withCert(certificateInputStream, "certificate_password")

}
finally {
	// Fermeture de l'inputStream correspondant au certificat
	ResourceUtils.close(certificateInputStream);
}

// Configuration des URLs par défaut de production vers les serveurs APNS
builder.withProductionDestination();

// Configuration d'un pool de 5 connexions vers les serveurs APNS
builder.asPool(5);
// Configuration d'une politique de reconnexion uniquement lorsqu'une connexion vers les serveurs APNS est fermée
builder.withReconnectPolicy(ReconnectPolicy.Provided.NEVER);

// Obtention de l'instance configurée
ApnsService apnsService = builder.build();

Note : Un exemple de configuration plus complet de l’objet ApnsService peut être trouvé dans le code source du composant camel-apns.

Afin d’intégrer de façon efficace notre composant avec Spring, la classe ApnsServiceBuilder se révèle être un bon point de départ pour écrire une factory permettant de construire un objet ApnsService via Spring.

La classe ApnsComponent

L’interface Component correspond à l’unité de base définissant un composant Camel. Ce dernier est associé au scheme d’une URI lorsqu’il est ajouté à un contexte d’exécution.

Une implémentation par défaut de cette interface est fournie (en l’occurrence la classe DefaultComponent), ce qui permet de se focaliser sur les fonctionnalités de notre composant. Un composant Camel a pour rôle de créer les endpoints relatifs aux URI définies dans le contexte Camel. Cette tâche est réalisée par la méthode createEndpoint.

Chaque endpoint créé à partir d’un même ApnsComponent utilisera une unique configuration pour échanger avec l’APNS. C’est pourquoi le service d’accès à l’APNS sera injecté au niveau de l’objet ApnsComponent.

Le bean apnsService peut être setté par le constructeur ou bien par un setter pour faciliter l’intégration avec Spring.

public class ApnsComponent extends DefaultComponent {

	private ApnsService apnsService;

	public ApnsComponent() {
		super();
	}

	public ApnsComponent(ApnsService apnsService) {
		super();
		AssertUtils.notNull(apnsService, "apnsService is mandatory");
		this.apnsService = apnsService;
	}

	public ApnsComponent(CamelContext context) {
		super(context);
	}

	public ApnsService getApnsService() {
		return apnsService;
	}

	@SuppressWarnings("unchecked")
	protected Endpoint createEndpoint(String uri, String remaining, Map parameters) throws Exception {
		ApnsEndpoint endpoint = new ApnsEndpoint(uri, this);
		setProperties(endpoint, parameters);

		return endpoint;
	}

    /**
     * L'objet ApnsService peut être setté s'il le constructeur vide a été appelé pour instancier un objet ApnsComponent
     */
	public void setApnsService(ApnsService apnsService) {
		if (this.apnsService != null) {
			throw new IllegalArgumentException("apnsService already setted");
		}
		this.apnsService = apnsService;
	}

}

La classe ApnsEndpoint

La classe ApnsEndpoint permet de gérer la création des objets Consumer et Producer définis dans les routes Camel. Dans notre cas, nous étendrons la classe ScheduledPollEndpoint. Elle permet d’implémenter un endpoint qui saura créer des objets Consumer dont le déclenchement s’effectuera à intervalle régulier.

C’est la classe ApnsEndpoint qui a la charge de gérer les paramètres passés dans l’URI. Ainsi, les tokens passés en paramètres de l’URI d’un objet ApnsEndpoint seront injectés automatiquement via les setters correspondant aux paramètres renseignés. Si un paramètre ne correspond pas à un setter déclaré, une exception sera alors lancée.

Afin de faciliter le travail de configuration des paramètres spécifiques au travail de polling, c’est la classe ScheduledPollEndpoint qui lira automatiquement les paramètres de déclenchement renseignés sur l’URI. L’ApnsEndpoint aura donc toutes les informations nécessaires pour configurer les paramètres de déclenchement de l’objet ApnsConsumer.

Dans le cas de l’exécution de tests, les paramètres de déclenchement de la consommation de messages en provenance de l’APNS peuvent être configurés de la façon suivante :

from("apns:consumer?initialDelay=500&amp;amp;amp;amp;amp;amp;delay=500&amp;amp;amp;amp;amp;amp;timeUnit=MILLISECONDS")
	.to("log:com.apache.camel.component.apns?showAll=true&amp;amp;amp;amp;amp;amp;multiline=true")
	.to("mock:result");

L’implémentation de la classe ApnsEndpoint donnera le code source suivant :

public class ApnsEndpoint extends ScheduledPollEndpoint {

	@SuppressWarnings("unused")
	private static final Log LOG = LogFactory.getLog(ApnsEndpoint.class);

	private CopyOnWriteArraySet<defaultconsumer> consumers = new CopyOnWriteArraySet</defaultconsumer><defaultconsumer>();

	private String tokens;

	public ApnsEndpoint(String uri, ApnsComponent component) {
		super(uri, component);
	}

	public String getTokens() {
		return tokens;
	}

	public void setTokens(String tokens) {
		this.tokens = tokens;
	}

	private ApnsComponent getApnsComponent() {
		return (ApnsComponent)getComponent();
	}

    /**
     * On obtient une instance de la clase ApnsService depuis l'object ApnsComponant
     */
	public ApnsService getApnsService() {
		return getApnsComponent().getApnsService();
	}

    /**
      * Indique que le endpoint n'est instancié qu'une seule fois par le contexte Camel
      */
	public boolean isSingleton() {
		return true;
	}

    /**
     * Permet d'obtenir la liste des consumers
     */
	protected Set</defaultconsumer><defaultconsumer> getConsumers() {
		return consumers;
	}

    /**
     * Permet de créer un consumer
     */
	public Consumer createConsumer(Processor processor) throws Exception {

		ApnsConsumer apnsConsumer = new ApnsConsumer(this, processor);
		configureConsumer(apnsConsumer);

		return apnsConsumer;
	}

	/**
	 * Permet de créer un producer
	 */
	public Producer createProducer() throws Exception {
		return new ApnsProducer(this);
	}

}

La classe ApnsProducer

La classe ApnsProducer correspond à la classe qui permet d’envoyer des notifications aux terminaux mobiles Apple via les serveurs APNS. La classe abstraite DefaultProducer sera étendue pour fournir un support de base à notre implémentation.

Seule la méthode process reste ainsi à renseigner. Il suffira d’y implémenter l’envoi de la notification à l’APNS comme suit :

public class ApnsProducer extends DefaultProducer {

	private static final transient Log LOG = LogFactory.getLog(ApnsProducer.class);

	private ApnsEndpoint endpoint;

	private List<string> tokenList;

	public ApnsProducer(ApnsEndpoint endpoint) {
		super(endpoint);
		this.endpoint = endpoint;
		configureTokens(apnsEndpoint);
	}

    /**
     * La méthode configureTokens permet d'extraire la liste de tokens destinataires
     * des notifications envoyées au endpoint.
     */
	private void configureTokens(ApnsEndpoint apnsEndpoint) {
		if (StringUtils.isNotEmpty(apnsEndpoint.getTokens())) {
			try {
				this.tokenList = extractTokensFromString(apnsEndpoint.getTokens());
			} catch (CamelException e) {
				throw new IllegalArgumentException(e);
			}
		}
	}

	/**
	 * Méthode appelée par le producer pour traiter les échanges Camel
	 */
	public void process(Exchange exchange) throws Exception {
		notify(exchange);
	}

	/**
	 * La méthode notify  est appelée pour envoyer une notification aux serveurs APNS.
	 */
	private void notify(Exchange exchange) throws ApnsException, CamelException {

		String payload = exchange.getIn().getBody(String.class);

                // Une copie de la liste des tokens à notifier est passée en paramètre,
                // ainsi que le payload du message
		endpoint.getApnsService().push(new ArrayList</string><string>(tokenList), payload);
	}

	/**
	 * On extrait une liste de tokens à partir d'une chaîne de caractères contenant
	 * des tokens séparés par un point virgule.
	 */
	private List</string><string> extractTokensFromString(String tokensStr) throws CamelException {

		tokensStr = StringUtils.trim(tokensStr);

		if (tokensStr.isEmpty()) {
			throw new CamelException("No token specified");
		}

		String[] tokenArray = tokensStr.split(";");

		int tokenArrayLength = tokenArray.length;
		for (String token : tokenArray) {
			token = token.trim();
			int tokenLength = token.length();
			// La taille d'un token est limitée à 64 caractères
			if (tokenLength != 64) {
				throw new CamelException("Token has wrong size['" + tokenLength + "']: " + token);
			}
		}

		List</string><string> tokens = Arrays.asList(tokenArray);

		return tokens;
	}

}

Une fois la notification envoyée aux serveurs APNS, le terminal Apple recevra la notification et l’affichera comme suit dans le cas d’une notification texte:

iphone-push-notification-1st-screenshots  iphone-push-notification-2nd-screenshots

La classe ApnsConsumer

La classe ApnsConsumer a pour objectif de nous permettre de consommer le flux feedback renvoyé par les serveurs APNS, permettant de connaître les terminaux mobiles Apple pour lesquels il n’est plus nécessaire d’envoyer de notifications (Par exemple, lorsque l’application a été désintallée).

Comme vu précédemment, la stratégie de consommation peut suivre un pattern de type Event Driven Consumer ou bien de Polling Consumer. Ici, nous choisirons le pattern Event Driven Consumer, et nous étendrons la classe ScheduledPollConsumer qui prend en charge toute la complexité de la gestion de consommation des messages et nous laisse nous concentrer sur le principal, c’est à dire, fournir les messages à consommer par nos routes de traitement.

Pour cela, il suffit d’implémenter la méthode poll qui permet d’interroger le service feedback d’Apple selon le timing d’interrogation configuré par l’URI.

C’est la classe ApnsEndpoint, qui au moment de la création de l’objet ApnsConsumer, configurera les paramétrages de polling à partir des paramètres récupérés via l’URI.

La méthode doStart aura pour rôle de vérifier qu’un seul objet ApnsConsumer est créé afin de ne pas consommer en double les messages d’une même configuration de l’ApnsService.

La méthode poll est implémentée ici de façon à récupérer les informations sur les appareils pour lesquels il ne faut plus envoyer de notifications. Pour cela, nous récupérons dans un premier temps une liste d’objets InactiveDevice renvoyés par le flux feedback de l’APNS, puis nous itérons sur cette liste pour les faire traiter par le processor Camel, qui n’est autre que la route de traitement Camel.

public class ApnsConsumer extends ScheduledPollConsumer {

	private static final int DEFAULT_CONSUME_INITIAL_DELAY = 10;
	private static final int DEFAULT_CONSUME_DELAY = 3600;
	private static final TimeUnit DEFAULT_CONSUME_TIME_UNIT = TimeUnit.SECONDS;
	private static final boolean DEFAULT_APNS_FIXED_DELAY = true;

	/**
	 * La configuration par défaut des paramètres de polling est fait lors de la
	 * construction de l'objet. Ces valeurs pourront être écrasées par des valeurs
	 * configurées via l'URI.
	 */
	public ApnsConsumer(ApnsEndpoint apnsEndpoint, Processor processor) {
		super(apnsEndpoint, processor);

		setInitialDelay(DEFAULT_CONSUME_INITIAL_DELAY);
		setDelay(DEFAULT_CONSUME_DELAY);
		setTimeUnit(DEFAULT_CONSUME_TIME_UNIT);
		setUseFixedDelay(DEFAULT_APNS_FIXED_DELAY);
	}

	/**
	 * Chaque élément de cette liste obtenue par l'appel de la méthode  getInactiveDevices()
	 * sera passé à la route Camel via l'appel de la méthode getProcessor().process(e)
	 */
	protected void poll() throws Exception {
		List<inactivedevice> inactiveDeviceList = getInactiveDevices();

		Iterator</inactivedevice><inactivedevice> it = inactiveDeviceList.iterator();

		while(it.hasNext()) {
			InactiveDevice inactiveDevice = it.next();

			Exchange e = getEndpoint().createExchange();

			e.getIn().setBody(inactiveDevice);

			// On donne chaque élément de la liste de terminaux inactifs
			// reçu sur le flux feedback pour qu'il soit traité par la route associée
			getProcessor().process(e);
		}
	}

	/**
	 * La méthode getInactiveDevices() permet d'obtenir depuis le flux feedback une
	 * liste d'objects InactiveDevice.
	 */
	private List</inactivedevice><inactivedevice> getInactiveDevices() {
		ApnsEndpoint ae = (ApnsEndpoint)getEndpoint();

		Map<string , Date> inactiveDeviceMap = ae.getApnsService().getInactiveDevices();

		List<inactivedevice> inactiveDeviceList = new ArrayList</inactivedevice><inactivedevice>();

		for (Entry<string , Date> inactiveDeviceEntry : inactiveDeviceMap.entrySet()) {
			String deviceToken = inactiveDeviceEntry.getKey();
			Date date = inactiveDeviceEntry.getValue();

			InactiveDevice inactiveDevice = new InactiveDevice(deviceToken, date);

			inactiveDeviceList.add(inactiveDevice);
		}

		return inactiveDeviceList;
	}

    @Override
	public ApnsEndpoint getEndpoint() {
		return (ApnsEndpoint)super.getEndpoint();
	}

	/**
	 * La consommation du flux feedback doit être faite par un unique consumer.
	 * La méthode doStart permet de s'enassurer lors du démarrage.
	 * Si un autre consumer est déjà déclaré, alors une exception sera lancée.
	 */
    @Override
    protected void doStart() throws Exception {
        // only add as consumer if not already registered
        if (!getEndpoint().getConsumers().contains(this)) {
            if (!getEndpoint().getConsumers().isEmpty()) {
                throw new IllegalStateException("Endpoint " + getEndpoint().getEndpointUri() + " only allows 1 active consumer but you attempted to start a 2nd consumer.");
            }
            getEndpoint().getConsumers().add(this);
        }
        super.doStart();
    }

    @Override
    protected void doStop() throws Exception {
        super.doStop();
        getEndpoint().getConsumers().remove(this);
    }

}

Conclusion

Nous avons vu dans cette seconde partie comment implémenter les classes nécessaires au développement d’un composant Apache Camel. Nous verrons dans une dernière partie comment mettre en place différentes stratégies de test pour valider notre composant.

Liens utiles

Le site google code du composant ‘camel-apns’ présenté dans l’article :

Parties suivantes et précédentes de l’article :