Posts mit dem Label GWT werden angezeigt. Alle Posts anzeigen
Posts mit dem Label GWT werden angezeigt. Alle Posts anzeigen

Sonntag, 19. Juli 2009

GWT 1.6 Hosted Mode: Internen Jetty Server mit JDNI Datasourcen und Environment Informationen für PostgreSQL versorgen

DSCF0005c
Update: Auch in GWT Version 2.0.3 scheint das Verfahren weiterhin zu funktionieren!
Als eines der mühseligsten Dinge, die man beim Einstieg in die GWT Programmierung herausfinden muss, empfand ich die Lösung der folgende Frage:
Wie bringt man den internen Jetty Server, der bei der Hosted Mode Entwicklung unter Eclipse mit dem Google Plugin verwendet wird dazu, der serverseitigen Programmierung JBDC Verbindungen bzw. Connection Pools und andere JNDI Ressourcen zur Verfügung zu stellen?
Vermutlich wird fast jedes halbwegs ambitionierte GWT Projekt irgendwann vor dieser Aufgabe stehen. Falls sie nicht gelöst wird müsste man auf die enormen Vorteile der Hosted Mode Entwicklung verzichten, sein GWT Projekt für jeden Testlauf übersetzen (Java –> Javascript) und ohne die hilfreichen Entwicklungsfunktionen auskommen.

Infos im Web

In den eigentlichen GWT Seiten findet man keine besonders hilfreichen Informationen. In dem Thread Setting DataSource (JNDI) in GWT 1.6 (hosted mode – Jetty) in den Google Groups findet man allerdings Hilfestellungen, die einen letztlich zu der Lösung bringen.
Bei meinem ersten Anlauf zur Problemlösung bin ich allerdings folgendem Link aus den Kommentaren gefolgt, weil die Lösung einfacher schien:
http://humblecode.blogspot.com/2009/05/gwt-16-using-jndi-datasource.html
Auch findet man weitere hilfreiche Links, z. B. zu passenden Jetty Seiten. Leider bin ich bei dieser Lösung an der Frage gescheitert, wie man dem Jetty Server den per Default fehlenden JNDI Context java:comp/env zur Verfügung stellt. Daher habe ich dann letztlich doch die Lösung aus den Google Groups umgesetzt. Dies, und die Anpassung für unsere PostgreSQL Datenbank, soll im Folgenden beschrieben werden:

Schrittchen für Schrittchen

1. Jetty und JDBC JARs
Zunächst sollte man sich die notwendigen JARs besorgen. Dies sind zum einem Erweiterungen für Jetty, zum anderen der JDBC Treiber für PostgreSQL. Hier die Links für den Download:
Jetty: http://dist.codehaus.org/jetty/
Hier findet man Jetty bis Version 6. Ab Version 7 ist der Download beim Eclipse Projekt zu finden. Beim Download einer 6er Version in eines der Unterverzeichnisse gehen und das größere (nicht-src) ZIP Archiv holen, darin finden sich die beiden JARs.
PostgreSQL JDBC: http://jdbc.postgresql.org/download.html
Ich habe die JDBC3 Version verwendet.
Insgesamt muss man dem Jetty Server dann folgende JARs verfügbar machen:
  • jetty-naming-*.jar
  • jetty-plus-*.jar
  • postgresql-*.jdbc3.jar
Man kann die JARs der Einfachheit halber in /war/WEB-INF/lib/ platzieren, auch wenn für die eigentliche Anwendung nicht gebraucht werden.  Die JARs müssen dann in den Launch Configuration Properties in den Classpath gesetzt werden (unter User Entries).
2. jetty-env.xml
Im /war/WEB-INF/ Verzeichnis die Datei jetty-env.xml anlegen. Hier werden nun die Ressourcen für den JNDI Kontext angelegt. Leider kann man dabei nur wenig von einer eventuell vorhandenen Konfiguration für Tomcat lernen, da Jetty vieles anders macht. In unserem Fall muss neben der DataSource auch eine Konfigurationsinformation abgelegt werden. Mit beiden Definitionen sieht meine jetty-env.xml so aus:
<?xml version="1.0"?>

<!-- Wird nur fuer die Nutzung des Hosted Modes bei der GWT Entwicklung gebraucht -->

<configure class="org.mortbay.jetty.webapp.WebAppContext">

<new class="org.mortbay.jetty.plus.naming.EnvEntry">
<arg></arg>
<arg>meinEnvParameter</arg>
<arg type="java.lang.String">irgendein String</arg>
<arg type="boolean">true</arg>
</new>

<new id="jdbc/meinDBPool" class="org.mortbay.jetty.plus.naming.Resource">
<arg></arg>
<arg>jdbc/meinDBPool</arg>
<arg>
<new class="org.postgresql.ds.PGSimpleDataSource">
<set name="User">meinPostgresBenutzer</set>
<set name="Password">meinPostgresPasswort</set>
<set name="DatabaseName">meinePostgresDatenbank</set>
<set name="ServerName">localhost</set>
<set name="PortNumber">5432</set>
</new>
</arg>
</new>

</configure>


Die entsprechenden Ressourcen müssen natürlich auch wie immer in der web.xml definiert werden:


<resource-ref>
<res-ref-name>jdbc/meinDBPool</res-ref-name>
<res-type>javax.sql.DataSource</res-type>
<res-auth>Container</res-auth>
</resource-ref>


Aber das sollte in den meisten Fällen schon der Fall sein.


3. env-Kontext konfigurieren


Ein Problem ist jetzt noch zu überwinden: Jetty kennt in der Standardkonfiguration noch keinen Environment Context, also das, was man typischerweise so abfragt:


Context initCtx = new InitialContext();
Context myEnv = (Context) initCtx.lookup("java:comp/env");


Möglicherweise soll Jetty auf diese Weise schlank gehalten werden für bestimmte Einsatzszenarien, aber hier stellt es noch einen weiteren Stolperstein dar, der zu überwinden ist. Es scheint möglich zu sein, Jetty über die XML Konfiguration mit dem Kontext zu versorgen, aber leider habe ich nicht herausgefunden wie. Daher wird der in den Google Groups beschriebene Weg eingeschlagen, der in der Implementierung einer eigenen Starterklasse für den internen Server besteht.


Das GWT verwendet per Default die Klasse



com.google.gwt.dev.shell.jetty.JettyLauncher



um den Server für den Hosted Mode zu starten. Diese Klasse befindet sich samt Quellcode im gwt-dev-windows.jar. Diese Klasse muss erweitert werden. Am einfachsten geht dies vermutlich in dem man sich den Quellcode in eine eigene Klasse kopiert (beim Upgrade auf das nächste GWT muss man vielleicht nachsehen, ob sich etwas verändert hat). Nennen wir die neue Klasse



org.unibi.sm.edit.MyCustomJettyLauncher



und legen sie im Sourceverzeichnis des GWT Projekts ab. Jetzt geht es nur noch darum dem WebAppClassLoader die entsprechende Konfiguration unterzuschieben, mit der er uns den gewünschten Kontext zur Verfügung stellt. Hier der entsprechende Codeschnipsel, am Ende findet sich noch einmal die komplette MyCustomJettyLaucher.java:


// Zusaetzlicher Quellcode Anfang:
private static String[] __dftConfigurationClasses = {
"org.mortbay.jetty.webapp.WebInfConfiguration", // 
"org.mortbay.jetty.plus.webapp.EnvConfiguration",// jetty-env
"org.mortbay.jetty.plus.webapp.Configuration", // web.xml
"org.mortbay.jetty.webapp.JettyWebXmlConfiguration",// jettyWeb
};
// Zusaetzlicher Quellcode Ende.

/**
* A {@link WebAppContext} tailored to GWT hosted mode. Features....
*/
protected final class WebAppContextWithReload extends WebAppContext {

/**
* Specialized {@link WebAppClassLoader} that allows outside…
*/
private class WebAppClassLoaderExtension extends WebAppClassLoader {

private static final String META_INF_SERVICES = "META-INF/services/";

public WebAppClassLoaderExtension() throws IOException {
super(bootStrapOnlyClassLoader, WebAppContextWithReload.this);

// Zusaetzlicher Quellcode Anfang:
setConfigurationClasses(__dftConfigurationClasses);
// Zusaetzlicher Quellcode Ende.
}


Es werden also im Konstruktor der inneren Klasse, die den WebAppClassLoader erweitert, eine Liste von zuvor definierten Konfigurationsklassen hinzugefügt. Als letztes muss jetzt noch die Run Configuration des Projektes dazu gebracht werden unsere neue Launcherklasse zu verwenden. Dazu wird einfach im Arguments-Reiter unter Program arguments die folgende Option ergänzt:



-server org.unibi.sm.edit.MyCustomJettyLauncher



So sieht es dann aus:


EclipseJetty


4. Start


Wenn alles richtig gemacht wurde sollte der interne Jetty jetzt ohne Fehlermeldungen starten und die serverseitige GWT Programmierung kann wie oben beschrieben auf Datenbanken und andere Environment Objekte zugreifen.


Wichtig: Um den Effekt von Änderungen zu testen sollte man eine eventuell laufende Hosted Mode Umgebung komplett stoppen und neu starten.




Der dritte Weg



Ergänzend noch der Hinweis auf einen möglichen dritten Weg zwischen Verzicht auf den Hosted Mode und der mühseligen Jetty Konfiguration: Oliver beschreibt einen Weg um den Hosted Mode ohne den eingebauten Server zu verwenden. Hier werden dann der serverseitige Code und die statische Inhalte des Projektes über einen externen AppServer ausgeführt bzw. geliefert. Das ist besonders dann interessant, wenn man sowieso einen schon fertig konfigurierten AppServer für seine Entwicklung hat. Allerdings bezieht sich Olivers Text auf eine schon etwas ältere GWT Version, bei der noch Tomcat als interner Server verwendet wurde.




Quellcode ‘org.unibi.sm.edit.MyCustomJettyLauncher’



package org.unibi.sm.edit;

import com.google.gwt.core.ext.ServletContainer;
import com.google.gwt.core.ext.ServletContainerLauncher;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.dev.shell.jetty.JettyNullLogger;
import com.google.gwt.dev.util.InstalledHelpInfo;

import org.mortbay.component.AbstractLifeCycle;
import org.mortbay.jetty.AbstractConnector;
import org.mortbay.jetty.Request;
import org.mortbay.jetty.RequestLog;
import org.mortbay.jetty.Response;
import org.mortbay.jetty.Server;
import org.mortbay.jetty.handler.RequestLogHandler;
import org.mortbay.jetty.nio.SelectChannelConnector;
import org.mortbay.jetty.webapp.WebAppClassLoader;
import org.mortbay.jetty.webapp.WebAppContext;
import org.mortbay.log.Log;
import org.mortbay.log.Logger;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.Enumeration;

/**
* A {@link ServletContainerLauncher} for an embedded Jetty server.
*/
public class MyCustomJettyLauncher extends ServletContainerLauncher {

/**
* Log jetty requests/responses to TreeLogger.
*/
public static class JettyRequestLogger extends AbstractLifeCycle implements
RequestLog {

private final TreeLogger logger;

public JettyRequestLogger(TreeLogger logger) {
this.logger = logger;
}

/**
* Log an HTTP request/response to TreeLogger.
*/
@SuppressWarnings("unchecked")
public void log(Request request, Response response) {
int status = response.getStatus();
if (status < 0) {
// Copied from NCSARequestLog
status = 404;
}
TreeLogger.Type logStatus, logHeaders;
if (status >= 500) {
logStatus = TreeLogger.ERROR;
logHeaders = TreeLogger.INFO;
} else if (status >= 400) {
logStatus = TreeLogger.WARN;
logHeaders = TreeLogger.INFO;
} else {
logStatus = TreeLogger.INFO;
logHeaders = TreeLogger.DEBUG;
}
String userString = request.getRemoteUser();
if (userString == null) {
userString = "";
} else {
userString += "@";
}
String bytesString = "";
if (response.getContentCount() > 0) {
bytesString = " " + response.getContentCount() + " bytes";
}
TreeLogger branch = logger.branch(logStatus, String.valueOf(status)
+ " - " + request.getMethod() + ' ' + request.getUri()
+ " (" + userString + request.getRemoteHost() + ')'
+ bytesString);
TreeLogger headers = branch.branch(logHeaders, "Request headers");
Enumeration headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String hdr = headerNames.nextElement();
String hdrVal = request.getHeader(hdr);
headers.log(logHeaders, hdr + ": " + hdrVal);
}
// TODO(jat): add response headers
}
}

/**
* An adapter for the Jetty logging system to GWT's TreeLogger. This
* implementation class is only public to allow {@link Log} to instantiate
* it.
* 
* The weird static data / default construction setup is a game we play with
* {@link Log}'s static initializer to prevent the initial log message from
* going to stderr.
*/
public static class JettyTreeLogger implements Logger {
private final TreeLogger logger;

public JettyTreeLogger(TreeLogger logger) {
if (logger == null) {
throw new NullPointerException();
}
this.logger = logger;
}

public void debug(String msg, Object arg0, Object arg1) {
logger.log(TreeLogger.SPAM, format(msg, arg0, arg1));
}

public void debug(String msg, Throwable th) {
logger.log(TreeLogger.SPAM, msg, th);
}

public Logger getLogger(String name) {
return this;
}

public void info(String msg, Object arg0, Object arg1) {
logger.log(TreeLogger.INFO, format(msg, arg0, arg1));
}

public boolean isDebugEnabled() {
return logger.isLoggable(TreeLogger.SPAM);
}

public void setDebugEnabled(boolean enabled) {
// ignored
}

public void warn(String msg, Object arg0, Object arg1) {
logger.log(TreeLogger.WARN, format(msg, arg0, arg1));
}

public void warn(String msg, Throwable th) {
logger.log(TreeLogger.WARN, msg, th);
}

/**
* Copied from org.mortbay.log.StdErrLog.
*/
private String format(String msg, Object arg0, Object arg1) {
int i0 = msg.indexOf("{}");
int i1 = i0 < 0 ? -1 : msg.indexOf("{}", i0 + 2);

if (arg1 != null && i1 >= 0) {
msg = msg.substring(0, i1) + arg1 + msg.substring(i1 + 2);
}
if (arg0 != null && i0 >= 0) {
msg = msg.substring(0, i0) + arg0 + msg.substring(i0 + 2);
}
return msg;
}
}

/**
* The resulting {@link ServletContainer} this is launched.
*/
protected static class JettyServletContainer extends ServletContainer {
private final int actualPort;
private final File appRootDir;
private final TreeLogger logger;
private final Server server;
private final WebAppContext wac;

public JettyServletContainer(TreeLogger logger, Server server,
WebAppContext wac, int actualPort, File appRootDir) {
this.logger = logger;
this.server = server;
this.wac = wac;
this.actualPort = actualPort;
this.appRootDir = appRootDir;
}

public int getPort() {
return actualPort;
}

public void refresh() throws UnableToCompleteException {
String msg = "Reloading web app to reflect changes in "
+ appRootDir.getAbsolutePath();
TreeLogger branch = logger.branch(TreeLogger.INFO, msg);
// Temporarily log Jetty on the branch.
Log.setLog(new JettyTreeLogger(branch));
try {
wac.stop();
wac.start();
branch.log(TreeLogger.INFO, "Reload completed successfully");
} catch (Exception e) {
branch.log(TreeLogger.ERROR,
"Unable to restart embedded Jetty server", e);
throw new UnableToCompleteException();
} finally {
// Reset the top-level logger.
Log.setLog(new JettyTreeLogger(logger));
}
}

public void stop() throws UnableToCompleteException {
TreeLogger branch = logger.branch(TreeLogger.INFO,
"Stopping Jetty server");
// Temporarily log Jetty on the branch.
Log.setLog(new JettyTreeLogger(branch));
try {
server.stop();
server.setStopAtShutdown(false);
branch.log(TreeLogger.INFO, "Stopped successfully");
} catch (Exception e) {
branch.log(TreeLogger.ERROR,
"Unable to stop embedded Jetty server", e);
throw new UnableToCompleteException();
} finally {
// Reset the top-level logger.
Log.setLog(new JettyTreeLogger(logger));
}
}
}

private static String[] __dftConfigurationClasses = {
"org.mortbay.jetty.webapp.WebInfConfiguration", // 
"org.mortbay.jetty.plus.webapp.EnvConfiguration",// jetty-env
"org.mortbay.jetty.plus.webapp.Configuration", // web.xml
"org.mortbay.jetty.webapp.JettyWebXmlConfiguration",// jettyWeb
};

/**
* A {@link WebAppContext} tailored to GWT hosted mode. Features hot-reload
* with a new {@link WebAppClassLoader} to pick up disk changes. The default
* Jetty {@code WebAppContext} will create new instances of servlets, but it
* will not create a brand new {@link ClassLoader}. By creating a new
* {@code ClassLoader} each time, we re-read updated classes from disk.
* 
* Also provides special class filtering to isolate the web app from the GWT
* hosting environment.
*/
protected final class WebAppContextWithReload extends WebAppContext {

/**
* Specialized {@link WebAppClassLoader} that allows outside resources
* to be brought in dynamically from the system path. A warning is
* issued when this occurs.
*/
private class WebAppClassLoaderExtension extends WebAppClassLoader {

private static final String META_INF_SERVICES = "META-INF/services/";

public WebAppClassLoaderExtension() throws IOException {
super(bootStrapOnlyClassLoader, WebAppContextWithReload.this);

setConfigurationClasses(__dftConfigurationClasses);
}

@Override
public URL findResource(String name) {
// Specifically for
// META-INF/services/javax.xml.parsers.SAXParserFactory
String checkName = name;
if (checkName.startsWith(META_INF_SERVICES)) {
checkName = checkName.substring(META_INF_SERVICES.length());
}

// For a system path, load from the outside world.
URL found;
if (isSystemPath(checkName)) {
found = systemClassLoader.getResource(name);
if (found != null) {
return found;
}
}

// Always check this ClassLoader first.
found = super.findResource(name);
if (found != null) {
return found;
}

// See if the outside world has it.
found = systemClassLoader.getResource(name);
if (found == null) {
return null;
}

// Warn, add containing URL to our own ClassLoader, and retry
// the call.
String warnMessage = "Server resource '"
+ name
+ "' could not be found in the web app, but was found on the system classpath";
if (!addContainingClassPathEntry(warnMessage, found, name)) {
return null;
}
return super.findResource(name);
}

/**
* Override to additionally consider the most commonly available JSP
* and XML implementation as system resources. (In fact, Jasper is
* in gwt-dev via embedded Tomcat, so we always hit this case.)
*/
@Override
public boolean isSystemPath(String name) {
name = name.replace('/', '.');
return super.isSystemPath(name)
|| name.startsWith("org.apache.jasper.")
|| name.startsWith("org.apache.xerces.");
}

@Override
protected Class<?> findClass(String name)
throws ClassNotFoundException {
// For system path, always prefer the outside world.
if (isSystemPath(name)) {
try {
return systemClassLoader.loadClass(name);
} catch (ClassNotFoundException e) {
}
}

try {
return super.findClass(name);
} catch (ClassNotFoundException e) {
// Don't allow server classes to be loaded from the outside.
if (isServerPath(name)) {
throw e;
}
}

// See if the outside world has a URL for it.
String resourceName = name.replace('.', '/') + ".class";
URL found = systemClassLoader.getResource(resourceName);
if (found == null) {
return null;
}

// Warn, add containing URL to our own ClassLoader, and retry
// the call.
String warnMessage = "Server class '"
+ name
+ "' could not be found in the web app, but was found on the system classpath";
if (!addContainingClassPathEntry(warnMessage, found,
resourceName)) {
throw new ClassNotFoundException(name);
}
return super.findClass(name);
}

private boolean addContainingClassPathEntry(String warnMessage,
URL resource, String resourceName) {
TreeLogger.Type logLevel = (System
.getProperty(PROPERTY_NOWARN_WEBAPP_CLASSPATH) == null) ? TreeLogger.WARN
: TreeLogger.DEBUG;
TreeLogger branch = logger.branch(logLevel, warnMessage);
String classPathURL;
String foundStr = resource.toExternalForm();
if (resource.getProtocol().equals("file")) {
assert foundStr.endsWith(resourceName);
classPathURL = foundStr.substring(0, foundStr.length()
- resourceName.length());
} else if (resource.getProtocol().equals("jar")) {
assert foundStr.startsWith("jar:");
assert foundStr.endsWith("!/" + resourceName);
classPathURL = foundStr.substring(4, foundStr.length()
- (2 + resourceName.length()));
} else {
branch.log(TreeLogger.ERROR,
"Found resouce but unrecognized URL format: '"
+ foundStr + '\'');
return false;
}
branch = branch.branch(logLevel, "Adding classpath entry '"
+ classPathURL
+ "' to the web app classpath for this session", null,
new InstalledHelpInfo("webAppClassPath.html"));
try {
addClassPath(classPathURL);
return true;
} catch (IOException e) {
branch.log(TreeLogger.ERROR, "Failed add container URL: '"
+ classPathURL + '\'', e);
return false;
}
}
}

/**
* Parent ClassLoader for the Jetty web app, which can only load JVM
* classes. We would just use null for the parent
* ClassLoader except this makes Jetty unhappy.
*/
private final ClassLoader bootStrapOnlyClassLoader = new ClassLoader(
null) {
};

private final TreeLogger logger;

/**
* In the usual case of launching {@link com.google.gwt.dev.HostedMode},
* this will always by the system app ClassLoader.
*/
private final ClassLoader systemClassLoader = Thread.currentThread()
.getContextClassLoader();

@SuppressWarnings("unchecked")
private WebAppContextWithReload(TreeLogger logger, String webApp,
String contextPath) {
super(webApp, contextPath);
this.logger = logger;

// Prevent file locking on Windows; pick up file changes.
getInitParams().put(
"org.mortbay.jetty.servlet.Default.useFileMappedBuffer",
"false");

// Since the parent class loader is bootstrap-only, prefer it first.
setParentLoaderPriority(true);
}

@Override
protected void doStart() throws Exception {
setClassLoader(new WebAppClassLoaderExtension());
super.doStart();
}

@Override
protected void doStop() throws Exception {
super.doStop();
setClassLoader(null);
}
}

/**
* System property to suppress warnings about loading web app classes from
* the system classpath.
*/
private static final String PROPERTY_NOWARN_WEBAPP_CLASSPATH = "gwt.nowarn.webapp.classpath";

static {
// Suppress spammy Jetty log initialization.
System.setProperty("org.mortbay.log.class", JettyNullLogger.class
.getName());
Log.getLog();

/*
* Make JDT the default Ant compiler so that JSP compilation just works
* out-of-the-box. If we don't set this, it's very, very difficult to
* make JSP compilation work.
*/
String antJavaC = System.getProperty("build.compiler",
"org.eclipse.jdt.core.JDTCompilerAdapter");
System.setProperty("build.compiler", antJavaC);
}

public ServletContainer start(TreeLogger logger, int port, File appRootDir)
throws Exception {
TreeLogger branch = logger.branch(TreeLogger.INFO,
"Starting Jetty on port " + port, null);

checkStartParams(branch, port, appRootDir);

// Setup our branch logger during startup.
Log.setLog(new JettyTreeLogger(branch));

// Turn off XML validation.
System.setProperty("org.mortbay.xml.XmlParser.Validating", "false");

AbstractConnector connector = getConnector();
connector.setPort(port);

// Don't share ports with an existing process.
connector.setReuseAddress(false);

// Linux keeps the port blocked after shutdown if we don't disable this.
connector.setSoLingerTime(0);

Server server = new Server();
server.addConnector(connector);

// Create a new web app in the war directory.
WebAppContext wac = new WebAppContextWithReload(logger, appRootDir
.getAbsolutePath(), "/");

RequestLogHandler logHandler = new RequestLogHandler();
logHandler.setRequestLog(new JettyRequestLogger(logger));
logHandler.setHandler(wac);
server.setHandler(logHandler);
server.start();
server.setStopAtShutdown(true);

// Now that we're started, log to the top level logger.
Log.setLog(new JettyTreeLogger(logger));

return new JettyServletContainer(logger, server, wac, connector
.getLocalPort(), appRootDir);
}

protected AbstractConnector getConnector() {
return new SelectChannelConnector();
}

private void checkStartParams(TreeLogger logger, int port, File appRootDir) {
if (logger == null) {
throw new NullPointerException("logger cannot be null");
}

if (port < 0 || port > 65535) {
throw new IllegalArgumentException(
"port must be either 0 (for auto) or less than 65536");
}

if (appRootDir == null) {
throw new NullPointerException("app root direcotry cannot be null");
}
}

}

Mittwoch, 24. Juni 2009

Nutzung von JSON Daten in GWT und GXT

Das Google Web Toolkit GWT bietet von Haus aus bereits eine relativ einfach nutzbare (c) H. Brune Möglichkeit Daten von Servern im JSON Format abzuholen. Hier soll ein kleiner Vergleich mit dem Möglichkeiten gezogen werden, die GXT bzw. Ext GWT bieten.

Wichtig ist dabei insbesondere die Frage, wie einfach JSON Daten mit Widgets verbunden werden können, da wir relativ viele Datensatzarten haben, die aber in mehr oder weniger gleichartigen Masken bearbeitet werden sollen. Die Frage, wie die Daten dann wieder an den Server zurück geschickt werden spielt hier erstmal keine Rolle.

Testvorbereitungen: Statische JSON Dateien

Um die Implementierung zu vereinfachen werden die JSON Daten im GWT über statische Dateien verfügbar gemacht. Im meinem über das Google Plugin automatisch erstellten Eclipse Projekt gibt es den /war-Ordner, der seid GWT Version 1.6 die komplette Webanwendung enthält. Für die statischen JSON Dateien wird dort einfach ein Unterordner angelegt, nennen wir ihn modell/.

Beispieldatei: /war/modell/abschluss.json

Inhalt der Datei:

[
{
id : 123,
name : Abschluss 1,
kuerzel : Abs1
},
{
id : 456,
name : Abschluss 2,
kuerzel : Abs2
},
]








Diese Datei kann im Jetty Server, der in Ecplise gestartet wird, unter http://localhost:8080/modell/abschluss.json abgerufen werden (bzw. unter dem konfigurierten Port).









JSON mit GWT









Hier nur eine ganze kurze Skizze wie dies funktioniert, die ausführliche Beschreibung findet sich in dieser GWT 1.6 Seite:













  1. JSON Quelle anlegen (haben wir schon)






  2. Stub Klasse anlegen, welche JSNI Methoden implementiert um direkt auf die Inhalte der JSON Struktur zuzugreifen. Instanzen der Stub Klasse werden über eine JSNI Methode generiert, die im Kern eval() aufruft (Sicherheitshinweise beachten!)







  3. Implementierung eines asynchronen RequestBuilder Aufrufes, der die JSON Daten holt und verarbeitet.







  4. Erzeugung einer Darstellung, z . B. mit einem Grid.











Wichtig: Der URL zur JSON Datei darf nicht GWT.getModuleBaseURL() vorangestellt werden, wie es im GWT Tutorial gezeigt wird. Es reicht eine Angabe dieser Art:









	private static final String JSON_URL = 
"/modell/abschluss.json";








Erfahrungen












  • Die Implementierung der Stub Klasse kann dann ärgerliche Mehrfacharbeit bedeuten, wenn es auf der Serverseite bereits entsprechende (Bean-)Klassen gibt. Hier lässt sich aber kein Reuse erzielen






  • Sobald man die Stub Klassen hat ist der Rest der Implementierung vergleichsweise einfach, abgesehen von der üblichen ‘Mühsal’ der Darstellung mit den relativ primitiven GWT Widgets










Quellcode








package com.chb.gxt2m3.client;

import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.core.client.JsArray;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.http.client.Request;
import com.google.gwt.http.client.RequestBuilder;
import com.google.gwt.http.client.RequestCallback;
import com.google.gwt.http.client.RequestException;
import com.google.gwt.http.client.Response;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.Grid;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.RootPanel;

public class GXTTest implements EntryPoint {

public void onModuleLoad() {

Button btLaden = new Button("Laden");
RootPanel.get("buttons").add(btLaden);

btLaden.addClickHandler(new ClickHandler() {

public void onClick(ClickEvent event) {
refreshAbschluesse();

}
});
}

private static final String JSON_URL_ABS = "/modell/abschluesse.json";

private void refreshAbschluesse() {

RootPanel.get("display").clear();
RootPanel.get("display").add(
new Label("Abschlüsse werden geladen von " + JSON_URL_ABS));

RequestBuilder builder = new RequestBuilder(RequestBuilder.GET,
JSON_URL_ABS);

try {
builder.sendRequest(null, new RequestCallback() {

public void onError(Request request, Throwable exception) {
displayError("JSON Aufruf konnte nicht ausgeführt werden");
}

public void onResponseReceived(Request request,
Response response) {

if (200 == response.getStatusCode()) {

clearError();
abschluesseZeigen(asArrayOfAbsData(response.getText()));

} else {
displayError("Fehler beim JSON Aufruf ("
+ response.getStatusText() + ")");
}

}
});
} catch (RequestException e) {
displayError("Zugriff auf JSON gescheitert");
}
}

private void abschluesseZeigen(JsArray abs) {

RootPanel.get("display").clear();

if (abs == null || abs.length() == 0) {
RootPanel.get("display").add(
new Label("Keine Abschlüsse geladen..."));
} else {
Grid agrid = new Grid(abs.length() + 1, 3);
RootPanel.get("display").add(agrid);
agrid.setWidget(0, 0, new HTML("Id"));
agrid.setWidget(0, 1, new HTML("Kürzel"));
agrid.setWidget(0, 2, new HTML("Name"));

for (int idx = 0; idx < abs.length(); idx++) {
AbschlussData a = abs.get(idx);
agrid.setWidget(idx + 1, 0, new Label(""
+ a.getId()));
agrid.setWidget(idx + 1, 1, new Label(a.getKuerzel()));
agrid.setWidget(idx + 1, 2, new Label("" + a.getName()));
}
}
}

private void clearError() {
RootPanel.get("errorMsg").clear();
}

private void displayError(String error) {
clearError();
if (error != null)
RootPanel.get("errorMsg").add(new Label(error));
}

private final native JsArray asArrayOfAbsData(String json) /*-{
return eval(json);
}-*/;
}








JSON mit GXT









Eine Stärke von GXT (dessen Version 2 heute als Milestone 3 vorliegt) sind sicher die tollen Widgets und die Databinding Möglichkeiten, durch die sich Änderungen am zu Grunde liegenden Datenmodell an alle Teile der Anwendung melden lassen. Leider gibt es in der Demoseite nur ein Gridbeispiel mit einer XML Quelle. Etwas Suchen zeigt einem aber, dass es auch einen JsonReader gibt. Man kann das Beispiel also übertragen.









Ein Problem ist aber der Aufbau der JSON Datei. Offenbar sind verschachtelte Formate nicht möglich, zumindest kann man diesen Thread im ExtJS Forum wohl so verstehen.









Hier gibt es aber noch ein anderen Beispiel: http://extjs.net/forum/showthread.php?t=71978

Dienstag, 5. Mai 2009

Externe Klassen im GWT nutzen

P1090268bIn meiner GWT 1.6 Anwendung nutzen sowohl der client- wie auch der serverseitige Code die gleichen Geschäftsmodellklassen. Diese Klassen liegen allerdings in einem anderen Eclipseprojekt und sind daher nicht ohne weiteres in meinem mit dem Google Plugin angelegten Eclipse GWT Projekt verfügbar. Damit alles rund läuft sind folgende Konfigurationen notwendig:

Eclipse Konfiguration

Simpel: ‘Wie immer’ in den Project Properties den Java Build Path um das Projekt erweitern, in welchem das Geschäftsmodell entwickelt wird (also den CLASSPATH erweitern). Danach kann man sowohl in der client- und serverseitigen GWT Programmierung schon mal die entsprechenden Klassen in Eclipse nutzen.

Generierung des client-seitigen Codes

Das aus meiner Sicht Beste am GWT, nämlich die Übersetzung von Javascript Code aus Java Code, sorgt für eine kleine Komplikation: Der GWT Compiler braucht für alle in der Clientprogrammierung referenzierten Klassen den Quellcode. Ohne Quellcode keine JS Konvertierung und damit keine Clientanwendung. Man findet in der Fehlerausgabe des Hosted Modes Einträge wie den Folgenden, so lange die Geschäftsmodellklassen nicht verfügbar sind:

[ERROR] Line 21: No source code is available for type x.y.z.modell.KlasseXY; did you forget to inherit a required module?

Ähnliche Meldungen erhält man, wenn man in Eclipse das Projekt neu übersetzt:

Compiling module x.y.z.GWTProj
   Refreshing module from source
      Validating newly compiled units
         Removing units with errors
            [ERROR] Errors in 'file:/…/GWTProj/src/x/y/z/client/Modell.java'
               [ERROR] Line 5: The import x.y.z..modell.KlasseXY cannot be resolved

…..

[ERROR] Line 40: Missing code implementation in the compiler

Der Hinweis in der Fehlermeldung auf ein fehlendes inherit führt einen in die Beschreibung der module XML files. Eigentlich hatte ich gehofft, dass es mindestens im Fall von Klassen, die keine Widgets sind, nicht notwendig sein würde mein komplettes Geschäftsmodell mit speziellen module XML files zu durchsetzen. Dafür finde ich aber leider keinen Weg. Musste daher also so vorgehen:

Das Geschäftsmodell befindet sich im Paket org.xy.smod. In diesem Paket gründe ich die neue Modul-XML-Datei SMod.gwt.xml, die folgenden Inhalt hat:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE module PUBLIC "-//Google Inc.//DTD Google Web Toolkit 1.6.4//EN" "
http://google-web-toolkit.googlecode.com/svn/tags/1.6.4/distro-source/core/src/gwt-module.dtd">

<module rename-to='smod'>

<source path="" />

</module>

Ich lege also ein neues ‘Modul’ an mit dem Namen smod. Die source-Anweisung ist notwendig, damit alle Klassen direkt im Paket einbezogen werden. Ansonsten ist die Datei leer. Insbesondere müssen nicht die sonst üblichen GWT Module wie com.google.gwt.user.User eingebunden werden, auch gibt es natürlich keinen entry-point.

Jetzt muss ich in der Modul-XML-Datei meiner eigentlichen Anwendung nur noch dieses Modul über inherit einbinden. Dazu wird folgende Zeile eingefügt:

…
<inherits name='org.xy.smod.SMod'/>
…

Damit sind wir fertig! Zumindest in meiner Umgebung findet der GWT Compiler jetzt die Quellcodes der Geschäftsmodellklassen, die in einem anderen Eclipse Projekt liegen, und kann sowohl die Kompilierung wie auch den Start im hosted mode erfolgreich durchführen.

Hinweis: Eine gute Beschreibung des GWT Compilers im Allgemeinen findet man in den im Internet frei erhältlichen Abschnitt ‘Understand the GWT Compiler’ aus dem Buch GWT in Practice.

Testen der serverseitigen Programmierung / Deployment im Webserver

Sobald man die serverseitige Programmierung testen will bzw. bei der Installation des Anwendungs-WARs in Produktionsserver müssen den RPC Servlets ebenfalls die Geschäftsmodellklassen zur Verfügung stehen. Da es sich hier wieder um eine reine Java Angelegenheit handelt ist nur der Bytecode z. B. in Form eines entsprechenden JARs notwendig. Das Jar packt man ‘wie immer’ in /WEB-INF/lib/ und ist fertig.

Montag, 6. April 2009

GWT Lernen: Wie funktionieren die RPCs?

DSCF0005c

Die RPCs (Remote Procedure Calls), die das GWT bietet, sind leicht zu nutzen. Aber wie funktioniert die ganze Sache eigentlich? Und sollten wir die RPCs einsetzen, oder lieber auf vorhandene bzw. allgemeinere Services setzen?

Wie funktioniert die GWT RPC ‘Magie’?

Das Diagramm, welches man in der Doku zum Thema RPCs findet, ist schon recht hilfreich.  Der GWT Compiler arbeitet gerade bei der clientseitigen Programmierung eines RPCs mit Namenskonventionen:

So muss das asynchrone Serviceinterface immer den Namen des synchronen Interfaces plus Async tragen und im gleichen Package liegen. Auch müssen hier die gleichen Methoden definiert werden, allerdings mit einem zusätzlichen Callback Parameter und mit Ergebnistyp void. Auf Java Ebene gibt es ansonsten keine Beziehung zwischen den beiden Interfaces.

Der Callback Parameter AsyncCallback<T> ist ein generisches Element.  Für ‘T’ soll hier der Ergebnistyp der Methode im synchronen Serviceinterface gesetzt werden, dann ist dieser Typ in der onSuccess Methode direkt ohne Casting nutzbar. In der Implementierung verwendet man von AsyncCallback abgeleitete Klassen um mit Erfolg oder Misserfolg des Serviceaufrufs umzugehen.

Bei der serverseitigen Implementierung des RPCs ist hingegen weniger ‘Magie’ im Spiel: Im Grunde geht nur darum ein spezielles Servlet zu implementieren, welches das synchrone Client(!) Interface implementiert und von der Klasse RemoteServiceServlet abgeleitet ist. An dieser Stelle spielt der Klassenname keine Rolle, es gibt hier keine ‘magischen’ Namenskonventionen. Man kann das Servlet also auch im Gegensatz zu den GWT Beispielen komplett anders benennen. Wichtig ist nur, dass diese Klasse nicht im gleichen Package landet wie die Clientklassen/-interfaces.

Die Adresse des Servlets (aka Service) sollte dann noch der Clientseite durch Einträge in der <Projekt>.gwt.xml Datei (der sogn. Modul XML Datei , module XML file) bekannt gemacht werden. Diese Verdrahtung ist gewöhnlichen Servletverknüpfungen in der web.xml nicht unähnlich, kommt aber mit einer einzigen Anweisung pro Service aus. Wichtig: Diese Verdrahtung ist nur für den Test im hosted mode notwendig. Sie hat keinen Einfluss auf die produktive Installation.

Dieser Onlinevortrag enthält eine Darstellung der RPC Programmierung, die an manchen Stellen die GWT Seiten etwas ergänzt.

GWT RPC vs. XML Services plus Struts

In der mit dem GWT  erstellten BIS Anwendung gäbe es zwei denkbare Pfade, wie die Serverkommunikation mit dem Ajax Client durchgeführt werden könnte:

A) Wir setzen komplett auf die GWT RPCs

Vorteil wäre eine zunächst einheitliche und vermutlich rasche Entwicklung, die komplett in der GWT ‘Welt’ bleibt. Hier gibt es Lösungen für Exception Handling und all die anderen kleinen Dinge, die eine Anwendung erst ausreichend robust für den Einsatz machen.

Allerdings müssten einige Teile der Anwendungslogik erneut implementiert werden, die an anderer Stelle schon vorhanden sind. Auch bei dieser Lösung würden auf der untersten Ebene wieder die Struts und iBatis nutzenden Basisklassen eingesetzt werden.

Aufgaben in der Programmierung um eine Datensatzart abzudecken:

  • Erstellung einer GWT Modellklasse, z. B. ‘Person’. Wichtig ist hier die Serialisierbarkeit
  • Erstellung einer RPC Klasse für lesende Zugriffe. Muss aus den vorhandenen Strutsklassen die Instanzen der GWT Modellklassen erzeugen
  • Erstellung einer RPC Klasse für schreibende Zugriffe. Hier müssen Rechtemanagement und Logging eingefügt werden sowie das Rückmapping des GWT Modells auf die Strutsklassen

Beim Deployment der Anwendung würde man wie in der GWT Doku beschrieben vorgehen und müsste nur der Anwendung den Connectionpool zur Datenbank bekannt machen, damit die Basisklassen arbeiten können.

B) Erstellung allgemeiner XML Services und Nutzung der vorhandenen Struts Programmierung

Eine Alternative könnte in der Erstellung von ‘allgemeinen’ XML Services sein, die nicht nur für die GWT Anwendung nutzbar wären. Ein Beispiel ist der schon existierende Service für die Publikationsdaten. Auch für andere Daten werden solche XML Services nachgefragt und müssen so oder so implementiert werden.

Im GWT lassen sich solche Services ebenfalls relativ einfach verarbeiten, auch wenn der Aufwand im Vergleich zu den RPCs höher erscheint, insbesondere wenn der XML Service nur für das GWT erstellt werden müsste.

Beim Schreiben der Daten auf den Server könnte aus dem GWT heraus ein ‘normaler’ Formularsubmit ausgelöst werden, welcher dann die entsprechende Strutsklasse populiert. Ab hier würde es ‘wie gewohnt’ weiter gehen.

Aufgaben in der Programmierung um eine Datensatzart abzudecken:

  • Implementierung der lesenden XML Schnittstelle
  • Implementierung einer GWT Modellklasse, die hier aber nur auf Clientseite verwendet wird (könnte u. U. ausgelassen werden, wenn alle Daten sofort in Widgets gesteckt werden, aber dies erscheint zu unflexibel)
  • Abbildung der XML Daten auf die Modellklasse
  • Aufbau einer kompletten Strutsanwendung, die mindestens in der Lage ist die schreibenden Operationen aufzunehmen. Das Rechtemanagement kann wie bisher über Filter erfolgen oder auch direkt in den Actionklassen.

Das Deployment einer solchen Anwendung wäre ähnlich wie das Deployment einer reinen Strutsanwendung. Es müsste nur das Verzeichnis mit den statischen GWT Inhalten in die Anwendung kopiert werden.

Wie ist die Trennung von Client- und Serverseite möglich?

Für die zuvor diskutierten BIS Szenarios wäre es teilweise sinnvoll, wenn wir RPC Services, die nicht weiter geschützt werden müssten, und die auch in anderen Anwendungen genutzt werden könnten, sofort in eine allgemeine Anwendung verlagern würden.

Die Trennung müsste eigentlich vergleichsweise einfach sein:

  • Die serverseitige Implementierung braucht nur Zugriff auf das synchrone Clientinterface. Dies kann über die gemeinsame Codebasis erreicht werden.
  • Die clientseitige Implementierung findet ihre Serviceendpunkte für das Testen durch die in der <Projekt>.gwt.xml definierten Adressen. Diese können sich auf den Serverkontext beziehen und nicht nur auf den Webapplikationskontext. Sobald die Clientimplementierung in JS übersetzt wurde wird beim RPC Aufruf der in den jeweiligen ServiceDefTarget.setServiceEntryPoint(String) Kommandos gesetzte Pfad verwendet. Hier spielen die Servletdefinitionen in der Modul XML Datei keine Rolle mehr. Daher:
  • Es müssen zu den auf der Clientseite definierten Adressen passende web.xml  Verdrahtungen der Serviceservlets eingerichtet werden.

Auf diese Weise ist eine gekoppelte Entwicklung, aber ein getrenntes Deployment im Produktivsystem möglich.

Ein denkbares Szenario ist folgendes:

  • Die übersetzte Clientseite, die ja nur aus statischen Inhalten besteht (HTML Startseite, Javascript, CSS, Bilder), wird auf den Apache Servern in einem Unterverzeichnis unter den htdocs abgelegt. Eine neue Version  der Anwendung wird einfach durch Hochladen dieser Dateien installiert.
  • Die serverseitige RPC Installation erfolgt in einer J2EE Anwendung auf den Tomcat Servern (WAR Archiv).

Eine Eigenschaft dieser Lösung ist die Möglichkeit Client- und Serverseite getrennt zu aktualisieren. Bei Änderungen an den RPC Interfaces müssen natürlich beide Seiten zugleich aktualisiert werden.

GWT Lernen: Deployment einer GWT Anwendung

Die Installation (das ‘Deployment‘) einer GWT Anwendung zerfälP1090013lt im Grunde in zwei Teile:

  • Deployment der clientseitigen statischen Inhalte, welche aus Bildern und CSS Inhalten, vor allem aber aus den vom GWT aus Java generierten Javascript Dateien bestehen
  • Deployment der serverseitigen RPC  Klassen

Clientseite

Die Dateien der Clientseite müssen nur ‘irgendwie’ vom Webbrowser erreicht werden können. Man kann sie also auf einem Webserver wie Apache HTTPD unterbringen oder in der J2EE Anwendung irgendwo, nur nicht im /WEB-INF-Verzeichnis.

Wo man diese Dateien genau ablegt ist im Grunde frei. Man muss sich nur ggf. Gedanken über die SOP – Same Origin Policy machen.

Serverseite

Alle serverseitigen Klassen müssen für das Deployment wie üblich in das /WEB/classes/Verzeichnis kopiert werden, oder, falls sie als JAR gebündelt sind, in das /WEB-INF/lib/ Verzeichnis.

Im Falle der RPCs handelt es sich letztlich um Servlets. Diese müssen wie gewohnt in der /WEB-INF/web.xml ‘verdrahtet’ werden. Dabei müssen die Pfade verwendet werden, die die Clientseite auch kennt.

Hier die genaue Beschreibung dazu in der GWT Doku.

Die Verbindung zwischen Client- und Serverseite

Die Clientseite der GWT Anwendung findet ihre RPCs über die Adressen, die in der <Projekt>.gwt.xml konfiguriert sind. Hier müsste es im Prinzip auch möglich sein, Adressen zu verwenden, die nicht innerhalb der gleichen Webanwendung liegen.