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