Grails und GWT gemeinsam nutzen

By | 30. Mai 2010

Ich baue derzeit an einer Webanwendung in Grails und bin an einem Punkt angekommen, an dem ich unheimlich gern auf GWT zurückgreifen würde. Ich sehe in beiden Technologien viele Vorteile und würde nicht behaupten, dass es sich zwischen den beiden entscheiden sollte. Vielmehr denke ich, dass sich Grails und GWT sehr gut ergänzen können.

Es existiert ein Grails / GWT Plugin mit dem es möglich ist GWT im Frontend und Grails im Backend zu verwenden. Aber es nicht komplett das was ich suche. Denn ich will keine reine GWT-Applikation bauen. Was mir vorschwebt ist eine vollfunktionsfähige, in Grails geschriebene Webseite, die ohne JavaScript auskommt. Auch wenn die breite Masse der Browser JavaScript unterstützt, gibt einige Benutzergruppen die nicht von der clientseitigen Dynamik profitieren können – Nutzer eines Screen-Readers z.B.

Meine Idee ist es nun die Anwendung in Grails zu schreiben und die zusätzliche clientseitige Funktionalität durch GWT zu ergänzen. Ich habe mir ein Szenario ausgedacht, das wie ich denke, ziemlich deutlich macht, was ich will: Meine Webanwendung stellt eine Liste von POI’s dar. Also geo-referenzierte Punkte, die bestimmte Lokalitäten markieren. Ein Klick auf einen der POI Namen öffnet mir eine Karte mit einem Marker auf der geografischen Position des POI’s. Je nach dem ob JavaScript aktiviert ist oder nicht, erscheint die Karte als ein in JS erstelltes Popup oder es folgt der Verweis auf eine seperate URL, auf der eine statische Karte gezeigt wird.

Für das Beispielprojekt verwende ich Grails 1.2.2, GWT 2.0.3, GWT-Query 1.0.0, GWT-Maps 1.0.4, Maven2 und schreibe den Code in IntelliJ Idea. Die Grails Anwendung erzeuge ich mir mit Hilfe des Grails-Maven-Plugins:

$> mvn org.apache.maven.plugins:maven-archetype-plugin:2.0-alpha-4:generate \
     -DarchetypeGroupId=org.grails\
     -DarchetypeArtifactId=grails-maven-archetype \
     -DarchetypeVersion=1.2.2 \
     -DgroupId=org.example \
     -DartifactId=gwt-with-grails
$> cd gwt-with-grails
$> mvn initialize

Die erzeuge pom.xml beinhaltet ein Minimum an Konfiguration und zumindest in meinem Fall musste ich aufgrund einiger Exceptions noch einige Abhängigkeiten nach pflegen.

  <dependency>
    <groupId>org.grails</groupId>
    <artifactId>grails-test</artifactId>
    <version>1.2.2</version>
  </dependency>
  <dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.5.8</version>
  </dependency>
  <dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>servlet-api</artifactId>
    <version>2.5</version>
    <scope>provided</scope>
  </dependency>

Laut der Maven Konvention gehören die Quellen in das ${basedir}/src/main/java Verzeichnis, in dem ich mir das org.example.gwt.share Package anlege und eine Poi Klasse definiere. Der Aufbau ist denkbar einfach: Eine eindeutige Id, ein Name und die geografischen Koordinaten.

public class Poi {
  private final Long id;
  private String name;
  private double lat;
  private double lng;
 
  /* Konstruktoren, getter und setter */
  ...
}

Der Einfachheit halber arbeite ich in diesem Beispiel mit Dummy-Daten und erzeuge mir ein Grails Controller mit einigen, statischen POI’s. In dem index Closure wird die index View gerendert und alle POI’s an die View weitergegeben.

class PoiController {
 
  static def POIS = [:]
  static {
    POIS[1] = new Poi(1, "Hannover",  52.423, 9.72)
    POIS[2] = new Poi(2, "Dortmund",  51.516, 7.43)
    POIS[3] = new Poi(3, "Dresden",   51.048, 13.75)
    POIS[4] = new Poi(4, "Stuttgart", 48.800, 9.20)
  }
 
  def index = {
    render view: 'index', model: [pois: POIS.collect { k, v -> v }]
  }
  ... 
}

Damit ich zu einem späteren Zeitpunkt auf die Daten der einzelnen POIs zugreifen kann, hinterlege ich mir die wichtigen Informationen im Markup – und zwar in versteckten Feldern, denen ich die display:none; CSS Eigenschaft über eine Klasse zuweise. Ich bin gerade dabei zu evaluieren, wie man Elemente vor Screen-Readern versteckt…

Die versteckten Elemente benötige ich später, um von GWT aus an die wirklich interessanten Informationen zu gelangen. Diese Vorgehensweise erscheint mir besser zu sein, als inline-JavaScript zu platzieren und damit den Code über mehrere Views zu verteilen. Um den POI Namen habe ich ein Link gezogen, der auf die show-Action des selben Controllers verweist. Bei einem Klick auf den Link wird eine statische Karte mit der Position des POI’s gezeigt. Dieser Link spielt später noch eine wichtige Rolle.

<%@ page contentType="text/html;charset=UTF-8" %>
<%@ page contentType="text/html;charset=UTF-8" %>
<html>
  <head>
    <meta name="layout" content="poi" />
    <title>Some interesting Poi's</title>
    ...
  </head>
  <body>
    <g:each in="${pois}" var="poi">
      <div class="poi">
        <span class="hidden id"  >${poi.id}</span>
        <span class="hidden name">${poi.name}</span>
        <span class="hidden lng" >${poi.lng}</span>
        <span class="hidden lat" >${poi.lat}</span>
 
        <g:link class="link" controller="poi" action="show" id="${poi.id}">
          ${poi.name}
        </g:link>
      </div>
    </g:each>
  </body>
</html>

Klickt der Anwender auf den Link, landet der Request in der show-Action. Hier wird anhand des POI’s der Link zu dem Google Static Maps Server gebaut und zusammen mit dem POI an die View übergeben – die entsprechend schlicht gestrickt ist.

class PoiController {
  ...
  static def KEY = "MY_GOOGLE_MAPS_KEY"
 
  def show = {
    def poi = POIS[params.id ? params.id.toInteger() : 1]
    def link = new StringBuffer()
    link << "http://maps.google.com/staticmap"
    link << "?zoom=13"
    link << "&size=512x512"
    link << "&markers=${poi.lat},${poi.lng},blue"
    link << "&key=${KEY}"
    link << "&sensor=false"
    render view: 'show', model: [link: link, poi: poi]
  }
}
<%@ page contentType="text/html;charset=UTF-8" %>
<html>
  <head>
    <meta name="layout" content="poi" />
    <title>${poi.name}</title>
  </head>
  <body>
    <img src="${link}" alt="Map of ${poi.name}" title="${poi.lng},${poi.lat}" />
  </body>
</html>

Nun starte ich die Grails Anwendung über den grails:run-app Goal und sehe im Browser wie erwartet eine Liste der POI’s. Klicke ich auf einen der Einträge, wird die statische Karte als Bild im Browser geladen. Bis hier verhält sich meine Anwendung wie gewollt und ein Anwender mit deaktiviertem JavaScript wird die Anwendung bedienen können.

Nun geht es darum den Klick-Handler des zuvor erwähnten Links mit JavaScript zu überschreiben. Nicht die show-Action soll aufgerufen werden, sondern es soll ein mit JavaScript erzeugtes Popup erscheinen, in dem eine dynamische Google Maps Karte mit einem Marker geladen ist.

Ich erweitere meine pom.xml Datei um die GWT-Abhängigkeiten:

  ...
  <dependency>
    <groupId>com.google.gwt</groupId>
    <artifactId>gwt-servlet</artifactId>
    <version>2.0.3</version>
    <scope>compile</scope>
  </dependency>  
  <dependency>
    <groupId>com.google.gwt</groupId>
    <artifactId>gwt-user</artifactId>
    <version>2.0.3</version>
  </dependency>
  ...
  <plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>gwt-maven-plugin</artifactId>
    <version>1.2</version>
  </plugin>
  ...

Nachdem das erledigt ist, erstelle ich eine neues GWT Modul und nenne es org.example.gwt.Poi. Hier ist drauf zu achten, dass die Java-Quellen für Client und Server sich im ${basedir}/src/main/java-Verzeichnis und die Modulbeschreibungen *.gwt.xml im ${basedir}/src/main/resources-Verzeichnis befinden. So sieht momentan meine Verzeichnisstruktur aus:
bildschirmfoto-2010-06-01-um-105059
(Übrigens, die HTML und CSS Datei, die für ein GWT Modul normalerweise erzeugt wird, benötigen wir nicht.) In meinem GWT Modul habe ich mir ein EntryPoint erstellt und es unter PoiEntryPoint gespeichert. Nun kommt ein wichtiger Schritt. Das GWT Modul muss einmal übersetzt werden. Dafür rufe ich den gwt:compile Goal auf und kopiere anschliessend das Kompilat, das sich irgendwo unter ${basdir}/target befindet nach ${basedir}/web-app/js. Das muss nur ein mal gemacht werden, damit der Browser die Einstiegs-JavaScript Datei findet und die GWT Anwendung sich auf dem gewohnten Weg starten und debuggen lässt. Die einzelnen Schritte können auf der GWT Seite nachgelesen werden.

In Grails muss ich nun dafür sorgen, dass der Einstiegspunkt in die GWT Anwendung geladen wird. Das ist die org.example.gwt.Poi.nocache.js Datei, die ich zuvor kopiert habe. Es bietet sich an diesen Import in einer Layout-View durchzuführen.

<html>
  <head>
    <title><g:layoutTitle default="My POIs"/></title>
    <g:layoutHead/>
  </head>
  <body>
    <g:layoutBody/>
    <!-- Wichtiger Schritt -->
    <g:javascript library="org.example.gwt.Poi/org.example.gwt.Poi.nocache"/>
  </body>
</html>

Starte ich meine Grails-Anwendung erneut, muss der Browser die org.example.gwt.Poi.nocache.js Datei geladen haben. Dafür am besten mal mit dem Safari Inspector oder Firebug den Inhalt der Datei betrachten. Befindet sich minifizierter JavaScript Code im Inhalt, ist alles gut gegangen.

An dieser Stelle ist die Brücke zu GWT bereits geschlagen und ich kann mich an die Implementierung des EntryPoints machen. Für GWT existiert ein jQuery Nachbau, mit dem es unheimlich einfach ist Elemente aus dem DOM abzugreifen und Operationen wie Event-Handler, Animationen oder CSS auf den Elementen durchzuführen. Das Modul heißt GwtQuery. (Ich habe das Modul in mein lokales Maven Repository installiert, weil ich kein öffentliches Repository fand, aus dem ich das ziehen konnte.)

public class PoiEntryPoint implements EntryPoint {
  private MapWidget map = null;
  private PopupPanel mapsPanel;
 
  public void onModuleLoad() {
    $(".poi").each(new Function() {
      public void f(Element e) {
        final Long id = Long.parseLong($(e).find("span.id").text());
        final Double lat = Double.parseDouble($(e).find("span.lat").text());
        final Double lng = Double.parseDouble($(e).find("span.lng").text());
        final String name = $(e).find("span.name").text();
 
        $(e).find("a.link").click(new Function() {
          public boolean f(Event e) {
            displayPoi(new Poi(id, name, lat, lng));
            return false;
          }
        });
      }
    });
  }
...

In dem EntryPoint iteriere ich über alle DIV-Container mit der Klasse poi und lese die versteckten Informationen aus. Der Klick-Handler des Links, der eigentlich auf die show Action verweist überschreibe ich und sorge mit dem return false; dafür, dass das Ziel nicht angesprungen wird. Stattdessen soll der POI auf einer Karte in einem Popup-Dialog erscheinen.

bildschirmfoto-2010-06-01-um-135714

Dadurch, dass meine Grails Anwendung bereits in einem Container läuft und mir die HTML Seiten mit Inhalt rendert, benötige ich keinen weiteren Server, den GWT im Normalfall starten würde. Deshalb erstelle ich mir eine GWT Konfiguration mit der -noserver Direktive und übergebe als Start-URL die URL meines Grails-Servers. In meinem Fall die http://localhost:8080/gwt-with-grails/poi/index

Nun setzte ich in dem EntryPoint einen Breakpoint, starte die GWT Konfiguration und öffne den Browser. Alles läuft gut, wenn in der onModuleLoad Methode über meine POI Elemente iteriert wird und die Klick-Handler erfolgreich überschrieben sind.

Der Rest des Codes ist trivial, aber ich poste ihn der Vollständigkeit wegen:

  ...
  private void displayPoi(final Poi poi) {
    loadGoogleMaps(new MapsCallback() {
      public void doWithMap(MapWidget mapWidget) {
        final LatLng coords = LatLng.newInstance(poi.getLat(), poi.getLng());
        map.clearOverlays();
        map.setCenter(coords);
        map.setZoomLevel(12);
        map.addOverlay(new Marker(coords));
        map.getInfoWindow().open(map.getCenter(), new InfoWindowContent(poi.getName()));
        mapsPanel.show();
      }
    });
  }
 
  private void loadGoogleMaps(final MapsCallback mapsCallback) {
    if (map != null) {
      mapsCallback.doWithMap(map);
    }
    else {
      mapsPanel = new PopupPanel(true, true);
      mapsPanel.setGlassEnabled(true);
      mapsPanel.setWidget(new Label("Loading..."));
      mapsPanel.setWidth("500px");
      mapsPanel.setHeight("500px");
 
      mapsPanel.setPopupPositionAndShow(new PopupPanel.PositionCallback() {
        public void setPosition(int offsetWidth, int offsetHeight) {
          int left = (Window.getClientWidth() - offsetWidth) / 2;
          int top = (Window.getClientHeight() - offsetHeight) / 2;
          mapsPanel.setPopupPosition(left, top);
        }
      });
 
      Maps.loadMapsApi("", "2", false, new Runnable() {
        public void run() {
          map = new MapWidget();
          map.setSize("100%", "100%");
          map.addControl(new LargeMapControl());
          mapsPanel.setWidget(map);
          mapsCallback.doWithMap(map);
        }
      });
    }
  }
 
  private interface MapsCallback {
    public void doWithMap(MapWidget mapWidget);
  }

Klicke ich nun auf einen POI Namen, öffnet sich ein modaler Dialog mit der Karte und dem Marker.

4 thoughts on “Grails und GWT gemeinsam nutzen

  1. Mark

    Danke für dieses tolle Tutorial. Sitze jetzt schon einige Nächte daran Grails mit GWT zu verbinden und gleichzeitig aber noch den Debug starten zu können. Jetzt klappt es!!
    Jetzt muss ich nur noch eine vernüftige ModuleBaseURL hinkriegen.

  2. Sergej Post author

    @Mark, danke für die Blumen 🙂

  3. Mark

    Sergey, hast du auch bereits einen Build task der beim deploy oder beim erstellen des war files die gwt scripts and die richtige stelle der applikation kopiert?

  4. Sergej Post author

    Hallo Mark,

    das macht das Maven GWT Plugin. Das steuerst du in der Regel über die Konfiguration des Plugins. Folgende Parameter sind entscheidend:
    <inplace>true</inplace>
    <webappDirectory>${gwt.war.dir}</webappDirectory>
    <warSourceDirectory>${gwt.war.src.dir}</warSourceDirectory>
    <webXml>${gwt.webxml.file}</webXml>

    Wobei webappDirectory das Verzeichnis ist, in das GWT das Kompilat ablegt.

    Gruß
    Sergej

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.