Mittwoch, 22. April 2009

Google App Engine Java Tutorial (einfache CRUD Anwendung)



In diesem Tutorial werde ich zeigen, wie man eine einfache CRUD (Create, Update, Delete) Anwendung für die Google App Engine schreibt. Dabei wird Googles Implementierung des JDO Datastore benutzt.

Die Dokumentation des JDO Datastore ist leider nicht für die Erstellung einer CRUD Anwendung geeignet. Daher nun hier meine Version.

Wir werden eine Fachklasse erzeugen, die ein Attribut hat. Die Objekte dieser Fachklasse werden in der Google Cloud gespeichert (via JDO), ausgelesen, bearbeitet und können dann auch wieder gelöscht werden. Die Ausgabe aller gespeicherten Objekte soll in einer HTML-Liste geschehen und die Bearbeitung der Objekte wird über ein HTML-Formular realisiert (siehe Abbildung oben).

1. "Web Application Project" in Eclipse erstellen
Hier kann die Google Dokumentation verwendet werden. Allerdings verwenden wir als Projektnamen "CRUD" und als Packetnamen "thobach". Das Google Web Toolkit benötigen wir nicht.


2. Singleton für PersistenceManagerFactory erstellen
In dem Package "thobach" erzeugen wir eine Java-Klasse mit dem Namen "PMF" und folgendem Inhalt:

package thobach;

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

Diese Klasse sichert uns einen schnellen Zugriff auf die PersistenceManagerFactory, die sonst bei ständiger Neuerzeugung viel Zeit benötigen würde.

3. Erzeugung der Fachklasse "Cocktail"
Im Package "thobach" erzeugen wir eine weitere Java-Klasse mit dem Namen "Cocktail". Diese wird unsere Business-Logik enthalten (anzeigen, speichern, bearbeiten, löschen) und hat folgenden Inhalt:

package thobach;

import java.util.List;

import javax.jdo.PersistenceManager;
import javax.jdo.Query;
import javax.jdo.annotations.*;

import thobach.PMF;


@PersistenceCapable(identityType = IdentityType.APPLICATION)
public class Cocktail {

@PrimaryKey
@Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
private Long id;

@Persistent
private String name;

@NotPersistent
private static PersistenceManager pm;

public Long getId() {
return id;
}

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

public String getName() {
return name;
}

/**
* Creates a new Cocktail object with the given name
*
* @param name
* String with the name of the Cocktail
*/
public Cocktail(String name) {
this.setName(name);
}

/**
* Returns the PersistenceManager for the Cocktail Class, creates one if not
* existent or closed
*
* @return PersistenceManager
*/
private static PersistenceManager getPersistenceManager() {
if (pm == null) {
pm = PMF.get().getPersistenceManager();
} else if (pm.isClosed()) {
pm = PMF.get().getPersistenceManager();
}
return pm;
}

/**
* Finds the Cocktail in the database by the given id
*
* @param id
* integer with the id of the Cocktail
* @return Cocktail object found in the database
*/
public static Cocktail find(int id) {
Cocktail cocktail = Cocktail.getPersistenceManager().getObjectById(
Cocktail.class, id);
return cocktail;
}

/**
* Makes the Cocktail object persistent
*
* @return boolean true if successful, false if not
*/
public boolean persist() {
boolean wasSuccessful;
try {
getPersistenceManager().makePersistent(this);
wasSuccessful = true;
} catch (Exception e) {
wasSuccessful = false;
} finally {
getPersistenceManager().close();
}
return wasSuccessful;
}

/**
* Deletes the Cocktail object from the persistent storage
*
* @return
*/
public boolean delete() {
boolean wasSuccessful;
try {
getPersistenceManager().deletePersistent(this);
wasSuccessful = true;
} catch (Exception e) {
wasSuccessful = false;
} finally {
getPersistenceManager().close();
}
return wasSuccessful;
}

/**
* Returns all Cocktails as a List from the persistent storage
*
* @return
*/
@SuppressWarnings("unchecked")
public static List findAll() {
String sqlFetchAll = "select from " + Cocktail.class.getName();
Query query = Cocktail.getPersistenceManager().newQuery(sqlFetchAll);
List cocktails = (List) query.execute();
return cocktails;
}

}
Wichtig ist hier, dass die Klasse "Cocktail" ihren eigenen PersistenceManager hat. Somit müssen die Cocktail-Objekte wenn sie aus der Goolge Cloud kommen nicht "detached" werden.

4. View-Klassen: CocktailList, CocktailForm und WebPage
Getreu dem MVC-Pattern (Model-View-Controller) erstellen wir im Folgenden noch drei View-Klassen.

Die Klasse CocktailList bekommt eine Liste mit Cocktails übergeben, die sie als HTML-Liste darstellen soll. Sie hat folgenden Inhalt:

package thobach;

import java.util.List;

import thobach.Cocktail;

public class CocktailList {

private List<Cocktail> cocktails;

public CocktailList(List<Cocktail> cocktails) {
this.cocktails = cocktails;
}

public String display() {
String output = "<h2>Exisiting cocktails:</h2>" + "<ul>";
for (Cocktail c : cocktails) {
output += "<li>" + c.getName() + " <a href=\"?id=" + c.getId()
+ "&amp;action=edit\">[edit]</a> <a href=\"?id="
+ c.getId() + "&amp;action=delete\">[delete]</a></li>";
}
output += "</ul>";
return output;
}

}


Die Klasse CocktailForm stellt ein Formular zum Hinzufügen und Bearbeiten von Cocktails dar und hat folgenden Inhalt:

package thobach;

import thobach.Cocktail;

public class CocktailForm {

private Cocktail editCocktail = null;

public CocktailForm(Cocktail editCocktail) {
if (editCocktail != null) {
this.editCocktail = editCocktail;
}
}

public String display() {
String output = "<form action=\"/crud\" method=\"post\">"
+ "<fieldset><legend>Cocktail</legend>"
+ "<label for=\"name\">Cocktail name:</label> "
+ "<input type=\"text\" name=\"name\" id=\"name\" value=\""
+ (editCocktail != null ? editCocktail.getName() : "")
+ "\" />"
+ (editCocktail != null ? "<input type=\"hidden\" name=\"id\" value=\""
+ editCocktail.getId() + "\" />"
: "")
+ (editCocktail == null ? "<input type=\"hidden\" name=\"action\" value=\"add\" />"
: "")
+ (editCocktail != null ? "<input type=\"hidden\" name=\"action\" value=\"edit\" />"
: "") + "<input type=\"submit\" value=\""
+ (editCocktail != null ? "edit" : "add") + " cocktail\" /> "
+ "<input type=\"submit\" name=\"action\" value=\"reset\" />"
+ "</fieldset>" + "</form>";
return output;
}

}

Um alles zusammen zu bauen, brauchen wir noch einen Container für die Liste und das Formular - unsere eigentliche Webseite mit folgendem Inhalt:

package thobach;

public class WebPage {

private String foot;
private String head;
private String content = "";

public WebPage(String title) {
head = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+ "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.1//EN\" \"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd\">"
+ "<html xmlns=\"http://www.w3.org/1999/xhtml\">" + "<head>"
+ "<title>" + title + "</title>" + "</head>" + "<body>";
foot = "</body>" + "</html>";
}

public String display() {
String output = head + content + foot;
return output;
}

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

}
5. Modifikation unserer Servlet-Klasse
Getreu dem MVC-Pattern stellt die Servlet-Klasse den Controller dar. Die Methoden doGet() und doPost() werden überschrieben. doPost() wird dabei die Formularbehandlung übernehmen und doGet() die Listendarstellung. In der init() Methode werden alle HTTP-Parameter abgefragt, displayContent() kümmert sich um die Ausgabe der View-Objekte.

package thobach;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import thobach.Cocktail;
import thobach.CocktailForm;
import thobach.CocktailList;
import thobach.WebPage;

@SuppressWarnings("serial")
public class CRUDServlet extends HttpServlet {

private String action;
private int id;
private String name;
private HttpServletRequest req;
private HttpServletResponse resp;

@Override
public void doGet(HttpServletRequest req, HttpServletResponse resp)
throws IOException {

init(req, resp);

// delete cocktail by id
if (action.equals("delete") && id > 0) {
Cocktail deleteCocktail = Cocktail.find(id);
deleteCocktail.delete();
id = 0;
}

displayContent();
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {

init(req, resp);

// add a new cocktail
if (action.equals("add") && !name.equals("")) {
Cocktail cocktail = new Cocktail(name);
cocktail.persist();
}

// edit cocktail by id - set name
if (action.equals("edit") && id > 0 && name != null && !name.equals("")) {
Cocktail editCocktail = Cocktail.find(id);
editCocktail.setName(name);
editCocktail.persist();
editCocktail = null;
id = 0;
}

displayContent();
}

/**
* Sets the content type and inits all given parameters
*
* @param _req
* @param _resp
* @throws IOException
*/
private void init(HttpServletRequest _req, HttpServletResponse _resp)
throws IOException {

req = _req;
resp = _resp;

resp.setContentType("text/xml");

// set name
name = "";
if (req.getParameter("name") != null
&& !req.getParameter("name").equals("")) {
name = req.getParameter("name");
}

// set id
id = 0;
if (req.getParameter("id") != null) {
id = Integer.parseInt(req.getParameter("id"));
}

// set action
action = "";
if (req.getParameter("action") != null
&& !req.getParameter("action").equals("")) {
action = req.getParameter("action");
}
}

private void displayContent() throws IOException {
// create XHTML page
WebPage page = new WebPage("Cocktail Database");
page.addContent("<h1>Cocktail Database</h1>");
// add list with cocktails
List<Cocktail> cocktails = Cocktail.findAll();
CocktailList list = new CocktailList(cocktails);
page.addContent(list.display());
// add form to add and edit cocktails
Cocktail editCocktail = null;
if (id > 0) {
editCocktail = Cocktail.find(id);
}
CocktailForm form = new CocktailForm(editCocktail);
page.addContent(form.display());
// print generated XHTML page
PrintWriter out = resp.getWriter();
out.println(page.display());
}
}

Nach dem Anlegen der Klassen müsste das Projekte folgende Struktur haben:

Das wars auch schon. Um das Projekt zu testen, wähle Run -> Run as ... -> Web Application und öffne im Browser die URL http://localhost:8080/.

Der komplette Code kann auch bei Google Code heruntergeladen werden.

Fragen und Kommentare sind gern willkommen. Viel Spaß!

3 Kommentare,:

  1. Endlich Java auf der AppEngine! Danke für das hilfreiche Tutorial.
    AntwortenLöschen
  2. ich krieg einen fehler wenn ich einen Cocktail eintragen will, sowohl lokal als auch auf dem appspot ... error: http://pastebin.org/51939

    bitte um Hilfe
    AntwortenLöschen
  3. In CocktailList gab es ein XML-Parser Problem, wenn man im Edit- und Delete-Link ein & statt &amp; verwendet hat. Ich habe es im Code repariert.
    AntwortenLöschen