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…

Publicités

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s