Google App Engine pour Java

A propos de cette série

Vous souvenez-vous de l'époque où Google App Engine était réservé aux Pythonistes ? Il s'agissait là d'une époque bien sombre. Google Inc. a ouvert sa plateforme de cloud-computing aux développeurs Java en avril 2009. Dans cette série de trois articles, Rick Hightower, qui est à la fois auteur et formateur sur Java, vous introduit à cette plateforme de développement Java fiable, robuste et amusante. Dans cet article, vous acquerrez une idée de la raison pour laquelle Google App Engine pour Java pourrait être la plateforme de déploiement pour votre prochaine killer-app qui passe si bien à l'échelle. Puis vous commencerez à utiliser les plugin Google pour Eclipse pour développer deux applications d'exemple: la première basée sur Google Web Toolkit (GWT) et la seconde sur l'API Servlet de Java. Vous apprendrez quelle est la valeur ajoutée de Google App Engine pour Java, à la fois pour développer une application à partir de zéro et pour la mettre à disposition de ses cinq millions d'utilisateurs. (Et c'est juste la version gratuite).

Montons en régime !

Article d'origine: 

Construire des killer-apps qui passent à l'échelle avec App Engine pour Java

Quand une idée vous démange, vous avez besoin de la gratter pour vous sentir mieux. En tant que développeurs, nous passons beaucoup de temps à trouver des idées pour différentes sortes d'applications. C'est amusant, n'est-ce-pas ? Comprendre comment réaliser un produit logiciel représente un défi intéressant. Imaginer quelque chose puis le créer apporte une certaine satisfaction. L'alternative (une démangeaison qu'on ne gratte pas) n'amène que de la frustration.

Une des raisons pour lesquelles beaucoup d'applications ne voient jamais le jour est le besoin d'une infrastructure. Une infrastructure bien maintenue implique généralement une équipe d'administrateurs systèmes, de DBAs et d'ingénieurs réseau, ce qui jusqu'à récemment était réservé aux plus fortunés. Même payer un tiers pour héberger votre application n'est pas sans risque: qu'arrivera-t-il si la popularité de l'application subit un coup d'accélérateur et qu'elle reçoit soudainement de nombreuses visites ? Ce qu'on appelle communément l'effet Slashdot (ndT: j'imagine que le nom vient du célèbre site Slashdot) peut enterrer une bonne idée, tout simplement parce qu'il est difficile de prédire les pics de charge.

Mais, comme nous le savons tous, les choses changent. L'approche des services Web a évolué, et aujourd'hui nous avons les moyens, via le cloud computing et son cousin plus costaud Platform As A Service (PAAS) de construire, déployer et distribuer des applications plus facilement. Désormais, quand vous écrivez le prochain Twitter et que vous le déployez sur une plateforme cloud, il passera à l'échelle comme une lettre à la poste. Waou, comme c'est agréable !

Dans cette série de trois articles, vous apprendrez par la pratique pourquoi le cloud computing et PAAS sont des sauts évolutifs si importants dans le développement logiciel, et en parallèle vous commencerez à utiliser une nouvelle plateforme assez excitante pour développer en Java: Google App Engine pour Java, qui est actuellement disponible en version alpha (ndT: ce n'est plus le cas à l'heure actuelle !). Je commencerai par un survol d'App Engine pour Java, avec les différents types de services applicatifs qu'il propose. Ensuite vous plongerez directement dans une application d'exemple - la première des deux que nous verrons - en utilisant le plugin Eclipse fourni par Google. La première application se basera sur le support de l'API Servlet par App Engine pour Java, et la seconde sur le support de GWT. Dans le deuxième article, vous créerez une petite application de gestion de contacts en utilisant les mêmes technologies. Et dans la dernière partie, vous vous servirez de votre application pour explorer le support de la persistence Java par App Engine, qui est fondé sur les Java Data Objects (JDO) et la Java Persistence API (JPA).

Maintenant, assez parlé: montons en régime !

A propos de Google App Engine pour Java

Google (qui est également à l'origine d'un moteur de recherche, je crois) a mis à disposition la première version Google App Engine en avril 2008. Au grand désappointement de beaucoup de développeurs Java, cette version initiale était réservée aux programmeurs Python - des gens qui pensent que les espaces sont là pour délimiter des blocs ! (Je le sais parce que j'ai écris un livre sur Python). Google a répondu à la pression populaire en sortant Google App Engine pour Java en avril 2009.

Google App Engine pour Java est une solution complète pour le développement Java d'entreprise: une IHM Ajax dans un navigateur pour la facilité d'utilisation, le support des outils Eclipse, et Google App Engine en bout de chaine. La facilité d'utilisation et l'outillage sont les points forts de Google App Engine pour Java par rapport à d'autres solutions de cloud computing.

Le développement d'applications dans App Engine pour Java implique l'utilisation des ressources de Google pour stocker et retrouver les objets Java. Le stockage des données est basé sur BigTable, mais les intarfaces JDO et JPA permettent d'écrire du code qui n'est pas directement lié à Big Table. En fait, Google fournit un support pour bon nombre d'APIs standards afin que vous puissiez écrire du code qui n'est pas lié à 100% à la plateforme App Engine pour Java.

App Engine pour Java repose sur les APIs Java standards qui suivent:

De plus, App Engine pour Java fournit un support pour les services applicatifs suivants:

L'import/export de données est important pour importer des données provenant d'autres sources dans votre application App Engine. C'est aussi un moyen supplémentaire de n'être pas lié à App Engine. Le support de CRON par Google est basé sur une URL interne qui est invoquée à des moments spécifiés, ce qui en fait un service intéressant qui n'a pas grand chose à voir avec App Engine. Le mécanisme d'authentification et d'autorisation est spécifique à App Engine pour Java, mais on pourrait écrire un ServletFilter, un aspect ou un plugin Spring Security pour minimiser ce couplage fort.

Créez votre première application App Engine pour Java

Si vous avez lu jusqu'à ce point, vous êtes prêt à commencer à construire votre première application App Engine pour Java. La première étape est d'installer le plugin Google App Engine pour Eclipse (ndT: si vous utilisez un autre environnement de développement, Google fournit également des scripts Ant permettant d'effectuer les mêmes opérations que le plugin); une fois cela fait, vous pouvez passer à la suite.

Ouvrez l'IDE Eclipse et vous verrez trois nouveaux boutons à côté du bouton Imprimer: un G dans une balle bleue, un G dans une boite à outils rouge, et un avion à réaction en miniature, comme dans la Figure 1:

Figure 1. De tous nouveaux boutons dans votre IDE Eclipse

Icônes du plugin Google App Engine

Voici ce que font ces boutons:

Vous utiliserez l'assistant de création de projet pour créer deux nouveaux projets: le premier basé sur des servlets et le second sur GWT. Vous vous servirez de la boite à outils pour compiler le projet GWT. Vous appuierez sur l'avion quand vous voudrez déployer le projet sur App Engine, le rendant ainsi accessible.

Maintenant, commencez en créant un projet App Engine pour Java. Tout d'abord, cliquez sur la balle bleue pour accéder à l'assistant de création de projet. Puis créez une application nommée SimpleServletApp et se trouvant dans le package gaej.example, comme le montre la Figure 2:

Figure 2. Démarrage d'un nouveau projet

Démarrage d'un nouveau projet

Notez que le support de GWT n'est pas activé pour ce premier exemple. A la fin de cette étape, l'assistant créera une application simple contenant une servlet de type Hello World. La figure 3 montre une copie d'écran du projet (ndT: j'ai gardé les copies d'écran de l'article d'origine, le résultat que l'on obtient maintenant peut être légèrement différent sur certains écrans, dont celui-là):

Figure 3. Le projet SimpleServletApp

Démarrage d'un nouveau projet

Remarquez les fichiers JAR qui sont automatiquement inclus dans ce nouveau projet basé sur des servlets:

Vous apprendrez comment utiliser les APIs de persistence de l'App Engine, ainsi que quelques-uns des services applicatifs qu'il fournit, dans la seconde partie de cette série.

Notez également l'existence du fichier de configuration du conteneur d'exécution de Google App Engine, nommé appengine.xml. Dans cet exemple, il est utilisé pour configurer l'emploi du fichier logging.properties qui paramètre la manière dont App Engine va produire les logs.

Premier aperçu d'une application App Engine de type servlet

Une fois que vous avez tout configuré dans l'assistant de création de projet, App Engine vous présente un squelette d'application avec une servlet Hello World. Regardez le code et essayez de lancer l'application en utilisant les outils Eclipse pour App Engine. Le point d'entrée de cette application est SimpleServletAppServlet, que voici dans le Listing 1:

Listing 1. SimpleServletAppServlet

package gaej.example;

import java.io.IOException;
import javax.servlet.http.*;

@SuppressWarnings("serial")
public class SimpleServletAppServlet extends HttpServlet {
    public void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {
        resp.setContentType("text/plain");
        resp.getWriter().println("Hello, world");
    }
}

La servlet est attachée à l'URI /simpleservletapp dans le fichier, comme le montre le Listing 2:

Listing 2. web.xml

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app xmlns="http://java.sun.com/xml/ns/javaee" version="2.5">
    <servlet>
        <servlet-name>simpleservletapp</servlet-name>
        <servlet-class>gaej.example.SimpleServletAppServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>simpleservletapp</servlet-name>
        <url-pattern>/simpleservletapp</url-pattern>
    </servlet-mapping>
    <welcome-file-list>
        <welcome-file>index.html</welcome-file>
    </welcome-file-list>
</web-app>

L'assistant de création de projet produit également un fichier index.html qui possède un lien vers la nouvelle servlet:

Listing 3. index.html généré par l'assistant de création de projet

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<!-- The HTML 4.01 Transitional DOCTYPE declaration-->
<!-- above set at the top of the file will set     -->
<!-- the browser's rendering engine into           -->
<!-- "Quirks Mode". Replacing this declaration     -->
<!-- with a "Standards Mode" doctype is supported, -->
<!-- but may lead to some differences in layout.   -->

<html>
  <head>
    <meta http-equiv="content-type" content="text/html; charset=UTF-8">
    
    <!--                                           -->
    <!-- Any title is fine                         -->
    <!--                                           -->
    <title>Hello App Engine</title>
  </head>

  <!--                                           -->
  <!-- The body can have arbitrary html, or      -->
  <!-- you can leave the body empty if you want  -->
  <!-- to create a completely dynamic UI.        -->
  <!--                                           -->
  <body>
    <h1>Hello App Engine!</h1>
    
    <table>
      <tr>
        <td colspan="2" style="font-weight:bold;">Available Servlets:</td>        
      </tr>
      <tr>
        <td><a href="simpleservletapp"/>SimpleServletAppServlet</td>
      </tr>
    </table>
  </body>
</html>

On a maintenant une application simple basée sur des servlets, utilisant seulement quelques APIs Java. Et c'est ce qui est important: App Engine pour Java offre d'accéder aux fonctionnalités proposées par l'App Engine au moyen d'APIs standard, ce qui permet de profiter de la vitalité des frameworks disponible sur la plateforme Java.

Qu'est-ce qui marche également avec App Engine pour Java?

Google maintient une liste des outils et des frameworks qui merchent bien avec l'App Engine pour Java (voir les Ressources). Par exemple, App Engine pour Java supporte de nombreux langages tournant sur la JVM, comme BeanShell, Groovy, Scala, JRuby, Jython, et Rhino. Du fait que l'App Engine supporte un bon nombre d'APIs Java SE et Java EE - comme les Servlets, JSP, JPA, JavaMail et JAXP (Java API for XML Processing) - beaucoup de frameworks fonctionnent sans problème. Par exemple, vous pouvez utiliser le framework Spring, bien que certaines adaptations soient nécessaires pour Spring ORM. Tapestry, Wicket, DWR, Tiles, SiteMesh, et Grails fonctionnent également. Struts 2 fonctionne avec un petit correctif. Mais d'autres frameworks et APIs tels que Hibernate et JDBC (les bases de données relationnelles ne sont pas supportées), JMX, les Web Services, JAX-RPC et JAX-WS, JCA, JNDI, JMS, EJB, et RMI ne marcheront pas.

Déploiement de l'application

Pour exécuter votre application avec l'outillage Eclipse, faites d'abord un clic droit sur le projet et sélectionnez le menu "Run As", puis choisissez "Web Application" avec la balle bleue, comme dans la Figure 4:

Figure 3. Exécution sur le serveur de développement App Engine pour Java

Exécution sur le serveur de développement App Engine pour Java

Vous devriez maintenant pouvoir accéder à l'adresse http://localhost:8080/simpleservletapp dans votre navigateur et voir l'application afficher le message Hello World.

Création d'une application GWT

Vous avez maintenant une idée du fonctionnement d'une application de type servlet dans l'App Engine pour Java, explorons donc l'outillage Eclipse pour les applications GWT. Commencez en cliquant la balle bleue dans la barre d'outil Eclipse pour lancer l'assistant de création de projet Google. Cette fois, sélectionnez le support de GWT, comme le montre la Figure 5:

Figure 5. Création d'une application GWT simple avec l'assistant de création de projet

Création d'une application GWT simple avec l'assistant de création de projet

Comme vous pouvez le voir dans la Figure 6, App Engine pour Java incorpore beaucoup plus d'artefacts dans une application GWT que dans une application de type servlet. L'application d'exemple est une IHM faite en GWT qui dialogue avec un service applicatif de salutation.

Figure 6. Artefacts de code fournis pour une application GWT

Artefacts de code fournis pour une application GWT

Il y a un JAR supplémentaire pour l'application GWT qui n'existait pas pour celle basée sur les servlets, gwt-servlet.jar.

Les autres artefacts sont les suivants:

Avant que vous n'examiniez l'architecture de l'application et le code source, regardez ce qui se passe quand on exécute l'application. Pour lancer l'application, cliquez sur la boite à outils rouges de la barre d'outils, puis cliquez sur le bouton Compile. Maintenant faites un clic droit sur le projet  et sélectionnez le menu Run As—> Web Application comme précédemment. Cette fois, comme vous travaillez sur une application GWT, vous accéderez à l'application à l'adresse http://127.0.0.1:8888/SimpleGWTApp.html (ndT: j'ai modifié l'URL et zappé la phrase suivante de l'article d'origine. Au moment de l'écriture de l'article, le client GWT ne s'exécutait pas dans un simple navigateur, comme vous le voyez sur la copie d'écran suivante. Mais fondamentalement le résultat est le même.) Allez-y et saisissez votre nom dans l'application, et observez la réponse. J'ai reçu la réponse de la Figure 7:

Figure 7. Exécution de l'application GWT de démonstration

Exécution de l'application GWT de démonstration

Dans les sections suivantes, nous parcourerons l'application de démonstration GWT. Si vous voulez en savoir plus sur GWT (ou suivre un tutoriel GWT), voir les Ressources.

Dans l'application GWT

Sur la base de la configuration fournie, l'outillage GWT d'Eclipse crée un embryon d'application contenant une page HTML (SimpleGWTApp.html, figurant dans le Listing 10) qui charge simplegwtapp.js et simplegwtapp.nocache.js. Ce code JavaScript est généré par GWT à partir de votre code Java, dans notre cas celui-ci se trouve dans le répertoire src et le package gaej.example.client (voir les Listings 6, 7 et 8).

Le point d'entrée principal pour la création de l'IHM est gaej.example.client.SimpleGWTApp, que l'on voit dans le Listing 8. Cette classe crée les éléments d'IHM GWT et les associe aux éléments du DOM HTML de SimpleGWTApp.html (voir le Listing 10). La page SimpleGWTApp.html définit deux éléments DOM nommés nameFieldContainer et sendButtonContainer (des colonnes d'une table). La classe SimpleGWTApp utilise RootPanel.get("nameFieldContainer") pour accéder au panneau associé à ces éléments DOM et les remplacer par des éléments d'IHM. La classe définit ensuite un champ texte et un bouton, que vous utilisez pour saisir un nom et l'envoyer au service de salutation (voir le Listing 10).

GWT sait que la classe SimpleGWTApp est le point d'entrée de l'application parce que SimpleGWTApp.gwt.xml le spécifie comme tel a l'aide de l'élément entry-point.

SimpleGWTApp cable le bouton, nommé sendButton, de telle manière que quand on clique sur lui SimpleGWTApp invoque la méthode greetServer de GreetingService. L'interface GreetingService est définie dans src/gaej.example.client.GreetingService.java (Listing 6).

Comme Ajax est intrinsèquement asynchrone, GWT définit une interface asynchrone pour accéder aux services distants. L'application SimpleGWTApp utilise l'interface asynchrone définie dans src/gaej.example.client.GreetingServiceAsync.java (Listing 7). GreetingServiceImpl (src/gaej.example.server.GreetingServiceImpl.java) implémente la méthode greetServer définie par GreetingService (Listing 5). La méthode GreetingServiceImpl.greetServer retourne une String qui est utilisée par SimpleGWTApp pour afficher le message d'accueil dans la boite de dialogue qu'elle crée.

Le descripteur de module GWT déclare le point d'entrée pour l'IHM de l'application, c'est-à-dire gaej.example.client.SimpleGWTApp, comme dans le Listing 4:

Listing 4. Descripteur de module GWT (src/gaej/example/SimpleGWTApp.gwt.xml)

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE module PUBLIC "-//Google Inc.//DTD Google Web Toolkit 1.6.4//EN"

"http://google-web-toolkit.googlecode.com/svn/tags/1.6.4/
  distro-source/core/src/gwt-module.dtd">


<module rename-to='simplegwtapp'>
  <!-- Inherit the core Web Toolkit stuff.                        -->
  <inherits name='com.google.gwt.user.User'/>

  <!-- Inherit the default GWT style sheet.  You can change       -->
  <!-- the theme of your GWT application by uncommenting          -->
  <!-- any one of the following lines.                            -->
  <inherits name='com.google.gwt.user.theme.standard.Standard'/>
  <!-- <inherits name='com.google.gwt.user.theme.chrome.Chrome'/> -->
  <!-- <inherits name='com.google.gwt.user.theme.dark.Dark'/>     -->

  <!-- Other module inherits                                      -->

  <!-- Specify the app entry point class.                         -->
  <entry-point class='gaej.example.client.SimpleGWTApp'/>
</module>

GreetingServiceImpl est l'implémentation du service de salutation, que vous pouvez voir dans le Listing 5. Il s'exécute côté serveur et le code client l'appelle via un appel de procédure distante.

Listing 5. Implémentation du service de salutation (src/gaej.example.server.GreetingServiceImpl.java)

package gaej.example.server;

import gaej.example.client.GreetingService;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;

/**
 * The server side implementation of the RPC service.
 */
@SuppressWarnings("serial")
public class GreetingServiceImpl extends RemoteServiceServlet implements
        GreetingService {

    public String greetServer(String input) {
        String serverInfo = getServletContext().getServerInfo();
        String userAgent = getThreadLocalRequest().getHeader("User-Agent");
        return "Hello, " + input + "!<br><br>I am running " + serverInfo
                + ".<br><br>It looks like you are using:<br>" + userAgent;
    }
}

GreetingService (Listing 6) est l'interface utilisée par le code client pour effectuer l'appel distant:

Listing 6. API synchrone (src/gaej.example.client.GreetingService.java)

package gaej.example.client;

import com.google.gwt.user.client.rpc.RemoteService;
import com.google.gwt.user.client.rpc.RemoteServiceRelativePath;

/**
 * The client side stub for the RPC service.
 */
@RemoteServiceRelativePath("greet")
public interface GreetingService extends RemoteService {
    String greetServer(String name);
}

GreetingServiceAsync est l'interface qui sera réellement utilisée par le code client (Listing 7). Chaque méthode fournit un objet de callback pour être notifié de façon asynchrone quand l'appel de procédure distante est terminé. Derrière le rideau, GWT utilise Ajax. Quand le client utilise Ajax, il est préférable de ne pas bloquer le client, d'où les appels asynchrones. Le blocage du client serait aller à contresens de la finalité d'Ajax.

Listing 7. API asynchrone (src/gaej.example.client.GreetingServiceAsync.java)

package gaej.example.client;

import com.google.gwt.user.client.rpc.AsyncCallback;

/**
 * The async counterpart of <code>GreetingService</code>.
 */
public interface GreetingServiceAsync {
    void greetServer(String input, AsyncCallback<String> callback);
}

SimpleGWTApp est là où le principal se passe. Il écoute les événements IHM, puis envoie des requêtes Ajax à GreetingService.

Listing 8. Le point d'entrée de l'application construit aussi l'IHM (src/gaej.example.client.SimpleGWTApp.java)

package gaej.example.client;

import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.core.client.GWT;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyUpEvent;
import com.google.gwt.event.dom.client.KeyUpHandler;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.DialogBox;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.TextBox;
import com.google.gwt.user.client.ui.VerticalPanel;

/**
 * Les classes servant de point d'entrée définit <code>onModuleLoad()</code>.
 */
public class SimpleGWTApp implements EntryPoint {
    /**
     * Message affiché à l'utilisateur quand le serveur ne peut pas être atteint
     * ou retourne une erreur.
     */
    private static final String SERVER_ERROR = "An error occurred while "
            + "attempting to contact the server. Please check your network "
            + "connection and try again.";

    /**
     * Crée un proxy du service distant pour parler au service de salutation côté serveur.
     */
    private final GreetingServiceAsync greetingService = GWT
            .create(GreetingService.class);

    /**
     * C'est la méthode servant de point d'entrée.
     */
    public void onModuleLoad() {
        final Button sendButton = new Button("Send");
        final TextBox nameField = new TextBox();
        nameField.setText("GWT User");

        // Vous pouvez ajouter des noms de style aux widgets
        sendButton.addStyleName("sendButton");

        // Ajoute nameField et sendButton à RootPanel
        // Utilisez RootPanel.get() pour obtenir l'élément body
        RootPanel.get("nameFieldContainer").add(nameField);
        RootPanel.get("sendButtonContainer").add(sendButton);

        // Donne le focus au champ nom quand l'application démarre
        nameField.setFocus(true);
        nameField.selectAll();

        // Crée la boite de dialogue
        final DialogBox dialogBox = new DialogBox();
        dialogBox.setText("Remote Procedure Call");
        dialogBox.setAnimationEnabled(true);
        final Button closeButton = new Button("Close");
        // Vous pouvez positionner l'id d'un widget en accédant à son Element
        closeButton.getElement().setId("closeButton");
        final Label textToServerLabel = new Label();
        final HTML serverResponseLabel = new HTML();
        VerticalPanel dialogVPanel = new VerticalPanel();
        dialogVPanel.addStyleName("dialogVPanel");
        dialogVPanel.add(new HTML("<b>Sending name to the server:</b>"));
        dialogVPanel.add(textToServerLabel);
        dialogVPanel.add(new HTML("<br><b>Server replies:</b>"));
        dialogVPanel.add(serverResponseLabel);
        dialogVPanel.setHorizontalAlignment(VerticalPanel.ALIGN_RIGHT);
        dialogVPanel.add(closeButton);
        dialogBox.setWidget(dialogVPanel);

        // Ajoute un gestionnaire pour fermer la DialogBox
        closeButton.addClickHandler(new ClickHandler() {
            public void onClick(ClickEvent event) {
                dialogBox.hide();
                sendButton.setEnabled(true);
                sendButton.setFocus(true);
            }
        });

        // Crée un gestionnaire pour sendButton et nameField
        class MyHandler implements ClickHandler, KeyUpHandler {
            /**
             * Se déclenche quand l'utilisateur clique sendButton.
             */
            public void onClick(ClickEvent event) {
                sendNameToServer();
            }

            /**
             * Se déclenche quand l'utilisateur saisit dans nameField.
             */
            public void onKeyUp(KeyUpEvent event) {
                if (event.getNativeKeyCode() == KeyCodes.KEY_ENTER) {
                    sendNameToServer();
                }
            }

            /**
             * Envoie le nom saisi dans nameField au serveur et attend une réponse.
             */
            private void sendNameToServer() {
                sendButton.setEnabled(false);
                String textToServer = nameField.getText();
                textToServerLabel.setText(textToServer);
                serverResponseLabel.setText("");
                greetingService.greetServer(textToServer,
                        new AsyncCallback<String>() {
                            public void onFailure(Throwable caught) {
                                // Affiche le message d'erreur RPC
                                dialogBox
                                        .setText("Remote Procedure Call - Failure");
                                serverResponseLabel
                                        .addStyleName("serverResponseLabelError");
                                serverResponseLabel.setHTML(SERVER_ERROR);
                                dialogBox.center();
                                closeButton.setFocus(true);
                            }

                            public void onSuccess(String result) {
                                dialogBox.setText("Remote Procedure Call");
                                serverResponseLabel
                                        .removeStyleName("serverResponseLabelError");
                                serverResponseLabel.setHTML(result);
                                dialogBox.center();
                                closeButton.setFocus(true);
                            }
                        });
            }
        }

        // Ajoute un gestionnaire pour envoyer le nom au serveur
        MyHandler handler = new MyHandler();
        sendButton.addClickHandler(handler);
        nameField.addKeyUpHandler(handler);
    }
}

Le  descripteur de déploiement de l'application Web (web.xml, voir Listing 9), expose en tant que ressource Web de type servlet. La servlet GreetingService y est associée à l'adresse /simplegwtapp/greet pour que SimpleGWTApp puisse la charger et l'appeler. Le descripteur de déploiement indique également que la page d'accueil de l'application est SimpleGWTApp.html, afin qu'elle soit toujours chargée.

Listing 9. Descripteur de déploiement qui configure GreetingServiceImpl (war/WEB-INF/web.xml)

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE web-app
    PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
    "http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app>

  <!-- Page par défaut -->
  <welcome-file-list>
    <welcome-file>SimpleGWTApp.html</welcome-file>
  </welcome-file-list>
 
  <!-- Servlets -->
  <servlet>
    <servlet-name>greetServlet</servlet-name>
    <servlet-class>gaej.example.server.GreetingServiceImpl</servlet-class>
  </servlet>
 
  <servlet-mapping>
    <servlet-name>greetServlet</servlet-name>
    <url-pattern>/simplegwtapp/greet</url-pattern>
  </servlet-mapping>

</web-app>

Le frontal HTML est SimpleGWTApp.html (Listing 10). C'est la page qui charge simplegwtapp.js et simplegwtapp.nocache.js, le code JavaScript généré par GWT à partir de votre ccode Java. Comme mentionné précédemment, on peut retrouver ce code dans le répertoire src et dans le package gaej.example.client (Listings 6, 7 et 8).

Listing 10. Page HTML qui affiche l'IHM GWT (war/SimpleGWTApp.html)

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<!-- The HTML 4.01 Transitional DOCTYPE declaration-->
<!-- above set at the top of the file will set     -->
<!-- the browser's rendering engine into           -->
<!-- "Quirks Mode". Replacing this declaration     -->
<!-- with a "Standards Mode" doctype is supported, -->
<!-- but may lead to some differences in layout.   -->

<html>
  <head>
    <meta http-equiv="content-type" content="text/html; charset=UTF-8">

    <!--                                                               -->
    <!-- Consider inlining CSS to reduce the number of requested files -->
    <!--                                                               -->
    <link type="text/css" rel="stylesheet" href="SimpleGWTApp.css">

    <!--                                           -->
    <!-- Any title is fine                         -->
    <!--                                           -->
    <title>Web Application Starter Project</title>
    
    <!--                                           -->
    <!-- This script loads your compiled module.   -->
    <!-- If you add any GWT meta tags, they must   -->
    <!-- be added before this line.                -->
    <!--                                           -->
    <script type="text/javascript" language="javascript" 
  src="simplegwtapp/simplegwtapp.nocache.js"></script>
  </head>

  <!--                                           -->
  <!-- The body can have arbitrary html, or      -->
  <!-- you can leave the body empty if you want  -->
  <!-- to create a completely dynamic UI.        -->
  <!--                                           -->
  <body>

    <!-- OPTIONAL: include this if you want history support -->
    <iframe src="javascript:''" id="__gwt_historyFrame" tabIndex='-1' 
  style="position:absolute;width:0;height:0;border:0"></iframe>

    <h1>Web Application Starter Project</h1>

    <table align="center">
      <tr>
        <td colspan="2" style="font-weight:bold;">Please enter your name:</td> 
      </tr>
      <tr>
        <td id="nameFieldContainer"></td>
        <td id="sendButtonContainer"></td>
      </tr>
    </table>
  </body>
</html>

Avec GWT, vous contrôlez le look and feel de l'appliction au moyen de CSS, comme le montre le Listing 11:

Listing 11. Feuille de style de l'IHM clGWT (war/SimpleGWTApp.css)

/** Add css rules here for your application. */

/** Example rules used by the template application (remove for your app) */
h1 {
  font-size: 2em;
  font-weight: bold;
  color: #777777;
  margin: 40px 0px 70px;
  text-align: center;
}

.sendButton {
  display: block;
  font-size: 16pt;
}

/** Most GWT widgets already have a style name defined */
.gwt-DialogBox {
  width: 400px;
}

.dialogVPanel {
  margin: 5px;
}

.serverResponseLabelError {
  color: red;
}

/** Set ids using widget.getElement().setId("idOfElement") */
#closeButton {
  margin: 15px 6px 6px;
}

Déploiement sur Google App Engine

Une fois que vous aurez créé la prochaine killer app (parce qu'on a vraiment besoin d'une application de salutation conviviale), vous voudrez la déployer. La finalité de l'utilisation de Google App Engine est de pouvoir déployer l'application sur l'infrastructure de Google, solide et qui facilite le passage à l'échelle. Google App Engine est conçu pour fournir une plateforme permettant de construire des applications qui passent à l'échelle, "pouvant passer de de un à plusieurs millions d'utilisateurs sans soucis d'infrastructure" (comme indiqué sur la page d'accueil de l'App Engine). Pour utiliser cette infrastructure, vous avez besoin d'un compte Google App Engine pour Java.

Comme beaucoup de choses ici bas, au départ c'est gratuit. La version gratuite de l'App Engine pour Java fournit à l'application déployée assez de CPU, de bande passante et d'espace de stockage pour servir environ 5 millions de pages par mois. Au delà, vous payez ce que vous dépensez. (Gardez présent à l'esprit que c'est ce qui est disponible pour la préversion d'App Engine pour Java (ndT: a priori au moment de la traduction les conditions n'ont pas changé)).

Une fois que vous avez un compte, vous devriez voir une liste vide d'applications sur le site d'App Engine pour Java. Cliquez le bouton Create New Application et un formulaire comme celui de la Figure 8 devrait apparaitre. Entrez un nom d'application qui soit unique et une description, après quoi vous verrez un message de confirmation avec votre identifiant d'application.

L'identifiant figure également dans le fichier app.yaml de votre application (ndT: serait-ce plutôt appengine-web.xml ?). Notez que cet identifiant ne peut pas être modifié. Si vous utilisez l'authentification de Google pour votre application, "GAEj Article For Rick Part 1" sera affiché dans les pages d'authentification pour l'accès à l'application. Vous utiliserez gaejarticleforrick pour déployer l'application sur l'App Engine avec le plugin App Engine d'Eclipse.

Figure 8. Création d'une nouvelle application sur l'App Engine pour Java

Création d'une nouvelle application sur l'App Engine pour Java

Une fois que vous avez paramétré l'ID d'application, vous pouvez déployer votre application depuis Eclipse. D'abord, appuyez sur le bouton de la barre d'outils qui ressemble au logo Google App Engine (un avion à réaction), comme dans la Figure 9:

Figure 9. Plugin Eclipse d'App Engine pour Java

Plugin Eclipse d'App Engine pour Java

Assurez-vous d'avoir sélectionné votre projet App Engine pour Java avant de cliquer sur Deploy dans la boite de dialogue de la Figure 10. On vous demandera vos identifiants Google, qui sont votre adresse mail et votre nom d'utilisateur.

Figure 10. Déploiement d'une nouvelle application sur l'App Engine pour Java

Déploiement d'une nouvelle application sur l'App Engine pour Java

La boite de dialogue de la Figure 10 a un lien vers "App Engine Project setting". Cliquez ce lien (qui est aussi accessible dans le fichier de propriétés du projet) et entre l'ID d'application (ici gaejarticleforrick), comme dans la Figure 11. Cliquez ensuite OK, puis Deploy.

Figure 11. Propriétés du projet Google App Engine

Propriétés du projet Google App Engine

Une fois votre application déployée, elle sera disponible à l'adresse http://<application id>.appspot.com/. Vous pouvez voir cette application tourner à http://gaejarticleforrick.appspot.com/.

Conclusion

Ceci conclut la première partie de mon introduction à Google App Engine pour Java. Pour l'instant, vous avez une vue générale de ce que propose l'App Engine pour Java et vous avez fait les premiers pas pour ce qui est d'utiliser le plugin App Engine d'Eclipse. Vous avez créé deux petites applications de test (une à base de servlets et l'autre utilisant GWT) et vous avez déployé l'application GWT sur la plateforme Google App Engine.

Les exemples utilisés jusque là ont concerné l'outillage et les fonctionnalités qui facilitent la création et le déploiement d'applications java qui passent à l'échelle - potentiellement on peut même atteindre la taille de YouTube ou Facebook. Dans la seconde partie, vous continuerez à explorer les possibilités offertes aux développeurs Java qui travaillent sur l'App Engine. Pour aller plus loin que les applications de démonstration de cet article, vous construirez une application de gestion de contacts. Cette application sera aussi l'élément central des exercices de la troisième partie, qui étudiera le magasin de données de l'App Engine.

Développer la killer app

Article d'origine: 

Développez votre propre application de gestion de contacts avec App Engine

Résumé: 

La finalité d'une plateforme dans le cloud comme Google App Engine for Java™ est de permettre d'imaginer, développer et déployer des applications de qualité professionnelle qui passent à l'échelle - sans pour autant casser votre tirelire ou devenir fou. Dans la seconde partie de cette introduction en trois parties à Google App Engine pour Java, Rick Hightower va plus loin que les exemples prêts à l'emploi de la première partie, et vous guidera pas à pas dans l'écriture et le déploiement d'une application simple de gestion de contacts utilisant l'App Engine pour Java.

Dans la première partie de cette introduction à la construction d'applications Java passant à l'échelle avec l'App Engine, vous avez pris connaissance de l'outillage Eclipse et de l'infrastructure de la plateforme de cloud computing de Google (ou PAAS) pour les développeurs Java. Les exemples de cet article étaient préfabriqués, pour que vous puissiez vous concentrer sur l'intégration de l'App Engine dans Eclipse et passer rapidement à la pratique du développement et du déploiement de différents types d'applications - à savoir une application utilisant Google Web Toolkit (GWT) et l'autre basée sur des servlets. Cet article bâtit sur ce fondement et prépare également aux exercices de programmation plus avancés de la troisième partie de la série.

L'application de gestion de contacts que vous développerez permet à l'utilisateur de stocker des informations basiques telles que le nom, l'adresse électronique, et le numéro de téléphone. Pour créer cette application, vous utiliserez l'assistant de création de projet GWT d'Eclipse.

Commençons par simuler, puis ayons de vrais contacts (ndT: tentative désespérée pour traduire le titre humoristique de l'original)

La première étape pour construire une nouvelle application pour l'App Engine, comme vous le savez maintenant, est de lancer l'assistant de création de projet dans Eclipse. Une fois que vous y êtes, choisissez d'utiliser GWT. (La première partie de cette série donne des informations détaillées sur la création d'un projet GWT pour Google App Engine).

Pour cet exercice, vous commencerez par une application qui simulera les opérations de base sur les contacts, nous les stockerons réellement plus tard. Pour l'instant donc, utilisons un objet d'accès aux données (DAO) qui aura une implémentation simulant nos opérations, comme le montre le Listing 1:

Listing 1. Interface ContactDAO

package gaej.example.contact.server;

import java.util.List;

import gaej.example.contact.client.Contact;

public interface ContactDAO {
    void addContact(Contact contact);
    void removeContact(Contact contact);
    void updateContact(Contact contact);
    List<Contact> listContacts();
}

ContactDAO définit des méthodes pour ajouter, supprimer, mettre à jour un contact et en retourner une liste complète. C'est une interface CRUD (ndT: voici une définition succincte) très basique qui permettra de gérer les contacts. La classe Contact est votre objet métier (Listing 2):

Listing 2. Objet métier Contact (gaej.example.contact.client.Contact)

package gaej.example.contact.client;

import java.io.Serializable;

public class Contact implements Serializable {
    
    private static final long serialVersionUID = 1L;
    private String name;
    private String email;
    private String phone;

    public Contact() {
        
    }
    
    public Contact(String name, String email, String phone) {
        super();
        this.name = name;
        this.email = email;
        this.phone = phone;
    }

    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getEmail() {
        return email;
    }
    public void setEmail(String email) {
        this.email = email;
    }
    public String getPhone() {
        return phone;
    }
    public void setPhone(String phone) {
        this.phone = phone;
    }
}

Pour cette première version de l'application, vous travaillerez avec un simulacre qui stocke les contacts dans une collection en mémoire, comme dans le Listing 3:

Listing 3. Classe DAO simulacre

package gaej.example.contact.server;

import gaej.example.contact.client.Contact;

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

public class ContactDAOMock implements ContactDAO {

    Map<String, Contact> map = new LinkedHashMap<String, Contact>();
    
    {
        map.put("rhightower@mammatus.com", 
          new Contact("Rick Hightower", "rhightower@mammatus.com", "520-555-1212"));
        map.put("scott@mammatus.com", 
          new Contact("Scott Fauerbach", "scott@mammatus.com", "520-555-1213"));
        map.put("bob@mammatus.com", 
          new Contact("Bob Dean", "bob@mammatus.com", "520-555-1214"));

    }
   
    public void addContact(Contact contact) {
        String email = contact.getEmail();
        map.put(email, contact);
    }

    public List<Contact> listContacts() {
        return Collections.unmodifiableList(new ArrayList<Contact>(map.values()));
    }

    public void removeContact(Contact contact) {
        map.remove(contact.getEmail());
    }

    public void updateContact(Contact contact) {        
        map.put(contact.getEmail(), contact);
    }
}

Création des services distants

Pour l'instant, votre objectif est de créer une IHM GWT qui permettra d'utiliser le DAO. Elle devrait utiliser toutes les méthodes de l'interface ContactDAO. La première étape est d'encapsuler toutes les fonctionnalités proposées par le DAO (les versions ultérieures de l'application devront stocker les données côté serveur, donc le DAO doit se trouver sur le serveur) dans un service, comme le montre le Listing 4:

Listing 4. ContactServiceImpl

package gaej.example.contact.server;

import java.util.ArrayList;
import java.util.List;

import gaej.example.contact.client.Contact;
import gaej.example.contact.client.ContactService;

import com.google.gwt.user.server.rpc.RemoteServiceServlet;

public class ContactServiceImpl extends RemoteServiceServlet implements ContactService {
    private static final long serialVersionUID = 1L;
    private ContactDAO contactDAO = new ContactDAOMock();

    public void addContact(Contact contact) {
        contactDAO.addContact(contact);
    }

    public List<Contact> listContacts() {
        List<Contact> listContacts = contactDAO.listContacts();
        return new ArrayList<Contact> (listContacts);
    }

    public void removeContact(Contact contact) {
        contactDAO.removeContact(contact);
    
    }

    public void updateContact(Contact contact) {
        contactDAO.updateContact(contact);
    }
}

Remarquez que ContactServiceImpl étend RemoteServiceServlet et définit des méthodes pour ajouter, supprimer et mettre à jour un contact, et en obtenir une liste. Il délègue toutes ces opérations à ContactDAOMock. ContactServiceImpl encapsule simplement ContactDAO pour exposer ses fonctionnalités à l'IHM GWT. ContactServiceImpl est associée à l'URI /contactlist/contacts dans le fichier web.xml (Listing 5):

Listing 5. Section de web.xml concernant ContactService

<servlet>
    <servlet-name>contacts</servlet-name>
    <servlet-class>gaej.example.contact.server.ContactServiceImpl</servlet-class>
  </servlet>
 
  <servlet-mapping>
    <servlet-name>contacts</servlet-name>
    <url-pattern>/contactlist/contacts</url-pattern>
  </servlet-mapping>

Pour que la partie cliente puisse accéder à ce service, il faut définir à la fois une interface classique et une interface asynchrone pour le service distant, comme dans les Listings 6 et 7:

Listing 6. ContactService

package gaej.example.contact.client;

import java.util.List;

import com.google.gwt.user.client.rpc.RemoteService;
import com.google.gwt.user.client.rpc.RemoteServiceRelativePath;

@RemoteServiceRelativePath("contacts")
public interface ContactService extends RemoteService {
    List<Contact> listContacts();
    void addContact(Contact contact);
    void removeContact(Contact contact);
    void updateContact(Contact contact);
}

Listing 7. ContactServiceAsync

package gaej.example.contact.client;

import java.util.List;

import com.google.gwt.user.client.rpc.AsyncCallback;

public interface ContactServiceAsync  {
    void listContacts(AsyncCallback<List <Contact>> callback);
    void addContact(Contact contact, AsyncCallback<Void> callback);
    void removeContact(Contact contact, AsyncCallback<Void> callback);
    void updateContact(Contact contact, AsyncCallback<Void> callback);
}

Notez que ContactService étend l'interface RemoteService et définit un RemoteServiceRelativePath qui spécifie un chemin relatif "contacts". Ce chemin relatif correspond au chemin qui a été défini pour le service dans le fichier web.xml (c'est une obligation). ContactServiceAsync prévoit des objets callback pour que l'IHM GWT puisse être notifiée des appels au serveur sans bloquer les activités du client.

Couper court au code spaghetti

Je ne suis pas un grand fan du code spaghetti et j'évite d'en écrire autant que possible. Un exemple de code spaghetti serait un ensemble de classes internes anonymes dont les méthodes définiraient des classes internes anonymes. Ces classes internes, à leur tour, feraient des callbacks de méthodes, qui seraient définies dans une classe interne. Beurk ! Franchement, je suis incapable de lire ou de comprendre un code si embrouillé, même si c'est le mien ! Donc, pour éclaircir un peu les choses, je suggère de découper l'IHM GWT en trois parties:

ContactListEntryPoint est le point d'entrée, il fait le cablage de l'IHM. ContactServiceDelegate encapsule les fonctionnalités de ContactService et cache le cablage des callbacks de classe interne. ContactListGUI gère tous les composants graphiques et traite les événements provenant de l'IHM et du service. ContactListGUI utilise ContactServiceDelegate pour effectuer des requêtes à ContactService.

The ContactListEntryPoint is the main entry point; it does GUI event wiring. The ContactServiceDelegate wraps the ContactService functionality and hides the inner class callback wiring. The ContactListGUI manages all of the GUI components and handles events from the GUI and the Service. The ContactListGUI uses the ContactServiceDelegate to make requests of the ContactService.

Le fichier ContactList.gwt.xml (qui se trouve dans gaej.example.contact) spécifie que ContactListEntryPoint est le point d'entrée de l'application au moyen de l'élément entry-point comme le montre le Listing 8:

Listing 8. ContactList.gwt.xml

<entry-point class='gaej.example.contact.client.ContactListEntryPoint'/>

La classe ContactListEntryPoint implémente l'interface EntryPoint de GWT (com.google.gwt.core.client.EntryPoint), ce qui signifie que cette classe sera appelée pour initialiser l'IHM. ContactListEntryPoint ne fait pas grand chose. Elle crée une instance de ContactListGUI et une instance de ContactServiceDelegate, et les met en relation afin qu'elles puissent collaborer.  Ensuite ContactListEntryPoint réalise le cablage des événements de l'IHM. Vous pouvez voir ContactListEntryPoint au Listing 9:

Listing 9. ContactListEntryPoint

package gaej.example.contact.client;

import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.user.client.ui.HTMLTable.Cell;

/**
 * Entry point classes define onModuleLoad().
 */
public class ContactListEntryPoint implements EntryPoint {
    private ContactListGUI gui;
    private ContactServiceDelegate delegate;
    
    /**
     * This is the entry point method.
     */
    public void onModuleLoad() {
        
        gui = new ContactListGUI();
        delegate = new ContactServiceDelegate();
        gui.contactService = delegate;
        delegate.gui = gui;
        gui.init();
        delegate.listContacts();
        wireGUIEvents();
                
        
    }

    private void wireGUIEvents() {
        gui.contactGrid.addClickHandler(new ClickHandler(){
            public void onClick(ClickEvent event) {
                 Cell cellForEvent = gui.contactGrid.getCellForEvent(event);
                 gui.gui_eventContactGridClicked(cellForEvent);                
            }});
        
        gui.addButton.addClickHandler(new ClickHandler(){
            public void onClick(ClickEvent event) {
                gui.gui_eventAddButtonClicked();
            }});

        gui.updateButton.addClickHandler(new ClickHandler(){
            public void onClick(ClickEvent event) {
                gui.gui_eventUpdateButtonClicked();
            }});
        
        gui.addNewButton.addClickHandler(new ClickHandler(){
            public void onClick(ClickEvent event) {
                gui.gui_eventAddNewButtonClicked();
                
            }});

    }
}

Notez que ContactListEntryPoint cable les événements pour addButton, updateButton, contactGrid, and et addNewButton. Pour  ce faire, elle enregistre des classes internes annoymes qui implémentent les interfaces écouteur des événements provenant des composants graphiques. C'est une technique très similaire à la gestion des événements dans Swing. Les événéments proviennent des composants graphiques créés par l'IHM (ContactListGUI), dont nous discuterons plus loin. Notez que la classe représentant l'IHM possèdes des méthodes gui_eventXXX pour répondre aux événements.

ContactListGUI crée les composants graphiques de l'IHM et répond aux événements qu'ils génèrent. Elle les traduit en actions que l'utilisateur veut effectuer sur ContactsService. ContactListGUI utilise ContactServiceDelegate pour invoquer les méthodes de ContactService. ContactServiceDelegate crée une interface asynchrone vers ContactService et l'utilise pour faire des appels Ajax asynchrones. ContactServiceDelegate notifie ContactListGUI des événements (succès ou échec) provenant du service. Vous pouvez voir ContactServiceDelegate dans le Listing 10:

Listing 10. ContactServiceDelegate

import java.util.List;

import com.google.gwt.core.client.GWT;
import com.google.gwt.user.client.rpc.AsyncCallback;

public class ContactServiceDelegate {
    private ContactServiceAsync contactService = GWT.create(ContactService.class);
    ContactListGUI gui;
    
    void listContacts() {
        contactService.listContacts(new AsyncCallback<List<Contact>> () {
                    public void onFailure(Throwable caught) {
                        gui.service_eventListContactsFailed(caught);
                    }
        
                    public void onSuccess(List<Contact> result) {
                        gui.service_eventListRetrievedFromService(result);
                        
                    }
        }//end of inner class
        );//end of listContacts method call.
    }
    
    void addContact(final Contact contact) {
        contactService.addContact(contact, new AsyncCallback<Void> () {
            public void onFailure(Throwable caught) {
                gui.service_eventAddContactFailed(caught);
            }

            public void onSuccess(Void result) {
                gui.service_eventAddContactSuccessful();
            }
        }//end of inner class
        );//end of addContact method call.        
    }

    void updateContact(final Contact contact) {
        contactService.updateContact(contact, new AsyncCallback<Void> () {
            public void onFailure(Throwable caught) {
                gui.service_eventUpdateContactFailed(caught);
            }

            public void onSuccess(Void result) {
                gui.service_eventUpdateSuccessful();
            }
        }//end of inner class
        );//end of updateContact method call.        
    }

    void removeContact(final Contact contact) {
        contactService.removeContact(contact, new AsyncCallback<Void> () {
            public void onFailure(Throwable caught) {
                gui.service_eventRemoveContactFailed(caught);
            }

            public void onSuccess(Void result) {
                gui.service_eventRemoveContactSuccessful();
            }
        }//end of inner class
        );//end of updateContact method call.        
    }
}

Remarquez que ContactServiceDelegate notifie ContactListGUI des événements de service au moyen de méthodes qui commencent par service_eventXXX. Comme mentionné précédemment, un de mes objectifs en écrivant ContactListGUI était d'éviter les classes internes imbriquées et de créer une classe IHM relativement plate (qui soit facile à lire et à utiliser par la suite). ContactListGUI fait seulement 186 lignes et est plutôt simple. Elle gère neuf composants graphiques et collabore avec ContactServiceDelegate pour les opérations CRUD, comme le montre le Listing 11:

Listing 11. ContactListGUI en action

package gaej.example.contact.client;

import java.util.List;

import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.Grid;
import com.google.gwt.user.client.ui.Hyperlink;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.TextBox;
import com.google.gwt.user.client.ui.HTMLTable.Cell;

public class ContactListGUI {
    /* Constantes. */
    private static final String CONTACT_LISTING_ROOT_PANEL = "contactListing";
    private static final String CONTACT_FORM_ROOT_PANEL = "contactForm";
    private static final String CONTACT_STATUS_ROOT_PANEL = "contactStatus";
    private static final String CONTACT_TOOL_BAR_ROOT_PANEL = "contactToolBar";
    private static final int EDIT_LINK = 3;
    private static final int REMOVE_LINK = 4;

    /* Composants graphiques */
    protected Button addButton;
    protected Button updateButton;
    protected Button addNewButton;
    protected TextBox nameField;
    protected TextBox emailField;
    protected TextBox phoneField;
    protected Label status;
    protected Grid contactGrid;
    protected Grid formGrid;
    
    /* Modèle de données */
    private List<Contact> contacts;
    private Contact currentContact;
    protected ContactServiceDelegate contactService;

 

Notez que ContactListGUI garde en mémoire le contact en cours d'affichage dans le formulaire (currentContact) et la liste des contacts (contacts). La Figure 1 montre la correspondance entre les composants graphiques et l'IHM:

Figure 1. Les composants graphiques de l'IHM de gestion de contacts

Les composants graphiques de l'IHM de gestion de contacts

Le Listing 12 montre comment ContactListGUI crée les composants graphiques et un formulaire de contact, puis place les composants dans le formulaire:

Listing 12. ContactListGUI crée et positionne les composants graphiques

public class ContactListGUI {
    /* Constants. */
    private static final String CONTACT_LISTING_ROOT_PANEL = "contactListing";
    private static final String CONTACT_FORM_ROOT_PANEL = "contactForm";
    private static final String CONTACT_STATUS_ROOT_PANEL = "contactStatus";
    private static final String CONTACT_TOOL_BAR_ROOT_PANEL = "contactToolBar";
    ...
    public void init() {
        addButton = new Button("Add new contact");
        addNewButton = new Button("Add new contact");
        updateButton = new Button("Update contact");
        nameField = new TextBox();
        emailField = new TextBox();
        phoneField = new TextBox();
        status = new Label();
        contactGrid = new Grid(2,5);

        buildForm();
        placeWidgets();
    }
    
    private void buildForm() {
        formGrid = new Grid(4,3);
        formGrid.setVisible(false);
        
        formGrid.setWidget(0, 0, new Label("Name"));
        formGrid.setWidget(0, 1, nameField);

        formGrid.setWidget(1, 0, new Label("email"));
        formGrid.setWidget(1, 1, emailField);
        
        formGrid.setWidget(2, 0, new Label("phone"));
        formGrid.setWidget(2, 1, phoneField);
        
        formGrid.setWidget(3, 0, updateButton);
        formGrid.setWidget(3, 1, addButton);
        
    }

    private void placeWidgets() {
        RootPanel.get(CONTACT_LISTING_ROOT_PANEL).add(contactGrid);
        RootPanel.get(CONTACT_FORM_ROOT_PANEL).add(formGrid);
        RootPanel.get(CONTACT_STATUS_ROOT_PANEL).add(status);
        RootPanel.get(CONTACT_TOOL_BAR_ROOT_PANEL).add(addNewButton);
    }

La méthode init de ContactListGUI est appelée par la méthode ContactListEntryPoint.onModuleLoad. La méthode init appelle la méthode buildForm pour créer une nouvelle grille de formulaire et la remplir avec les champs nécessaires à l'édition des données des contacts. La méthode init appelle ensuite la méthode placeWidgets, qui place les composants contactGrid, formGrid, status, et addNewButton aux endroits définis dans la page HTML qui héberge l'IHM de l'application, et que vous voyez dans le Listing 13:

Listing 13. ContactList.html définit l'affichage des composants graphiques

<h1>Contact List Example</h1>

    <table align="center">
      <tr>
        <td id="contactStatus"></td> <td id="contactToolBar"></td>
      </tr>
      <tr>
        <td id="contactForm"></td>
      </tr>
      <tr>
        <td id="contactListing"></td>
      </tr>
    </table>

 

Les constantes (comme CONTACT_LISTING_ROOT_PANEL="contactListing") correspondents aux IDs des éléments (comme id="contactListing") définis dans la page HTML. Ceci permet au concepteur de la page d'avoir un meilleur contrôle sur l'affichage des composants de l'application.

Maintenant que les bases de l'application sont posées, déroulons quelques cas d'utilisation courants.

Afficher une liste au chargement de la page

Quand la page de l'application de gestion des contacts se charge pour la première fois, elle appelle la méthode onModuleLoad de ContactListEntryPoint. onModuleLoad appelle la méthode ContactServiceDelegate.listContacts, qui effectue un appel asynchrone au service listContact. Quand la méthode listContact s'achève, une classe interne anonyme définie dans ContactServiceDelegate appelle la méthode de gestion de l'événement en provenance du service nommée service_eventListRetrievedFromService, que vous voyez dans le Listing 14:

Listing 14. Gestion d'événement au retour de listContact

public class ContactListGUI { 
  ...
  public void service_eventListRetrievedFromService(List<Contact> result) {
        status.setText("Retrieved contact list");
        this.contacts = result;
        this.contactGrid.clear();
        this.contactGrid.resizeRows(this.contacts.size());
        int row = 0;
        
        for (Contact contact : result) {
            this.contactGrid.setWidget(row, 0, new Label(contact.getName()));
            this.contactGrid.setWidget(row, 1, new Label (contact.getPhone()));
            this.contactGrid.setWidget(row, 2, new Label (contact.getEmail()));
            this.contactGrid.setWidget(row, EDIT_LINK, new Hyperlink("Edit", null));
            this.contactGrid.setWidget(row, REMOVE_LINK, new Hyperlink("Remove", null));
            row ++;
        }
    }

La méthode de gestion d'événement service_eventListRetrievedFromServic stocke la liste de contacts envoyée par le serveur. Puis elle nettoie contactGrid qui affiche cette liste. Elle met à jour le nombre de lignes nécessaires pour correspondre à la taille de la liste retournée par le serveur. Ensuite elle itère sur cette liste de contacts, plaçant le nom, le téléphone et l'adresse électronique de chaque contact dans les trois première colonnes de chaque ligne. Elle fournit également des liens d'édition et de suppression pour chaque contact, permettant ainsi aux utilisateurs de modifier et de supprimer facilement des contacts.

Edition d'un contact existant

Quand un utilisateur clique sur le lien d'édition de la liste des contacts, gui_eventContactGridClicked est appelée, comme le montre le Listing 15:

Listing 15. Méthode de gestion d'événement gui_eventContactGridClicked

public class ContactListGUI { 
  ...

    public void gui_eventContactGridClicked(Cell cellClicked) {
         int row = cellClicked.getRowIndex();
         int col = cellClicked.getCellIndex();
        
         Contact contact = this.contacts.get(row);
         this.status.setText("Name was " + contact.getName() + " clicked ");
        
         if (col==EDIT_LINK) {
             this.addNewButton.setVisible(false);
             this.updateButton.setVisible(true);
             this.addButton.setVisible(false);
             this.emailField.setReadOnly(true);
             loadForm(contact);
         } else if (col==REMOVE_LINK) {
             this.contactService.removeContact(contact);
         }
    }
   ...
    private void loadForm(Contact contact) {
        this.formGrid.setVisible(true);
        currentContact = contact;
        this.emailField.setText(contact.getEmail());
        this.phoneField.setText(contact.getPhone());
        this.nameField.setText(contact.getName());
    }

La méthode gui_eventContactGridClicked doit déterminer si c'est le lien d'édition ou de suppression qui a été cliqué, ce qu'elle fait en trouvant quelle colonne a été cliquée. Elle cache ensuite addNewButton et addButton et rend visible updateButton. updateButton s'affiche dans formGrid et permet à l'utilisateur d'envoyer l'information mise à jour à ContactService. Elle met aussi emailField en lecture seule pour que l'utilisateur ne puisse pas l'éditer. Ensuite, gui_eventContactGridClicked appelle loadForm qui rend visible, positionne le contact qui doit être édité puis copie les propriétés du contact dans les composants emailField, phoneField, et nameField.

Quand l'utilisateur clique updateButton, la méthode de gestion d'événement gui_eventUpdateButtonClicked est appelée, comme le montre le Listing 16. Cette méthode rend addNewButton visible (pour que l'utilisateur puisse ajouter des contacts) et cache formGrid. Elle appelle ensuite copyFieldDateToContact, qui copie le texte des composants emailField, phoneField, et nameField dans les propriétés de currentContact. Elle appelle ensuite la méthode ContactServiceDelegate.updateContact en passant le contact modifié en paramètre.

Listing 16. Méthode de gestion d'événement gui_eventUpdateButtonClicked

public class ContactListGUI { 
  ...

    public void gui_eventUpdateButtonClicked() {
        addNewButton.setVisible(true);
        formGrid.setVisible(false);
        copyFieldDateToContact();
        this.contactService.updateContact(currentContact);
    }
    private void copyFieldDateToContact() {
        currentContact.setEmail(emailField.getText());
        currentContact.setName(nameField.getText());
        currentContact.setPhone(phoneField.getText());
    }

 

Ces deux scénarions devraient vous donner une idée du fonctionnement de l'application, et de la manière dont il s'appuie sur l'infrastructure fournie par l'App Engine pour Java. Le code complet de ContactListGUI se trouve au Listing 17:

Listing 17. Code complet de ContactListGUI

package gaej.example.contact.client;

import java.util.List;

import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.Grid;
import com.google.gwt.user.client.ui.Hyperlink;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.TextBox;
import com.google.gwt.user.client.ui.HTMLTable.Cell;

public class ContactListGUI {
    /* Constants. */
    private static final String CONTACT_LISTING_ROOT_PANEL = "contactListing";
    private static final String CONTACT_FORM_ROOT_PANEL = "contactForm";
    private static final String CONTACT_STATUS_ROOT_PANEL = "contactStatus";
    private static final String CONTACT_TOOL_BAR_ROOT_PANEL = "contactToolBar";
    private static final int EDIT_LINK = 3;
    private static final int REMOVE_LINK = 4;

    /* GUI Widgets */
    protected Button addButton;
    protected Button updateButton;
    protected Button addNewButton;
    protected TextBox nameField;
    protected TextBox emailField;
    protected TextBox phoneField;
    protected Label status;
    protected Grid contactGrid;
    protected Grid formGrid;
    
    /* Data model */
    private List<Contact> contacts;
    private Contact currentContact;
    protected ContactServiceDelegate contactService;
        
    public void init() {
        addButton = new Button("Add new contact");
        addNewButton = new Button("Add new contact");
        updateButton = new Button("Update contact");
        nameField = new TextBox();
        emailField = new TextBox();
        phoneField = new TextBox();
        status = new Label();
        contactGrid = new Grid(2,5);

        buildForm();
        placeWidgets();
    }
    
    private void buildForm() {
        formGrid = new Grid(4,3);
        formGrid.setVisible(false);
        
        formGrid.setWidget(0, 0, new Label("Name"));
        formGrid.setWidget(0, 1, nameField);

        formGrid.setWidget(1, 0, new Label("email"));
        formGrid.setWidget(1, 1, emailField);
        
        formGrid.setWidget(2, 0, new Label("phone"));
        formGrid.setWidget(2, 1, phoneField);
        
        formGrid.setWidget(3, 0, updateButton);
        formGrid.setWidget(3, 1, addButton);
        
    }

    private void placeWidgets() {
        RootPanel.get(CONTACT_LISTING_ROOT_PANEL).add(contactGrid);
        RootPanel.get(CONTACT_FORM_ROOT_PANEL).add(formGrid);
        RootPanel.get(CONTACT_STATUS_ROOT_PANEL).add(status);
        RootPanel.get(CONTACT_TOOL_BAR_ROOT_PANEL).add(addNewButton);
    }

    private void loadForm(Contact contact) {
        this.formGrid.setVisible(true);
        currentContact = contact;
        this.emailField.setText(contact.getEmail());
        this.phoneField.setText(contact.getPhone());
        this.nameField.setText(contact.getName());
    }


    private void copyFieldDateToContact() {
        currentContact.setEmail(emailField.getText());
        currentContact.setName(nameField.getText());
        currentContact.setPhone(phoneField.getText());
    }

    public void gui_eventContactGridClicked(Cell cellClicked) {
         int row = cellClicked.getRowIndex();
         int col = cellClicked.getCellIndex();
        
         Contact contact = this.contacts.get(row);
         this.status.setText("Name was " + contact.getName() + " clicked ");
        
         if (col==EDIT_LINK) {
             this.addNewButton.setVisible(false);
             this.updateButton.setVisible(true);
             this.addButton.setVisible(false);
             this.emailField.setReadOnly(true);
             loadForm(contact);
         } else if (col==REMOVE_LINK) {
             this.contactService.removeContact(contact);
         }
    }


    public void gui_eventAddButtonClicked() {
        addNewButton.setVisible(true);
        formGrid.setVisible(false);
        copyFieldDateToContact();
        this.phoneField.getText();
        this.contactService.addContact(currentContact);
    }

    public void gui_eventUpdateButtonClicked() {
        addNewButton.setVisible(true);
        formGrid.setVisible(false);
        copyFieldDateToContact();
        this.contactService.updateContact(currentContact);
    }

    public void gui_eventAddNewButtonClicked() {
        this.addNewButton.setVisible(false);
        this.updateButton.setVisible(false);
        this.addButton.setVisible(true);
        this.emailField.setReadOnly(false);
        loadForm(new Contact());
    }


    public void service_eventListRetrievedFromService(List<Contact> result) {
        status.setText("Retrieved contact list");
        this.contacts = result;
        this.contactGrid.clear();
        this.contactGrid.resizeRows(this.contacts.size());
        int row = 0;
        
        for (Contact contact : result) {
            this.contactGrid.setWidget(row, 0, new Label(contact.getName()));
            this.contactGrid.setWidget(row, 1, new Label (contact.getPhone()));
            this.contactGrid.setWidget(row, 2, new Label (contact.getEmail()));
            this.contactGrid.setWidget(row, EDIT_LINK, new Hyperlink("Edit", null));
            this.contactGrid.setWidget(row, REMOVE_LINK, new Hyperlink("Remove", null));
            row ++;
        }
    }

    public void service_eventAddContactSuccessful() {
        status.setText("Contact was successfully added");
        this.contactService.listContacts();
    }

    public void service_eventUpdateSuccessful() {
        status.setText("Contact was successfully updated");
        this.contactService.listContacts();
    }
    public void service_eventRemoveContactSuccessful() {
        status.setText("Contact was removed");
        this.contactService.listContacts();
        
    }

    public void service_eventUpdateContactFailed(Throwable caught) {
        status.setText("Update contact failed");
    }

    public void service_eventAddContactFailed(Throwable caught) {
        status.setText("Unable to update contact");
    }

    public void service_eventRemoveContactFailed(Throwable caught) {
        status.setText("Remove contact failed");
    }

    public void service_eventListContactsFailed(Throwable caught) {
        status.setText("Unable to get contact list");
        
    }

}

 

Conclusion

Cette seconde partie de l'introduction en trois volets à Google App Engine pour Java vous à initier à la façon de créer une application GWT en utilisant le plugin Eclipse pour l'App Engine. En construisant une application simple de gestion des contacts, vous avez appris comment:

Restez à l'écoute pour le troisième volet, dans lequel vous apporterez des raffinements à l'application de gestion des contacts et ajouterez le support de la persistence avec les fonctionnalités offertes par le magasin de données de l'App Engine.

Persistance et relations

Article d'origine: 

Persistance Java avec le magasin de données de Google App Engine

Résumé: 

La persistance des données est la pierre de touche d'une application qui passe à l'échelle dans des environnements d'entreprise. Dans ce dernier article de sa série d'introduction à Google App Engine for Java™, Rick Hightower aborde le défi que représente le framework actuel de persistance de l'App Engine, basé sur Java. Apprenez les raisons essentielles qui font que la persistance Java de la préversion actuelle n'est pas encore prête pour des applications sensibles, tout en suivant une démonstration fonctionnelle de ce que l'on peut faire pour rendre persistantes les données dans des application App Engine pour Java. Pour cela, vous aurez besoin que l'application de gestion des contacts du second article soit opérationnelle, tandis que vous utilisez l'API JDO pour rendre persistants, requêter, mettre à jour et supprimer des objets Contact.

L' App Engine pour Java cherche à nous décharger du souci de l'écriture d'une couche de persistance pour des applications Web passant bien à l'échelle, mais y arrive-t-il vraiment ? Dans cet article, je conclus mon introduction à l' App Engine pour Java par une vue d'ensemble de son framework de persistance, qui est basé sur Java Data Objects (JDO) et Java Persistence API (JPA). Bien qu'initialement prometteur, la persistance Java présente pour l'instant de sérieux inconvénients, que j'explique en donnant des exemples. Vous apprendrez comment la persistance de l' App Engine pour Java fonctionne, quels en sont les défis, et quels choix se présentent à vous quand vous travaillez sur la plateforme de cloud de Google pour les développeurs Java.

Tandis que vous lisez l'article et voyez les exemples fonctionner, gardez en mémoire que l' App Engine pour Java actuel n'est qu'une préversion. Bien que la persistance Java puisse ne pas correspondre pour le moment à ce que vous espériez, ou à ce dont vous avez besoin, cela pourrait et devrait changer à l'avenir. Ce que j'ai appris en écrivant cet article est qu'il ne faut être ni craintif ni conservateur pour utiliser l' App Engine pour Java afin de construire des applications effectuant beaucoup de manipulation de données et passant bien à l'échelle. Ce serait plutôt comme plonger du côté le plus profond de la piscine sans maitre-nageur en vue : il ne dépendra que de vous que le projet reste à flot ou coule comme une pierre.

Notez que les exemples de cet article sont basés sur l'application de gestion des contacts du second article. Vous aurez besoin que cette application tourne pour mettre à profit les exemples donnés ici.

Eléments de base et abstractions perméables (ndT: leaky abstraction)

Comme le Google App Engine d'origine, App Engine pour Java se repose sur l'infrastructure interne de Google en ce qui concerne les trois piliers du développement d'application qui passent à l'échelle: la distribution, la réplication et la répartition de la charge. Comme on travaille avec l'infrastructure de Google, le plus gros de cette magie se déroule en coulisse, et est accessible via les APIs basées sur des standards proposées par l' App Engine pour Java. L'interface du magasin de données est basée sur JDO et JPA, elles-mêmes se reposant sur le projet Open Source DataNucleus. L'App Engine pour Java fournit également une API de bas niveau pour attaquer directement le magasin de données, qui est basé sur l'implémentation de BigTable par Google (voir le premier article pour en savoir plus sur BigTable).

La persistance d'App Engine pour Java est loin d'être aussi simple que la persistance en pur Google App Engine, toutefois. Les interfaces JDO et JPA présentent quelques abstractions perméables dûes au fait que BigTable n'est pas une base de données relationnelle. Par exemple, avec App Engine pour Java, vous ne pouvez pas faire de jointures dans vos requêtes. Vous pouvez créer des relations avec JPA et JDO, mais elles sont utilisées seulement lors de la persistance. Et quand vous rendez des objets persistants, ils peuvent être persistés dans la même transaction seulement s'ils font partie du même groupe d'entités. Par convention, les relations filles sont dans le même groupe d'entités que leur mère. Inversement, des relations de même niveau sont dans des groupes distincts.

Repenser la normalisation des données

Travailler avec le magasin de données de l'App Engine requiert de votre part que vous repensiez à votre endoctrinement concernant les bénéfices de la normalisation des données. Bien sûr, si vous travaillez depuis assez longtemps dans le monde réel, vous avez probablement déjà sacrifié la normalisation sur l'autel des performances une fois ou deux. La différence est qu'en rapport avec le magasin de données de l'App Engine, il faut dénormaliser tôt et souvent. La dénormalisation n'est plus un gros mot mais un outil de conception que vous appliquerez dans bien des aspects de vos applications.

Le principal inconvénient de cette persistance perméable intervient quand vous essayez de porter une application écrite pour un SGBDR vers l'App Engine pour Java. Le magasin de données de l'App Engine pour Java ne remplace pas aisément une base de données relationnelle, et ce que vous faites avec l'App Engine pour Java peut être difficile à traduire pour un SGBDR. Prendre un schéma existant et le porter dans le magasin de données est un scénario encore moins sympathique. Si vous décidez de porter une application d'entreprise Java existante sur l'App Engine, il vous faudra prendre des précautions et faire une analyse. Google App Engine est une plateforme destinée aux applications qui sont spécialement conçues pour elle. Le support de JDO et JPA permet à ces applications d'être portées plus facilement d'être portées, tout en restant non-normalisées, vers des applications d'entreprise plus traditionnelles.

Le problème des relations

Un autre point faible de la préversion de l'App Engine pour Java est sa gestion des relations. Afin de créer des relations, il faut pour le moment utiliser des extensions de JDO spécifiques à l'App Engine pour Java. Etant donné que les clés sont générées sur la base d'artifacts BigTable — c'est-à-dire que la clé primaire d'une fille inclut la clé primaire de sa mère — il est nécessaire de gérer les données dans une base de données qui n'est pas relationnelle. La persistance de données est une autre limitation. Des problèmes complexes voient le jour si vous utilisez la classe non-standard Key de l'App Engine pour Java. Tout d'abord, comment se servir de Key quand vous portez votre modèle vers un SGBDR ? Ensuite, la classe Key ne peut pas être traduite par le moteur GWT, ce qui signifie qu'un objet du modèle utilisant cette classe ne peut pas être utilisé dans une application GWT.

Bien sûr, au moment où cet article est écrit, il s'agit plutôt d'une préversion de Google App Engine pour Java. Elle n'est pas considérée comme prête pour la production. Cela devient particulièrement évident quand on étudie la documentation des relations en JDO, qui est rare et contient des exemples incomplets.

Le kit de développement de l'App Engine pour Java est fourni avec une série de programmes d'exemple. La plupart de ces exemple utilisent JDO, et aucun JPA. Aucun de ces exemples (y compris celui qui s'appelle jdoexamples) ne montre même une relation simple. Au lieu de cela, tous ces exemples utilisent un objet unique pour stocker les données dans le magasin. Le groupe Google App Engine for Java discussion group est plein de questions sur la manière de faire fonctionner une relation simple, avec peu de réponses. Quelques développeurs sont arrivés à le faire, mais ces complications ont nécessité beaucoup d'huile de coude.

La conclusion en ce qui concerne les relations dans l'App Engine pour Java est qu'il faut les gérer sans beaucoup d'aide de JDO ou de JPA. Cependant, BigTable est une technologie éprouvée pour ce qui est de résister à la montée en charge et vous pouvez bâtir sur ce fondement. S'appuyer sur BigTable libère de l'obligation d'employer une API façade qui est loin d'être terminée. Par contre, il faudra faire avec une API de plus bas niveau.

Java Data Objects et App Engine pour Java

Bien qu'il n'y ait peut-être pas grand sens à porter une application Java traditionnelle vers l'App Engine, et en prenant également en compte le défi posé par les relations, il existe tout de même des scénarios de persistance pour lesquels l'utilisation de la plateforme a du sens. je conclurais cet article avec un exemple qui devrait vous donner un avant-goût de la manière dont la persistance de l'App Engine fonctionne. Nous démarrerons avec l'application de gestion de contacts de la seconde partie, mais cette fois nous ajouterons le support de la persistance des objets Contact dans le magasin de données de l'App Engine.

Dans l'article précédent, vous avez créé une IHM GWT pour effectuer des opérations CRUD sur les objets Contact. Vous avez défini l'interface du Listing 1:

Listing 1. L'interface ContactDAO

package gaej.example.contact.server;

import java.util.List;

import gaej.example.contact.client.Contact;

public interface ContactDAO {
	void addContact(Contact contact);
	void removeContact(Contact contact);
	void updateContact(Contact contact);
	List<Contact> listContacts();
}

Ensuite, vous avez créé un simulacre qui utilisait des données se trouvant dans une collection en mémoire, comme le rappelle le Listing 2.

Listing 2. Simulacre implémentant ContactDAO

package gaej.example.contact.server;

import gaej.example.contact.client.Contact;

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

public class ContactDAOMock implements ContactDAO {
    Map<String, Contact> map = new LinkedHashMap<String, Contact>();
    {
        map.put("rhightower@mammatus.com", new Contact("Rick Hightower", 
                                 "rhightower@mammatus.com", "520-555-1212"));
        map.put("scott@mammatus.com", new Contact("Scott Fauerbach", 
                                 "scott@mammatus.com", "520-555-1213"));
        map.put("bob@mammatus.com", new Contact("Bob Dean", 
                                 "bob@mammatus.com", "520-555-1214"));
    }
    
    public void addContact(Contact contact) {
        String email = contact.getEmail();
        map.put(email, contact);
    }

    public List<Contact> listContacts() {
        return Collections.unmodifiableList(new ArrayList<Contact>(map.values()));
    }

    public void removeContact(Contact contact) {
        map.remove(contact.getEmail());
    }

    public void updateContact(Contact contact) {
        map.put(contact.getEmail(), contact);
    }
}

Maintenant voyons ce qui se passe si nous remplaçons cette implémentation simulacre avec une implémentation qui interagit avec le magasin de données de l'App Engine. Dans cet exemple, nous utiliserons JDO pour rendre la classe Contact persistente. Une application écrite avec le plugin Google pour Eclipse a déjà toutes les librairies nécessaires à l'utilisation de JDO. Elle inclut également un fichier jdoconfig.xml, donc une fois que la classe est annotée, nous sommes prêts à utiliser JDO.

Le Listing 3 montre l'interface implémentée afin d'utiliser JDO pour rendre persistents, requêter, mettre à jour et supprimer des objets:

Listing 3.  ContactDAO avec JDO

package gaej.example.contact.server;

import gaej.example.contact.client.Contact;

import java.util.List;

import javax.jdo.JDOHelper;
import javax.jdo.PersistenceManager;
import javax.jdo.PersistenceManagerFactory;

public class ContactJdoDAO implements ContactDAO {
    private static final PersistenceManagerFactory pmfInstance = JDOHelper
			.getPersistenceManagerFactory("transactions-optional");
    
    public static PersistenceManagerFactory getPersistenceManagerFactory() {
		return pmfInstance;
	}

    public void addContact(Contact contact) {
        PersistenceManager pm = getPersistenceManagerFactory()
				.getPersistenceManager();
        try {
            pm.makePersistent(contact);
        } finally {
            pm.close();
        }
    }

    @SuppressWarnings("unchecked")
    public List<Contact> listContacts() {
        PersistenceManager pm = getPersistenceManagerFactory()
				.getPersistenceManager();
        String query = "select from " + Contact.class.getName();
        return (List<Contact>) pm.newQuery(query).execute();
    }

    public void removeContact(Contact contact) {
        PersistenceManager pm = getPersistenceManagerFactory()
				.getPersistenceManager();
        try {
            pm.currentTransaction().begin();

            // Nous n'avons pas de référence vers le Contact sélectionné.
            // Il nous faut d'abord le retrouver
            contact = pm.getObjectById(Contact.class, contact.getId());
            pm.deletePersistent(contact);

            pm.currentTransaction().commit();
        } catch (Exception ex) {
            pm.currentTransaction().rollback();
            throw new RuntimeException(ex);
        } finally {
            pm.close();
        }
    }
    
    public void updateContact(Contact contact) {
        PersistenceManager pm = getPersistenceManagerFactory()
				.getPersistenceManager();
        String name = contact.getName();
        String phone = contact.getPhone();
        String email = contact.getEmail();
        
        try {
            pm.currentTransaction().begin();
            // Nous n'avons pas de référence vers le Contact sélectionné.
            // Il nous faut d'abord le retrouver
            contact = pm.getObjectById(Contact.class, contact.getId());
            contact.setName(name);
            contact.setPhone(phone);
            contact.setEmail(email);
            pm.makePersistent(contact);
            pm.currentTransaction().commit();
        } catch (Exception ex) {
            pm.currentTransaction().rollback();
            throw new RuntimeException(ex);
        } finally {
            pm.close();
        }
    }
}

Méthode par méthode

Maintenant examinons ce qui se passe dans chacune des méthodes du Listing 3. Vous vous rendrez compte que, bien que les noms de méthodes puissent être nouveaux, leur action est pour la plus grande part familière.

Tout d'abord, afin d'accéder au PersistenceManager, nous avons créé une instance statique de PersistenceManagerFactory. Si vous avez déjà travaillé avec JPA, PersistenceManager est l'équivalent d'un EntityManager. Si vous connaissez Hibernate, PersistenceManager est l'équivalent d'une session. L'idée importante est que PersistenceManager est l'interface principale vers le système de persistance JDO. Il représente une session de base de données. La méthode PersistenceManagerFactory retourne le qui a été initialisé de manière statique, comme le montre le Listing 4:

Listing 4. getPersistenceManagerFactory() retourne l'instance de PersistenceManagerFactory

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

public static PersistenceManagerFactory getPersistenceManagerFactory() {
	return pmfInstance;
}

La méthode addContact() ajoute de nouveaux contacts dans le magasin de données. Pour ce faire, elle crée une instance PersistenceManager de puis appelle sa méthode makePersistence(). La méthode makePersistence() prend l'objet transient Contact (que l'utilisateur aura rempli dans l'IHM GWT) et en fait un objet persistant. Tout ceci est codé dans le Listing 5:

Listing 5. addContact()

public void addContact(Contact contact) {
    PersistenceManager pm = getPersistenceManagerFactory()
			.getPersistenceManager();
    try {
    	pm.makePersistent(contact);
    } finally {
    	pm.close();
    }
}

Remarquez que dans le Listing 5, la fermeture persistenceManager de se trouve dans un bloc finally. Ceci assure que les ressources qui lui sont associées seront bien nettoyées.

La méthode listContact(), qui se trouve dans le Listing 6, crée une requête au moyen de persistenceManager. Elle invoque ensuite la méthode execute(), qui retourne la liste de se trouvant dans le magasin de données.

Listing 6. listContacts()

@SuppressWarnings("unchecked")
public List<Contact> listContacts() {
    PersistenceManager pm = getPersistenceManagerFactory()
			.getPersistenceManager();
    String query = "select from " + Contact.class.getName();
    return (List<Contact>) pm.newQuery(query).execute();
}

La méthode cherche un contact par son ID avant de le supprimer du magasin, comme vous le voyez au Listing 7. Une suppression directe du contact ne fonctionnerait pas puisque le Contact en provenance de l'IHM ne sait rien de JDO. Il faut obtenir un Contact associé à un PersistenceManager avant de pouvoir le supprimer.

Listing 7. removeContact()

public void removeContact(Contact contact) {
    PersistenceManager pm = getPersistenceManagerFactory()
			.getPersistenceManager();
    try {
	pm.currentTransaction().begin();
	// Nous n'avons pas de référence vers le contact sélectionné.
	// Donc nous devons d'abord le récupérer
	contact = pm.getObjectById(Contact.class, contact.getId());
	pm.deletePersistent(contact);
	pm.currentTransaction().commit();
    } catch (Exception ex) {
	pm.currentTransaction().rollback();
	throw new RuntimeException(ex);
    } finally {
	pm.close();
    }
}

La méthode updateContact() du Listing 8 est similaire à la méthode removeContact() en ce qu'elle recherche d'abord le Contact. Elle copie ensuite les propriétés du Contact. Ces propriétés sont passées en argument au Contact qui a été récupéré par le gestionnaire de persistance. Les objets qui ont été ramenés sont analysés par le PersistenceManager. Si un objet a été modifié, les modifications sont écrites dans la base de données par le PersistenceManager quand la transaction est validée.

Listing 8. updateContact()

public void updateContact(Contact contact) {
    PersistenceManager pm = getPersistenceManagerFactory()
			.getPersistenceManager();
    String name = contact.getName();
    String phone = contact.getPhone();
    String email = contact.getEmail();

    try {
	pm.currentTransaction().begin();
	// Nous n'avons pas de référence vers le contact sélectionné.
	// Donc nous devons d'abord le récupérer
	contact = pm.getObjectById(Contact.class, contact.getId());
	contact.setName(name);
	contact.setPhone(phone);
	contact.setEmail(email);
	pm.makePersistent(contact);
	pm.currentTransaction().commit();
    } catch (Exception ex) {
	pm.currentTransaction().rollback();
	throw new RuntimeException(ex);
    } finally {
	pm.close();
    }
}

Annotations pour la persistance des objets

Afin de permettre à un Contact de persister, il faut préciser que celui-ci en est capable avec l'annotation
@PersistenceCapable. Il faut ensuite annoter tous les champs persistants, comme vous le montre le Listing 9:

Listing 9. Contact peut maintenant persister

package gaej.example.contact.client;

import java.io.Serializable;

import javax.jdo.annotations.IdGeneratorStrategy;
import javax.jdo.annotations.IdentityType;
import javax.jdo.annotations.PersistenceCapable;
import javax.jdo.annotations.Persistent;
import javax.jdo.annotations.PrimaryKey;

@PersistenceCapable(identityType = IdentityType.APPLICATION)
public class Contact implements Serializable {
    private static final long serialVersionUID = 1L;
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Long id;
    @Persistent
    private String name;
    @Persistent
    private String email;
    @Persistent
    private String phone;

    public Contact() {
    }

    public Contact(String name, String email, String phone) {
    	super();
    	this.name = name;
	this.email = email;
	this.phone = phone;
    }

    public Long getId() {
	return id;
    }

    public void setId(Long id) {
	this.id = id;
    }

    public String getName() {
	return name;
    }

    public void setName(String name) {
	this.name = name;
    }

    public String getEmail() {
	return email;
    }

    public void setEmail(String email) {
    	this.email = email;
    }

    public String getPhone() {
	return phone;
    }

    public void setPhone(String phone) {
	this.phone = phone;
    }
}

Par la grâce de la programmation orientée-objet et de la conception par interface, vous pouvez tout simplement remplacer le ContactDAOMock de départ par le nouveau ContactJdoDAO. L'IHM GWT marchera avec JDO sans modification.

En fin de compte, la seule modification à faire pour provoquer cet échange se situe dans la manière dont le DAO est instantié dans le service, comme le montre le Listing 10:

Listing 10. RemoteServiceServlet

public class ContactServiceImpl extends RemoteServiceServlet implements ContactService {
    private static final long serialVersionUID = 1L;
    //private ContactDAO contactDAO = new ContactDAOMock();
    private ContactDAO contactDAO = new ContactJdoDAO();
    ...

En conclusion

Dans cette troisième partie, j'ai introduit le support actuel la persistance dans Google App Engine pour Java, qui constitue une des pierres angulaires du déploiement d'applications qui passent à l'échelle. Le constat général est assez décevant, bien qu'il soit important de garder à l'esprit qu'il s'agit d'une plateforme en évolution. Les applications écrites pour cette préversion de l'App Engine sont liées à son infrastructure de persistance, même si elles utilisent JDO ou JPA. De même, cette préversion n'offre que peu de documentation sur son framework de persistance, et les exemples fournis montrent qu'il est à peu près impossible de faire fonctionner même les relations les plus simples.

Même si les implémentations de JDO et JPA étaient complètement terminées, il serait à l'heure actuelle bien difficile de porter une application écrite pour l'App Engine vers une application d'entreprise classique, utilisant un SGBD. A tout le moins, cela nécessiterait un recodage assez costaud pour que cela fonctionne.

J'espère que la persistance va se bonifier avec le temps. Si vous devez vraiment travailler avec l'App Engine maintenant, vous devriez probablement court-circuiter l'utilisation des APIs Java et travailler directement avec les APIs de bas niveau du magasin de données. Il est possible de travailler avec la plateforme App Engine pour Java, mais il faut s'attendre à un certain travail d'apprentissage, dû à la fois à la perméabilité des abstractions décrites au début de cet article et aux fonctionnalités qui soit ne fonctionnent pas encore bien, soit qui ne sont pas encore bien documentées.