TodoList

  • publier sur le blog ippon mon article sur la directive de carte de France
  • idem avec article sur le stateProvider dans angularJs
  • article sur chat en websockets avec atmosphere  Jhipster lâche atmosphere trop compliqué pour truc natif spring dont j’ai oublié le nom… A revoir.
  • écrire un article sur les window function en sql
  • faire un article pour mon projet du jeu de société batisseurs en jhipster/mongodb
  • faire un article pour mon projet de jeu style civilisation en jhipster / postgresql et push sur heroku

Tant de tâches et si peu de temps…

Play 2 et Rest : colonies


Bientôt, une nouvelle mission à faire du Play 2 et du Rest, donc on se retrousse les manches et on fait un projet perso là dessus !

Le Projet

Le projet est le suivant : vous connaissez Dwarf Fortress ? Bon ben c’est un jeu en ascii moche où on essaye de faire une forteresse avec des nains et où on meurt tout le temps. J’ai des qualifications très fortes pour faire des trucs moches (cf mes projets précédents) donc faisons notre version web dont je traine plusieurs ancêtres dans mes différentes clés USB, et qui s’appellent tous Astrum Incognitum : Colonies. C’est long mais personnellement je trouve cela classieux, et qui ici doute de mon bon goût supérieur ? Bien. Donc :

  1. un monde (« astre ») fait en génération pseudo-procédurale (ça veut dire « au hasard » mais en plus joli, et pseudo parce que pas complètement au hasard)
  2. Des joueurs représentés dans le jeu par une boite qui donne des ordres aux…
  3. Colons qui obéissent ou pas aux ordres et acquièrent des compétences
  4. Des objets à amasser ramasser transformer détruire construire

J’ai longuement hésité entre du Sql et du NoSql… Puis finalement bon, la future mission sera avec BDD en Sql donc il faut s’y plier.

Architecture

Play 2 et Rest, donc Rest servira à CRUDer les infos du joueur, le monde persistant, les colons, les ressources, les ennemis, etc, autant pour l’administration que pour les clients et les pages web et les outils d’administration seront en play 2 classique.

Gwt et Appfuse

Nouveau framework : GWT, nouvelle couche de persistence en NoSql : le Datastore de google et le PaaS (Plateform as a Service, ou « filez vos programmes on gère le bousin sur nos serveurs »). Le but sera de développer une appli de gestion de partie de jeu de société. Définition du projet L’application Réservajeux (merci j’ai trouvé le nom tout seul, en effet) permet à un utilisateur de :

  • créer un compte avec login et mot de passe
  • renseigner les jeux qu’il possède
  • créer ou rejoindre un groupe de joueurs
  • créer un lieu de jeu
  • proposer afficher ou s’inscrire un créneau de jeu avec jeu de société

Le début est très simple  : installation du plugin pour éclipse (si vous voulez donner des sous pour l’achat de idea, n’hésitez pas à me contacter) et une fois installé, la belle icône G nous permet de créer une web application en un clic.

Créons là après avoir suivi les instructions du post sur git (création d’un reservajeux dans le dépot, clonage dans un répertoire de projets), et créons le dans le répertoire reservajeux du répertoire de projets, pour pouvoir commiter le projet vide et tout propre. Par contre, pas de maven, et il faut garder les fichiers de conf eclipse. La création du compte appengine suit, il suffit de se connecter avec un compte gmail pour  avoir son compte lié au compte appengine, c’est vraiment simple.

Déployons notre application par la même occasion, cela se fait en 3-4 minutes le temps de compiler et d’uploader les fichiers. Une fois l’appli déployée, on peut tester dans le domaine reservajeux.appspot.com notre application. Le premier appel prend quelques secondes, le second est quasi immédiat. Par défaut et pour les comptes gratos, Google ne laisse pas les instances tourner mais les ferme après un peu d’inactivité donc tout est logique. On peut aussi le lancer en local, par contre l’url proposée ressemble à http://127.0.0.1:8888/Reservajeux.html?gwt.codesvr=127.0.0.1:9997 et propose le téléchargement d’un plugin pour chrome ou firefox. N

‘ayant pas réussi à l’utiliser (il est bien présent dans chrome mais même après redémarrage l’application demande à le télécharger), j’ai du chercher un mode non-développement ; il suffit simplement de virer l’option gwt.codesvr : http://127.0.0.1:8888/Reservajeux.html

Cela reste du mode developpement pour le serveur, on perd juste le mode développement côté client. Mais quand on n’arrive pas à dompter la machine pour lui installer un plugin… Bref. J’utiliserai le tuto officiel et un autre tuto que j’ai trouvé pour avancer rapidement, mais ça peut varier… Nous allons commencer par le mode simple : CRUD d’un jeu et CRUD de parties : date, heure, lieu et listes de noms de joueurs pour y participer. Créons nos entités :

@PersistenceCapable
public class Jeu {
@PrimaryKey
@Persistent(valueStrategy=IdGeneratorStrategy.IDENTITY)
private Key key;

@Persistent
private String nom;

@Persistent
private String urlFiche;
// setters et getters
}

@PersistenceCapable
public class Partie {
  @Persistent
  private Date date;

  @Persistent
  private List joueurs;

  @PrimaryKey
  @Persistent(valueStrategy=IdGeneratorStrategy.IDENTITY)
  private Key key;

  @Persistent
  private Key keyJeu;

  @Persistent
  private String lieu;

  // Setters et getters
}

Et créons notre service de Crud et son AsynVersion :

@RemoteServiceRelativePath("jeu")
public interface JeuxService extends RemoteService {
  void creerJeu(Jeu jeu) throws IllegalArgumentException;
  void supprimerJeu(Jeu jeu) throws IllegalArgumentException;
  List listeJeu();
}
public interface JeuxServiceAsync {
  void creerJeu(Jeu jeu, AsyncCallback callback) throws IllegalArgumentException;
  void supprimerJeu(Jeu jeu, AsyncCallback callback) throws IllegalArgumentException;
  void listeJeu(AsyncCallback<List> callback);
}

Ensuite côté serveur nous allons créer le singleton qui nous fournira notre PersistenceManagerFactory. Point de spring pour ce petit projet, donc pas d’injection, on le récupère via le singleton.

public final class PMF {
  private static final PersistenceManagerFactory pmfInstance = JDOHelper
      .getPersistenceManagerFactory("transactions-optional");

  private PMF() {
  }

  public static PersistenceManagerFactory get() {
    return pmfInstance;
  }
}

Et enfin le service web qui fait tout cela ; on pourrait ajouter une couche entre le ServiceServlet qui ferait le traitement plutôt que le laisser ici (et les 3 fonctions ne feraient qu’appeler les méthodes de cette couche), mais c’est un projet simple donc faisons un peu moche et rapide :

@SuppressWarnings("serial")
public class JeuxServiceImpl extends RemoteServiceServlet implements
    JeuxService {

  private PersistenceManager getPersistenceManager() {
    return PMF.get().getPersistenceManager();
  }

  @Override
  public void creerJeu(Jeu jeu) throws IllegalArgumentException {
    PersistenceManager persistenceManager = getPersistenceManager();
if (jeu.getNom().length() < 4 || jeu.getNom().length() > 30) {
 throw new IllegalArgumentException("Erreur sur le nom du jeu : " + jeu.getNom());
 }
    try {
      persistenceManager.makePersistent(jeu);
    } finally {
      persistenceManager.close();
    }
  }

  @Override
  public void supprimerJeu(Jeu jeu) {
    PersistenceManager persistenceManager = getPersistenceManager();
    try {
      persistenceManager.deletePersistent(jeu);
    } finally {
      persistenceManager.close();
    }
  }

  @Override
  public List listeJeu() {
    PersistenceManager persistenceManager = getPersistenceManager();
    List listeJeux = new ArrayList();
    try {

      Extent e = persistenceManager.getExtent(Jeu.class, true);
      Iterator iter=e.iterator();
      while (iter.hasNext())
      {
          Jeu jeu=(Jeu)iter.next();
          listeJeux.add(jeu);
      }
    } finally {
      persistenceManager.close();
    }
    return listeJeu();
  }
}

Enfin on code le EntryPoint, qui correspond à la page.


public class Reservajeux implements EntryPoint {
  private final JeuxServiceAsync jeuxService = GWT.create(JeuxService.class);

  private final FlexTable jeuxFlextable = new FlexTable();
  private final Button creeJeuButton = new Button("Créer le jeu");
  private final TextBox nomJeuField = new TextBox();
  private final TextBox urlJeuField = new TextBox();
  private final Label errorLabel = new Label();
  
  private ArrayList<String> jeux = new ArrayList<String>();

  public void onModuleLoad() {
    RootPanel.get("creeJeuButtonContainer").add(creeJeuButton);
    RootPanel.get("listeJeuxContainer").add(jeuxFlextable);
    RootPanel.get("nomJeuContainer").add(nomJeuField);
    RootPanel.get("nomJeuContainer").add(urlJeuField);
    creeJeuButton.addStyleName("sendButton");

    nomJeuField.setText("Nom du jeu");

    jeuxFlextable.setText(0, 0, "Nom");
    jeuxFlextable.setText(0, 1, "Fiche");
    jeuxFlextable.setText(0, 2, "");
    
    jeuxFlextable.setCellSpacing(5);
    jeuxFlextable.setCellPadding(5);
    jeuxFlextable.setWidth("100%");

    creeJeuButton.addClickHandler(new ClickHandler() {
      public void onClick(ClickEvent event) {
        ajouterJeu();
      }
    });

    RootPanel.get("errorLabelContainer").add(errorLabel);

    jeuxService.listeJeu(new AsyncCallback<List<Jeu>>(){
      @Override
      public void onFailure(Throwable caught) {
        Window.alert("Erreur récupération des jeux");
      }

      @Override
      public void onSuccess(List<Jeu> result) {
        for (Jeu jeu : result) {
          addLigne(jeu);
        }
      }
    });
  }

  private void ajouterJeu() {
    final String texteJeu = nomJeuField.getText().toUpperCase().trim();
    nomJeuField.setFocus(true);
    nomJeuField.setText("");
    urlJeuField.setText("");
    final Jeu jeu = new Jeu();
    jeu.setNom(texteJeu);
    jeu.setUrlFiche(urlJeuField.getText().trim());
    
    jeuxService.creerJeu(jeu, new AsyncCallback<Void>() {
      @Override
      public void onFailure(Throwable caught) {
        errorLabel.setText("Erreur lors de la création " + caught.getMessage());
      }

      @Override
      public void onSuccess(Void result) {
        addLigne(jeu);
      }
    });
  }

  private void addLigne(final Jeu jeu){
    int row = jeuxFlextable.getRowCount();
    jeuxFlextable.setText(row, 0, jeu.getNom());
    jeuxFlextable.setWidget(row, 1, new Hyperlink("fiche", jeu.getUrlFiche()));
    
    Button removeJeu = new Button("x");
    removeJeu.addClickHandler(new ClickHandler() {
      public void onClick(ClickEvent event) {
        jeuxService.supprimerJeu(jeu, new AsyncCallback<Void>() {
          @Override
          public void onFailure(Throwable caught) {
            errorLabel.setText("Erreur lors de la suppression " + caught.getMessage());
          }
          
          @Override
          public void onSuccess(Void result) {
            int removedIndex = jeux.indexOf(jeu.getNom());
            jeux.remove(removedIndex);
            jeuxFlextable.removeRow(removedIndex + 1);
          }
        });
      }
    });
    jeuxFlextable.setWidget(row, 3, removeJeu);
  }
  
}

La grosse difficulté aura été de comprendre une erreur de StackOverFlowException, qui nécessitait la suppression du répertoire gwt-unitCache, et surtout une erreur de Invalid Type sur l’objet Jeu.

Dans Gwt, les objets qui sont communs doivent hériter de l’interface « IsSerializable » et non pas Serializable comme dans mon souvenir douteux d’un ancien essai. Il fallait également avoir une clé de type Long et non pas String ou Key :

@PersistenceCapable
public class Jeu implements IsSerializable {
  @PrimaryKey
  @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
  @Extension(vendorName = "datanucleus", key = "gae.encoded-pk", value = "true")
  Long id;
  
  @Persistent
  private String nom;

  @Persistent
  private String urlFiche;
}

Ceci nous fait un beau tableau qui peut être mis à jour mais pour l’instant je reste un peu refroidi par les exceptions qui ne proposent pas de solutions probables au problème. Un ptit message « Vos objets partagés sont-ils IsSerializable ? » n’aurait pas été de refus sur l’exception « IncompatibleRemoteServiceException ».

gwt

C’est encore loin du compte (remarquez l’attention donnée au choix des données de test), mais je dois repasser sur un autre projet, donc on s’arrête pour le moment sur GWT. Et en plus ça ne supporte pas les sources en Cp1251, donc problèmes de caractères sur les accents. Tsss…

Revenir sur sevenwonders

J’ai du mettre en pause le blog pendant 3 mois pour une mission qui demandait pas mal de temps de cerveau disponible et qui m’a beaucoup appris. Si j’avais donc à refaire sevenwonders, voilà ce qui changerait en gros:

  • utilisation de Spring Roo pour créer rapidement la partie web
  • faire plus attention aux modifieurs d’accès java (par défaut j’oublie de mettre des private partout où je peux) ou static ou final
  • utilisation de Guava pour faire des fonctions de filtrage avec les prédicats sur les cartes et utiliser les fonctions helpers sur les collections, la possibilité de marquer des champs en nullable, etc
  • Utilisation de testNg et son dataprovider fort utile qui permet de tester plein de cas de tests en mettant les données et résultats attendus dans des tableaux :
@Resource(name="listeMerveilles")
List<Merveille> listeMerveilles;

@DataProvider
  private Object[][] merveillesProvider() {
    Object[][] testMerveille = new Object[7][3];
    return new Object[][] {
        {listeMerveilles.get(0), 1, "The Colossus of Rhodes", Lists<Cout>.asList(new Cout(0, Ressource.Brique, Ressource.Brique), new Cout(0, Ressource.Minerai), new Cout(0, Ressource.Pierre), null)},
        {listeMerveilles.get(1), 2, "The Pyramids Face B", Lists<Cout>.asList(new Cout(0, Ressource.Brique, Ressource.Brique), new Cout(0, Ressource.Minerai), null, null)},
        // etc...
    };
  }
@Test(dataProvider = "merveillesProvider")
public void testCoutEtagesMerveilles(Merveille merveille, Integer id, List<Cout> couts) {
     assertEquals(merveille.getId(), id);
     for (int i = 0; i < couts.size()) {
          assertEquals(merveille.getCoutAge(i), couts.get(i));
     }
}

Ainsi on peut tester tous les coûts de toutes les merveilles plus efficacement et en séparant le test des données à tester.

Bref, à 4 mois d’écarts le projet aurait été différent… Je le reprendrai dans un futur indéterminé pour tester le dépot sur un cloudbees ou autre PaaS.

Seven Wonders 5 – MVC (Spring Me VoiCi) et Jetty

L’appli web sera écrite en MVC. Plusieurs possibilités : l’écrire from scratch, ou en utilisant  le wizard de SpringSource, ou l’archetype appfuse spring mvc, ou en partant de tatami sur github…

Pour aller vite, je reprend un projet de test Spring MVC créé avec SpringSource que je transfère dans mon module sevenwonders-web. Celui-ci gagne un HomeControlleur.java et sa home.jsp, un web.xml, un root-context.xml et un servlet-context.xml ainsi qu’un log4j.xml.

Pour faire du beau site (disons « pas moche ») rapidement, récupérons Twitter Bootstrap pour le css et la mise en forme du site.

Nous allons enfin utiliser jetty pour lancer rapidement l’application. Grâce aux conventions maven, le plugin jetty sait directement où chercher le war (dans target) et utilise un tas de paramètres par défaut, dont le port à 8080.

Ajoutons simplement le plugin Jetty à notre pom :


<plugin>
 <groupId>org.mortbay.jetty</groupId>
 <artifactId>jetty-maven-plugin</artifactId>
 <version>${maven.jetty.version}</version>
 <configuration>
 <scanIntervalSeconds>${jetty.scanIntervalSeconds}</scanIntervalSeconds>
 <stopKey>stop-jetty</stopKey>
 <stopPort>9999</stopPort>
 <systemProperties>
 <systemProperty>
 <name>jetty.port</name>
 <value>${jetty.port}</value>
 </systemProperty>
 </systemProperties>
 </configuration>
 <executions>
 <execution>
 <id>start-jetty</id>
 <phase>pre-integration-test</phase>
 <goals>
 <!-- <goal>run-exploded</goal> -->
 <goal>run</goal>
 </goals>
 <configuration>
 <scanIntervalSeconds>0</scanIntervalSeconds>
 <daemon>true</daemon>
 </configuration>
 </execution>
 <execution>
 <id>stop-jetty</id>
 <phase>post-integration-test</phase>
 <goals>
 <goal>stop</goal>
 </goals>
 </execution>
 </executions>
 </plugin>

Il suffit de spécifier maintenant la version utilisée, en l’occurence la dernière : <maven.jetty.version>8.1.5.v20120716</maven.jetty.version>

Enfin, lançons le mvn jetty:run et allons voir en localhost:8080 le beau site web un peu flou qui nous attend, la simplicité de cette étape me mettant la larme à l’oeil.

Bon, essuyons vite la larme, la page est vide, ou quasiment, vu que je suis parti d’un contrôleur dont la simplicité est un hommage aux jardins de pierre japonais et qui se contente d’afficher l’heure qu’il est.

@Controller
public class HomeController {

private static final Logger logger = LoggerFactory.getLogger(HomeController.class);

/**
* Simply selects the home view to render by returning its name.
*/
@RequestMapping(value = "/", method = RequestMethod.GET)
public String home(Locale locale, Model model) {
logger.info("Welcome home! the client locale is "+ locale.toString());

Date date = new Date();
DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, locale);

String formattedDate = dateFormat.format(date);

model.addAttribute("serverTime", formattedDate);

return "home";

}

home.jsp :

<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ taglib uri="http://www.springframework.org/tags/form" prefix="form"%>
<%@ page session="false"%>
<html>
<head>
<title>Home</title>
</head>
<body>
	<h1>Hello world!</h1>

	<P>The time on the server is ${serverTime}.</P>

</body>
</html>

Nous pourrions nous arrêter sur ce bel écran et atteindre l’illumination, mais continuons d’avancer vers le but que nous nous sommes fixés et ajoutons un formulaire pour créer une partie. Ce formulaire sera très simple et n’aura comme choix que le nombre de bots de la partie.

Initialisation du formulaire :

@ModelAttribute("partieForm")
  public PartieForm initPartieForm(){
	  PartieForm partieForm = new PartieForm();
	  partieForm.setNombreBots(4);
	  return partieForm;
  }

@RequestMapping(method=RequestMethod.POST)
  public ModelAndView submitForm(@ModelAttribute("partieForm") PartieForm partieForm, Model model) {
    Partie partie = new Partie();
    partieManager.initPartie(partie, partieForm.getNombreBots());
    model.addAttribute("partie", partie);
    for (Joueur joueur : partie.getListeJoueurs()) {
      System.out.println("joueur " + joueur.getMerveille().getNomEn());
    }
    return new ModelAndView("partie", "partie", partie);
  }

On fait un tas de belles jsp avec des beaux css pour faire du bel affichage, et voilà ce qui arrive quand on n’est pas une brute en design et qu’on en fait quand même :

Bon, c’est pas encore bon, mais intéressons nous plutôt à l’erreur « fichier ayant une section mappée utilisateur , elle provient de l’utilisation de jetty et empêche la modification de fichiers utilisés par jetty. La solution a été trouvée ici : http://blog.xebia.fr/2009/06/10/maven-jetty-plugin-et-section-mappee/ et en rajoutant la ligne du nouveau connector, le problème disparait.

Ensuite on code un paquet de trucs. Oui, en langage littéraire ça s’appelle une ellipse, mais c’est ce qui arrive quand on code plus vite qu’on n’écrit ses articles sur le blog. Bref :

Création du service de bot qui sert à choisir une action pour les places gérées par l’ordinateur

Du javascript jsp et css pour mettre en forme l’interface ; sur ce point je rame un peu, difficile de faire tenir 7 merveilles + une dizaine de cartes sur un écran (de surcroît quand c’est un portable 12 pouces) ainsi que le formulaire pour jouer une action. Je devrais probablement n’afficher qu’un résumé des infos des joueurs qui ne sont pas soi / voisin droite / voisin gauche, avec un popup pour leur jeu entier, mais ça nécessite pas mal de temps à creuser les css div html toussa toussa… Bref pour l’instant tout est en une page, et passablement encore hideux. Et voici :

Ah suite : un bon tuto pour débugguer du jetty : http://karamimed.developpez.com/tutoriels/java/web/eclipse/maven-jetty/debug/