Watch this site for election results.
?
?
?

The president is...

A codelab in the cloud
with App Engine

Martin Görner, Alexis Moussine-Pouchkine, Didier Girard

Résultats annoncés sur ce site. --self-- --self-- --self-- Le président est... Un exercice de style et de montée en charge sur App Engine --self--

To help you in this challenge, you can use a powerful tool: App Engine. Our cloud experts will guide you along the way.

One more thing, you have only 2 hours.

Ready to scale ?

You won the contract of your life. Your job is to build the web site announcing, on D-day, the results of the presidential election.

The site will have to withstand a peak of potentially 300 million users on election day. Traffic can peak at above 10 milion simultaneous users at 8PM when the results are announced. Traffic will then taper off to zero over the next few days. The site must have the social features users expect: comments, satisfaction votes, image publishing, ...

The site cannot fail,
especially not at peak traffic!

Pour ceci vous disposez d'une plateforme, Google AppEngine et d'experts pour vous guider. Le défi est à relever en 2 heures seulement. Prêt à relever le défi ? Vous venez de gagner un contrat. Il s'agit de développer le site web qui va annoncer au soir des élections le nouveau président de la République Française. Le site devra pouvoir accueillir un traffic potentiel maximum de 50 millions d'utilisateurs le jour J. Ce jour là, un pic à 2 millions d'utilisateurs simultanés est attendu a 20h. Ensuite, le lendemain, 1 million d'utilisateurs viendront et les jours suivants quelques milliers d'utilisateurs au grand maximum. Le site devra proposer des fonctions de type social : vote de satisfaction, commentaires, publications d'images, ... Il ne doit pas faire défaut, il n'est pas concevable qu'il tombe a l'heure H.

You should now have a "g" icon in the Eclipse toolbar.

Eclipse and Java 7 troubleshooting:

Where is my JDK Java 7 ?

On MacOS, run this in a terminal:
/usr/libexec/java_home

On Windows, usually:
C:\Program Files\Java\jdk1.7.XXXX

Eclipse starts on JRE 6 and the Google plugin does not like it: force java 7 in eclipse.ini (next to the Eclipse executable, in the app bundle on mac)
On MacOS: -vm
/Library/Java/JavaVirtualMachines/jdk1.7.XXXX.jdk/Contents/Home
 
On Windows: -vm
C:\Program Files\Java\jdk1.7.XXXX\bin\javaw.exe

Which JDK does Eclipse use to compile my projects ?

Eclipse > Preferences > Java > Installed JREs

What Java system libraries does Eclipse use for my project ?

Project > Properties > Java Build Path (Librairies tab)

How to compile in Java 7 ?

Project > Properties > Java Compiler
0

Setup (Eclipse plugin option, recommended for the codelab)

Vous devriez maintenant avoir une icône "g" dans Eclipse. Problèmes avec Eclipse et Java 7 ? Où est mon JDK Java 7 ? Sur MacOS, lancer ceci dans le terminal: --self-- Sur Windows, habituellement: --self-- Eclipse se lance sur JRE 6 et le plugin Google n'aime pas ça: forcer Java 7 dans  --self-- (à côté de l'exécutable Eclipse, dans la app bundle sur mac) en ajoutant: Sur MacOS: --self-- --self-- Sur Windows: --self-- --self-- Quel est le JDK avec lequel Eclipse compile vos projets ? Eclipse > Préférences > Java > JREs Installés Quelle librairie système Java est utilisée par votre projet ? Projet > Propriétés > Java Build Path (onglet Librairies) Comment compiler en Java 7 ? Projet > Propriétés > Compilateur Java --self-- Prérequis (option plugin Eclipse, recommandée pour le codelab) --self-- (pas JRE) --self-- Les librairies  --self-- et  --self-- à télécharger (explications plus loin) --self-- et  --self-- disponible dans --self-- --self--

Maven is a project format and build system for Java. Maven projects are portable between IDEs or continuous integration tools (Eclipse, NetBeans, IntelliJ, Jenkins). They include dependencies, including our dependency on the App Engine SDK which will be downloaded automatically.

Your first "Hello App Engine" servlet:

  • Create a new Maven Project using an archetype
  • Filter by com.google.appengine.archetypes and select skeleton-archetype (1.7.5)
  • GroupId: com.myname, ArtefactID: thepresident
  • Create the servlet PresidentServlet.java with
    response.getWriter().write("Hello App Engine");
  • Run local server with the SDK (goal: appengine:devserver)
    Eclipse: Run > Run Configurations... > DevAppServer
    First run: the App Engine SDK will be downloaded (50MB)
0

Setup (Maven option, better for a real project)

Maven est un format de projet et un système de build pour Java. Les projets Maven sont portables entre IDEs, outils d'intégration continue etc. (Eclipse, NetBeans, IntelliJ, Jenkins). Ils incluent les dépendances, y compris la dépendance vers le SDK App Engine qui sera téléchargé automatiquement. Vore première servlet "Hello App Engine": Créer un nouveau Projet Maven d'après un archetype Filtrer par --self-- et sélectionner --self-- --self-- --self-- --self-- --self-- Créer une servlet --self-- avec --self-- Lancer en local avec le SDK (goal appengine:devserver) --self-- --self-- Premier run: le SDK App Engine va se télécharger (50MB) --self-- Prérequis (option Maven, pour un vrai projet) --self-- (pas JRE) --self-- (ou un autre éditeur Java) --self-- (sur mac, maven est déjà installé) Le plugin Maven --self-- pour Eclipse à installer via --self-- Se connecter à --self-- --self-- --self--
Structure of the Web project:

When you use the assistant to create the servlet, the standard file web.xml is updated to map the servlet.

The file appengine-web.xml contains default values and will be used later to specify the application id as well as other App Engine specific parameters.

Having trouble ?

If you see the error "port occupied" when launching your app, use the red "Terminate" button in the "Console" (Window > Show View > Console).
1

My first servlet "Hello App Engine" (Eclipse plugin option)

Structure du projet Web: En utilisant l'assistant pour créer la servlet, le fichier standard  --self-- est mis à jour pour effectuer le mapping de celle-ci. Le fichier  --self-- contient des valeurs par défaut et sera utilisé plus tard pour spécifier l'id et la version de l'application et d'autres paramètres optionels propres à AppEngine --self-- Un problème? Si en relançant vore application vous avez une erreur --self-- , utiliser le bouton rouge "Terminate" dans le haut de la fenêtre --self-- --self-- --self-- Ma première servlet "Hello App Engine (option plugin Eclipse) --self-- Nom:  --self-- --self-- --self-- Décocher --self-- (pour aujourd'hui), Cocher --self-- puis "Finish" pour générer le projet. Personnaliser la servlet: par exemple --self-- (dans --self-- Lancer en local: --self-- --self-- --self-- Puis rajouter les librairies  --self-- et  --self-- : (1) glisser-déplacer les .jar sur war/WEB-INF/lib. (2) Les référencer dans les propriétés du projet, "Java Build Path", onglet "Libraries", bouton "Add JARs..."

Let us switch to JSP.
JSP is like HTML, but you can:

(Next time, to write cleaner code, prefer GWT or a JavaScript framework like AngularJS)

Passons en JSP. JSP c'est comme du HTML, mais on peut: exécuter du code java entre --self-- ...code java... --self-- afficher une valeur via --self-- truc.getMachin() --self-- avoir accès aux objets JSP suivants: --self-- --self-- --self-- --self-- --self-- --self-- (Dans le futur, pour écrire du code propre, préférez GWT ou un framework comme AngularJS)

file: war/comments.jsp

<! DOCTYPE html>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
  <head><title>The president is...</title></head>
  <body>
    <H1>The president is...</H1>
    <img src="obama.png">
    <p><%=request.getProtocol() %></p>
  </body>
</html>

This page can be set as the default landing page in WEB-INF/web.xml

You fill find pictures of the candidates behind the first slide.

2

Add a JSP page

The president is...

HTTP/1.1

 
 
The name of the new president is not known yet so we display something else, for instance:

request.getProtocol()
fichier: --self-- --self-- Le président est... Le président est... hollande.png --self-- On peut configurer cette page comme page par défaut dans WEB-INF/web.xml Vous trouverez les photos des candidats derrière la première diapositive. --self-- Ajouter une page JSP Le président est... --self-- On n'a pas encore le nom de l'élu alors faute de mieux on affiche le protocole en utilisant: --self--

file: war/comments.jsp

<! DOCTYPE html>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="com.google.common.html.HtmlEscapers" %> <!-- Guava -->

<html>
  <body>
    <H1>The president is...</H1>
    <img src="romney.png" />
    <% 
        String comment = request.getParameter("user-comment");
        if (comment != null)
	    		comment = HtmlEscapers.htmlEscaper().escape(comment);
    %>
    <p>You: <%=comment %></p>
    <form action="" method="post">
         <textarea name="user-comment" ></textarea><br/>
         <input type="submit" value="OK" />
     </form> 
  </body>
</html>
XSS vulnerability WARNING! DO NOT insert user-generated text into an HTML page without escaping.
3

Add a comments field - without persistency for now

The president is...

You: not my first choice...

You need an input box:

<form action="" method="post">
   <textarea name="user-comment" >
</textarea>
<input type="submit" value="OK" /> </form>

And a way to get request parameters:

request.getParameter("user-comment")
fichier: --self-- Le président est... sarkozy.png ATTENTION aux vulnérabilités XSS! Ne jamais insérer dans une page du texte en provenance d'un utilisateur sans escaping. --self-- Ajouter un champ de commentaire - sans persistence pour l'instant Le président est... Vous: pas mon premier choix... Il vous faut une zone de saisie: --self-- Et la fonction qui donne un paramètre de requête:

file: war/comments.jsp

<! DOCTYPE html>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="com.google.appengine.api.users.*" %>
<%@ page import="com.google.common.html.HtmlEscapers" %> <!-- Guava -->

<html><body>
    <H1>The president is...</H1> <img src="obama.jpg" />
<%
    UserService userService = UserServiceFactory.getUserService();
    User user = userService.getCurrentUser();
    if (user == null)
    {%>
        <p>Hello, <a href="<%= userService.createLoginURL(request.getRequestURI()) %>">
        please log in</a> to post comments.</p>
    <%}
    else
    {
        String comment = request.getParameter("user-comment");
        if (comment != null)
            comment = HtmlEscapers.htmlEscaper().escape(comment);
%>
        <p><b><%=user.getNickname()%>:</b> <%=comment%></p>
    
        <form action="" method="post">
            <textarea name="user-comment"></textarea><br/>
            <input type="submit" value="OK" />
            <!-- logout link -->
            <a href="<%= userService.createLogoutURL(request.getRequestURI()) %>">logout</a>
        </form>
<% } %>
</body></html>
4

Identify the user with a Google login

The president is...

You: not my first choice...
logout

Use the App Engine UserService API:

UserService userService =
   UserServiceFactory.getUserService();
User user = userService.getCurrentUser();
String n = user.getNickname();
String e = user.getEmail();

You will also need the login and logout URLs:

userService.createLoginURL("myTargetURL")
userService.createLogoutURL("myTargetURL");
fichier: war/comments.jsp --self-- Le président est... hollande.png --self-- --self-- --self-- Bonjour --self-- identifiez-vous pour pouvoir commenter l'élection --self-- lien de déconnexion --self-- déconnexion --self-- Identifier l'utilisateur par son compte Google Le président est... Vous: pas mon premier choix... déconnexion Utiliser le UserService d'App Engine: Il vous fournit aussi les URLs de connexion et déconnexion:

High Replication Datastore
concurrent access, replication, scale

Data layout: think "XML" rather than "database table"

Indexed queries only.

Eventually consistent queries.

Entity = kind + id +
(multivalued) properties

High Replication Datastore réplication, accès simultanés, montée en charge Organisation des données: penser "XML" plutôt que tables. Requêtes indexées uniquement. Cohérence à terme des requêtes. --self-- --self--

file: src/com/me/president/Comments.java

package com.me.president;
import java.util.*;
import com.google.appengine.api.datastore.*;
import com.google.appengine.api.datastore.Entity;

public class Comments {
    static public void store(String text, String user) {
        Entity commentEntity = new Entity("Comment");
        commentEntity.setProperty("user", user);
        commentEntity.setProperty("date", new Date());
        commentEntity.setProperty("text", text);

        DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
        datastore.put(commentEntity);
    }
	
    static public List<Entity> retrieveAll() {
        DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();

        Query query = new Query("Comment");
        query.addSort("date", Query.SortDirection.DESCENDING);
        return datastore.prepare(query).asList(FetchOptions.Builder.withLimit(100));
    }
}
Usage:
// store:
Comments.store("Hello", "Nicolas");
// retrieve:
List<Entity> comments = Comments.retrieveAll();
for (Entity commentEntity: comments) { ... commentEntity.getProperty("text") ... }
5

Store comments on the server

The president is...

You:: he will have to do.
George: I voted for him.
Bar: so unfair!
logout

Low level datastore access:

DatastoreService datastore =
 DatastoreServiceFactory.getDatastoreService();

Write:

Entity commentEntity = new Entity("Comment");
commentEntity.setProperty("date", new Date());
commentEntity.setProperty("text", text);
datastore.put(commentEntity);

Read: 100 results sorted by date

Query query = new Query("Comment");
query.addSort("date", Query.SortDirection.DESCENDING);
List<Entity> results =
   datastore.prepare(query)
      .asList(FetchOptions.Builder.withLimit(100));
fichier: --self-- --self-- Utilisation: --self-- Conserver les commentaires sur le serveur Le président est... Vous Ah! ça ira, ça ira, ça ira Nicolas: j'ai voté pour lui. François: c'est injuste. déconnexion Accès bas niveau au datastore: Écriture: Lecture: 100 résultats triés par date

file: src/com/me/president/ObjectifyDAO.java

package com.me.president;
import com.googlecode.objectify.Objectify;     import com.googlecode.objectify.ObjectifyService;

public class ObjectifyDAO
{
    static { ObjectifyService.register(Comment.class); } // register all @Entity classes
    static Objectify ofy() { return ObjectifyService.ofy(); }  // proxy for Objectify
}

file: src/com/me/president/Comment.java

package com.me.president;
import java.util.Date; import java.util.List;
import com.googlecode.objectify.annotation.Entity;
import com.googlecode.objectify.annotation.Id;
import com.googlecode.objectify.annotation.Index;
import static com.me.president.ObjectifyDAO.ofy; // now ofy() goes through the DAO

@Entity public class Comment
{
   @Id Long id;     @Index Date date;     public String user, text;

   public Comment() {}
   public Comment(String text, String user) { this.user = user; this.text = text; this.date = new Date(); }

   public static void store(String text, String user) { ofy().save().entity(new Comment(text, user)); }

   public static List<Comment> retrieveAll()
   { return ofy().load().type(Comment.class).order("-date").limit(100).list(); }
}
6

The same thing with Objectify 4

Use a Plain Old Java Object (POJO):
@Entity
public class Comment
{
    @Id Long id;
    @Index Date date;
    String user;
    String text;

    public Comment() {}
    ...
}

If the Id is of type Long, it will be generated automatically on write.

Registering data classes in a Data Access Object:

public class ObjectifyDAO
{// register all @Entity classes
 static { ObjectifyService.register(Comment.class); }
// proxy for Objectify
 static Objectify ofy() {return ObjectifyService.ofy();}
}
// then use ofy() thanks to the declaration:
import static com.me.president.ObjectifyDAO.ofy;

Write:

ofy().save().entity(new Comment(text, user));

Read: 100 results sorted by date

List<Comment> list = ofy().load().type(Comment.class)
                   .order("-date").limit(100).list();
for (Comment c: list) { ... }
Objecify 4 also requires the ObjectifyFilter in web.xml.
fichier: --self-- // enregistrement des classes @Entity // proxy pour Objectify fichier: --self-- // ofy() passe désormais par le DAO --self-- La même chose avec Objectify 4 Utiliser un Plain Old Java Object (POJO): L'écriture en base auto-génère l'id si il est de type Long. Enregistrement des classes et Data Access Object: // enregistrer toutes les classes @Entity // proxy pour Objectify // puis utiliser ofy() grâce à la déclaration: Écriture: Lecture: 100 résultats triés par date Il faut aussi mettre en place ObjectifyFilter dans web.xml --self--

App Engine application architecture

"SYSTEM 2000"
session state in RAM
with sticky load balancing
pb on instance failure
insufficient memory
slow load balancing
database scalability pb
datastore→
memcache→
frontends→
efficient load balancing→
Google App Engine
stateless instances, state stored in memcache and datastore
Architecture d'application sur App Engine “système 2000” session en RAM avec sticky load balancer --self-- pb sur arrêt d'instance --self-- mémoire insuffisante --self-- load balancing lent --self-- pb scalabilité base --self-- --self-- --self-- load balancing efficace→ --self-- instances stateless, état dans memcache et datastore

file: /src/com/me/president/Comment.java

The Comment class must be Serializable

public class Comment implements Serializable

Create a new method for retrieving comments with caching:

public static List<Comment> retrieveAllCached() {
    MemcacheService cache = MemcacheServiceFactory.getMemcacheService();
    List<Comment> comments = (ArrayList<Comment>)cache.get("100Comments");
    if (comments == null) {
        // read from Datastore
        comments = retrieveAll();
        // write to Memcache
        cache.put("100Comments", new ArrayList<Comment>(comments));
    }
    return comments;
}

Finally, invalidate the cache on each new comment:

import static com.me.president.ObjectifyDAO.ofy;

public static void store(String text, String user) {
	ofy().save().entity(new Comment(text, user));
	MemcacheServiceFactory.getMemcacheService().delete("100Comments"); // flush cache
}
7

Optimize comments with Memcache

The interface:

MemcacheService cache =
   MemcacheServiceFactory 
     .getMemcacheService();
     
cache.put("myKey", Object);
cache.get("myKey");
cache.delete("myKey");

Memcache makes sure all your instances see the same value.

Just keep the result of the last request in Memcache (serialized) until a new comment is added.

Memcache offers fast access and less contention for data primarily fetched from the datastore.

fichier: --self-- La classe Comment doit devenir sérialisable: --self-- On crée une nouvelle méthode pour la lecture des commentaires (avec cache): // lecture à partir du Datastore // enregistrement dans Memcache Enfin, on invalide le cache à chaque nouveau commentaire: --self-- --self-- Optimiser les commentaires avec Memcache L'interface: Memcache s'assure que toutes vos instances voient la même valeur. Il suffit de garder le résultat de la dernière requête en Memcache (sérialisée) tant qu'un nouveau commentaire n'a pas été écrit. L'utilisation de Memcache permet un accès rapide, avec moins de risques de contention, à des données issues principalement du datastore.

file: war/WEB-INF/appengine-web.xml

<?xml version="1.0" encoding="utf-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
    <!-- Your application id as defined on http://appengine.google.com -->
    <application>thepresident-is</application>
    <!-- The version of your application, it can be a string (alpha, beta, ...) -->
    <version>1</version>
    ...
</appengine-web-app>

Reference documentation on appengine-web.xml.

This configuration is also possible via the Eclipse plugin:

8

Deploy to App Engine

 

 

 

(maven option: maven goal appengine:update
Eclipse (Maven): Run > Run Cofigurations... > UpdateApplication)
fichier: war/WEB-INF/appengine-web.xml Votre ApplicationId sur lepresident-est Version de votre application, cela peut etre un texte (alpha, beta, etc...) Documentation de référence sur appengine-web.xml . Le plugin App Engine pour Eclipse a aussi un réglage de projet pour l'App id : --self-- Déployer sur App Engine! Aller sur --self-- et créer un identifiant pour votre application. Elle sera accessible à l'adresse http://monidentifiant.appspot.com Spécifier cet identifiant dans appengine-web.xml Déployer (plugin Eclipse): choisir "Deploy to App Engine..." dans le menu "g". (option maven: maven goal appengine:update --self-- --self--

On the following slides:

  1. CRON jobs
  2. Sharded counters
  3. Composite datastore indices
  4. Uploading images with Blobstore and ImageService
  5. Securing admin URLs
  6. Receiving emails

More on App Engine:

And beyond:

Keep building ?

Il vous reste à découvrir: Les tâches planifiées CRON Les compteurs éclatés Les indices composites dans le datastore L'upload d'une image avec Blobstore et ImageService Comment protéger les URLs administrateur Comment recevoir des emails Pour vous perfectionner: --self-- et  --self-- --self-- --self-- --self-- Et au-delà d'AppEngine: --self-- --self-- --self-- On continue ?

The cron servlet writes the name of the elected president into the datastore.
file: /src/com/me/president/CronServlet.java

public class CronServlet extends HttpServlet
{   @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException
    {
        Entity Elected = new Entity("Elected");
        Elected.setProperty("name", "Barack obama"); //or Mitt Romney
        DatastoreServiceFactory.getDatastoreService().put(Elected);
    }
}

Servlet mapping in WEB-INF/web.xml, with access restrictions:

<servlet>
   <servlet-name>CronServlet</servlet-name> <servlet-class>com.me.thepresident.CronServlet</servlet-class>
</servlet>
<servlet-mapping>
   <servlet-name>CronServlet</servlet-name> <url-pattern>/cronAllowPresidentDisplay</url-pattern>
</servlet-mapping>
<security-constraint>
  <web-resource-collection>   <url-pattern>/cron*</url-pattern>   </web-resource-collection>
  <auth-constraint>           <role-name>admin</role-name>        </auth-constraint>
</security-constraint>

Conditional display (file war/comments.jsp):

Entity e = DatastoreServiceFactory.getDatastoreService().prepare(new Query("Elected")).asSingleEntity();
if (e == null) /*wait message*/ else /*display president*/ e.getProperty("name");
9

Display the name of the president at 08:00pm sharp using a Cron job

The president is:

Barack Obama

Cron jobs are servlets. Their URL is defined in web.xml.

Their periodicity can be set in WEB-INF/cron.xml (full syntax):

<?xml version="1.0" ?>
<cronentries>
  <cron>
    <url>/cronAllowPresidentDisplay</url>
    <schedule>6 of May 20:00</schedule>
<!-- every 2 mins, every sunday 9:00 -->
    <timezone>Europe/Paris</timezone>
  </cron>
</cronentries>
La servlet: elle écrit le nom du président élu dans le datastore fichier: --self-- François Hollande ou un autre Son mapping dans WEB-INF/web.xml, avec restriction d'accès: Affichage conditionnel (fichier war/comments.jsp) message d'attente affichage du président --self-- Afficher le nom du président à 20h00 précises avec une tâche planifiée Cron Le président est... François Hollande Les Cron lancent des servlets dont l'URL est définie dans web.xml. Périodicité à configurer dans WEB-INF/cron.xml ( syntaxe complète ) :

What is a sharded counter ?

Writes to the same datastore entity are limited to a few
writes per second: so how can I implement a counter ?
⇒Atomic increment in Memcache and a sharded counter in Datastore.

Qu'est-ce qu'un compteur éclaté ? Les écritures sur la même entité du datastore sont limitées à quelques-unes par seconde: comment faire un compteur ? ⇒Incrément atomique dans Memcache et compteur éclaté dans Datastore.
import static com.me.president.ObjectifyDAO.ofy;

@Entity
public class Counter {
  @Id String key;
  @Index String name;
  @Index Long value;

  Counter() {}  // Empty constructor
  Counter(String name, Integer shard_N, Long val) {
    this.name = name;
    this.value = val;
    key = name + "_shard" + shard_N.toString();
  }

  public static void increment(String name) {
    MemcacheService cache =
      MemcacheServiceFactory.getMemcacheService();
    Long val = cache.increment(name, 1); // atomic
    if (val == null) {
       val = Counter.read(name);
       Long init = (val == null) ? 0L : val;
       // atomic increment with initial value
       val = cache.increment(name, 1, init);
    }
    Counter.write(name, val);
  }
      
  // Counter.java continued

  public static Long value(String name) {
    MemcacheService cache =
      MemcacheServiceFactory.getMemcacheService();
    Long val = (Long)cache.get(name);
    if (val == null) {
      val = Counter.read(name);
      if (val != null) {
        cache.put(name, val);
      }
    }
    return val;
  }

  static Long read(String name) {
    Counter c = ofy().load().type(Counter.class)
            .filter("name", name).order("-value")
                                    .first().get();
    return (c == null ? null : c.value);
  }

  static void write(String name, Long value) {
    Random r = new Random();
    Integer shard_N = r.nextInt(10);
    ofy().save().entity(
              new Counter(name, shard_N, value));
  }
}
      
usage:  Counter.increment("pluscounter");   Counter.value("minuscounter");
10

Implement a counter that scales under load

//key = name+shard_N
public class @Entity Counter {@Id String key; @Index String name; @Index Long value;}

The president is:

Mitt Romney
+1
(17K)
-1
(10K)

Atomic increment in Memcache:

Long C = MemcacheServiceFactory
.getMemcacheService().increment("myCounter", 1);

Save the new value in one of 10 counters in the datastore:

Integer shard_N = new Random().nextInt(10);
ofy().save().entity(new Counter(name,shard_N,C));

Read: if Memcache fails, MAX of the counters in the datastore:

ofy().load().type(Counter.class).filter("name",
             name).order("-value").first().get();
utilisation:    --self-- --self-- Implémenter un compteur qui tient la charge Le président est... Nicolas Sarkozy --self-- --self-- --self-- --self-- Memcache: incrément atomique Sauver la nouvelle valeur dans un parmi 10 compteurs du Datastore. Lecture: si échec du Memcache, MAX des compteurs du datastore.

file: war/WEB-INF/appengine/datastore-indexes.xml

<?xml version="1.0" encoding="utf-8"?>
<datastore-indexes autoGenerate="false">
     <datastore-index kind="Counter" ancestor="false" source="manual">
         <property name="name" direction="asc"/>
         <property name="value" direction="desc"/>
    </datastore-index>
</datastore-indexes>

The SDK generates this file automatically in WEB-INF/appengine-generated/datastore-indexes-auto.xml copy it to WEB-INF/appengine/datastore-indexes.xml

11

Add a composite index on an entity

 

fichier: war/WEB-INF/appengine/datastore-indexes.xml Le SDK autogénère ce fichier dans le le répertoire --self-- à copier dans --self-- --self-- Ajouter un index multiple sur une entité Le datastore exécute uniquement des requêtes indexées. Objectify crée un index simple sur chaque propriété annotée avec --self-- --self-- Si l'on fait une requête sur plus d'une propriété, il faut créer un index pour chaque combinaison manuellement. Pour notre compteur, il nous faut un index combiné avec "name" et "value" DESC

file: war/admin.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@page import="com.google.appengine.api.blobstore.BlobstoreServiceFactory"%>

<form action="<%= BlobstoreServiceFactory.getBlobstoreService().createUploadUrl("/upload") %>" 
	method="post" enctype="multipart/form-data">
    <label>President's name :   </label> <input type="text" name="president-name"/>
    <label>President's picture : </label> <input type="file" name="president-pic"/>
    <input type="submit"/>
</form>
file: UploadServlet.java (map it on /upload)
package com.me.president;

public class UploadServlet extends HttpServlet {
  @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp)
                        throws ServletException, IOException {
    BlobstoreService bs = BlobstoreServiceFactory.getBlobstoreService();
    Map<String, List<BlobKey>> blobs = bs.getUploads(req);
    BlobKey key = blobs.get("president-pic").get(0);
    String presUrl  = ImagesServiceFactory.getImagesService().getServingUrl(key);
    String presName = req.getParameter("president-name");
    resp.sendRedirect(presUrl); // just for debug
    // TODO: keep the name and the pic URL of the president in the datastore (using an
    // Objectify class) and use these values to display the elected president.
  }
}
12

Upload the photo of the president using Blobstore and ImageService.

Create a form for uploading a photo to a URL given by the Blobstore:

<% url = BlobstoreServiceFactory.getBlobstoreService().createUploadUrl("/upload"); %>
<form action="<%= url %>" method="post" enctype="multipart/form-data">
<input type="file" name="president-pic"/> ...

Get the Blobstore key of the picture in the servlet /upload:

void doPost(HttpServletRequest req, HttpServletResponse resp) {
BlobstoreService bs = BlobstoreServiceFactory.getBlobstoreService();
BlobKey key = bs.getUploads(req).get("president-pic").get(0); ...

Serve it to the client using the ImageService:

ImagesServiceFactory.getImagesService().getServingUrl(key);
fichier: war/admin.jsp Nom du président Photo du président fichier: UploadServlet.java (servlet à mapper sur /upload) juste pour débugger A FAIRE: conserver le nom et l'URL de l'image du président dans le datastore (une classe Objectify) et utiliser ces valeurs pour afficher le président à l'heure H. --self-- Uploader la photo du président grâce à Blobstore et ImageService Créer un formulaire d'upload vers une URL donnée par le Blobstore: Récupérer la clé de l'image dans la servlet  --self-- : La servir au client grâce à ImageService:

file: war/WEB-INF/web.xml

<security-constraint>
    <web-resource-collection>
        <url-pattern>/cron*</url-pattern>
        <url-pattern>/admin.jsp</url-pattern>
        <url-pattern>/upload</url-pattern>
    </web-resource-collection>
    <auth-constraint>
        <role-name>admin</role-name>
    </auth-constraint>
</security-constraint>
13

Secure admin URLs

 

fichier: war/WEB-INF/web.xml --self-- Protéger les URLs accessibles par les administrateurs Il suffit d'ajouter une contrainte de sécurité dans le fichier web.xml Indiquez le role : admin La vérification est faite automatiquement Les tâches planifiées sont lancées par Google App Engine en mode administrateur.

file: src/com/me/president/MailHandler.java

package com.me.president;
import java.util.Properties;
import javax.mail.*;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class MailHandlerServlet extends HttpServlet { 
    public void doPost(HttpServletRequest req, HttpServletResponse resp) throws Exception { 
        Properties props = new Properties(); 
        Session session = Session.getDefaultInstance(props, null); 
        MimeMessage message = new MimeMessage(session, req.getInputStream());
        Comment.store((String)((Multipart)message.getContent()).getBodyPart(0).getContent()
            , ((InternetAddress)message.getFrom()[0]).getAddress());
        Message reply = message.reply(false);
        reply.setFrom(new InternetAddress("noreply@cloudpresident.appspotmail.com"));
        reply.setText("Thank you for your opinion.");
        Transport.send(reply);
    }
}
14

Post comments by email

 

Enable email reception on address name@id_application.appspotmail.com

<inbound-services>
      <service>mail</service>
</inbound-services>

Create a servlet mapped on    /_ah/mail/*

To read the contents of an email:

Properties props = new Properties(); 
Session session = Session.getDefaultInstance(props, null); 
MimeMessage message = new MimeMessage(session, req.getInputStream());
fichier: --self-- Merci pour votre commentaire --self-- Recevoir un mail Indiquez que l'application peut recevoir des mails sur nom --self-- Créer une servlet mappé sur l'URL /_ah/mail/* Lire le contenu du mail :

Contact :
+Alexis Moussine-Pouchkine, Google
+Martin Görner, Google, gplus.to/martin.gorner
+Didier Girard, SFEIR

This presentation is available online:
http://cloudpresident.appspot.com

More info
http://developers.google.com/appengine

--self-- --self-- --self-- --self-- --self-- --self-- --self-- --self-- Cette présentation est en ligne : --self-- --self-- --self-- Plus d'infos : --self--