dimarts, 27 de desembre del 2011

Una aproximació a Google App Engine. Com desenvolupar al núvol de Google.

Google App Engine és la plataforma  que ofereix Google i en la que es poden executar aplicacions web pròpies desenvolupades en Java, i d'altres llenguatges, de forma gratuita fins a un cert nivell de tràfic o recursos. L'interès de Google App Engine és que a l'estar-ne integrat, permet aprofitar els serveis de la "plataforma Google"i, a més, es disposa d'una API que permet accedir a aquests serveis.

Només cal tenir un compte a Google i registrar-se amb Google App Engine per a poder accedir a aquest servei de plataforma (Platform as a Service, o PaaS) i poder desenvolupar aplicacions en el núvol personal a Google.

Google App Engine és, doncs, una plataforma de cloud computing en la que podrem desplegar aplicacions web (per exemple, un CMS com Vosao) i disposarem d'APIS que permetran l'autenticacio , amb la resta de serveis que  tinguem a Google, com Docs,o enviar correus amb GMail, o

No confondre Google App Engine amb Google Apps (amb s). Aquest últim ve  a ser la versió "personalitzada" i depenent del grau de personalització, de pagament, dels serveis de Google .  Per cert, Apps també pot ser considerat una plataforma de desenvolupament. Google Apps Script permet crear macros per automatitzar tasques amb Javascript. Poso en cartera un post per parlar d'Apps i de les seves macros.

Però tornem a l'App Engine. Hi han  restriccions que cal tenir en compte. Entre d'altres: Només es pot fer servir un subconjunt de les classes del JRE estàndard, la "llista blanca de classes de JRE"; les aplicacions no poden crear fils (Thread); Els processos de servidor en resposta a peticions no poden trigar més de 30s; només es pot executar codi en resposta a peticions rebudes per HTTP; les aplicacions estan limitades pels frameworks que imposa Google i, per exemple, es pot consultar, afegir, modificar i esborrar dades de la base de dades NO-Relacional  BigTable que proporciona Google App Engine, però no es poden fer servir bases de dades relacionals com MySQL.

A la wikipedia podem trobar una bona descripció del Google App Engine. En tot cas, les especificacions i restriccions depenen de Google.

El que m'interessa a mi és com funciona des del punt de vista del desenvolupador. Res millor que seguir els demos de l'SDK de Google App Engine (GAE).

Cal el JDK 1.6 com a mínim.
L'SDK de Google App Engine per Java i la documentació (http://code.google.com/intl/ca-ES/appengine/downloads.html)

Un cop descarregat i instal·lat tot aquest material, em dirigeixo a la java Getting Started Guide, per a compilar les proves faré servir Apache Ant.

En aquest moment tinc a /home/albert la carpeta
appengine-java-sdk
Que he renombrat eliminant-ne el sufix amb el número de versió seguint les indicacions de la guia; i la carpeta
google-appengine-docs
que conté la documentació i que també he renombrat eliminat-ne el sufix amb la data de versió.

Ara puc provar l'aplicació Guestbook que va amb les demos de l'appengine, fent
./appengine-java-sdk/bin/dev_appserver.sh appengine-java-sdk/demos/guestbook/war 
des de la carpeta /home/albert

L'aplicació engega:

albert@athena:~$ ./appengine-java-sdk/bin/dev_appserver.sh appengine-java-sdk/demos/guestbook/war
31/12/2011 19:32:49 com.google.apphosting.utils.jetty.JettyLogger info
INFO: Logging to JettyLogger(null) via com.google.apphosting.utils.jetty.JettyLogger
31/12/2011 19:32:50 com.google.apphosting.utils.config.AppEngineWebXmlReader readAppEngineWebXml
INFO: Successfully processed /home/albert/appengine-java-sdk/demos/guestbook/war/WEB-INF/appengine-web.xml
31/12/2011 19:32:51 com.google.apphosting.utils.config.AbstractConfigXmlReader readConfigXml
INFO: Successfully processed /home/albert/appengine-java-sdk/demos/guestbook/war/WEB-INF/web.xml
31/12/2011 20:33:08 com.google.appengine.tools.development.DevAppServerImpl start
INFO: The server is running at http://localhost:8080/
31/12/2011 20:33:09 com.google.appengine.tools.development.DevAppServerImpl start
INFO: The admin console is running at http://localhost:8080/_ah/admin
31/12/2011 20:34:04 com.google.appengine.tools.development.LocalResourceFileServlet doGet
WARNING: No file found for: /favicon.ico

I fent http://localhost:8080


Puc deixar alguns missatges de prova:


També podem fer un cop d'ull a la consola d'administració, a http://localhost:8080/_ah/admin


Podem examinar el visor de dades El datastore viewer, o es pot examinar la finestra XMPP per als missatges de xats, el correu electrònic associat... En particular, al Datastore Viewer podrem examinar els missatges que ja hem afegit.

Com ho fa? a la carpeta appengine-java-sdk/demos/guestbook/war trobem el codi font de l'aplicació.

En un primer cop d'ull ens adonem que es tracta d'una aplicació web java gairebé estàndar: hi trobem jsp i css i una carpeta WEB-INF amb els habituals sub-carpetes classes i lib. A més també hi trobo el web.xml de les aplicacions web i, como a variació sobre el estàndar, un document appengine-web.xml.

En aquest cas,  appengine-web.xml només informa de la versió de l'aplicació i del fitxer de propietats del sistema de logs. Sobre aquest últim, dir que el sistema de logs del GAE és similar al que hom implementa amb log4j.

Vet aquí l'appengine-web.xml


<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
<application/>
<version>1</version>
<system-properties>
<property name="java.util.logging.config.file" value="WEB-INF/logging.properties"/>
</system-properties>
</appengine-web-app>


A continuació, examino la resta del codi.

Primer de tot, la pàgina guestbook.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.util.List" %>
<%@ page import="com.google.appengine.api.users.User" %>
<%@ page import="com.google.appengine.api.users.UserService" %>
<%@ page import="com.google.appengine.api.users.UserServiceFactory" %>
<%@ page import="com.google.appengine.api.datastore.DatastoreServiceFactory" %>
<%@ page import="com.google.appengine.api.datastore.DatastoreService" %>
<%@ page import="com.google.appengine.api.datastore.Query" %>
<%@ page import="com.google.appengine.api.datastore.Entity" %>
<%@ page import="com.google.appengine.api.datastore.FetchOptions" %>
<%@ page import="com.google.appengine.api.datastore.Key" %>
<%@ page import="com.google.appengine.api.datastore.KeyFactory" %>


<html>
  <head>
    <link type="text/css" rel="stylesheet" href="/stylesheets/main.css" />
  </head>


  <body>


<%
    String guestbookName = request.getParameter("guestbookName");
    if (guestbookName == null) {
        guestbookName = "default";
    }
    UserService userService = UserServiceFactory.getUserService();
    User user = userService.getCurrentUser();
    if (user != null) {
%>
<p>Hello, <%= user.getNickname() %>! (You can
<a href="<%= userService.createLogoutURL(request.getRequestURI()) %>">sign out</a>.)</p>
<%
    } else {
%>
<p>Hello!
<a href="<%= userService.createLoginURL(request.getRequestURI()) %>">Sign in</a>
to include your name with greetings you post.</p>
<%
    }
%>


<%
    DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
    Key guestbookKey = KeyFactory.createKey("Guestbook", guestbookName);
    // Run an ancestor query to ensure we see the most up-to-date
    // view of the Greetings belonging to the selected Guestbook.
    Query query = new Query("Greeting", guestbookKey).addSort("date", Query.SortDirection.DESCENDING);
    List<Entity> greetings = datastore.prepare(query).asList(FetchOptions.Builder.withLimit(5));
    if (greetings.isEmpty()) {
        %>
        <p>Guestbook '<%= guestbookName %>' has no messages.</p>
        <%
    } else {
        %>
        <p>Messages in Guestbook '<%= guestbookName %>'.</p>
        <%
        for (Entity greeting : greetings) {
            if (greeting.getProperty("user") == null) {
                %>
                <p>An anonymous person wrote:</p>
                <%
            } else {
                %>
                <p><b><%= ((User) greeting.getProperty("user")).getNickname() %></b> wrote:</p>
                <%
            }
            %>
            <blockquote><%= greeting.getProperty("content") %></blockquote>
            <%
        }
    }
%>


    <form action="/sign" method="post">
      <div><textarea name="content" rows="3" cols="60"></textarea></div>
      <div><input type="submit" value="Post Greeting" /></div>
      <input type="hidden" name="guestbookName" value="<%= guestbookName %>"/>
    </form>


    <form action="/guestbook.jsp" method="get">
      <div><input type="text" name="guestbookName" value="<%= guestbookName %>"/></div>
      <div><input type="submit" value="Switch Guestbook" /></div>
    </form>


  </body>
</html>

La pàgina comença fent la importació de les classes del Google App Engine que farà servir després.

<%@ page import="com.google.appengine.api.users.User" %>
<%@ page import="com.google.appengine.api.users.UserService" %>
<%@ page import="com.google.appengine.api.users.UserServiceFactory" %>
<%@ page import="com.google.appengine.api.datastore.DatastoreServiceFactory" %>
<%@ page import="com.google.appengine.api.datastore.DatastoreService" %>
<%@ page import="com.google.appengine.api.datastore.Query" %>
<%@ page import="com.google.appengine.api.datastore.Entity" %>
<%@ page import="com.google.appengine.api.datastore.FetchOptions" %>
<%@ page import="com.google.appengine.api.datastore.Key" %>
<%@ page import="com.google.appengine.api.datastore.KeyFactory" %>


A continuació, determina si l'usuari està logat amb  userService.getCurrentUser(); per a poder presentar l'opció de logar-se (userService.createLoginURL(request.getRequestURI())) o deslogar-se (userService.createLogoutURL(request.getRequestURI())) .

Segueix amb la preparació de la "base de dades": el DataStore. Per defecte el llibre de visites és el "default". Es podran crear altres llibres de visites amb el paràmetre guestbookName. Aquest paràmetre es podrà modificar amb el formulari que presenta la pàgina.

Amb Key guestbookKey = KeyFactory.createKey("Guestbook", guestbookName); es crea el llibre de visites (o s'obre per accedir-hi si ja existia) i se n'obté la clau de referència (compte que els DataStore NO són bases de dades relacionals!).

A continuació obté la llista dels cinc darrers missatges del llibre de visites ordenats per data, des del més recent:

Query query = new Query("Greeting", guestbookKey).addSort("date", Query.SortDirection.DESCENDING);
List<Entity> greetings = datastore.prepare(query).asList(FetchOptions.Builder.withLimit(5))


Si no hi han missatges, n'informa:

if (greetings.isEmpty()) {
        %>
        <p>Guestbook '<%= guestbookName %>' has no messages.</p>
        <%


I si n'hi han, itera per la llista presentant-ne l'autor i el text del missatge:

} else {
        %>
        <p>Messages in Guestbook '<%= guestbookName %>'.</p>
        <%
        for (Entity greeting : greetings) {
            if (greeting.getProperty("user") == null) {
                %>
                <p>An anonymous person wrote:</p>
                <%
            } else {
                %>
                <p><b><%= ((User) greeting.getProperty("user")).getNickname() %></b> wrote:</p>
                <%
            }
            %>
            <blockquote><%= greeting.getProperty("content") %></blockquote>
            <%
        }
    }


Finalment, la pàgina presenta els camps en el que es pot introduir el missatge i seleccionar el llibre de visites en el que es vol desar.
Internament, els dos camps s'organitzen en dos formularis HTML, el primer invoca al servlet /sign i li passa el text del missatge i el llibre de visites en el que ha d'inserir-lo. El segon formulari auto-invoca la mateixa pàgina guestbook.jsp i permet modificar el paràmetre guestbookName (per defecte,  default), es dir, permet crear un llibre de vistes nou, o seleccionar-ne un d'existent.


<form action="/sign" method="post">
<div><textarea name="content" rows="3" cols="60"></textarea></div>
<div><input type="submit" value="Post Greeting" /></div>
<input type="hidden" name="guestbookName" value="<%= guestbookName %>"/>
</form>


<form action="/guestbook.jsp" method="get">
<div><input type="text" name="guestbookName" value="<%= guestbookName %>"/></div>
<div><input type="submit" value="Switch Guestbook" /></div>
</form>


La pàgina guestbook.jsp fa molta de l'operativa del llibre de visites tota sola, però cal alguna cosa més. a  a la carpeta appengine-java-sdk/demos/guestbook/src també trobo el codi font del parell de servlets que completen l'aplicació (més un parell de classes auxiliars):

Els servlets són invocats pels formularis anteriors. A web.xml (a la carpeta appengine-java-sdk/demos/guestbook/war/WEB-INF/web.xml) veig que


<web-app xmlns="http://java.sun.com/xml/ns/javaee" version="2.5">

<servlet>

<servlet-name>guestbook</servlet-name>

<servlet-class>guestbook.GuestbookServlet</servlet-class>
</servlet>

<servlet-mapping>

<servlet-name>guestbook</servlet-name>

<url-pattern>/guestbook</url-pattern>
</servlet-mapping>

<servlet>

<servlet-name>sign</servlet-name>

<servlet-class>guestbook.SignGuestbookServlet</servlet-class>
</servlet>

<servlet-mapping>

<servlet-name>sign</servlet-name>

<url-pattern>/sign</url-pattern>
</servlet-mapping>

<welcome-file-list>

<welcome-file>guestbook.jsp</welcome-file>
</welcome-file-list>
</web-app>


Per tant  /sign es refereix a SignGuestbookServlet.

package guestbook;


import java.io.IOException;
import java.util.Date;
import java.util.logging.Logger;
import javax.jdo.PersistenceManager;
import javax.servlet.http.*;
import com.google.appengine.api.users.User;
import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;


import guestbook.Greeting;
import guestbook.PMF;


public class SignGuestbookServlet extends HttpServlet {
    private static final Logger log = Logger.getLogger(SignGuestbookServlet.class.getName());


    public void doPost(HttpServletRequest req, HttpServletResponse resp)
                throws IOException {
        UserService userService = UserServiceFactory.getUserService();
        User user = userService.getCurrentUser();


        String content = req.getParameter("content");
        Date date = new Date();
        Greeting greeting = new Greeting(user, content, date);


        PersistenceManager pm = PMF.get().getPersistenceManager();
        try {
            pm.makePersistent(greeting);
        } finally {
            pm.close();
        }


        resp.sendRedirect("/guestbook.jsp");
    }
} 

Hi veig la importació de les classes de l'AppEngine que permeten fer l'autenticació (User, UserService i UserServiceFactory). També fa la importació de les classes auxiliars Greeting i PMF.

La persistència de dades s'aconsegueix amb JDO. Greeting correspon al model. i PMF és el PersistenceManagerFactory.

Greeting:


package guestbook;


import java.util.Date;
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;
import com.google.appengine.api.users.User;


@PersistenceCapable(identityType = IdentityType.APPLICATION)
public class Greeting {
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Long id;


    @Persistent
    private User author;


    @Persistent
    private String content;


    @Persistent
    private Date date;


    public Greeting(User author, String content, Date date) {
        this.author = author;
        this.content = content;
        this.date = date;
    }


    public Long getId() {
        return id;
    }


    public User getAuthor() {
        return author;
    }


    public String getContent() {
        return content;
    }


    public Date getDate() {
        return date;
    }


    public void setAuthor(User author) {
        this.author = author;
    }


    public void setContent(String content) {
        this.content = content;
    }


    public void setDate(Date date) {
        this.date = date;
    }
}

i PMF:


package guestbook;


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


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


    private PMF() {}


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



A appengine-java-sdk/demos/guestbook/war/WEB-INF/classes/META-INF trobarem el fitxer de configuració de JDO: el jdoconfig.xml ,on es diu que es farà servir el appengine per a la persistència.


<jdoconfig xmlns="http://java.sun.com/xml/ns/jdo/jdoconfig" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:noNamespaceSchemaLocation="http://java.sun.com/xml/ns/jdo/jdoconfig">

<persistence-manager-factory name="transactions-optional">

<property name="javax.jdo.PersistenceManagerFactoryClass"value="org.datanucleus.store.appengine.jdo.DatastoreJDOPersistenceManagerFactory"/>

<property name="javax.jdo.option.ConnectionURL" value="appengine"/>

<property name="javax.jdo.option.NontransactionalRead" value="true"/>

<property name="javax.jdo.option.NontransactionalWrite" value="true"/>

<property name="javax.jdo.option.RetainValues" value="true"/>

<property name="datanucleus.appengine.autoCreateDatastoreTxns" value="true"/>
</persistence-manager-factory>
</jdoconfig>


Amb JDO, gravar la informació és tan senzill com instanciar el model (Greeting greeting = new Greeting(user, content, date);) i fer-lo persistent (pm.makePersistent(greeting);)

L'altre servlet és GuestbookServlet


package guestbook;


import java.io.IOException;
import javax.servlet.http.*;
import com.google.appengine.api.users.User;
import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;


public class GuestbookServlet extends HttpServlet {
    public void doGet(HttpServletRequest req, HttpServletResponse resp)
              throws IOException {
        UserService userService = UserServiceFactory.getUserService();
        User user = userService.getCurrentUser();


        if (user != null) {
            resp.setContentType("text/plain");
            resp.getWriter().println("Hello, " + user.getNickname());
        } else {
            resp.sendRedirect(userService.createLoginURL(req.getRequestURI()));
        }
    }
}

Aquest segon servlet no s'invoca directament en el codi. Es tracta d'un servlet que ens mostra com utilitzar la Users API per a, depenent de si l'usuari està logat o no, fer una acció o altre. Per a invocar-lo directament faig 

http://localhost:8080/guestbook. 

Suposant que no m'he logat abans (fent click a sign in), El resultat és que es mostra el següent:



En l'entorn amb el que faig les proves, la invocació a  createLoginURL porta a una pantalla com l'anterior. El que fa el servlet és que si l'usuari no estàlogat, redirecciona a aquesta pantalla de login. Si estiguéssim a l'entorn "de  producció" de Google, aleshores ens dirigiríem a la pàgina de login de Google.  

Tornant a l'entorn de prova, si en canvi hagués estat logat  obtindria:



Una aplicació ben senzilla. Tampoc hi ha gaire cosa a explicar del codi.

Un cop ja tinc l'aplicació en funcionament, ve el moment de desplegar-la en producció. He de fer "el deploy".

El desplegament és també molt senzill. Primer de tot cal identificar-se al Google App Engine. Accedeixo a la web http://code.google.com/intl/ca-ES/appengine/ i faig click a Sign Up.



I creo una aplicació. Click a Create Application.


Em demana un número de telèfon per verificar la creació   



Des de Barcelona, el codi de regió és +34. Rebo el codi i l'he de posar a la següent pantalla.



Ara he d'emplenar un formulari de registre de l'aplicació. Poso el títol  de l'aplicació, i l'identificador. Fixem-nos que aquí es defineix la URL d'accés de l'aplicació. En aquest cas serà

http://stsoftlliure-proves.appspot.com



Accepto les condicions del servei i, finalment, l'aplicació queda registrada.

 

Ara que ja tinc preparat l'espai on ubicar l'aplicació, faig el deploy des de la línia de comandes, dent servir l'aplicació appcfg.h que es proporciona amb l'SDK de l'App Engine. L'aplicació ens demanarà el correu gmail del compte i el password. Es connectarà a Internet i desplegarà l'aplicació. Fixem-nos que no cal crear un fitxer .war.


albert@apolo:~/appengine-java-sdk/bin$ appcfg.sh -A stsoftlliure-proves update /home/albert/appengine-java-sdk/demos/guestbook/war
Reading application configuration data...
19/02/2012 13:16:45 com.google.apphosting.utils.config.AppEngineWebXmlReader readAppEngineWebXml
INFO: Successfully processed /home/albert/appengine-java-sdk/demos/guestbook/war/WEB-INF/appengine-web.xml
2012-02-19 13:16:45.947:INFO::Logging to STDERR via org.mortbay.log.StdErrLog
19/02/2012 13:16:46 com.google.apphosting.utils.config.AbstractConfigXmlReader readConfigXml
INFO: Successfully processed /home/albert/appengine-java-sdk/demos/guestbook/war/WEB-INF/web.xml
19/02/2012 13:16:46 com.google.apphosting.utils.config.IndexesXmlReader readConfigXml
INFO: Successfully processed /home/albert/appengine-java-sdk/demos/guestbook/war/WEB-INF/appengine-generated/datastore-indexes-auto.xml
Beginning server interaction for stsoftlliure-proves...
0% Created staging directory at: '/tmp/appcfg2668004339756750410.tmp'
5% Scanning for jsp files.
8% Compiling jsp files.
19/02/2012 13:16:55 com.google.apphosting.utils.config.AbstractConfigXmlReader readConfigXml
INFO: Successfully processed /tmp/appcfg2668004339756750410.tmp/WEB-INF/web.xml
20% Scanning files on local disk.
25% Initiating update.
28% Cloning 1 static files.
31% Cloning 27 application files.
40% Uploading 1 files.
52% Uploaded 1 files.
61% Initializing precompilation...
68% Sending batch containing 1 file(s) totaling 1KB.
90% Deploying new version.
95% Will check again in 1 seconds.
98% Will check again in 2 seconds.
99% Will check again in 4 seconds.
99% Closing update: new version is ready to start serving.
99% Uploading index definitions.


Update completed successfully.
Success.
Cleaning up temporary files...
albert@apolo:~/appengine-java-sdk/bin$ 

Verifico que la nova aplicació ja és visible al Dashboard...

 

...I ja puc fer servir la meva nova aplicació instal·lada al Google App Engine.

Accedeixo a l'adreça http://stsoftlliure-proves.appspot.com i puc provar l'aplicació:

Cap comentari:

Publica un comentari a l'entrada