Google App Engine


Source in english

einf.xhtml

Das Prinzip realisiert das MVC-Pattern:

gapp_prinzip

Java Server Pages

Vereinbarungen (meist import) Java-Code Java-Code: <%= ... %> wird gegen die Ausschriften des Codes ersetzt
<%@ ... %>
<% ... %>
<%= ... %>

Beispiel:

<% a = 3; if (a < 5) { %>
	<p>Der Wert <%= a %> ist kleiner als 5</p>
<% } else { %>
        <p>Hallo%</p>
<% } %>

erzeugt:

Der Wert 3 ist kleiner als 5

Das Wort Hallo erscheint nie!

Der Persistenz-Speicher

Der Persistenz-Speicher benutzt eine verteilte Architektur um die Skalierbarkeit zu gewährleisten. Er besteht aus 2 Komponenten:

Der Persistenzspeicher speichert so genannte Entities bis zu einer Größe von 1 MB. Die Konfiguratation geschieht über eine Datei:

war/WEB-INF/classes/META-INF/jdoconfig.xml

<?xml version="1.0" encoding="utf-8"?>
<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>

Die Entities sind Java-Objekte mit so genannten Annotations (seit Java 5 vorhanden), die regeln, welche Teile das Objekts persistent gespeichert werden sollen:

src/Kandidat.java (gekürzt)

package wahl;

 ....

@PersistenceCapable(identityType = IdentityType.APPLICATION)
public class Kandidat {
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Key key;

    @Persistent
    private String name;

    @Persistent
    private int stimmen = 0;

    Kandidat(String name) { this.name = name; }
    public int getStimmen() { return this.stimmen; }
    public void incrStimme() { this.stimmen++; }
    public String getName() { return name; }
    public String createButton() {
        return "<input type=\"radio\" value=\"" + name + "\" name=\"Wahlbutton\"/>";
    }
}

Diese Klassen müssen einen Primary Key vom Typ Key haben!

Um den Persistenzspeicher zu nutzen, muss ein Persistenz-Manager erzeugt werden:

src/PMF.java

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;
    }
}

Erzeugen eines neuen persistenten Objekts:

PersistenceManager pm = PMF.get().getPersistenceManager();
try {
	pm.makePersistent(new Kandidat("Richard Wagner"));
} finally {
	pm.close();
}

Wenn der Key bekannt ist, kann auf ein spezielles Objekt mit:

Kandidat kand = pm.getObjectById(Kandidat.class, key)

zugegriffen werden. Ansonsten muss eine Liste erzeugt werden. Manipulationen des selektierten Objektes ändern auch das persistente Objekt:

src/PMF.java

pm = PMF.get().getPersistenceManager();
query = "select from " + Kandidat.class.getName();
kandidaten = (List<Kandidat>) pm.newQuery(query).execute();
for (Kandidat kandidat : kandidaten) {
	if (kandidat.getName().equals(gesuchter_name)) {
		kandidat.incrStimme();
		pm.close();
		return;
	}
}

Java-Servlets

Ein Servlet ist der Kern einer Technologie zur Bearbeitung von GET- und POST-Requests. Java bietet dafür die Klassen HttpServlet, HttpServletRequest und HttpServletResponse aus der Servlet-API an. Das HttpServlet bietet die leeren Methoden:

doGet(HttpServletRequest req, HttpServletResponse resp)

und

doPost(HttpServletRequest req, HttpServletResponse resp)

an. Praxis ist es, eine Subklasse von HttpServlet zu bilden und (eine) diese(r) Methoden zu überschreiben. Das Objekt req steht für den Request und die Parameter können mit über die Methode getParameter(String param) erfragt werden.

Das Objekt resp steht für die Antwort an den anfragenden WEB-Browser. Durch spezielle Methoden kann ein HTTP-Header erzeugt werden und durch die Methode setContentType(String content_type) kann der Content-Type: ... Header festgelegt werden. Dieser Header ist Pflicht! Durch sukzessive Rufe von resp.getWriter().println(String s) wird die Anwort an den WEB-Browser zusammengestellt. Ist der Content-Type text/html so sollte die Aneinanderreihung dieser Strings eine gültige HTML-Seite ergeben:

src/WahlServlet.java

import java.io.IOException;
import javax.servlet.http.*;
import javax.jdo.PersistenceManager;
import java.util.List;

public class WahlServlet extends HttpServlet {
        public void doPost(HttpServletRequest req, HttpServletResponse resp)
        throws IOException {
                String query = null;
                PersistenceManager pm = null;
                List<Kandidat> kandidaten = null;

                String wahlname = req.getParameter("Wahlbutton");
                if (wahlname != null) {
                        pm = PMF.get().getPersistenceManager();
                        query = "select from " + Kandidat.class.getName();
                        kandidaten = (List<Kandidat>) pm.newQuery(query).execute();
                        for (Kandidat kandidat : kandidaten) {
                                if (kandidat.getName().equals(wahlname)) {
                                        kandidat.incrStimme();
                                        break;
                                }
                        }
                        pm.close();
                        resp.sendRedirect("/");
                        return;
                }
                String neuname = req.getParameter("kandidat");
                if (neuname.length() < 1) {
                        resp.sendRedirect("/");
                        return;
                }
                pm = PMF.get().getPersistenceManager();
                query = "select from " + Kandidat.class.getName();
                kandidaten = (List<Kandidat>) pm.newQuery(query).execute();
                for (Kandidat kandidat : kandidaten) {
                        if (kandidat.getName().equals(neuname)) {
                                resp.setContentType("text/html");
                                resp.getWriter().println("<html><body><h1>Wahl</h1><hr/>");
                                resp.getWriter().println("<p>" + neuname + " steht bereits zur Wahl.</p>");
                                resp.getWriter().println("<p><a href=\"/\">Zurück</a></p></body></html>");
                                pm.close();
                                return;
                                }
                }
                try {
                        pm.makePersistent(new Kandidat(neuname));
                } finally {
                        pm.close();
                }
                resp.setContentType("text/html");
                resp.getWriter().println("<html><body><h1>Wahl</h1><hr/>");
                resp.getWriter().println("<p>" + neuname + " wurde als Kandidat aufgenommen.</p>");
                resp.getWriter().println("<p><a href=\"/\">Zurück</a></p></body></html>");
        }
}

Die Java-Server-Page hat folgenden Aufbau:

war/wahl.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>

<%@ page import="java.util.List" %>
<%@ page import="javax.jdo.PersistenceManager" %>
<%@ page import="wahl.Kandidat" %>
<%@ page import="wahl.PMF" %>

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
</head>
<body>
<h1>Hallo</h1>
<hr/>
<form action="/wahlauswertung" method="post">
<%
        boolean kands = true;
        PersistenceManager pm = PMF.get().getPersistenceManager();
        String query = "select from " + Kandidat.class.getName();
        List<Kandidat> kandidaten = (List<Kandidat>) pm.newQuery(query).execute();
        if (kandidaten.isEmpty()) {
                kands = false;
%>
                <p>keine Kandidaten</p>
<%      }
        else {
%>
                <p>Um einen Kandidaten zu wählen, selektieren Sie den Knopf
                    in der letzten Spalte der Tabelle. Um alle Kandidaten zu
                    löschen, selektieren Sie den Knopf hinter
                    <code>alle löschen</code>:
               </p>
                <table border="1"><tr><th>Name</th>
		<th>Stimmen</th><th>&nbsp;</th></tr>
<%
                        for (Kandidat kandidat : kandidaten) {
%>
                                <tr><td>
                                <%=kandidat.getName() %>
                                </td><td>
                                <%=kandidat.getStimmen() %>
                                </td><td>
                                <%=kandidat.createButton() %>
                                </td></tr>
<%                      } %>
                </table>
<%      } %>

<p>Um einen neuen Kandidaten hinzuzufügen,
<% if (kands) { %>
        wählen Sie niemanden und
<% } %>
                tragen Sie einfach des Neuen Namen hier ein:</p>
<p>Neuer Kandidat: <input type="text" name="kandidat"/></p>
<div><input type="submit" value="Senden"/></div>
</form>
</body>
</html>

URLs

Die Zuordnung von URLs zu JSPs geschieht über einen speziellen XML-Dialekt:

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 xmlns="http://java.sun.com/xml/ns/javaee" version="2.5">
    <servlet>
        <servlet-name>wahlservlet</servlet-name>
        <servlet-class>wahl.WahlServlet</servlet-class>
    </servlet>

    <servlet-mapping>
        <servlet-name>wahlservlet</servlet-name>
        <url-pattern>/wahlauswertung</url-pattern>
    </servlet-mapping>

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

Hier wird zum Beispiel wahl.jsp als Willkommens-Seite festgelegt. Somit wird bei Angabe des URIs / stets diese JSP geladen. So erklärt sich auch die Wirkung von resp.sendRedirect("/");.

Die URI /wahlauswertung führt zum Servlet wahlservlet, welches wiederum durch die Klasse WahlServlet aus dem Paket wahl repräsentiert wird. Dadurch erklärt sich der Action-URL:

<form action="/wahlauswertung" method="post">

Entwickeln und Upload

Das Betreiben einer Applikation in der Google App Engine ist (bis zu einer bestimmten Größenordnung) kostenlos. Allerdings benötigt man einen Account, welcher wiederum die Angabe einer E-Mail-Adresse und (später) einer Handynummer zwingend erfordert.

Zum Entwickeln kann man entweder mit Eclipse- Plugins arbeiten oder man kann den Apache Ant verwenden. Für letztere Variante lädt man sich das Google App Engine SDK herunter.

Apache Ant erwartet eine spezielle Verzeichnisstruktur:

verz_struk

Zum Erzeugen der ausführbaren Dateien benötigt man eine Datei build.xml:

build.xml

<project>
  <property name="sdk.dir" location="../appengine-java-sdk-1.2.6" />
  <import file="${sdk.dir}/config/user/ant-macros.xml" />
  <path id="project.classpath">
    <pathelement path="war/WEB-INF/classes" />
    <fileset dir="war/WEB-INF/lib">
      <include name="**/*.jar" />
    </fileset>
    <fileset dir="${sdk.dir}/lib">
      <include name="shared/**/*.jar" />
    </fileset>
  </path>
  <target name="copyjars"
      description="Copies the App Engine JARs to the WAR.">
    <copy
        todir="war/WEB-INF/lib"
        flatten="true">
      <fileset dir="${sdk.dir}/lib/user">
        <include name="**/*.jar" />
      </fileset>
    </copy>
  </target>
  <target name="compile" depends="copyjars"
      description="Compiles Java source and copies other source files to the WAR.">
    <mkdir dir="war/WEB-INF/classes" />
    <copy todir="war/WEB-INF/classes">
      <fileset dir="src">
        <exclude name="**/*.java" />
      </fileset>
    </copy>
    <javac
        srcdir="src"
        destdir="war/WEB-INF/classes"
        classpathref="project.classpath"
        debug="on" />
  </target>
  <target name="datanucleusenhance" depends="compile"
      description="Performs JDO enhancement on compiled data classes.">
    <enhance_war war="war" />
  </target>
  <target name="runserver" depends="datanucleusenhance"
      description="Starts the development server.">
    <dev_appserver war="war" />
  </target>
</project>

Dieses gibt dem Apache Ant die zu erzeugenden Ziele, wie Compilieren oder Starten und deren Abhängigkeiten vor. Eine genauere Erläuterung findet men unter http://code.google.com/intl/de-DE/appengine/docs/java/tools/ant.html

Um das Programm lokal zu testen, gibt man das Kommando:

ant runserver

Das Google App Engine SDK hat einen eigenen WEB-Server. Deshalb kann man die Anwendung durch Laden folgenden URLs ausprobieren:

http://localhost:8080

Um die Datei bei Google zu platzieren, bringt das Google App Engine SDK ein eigenes Upload-Script mit:

../appengine-java-sdk-1.2.6/bin/appcfg.sh update war

Voraussetzung ist ein Google-Account. Um diesen anzulegen, muss man dem Link Sign up auf der Google App Engine Seite folgen.

Dasselbe Script dient auch dem Ändern der Anwendung.

Die Quelltexte der diesem Abschnitt vorgestellten Beispielapplikation stehen unter http://vsr.informatik.tu-chemnitz.de/staff/jan/gappen/wahl/wahl.xhtml zur Verfügung.

Das Programm selbst kann unter http://diewahlderwahl.appspot.com ausprobiert werden.