Sonntag, 19. Juni 2011

GWT Request Factory - ein kleines Beispiel (CRUD Example)


I. Ziel dieses Beispiels
In diesem Blog stellen wir ein kleines GWT RequestFactory - Beispiel vor, das die vier CRUD-Operationen (neu, laden, speichern und löschen) abdeckt. Das Laden wird hier in Form von < (prev) und > (next) realisiert. Das Ergebnis dieses Blogs ist in nachfolgendem Bild zu sehen:

Abbildung 1: Eine kleine CRUD-Applikation mit GWT Requestfactory

Sie können das vollständige Beispiel als Eclipse Projekt downloaden unter:
http://www.langlaufen.de/gwt-blog/downloads/RequestFactoryExampleI.zip


II. Erforderliche Konfiguration
Dieses Beispiel basiert verwendet folgende IDE-Komponenten / Konfiguration:
a) Eclipse Helios: http://www.eclipse.org/downloads/ (beachten Sie, dass die verwendete JDK-Version mit der Eclipse-Version korrespondiert 32bit/64bit). 
b) Google Plugin for Eclipse (GPE 2.3): http://code.google.com/intl/de-DE/eclipse/
c) JSON.jar: Durch die Installation des GPE wird das json.jar nicht automatisch mitinstalliert. Wenn Sie das json.jar nicht zum ClassPath hinzufügen erhalten Sie z.B. nachfolgende Fehlermeldung:

Unable to load module entry point class ... java.lang.NoClassDefFoundError: org/json/JSONException

Kopieren Sie das json.jar in das WEB-INF/lib - Verzeichnis und fügen Sie es zusätzlich zum ClassPath des Projektes hinzu:

Abbildung 3: Hinzufügen des json.jar zum ClassPath des Projektes
d) App Engine Settings: Das Projekt ist nicht für die Verwendung der Google App Engine konfiguriert.
Abbildung 4: Google Projekt Properties / App Engine

e) Einträge in der web.xml: Fügen Sie der web.xml folgende Einträge hinzu:
<servlet>
<servlet-name>requestFactoryServlet</servlet-name>
<servlet-class>com.google.web.bindery.requestfactory.server.RequestFactoryServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>requestFactoryServlet</servlet-name>
<url-pattern>/gwtRequest</url-pattern>
</servlet-mapping>


Fehlen diese Einträge, so erhalten Sie beim ersten Request z.B. folgende Fehlermeldung:
Error 404 NOT_FOUND

Damit ist unser Projekt für die Verwendung der RequestFactory konfiguriert.

III. Projektstruktur
Zu Realisierung unseres Beispiels benötigen wir folgende Java-Klassen:
a) RequestFactoryExampleI: diese Klasse stellt die eigentliche Applikation dar und implementiert den EntryPoint. Die Klasse muss im oben dargestellten Client-Package liegen.
b) Person: Die Person Klasse residiert serverseitig und ist eine reine Java Klasse, die nicht den clientseitigen Einschränkungen unterliegt (Verwendung der zulässigen Datentypen, JavaLibs etc). 
c) PersonProxy: Um auf das Domain Objekt Person, zugreifen zu können, benötigt man ein (oder mehrere) Proxy Interfaces. Das PersonProxy ist das Interface, dass zum Zugriff auf die Klasse Person definiert ist. Das Proxy-Interface beinhalten ausschließlich Getter und Setter Definitionen.
d) PersonRequest: Request Interfaces definieren Deklarationen zum Zugriff auf Methoden der Domain Klasse (hier z.B. auf die Klasse Person). 
e) RequestFactoryX: dieses Interface beinhaltet die verfügbaren Request Interfaces der Applikation. Die Komponenten c) bis e) sollten im shared Paket der Applikation gespeichert werden. 

Die gesamte Projektstruktur sehen Sie nachfolgend:
Abbildung 2: Projektstrutur des Beispiels 

IV. Wichtige Regeln

a) Jeder RequestContext kann nur einmal zur Übermittlung eines Request verwendet werden (d.h. Verwendung der .fire-Methode). Für jeden weiteren Request muss ein eigener RequestContext generiert werden.
b) Ein EntityProxy kann immer nur genau einem RequestContext zugewiesen und in diesem Context für einen Request verwendet werden.
c) Ein EntityProxy kann immer nur von genau einem RequestContext editiert werden.
d) Ein per RequestContext bezogenes Objekt ist zunächst immutable (unveränderlich, d.h. readonly), um es zu editieren (d.h. Verwendung der Setter) muss es in den Editierzustand versetzt werden. Da dieses EntityProxy bereits über einen RequestContext bezogen wurde, dieser RequestContext jedoch nicht erneut verwendet werden kann, muss ein neues EntityProxy generiert werden, das auch einem neuen RequestContext zugeordnet ist. Dazu muss wie folgt vorgegangen werden:

  • Generieren eines neuen RequestContext
  • Generieren einer editierbaren Version des EntityProxies
Beispiel:
PersonRequest requestContextNeu = requestFactory.personRequest();
PersonProxy personProxyNeu = requestContextNeu.edit(personProxyBisher);

e) Wird ein EntityProxy nicht mittels RequestContext vom Server bezogen, sondern mittels der .create - Methode des ContextProxy erstellt  (d.h. der RequestContext ist noch für keinen Request verwendet, die .fire Methode des RequestContext ist noch nicht ausgelöst worden), so ist dieses EntityProxy per default editierbar (d.h. mutable). Dieses EntityProxy darf nicht erneut in den Editierstatus versetzt werden, da hier Regel c) gilt.
f) Im EntityProxy dürfen nur Getter und Setter der Domain-Klasse verwendet werden (keine Methoden etc.). Die Deklaration muss exakt mit der Deklaration in der Domain-Klasse übereinstimmen. Es müssen aber nicht alle Getter / Setter der Domainklasse verwendet werden.  Jede Verwendung eines Setters erfordert auch den zugehörigen Getter.
g) Im RequestContext dürfen nur Methoden der Domain-Klasse verwendet werden (Ausnahme: die Implementierung der Methoden erfolgt in einer eigenen Service Klasse, diese Variante wird hier nicht behandelt). Die Deklarationen müssen exakt mit der Domain-Klasse korrespondieren. Es müssen aber nicht alle Methoden der Domainklasse verwendet werden
h) Eie Entity(Domain)-Klasse   m u s s    folgende drei Methoden implementiert haben:

  • public Long getId() {...}
  • public Integer getVersion() {...}
  • public static Person findPerson(Long id) {...}
Die obige Klassenbezeichnung 'Person' ist durch die jeweilige Entity-Klassenbezeichnung zu ersetzen. Zu beachten ist, dass die Parametertyp der Find-Methode (hier Long), mit dem Rückgabeparametertyp der 'getId()' -Methode korrespondieren muss. D.h. wenn die getId()-Methode als Parametertyp einen Long-Wert zurückliefert, muss auch die find-Methode einen Long-Wert als Parametertyp verwenden.Im RequestContext ist diese Deklaration nur erforderlich, wenn diese Methoden im Programm verwendet werden sollen.

V. Mögliche Probleme und deren Ursachen
Nachfolgend werden einigen Fehler und deren möglichen Ursachen beschrieben (nicht abschließend):

HTTP ERROR: 500 - INTERNAL_SERVER_ERROR
RequestURI=/gwtRequest
  • fehlerhafte Konfiguration
  • fehlerhafte ProxyFor/ServiceFor Annotationen
  • die deklarierten Methoden im Proxy bzw. RequestContext haben kein Equivalent in der Entity- bzw. Serviceklasse: es existiert z.B. eine Getter-Deklaration im Proxy-Interface, dazu gibt es keine entsprechende Getter Implementierung in der Entity-Klasse. Das Problem tritt auch auf, wenn die Getter-Methode im Programmcode gar nicht angesprochen wird.
  • die Entity-Klasse muss die drei Methoden implementiert haben:
    • public Long getId() {...}
    • public Integer getVersion() {...}
    • public static Person findPerson(Long id) {...}. Die Klassenbezeichnung 'Person' ist durch die jeweilige Entity-Klassenbezeichnung zu ersetzen. Zu beachten ist, dass die Parametertyp der Find-Methode (hier Long), mit dem Rückgabeparametertyp der 'getId()' -Methode korrespondieren muss. D.h. wenn die getId()-Methode als Parametertyp einen Long-Wert zurückliefert, muss auch die find-Methode einen Long-Wert als Parametertyp verwenden. Im RequestContext ist diese Deklaration nur erforderlich, wenn diese Methoden im Programm verwendet werden sollen.
  • Verwendung eines Setters ohne zugehörigen Getter


Error 404 NOT_FOUND
  • fehlende Einträge in der web.xml (siehe oben)

Unable to load module entry point class ... java.lang.NoClassDefFoundError: org/json/JSONException
  • json.jar ist nicht im ClassPath

The autobean has been frozen
  • das EntityProxy ist nicht im editierbaren Zustand

Attempting to edit an EntityProxy previously edited by another RequestContext
  • das EntityProxy muss durch den RequestContext in den Editierzustand versetzt worden sein, der auch den Request übernehmen soll

VI. Source Code
Nachfolgend finden Sie den Source Code zu diesem Beispiel. Im Source Code sind einige Kommentare enthalten, die die genaue Verwendung von RequestContext und EntityProxy verdeutlichen. Die Domain Klasse Person implementiert einen einfachen Datenspeicher auf Basis einer HashMap. Der Fokus dieses Beispiel liegt auf der Verwendung der RequestFactory Komponenten. Alle anderen Aspekte wurden deshalb so einfach wie möglich gehalten.

Klasse: RequestFactoryExampleI

package de.langlaufen.gwt.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.shared.EventBus;
import com.google.gwt.event.shared.SimpleEventBus;
import com.google.gwt.user.client.ui.AbsolutePanel;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.DecoratorPanel;
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.web.bindery.requestfactory.shared.Receiver;
import com.google.web.bindery.requestfactory.shared.ServerFailure;
import de.langlaufen.gwt.shared.PersonProxy;
import de.langlaufen.gwt.shared.PersonRequest;
import de.langlaufen.gwt.shared.RequestFactoryX;
public class RequestFactoryExampleI implements EntryPoint {
private static final EventBus eventBus = new SimpleEventBus();
private static final RequestFactoryX requestFactory = GWT.create(RequestFactoryX.class);
private PersonProxy personProxy = null;
private PersonRequest personRequest = null;
final TextBox idTextbox = new TextBox();
final TextBox vornameTextbox = new TextBox();
final TextBox nachnameTextbox = new TextBox();
private void createNew() {
// ein neues personProxy wird über die .create-Methode erzeugt. Es ist
// damit per default mutable (editierbar). Da es dem hier verwendeten
// personRequest zugeordnet ist
// kann es auch nur in diesem Context für Requests verwendet werden.
// Jedes entityProxy kann gleichzeitig nur von genau einem
// requestContext in den Editierstatus versetzt werden
// es ist somit nicht möglich, diese Objekt erneut in den EditMode zu
// versetzen.
personRequest = requestFactory.personRequest();
personProxy = personRequest.create(PersonProxy.class);
}
private void populate(PersonProxy personProxy) {
if (personProxy.getId() == null) {
idTextbox.setText("<neu>");
} else {
idTextbox.setText(personProxy.getId().toString());
}
vornameTextbox.setText(personProxy.getVorname());
nachnameTextbox.setText(personProxy.getNachname());
}
public void onModuleLoad() {
requestFactory.initialize(eventBus);
RootPanel rootPanel = RootPanel.get();
DecoratorPanel decPanel = new DecoratorPanel();
decPanel.setSize("290px", "180px");
rootPanel.add(decPanel);
AbsolutePanel absolutePanel = new AbsolutePanel();
absolutePanel.setSize("290px", "180px");
decPanel.add(absolutePanel);
Label idLabel = new Label("Id");
absolutePanel.add(idLabel, 0, 16);
idLabel.setSize("52px", "24px");
Label vornameLabel = new Label("Vorname");
absolutePanel.add(vornameLabel, 0, 45);
vornameLabel.setSize("52px", "24px");
Label lblNachname = new Label("Nachname");
absolutePanel.add(lblNachname, 0, 74);
lblNachname.setSize("62px", "24px");
idTextbox.setEnabled(false);
absolutePanel.add(idTextbox, 71, 16);
idTextbox.setSize("50px", "12px");
absolutePanel.add(vornameTextbox, 71, 45);
vornameTextbox.setSize("206px", "12px");
absolutePanel.add(nachnameTextbox, 71, 74);
nachnameTextbox.setSize("206px", "12px");
this.createNew();
this.populate(personProxy);
Button newButton = new Button("neu");
absolutePanel.add(newButton, 10, 129);
newButton.setSize("60px", "32px");
newButton.addClickHandler(new ClickHandler() {
public void onClick(ClickEvent event) {
personRequest = requestFactory.personRequest();
personProxy = personRequest.create(PersonProxy.class);
populate(personProxy);
}
});
Button loadPrevButton = new Button("<");
absolutePanel.add(loadPrevButton, 80, 129);
loadPrevButton.setSize("25px", "32px");
loadPrevButton.addClickHandler(new ClickHandler() {
public void onClick(ClickEvent event) {
if (personProxy.getId() != null) {
// In diesem Fall wurde der personRequest bereits einmal
// verwendet, es muss ein neuer personRequest generiert werden.
// Eine Id kann das personProxy nur durch einen Aufruf
// des Servers (mittels requestContext) erhalten haben.
// Hat das entityProxy keine Id, so wurde es über .create erzeugt. Der
// zugehörige personRequest wurde in diesem Fall noch nicht
// verwendet. Das PersonProxy   m u s s   deshalb über den gleichen personRequest zum
// Server gesendet werden.
personRequest = requestFactory.personRequest();
}
personRequest.getPrevPerson(personProxy).fire(new Receiver<PersonProxy>() {
@Override
public void onSuccess(final PersonProxy prevPersonProxy) {
if (prevPersonProxy == null) {
createNew();
} else {
personProxy = prevPersonProxy;
}
populate(personProxy);
}
@Override
public void onFailure(ServerFailure error) {
// die onFailure-Implementierung ist optional und wird in den nachfolgenden Implementierungen weggelassen
super.onFailure(error);
}
});
}
});
Button loadNextButton = new Button(">");
absolutePanel.add(loadNextButton, 115, 129);
loadNextButton.setSize("25px", "32px");
loadNextButton.addClickHandler(new ClickHandler() {
public void onClick(ClickEvent event) {
if (personProxy.getId() != null) {
// In diesem Fall wurde der personRequest bereits einmal
// verwendet, es muss ein neuer personRequest generiert werden.
// Eine Id kann das personProxy nur durch einen Aufruf
// des Servers (mittels requestContext) erhalten haben.
// Hat das entityProxy keine Id, so wurde es über .create erzeugt. Der
// zugehörige personRequest wurde in diesem Fall noch nicht
// verwendet. Das PersonProxy   m u s s   deshalb über den gleichen personRequest zum
// Server gesendet werden.
personRequest = requestFactory.personRequest();
}
personRequest.getNextPerson(personProxy).fire(new Receiver<PersonProxy>() {
@Override
public void onSuccess(final PersonProxy nextPersonProxy) {
if (nextPersonProxy == null) {
createNew();
} else {
personProxy = nextPersonProxy;
}
populate(personProxy);
}
});
}
});
Button saveButton = new Button("speichern");
absolutePanel.add(saveButton, 150, 129);
saveButton.setSize("65px", "32px");
saveButton.addClickHandler(new ClickHandler() {
public void onClick(ClickEvent event) {
if (personProxy.getId() == null) {
// personProxy wurde mit .create des RequestContext erzeugt
// und ist somit per default mutable (editierbar).
// Damit steht der aktuelle RequestContext (personRequest)
// für einen Request zur Verfügung, das aktuelle EntityProxy (personProxy)
// ist diesem RequestContext zugeordnet.
// Keine weitere Aktion erforderlich
} else {
// personProxy wurde mittels einer Service-Methode vom
// Server übermittelt und ist somit per default immutable
// (readonly)
// Ein neuer RequestContext muss erzeugt werden, da der ursprüngliche RequestContext
// nicht mehr für einen neuen Request verfügbar ist. Anschließend muss das (bisherige) personProxy auf
// editierbar gesetzt werden. Dabei entsteht ein neues personProxy, das zugleich dem neuen requestContext
// zugewiesen ist
// Die RequestFactory liefert einen neuen RequestContext, der im personRequest gespeichert wird.
personRequest = requestFactory.personRequest();
// durch Verwendung der .edit Methode entsteht aus dem alten personProxy das neuen personProxy
personProxy = personRequest.edit(personProxy);
}
personProxy.setVorname(vornameTextbox.getText());
personProxy.setNachname(nachnameTextbox.getText());
personRequest.persistPerson(personProxy).fire(new Receiver<PersonProxy>() {
// Object wird persistiert. Wichtig ist, dass dabei eine ID
// vergeben wird (falls es sich um ein neues Object handelt)
@Override
public void onSuccess(final PersonProxy persistedPersonProxy) {
personProxy = persistedPersonProxy;
populate(personProxy);
}
});
}
});
Button deleteButton = new Button("l&ouml;schen");
absolutePanel.add(deleteButton, 225, 129);
deleteButton.setSize("60px", "32px");
deleteButton.addClickHandler(new ClickHandler() {
public void onClick(ClickEvent event) {
if (personProxy.getId() != null) {
// das Löschen ist nur erforderlich, wenn das PersonProxy bereits persistiert wurde
// in diesem Fall benötigen wir auch einen neuen PersonRequest
// die Methode 'deletePerson' liefert automatisch das nächste PersonProxy, dieses wird anschließend angezeigt
personRequest = requestFactory.personRequest();
personRequest.deletePerson(personProxy).fire(new Receiver<PersonProxy>() {
@Override
public void onSuccess(final PersonProxy nextPersonProxy) {
if (nextPersonProxy == null) {
createNew();
} else {
personProxy = nextPersonProxy;
}
populate(personProxy);
}
});
}
}
});
}
}
Klasse: Person

package de.langlaufen.gwt.server.domain;
import java.util.HashMap;import java.util.Map;import java.util.Map.Entry;
public class Person {
private static Long idCounter = new Long(0); private static HashMap<Long, Person> personStorage = new HashMap<Long, Person>();
private Long id; private Integer version; private String vorname; private String nachname;
public String getVorname() { return this.vorname; }
public void setVorname(String vorname) { this.vorname = vorname; }
public String getNachname() { return this.nachname; }
public void setNachname(String nachname) { this.nachname = nachname; }
public Long getId() { return this.id; }
public void setId(Long id) { this.id = id; }
public Integer getVersion() { return this.version; }
public void setVersion(Integer version) { this.version = version; }
public static Person persistPerson(Person persistPerson) { if (persistPerson.getId() == null) { idCounter++; persistPerson.setId(idCounter); persistPerson.setVersion(1); } else { persistPerson.setVersion(persistPerson.getVersion() + 1); } personStorage.put(persistPerson.getId(), persistPerson); return persistPerson; }
public static Person findPerson(Long id) { return personStorage.get(id); }
public static Person getPrevPerson(Person person) { if (person != null) { for (int i = personStorage.entrySet().size() - 1; i >= 0; i--) { @SuppressWarnings("unchecked") Map.Entry<Long, Person> map = (Entry<Long, Person>) personStorage.entrySet().toArray()[i]; if (person.getId() == null || map.getKey() < person.getId()) { // ohne id wird die letzte Person zurückgeliefert return map.getValue(); } } } return null; }
public static Person getNextPerson(Person person) { if (person != null) { for (Map.Entry<Long, Person> map : personStorage.entrySet()) { if (person.getId() == null || map.getKey() > person.getId()) { // ohne id wird die erste Person zurückgeliefert return map.getValue(); } } } return null; }
public static Person deletePerson(Person person) { Person nextPerson = null; if (person != null) { nextPerson = getNextPerson(person); personStorage.remove(person.getId()); } return nextPerson; }}
Interface: PersonProxy


package de.langlaufen.gwt.shared;
import com.google.web.bindery.requestfactory.shared.EntityProxy;
import com.google.web.bindery.requestfactory.shared.ProxyFor;
@ProxyFor(de.langlaufen.gwt.server.domain.Person.class)
public interface PersonProxy extends EntityProxy {
String getNachname();
String getVorname();
void setNachname(String nachname);
void setVorname(String vorname);
public Long getId();
public Integer getVersion();
}
Interface: PersonRequest

package de.langlaufen.gwt.shared;

import com.google.web.bindery.requestfactory.shared.Request;
import com.google.web.bindery.requestfactory.shared.RequestContext;
import com.google.web.bindery.requestfactory.shared.Service;
import de.langlaufen.gwt.server.domain.Person;
@Service(Person.class)
public interface PersonRequest extends RequestContext {
Request<PersonProxy> getPrevPerson(PersonProxy personProxy);
Request<PersonProxy> getNextPerson(PersonProxy personProxy);
Request<PersonProxy> persistPerson(PersonProxy personProxy);
Request<PersonProxy> deletePerson(PersonProxy personProxy);
}
Interface: RequestFactoryX
package de.langlaufen.gwt.shared;
import com.google.web.bindery.requestfactory.shared.RequestFactory;


public interface RequestFactoryX extends RequestFactory{
PersonRequest personRequest();
}

References:
http://code.google.com/intl/de-DE/webtoolkit/doc/latest/DevGuideRequestFactory.html
http://tbroyer.posterous.com/gwt-211-requestfactory
http://fascynacja.wordpress.com/2011/04/17/exciting-life-of-entity-proxies-in-contexts-of-requestfactory/

Keine Kommentare:

Kommentar veröffentlichen