Article de blog ippon

Préview de mon article sur une directive de carte angularJS que je vais bien réussir à publier sur le blog d’ippon… En cours de soumission, c’est déjà ça.

Développement d’une carte sous Angular JS.

En quelques années AngularJS est devenu le framework javascript de référence. L’apprentissage de ce framework est rapide, mais la création de nouveaux composants via des directives est plus complexe.

Nous allons voir comment développer un composant de favoris sur les villes et départements de France en directive Angular. L’utilisateur doit pouvoir sélectionner des départements sur une carte de France puis sélectionner des villes dans ceux-ci pour les ajouter à une liste de favoris.

Quelques considérations spécifiques au projet : le client travaille uniquement en France métropolitaine et considère la Corse comme un seul département (toutes mes excuses).

De plus, certains départements n’ont aucune ville à mettre en favori, et doivent donc être cachés.

Tags : javascript, angularjs, directive, scope, twitter bootstrap, modale, angular-ui, Rest

La structure de données :

Nos données de départ sont fournies via un service REST sous forme de liste de départements avec leurs villes. Un champ “favori” booléen indique la présence ou l’absence de la ville dans les favoris, et un département est donc favori si au moins une de ses villes est favorite :

  1. [
  2. { nom : « Calvados »,
  3.  code : « 14 »,
  4.  villes : [
  5. { nom : « Caen »,
  6.  code : « 14000 »,
  7.  favori : true
  8.        },
  9. // … autres villes du département
  10. ],
  11.  // … autres départements

}


Le but est de présenter de manière visuelle simple cette structure au client.

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…