WebSocket, et comment on teste?

18 juin 2014

Tags : Assertj, Awaitility, Java, Jetty, SimpleWeb4j, websocket

On a vu comment implémenter des websockets avec SimpleWeb4j dans un précédant article, nous allons maintenant voir comment tester de manière automatisée la partie serveur websocket.


Librairies tierces

Pour garder un code relativement propre, je vais utiliser trois librairies :


Je veux faire quoi comme test?

Je cherche à tester le code du billet précédant qui, pour rappel, était un système de chat. Le serveur de websocket reçoit donc des messages qu'il redistribue à tous les clients connectés.

Scénario de test :

  • Ouverture d'une session 1.
  • Envoi d'un message par la session 1 (message 1).
  • Ouverture d'une session 2.
  • Envoi d'un message par la session 2 (message 2).
  • Envoi d'un message par la session 1 (message 3).
  • Fermeture de la session 1.
  • Envoi d'un message par la session 2 (message 4).
  • Fermeture de la session 2.

Vérifications à faire :

  • La session 1 a reçu 3 messages (les messages 1 à 3)
  • La session 2 a reçu 3 messages (les messages 2 à 4)


Code de test

Assez parler, place au code. J'espère que les commentaires sont suffisants, si ce n'est pas le cas, n'hésitez pas à le dire j'en ajouterais.

Commentaires

Services REST avec SimpleWeb4J

30 mai 2014

Tags : Java, json, REST, SimpleWeb4j

Cet article est le deuxième article de la série sur SimpleWeb4j

Dans le première article on a vu comment démarrer SimpleWeb4j en une ligne de code, dans l'article d'aujourd'hui, nous allors voir comment exposer des services REST en json.

Premiers cas : exposer une String "Hello World"

J'espère que le code est suffisamment simple pour ne pas avoir besoin d'explication.

On peux voir qu le résultat est entouré de guillemets, ceci est du au fait que SimpleWeb4j transforme la chaîne de caractère en json.


Deuxième cas : utilisation d'une variable de route

Cet exemple mérite peut-être un peu plus d'explications. SimpleWeb4j permet d'ajouter des paramètres à la route, ces paramètres sont préfixés de ':'. Pour les récupérer on utilise le paramètre RouteParameters qui est le deuxième argument de notre lambda. Le premier argument quand à lui contient l'objet envoyé dans le request body de la requête, nous ne l'utilisons donc pas.


Troisième cas : objet complexe

Nous avons vu que renvoyer une chaîne de caractère était plutôt simple, voyons maintenant comment renvoyer un objet plus complexe.

On peut donc voir que renvoyer un objet n'est pas plus compliqué, SimpleWeb4j s'occupe de transformer celui-ci en json



J'espère vous avoir donner envie de regarder SimpleWeb4j de plus près, si quelque chose vous manque, n'hésitez pas à le dire ou à faire une pull request sur github.

Commentaires

SimpleWeb4J - quick start

28 mai 2014

Tags : Java, SimpleWeb4j

Ceux qui me suivent connaissent sans doute déjà SimpleWeb4j. Il s'agit d'encore un framework web donc le but est de pouvoir faire du web en java de manière très simple.

Je n'ai jamais écrit d'article sur SimpleWeb4j, je vais donc écrire une série d'articles sur ce sujet afin de vous montrer la simplicité de ce framework.

Je vais commencer par le classique Quick Start, ou comment démarrer un serveur Web capable de servir des ressources statiques.

Dépendance maven

Ressources statiques

Mettez vos ressources statiques au sein d'un package "public" dans votre classpath.

Classe main

Il vous suffit ensuite de démarrer le serveur :

Et voilà, vous avez un serveur prêt sur http://localhost:9999, pas besoin de déclaration xml ou autre complexité, un simple appel à start suffit.

Commentaires

Scheduler en Java

27 mai 2014

Tags : ExecutorService, Java, simple

Si je vous demande comment déclencher une tache récurrente en Java, je suis sûr que certains d'entre vous répondront Quartz...

Ceux qui répondent ça ne connaissent sans doute pas les ExecutorService de Java, et plus particulièrement l'interface ScheduledExecutorService (apparue en Java 1.5...).

Voici donc un petit exemple permettant d'afficher l'heure courante toute les secondes :

Ne vous habituez pas à un article par jour, ça ne va pas durer :)

Commentaires

Un cache en java

26 mai 2014

Tags : Java, java8, simple

Après bientôt un an sans écrire d'article, je vais essayer de me remettre à écrire des trucs... Je ne garanti pas que ce soit intéressant :)

Le premiers de cette renaissance sera sur les caches simples en Java.

Je parle ici de cache non révocable (j'en ai eu besoin très récemment). Voici la classe permettant de faire ceci :

Vous l'aurez compris, cet article cherche a vous présenter la méthode ConcurrentHashMap.computeIfAbsent, cette méthode est apparue en Java 8.

Cette dernière permet donc de remplir notre Map si la clé est absente, et ce de manière Thread safe!

J'espère pouvoir publier le prochain article dans moins d'un an, à bientôt donc :)

Commentaires

Code Story : la sélection finale

25 février 2013

Tags : CodeStory, Devoxx, Google, Java

Si vous me suivez sur twitter, vous savez sans doute déjà que Jeudi dernier (21/02) avait lieu la finale de Code Story. Je vais tenter de vous raconter comment s'est passée cette soirée haute en stress

Le lieu

Cette soirée s'est passée chez google à Paris. En tant que fanboy Google, ça suffisait déjà à faire de cette soirée une super soirée :)

Pour ceux qui ne connaissent pas les bureaux de Google, je peux vous dire un truc, c'est que ça fait envie. Pour vous faire baver, voici simplement la photo du "coin" fumeurs :



Le déroulement

Tout est expliqué sur le blog de Code Story, voici cependant un petit résumé. On avait des données avec pour chaque participant à Code Story, sa ville, trois trucs qu'il aime et deux trucs qu'il n'aime pas. Avec ça on devait construire un site internet from scratch permettant aux participants de connaître ceux avec qui ils ont des atomes crochus.


Notre participation

Les participants étaient par binômes pour ce concours. J'étais pour ma part avec Alexandre Ardhuin (@a14n).

Nous sommes arrivés les mains dans les poches sans rien avoir préparé (ce qui nous a sans doute fait défaut). La première étape a donc été de monter une stack Web et de publier ça pour que ce soit accessible sur Internet. Cette étape nous a pris à peu près 1 heure, à la moitié du temps nous avions donc un magnifique site web accessible sur Internet qui affichait "coucou"...

Pour afficher autre chose que "coucou", nous avons utilisé le framework Angular.js ce qui nous a permis d'afficher rapidement la liste des participants, le tout codé à l'arrache dans un seul fichier index.html

Alexandre est ensuite parti sur une présentation géographique des participants via Google Map, mais il n'a pas pu aller au bout à cause des quotas sur le geo-codage, dommage...

Je suis pour ma part parti sur le refactoring du code pour séparer Javascript, templates HTML et index.html afin de pouvoir mettre en place les routes Angular.js. Tout cela afin d'obtenir un lien par participant affichant les détails de celui-ci. Toutefois, je suis resté bloqué sur de la syntaxe Javascript pendant au moins 10 minutes :


angular.module('CodeStory');
différent de :

angular.module('CodeStory', []);
Dans le premiers cas, rien ne s'affiche, et l'erreur dans le console est : "Error: No module: CodeStory", en bref, un compilateur c'est pas si mal!

Ce refactoring m'a pris beaucoup de temps du coup (environ 20 minutes), et par conséquent il nous restait 25 minutes à peine pour faire la page de détail d'un participant. Nous avons mis environ 15 minutes à mettre en place une page affichant les personnes de la même ville, puis 7 minutes pour afficher les personnes ayant un "Like" en commun. Le "git push" permettant de déployer la dernière fonctionnalité a été fait sur le gong (heureusement qu'on avait un déploiement simplifié!).

Le code final est dispo sur github.

Le résultat

Une fois l'épreuve terminée, chaque binôme a eu 3 minutes pour présenter l'application aux jury. Nous sommes passés en premier, ce qui a permis de faire rapidement retomber le stress...

Au final c'est Xavier Hanin (@Xavierhanin) et Christophe Labouisse (@XtlCnslt) qui ont gagné, et nous avons été second. Un grand bravo à eux, et je leur donne rendez-vous à Devoxx France pour les voir en action :)

Nos erreurs

Notre première erreur a été de venir les mains dans les poches, il était prévisible que le Jury allait nous demander de coder une appli Web, si nous étions venus avec une stack Web prête à déployer, nous aurions gagné une heure...

N'ayant pas de stack web prête à déployer nous aurions dû partir sur un site directement hébergé sur github, n'ayant jamais utilisé le backend, cela aurait fait l'affaire, et nous aurions encore une fois gagné une heure...

Pour finir, voici :

Pour les plus curieux le diff est dispo sur github



Si vous avez eu le courage de lire jusque là, vous aurez peut-être le courage de regarder la vidéo!

Commentaires

Code Story en java : Jajascript et les performances

05 février 2013

Tags : CodeStory, Jajascript, Java

Cet article est le dernier d'une série de trois articles sur ma participation à Code Story en Java. Les articles précédents sont :

Je vais essayer de vous faire un retour sur l'étape qui a donné le plus de difficultés à tous les participants : Jajascript.

L'énoncé

Voici l'énoncé tel que nous l'avons reçu :

Location d’astronef sur Jajascript

Votre cousin par alliance, Martin O. sur la planète Jajascript vient de monter sa petite entreprise de vol spatial privé: Jajascript Flight Rental. Il loue aux grosses corporations son astronef lorsqu’elles ont de fortes charges ou un pépin avec leurs propres appareils. Il s’occupe de la maintenance et de l’entretien de son petit astronef. Il ne pouvait s’en payer qu’un pour démarrer.

Ces grosses corporations envoient des commandes de location qui consistent en un intervalle de temps, et le prix qu’ils sont prêts à payer pour louer l’astronef durant cet intervalle.

Les commandes de tous les clients sont connues plusieurs jours à l’avance. Ce qui permet de faire un planning pour une journée. Les commandes viennent de plusieurs sociétés différentes et parfois elles se chevauchent. On ne peut donc pas toutes les honorer.

Idéalement, il faut donc être capable de prendre les plus rentables, histoire de maximiser les gains de sa petite entreprise, et de s’acheter d’autres astronefs. Votre cousin passe des heures à trouver le planning idéal et vous demande pour un planning donné de calculer une solution qui maximise son gain.

Exemple

Considérez par exemple le cas où la JajaScript Flight Rental à 4 commandes :

MONAD42 : heure de départ 0, durée 5, prix 10
META18 : heure de départ 3, durée 7, prix 14
LEGACY01 : heure de départ 5, durée 9, prix 8
YAGNI17 : heure de départ 5, durée 9, prix 7

La solution optimale consiste à accepter MONAD42 et LEGACY01, et le revenu est de 10 + 8 = 18. Remarquez qu’une solution à partir de MONAD42 et YAGNI17 est faisable (l’avion serait loué sans interruption de 0 à 14) mais non optimale car le bénéfice ne serait que de 17.

Précisions

L’identifiant d’un vol ne dépasse jamais 50 caractères, les heures de départs, durée et prix sont des entiers positifs raisonnablement grands.

Serveur

Votre serveur doit répondre aux requêtes http POST de la forme http://serveur/jajascript/optimize avec un payload de la forme :

[
{ "VOL": "NOM_VOL", "DEPART": HEURE, "DUREE": DUREE, "PRIX": PRIX }, ...
]

En reprenant l’exemple ci dessus :

[
{ "VOL": "MONAD42", "DEPART": 0, "DUREE": 5, "PRIX": 10 },
{ "VOL": "META18", "DEPART": 3, "DUREE": 7, "PRIX": 14 },
{ "VOL": "LEGACY01", "DEPART": 5, "DUREE": 9, "PRIX": 8 },
{ "VOL": "YAGNI17", "DEPART": 5, "DUREE": 9, "PRIX": 7 }
]

Vous devrez répondre le résultat suivant :

{
"gain" : 18,
"path" : ["MONAD42","LEGACY01"]
}

Le gain représentant la somme optimale, path représentant l’ordre des vols.

Bons calculs !


Premier algo très naïf

Le premier algo que j'ai pondu était très naïf, je calculais absolument toutes les solutions possibles (voir plusieurs fois chaque solution). Voici la méthode principale de l'époque :


private void calculate(Planning actualPlanning, Collection<Commande> commandesToAdd) {
if (actualPlanning != null) {
addToPlanningsIfBetter(actualPlanning);
}
for (Commande commandeToAdd : commandesToAdd) {
if (actualPlanning == null || actualPlanning.canAddCommande(commandeToAdd)) {
Planning newPlanning = new Planning(actualPlanning);
newPlanning.addCommande(commandeToAdd);
Collection <Commande> newCommandesToAdd =
Collections2.filter(commandesToAdd, new FilterCommande(commandeToAdd));
calculate(newPlanning, newCommandesToAdd);
}
}
}

Dans cet algo récursif, je parcours toutes les commandes, et pour chaque commande, je regarde si elle est compatible à mon planning actuel, si elle l'est je l'ajoute au planning et je fais l'appel récursif. Cet algo est donc ni élégant ni performant, mais a le mérite de marcher. Pour les étapes précédentes cela suffisait, je me suis donc arrêté là, enfin jusqu'à la surprise.


La surprise

Pour Jajascript, ils nous ont fait une petite surprise, ils se sont mis à tester les performances... La méthode pour tester les performances était la suivante : le robot envoie une requête avec un certain nombre de commandes, si je réponds en moins de 30 secondes, il en envoie plus, et ce jusqu'à ce que je réponde en plus de 30 secondes. Voici la liste complète des marches utilisées par le robot :

  • 5 commandes
  • 10 commandes
  • 15 commandes
  • 20 commandes
  • 25 commandes
  • 30 commandes
  • 35 commandes
  • 40 commandes
  • 45 commandes
  • 50 commandes
  • 55 commandes
  • 60 commandes
  • 65 commandes
  • 70 commandes
  • 75 commandes
  • 80 commandes
  • 85 commandes
  • 90 commandes
  • 95 commandes
  • 100 commandes
  • 150 commandes
  • 250 commandes
  • 500 commandes
  • 1000 commandes
  • 1500 commandes
  • 2000 commandes
  • 2500 commandes
  • 3000 commandes
  • 3500 commandes
  • 4000 commandes
  • 5000 commandes
  • 10000 commandes
  • 50000 commandes

Du coup mon premier algo naïf répondait en plus de 30 secondes à partir de 30 commandes (nous ne connaissions pas le max à l'époque qui semblait être aux alentours de 100 commandes). Il a donc fallu optimiser tout ça. Je ne vais pas vous présenter tous les commits d'optimisation par lesquels je suis passés (ça représente près de 50 commits...). Je vais plutôt tenter de vous montrer les grandes étapes ainsi que les performances associées. Pour les performances, je n'ai inclus que le calcul brut (sans la couche HTTP ou Json).


Algo récursif optimisé

Après quelques commits, je suis arrivé à un algo récursif un peu plus optimisé :


private void calculate(Planning actualPlanning, Collection<Commande> commandesToAdd) {
if (actualPlanning != null) {
addToPlanningsIfBetter(actualPlanning);
}
Iterator <Commande> itCommande = commandesToAdd.iterator();

while (itCommande.hasNext()) {
Commande commandeToAdd = itCommande.next();
itCommande.remove();
if (actualPlanning == null || actualPlanning.canAddCommande(commandeToAdd)) {
calculate(new Planning(actualPlanning).addCommande(commandeToAdd), newArrayList(commandesToAdd));
}
}
}

Les grosses différences pour en arriver là sont (pas toutes visibles dans le code ci-dessus) :

  • Tri des commandes par heure de départ croissante
  • On ne stocke que le meilleur planning
  • On enlève la commande courante avant de continuer dans la récursivité
  • On stocke l'heure de fin d'un planning afin de savoir rapidement si une commande est compatible avec

Pour les plus curieux, le diff complet est dispo sur github. Cet algo tiens jusqu'à 70 commandes à peu près (c'est déjà beaucoup mieux, mais on est loin des 50000 commandes...).


Algo récursif très optimisé

Afin d'aller au bout des optimisations de l'algo récursif, j'ai modifié/enlevé tout ce qui prenait du temps sans changer l'algo. L'optimisation principale dans cette étape a été le passage en types primitifs avec utilisation de tableau d'entier. Quand on regarde le code obtenu, on a un peu l'impression de revoir du C... Toutes ces optimisations permettent tout de même d'atteindre les 5000 commandes environs. Pour les plus curieux, le diff complet est disponible sur github.


Algo itératif

Atteignant les limites du récursif, il a fallu passer à un algo itératif. Cet algo repose sur une pile des dernières solutions trouvées, afin de regarder dans ces solutions laquelle est la meilleure pour une commande donnée. Voici la méthode principale :


private void calculateIteratif() {
// Parcours de toutes les commandes
for (int i=0; i<nbCommands;i++) {
Solution bestSolutionToAdd = null;
int bestPrice = -1;
for (Solution solution : lastSolutions) {
if (starts[i] >= solution.heureFin
&& solution.prix > bestPrice ) {
bestSolutionToAdd = solution;
bestPrice = solution.prix;
}
}

lastSolutions.removeFirst();

boolean[] newAceptedCommands = Arrays.copyOf(bestSolutionToAdd.acceptedCommands, bestSolutionToAdd.acceptedCommands.length);
newAceptedCommands[i] = true;
Solution newSolution = new Solution(ends[i], bestSolutionToAdd.prix + prices[i], newAceptedCommands);
lastSolutions.addLast(newSolution);
}

for (Solution solution : lastSolutions) {
addToPlanningsIfBetter(solution.acceptedCommands, solution.prix);

}
}

Ce passage en itératif permet d'atteindre 200000 commandes environ (ce qui était suffisant pour le concours). Pour les plus curieux, le diff complet est disponible sur github


Algo itératif optimisé

Je vais maintenant vous présenter le code final obtenu après encore quelques optimisations, et finalement un retour à de l'objet pur (ce qui me fait perdre un peu en performance, mais rend le code tellement plus lisible). Avant d'arriver à ce résultat, je suis passé par un BitSet plutôt qu'un tableau de booléens, ce qui m'avait fait gagner pas mal (classe à connaître donc). Si vous voulez voir tout les commits intermédiaires, ça se passe sur github.

Voici tout d'abord la classe Solution permettant de stocker une solution trouvée :


import com.google.common.base.Optional;
import com.google.common.primitives.Ints;

import java.util.LinkedList;
import java.util.List;

public class Solution implements Comparable<Solution> {
public final int price;
public final int endTime;
public final Optional <Solution> oldSolution;
public final Flight newFlight;

Solution(int price, Optional <Solution> oldSolution, Flight newFlight) {
this.price = price;
this.oldSolution = oldSolution;
this.newFlight = newFlight;
this.endTime = newFlight.endTime;
}

public List <Flight> getFlights() {

LinkedList <Flight> flights = new LinkedList <Flight>();
flights.add(newFlight);

Optional <Solution> currentSolution = oldSolution;
while (currentSolution.isPresent()) {
flights.addFirst(currentSolution.get().newFlight);
currentSolution = currentSolution.get().oldSolution;
}

return flights;
}

public boolean isBetterThan(Optional <Solution> bestSolutionToAdd) {
return !bestSolutionToAdd.isPresent() || price > bestSolutionToAdd.get().price;
}

@Override
public int compareTo(Solution o) {
return Ints.compare(price, o.price);
}
}
Le point principal à noter dans cette classe, est la façon de stocker une solution. Plutôt que de stocker une image complète d'une solution, on stocke la solution précédente plus la commande qu'on lui a ajoutée. Cette technique permet d'économiser énormément de mémoire, mais permet également de diminuer énormément le coup de la création d'une solution (appel au constructeur).

Voici maintenant la classe JajascriptService contenant tout l'algo :


import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.collect.Lists;

import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

public class JajascriptService {

/**
* Flights to optimize.
*/
private List<Flight> flights;
/**
* Last solutions found.
*/
private LinkedList <Solution> lastSolutions = new LinkedList <Solution>();


public JajascriptService(List <Flight> flights) {
this.flights = flights;
Collections.sort(this.flights);
}

public JajaScriptResponse calculate() {

Solution solution = calculateSolution();

// Construct the path with the array of accepted flights.
List<String> path = Lists.transform(solution.getFlights(), new Function<Flight, String>() {
@Override
public String apply(Flight input) {
return input.getName();
}
});

return new JajaScriptResponse(solution.price, path);
}

private static class BestSolutions {
Optional <Solution> bestCompatibleSolution = Optional.absent();
Optional <Solution> bestSolutionWithEquivalentEndTime = Optional.absent();

int getPriceOfCompatibleSolution() {
return getPriceOfAnOptionalSolution(bestCompatibleSolution);
}

int getPriceOfEquivalentEndTimeSolution() {
return getPriceOfAnOptionalSolution(bestSolutionWithEquivalentEndTime);
}

private static int getPriceOfAnOptionalSolution(Optional <Solution> optionalSolution) {
return optionalSolution.isPresent() ? optionalSolution.get().price : 0;
}
}

/**
* @return an optimal solution.
*/
private Solution calculateSolution() {
// Iterate on all flights.
for (Flight flight : flights) {
// Pre-calculate endTime for future needs.
flight.calculateEndTime();

BestSolutions bestSolutions = getBestSolutionsForAFlight(flight);

int newPrice = flight.price + bestSolutions.getPriceOfCompatibleSolution();

if (newPrice > bestSolutions.getPriceOfEquivalentEndTimeSolution()) {
// Add the new solution to FIFO only if it's better than other solution with lower or equal endTime.
lastSolutions.addLast(new Solution(newPrice, bestSolutions.bestCompatibleSolution, flight));
}

if (bestSolutions.bestCompatibleSolution.isPresent()) {
// If we found a compatible solution, we remove all solution with endTime lower than last flight startTime and lower price.
removeOldSolutions(flight.startTime, bestSolutions.bestCompatibleSolution.get().price);
}
}

// Search the best solution in FIFO.
Collections.sort(lastSolutions);
return lastSolutions.getLast();
}

/**
* Remove all solution with lower or equal endTime than lastStartTime and lower price than priceOfCompatibleSolution.
*/
private void removeOldSolutions(int lastStartTime, int priceOfCompatibleSolution) {
Iterator <Solution> lastSolutionIterator= lastSolutions.iterator();

while (lastSolutionIterator.hasNext()) {
Solution oldSolution = lastSolutionIterator.next();
if (oldSolution.endTime <= lastStartTime
&& oldSolution.price < priceOfCompatibleSolution) {
lastSolutionIterator.remove();
}
}
}

/**
* Get the best solution in {@link JajascriptService#lastSolutions} for a flight.
*/
private BestSolutions getBestSolutionsForAFlight(Flight flight) {
BestSolutions bestSolutions = new BestSolutions();
// Search the best solution in FIFO we can take for this flight.
for (Solution solution : lastSolutions) {
if (flight.startTime >= solution.endTime && solution.isBetterThan(bestSolutions.bestCompatibleSolution)) {
bestSolutions.bestCompatibleSolution = Optional.of(solution);
}
if (flight.endTime >= solution.endTime && solution.isBetterThan(bestSolutions.bestSolutionWithEquivalentEndTime)) {
bestSolutions.bestSolutionWithEquivalentEndTime = Optional.of(solution);
}
}
return bestSolutions;
}
}
Cette version étant plutôt bien commentée, je vous laisse lire les commentaires...

Au final, cette version permet de traiter 1 000 000 de commandes en 200 millisecondes.


Différences entre les algos

En guise de récapitulatif, voici un petit graphique avec les performances de tous les algos




J'espère que ce retour sur ma participation à ce concours passionnant vous a plu. J'espère avoir le temps de vous faire un petit retour sur ma participation en Ceylon et en Scala, mais je ne vous garantis rien...

Commentaires

CodeStory en java : Scalaskel et la calculette

04 février 2013

Tags : CodeStory, Groovy, Java, Scalaskel, TDD

Cet article fait suite à l'article Ma participation à CodeStory en java : Intro.
Le but de cet article va être de vous parler de deux étapes du concours CodeStory : Scalaskel et la Calculette en vous présentant mon implémentation en Java.


Scalaskel

Voici l'énoncé tel que nous l'avons reçu par POST :

L’échoppe de monade sur Scalaskel.

Sur la planète Scalaskel, une planète en marge de la galaxie, aux confins de l’univers, la monnaie se compte en cents, comme chez nous. 100 cents font un groDessimal. Le groDessimal est la monnaie standard utilisable partout sur toutes les planètes de l’univers connu. C’est un peu compliqué à manipuler, mais si on ne s’en sert pas y’a toujours des erreurs d’arrondis incroyables quand on les soustrait ou on les divise, c’est idiot, mais c’est comme ça. Sur Scalaskel, on utilise rarement des groDessimaux, on utilise des pièces plus petites : Le Foo vaut 1 cent, le Bar vaut 7 cents, le Qix vaut 11 cents et le Baz vaut 21 cents.

Vous tenez une échoppe de monade et autres variables méta-syntaxiques sur Scalaskel. Pour faire face à l’afflux de touristes étrangers avec les poches remplies de groDessimaux vous avez besoin d’écrire un programme qui pour toute somme de 1 à 100 cents, vous donnera toutes les décompositions possibles en pièces de Foo, Bar, Qix ou Baz.

Par exemple, 1 cent ne peut se décomposer qu’en une seule pièce Foo. Par contre 7 cents peuvent se décomposer soit en 7 pièces Foo, soit en 1 pièce Bar.

Serveur Web :

Votre serveur doit répondre aux requêtes http GET de la forme http://serveur/scalaskel/change/X, X étant une valeur en cents de 1 à 100 cents.

La réponse attendue est un json de la forme :

[{“foo”: w, “bar”: x, “qix”: y, “baz”: z}, …]

Exemples Pour http://serveur/scalaskel/change/1 il faut répondre :

[ {“foo”: 1} ]

Pour http://serveur/scalaskel/change/7 il faut répondre :

[ {“foo”: 7}, {“bar”: 1} ]

L’ordre des valeurs dans le tableau json, ainsi que le formatage n’a pas d’importance à partir du moment où c’est du json valide, il s’entend.

Bon courage !

Première chose à mettre en place, le test unitaire (et oui, même si on est pressé, le TDD c'est bien) :


import com.meterware.httpunit.WebConversation;
import com.meterware.httpunit.WebResponse;

import static org.junit.Assert.assertEquals;

public class ScalaskelTest extends WebServerTestUtil {

@Test
public void should_answer_to_1cent() throws Exception {
WebConversation wc = new WebConversation();
WebResponse response = wc.getResponse(getURL() + "/scalaskel/change/1");
assertEquals(200, response.getResponseCode());
assertEquals("[{\"foo\":1}]", response.getText());
}

@Test
public void should_answer_to_7cent() throws Exception {
WebConversation wc = new WebConversation();
WebResponse response = wc.getResponse(getURL() + "/scalaskel/change/7");
assertEquals(200, response.getResponseCode());
assertEquals("[{\"foo\":7},{\"bar\":1}]", response.getText());
}

@Test
public void should_answer_to_11cent() throws Exception {
WebConversation wc = new WebConversation();
WebResponse response = wc.getResponse(getURL() + "/scalaskel/change/11");
assertEquals(200, response.getResponseCode());
assertEquals("[{\"foo\":11},{\"foo\":4,\"bar\":1},{\"qix\":1}]", response.getText());
}

@Test
public void should_answer_to_21cent() throws Exception {
WebConversation wc = new WebConversation();
WebResponse response = wc.getResponse(getURL() + "/scalaskel/change/21");
assertEquals(200, response.getResponseCode());
assertEquals("[{\"foo\":21},{\"foo\":14,\"bar\":1},{\"foo\":10,\"qix\":1},{\"foo\":7,\"bar\":2},{\"foo\":3,\"bar\":1,\"qix\":1},{\"bar\":3},{\"baz\":1}]", response.getText());
}
}
Rien d'extraordinaire dans ce test, il contient seulement les cas basiques (en même temps, le calcul à la main est assez chiant comme ça).

Si vous avez lu l'article précédent, vous savez que mon code en l'état ne gère pas les requêtes par path du type "/scalaskel/change/1", commençons donc par là. Dans la même idée que le "QueryHandler" présenté dans l'article précendent, j'ai d'abord créé un "PathHandler" :


import fr.ybonnel.codestory.WebServerResponse;
import javax.servlet.http.HttpServletRequest;

public abstract class AbstractPathHandler {
public abstract WebServerResponse getResponse(HttpServletRequest request, String payLoad, String... params) throws Exception;
}
La "request" sert à récupérer deux choses : la méthode (GET/POST) et le path. Le payload est utile pour les énoncés et jajascript (données envoyées en POST). Et pour finir les params sont le résultat de l'application d'un pattern que nous verrons ensuite.

Tout comme pour les Query, j'ai mis en place un enum relativement proche, mis à part qu'il n'a pas de méthodes abstraites. Cet enum gère de manière générique le routage avec la méthode et un pattern. Voici le code complet de cet enum :


import fr.ybonnel.codestory.WebServerResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public enum PathType {

INSERT_ENONCE(new InsertEnonceHandler(), "/enonce/(\\d+)", "POST"),
GET_ENONCES(new GetEnoncesHandler(), "/enonce(/)*", "GET"),
SCALASKEL_CHANGES(new ChangeScalaskelHandler(), "/scalaskel/change/(\\d+)", "GET"),
JAJASCRIPT(new OptimizeJajascriptHandler(), "/jajascript/optimize.*", "POST");

private AbstractPathHandler handler;

private Pattern pathPattern;
private String method;

PathType(AbstractPathHandler handler, String pathPattern, String method) {
this.handler = handler;
this.pathPattern = Pattern.compile(pathPattern);
this.method = method;
}


public Matcher isThisPath(String method, String path) {
if (this.method.equals(method)) {
return pathPattern.matcher(path);
} else {
return null;
}
}

public static WebServerResponse getResponse(HttpServletRequest request, String payLoad) throws Exception {
for (PathType onePath : values()) {
Matcher isThisPath = onePath.isThisPath(request.getMethod(), request.getPathInfo());
if (isThisPath != null && isThisPath.matches()) {
return onePath.handler.getResponse(request, payLoad, extractParameters(isThisPath));
}
}
return new WebServerResponse(HttpServletResponse.SC_NOT_FOUND, "This path is unknown");
}

private static String[] extractParameters(Matcher thisPath) {
String[] params = new String[thisPath.groupCount()];
for (int groupIndex = 1; groupIndex <= thisPath.groupCount(); groupIndex++) {
params[groupIndex - 1] = thisPath.group(groupIndex);
}
return params;
}
}

Maintenant que nous avons vu la tuyauterie, passons à l'algo de Scalaskel en lui-même. Au niveau modèle, j'ai deux classes, une représentant une solution (Change), et un enum représentant les pièces :


import com.fasterxml.jackson.annotation.JsonProperty;

public class Change {
@JsonProperty
private Integer foo;
@JsonProperty
private Integer bar;
@JsonProperty
private Integer qix;
@JsonProperty
private Integer baz;

public Change(Change change) {
if (change != null) {
foo = change.foo;
bar = change.bar;
qix = change.qix;
baz = change.baz;
}
}

public void pay(Coin coin) {
switch (coin) {
case FOO:
foo = foo == null ? 1 : foo + 1;
break;
case BAR:
bar = bar == null ? 1 : bar + 1;
break;
case QIX:
qix = qix == null ? 1 : qix + 1;
break;
case BAZ:
baz = baz == null ? 1 : baz + 1;
break;
}
}
}

import java.util.List;

import static com.google.common.collect.Lists.newArrayList;

public enum Coin {
FOO(1),
BAR(7),
QIX(11),
BAZ(21);

private int value;

Coin(int value) {
this.value = value;
}

public int getValue() {
return value;
}

public boolean canPay(int cents) {
return cents >= value;
}

private static class ListHolder {
private static final List<Coin> valuesAsLists = newArrayList(values());
}

public static List<Coin> valuesAsLists() {
return ListHolder.valuesAsLists;
}
}
La classe Change contient des annotations pour la sérialisation Json avec Jackson, elle contient également un constructeur par copie servant dans l'algo, ainsi qu'une méthode pay, permettant d'ajouter une pièce à une solution. Quand à la classe Coin, elle contient une méthode "canPay" permettant de savoir si on peut ajouter la pièce pour un nombre de cents restant. Les listes sont là pour pouvoir utiliser la méthode Collections2.filter de Guava.

Et pour finir, voici la classe principale : le service permettant de faire le calcul.


import com.google.common.base.Predicate;
import com.google.common.collect.Collections2;
import com.google.common.collect.Lists;

import java.util.List;

public enum ScalaskelChangeService {
INSTANCE;

public static ScalaskelChangeService getInstance() {
return INSTANCE;
}

public List<Change> calculateChanges(int cents) {
return completeChanges(cents, null, null);
}

private List<Change> completeChanges(int cents, Change currentChange, Coin lastCoin) {
// Stop condition of recursivity
if (cents == 0) {
return Lists.newArrayList(currentChange);
}
List<Change> changes = Lists.newArrayList();
for (Coin coin : Collections2.filter(
Coin.valuesAsLists(),
new FilterCoins(lastCoin, cents))) {
Change change = new Change(currentChange);
change.pay(coin);
changes.addAll(completeChanges(cents - coin.getValue(), change, coin));
}
return changes;
}

/**
* Filter coins with this rule :
* coin is keeped only if :
* <ul>
* <li>its value is bigger or equals thant lastCoin</li>
* <li>we can pay with the coin.</li>
* </ul>
*/
private static class FilterCoins implements Predicate {

private int minValue;
private int centsToPay;

private FilterCoins(Coin lastCoin, int centsToPay) {
minValue = lastCoin == null ? 0 : lastCoin.getValue();
this.centsToPay = centsToPay;
}

@Override
public boolean apply(Coin input) {
return minValue <= input.getValue() && input.canPay(centsToPay);
}
}
}
Cette classe est un enum pour le côté singleton. La classe FilterCoins permet de filtrer les pièces utilisables en fonction de la dernière pièce utilisée ainsi que le nombre de cents restant à payer. Pour l'algo, il s'agit d'un algo récursif relativement bourrin. Il est sûr que si les performances avaient été un critère, j'aurais sûrement modifié l'algo. J'espère que mon code est suffisamment clair pour ne pas avoir à l'expliquer d'avantage.

La dernière partie que nous n'avons pas vu est le "PathHandler" permettant de lier le service au WebServer :


import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import fr.ybonnel.codestory.WebServerResponse;
import fr.ybonnel.codestory.path.scalaskel.Change;
import fr.ybonnel.codestory.path.scalaskel.ScalaskelChangeService;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.List;

public class ChangeScalaskelHandler extends AbstractPathHandler {

private ObjectMapper objectMapper = new ObjectMapper().setSerializationInclusion(JsonInclude.Include.NON_NULL);

private boolean wrongParams(int centsToPay) {
return centsToPay <= 0 || centsToPay > 100;
}

@Override
public WebServerResponse getResponse(HttpServletRequest request, String payLoad, String... params) throws JsonProcessingException {
int centsToPay = Integer.parseInt(params[0]);
if (wrongParams(centsToPay)) {
return new WebServerResponse(HttpServletResponse.SC_FORBIDDEN, "Wrong parameters");
}

List<Change> changes = ScalaskelChangeService.getInstance().calculateChanges(centsToPay);
return new WebServerResponse(HttpServletResponse.SC_OK, objectMapper.writeValueAsString(changes));
}
}
Dans cette classe, vous pouvez voir une petite protection afin d'éviter de faire tomber le serveur en demandant le change de 1.000.000 de cents. On peut également y voir la sérialisation Json avec Jackson, mais rien de sorcier non plus.


La calculette

Pour cet exercice, pas d'énoncé, la première requête reçue fut : "/?q=1+1" à laquelle on se doute qu'il faut répondre "2". Je ne vais pas vous détailler toutes les étapes par lesquelles je suis passé pour arriver au bout. Voici la liste des requêtes reçues (dans l'ordre) :

  • 1+1
  • 2+2
  • 3+3
  • 4+4
  • 5+5
  • 6+6
  • 7+7
  • 8+8
  • 9+9
  • 1*1
  • 2*2
  • 3*3
  • 4*4
  • 5*5
  • 6*6
  • 7*7
  • 8*8
  • 9*9
  • 1+2*2
  • (1+2)*2
  • (1+2+3+4+5+6+7+8+9+10)*2
  • (1+2)/2
  • ((1+2)+3+4+(5+6+7)+(8+9+10)*3)/2*5
  • 1,5*4
  • ((1,1+2)+3,14+4+(5+6+7)+(8+9+10)*4267387833344334647677634)/2*553344300034334349999000
  • ((1,1+2)+3,14+4+(5+6+7)+(8+9+10)*4267387833344334647677634)/2*553344300034334349999000/31878018903828899277492024491376690701584023926880
  • (-1)+(1)
  • 1,0000000000000000000000000000000000000000000000001*1,0000000000000000000000000000000000000000000000001
Cette liste vous permet sans doute de voir par quelles étapes je suis passé :
  • Gestion des sommes
  • Gestion des multiplication
  • Gestion des priorités
  • Gestion des parenthèses
  • Gestion des divisions
  • Gestion des décimaux
  • Gestion des grands nombres
  • Gestion des nombres négatifs
  • Gestion des nombres de décimales élevés
Pour la première version de mon code, j'ai tout fait à la main à grand coup de Pattern, voici le résultat final :

import com.google.common.base.Throwables;
import fr.ybonnel.codestory.query.calculate.Operator;
import fr.ybonnel.codestory.query.calculate.SearchParanthesis;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class CalculateQueryHandler extends AbstractQueryHandler {

private static final String NOMBRE = "\\-?\\d+\\.?\\d*";
private static final String PATTERN_DIVIDE = "(" + NOMBRE + ")/(" + NOMBRE + ")";
private static final String PATTERN_MULTIPLY = "(" + NOMBRE + ")\\*(" + NOMBRE + ")";
private static final String PATTERN_ADD = "(" + NOMBRE + ")\\+(" + NOMBRE + ")";

// operators list by priority.
private List operators = new ArrayList() {{
// operator divide.
add(new Operator(PATTERN_DIVIDE) {
@Override
public BigDecimal operate(BigDecimal a, BigDecimal b) {
try {
return a.divide(b);
} catch (ArithmeticException exception) {
return a.divide(b, 1000, RoundingMode.HALF_UP);
}
}
});
// Operator Multiply.
add(new Operator(PATTERN_MULTIPLY) {
@Override
public BigDecimal operate(BigDecimal a, BigDecimal b) {
return a.multiply(b);
}
});
// Operator Add.
add(new Operator(PATTERN_ADD) {
@Override
public BigDecimal operate(BigDecimal a, BigDecimal b) {
return a.add(b);
}
});
}};

private Pattern patternParenthesis = Pattern.compile("\\((.*)\\)");
private NumberFormat format = new DecimalFormat("#0.#", new DecimalFormatSymbols(Locale.FRANCE));

public CalculateQueryHandler() {
format.setMaximumFractionDigits(500);
}

@Override
public String getResponse(String query) {
String result = null;
try {
result = calculateWithParenthesis(query.replace(' ', '+').replace(',', '.'));
} catch (ParseException e) {
Throwables.propagate(e);
}

try {
BigDecimal retour = new BigDecimal(result);
return format.format(retour);
} catch (NumberFormatException numberFormatException) {
numberFormatException.printStackTrace();
return null;
}
}

private String calculateWithParenthesis(String calculateQuery) throws ParseException {
Matcher matcherParenthsis = patternParenthesis.matcher(calculateQuery);

while (matcherParenthsis.find()) {
SearchParanthesis searchParanthesis = new SearchParanthesis(calculateQuery).invoke();
int start = searchParanthesis.getStart();
int end = searchParanthesis.getEnd();

// Calculate the content of parenthesis.
String queryBetweenParenthesis = calculateQuery.substring(start + 1, end - 1);
String result = calculateWithoutParenthesis(queryBetweenParenthesis);

// Replace the parenthesis group with result.
calculateQuery = calculateQuery.substring(0, start) + result + calculateQuery.substring(end);
matcherParenthsis = patternParenthesis.matcher(calculateQuery);
}

calculateQuery = calculateWithoutParenthesis(calculateQuery);
return calculateQuery;
}

private String calculateWithoutParenthesis(String calculateQuery) throws ParseException {

for (Operator operator : operators) {
Matcher matcher = operator.matcher(calculateQuery);

while (matcher.find()) {
BigDecimal a = new BigDecimal(matcher.group(1));
BigDecimal b = new BigDecimal(matcher.group(2));
BigDecimal result = operator.operate(a, b);

// Replace sur operation in string by result.
calculateQuery = calculateQuery.substring(0, matcher.start()) + result.toString() + calculateQuery.substring(matcher.end());

matcher = operator.matcher(calculateQuery);
}
}

return calculateQuery;
}
}
Je vous l'accorde, le code est relativement complexe, mais je suis assez fier d'avoir pondu une calculette à la main.

Devant tout ce code, j'ai décidé de simplifier tout ça grâce à groovy, vous allez voir le code est grandement simplifié :


import groovy.lang.GroovyShell;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.NumberFormat;
import java.util.Locale;

public class CalculateQueryHandler extends AbstractQueryHandler {
private NumberFormat format = new DecimalFormat("#0.#", new DecimalFormatSymbols(Locale.FRANCE));
private GroovyShell shell = new GroovyShell();

public CalculateQueryHandler() {
format.setMaximumFractionDigits(500);
}

@Override
public String getResponse(String query) {
Object object = shell.evaluate("return " + query.replace(' ', '+').replace(',', '.'));
return formatGroovyReturn(object);
}

private String formatGroovyReturn(Object object) {
try {
return format.format(object);
} catch (NumberFormatException numberFormatException) {
numberFormatException.printStackTrace();
return null;
}
}
}
Et voilà, merci groovy, finalement pas besoin de réinventer la roue...




Cet article est le deuxième d'une série de trois articles dont le dernier est sans doute le plus intéressant : Jajascript et les performance. Afin de vous donner un avant-goût, vous pouvez regarder l'article sur le blog Code Story : Location d’astronef sur Jajascript.

Commentaires

Ma participation à CodeStory en java : Intro

03 février 2013

Tags : CodeStory, Java, TDD

La première étape du concours Code Story vient de se terminer, je vais donc tenter de faire un résumé de ma participation à ce concours. Pour les curieux, le classement est disponible ici.
Etant sans doute un peu malade, j'ai participé à ce concours 3 fois : en Java, en Ceylon et en Scala. Mon inscription principale avec le code le plus soigné et complet reste celle en Java. Je vais donc dans un premier temps vous faire un retour sur la partie Java. L'ensemble du code source de ma participation en java est disponible sur github
Les étapes de l'application étant nombreuses, je vais découper ça en plusieurs articles :

Principes du concours

Afin de comprendre les choix que j'ai faits au niveau du code, il est important de connaître les principes de ce concours. Au départ, on s'inscrit en donnant juste une URL publique (ex : http://serveur.mondomain.fr:8080). Le seul truc que l'on sait avant de commencer, c'est qu'on va recevoir une requête GET sur http://serveur.mondomain.fr:8080/?q=Quelle+est+ton+adresse+email à laquelle il faut répondre avec l'adresse email, pour les curieux, le règlement est disponible ici. On se doute également dès le départ qu'on recevra d'autres requêtes HTTP auxquelles il faudra répondre (d'où le besoin de logs).


Répondre à des questions fixes

Les premières requêtes n'étaient pas très compliquées, il s'agissait simplement de questions avec une réponse fixe attendue. Pour information, voici la liste des questions reçues :

  • ?q=Quelle est ton adresse email
  • ?q=Es tu abonne a la mailing list(OUI/NON)
  • ?q=Es tu heureux de participer(OUI/NON)
  • ?q=Es tu pret a recevoir une enonce au format markdown par http post(OUI/NON)
  • ?q=Est ce que tu reponds toujours oui(OUI/NON)
  • ?q=As tu bien recu le premier enonce(OUI/NON)
  • ?q=As tu bien recu le second enonce(OUI/NON)
  • ?q=As tu passe une bonne nuit malgre les bugs de l etape precedente(PAS_TOP/BOF/QUELS_BUGS)
  • ?q=As tu copie le code de ndeloof(OUI/NON/JE_SUIS_NICOLAS)
  • ?q=Souhaites-tu-participer-a-la-suite-de-Code-Story(OUI/NON)
Afin de pouvoir rapidement faire évoluer l'application pour ajouter la gestion de futures questions, voici comment j'ai développé.

Première étape le test JUnit (et oui j'ai fait du TDD), voici donc l'exemple du test pour la troisième question :

@Test
public void should_answer_to_participate() throws Exception {
WebConversation wc = new WebConversation();
WebResponse response = wc.getResponse(getURL() + "/?q=Es tu heureux de participer(OUI/NON)");
assertEquals(200, response.getResponseCode());
assertEquals("Response must be 'OUI'", "OUI", response.getText());
}
Le test est donc très simple, rapide à écrire, et se met à la place du robot pour effectuer le test (la librairie utilisée est HttpUnit).

Voyons maintenant comment j'ai codé le serveur. A travers les premières questions, on se rend compte qu'une partie des requêtes reçues va l'être avec le paramètre "q", j'ai donc décidé de créer la notion de QueryHandler chargée de répondre aux requêtes reçues avec ce paramètre. Voici la classe abstraite associée :

public abstract class AbstractQueryHandler {
public abstract String getResponse(String query);
}

La tuyauterie pour appeler le handler (et le bon) et relativement simple. Côté handler HTTP, c'est juste la récupération du paramètre et l'appel d'une méthode de la classe chargée d'appeler le bon QueryHandler :

String query = request.getParameter(QUERY_PARAMETER);
if (query != null) {
response = QueryType.getResponse(query);
}
La variable "response" contient le texte à renvoyer, mais également le status HTTP à utiliser. La classe "QueryType" est en fait un "enum", prenant en paramètre de constructeur le handler à utiliser, et ayant une méthode abstraite "isThisQueryType" prenant en paramètre la "query" et renvoyant un boolean permettant de savoir si c'est lui qui est responsable de cette "query". Et pour finir, cet enum contient une méthode statique chargée d'appeler le bon QueryHandler en fonction de la query. Voici la classe avec l'exemple de question vu plus haut :
import fr.ybonnel.codestory.WebServerResponse;
import javax.servlet.http.HttpServletResponse;

public enum QueryType {

PARTICIAPATE(new FixResponseQueryHandler("OUI")) {
@Override protected boolean isThisQueryType(String query) {
return query.equals("Es tu heureux de participer(OUI/NON)");
}
};

private AbstractQueryHandler queryHandler;

QueryType(AbstractQueryHandler queryHandler) {
this.queryHandler = queryHandler;
}

protected abstract boolean isThisQueryType(String query);

public static WebServerResponse getResponse(String query) {
for (QueryType oneQuestion : values()) {
if (oneQuestion.isThisQueryType(query)) {
return new WebServerResponse(HttpServletResponse.SC_OK, oneQuestion.queryHandler.getResponse(query));
}
}
return new WebServerResponse(HttpServletResponse.SC_NOT_FOUND, "Query " + query + " is unknown");
}
}
Vous pouvez donc voir dans cette enum l'apparition de la classe FixResponseQueryHandler, cette classe est très simple, son rôle est simplement de renvoyer une réponse fixe, la voici :
public class FixResponseQueryHandler extends AbstractQueryHandler {

private String response;
public FixResponseQueryHandler(String response) {
this.response = response;
}

@Override public String getResponse(String query) {
return response;
}
}

Voici donc tous les éléments que j'ai mis en place pour les questions avec réponses fixes. A posteriori, on aurait pu mettre en place une simple Map (c'est ce que j'ai fait en Ceylon et en Scala), mais c'est le problème de coder au fur et à mesure que les questions arrivent, on ne sait pas trop ce qui va arriver ensuite... J'aurais évidement pu refactorer tout ça (les tests unitaires me permettant d'être sûr de ne pas tout casser), mais j'ai eu la flemme, d'autant que ce n'est pas la partie la plus intéressante, les énoncés suivants étant bien plus sympas.


Les logs

Un élément important de ce concours, c'est que nous ne recevions aucune consigne par mail, twitter ou pigeon voyageur. Le seul mode d'interaction était l'envoi d'une requête HTTP par le robot qui renvoyait la requête tant qu'on ne répondait pas correctement. Afin de pouvoir être réactif aux nouvelles requêtes en toute circonstance, je me suis ajouté un système de logs en base de donnée avec possibilité de les récupérer par un appel http (ce qui me permettait de la faire même depuis mon téléphone). Je ne vais pas vous détailler comment j'ai mis en place ce système, mais plutôt comment j'ai mis en place la BDD tout en gardant un système simple et rapide à tester.

Mise en place de la BDD

Comme vous le savez, je n'ai pas de stack compliquée pour cette application, pour mettre en place une BDD simple, je suis parti sur un h2 embarqué. Première étape ajout de la dépendance au pom.xml :


<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.3.170</version>
</dependency>

Sur l'utilisation de celle-ci, rien d'extra-ordinaire, je suis parti sur du jdbc à la main. Ça peux paraître archaïque, mais pour gérer 2 pauvres tables (une pour les logs et une pour les énoncés), pas besoin de sortir l'artillerie lourde. Pour les curieux, voici la classe centrale pour gérer la base :


import com.google.common.base.Throwables;
import org.h2.jdbcx.JdbcDataSource;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;

public enum DatabaseManager {

INSTANCE;

public static final String TYPE_Q = "Q";
public static final String DB_DRIVER = "org.h2.Driver";
public static final String DB_USER = "sa";

private JdbcDataSource ds;

DatabaseManager() {
try {
Class.forName(DB_DRIVER);

boolean databaseExists = doesDatabaseExists();

ds = new JdbcDataSource();
ds.setURL(DatabaseUtil.getUrl());
ds.setUser(DB_USER);
ds.setPassword(DB_USER);

if (!databaseExists) {
createDatabase();
}
} catch (Exception exception) {
Throwables.propagate(exception);
}
}

private static boolean doesDatabaseExists() {
boolean databaseExists = false;
try {
String url = DatabaseUtil.getUrl() + ";IFEXISTS=TRUE";
Connection connection = DriverManager.getConnection(url, DB_USER, DB_USER);
connection.close();
databaseExists = true;
} catch (SQLException ignore) {
}
return databaseExists;
}

public void createDatabase() throws SQLException {
Connection conn = ds.getConnection();

Statement statementDrop = conn.createStatement();
statementDrop.executeUpdate("DROP TABLE IF EXISTS LOG");

Statement statement = conn.createStatement();
statement.executeUpdate("CREATE TABLE LOG (" +
"HEURE TIMESTAMP," +
"TYPE_LOG VARCHAR(10)," +
"MESSAGE VARCHAR(500))");

statementDrop = conn.createStatement();
statementDrop.executeUpdate("DROP TABLE IF EXISTS ENONCE");

statement = conn.createStatement();
statement.executeUpdate("CREATE TABLE ENONCE (" +
"ID INTEGER," +
"TITLE VARCHAR(100)," +
"ENONCE VARCHAR(4000))");

conn.close();
}

private LogDao logDao;

public LogDao getLogDao() {
if (logDao == null) {
logDao = new LogDao(ds);
}
return logDao;
}

private EnonceDao enonceDao;

public EnonceDao getEnonceDao() {
if (enonceDao == null) {
enonceDao = new EnonceDao(ds);
}
return enonceDao;
}
}

Le but était de vous montrer que pour faire des trucs simples, il n'est pas toujours utile de sortir l'artillerie lourde avec du JPA ou autre...

Et pour les tests unitaires?

Comme vous avez pu voir, les tests unitaires ne sont pas vraiment unitaire puisqu'ils testent l'application de bout en bout. Se pose du coup la question de la base de donnée qui ralentie les tests de manière inutile, heureusement h2 possède un mode mémoire qui rend la base très rapide et non persistante après les tests (ce qui permet de repartir à zéro à chaque fois). Pour passer d'un mode à l'autre, rien de sorcier :

public static String getUrl() {
if (test) {
return "jdbc:h2:mem:codestory;DB_CLOSE_DELAY=-1";
}
return "jdbc:h2:./codestory";
}
Il ne faut pas oublier le "DB_CLOSE_DELAY=-1" qui permet de garder la base soit active tant que la jvm existe (ça évite d'avoir des problèmes de structures qui disparaissent d'un test à l'autre).




À bientôt pour Scalaskel et la Calculette, puis pour le plus interessant : Jajascript et les performances.

Commentaires

Marre du cloud et du JEE -> vive l'auto-hébergement et les main.

08 janvier 2013

Tags : auto-hébergement, cloud, CodeStory, git, Java, JEE, Jetty, NoMock, simple

Récemment l'équipe CodeStory a lancé le concours pour la sélection 2013, informations ici.

Pour participer, il faut un serveur public qui répond à des requêtes HTTPs. Pour la première étape, il faut que le serveur réponde à la requête GET "http://foobar.com:9090/?q=Quelle+est+ton+adresse+email" avec votre adresse email.
Rien de bien compliqué (en tout cas pour le moment), mais il faut évidement que votre serveur puisse évoluer pour répondre aux prochaines questions.



Architecture technique

Pour répondre au besoin de CodeStory, il y a plusieurs solutions (en restant dans l'univers java) :

  • Du JEE (ou conteneur de servlet simple) hébergé chez cloudbees ou à la maison.
  • Du play hébergé chez heroku, cloudbees ou à la maison.
  • Du Google App Engine.
  • Ou beaucoup plus simple :)
Dans le cadre de CodeStory, j'ai décidé de partir sur le beaucoup plus simple (assez largement inspiré par une présentation que David Gageot avait faîte au BreizhJUG en 2011, disponible sur parleys). Je suis donc parti sur un jetty embarqué et démarré depuis un simple main.

Pour mettre en place cette "architecture", deux étapes très compliquées :

  • Le pom.xml
  • La classe main


Le pom.xml

Il faut juste ajouter la dépendance vers Jetty :


<dependency>
<groupId>org.mortbay.jetty</groupId>
<artifactId>jetty</artifactId>
<version>6.1.25</version>
</dependency>


La classe main

Le boulot de la classe main est simplement de démarrer le serveur et traiter les requêtes HTTP :


package fr.ybonnel.codestory;

import org.mortbay.jetty.Server;
import org.mortbay.jetty.handler.AbstractHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class WebServer extends AbstractHandler {

public static final String QUERY_PARAMETER = "q";

@Override
public void handle(String target,
HttpServletRequest request,
HttpServletResponse httpResponse,
int dispatch)
throws IOException, ServletException {
// Traitement de la requète.
}

public static void main(String[] args) throws Exception {
int port = 10080;
if (args.length == 1) {
port = Integer.parseInt(args[0]);
}
Server server = new Server(port);
server.setHandler(new WebServer());
server.start();
server.join();
}
}


Les intérêts

L'intérêt d'une telle "architecture" est la simplicité, ce qui se traduit par trois avantages :

  • Rapidité de démarrage : 38ms sur mon poste qui n'est pas un fourdre de guerre.
  • Tests unitaires sans mock : grâce à la rapidité de démarrage, on peux faire des tests qui démarrent le serveur, exécutent une requête GET, et arrêtent le serveur. On se place donc à la place du client, ce qui est sans doute une garantie d'avoir le résultat attendu.
  • Facilité d'installation : juste un jar à exécuter (donc très simple que ce soit dans l'IDE ou dans sur un serveur).



Tests unitaires

Comme on l'a vu, pour les tests unitaires, rien de bien sorcier :

  • On démarre le serveur (dans une méthode @Before, pour qu'elle soit exécutée avant chaque test)
  • On fait le test (envoi d'une requête GET, et vérifications sur la réponse).
  • On arrête le serveur (dans une méthode @After).

Code complet du test de la première étape :


package fr.ybonnel.codestory;

import com.google.api.client.http.*;
import com.google.api.client.http.javanet.NetHttpTransport;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mortbay.jetty.Server;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

import static junit.framework.Assert.assertEquals;

public class WebServerTest {

public static final int PORT = 18080;
private Server server;

@Before
public void setup() throws Exception {
WebServer.setTest(true);
server = new Server(PORT);
server.setHandler(new WebServer());
server.start();

new Thread(){
@Override
public void run() {
try {
server.join();
} catch (InterruptedException ignore) {
}
}
}.start();
}

@After
public void teardown() throws Exception {
server.stop();
}

@Test
public void should_answear_to_whatsyourmail() throws Exception {
String url = "http://localhost:" + PORT + "/?q=Quelle+est+ton+adresse+email";
HttpResponse response = sendGetRequest(url);
assertEquals("Status code must be 200", 200, response.getStatusCode());
assertEquals("Response must be my mail",
"ybonnel@gmail.com",
responseToString(response));
}

private static final HttpTransport HTTP_TRANSPORT = new NetHttpTransport();

private HttpResponse sendGetRequest(String url) throws IOException {
HttpRequestFactory requestFactory =
HTTP_TRANSPORT.createRequestFactory();
HttpRequest request = requestFactory.buildGetRequest(new GenericUrl(url));
return request.execute();
}

private String responseToString(HttpResponse response) throws IOException {
BufferedReader bufReader = new BufferedReader(new InputStreamReader(
response.getContent(), response.getContentCharset()));
StringBuilder builder = new StringBuilder();
String line = bufReader.readLine();
while (line != null) {
builder.append(line);
line = bufReader.readLine()
}
return builder.toString();
}
}
Pour les tests suivant, seule la méthode @Test est à réécrire.
Pour faciliter l'écriture des requêtes http, j'utilise la librairie google-http-client, mais si vous avez mieux, je suis preneur.
EDIT : j'utilise maintenant JWebUnit, beaucoup plus simple.



Déploiement

Je n'ai pas encore parlé d'hébergement, ce qui pour un serveur qui doit être accessible publiquement reste important.

Ayant un serveur dédié à disposition, je suis parti sur de l'auto-hébergement. Si vous me demandez "pourquoi", je vous répondrai "parce que"...

Afin de m'auto-héberger j'ai suivi trois étapes :

  • Assemblage du jar
  • Démarrage et arrêt
  • Déploiement simplifié


Assemblage du jar

Mon build est sous maven, créer un jar contenant les dépendances n'est donc pas très compliqué, il suffit d'ajouter la configuration qui va bien dans le pom.xml :


<build>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<archive>
<manifest>
<mainClass>fr.ybonnel.codestory.WebServer</mainClass>
</manifest>
</archive>
<finalName>${artifactId}</finalName>
<appendAssemblyId>false</appendAssemblyId>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
</plugin>
</plugins>
</build>


Scripts de démarrage et d'arrêt

Pour le démarrage et l'arrêt, je ne me suis pas cassé la tête :

  • Démarrage :
    java -jar code-story.jar >> serveur.log 2>&1 &
  • Arrêt :
    ps -ef | grep java | grep code-story | grep -v grep | while read a b c 
    do
    kill -9 $b
    done


Déploiement simplifié

Le dernier truc auquel je tenais est un déploiement simple, je suis passé par deux étapes avec des logiques très différentes :

  • Déploiement par update des sources
  • Déploiement par git push

Déploiement par update des sources

Ma première façon de déployer était relativement simple, j'ai fait un clone de mon repo git sur le serveur. Donc pour redéployer, je faisait simplement un "git pull", suivi d'une compilation et restart du serveur.
Mon script de déploiement ressemblait donc à :


git pull
mvn clean install assembly:single
Il suffisait ensuite de redémarrer le serveur pour prendre en compte le nouveau jar.

Quelques inconvénients cependant à cette technique :

  • Il faut se connecter au serveur pour le mettre à jour.
  • On mélange les sources et la partie serveur au même endroit

Déploiement par git push

J'ai eu envie que le déploiement se résume à un "git push serveur master" depuis mon poste de dev (fortement inspiré de la façon de déployer sur heroku).

Première étape, créer le repo git sur le serveur (depuis le serveur) :


mkdir CodeStory.git
cd CodeStory.git
git init --bare
Et voilà, j'ai un repo git accessible par ssh.

Deuxième étape, pousser le contenu actuel sur le repo (depuis mon poste de dev) :


git remote add serveur ssh://ybonnel@XXX.XXX.XXX.XXX:XXXX/home/ybonnel/CodeStory.git
git push serveur master

Troisième étape, créer la partie serveur (sur le serveur donc) :


mkdir CodeStory-server
cd CodeStory-server/
cp ../CodeStory/target/code-story.jar .
cp ../CodeStory/scripts/* .
Mon répertoire "CodeStory-server" contient donc :
  • Le jar
  • Le script de démarrage et le script d'arrêt

Quatrième et dernière étape, créer le hook sur le repo git. Pour ce faire, j'ai créé le script "post-receive" dans "CodeStory.git/hooks" dont voici le contenu :


echo "Updating server..."
rm -rf /home/ybonnel/CodeStory
git clone /home/ybonnel/CodeStory.git /home/ybonnel/CodeStory
cd /home/ybonnel/CodeStory
./updateServeur.sh
echo "Update and restart of server are done"
Et voici le contenu du script "updateServeur.sh" :

mvn clean install assembly:single
if [ $? -eq 0 ]
then
cp scripts/* ../CodeStory-server/
cp target/code-story.jar ../CodeStory-server/code-story.jar.new
cd ../CodeStory-server
./stopServeur.sh
mv code-story.jar code-story.jar.old
mv code-story.jar.new code-story.jar
./startServeur.sh
sleep 1
tail -10 serveur.log
fi
Un fois ce hook mis en place, lorsque je fait un "git push serveur master" depuis mon poste de dev, une compile maven se lance, et si le build maven est OK, le serveur est mis à jour. Et je vois le résultat de la compile et du déploiement en direct lors de mon git push.



Et dans la vrai vie?

Maintenant vous allez me dire, c'est bien sympa ton truc, mais dans la vrai vie, les projets sont un peu plus compliqués que simplement fournir un email en réponse à un GET...
Les architectures Web modernes sont souvent composées d'une partie serveur qui répond du JSON, et une partie cliente qui joue avec (y a qu'à voir le succès de angular.js). Et avec des architectures de ce type, répondre du JSON est-il beaucoup plus compliqué que répondre une adresse email?

Pour information, mon site ybo-tv est hébergé sur un tomcat, mais il serait relativement facile de le basculer sur une architecture de ce type (pas de stack lourde juste pour répondre du JSON, c'est facilement faisable en spécifique).

Commentaires

Comparaison des parseurs CSV

23 août 2012

Tags : Comparatif, CSV, CsvEngine, Java

Mon petit projet "CsvEgine" commençant à ressembler à quelque chose, je vais me lancer dans un petit comparatifs des parseurs que je peux trouver sur internet.

Pour pouvoir comparer ces différents parseurs je vais utiliser l'exemple basique de l'utilisation de CsvEngine : Basics.
Il s'agit donc de parser un fichier CSV contenant des chiens avec comme attributs le nom, la race et le propriétaire.
On verra également comment écrire le fichier.
La troisième étape de comparaison sera la validation :

  • Le nom et la race sont obligatoires
  • La race doit faire partie d'une liste connues de races

La quatrième étape sera du parsing CSV complexe, ajout de retour à la ligne pour le propriétaire.

Et enfin pour finir la comparaison je finirai avec un petit bench, en reprenant ce que j'avais fait mon "CsvEngine" (qui à l'époque s'appelait MoteurCsv) : Bench MoteurCsv

Pour la liste des parseurs, je suis parti de cet article : Java et CSV tour d'horizon des solutions open-source
Je rajouterai quand même CsvEngine :)

Comme d'habitude, l'ensemble du code est disponible sur github : CsvJavaComparaison.

BeanFiles

Site : http://code.google.com/p/beanfiles/

Étape 0 : Documentation et mise en place

Voici tout d'abord les problèmes que j'ai rencontré avec BeanFiles pour la mise en place :
  • La documentation est très pauvre : une page sur le wiki, plus une classe de test.
  • BeanFiles est une librairie construite avec maven, mais je n'ai pas trouvé de repo maven associé, ce qui a compliqué la mise en place
  • La documentation est relativement pauvre, elle ne contient qu'un exemple de code, mais pas de tuto de mise en place, du coup j'ai été obligé d'aller voir dans le pom.xml du code source pour avoir les dépendances.


Étape 1 : Lecture du fichier CSV simple

La mise en place est relativement simple une fois que les problèmes ont été résolus :).
Mise à part les problèmes sités plus haut, BeanFiles n'aime pas du tout avoir des lignes vides à la fin du fichier.
Une autre limitation, les attributs de la classe doivent avoir les mêmes noms que les entêtes dans le fichier CSV.

La lecture est relativement simple :
public List<Dog> getDogs(InputStream stream) throws IOException {
CSVReaderIterator<Dog> readerIterator = new CSVReaderIterator<Dog>(Dog.class, stream);
stream.close();
List<Dog> dogs = new ArrayList<Dog>();
for (Dog dog : readerIterator) {
dogs.add(dog);
}
return dogs;
}
J'aurais préféré que la fermeture du stream soit gérée par la librairie.
L'utilisation d'un iterator est pas bête, toutefois elle peux donner l'impression que la lecture du fichier se fait au fur et à mesure, alors que pas du tout :)
Du coup comme la lecture est finie après l'appel au constructeur, j'aurais bien aimé pouvoir récupérer directement la liste (un getAll() sur l'iterator).
Je trouve qu'avoir mis le parsing dans le constructeur n'est pas génial...

Étape 2 : écriture de fichier CSV

Il n'est pas possible d'écrire avec cette librairie.

Étape 3 : validation

Pas de validation non plus.

Étape 4 : parsing complexe.

Aucun problème avec les retour à la ligne pour BeanFiles.

Étape 5 : bench

Pour le bench, j'ai généré un fichier "moyen" (100 000 lignes) et un gros fichier (1 000 000 lignes).
Pour le fichier moyen, le temps moyen de traitement est de 1292ms.

Pour le gros fichier, il a fallu que je monte la JVM à 2Go. C'est un problème que je vois à BeanFiles, il n'y a pas de possibilité d'ajouter un handler pour effectuer un traitement au fur et à mesure de la lecture. Pour traiter de gros fichiers, c'est donc un gros problème.
Pour traiter ce fichier, le temps moyen de traitement est de 15120ms avec une consommation mémoire d'un peu moins de 1,6Go.

BeanIO

BeanIO est sûrement très bien, mais j'aime pas trop les configuration XML, donc je passe.

Commons-Csv

Je ne l'ai pas étudié non plus, pour deux raisons :
  • La version actuelle est la version 1.0-SNAPSHOT
  • C'est bas niveau, un peu comme open-csv


CsvToSql

Encore du XML... et en plus, ça n'a pas l'air adapté à ce que je veux faire.

FlatPack

Encore du XML...

JavaCSV

Les exemples de code montre que cette librairie ne fait pas vraiment du mapping : JavaCSV Samples

JCsv

Site : http://code.google.com/p/jcsv/

Étape 0 : Documentation et mise en place

La mise en place est très simple et documentée, juste la dépendance à ajouter et c'est parti.
C'est le gros point positif pour cette librairie la documentation est très riche.

Étape 1 : Lecture du fichier CSV simple

Étant fan du principe des annotations, je choisi d'utiliser cette méthode pour mon parsing :
public class Dog {
@MapToColumn( column = 0)
private String name;
@MapToColumn( column = 1)
private String race;
@MapToColumn( column = 2)
private String proprietary;
}
J'aurais préféré pouvoir utiliser la ligne d'entête mais bon...

La lecture n'est pas très compliquée non plus :
public List<Dog> getDogs(InputStream stream) throws IOException {
Reader reader = new InputStreamReader(stream);

ValueProcessorProvider provider = new ValueProcessorProvider();
CSVEntryParser<Dog> entryParser = new AnnotationEntryParser<Dog>(Dog.class, provider);
CSVReader<Dog> csvDogReader = new CSVReaderBuilder<Dog>(reader)
.entryParser(entryParser)
.strategy(new CSVStrategy(',', '"', '#', true, true)).build();

return csvDogReader.readAll();
}
Lors de mon premier essai, je n'avais pas mis de "strategy", et l'exception remontée n'était pas très parlante (ArrayIndexOutBoundException)... Mis à part ça, je n'ai pas eu de d'autres problèmes.

Étape 2 : écriture de fichier CSV

L'écriture n'est pas très compliquée, par contre on ne peut pas utiliser les annotations, ce qui est un peu dommage.
public void writeFile(List<Dog> dogs, File file) throws IOException {

CSVEntryConverter<Dog> entryConverter = new CSVEntryConverter<Dog>() {
@Override
public String[] convertEntry(Dog dog) {
String[] columns = new String[3];
columns[0] = dog.getName();
columns[1] = dog.getRace();
columns[2] = dog.getProprietary();

return columns;
}
};
CSVWriter<Dog> csvDogWriter = new CSVWriterBuilder<Dog>(new FileWriter(file))
.entryConverter(entryConverter)
.strategy(new CSVStrategy(',', '"', '#', true, true))
.build();
csvDogWriter.writeAll(dogs);
csvDogWriter.close();
}
Le résultat n'est pas très bon, si les champs contiennent des retours à la ligne, il n'ajoute pas les caractères '"' avant et après. Il n'ajoute pas l'entête.
Bref, l'écriture existe, mais elle n'est pas satisfaisante de mon point de vue.

Étape 3 : validation

Pas de principe de validation.

Étape 4 : parsing complexe.

Aucun problème avec les retours à la ligne pour JCsv.

Étape 5 : bench

J'utilise les mêmes fichiers que pour BeanFiles.
Pour le fichier moyen, le temps moyen de traitement est de 39 219ms.

Pas de problème de consommation mémoire, JCsv permet de lire ligne par ligne, on a donc pas besoin de tout stocker dans une liste. Par contre les performances sont très mauvaises, cela provient du fait que JCsv appelle getAnnotations pour chaque ligne, et ne met rien en cache.

Le gros fichier confirme le bench avec le fichier moyen :
  • Temps de traitement moyen : 367 449ms


JSefa

Site : http://jsefa.sourceforge.net/

Étape 0 : Documentation et mise en place

La mise en place n'est pas documentée et il n'existe pas de repo maven (en tout cas je l'ai pas trouvé), par contre il suffit d'ajouter le jar.
Au niveau documentation, il existe une page avec les exemples basiques, pour des trucs plus complexes, il faut regarder la javadoc ou le code source.

Étape 1 : Lecture du fichier CSV simple

La déclaration du mapping via les annotations est plutôt simple :
@CsvDataType
public class Dog {
@CsvField(pos = 0)
private String name;
@CsvField(pos = 1)
private String race;
@CsvField(pos = 2)
private String proprietary;
}
J'aurais préféré pouvoir utiliser la ligne d'entête mais bon...

La lecture n'est pas très compliquée non plus :
public List<Dog> getDogs(InputStream stream) throws IOException {
CsvConfiguration config = new CsvConfiguration();
config.setFieldDelimiter(',');
Deserializer deserializer = CsvIOFactory.createFactory(config, Dog.class).createDeserializer();

List<Dog> dogs = new ArrayList<Dog>();

deserializer.open(new InputStreamReader(stream));
while (deserializer.hasNext()) {
dogs.add(deserializer.<Dog>next());
}
deserializer.close(true);

return dogs;
}
Pour filtrer l'entête on est obligé d'ajouter un Filter, un simple boolean dans les config aurait été appréciable...

Étape 2 : écriture de fichier CSV

L'écriture n'est pas très compliquée non plus :
public void writeFile(List<Dog> dogs, File file) throws IOException {
CsvConfiguration config = new CsvConfiguration();
config.setFieldDelimiter(',');
Serializer serializer = CsvIOFactory.createFactory(config, Dog.class).createSerializer();

serializer.open(new FileWriter(file));
for (Dog dog : dogs) {
serializer.write(dog);
}
serializer.close(true);
}
Toujours pas moyen d'ajouter l'entête.

Étape 3 : validation

Il existe une couche de validation, par contre elle n'est pas documentée, et je n'ai pas réussi à la faire fonctionner (n'hésitez pas à corriger mon code sur github si vous savez comment faire :) ).

Étape 4 : parsing complexe.

Aucun problème avec les retour à la ligne pour JSefa.

Étape 5 : bench

J'utilise les mêmes fichiers que pour BeanFiles.
Pour le fichier moyen, le temps moyen de traitement est de 791ms.

Le gros fichier confirme le bench avec le fichier moyen :
  • Temps de traitement moyen : 7 652ms


open-csv

C'est une librairie bas niveau (utilisée par la plupart des parseurs haut niveaux), je l'étudierai donc pas ici.

Ostermiller CSV

Dans cette librairie, le mapping se fait à la main, je ne l'étudierai donc pas.

Skife CSV

Encore une librairie bas niveau.

Super CSV

Site : http://supercsv.sourceforge.net/

Étape 0 : Documentation et mise en place

La mise en place n'est pas documentée, c'est pas du maven, dont pas si simple que ça, faut bien penser à mettre les deux jar dans les dépendances...
De manière générale la documentation est plutôt pas mal (malgré le fait qu'il n'existe pas de documentation pour la mise en place).

Étape 1 : Lecture du fichier CSV simple

Pas d'annotation pour cette librairie, tout se fait par le nom des attributs.
Pour les options il faut passer par des CellProcessors, j'y reviendrai pour la validation.

La lecture n'est pas très compliquée :

public List<Dog> getDogs(InputStream stream) throws IOException {
List<Dog> dogs = new ArrayList<Dog>();

ICsvBeanReader inFile = new CsvBeanReader(new InputStreamReader(stream), CsvPreference.STANDARD_PREFERENCE);
final String[] header = inFile.getCSVHeader(true);
Dog dog;
while( (dog = inFile.read(Dog.class, header)) != null) {
dogs.add(dog);
}
inFile.close();
return dogs;
}
La gestion de l'entête et l'itération se fait à la main, je trouve ça dommage. Pour le reste c'est plutôt efficace.

Étape 2 : écriture de fichier CSV

Pour l'écriture, il n'y a pas de gestion de mapping, tout ce fait à la main, du coup je ne l'étudierai pas ici.

Étape 3 : validation

Pour la validation, c'est plutôt efficace, par contre cela repose sur l'ordre des champs, un peu dommage.
Il faut donc déclarer un tableau de CellProcessor :
public static final CellProcessor[] userProcessors = new CellProcessor[] {
new NotNull(),
new IsIncludedIn(new HashSet<Object>(DogValid.POSSIBLE_RACES)),
null
};
On passe ensuite ce tableau pour le parsing des lignes :
inFile.read(DogValid.class, header, userProcessors)


Étape 4 : parsing complexe.

Aucun problème avec les retours à la ligne pour Super Csv.

Étape 5 : bench

J'utilise les mêmes fichiers que pour BeanFiles.
Pour le fichier moyen, le temps moyen de traitement est de 749ms.

Le gros fichier confirme le bench avec le fichier moyen :
  • Temps de traitement moyen : 7 523ms


CsvEngine

Pour ceux qui ne le savent pas, je suis le développeur de cette librairie, je ne suis donc sans doute pas très objectif :).
Site : https://github.com/ybonnel/CsvEngine

Étape 0 : Documentation et mise en place

La mise en place est très simple et documentée : Wiki install.
De manière générale, entre le wiki, la javadoc et les tests, je pense objectivement que CsvEngine est la librairie la plus documentée de celles que j'ai testées.

Étape 1 : Lecture du fichier CSV simple

Il faut tout d'abord ajouter les annotations à la classe dog :
@CsvDataType
@CsvFile
public class Dog {
@CsvColumn("name")
private String name;
@CsvColumn("race")
private String race;
@CsvColumn("proprietary")
private String proprietary;
}
Un truc qu'il faudra ajouter dans CsvEngine et le fait de rendre le nom du champs CSV facultatif (déduit du nom de l'attribut).

La lecture est très simple :

public List<Dog> getDogs(InputStream stream) throws IOException, CsvErrorsExceededException {
CsvEngine engine = new CsvEngine(Dog.class);
return engine.parseInputStream(stream, Dog.class).getObjects();
}


Étape 2 : écriture de fichier CSV

L'écriture n'est pas plus compliquée que la lecture :
public void writeFile(List<Dog> dogs, File file) throws IOException {
CsvEngine engine = new CsvEngine(Dog.class);
engine.writeFile(new FileWriter(file), dogs, Dog.class);
}


Étape 3 : validation

Pour la validation tout passe par les annotations :
@CsvFile
public class DogValid {
@CsvColumn(value = "name", mandatory = true)
private String name;
@CsvValidation(ValidatorRace.class)
@CsvColumn(value = "race", mandatory = true)
private String race;
@CsvColumn("proprietary")
private String proprietary;

public static class ValidatorRace extends ValidatorCsv {
@Override
public void validate(String field) throws ValidateException {
if (!POSSIBLE_RACES.contains(field)) {
throw new ValidateException("The race \"" + field + "\" isn't correct");
}
}
}
}
On lit ensuite la fichier comme d'habitude :
public List<DogValid> readDogsValid(InputStream stream) throws CsvErrorsExceededException {
CsvEngine engine = new CsvEngine(DogValid.class);
return engine.parseInputStream(stream, DogValid.class).getObjects();
}


Étape 4 : parsing complexe.

Aucun problème avec les retour à la ligne pour CsvEngine.

Étape 5 : bench

J'utilise les mêmes fichiers que pour BeanFiles. Pour le fichier moyen, le temps moyen de traitement est de 693ms.

Le gros fichier confirme le bench avec le fichier moyen :
  • Temps de traitement moyen : 7 028ms


Conclusion

Première conclusion, écrire un article aussi long avec l'éditeur de Blogger est une corvée... faut que je trouve autre chose pour mon blog.

Voici un petit tableau récapitulatif des tests que j'ai pu mené sur ces librairies :
DocumentationMise en placeLectureÉcritureValidationTemps de traitement
BeanFiles--=XX15 120
JCsv++--X367 449
JSefa-=-=-7 652
Super CSV+=+-+7 523
CsvEngine+++++7 028
Légende :
  • X : N'existe pas
  • - : Existe mais pas terrible
  • = : Existe et marche plutôt bien
  • + : Existe et est vraiment bien fait :)
De mon point de vue tout à fait objectif CsvEngine est la meilleure librairie sur tout les points, son seul défaut est sans doute mon Anglais très approximatif.

Commentaires

Retour de Devoxx France : troisième journée

08 mai 2012

Tags : Devoxx, Java

Après mon retour sur la première journée (ici) et la deuxième journée (ici), voici celui de la troisième et dernière journée.

Les keynotes

Après avoir avalé un nombre de cafés important (la fatigue commençant à se faire sentir), je me dirige vers la salle des keynotes. La salle est toujours aussi impressionnante (950 places).

Trends in mobile application development

Je vais pas pouvoir vraiment vous parler de cette keynote, elle était réalisée par IBM, et tout le monde s'endormait...
La keynote était tellement intéressante que tout le monde se lâchait sur twitter. La seule chose marrante de cette conf a été la fin, où Nicolas Martignole nous a dit que le tweet wall était en panne et qu'il ne pouvait pas l'afficher, belle manière de ne pas mettre mal à l'aise un des sponsors de Devoxx :).

Portrait du développeur en "The Artist"

Rien à voir avec la conf précédente, c'est plutôt l'inverse...
Patrick Chanezon nous livre son portrait de la vie classique d'un développeur, passionnant. Je ne peux que vous conseiller d'aller voir les slides sur slide-share.
Le sujet principale était le cloud, mais cette conf allait beaucoup plus loin et j'ai adoré.

Abstraction Distractions for France

Neal Ford nous parle de couches abstractions, je pense que c'est une des conf les plus impressionnante que j'ai vu, Neal est un très bon orateur et la conf est très bien préparée. Ça ressemblait à une key-note Apple :).

Kotlin ou CodeStory?

Finnalement je suis retourné à CodeStory, si vous ne savez pas ce que c'est, aller voir mon billet sur la deuxième journée :).
Toujours aussi intéressant, cette fois j'ai ramener mon Ordi, j'ai donc même forké et mis en place l'environnement de dev, mais le temps d'installer ZombyJS, j'ai pas pu aller beaucoup plus loin (il fallait installer Node.js...).
Kotlin est lui un nouveau langage créé par Jetbrain, j'irai voir la conf quand elle sera disponible sur Parleys.

Changeons la conception de nos applications grâce aux services Cloud

Cette conf donnée par Cyrille Le Clerc est intéressante mais je ne l'ai pas trouvé adaptée au publique, pas assez de code et beaucoup trop de présentation de produits... Elle avait cependant l'intérêt de faire un inventaire des produits, je me suis noté qu'il fallait que la revoie lorsqu'elle sortirai sur Parleys.

An overview of Guava : Google Core Libraries for Java

Ben en fait non, quand je suis arrivé devant la porte, c'était déjà plein, et je me suis donc fait refoulé...
Du coup, je suis retourné voir CodeStory, chose marrante ils ont mis en place un système de Cache à l'aide de Guava :).

Java Caching with Guava

Après avoir vu une utilisation du cache avec Guava, je vais voir la conf théorique sur le sujet. En toute franchise, je me suis rendu compte que rien ne vaux la pratique. J'ai l'impression d'avoir plus appris avec les 10 minutes d'explications par la pratique qu'avec les 55 minutes de conf théorique.
Pour ceux qui ne connaissent pas Guava, je pense que c'est la première dépendance à ajouter sur un projet. Guava c'est apache-common en moderne et mieux fait :).

Android, Graphisme et Performance

Romain Guy (développeur de la couche graphique d'android) nous livre quelques astuces pour réaliser des applications Android qui marchent bien.
Un des trucs que j'ai noté (et que je n'ai pas encore eu le temps de tester), c'est le StrictMode qui permet de faire planter l'application dès qu'on fait un accès à une ressource lente dans le UI Thread. Cela permet de corriger tous les accès qui sont fait dans le UI Thread et qui ne devraient pas.

Les Cast Codeurs Podcast

Pour la dernière "conf" de la journée, je suis aller voir l'enregistrement live des Cast Codeurs. Pour ceux qui ne connaissent pas, c'est LE Podcast Java en France. L'ambiance était excellente, Atlassian distribuait des bières ce qui ne gâcher rien :). Si vous voulez vous rendre compte de l'ambiance, il suffit d'écouter l'épisode associé (ici).
Pour les bretons, il y aura un autre enregistrement live au Breizhcamp.


Toutes les conférences de Devoxx France étaient filmées et seront disponible sur Parleys au fil de l'eau durant l'année en gratuit ou rapidement en payant (les participants y ont accès tout de suite).
Je tiens à remercier les organisateurs (surnommés les polos rouges), et vivement l'année prochaine!

Commentaires

Retour de Devoxx France : première journée

24 avril 2012

Tags : Devoxx, Java

J'ai eu l'occasion de participer à LA grosse conf Java en France, et franchement c'était que du bon.

Les rencontres

Un des trucs les plus plaisant à Devoxx a été les rencontres que j'ai pu faire et les discussions que j'ai pu avoir avec les speakers et participants. Un exemple : c'est pas tous les jours qu'on a l'occasion de discuter avec un développeur de Montain View qui bosse sur Android (Romain Guy en l'occurrence). Ce n'est pas non plus tous les jours qu'on a l'occasion de développer avec un développeur d'exception (David Gageot).

Je vais donc maintenant essayer de vous faire un petit compte rendu de ces trois jours qui furent très riches!

Premier jour

Première étape : le café, et les discussions avec les têtes connues et moins connues.
Pour ma part je suis arrivé à 8h tous les matins, ce qui m'a permis de prendre le temps de discuter, les confs ne commençant qu'à 9h30.

La première journée est un peu particulière, elle est principalement consacrée à des "Universités" et des "Hands-on Labs" qui sont des sessions de 3 heures avec pour les Labs de la manip sur son ordi. Ça laisse donc le temps de rentrer un peu plus dans le détail du sujet, et un peu moins le survoler.

Les 3 A pour Java EE 6

Cette session était donc consacrée à Java EE6. Elle s'est déroulée sous forme de TP permettant de mettre en pratique les nouveautés JEE6. Le TP s'appuyait sur le projet exemple initié par Antonio Goncalves.
J'ai profité de cette session pour tester NetBeans, et franchement, pour faire du JEE, c'est juste excellent. On était 2 sous NetBeans sur 40 personnes environ. Ceux qui utilisaient Eclipse ont commencé à avoir un environnement de dev fonctionnel au bout d'un peu plus d'1h, alors que j'avais quasiment fini. J'avais tout de même anticipé en préparant l'environnement la veille. L'absence d'internet sur place rendant toute installation compliquée.
J'ai donc pu mettre en pratique CDI et JAX RS, et j'ai plutôt été agréablement surpris. On va peut être pouvoir enfin se passer de Spring :). Les speakers étaient très disponibles et plutôt sympas (ce qui était le cas pour tout les speakers, je ne vais donc pas le répéter :) ).

Après cette session, une petite pause déjeuner. J'ai pu manger à ma faim, et sans être exceptionnel (ça vaut pas les galettes saucisses du breizhcamp), c'était plutôt bon, merci donc aux organisateurs. Encore une occasion de discuter et partager avec plein de monde.

Hackergarten Paris

Ne trouvant pas de sujet qui m'intéresse particulièrement, je décide d'aller au Hackergarten. J'ai donc pu découvrir ce que c'est que cette chose imprononçable. C'est en fait l'occasion de contribuer à un projet open-source. La session commence par un inventaire des projets open-source représentés (par un contributeur). Voici ce qui est ressorti de l'inventaire (du moins ceux dont je me souviens) :
  • Maven
  • Groovy
  • Jenkins
  • Infinitest
  • Je sais plus mais y en avait d'autre :)
Il y avait donc du lourd, j'ai décidé de contribuer à Infinitest. Ce projet se concrétise par un plugin Eclipse et un plugin Intellij permettant d'avoir un état des tests unitaires en permanence dans l'IDE. Dans Eclipse, à chaque build (autant dire tout le temps), le plugin analyse les deltas afin de lancer les tests potentiellement impactés par la modification.
Nous nous somme retrouvés à deux pour contribuer à Infinitest encadrés par David Gageot qui est le Leader du projet. Pendant que David nous présente le code, j'installe l'environnement de dev. David nous présente également une idée de fonctionnalité à réaliser. Actuellement le plugin analyse les dépendances entre classes afin de sélectionner les tests à exécuter, mais si un test est impacté par une modification de fichier plat, il n'est pas exécuté. Sans rentrer dans les détails, cette session c'est traduite par une pull request dans github, c'était donc plutôt concret. Cela m'a permis de découvrir le développement de plugin Eclipse, et toute la difficulté pour réaliser des tests automatisés dans ce genre d'environnement.



Les tools in action

La journée s'est terminée pour ma part par trois tools in action, ce format est un format intense : 30 minutes pour présenter un sujet.

Le premier était sur Selenium Grid, qui permet de mettre en place un pool de browser qui serviront ensuite aux tests automatisés avec Selenium. Malgré toutes les difficultés qu'a pu rencontrer Mathilde Lemee pour les démos, cela donne une bonne idée du fonctionnement de l'outil et donne envie de tester.

Le suivant était sur AndroidAnnotations : sur scène deux speakers (Pierre-Yves Ricau et Alexandre Thomas). AndroidAnnotations est un framework dont le but est de simplifier les développements Android en utilisant massivement les annotations. Les 30 minutes ont quasiment été entièrement utilisées à du live-coding, ça aurait pu être un exercice casse gueule, mais les deux speakers ont préparé leur truc et tout se déroule sans encombre. Résultat : faut que j'essaye, et j'ai bien envie de contribuer au projet.

La tout dernière session fue consacrée aux lambdas en actions avec la beta du JDK8. Ce que j'ai retenu : enfin une évolution majeure du langage Java. Une conséquence marrante des Lambdas est la quasi introduction du polymorphisme (les interface peuvent maintenant avoir des implémentations par défaut pour les méthodes).

L'article sur la deuxième journée arrivera un peu plus tard :)

Les photos ont été récupérées sur les albums de Claude

Commentaires

Bench de MoteurCsv

22 février 2012

Tags : CSV, Java

Suite à la publication du billet MoteurCsv le JPA du CSV, quelqu'un m'a demandé si j'avais fait un bench. N'en ayant pas fait, je me suis lancé.
Les sources du bench sont disponibles sur github

Génération du fichier de test

Le "cahier des charges" fournit par "bbo" était le suivant :
  • ~10 000 000 de lignes.
  • ~200 caractères par ligne.
  • ~20 champs (4/5 gros champs autour de 20 caractères.)
C'est avec ces contraintes que j'ai créé la classe GenerationFchierCsv.
Cette classe génère donc un fichier de 10 000 000 de lignes contenant des lignes avec 20 champs :
  • Quatre champs de type String et de 30 caractères
  • Quatre champs de type Boolean (1 ou 0)
  • Quatre champs de type Integer et de 5 chiffres
  • Quatre champs de type Double et de 5 chiffres avant la virgule et 5 chiffres après la virgule
  • Quatre champs de type String et de 5 caractères

Voici le résultat de la génération du fichier :
Statistiques sur la taille des lignes :
  • Minimum : 215 caractères
  • Maximum : 227 caractères
  • Moyenne : 225,666 caractères
Taille du fichier : 2 266 666 035 octets soit 2,11 Go
Génération en 70 576ms
Le résultat est donc à peu près conforme au cahier des charges.

Constitution du bench

Le bench est réalisé par la classe Bench (je sais, je suis trop bon pour trouver des noms).
Pour le bench, je ne pouvais pas utiliser la méthode permettant de parser tout le fichier et qui renvoie une liste d'objet. Vu la taille du fichier, il est facile de comprendre que le mettre entièrement en mémoire ne serait pas une bonne idée.
Heureusement le moteur contient une autre méthode permettant de réaliser un traitement pour chaque ligne : MoteurCsv.parseFileAndInsert
Dans le cadre du bench j'ai donc utilisé cette méthode en ne réalisant aucun traitement :
public static void bench1() throws FileNotFoundException {
long startTime = System.currentTimeMillis();
moteur.parseFileAndInsert(new FileReader(fichier), ObjetCsv.class,
new InsertObject<objetcsv>() {
@Override
public void insertObject(ObjetCsv objet) {
// On ne fait rien dans le cadre du bench.
}
});
long elapsedTime = (System.currentTimeMillis() - startTime);
System.out.println("Lecture du fichier : " + elapsedTime + "ms");
}

J'ai également créé une méthode permettant de voir l'utilisation mémoire au fur et à mesure du test.
Le but de cette méthode est de regarder la mémoire occupée avant et après un GC entre chaque itération du bench. Cela permettra de vérifier entre autre qu'il n'y ait pas fuite mémoire.
public static void gestionMemoire() {
// Mémoire totale allouée
long totalMemory = Runtime.getRuntime().totalMemory();
// Mémoire utilisée
long currentMemory = totalMemory - Runtime.getRuntime().freeMemory();
System.out.println("Mémoire avant gc : " + (currentMemory / 1024) + "ko/" + (totalMemory / 1024) + "ko");
System.gc();
// Mémoire totale allouée
totalMemory = Runtime.getRuntime().totalMemory();
// Mémoire utilisée
currentMemory = totalMemory - Runtime.getRuntime().freeMemory();
System.out.println("Mémoire après gc : " + (currentMemory / 1024) + "ko/" + (totalMemory / 1024) + "ko");
}

Résultats

Les tests ont été menés sur un MacBookPro équipé d'un disque SSD et d'un Code i5.
ÉtapeTempsMémoire occupée avant GCMémoire occupée après GC
Avant de commencer/1 734ko338ko
Instanciation du moteur58 222µs14 049ko387ko
Lecture du fichier (itération 1)65 902ms14 049ko387ko
Lecture du fichier (itération 2)65 864ms13 796ko341ko
Lecture du fichier (itération 3)64 638ms14 059ko341ko
Lecture du fichier (itération 4)65 214ms13 700ko341ko
Lecture du fichier (itération 5)66 560ms14 027ko341ko

Ces résultats permettent de montrer plusieurs choses :
  • Temps pour instancier le moteur : quasi-null
  • Mémoire persistante pour le moteur : quelques Ko
  • Performances plutôt satisfaisantes avec un peu plus d'une minute pour lire un fichier de plus de 2Go

J'ai également fait du profiling avec YourKit afin de vérifier le comportement interne du moteur, cela a montré que la majorité du temps est passé dans la librairie open-csv, l'overhead du moteur est donc plutôt faible.

Reproduire le bench

N'hésitez pas à reproduire le bench et à me dire les résultats que vous obtenez :
  • Cloner le projet depuis github : github.com/ybonnel/BenchMoteurCsv
  • Importer le projet en tant que projet maven dans Eclipse
  • Lancer d'abord le main de la classe GenerationFchierCsv afin de générer le fichier de test
  • Lancer ensuite le main de classe Bench afin de lancer le bench en lui-même
Si vous connaissez d'autres parseurs CSV n'hésitez pas à ajouter des benchs dans le projet pour comparer les performances avec d'autres parseurs.

Commentaires

MoteurCsv le JPA du CSV

20 février 2012

Tags : CSV, Java

Voici un titre qui va faire gonfler mes chevilles :)
Je vais donc vous présenter une petite librairie Java sans prétention que j'ai réalisé afin de pourvoir lire et écrire des fichiers CSV facilement.

C'est quoi le CSV?

Bon pour la définition du format CSV, je vous invite à consulter l'article wikipedia.

Pourquoi cette librairie?

Cette librairie a été crée afin de pouvoir gérer le format GTFS. Ce format est utilisé afin de décrire un réseau de Transports en commun. Techniquement, c'est un zip contenant des fichiers CSV.
J'ai eu à utiliser ce format suite à l'ouverture des données de transports à Rennes où j'ai réalisé une application Android (Transports Rennes) permettant de consulter les horaires de bus.
A l'époque où j'ai créé cette librairie, je baignais dans du JPA la journée, et j'avoue que j'aime beaucoup l'idée de décrire du mapping directement dans la classe associé en utilisant des annotations.
C'est comme ça que MoteurCsv est né.

Alors comment ça s'utilise?

J'ai essayé de simplifier au maximun l'utilisation de la librairie.

Installation

Maven

Si vous êtes sous maven, l'intégration dans votre projet est très simple.
Il suffit d'ajouter dans votre pom.xml :
<dependencies>
<dependency>
<groupId>fr.ybo</groupId>
<artifactId>moteurcsv</artifactId>
</dependency>
</dependencies>

<repositories>
<repository>
<id>ybonnel-release</id>
<url>https://repository-ybonnel.forge.cloudbees.com/release/</url>
</repository>
</repositories>

Autre

Si vous n'êtes pas sous maven, il vous suffit d'intégrer les deux jars suivant :

Utilisation

Maintenant le vif du sujet, comment utiliser cette petite librairie.
Afin de pouvoir lire ou écrire un CSV il faut commencer par décrire la classe correspondante.
On va prendre pour l'exemple un CSV décrivant des personnes avec deux colonnes : "nom" et "prenom".
Voici un exemple correspondant :
nom,prenom
Bonnel,Yan
Vador,Dark
Noël,Père

Déclaration de la classe

Nous allons donc créer la classe Personne associée avec les annotations permettant de faire le mapping :

// Annotation permettant de dire au moteur que cette classe est associée à un fichier CSV.
@FichierCsv
public class Personne {

// Annotation permettant de dire au moteur que cet attribut est mappé avec la colonne "nom" du CSV.
@BaliseCsv("nom")
private String nom;

// Annotation permettant de dire au moteur que cet attribut est mappé avec la colonne "prenom" du CSV.
@BaliseCsv("prenom")
private String prenom;
}

Création du moteur

MoteurCsv moteur = new MoteurCsv(Personne.class);

Lecture d'un fichier CSV

Voici maintenant le code permettant de lire le fichier, et de le transformer en une liste d'objets :
InputStream stream = new FileInputStream(new File("personnes.csv"));
List<Personne> personnes = moteur.parseInputStream(stream, Personne.class);

Écriture d'un fichier CSV

Et le core permettant d'écrire un fichier :
Writer writer = new FileWriter(new File("personnes.csv"));
moteur.writeFile(writer, personnes, Personne.class);

Conclusion

J'espère que cette petite librairie sera utile à quelqu'un d'autre que moi :)
Elle est open-source (LGPL v3) : github.com/ybonnel/MoteurCsv
N'hésitez donc pas à forker et à faire des pull request!
Site généré contenant entre autre la javadoc : ybonnel.github.com/MoteurCsv

Commentaires