Snäckbox in einem neuen Look

Wir haben ein kleines Update von Snäckbox hochgeladen. Eigentlich eine Abwandlung davon, in der es möglich ist gemischte Snäckboxen zusammenzustellen. Den Bestellvorgang haben wir vereinfacht und auch so sieht alles einwenig bunter aus ;)

August 3rd, 2011

Tags:, Posted in Snäckbox 3 Comments »

Modularisierung von GWT Anwendungen

Es macht viel Sinn eine GWT Anwendung zu modularisieren. Denn, was mit GWT richtig gut gelingt, ist die Wiederverwendung bereits geschriebenen Codes. Und, da der GWT Compiler nur referenzierte Klassen übersetzt, braucht man sich keine Sorgen über Code zu machen, der sich zwar im Modul befindet aber nicht verwendet wird. Das ist eine die gute Nachricht.

Ich habe mir ein Szenario ausgedacht, das veranschaulichen soll, wie die Modularisierung von GWT Projekten funktionieren kann; und ich setze dabei auf Maven. Insbesondere auf das GWT Maven Plugin.

Modul Abhängigkeiten

Modul Abhängigkeiten

  • API Ist ein Modul, dass Transferobjekte, Service-Interfaces und Command-Objekte enthält. Also der kleinste gemeinsame Nenner von Server und Client. Dieses Modul hat keine Abhängigkeiten zu GWT oder sonstigen Bibliotheken. Es enthält nur Klassen, die sowohl der Client, als auch der Server kennen müssen. So dass dieses Modul praktisch auch für andere Clients genutzt werden kann. Ich denke dabei an eine iPhone oder Android App.
  • GWT-Base Ist das solide Fundament weiterer GWT Module. Es kennt GIN, einen EventBus, eine RPC Schnittstelle, allgemeine Events, Fehlerhandling, Mocks, Testklassen, usw. Also alles das, was eine GWT Anwendung grundsätzlich benötigt und für das jedes mal ein neues Setup notwendig wäre.
  • GWT-Shop enthält die Basisfunktionalität des Shopsystems.
  • Die weiteren Module sind dann die konkrete Anwendungen.

Der Anfang – das Wurzelprojekt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<?xml version="1.0" encoding="UTF-8"?>
<project ...>
  <groupId>com.example.client</groupId>
  <artifactId>root</artifactId>
  <packaging>pom</packaging>
  <version>1.0</version>
 
  <modules>
    <module>api</module>
    <module>gwt-base</module>
    <module>gwt-shop</module>
    <module>gwt-...</module>
    <module>...</module>
  </modules>
 
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.7</version>
      <scope>test</scope>
    </dependency>
    ...
  </dependencies>
 
  <properties>
    <projectVersion>0.0.3</projectVersion>
    <gwtVersion>2.3.0</gwtVersion>
    <maven.compiler.source>1.6</maven.compiler.source>
    <maven.compiler.target>1.6</maven.compiler.target>
  </properties>
 
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>2.0.2</version>
        <configuration>
          <source>${maven.compiler.source}</source>
          <target>${maven.compiler.target}</target>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-site-plugin</artifactId>
        <version>2.0-beta-6</version>
        <configuration>
          <inputEncoding>UTF-8</inputEncoding>
          <outputEncoding>UTF-8</outputEncoding>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

Ich beginne mit dem Wurzelprojekt und definiere die groupId, die artifactId und die version. Das packaging muss pom sein, um Maven zu erklären, dass es Untermodule gibt, die sich auf dieses Wurzelprojekt beziehen.

Zudem kommen die einzelnen Module hinzu. Dann kann ich hier bereits die Möglichkeit nutzen Abhängigkeiten anzugeben, die in allen anderen Modulen ebenfalls benötigt werden. z.B. jUnit. Auch welche Compiler-Version zu verwenden ist und welches Encoding. Alles das, was nur einmal gemacht werden sollte und was für die Untermodule ebenfalls gilt.

Zum Schluss definiere ich noch eine Eigenschaft, die ich mit projectVersion benenne und als Wert eine Versionsnummer vergebe. Wozu die ist, dazu komme ich gleich.

Das API Modul

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<?xml version="1.0" encoding="UTF-8"?>
<project ...>
  <groupId>com.example.client</groupId>
  <artifactId>api</artifactId>
  <version>${projectVersion}</version>
 
  <parent>
    <groupId>com.example.client</groupId>
    <artifactId>root</artifactId>
    <version>1.0</version>
  </parent>
 
  <build>
    <resources>
      <resource>
        <directory>src/main/java</directory>
        <includes>
          <include>**/*.java</include>
        </includes>
      </resource>
      <resource>
        <directory>src/main/resources</directory>
        <includes>
          <include>**/*.xml</include>
        </includes>
      </resource>
    </resources>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-source-plugin</artifactId>
        <version>2.1.2</version>
        <executions>
          <execution>
            <id>attach-sources</id>
            <phase>verify</phase>
            <goals>
              <goal>jar-no-fork</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
</project>

Das Wurzelprojekt steht, so dass ich mich nun an das API Modul machen kann. Das es sich um ein reines Java-Modul ohne weitere Abhängigkeiten handelt, gibt es hier nur drei Besonderheiten, auf die ich aufmerksam machen möchte:

  1. Die Version des Moduls bekommt den Wert aus der in dem Wurzelprojekt definierten ${projectVersion}-Variable. In Maven gibt es leider nicht die Möglichkeit auf die Version des übergeordneten Projekts zu verweisen (Die Verwendung von ${parent.version} als Version ist nicht erlaubt). Andererseits möchte ich nicht für jedes Modul eine eigene Version pflegen, deshalb diese Krücke.
  2. Da der GWT Compiler den Quellcode seiner Abhängigkeiten kennen muss, nutze ich das maven-source-plugin, um alle Quellen (*.java) mit in das Archiv zu packen.
  3. Damit GWT dieses Modul als ein GWT-Modul erkennt, muss noch die Api.gwt.xml definiert werden und sich ebenfalls im Archiv befinden. Die Moduldefinition muss dabei lediglich den Packagenamen der für GWT sichtbaren Quellen enthalten:

1
2
3
4
<?xml version="1.0" encoding="UTF-8"?>
<module rename-to='client-api'>
  <source path='client'/>
</module>

Das GWT-Base Modul

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
<?xml version="1.0" encoding="UTF-8"?>
<project ...>
  <groupId>com.example.client</groupId>
  <artifactId>gwt-base</artifactId>
  <packaging>jar</packaging>
  <version>${projectVersion}</version>
 
  <parent>
    <groupId>com.example.client</groupId>
    <artifactId>root</artifactId>
    <version>1.0</version>
  </parent>
 
  <dependencies>
    <dependency>
      <groupId>com.example.client</groupId>
      <artifactId>api</artifactId>
      <version>${projectVersion}</version>
    </dependency>
    <dependency>
      <groupId>com.google.gwt</groupId>
      <artifactId>gwt-user</artifactId>
      <version>2.3.0</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>com.google.gwt</groupId>
      <artifactId>gwt-servlet</artifactId>
      <version>2.3.0</version>
      <scope>runtime</scope>
    </dependency>
    <dependency>
      <groupId>com.google.gwt.inject</groupId>
      <artifactId>gin</artifactId>
      <version>1.5.0</version>
    </dependency>
    <dependency>
      <groupId>com.google.inject</groupId>
      <artifactId>guice</artifactId>
      <version>3.0</version>
    </dependency>
    <dependency>
      <groupId>com.google.inject.extensions</groupId>
      <artifactId>guice-servlet</artifactId>
      <version>3.0</version>
    </dependency>
  </dependencies>
 
  <build>
    <resources>
      <resource>
        <directory>src/main/java</directory>
        <includes>
          <include>**/*.java</include>
        </includes>
      </resource>
      <resource>
        <directory>src/main/resources</directory>
        <includes>
          <include>**/*.xml</include>
        </includes>
      </resource>
    </resources>
 
    <plugins>
      <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>gwt-maven-plugin</artifactId>
        <version>2.3.0</version>
        <configuration>
          <compileReport>true</compileReport>
          <modules>
            <module>com.example.client.gwtbase.Base</module>
          </modules>
          <compileSourcesArtifacts>
            <compileSourcesArtifact>com.example.client:api</compileSourcesArtifact>
          </compileSourcesArtifacts>
        </configuration>
      </plugin>
 
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-source-plugin</artifactId>
        <version>2.1.2</version>
        <executions>
          <execution>
            <id>attach-sources</id>
            <phase>verify</phase>
            <goals>
              <goal>jar-no-fork</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
 
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-jar-plugin</artifactId>
        <version>2.2</version>
        <executions>
          <execution>
            <id>add-test-files</id>
            <goals>
              <goal>test-jar</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
 
  <repositories>
    <repository>
      <id>gin</id>
      <url>http://repo2.maven.org/maven2/</url>
    </repository>
  </repositories>
</project>

Jetzt wird es spannend, denn es geht um das erste richtige GWT Modul. Folgendes ist wichtig:

  1. Es wird die Abhängigkeit zum API Modul definiert.
  2. Es werden weitere Abhängigkeiten zu GWT, GIN und Guice definiert.
  3. Das GWT Maven Plugin kommt zum Einsatz und wird so konfiguriert, dass die Quellen des API-Moduls gezogen und mit übersetzt werden. Siehe compileSourcesArtifacts in der POM und mehr dazu gibt es hier.
  4. Dieses Modul wird ebenfalls mit den Quellen in das Maven Repository gepackt. Darüber hinaus werden auch noch die Quellen aus den Testverzeichnissen mitgenommen. Der Grund ist, auf diese Weise kann bereits hier die Testumgebung mit Mocks und Basis-Testklassen aufgebaut werden. Da Guice als DI Framework verwendet wird, bietet es sich an Guice-TestModule zu implementieren und sie dann in den anderen GWT Modulen wieder zu verwenden.

Wie die GIN und Guice Konfiguration für das Base-Modul aussieht, erkläre ich in einem anderen Post.

Weitere GWT Module

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
<?xml version="1.0" encoding="UTF-8"?>
<project>
  <artifactId>gwt-shop</artifactId>
  <version>${projectVersion}</version>
 
  <parent>
    <groupId>com.example.client</groupId>
    <artifactId>gwt</artifactId>
    <version>1.0</version>
  </parent>
 
  <dependencies>
    <dependency>
      <groupId>com.example.client</groupId>
      <artifactId>gwt-base</artifactId>
      <version>${projectVersion}</version>
    </dependency>
    <dependency>
      <groupId>com.example.client</groupId>
      <artifactId>gwt-base</artifactId>
      <version>${projectVersion}</version>
      <type>test-jar</type>
    </dependency>
    <dependency>
      <groupId>com.googlecode.gwtquery</groupId>
      <artifactId>gwtquery</artifactId>
      <version>1.0.0</version>
      <classifier>2.3.0</classifier>
      <scope>provided</scope>
    </dependency>
  </dependencies>
 
  <build>
    <plugins>
      <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>gwt-maven-plugin</artifactId>
        <version>2.3.0</version>
        <executions>
          <execution>
            <goals>
              <goal>compile</goal>
              <goal>test</goal>
              <goal>generateAsync</goal>
            </goals>
          </execution>
        </executions>
        <configuration>
          <modules>
            <module>com.example.client.gwtshop.Shop</module>
          </modules>
          <compileSourcesArtifacts>
            <compileSourcesArtifact>com.example.client:api</compileSourcesArtifact>
            <compileSourcesArtifact>com.example.client:gwt-base</compileSourcesArtifact>
          </compileSourcesArtifacts>
 
          <inplace>true</inplace>
          <webappDirectory>${gwt.war.dir}</webappDirectory>
          <warSourceDirectory>${gwt.war.dir}</warSourceDirectory>
        </configuration>
      </plugin>
    </plugins>
  </build>
 
  <repositories>
    <repository>
      <id>gwtquery-plugins</id>
      <url>http://gwtquery-plugins.googlecode.com/svn/mavenrepo</url>
    </repository>
  </repositories>
 
  <properties>
    <gwt.loglevel>INFO</gwt.loglevel>
    <gwt.war.dir>${basedir}/../../GRAILS_ROOT/web-app</gwt.war.dir>
  </properties>
</project>

Das GWT-Shop Modul soll hier das letzte sein. Aber auch aus dieser pom.xml gibt es was zu lernen:

  1. Zum einen definiert dieses Modul wie erwartet eine Abhängigkeit zu dem GWT-Base Modul. Aber diese Abhängigkeit existiert zwei mal. Einmal für die eigentlichen Quellen, und das zweite mal für die Test-Quellen. Das wird mit dem <type>test-jar</type> erreicht. Auf diese Weise kann auf die Testklassen des Basismoduls zugegriffen werden.
  2. Dann gibt es in der Konfiguration des GWT Maven Plugins ein executions Block, der dafür sorgt, dass in der Übersetzungsphase nicht nur der Java-Code übersetzt wird, sondern auch der GWT-Compiler anschließend anspringt und das JavaScript Kompilat erzeugt.
  3. Unter compileSourcesArtifacts werden nicht nur die API Quellen gezogen und in die Übersetzung mit einbezogen, sondern auch die Quellen des GWT-Base Moduls.
  4. Ich baue meine Anwendungen mit TeamCity. Das Deployment funktioniert so, dass zuerst die GWT Anwendungen übersetzt werden, dann die Grails Anwendung übersetzt und gepackt und anschließend in ein Tomcat deployed wird. Das alles geschiet automatisch und da bietet es sich an, die GWT Kompilate direkt in ein Verzeichnis der Grails Anwendung zu kopieren, damit es automatisch in das WAR-Archiv kopiert wird. Das geschieht mit der Angaben von webappDirectory und warSourceDirectory, die auf das Zielverzeichnis in der Grails Anwendung zeigen.

So, wenn ich alles richtig gemacht und nichts vergessen habe, dann sollte ein mvn clean install auf dem Wurzelprojekt zuerst das API-Modul und dann das GWT-Base Modul in das lokale Maven Repository installieren. Anschließend sollte das GWT-Shop Modul kompilieren. Und wenn das alles hingehauen hat und kein Test fehlgeschlagen ist, sollte sich das Kompilat in einem Verzeichnis innerhalb der Grails Anwendung befinden. Erste Sahne :)

Weiter geht es in einem anderen Post um das Setup des GWT-Moduls. Ich werde zeigen wie GIN aufgesetzt und die “Ginjectoren” über mehrere Module hinweg organisiert werden können. Außerdem geht es um RPC und den EventBus…

Juli 14th, 2011

Tags:,,, Posted in Grails,GWT 4 Comments »

Lessons learned: GWT

Ich arbeite seit knapp zwei Jahren mit GWT und habe bereits einiges ausprobiert. Meiner Meinung nach ist GWT eins der genialsten Werkzeuge, die einem Webentwickler gegeben wurden. Aber auch eins, mit dem man sich leicht verrennen kann.

Meine Lieblingskombination ist GWT zusammen mit Grails. Damit bin ich bisher ganz gut gefahren und es macht echt Spaß. Bloß, es gibt immer wieder Situationen, in denen ich mir sage: Na das hättest du aber auch leichter haben können.

Mit GWT neige ich dazu die Oberfläche mit UiBinder und Co. zu erstellen. Am besten auch noch komplett mit Layout. Es ist bequem und dann auch perfekt testbar. Das Ergebnis sind GWT Kompilate in der Größe von 1MB und mehr. Diese JavaScript Dateien wollen erstmal vom Client geladen und verarbeitet werden, was eine gewisse Zeit in Anspruch nimmt. Das neue CodeSplitting Feature erlaubt zwar mittlerweile die Kompilate zu trennen und ein Nachladen zu ermöglichen, aber weitere Probleme bleiben.

Beispiel: UI Monolithen. Wenn der Browser zunächst ein Fragment mit der Größe von 500KB und mehr laden muss, um dann den DOM aufzubauen, merkt der Benutzer das. Denn es dauert länger. Zwar kann man ihm einen kleinen Spinner zeigen, der ihn drauf hinweist, dass die Anwendung gerade etwas tut, aber gut fühlt sich das nicht an. Ziel wäre es dem Benutzer Inhalt zu zeigen, der ihn für die nächsten paar Sekunden beschäftigt, während der dynamische Code im Hintergrund allerlei anderen Kram macht.

Beispiel: Suchmaschinen. Wenn sich die GWT Anwendung in den DOM lädt, dann ist er oft vorher nackt. Den wirklich interessanten Markup Code erzeugt die GWT Anwendung und dieser beleibt für die Suchmaschine unsichtbar.

Beispiel: Dynamische Links. Rendere ich mit GWT eine dynamische Ansicht und möchte daraus auf weiteren Inhalt meiner Internetseite verweisen, muss ich GWT diesen Link irgendwie übermitteln. Da gibt es mehrere Möglichkeiten. Ich kann ein Feld im DOM verstecken und ihn dann über die ID ansprechen, oder ich übertrage die URL per RPC an den Client. Ändert sich aber meine Ansicht (kommt ein weiterer Link hinzu), dann muss zwangsweise serverseitig der Code angepasst werden, was die Entwicklung erschwert.

Beispiel: Formatierung. In der ersten Shop-Version von Snäckbox habe ich eine Klasse geschrieben, die Geldbeträge, Gewichte, Datum usw. formatiert. Der Ansatz war nicht schlecht, bloß absolut überflüssig. Denn die meisten Formatierungen mussten serverseitig ebenfalls implementiert werden, wenn beispielsweise eine Email versendet oder PDF erzeugt wurde. Doppelter Code also. Ich bin dann später dazu übergegangen nur noch formatierte Strings an den Client zu übertragen.

Beispiel: Lokalisierung. Mit GWT ist es einfach zu internationalisieren. Interface definieren, ein paar Annotations verwenden, fertig. Aber, serverseitig passiert das in den meisten Fällen auch. Vielleicht müssen sogar die selben Meldungen übersetzt werden (z.B. Fehlermeldungen). In dem Fall ist es wichtig diese Meldungen bei Änderungen synchron zu halten und bei einer wachsenden Applikation ist es irgendwann nicht möglich den Überblick darüber zu behalten.

Beispiel: Änderungen an der UI. Ändere ich die Farbe einer Linie oder die Schriftgröße, und sehe mir anschließend das Ergebnis im Browser an, muss ich unter GWT wegen einer CSS Lappalie ein paar Sekunden warten. Kommt ganz auf die Anwendungsgröße an. Bei einer Grails-Anwendung ist das Ergebnis schneller da. Auf den Tag gesehen summiert sich die Differenz. Ganz abgesehen davon, sollte das Programm auf den Entwickler warten, nicht umgekehrt.

Mein Fazit: Wenn ich die Möglichkeit habe serverseitig einige der hier beschriebenen Aufgaben zu erledigen, dann würde ich das tun. Insbesondere, was die Formatierung von Werten betrifft. Natürlich ist das von Problemstellung zu Problemstellung unterschiedlich und muss im Einzelfall entschieden werden. Aber Fakt bleibt, dass GWT all die wunderbaren Dinge ermöglicht und den Entwickler “verführt” Code zu schreiben, der z.B. in Grails viel schneller realisiert werden kann.

Lösungsvorschlag

Ich habe vor einigen Monaten begonnen die GWT Anwendungen auf eine anderen Weise zu entwickeln, als ich es bisher getan habe. Statt große Monolithe zu bauen, die eine komplette UI erstellen, setze ich auf starke Modularisierung der GWT Anwendungen, kurze Ladezeiten und so wenig UI Code, wie möglich. Zusätzlich versuche ich die Client-Anwendung so “dumm” zu halten, wie es geht. Sie soll weder Meldungen übersetzen, noch Geldbeträge oder Datum und Uhrzeit formatieren können.

Die Stärken von GWT versuche ich aber dennoch so gut es geht zu nutzen: Typisierung, Testbarkeit und RPC.

Ich möchte eine kleine Serie von Artikeln darüber schreiben und werde sie dann nach und nach hier verlinken. Anfangen werde ich mit der Modularisierung von GWT Anwendungen.

P.S.
Die Frage nach der Tauglichkeit in der Praxis, werde ich mit Snäckbox 2.0 beantworten, das in weniger als einem Monat online gehen wird ;)

Juli 14th, 2011

Posted in Grails,GWT No Comments »

Teil 2: Eine Facebook App mit Grails bauen

Nach dem ich mich im ersten Teil mit einem Fan-Gate beschäftigt habe, geht es mir in diesem Teil um eine Facebook-App, die auf Benutzerdaten zugreift.

Zunächst einmal kann jede Facebook-App auf bestimmte Informationen eines Benutzers zugreifen, ohne dass sie eine Erlaubnis des Benutzers benötigt. Dazu gehören der Vorname, Nachname und ein Bild des Benutzers. Möchte die App mehr über den Nutzer wissen, muss sie um Erlaubnis fragen und angeben, auf welche Daten zugegriffen wird.

Weil ich plane mit personenbezogenen Daten zu Arbeiten, halte ich es für eine gute Idee einen Controller zu schreiben, der stets sicherstellt, dass der aktuelle Anwender a) bei Facebook angemeldet, er b) meiner App die notwendigen Rechte gegeben hat und c) meine App mit Facebook kommunizieren darf. Den Controller nenne ich UserAwareController. Der grundsätzliche Aufbau sieht wie folgt aus:

abstract class UserAwareController {
  def beforeInterceptor = [action: this.&loadCurrentUser]
  def fbClient, currentUser
 
  def getFacebookUser() {
    ...
  }
 
  def getAccessToken() {
    session.fb?.access_token
  }
 
  void setAccessToken(String token) {
    if (session.fb) { session.fb = [:] }
    session.fb.access_token = token
  }
 
  def getFacebookClient() {
    if (!fbClient) { fbClient = new DefaultFacebookClient(accessToken) }
    fbClient
  }
 
  protected def loadCurrentUser() {
    ...
  }
}

Wann immer ich eine Funktion umsetzen möchte, die einen Facebook Benutzer voraussetzt, leite ich von diesem Controller ab. Das wichtigste Instrument ist der Interceptor, der prüft, ob ein Access Token vorliegt und wenn ja – ist er noch gültig? (Um mit Facebook zu kommunizieren, nutze ich RestFB, eine sehr schöne Bibliothek, die auf der Graph-API aufbaut.)

  ...
  protected def loadCurrentUser() {
    // Wenn ein Access Token verfügbar ist, aber der Facebook Benutzer noch nicht geladen ist,
    // dann holen wir ihn, um zu testen, ob der Access Token gültig ist.
    if (accessToken && !currentUser) {
      try {
        currentUser = facebookClient.fetchObject("me", User.class)
      }
      catch(FacebookOAuthException e) {
        log.warn "Something is wrong with the current access_token. Delete it and request a new one..."
        // Etwas scheint mit dem Access Token nicht i.O. zu sein. Lösche ihn!
        setAccessToken(null)
      }
    }
 
    // Wenn kein Access Token verfügbar ist, dann führe eine Authentifizierung durch.
    if (!accessToken) {
      def returnUrl = createLink(controller: params.controller, action: params.action)
      redirect(controller: 'auth', action: 'authenticate', params: [redirectUrl: returnUrl])
      return false
    }
 
    return true
  }
  ...

Wenn also ein Access Token da ist und der Facebook Benutzer mit der Graph API gelesen werden kann, ist alles gut und der ableitende Controller kann mit dem Benutzer arbeiten. Damit ich mir merke, welche Facebook Benutzer ich bereits kenne, erstelle ich ein Domain Model und speichere ein paar eindeutige Informationen, wie die Facebook-ID in die eigene Datenbank.

  ...
  def getFacebookUser() {
    def facebookUser = FacebookUser.findByFacebookId(currentUser.id)
    if (!facebookUser) {
      facebookUser = new FacebookUser(facebookId: currentUser.id)
      if (facebookUser.save()) {
        log.info "New facebook user saved into database"
      }
      else {
        log.error "Could not create new facebook user. Reason = ${facebookUser.errors}"
      }
    }
 
    facebookUser
  }
  ...

Falls der Access Token aber doch mal ungültig sein sollte, dann wird der Nutzer zum AuthController weitergeleitet, der den Nutzer anmeldet. Die Anmeldung läuft immer gleich ab.

  1. Authentifizierung des Anwenders
  2. Erteilung der Berechtigungen für den Zugriff auf personenbezogene Daten
  3. Authentifizierung der App

Mehr dazu hier.

Wenn ein Nutzer aber bereits bei Facebook angemeldet ist, dann muss er sich nicht nochmal anmelden. Genauso verhält sich das mit der Erteilung der Rechte für die Nutzung seiner Daten. Einmal erteilt, bleibt eine erneute Nachfrage aus (es sei denn er entzieht der App die Rechte wieder).

Authentifizierung bei Facebook

Ich muss zugeben, dass ich eine echte Nuss knacken musste, bevor das alles so funktionierte, wie ich mir das vorgestellt habe.

Für die Anmeldung des Benutzers und der App, habe ich ein AuthController erstellt und den Algorithmus, der in den FB-Docs beschrieben ist, in der authenticate Methode implementiert:

/**
 * Authentifiziert den Anwender und diese Anwendung gegen Facebook
 */
class AuthController {
  ...
  AuthController() {
    if (!session.fb) { session.fb = [:] }
  }
 
  ...
  def authenticate = {
    // Soll nach der Autehntifizierung ein Redirect auf
    // eine bestimmte URL stattfinden?
    if (params.redirectUrl) {
      session.fb.redirectUrl = params.redirectUrl
    }
 
    // Wenn ein Fehler aufgetreten ist, dann zeigen wir
    // dem Benutzer eine Seite mit dem Problem
    if (params.error) {
      log.warn "Some errors occured during authentication: ${params.error}"
      processAuthError()
    }
 
    // Wenn wir kein Code bekommen haben, mit dem wir die App
    // authentifizieren können, beginnen wir mit der Anmeldung
    // des Benutzers...
    else if (!params.code) {
      log.info "authenticate user..."
      authenticateUser()
    }
    // Wir haben ein Code bekommen. Melden wir nun
    // die App an...
    else {
      log.info "authenticate app with code ${params.code}"
      authenticateApp()
 
      // Wenn wir uns zuvor eine Zieladresse gemerkt haben,
      // dann führen wir nun den Redirect durch und löschen
      // die gemerkte Adresse
      if (session.fb.redirectUrl) {
        redirect(url: session.fb.redirectUrl)
        session.fb.redirectUrl = null
      }
      // Keine Redirect-Adresse. Zeige Wurzel der Anwendung
      else {
        redirect(url: config.grails.serverURL)
      }
    }
  }
  ...
}

Der Ablauf gestaltet sich wie folgt:

  1. Beim ersten Aufruf der authenticate Methode merkt sich der Controller, an welche URL weitergeleitet werden soll, sobald die Anmeldung abgeschlossen ist. Dann startet er die Anmeldung des Benutzers. Methode authenticateUser
  2. Wenn die authenticate Methode zum zweiten mal aufgerufen wird, dann ist der Benutzer angemeldet und hat der App die notwendigen Rechte gegeben. Es wird ein code-Parameter mitgeliefert, mit dem nun die App authentifiziert werden kann. Methode authenticateApp
  3. Wenn keine Fehler aufgetreten sind, wird der Browser an die zuvor gemerkte URL umgeleitet.

Anmeldung des Benutzers

Schauen wir uns zunächst die Anmeldung des Benutzers näher an.

  ...
  private def authenticateUser() {
    // Facebook soll diesen Wert in seiner Antwort liefern,
    // um zu beweisen, dass es sich um Facebook handelt.
    session.state = session.id.encodeAsSHA1()
    def redirectParams = [
            // Die ID der App
            'client_id': config.facebook.app.id,
            // Die Umleitung erfolgt auf die Canvas URL von Facebook.
            'redirect_uri':  "${config.facebook.url.base}/${config.facebook.app.name}/auth/authenticate".encodeAsURL(),
            // Die Rechte, die diese App braucht.
            'scope': PERMISSIONS.join(','),
            'state': session.state
    ]
 
    def url = createOAuthUrl(redirectParams)
    log.info "redirect to ${url}"
    // Fix für http://bugs.developers.facebook.net/show_bug.cgi?id=11326
    render(template: "/shared/fb_redirect", model: [redirectUrl: url])
  }
  ...

Diese Funktion baut eine URL in der Form https://www.facebook.com/dialog/oauth?client_id=15***8&redirect_uri=CANVAS_URL/auth/authenticate&state=49***d6f&scope=email,offline_access zusammen. An dieser URL gibt es eine Besonderheit: der redirect_uri-Parameter. Aus den Docs geht nicht klar hervor, wie sich dieser URL zusammenzusetzen hat. Deshalb habe ich bisher angenommen, dass es die URL meines Servers ist. Das ist falsch.

Die URL muss diese Form haben: http://apps.facebook.com/CANVAS_PAGE. Wobei CANVAS_PAGE den Einstellungen der App entnommen werden muss. Wenn dass nicht gegeben ist, leitet Facebook nach der Anmeldung den Benutzer auf meinen Server weiter und lädt die Anwendung nicht wieder in ein iFrame.

Hänge ich außerdem weitere Parameter an die URL an, werden sie an meine App weitergegeben. So wie in diesem Fall /auth/authenticate den Aufruf der authenticate-Methode meines AuthControllers bewirkt.

Eine zweite Besonderheit ist das Rendern des Templates "/shared/fb_redirect", dem die zuvor erstellte URL übergeben wird. Ein einfaches Redirect funktioniert hier Aufgrund eines Facebook-Bugs nicht. Deshalb muss eine JavaScript-Krücke her:

<%@ page contentType="text/html;charset=UTF-8" %>
<html>
  <body>
    <script type="text/javascript">top.location.href = "${redirectUrl}";</script>
  </body>
</html>

Hat der Benutzer sich angemeldet und der App die Rechte erteilt, ruft Facebook erneut die authenticate-Methode auf und übergibt einen code-Parameter mit dem nun die App authentifiziert wird. (Davon bekommt der Benutzer nichts mit)

  ...
  private def authenticateApp() {
    def code = params.code
    def urlParams = [
            // Die ID der App
            'client_id': config.facebook.app.id,
            // Der geheime Schlüssel der App
            'client_secret': config.facebook.app.secret,
            // Ist für unsere Zwecke nicht erforderlich, aber Facebook verlangt diesen Parameter
            'redirect_uri': "${config.facebook.url.base}/${config.facebook.app.name}/auth/authenticate".encodeAsURL(),
            // Der eben gelieferte Code
            'code': code
    ]
 
    def url = createAccessTokenUrl(urlParams)
    log.info "fetch resource: ${url}"
    // Authentifiziere die App und lese die Antwort von Facebook
    String result = fetchResource(url)
 
    // An dieser Stelle ist das Access Token verfügbar
    result.split(/&/).each { pair ->
      def (key, value) = pair.split('=')
      session['fb'][key] = value
    }
  }
  ...

An dieser Stelle ist der Benutzer angemeldet, hat der App Zugriffsrechte erteilt und auch die App ist für die Kommunikation mit Facebook bereit. Der Access Token ist in der Session (session.fb.access_token) gespeichert und kann genutzt werden.

Juni 22nd, 2011

Tags:, Posted in Grails,Groovy,Snäckbox No Comments »

Teil 1: Ein Facebook Fan-Gate/Reveal mit Grails bauen

Ok, auch wenn es einwenig nach Industriespionage klingt, ich habe eine Erlaubnis das hier zu schreiben. Soviel vorab ;) Meine Aufgabe besteht die nächsten Tage darin für snaeckbox.de ein Fan-Gate für Facebook zu schreiben. Es gibt eine Menge Artikel im Netz, wie das mit PHP zu realisieren ist, aber ein direktes Beispiel in Grails habe ich nicht gefunden. Deshalb gebe ich mal meine Lösung wieder:

Schritt 0: Erstelle eine leere und lauffähige Grails Anwendung.

Ich arbeite mit Maven und habe mit folgenden Befehl zunächst eine neue Grails Anwendung erzeugt…

mvn archetype:generate -DarchetypeGroupId=org.grails -DarchetypeArtifactId=grails-maven-archetype -DarchetypeVersion=1.3.4 -DgroupId=DEINE_GROUP_ID -DartifactId=DEINE_ARTIFACT_ID

Ich gehe im Nachfolgenden davon aus, dass du lokal eine Grails Anwendung hast und sie auch starten kannst. In diesem Beispiel läuft meine Anwendung unter http://localhost:8080/sb-cms-fb/

Schritt 1: Facebook Anwendung erstellen

Ich habe als erstes bei Facebook eine Anwendung erstellt. Namen der App eingegeben, die Bestimmungen akzeptiert und dann zu den Einstellungen navigiert.

Während der Entwicklung wäre es natürlich quatsch für jeden Test meine Anwendung auf einen im Netz erreichbaren Server zu deployen. Facebook öffnet die App in einem iFrame und da spricht nichts dagegen, dass die Anwendung während der Entwicklung lokal gehostet wird. Deshalb soll die Website-> Site URL auf meine lokal laufende Anwendung zeigen.
bildschirmfoto-2011-06-16-um-100249


Unter dem Punkt Facebook Integration -> Canvas Page habe ich einen Bezeichner für meine Anwendung vergeben. Es dürfen nur Buchstaben, Unterstriche und Bindestriche verwendet werden. Diesen Bezeichnet hängt Facebook an die URL http://apps.facebook.com/MEIN_BEZEICHNER an. Der Name sollte also schon etwas sein, was ich der Welt da draußen auch zumuten möchte.

Unter Facebook Integration -> Canvas URL stelle ich vorerst ebenfalls die lokale Serveradresse ein. Später wird das die Wurzeladresse meiner Anwendung im Netz.
bildschirmfoto-2011-06-16-um-100313


Zu guter letzt aktiviere ich noch den Sandbox-Modus. Damit stelle ich sicher, dass während der Entwicklung außer mir, keiner der Facebook Benutzer Zugriff auf die Anwendung bekommt. Wer der Administrator bzw. der Entwickler ist, bestimme ich übrigens unter Info -> Manage Users
bildschirmfoto-2011-06-16-um-101046

Schritt 2: Verbindung testen

Theoretisch funktioniert das so. Ich starte einen Server, der lokal unter http://localhost:8080/sb-cms-fb/ erreichbar ist. Dann navigiere ich im Browser auf http://apps.facebook.com/MEIN_BEZEICHNER. Facebook erzeugt ein iFrame, dass auf meine lokale Anwendung zeigt, und stellt mir den Inhalt dar.

Um das Ganze sauber zu gestalten, erstelle ich einen FanGateController und verdrahte in den UrlMappings.groovy die Wurzel mit diesem Controller.

class UrlMappings {
  static mappings = {
    ...
    "/"(controller: 'fanGate')
    ...
}

Im Controller implementiere ich das index-Closure mit dem folgendem Inhalt:

package de.snaeckbox.fb
 
class FanGateController {
 def index = {
    render(text: params.signed_request ?: 'params.signed_request missing')
  }
}

Ich starte nun Grails und navigiere über die Facebook-URL auf meine Anwendung. Was ich zu sehen bekomme ist ein kryptischer String, der bei mir wie folgt aussieht: dNnxTShtVfI0o0-Fvk(...)taW4iOjIxfX19

Facebook schickt beim Aufruf der Anwendung Informationen mit, die von Bedeutung sind. Der s.g. Signed Request. Diese Daten sind aus Sicherheitsgründen verschlüsselt und müssen dekodiert werden. Um die Implementierung ernsthaft anzugehen, würde ich vorschlagen einen FacebookService zu implementieren, das diese Arbeit übernimmt. Und weil zu jeder Klasse auch eine Testklasse gehört, dient der gesendete String von Facebook als Testdaten. Also in die Zwischenablage kopieren, Facebook Service erstellen und als statische Variable merken…

Schritt 3: Den Signed Request dekodieren

In der Zusammenfassung der Anwendung unter Facebook sind Anwendungsnummer, API-Schlüssel, etc. dargestellt. Diese habe ich in der Config.groovy erstmal als Konfiguration hinterlegt, damit der FacebookService später drauf zugreifen kann:

facebook {
  api {
    id        = '7226********'
  }
  app {
    id        = '2367******'
    secret    = '7d01********'
    baseurl   = 'http://apps.facebook.com'
    name      = 'sna****'
  }
}

Wie man den Request in Groovy/Grails dekodiert habe ich bei Matthias Gall gefunden und den Algorithmus größtenteils übernommen.

import grails.converters.JSON
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
 
class FacebookService {
  def grailsAppliction
 
  /**
   * Dekodiert den signierten FB Request und liefert ein JSON Objekt mit den Daten.
   *
   * @param request Der Request
   * @return Die dekodierten Daten
   */
  def decodeSignedRequest(String request) {
    if (!request || request.empty) {
      throw new IllegalArgumentException("Could not decode facebook request. It's empty!")
    }
 
    def parts = request.split(/\./, 2 )
    def (signature, payload) = [parts[0], parts[1]]*.tr( '-_', '+/' )*.decodeBase64()
 
    def data = JSON.parse(new String(payload))
    if (data.algorithm != "HMAC-SHA256") {
      throw new IllegalArgumentException("Unknown signature request algorithm: ${data.algorithm}")
    }
 
    def secret = config.app.secret
    def expectedSignature = Mac.getInstance("HmacSHA256").with {
      init(new SecretKeySpec(secret.bytes, "HmacSHA256"))
      update(parts[1].bytes)
      doFinal()
    }
 
    if(signature != expectedSignature) {
      throw new IllegalArgumentException("Request does not seem to be from Facebook. Signature mismatch!")
    }
 
    return data
  }
 
  def getConfig() {
    grailsAppliction.config.facebook
  }
}

In dem FacebookServiceTests teste ich mit dem zuvor kopierten String, ob der Dekodierungsmechanismus auch wirklich funktioniert. Testdaten sind schließlich alles. Wichtig ist natürlich, dass du deine Anwendungsdaten einträgst und nicht meine übernimmst. Also den signierten String und den Anwendungs-Geheimcode in der gemockten Konfiguration (config.app.secret, siehe setUp()).

class FacebookServiceTests extends GrailsUnitTestCase {
  def static SIGNED_REQUEST = "dNnxTShtVfI0o*********aW4iOjIxfX19"
  def service
 
  protected void setUp() {
    super.setUp()
    service = new FacebookService()
 
    service.metaClass.getConfig = { ->
      def config = new ConfigObject()
      config.app.secret  = '7d014***'
      return config
    }
  }
 
  void testDecodeSignedRequest() {
    def data = service.decodeSignedRequest(SIGNED_REQUEST)
    assertEquals(1308210538, data.issued_at)
    assertEquals(21, data.user.age.min)
    assertEquals("de_DE", data.user.locale)
    assertEquals("de", data.user.country)
  }
}

Natürlich wird der Test bei dir fehlschlagen, denn ich überprüfe im Test einen Zeitstempel, der von deinem abweichen wird und evtl. auch die Sprache. In dem Fall ist der Debugger dein Freund :)

Schritt 4: Test, ob ein “Like” bereits vorliegt, oder nicht

bildschirmfoto-2011-06-16-um-122222 Bis heute ist mir nicht ganz klar, wie ich es geschafft habe, dass die Anwendung in meiner Seitenleiste auftaucht. Sie war irgendwann nach etlichen Bemühungen einfach drin. :-/ Ich bin keine Facebook-Crack und musste mich durch den Einstellungs-Dschungel kämpfen.

Wie auch immer… Den FanGateController habe ich nun dahingehend abgeändert, dass der signierte FB Request an den FacebookService gegeben wird, der mir anschließend die JSON Daten liefert.

class FanGateController {
  def facebookService
  def index = {
    try {
      JSONElement data = facebookService.decodeSignedRequest(params.signed_request)
      log.info "FB user entered our fan gate: ${data}"
      def liked = facebookService.isLiked(data)
      render(view: liked ? 'liked' : 'notliked')
    }
    catch (IllegalArgumentException e) {
      render(text: 'Wir haben keine Daten von Facebook bekommen')
    }
  }
}

Diese Daten variieren in Abhängigkeit des Facebook-Nutzers. Es spielt z.B. eine Rolle, ob er angemeldet ist oder nicht. Oder ob er sich auf einer bestimmten Seite befindet, in der die App verknüpft ist oder nicht. Kommt der Request z.B. von einer Seite, dann wird ein page Objekt geliefert. Genau dieses Objekt zapfe ich an, um festzustellen, ob der der Benutzer der Facebook-App bereits sein Like gegeben hat oder nicht. Dazu erweitere ich den FacebookService.

  ...
  boolean isLiked(JSONElement jsonData) {
    jsonData?.page?.liked
  }
  ...

An dieser Stelle kannst du ansetzen und deine Views bauen, damit deine Fans auch ordentlich was hübsches zu sehen bekommen. Ich für mein Teil habe noch etwas zu tun und melde mich mit einem Nachtrag, sobald unser Fan-Gate online geht. Denn einfach einen Text zu hinterlegen reicht da nicht. Wir wollen unseren Fans auch was bieten ;) Was das sein wird? Dazu später…

Juni 16th, 2011

Tags:,,, Posted in Grails,Groovy No Comments »

Der neue Snäckbox-Blog ist gestartet

Mal sehen schauen welche Grails-Plugins es für WordPress gibt. Vielleicht integrieren wir den Blog in die snäckbox.de Seite…

Mai 2nd, 2011

Posted in Allgemeines No Comments »

PayPal IPN und Umlaute

Einer meiner Freunde, Marius, hat heute unbeabsichtigt und unbemerkt einige Exceptions auf Snäckbox ausgelöst.

Er hat bestellt und mit PayPal bezahlt. Nachdem die Zahlung bestätigt wurde, schickte der PayPal-Server eine Benachrichtigung an den Snäckbox-Server und sendete einen Haufen zusätzlicher Informationen mit.

Unteranderem wird die Lieferadresse des Käufers mitgeschickt. Der Snäckbox Server validiert die empfangenen URL-Parameter gegen den PayPal-Server, so wie in der PayPal-Dokumentation beschrieben. Was in der Regel bestens funktioniert, aber heute eben nicht ;)

Ich bekam eine Exception, dass der PayPal-Request INVALID sei. Was bedeuten würde, dass nicht der PayPal-Server die Benachrichtigung geschickt hat, sondern jemand anders.

Das Problem war der Straßenname der Lieferadresse, die Marius bei PayPal hinterlegt hat. Da drin war ein Umlaut. Und als PayPal die Bestätigung per IPN gesendet hat, kam auf der Snäckbox-Seite statt dem erwarteten ö(%C3%B6) ein %EF%BF%B an. Der Grund dafür war das Encoding, das PayPal verwendet hat. Die Daten wurden mit windows-1252 kodiert gesendet und kamen falsch an.

Die Lösung ist, das Encoding bei PayPal einzustellen (bei dem Konto, das die IPN Benachrichtigungen verschickt): Mein Profil -> Sprachcodierung -> Weitere Optionen -> Codierung auf UTF-8 setzen.

Danke Marius :)

April 20th, 2011

Tags:,, Posted in Snäckbox No Comments »

snaeckbox.de ist online gegangen

Ich bin stolz sagen zu können, dass snaeckbox.de endlich online ist :) War ein Stück Arbeit, aber wir haben trotz der Geburt meiner Tochter den Termin halten können.

Softwaretechnisch ist Snäckbox mein bisher coolstes Projekt. Ich habe zum ersten mal an einem Projekt mitgearbeitet, dass mir nach Monaten Entwicklung nicht auf den Geist geht, weil ein Bug-Fix, den nächsten Bug produziert. Bei Snäckbox ist es anders. Knapp 600 Tests machen die Entwicklung immer noch zu einem Genuss. Und jede neue Zeile Code sichern wir mit einem weiteren Test ab. Erfordert Disziplin, aber die Entwicklungszeit bleibt konstant und die Entwickler motiviert. Ein Hoch auf die Tests…

April 19th, 2011

Tags: Posted in Snäckbox 2 Comments »

Nach OS X Update: NoClassDefFoundError: org/codehaus/plexus/classworlds/launcher/Launcher

Hat mich einwenig Zeit gekostet, aber Apple hat scheinbar Maven3 mit dem neusten Java Update ausgeliefert. Daher führte bei mir der Aufruf von mvn zu der besagten NoClassDefFoundError: org/codehaus/plexus/classworlds/launcher/Launcher-Exception

Dank dem Artikel habe ich einen Blick in das /usr/share Verzeichnis geworfen und tatsächlich einen Link auf Maven3 gefunden.

$ ls -la /usr/share/maven
maven -> java/maven-3.0.2

Ich habe den Link entfernt und auf meine alte Maven Installation gesetzt – schon geht es :)

$ sudo rm /usr/share/maven
$ sudo ln -s /Library/Maven/2.2.1 /usr/share/maven
April 15th, 2011

Tags: Posted in Allgemeines No Comments »

Mehrere Grails Anwendungen mit einer gemeinsamen Code-Basis

Für Snäckbox wollte ich heute eine Architekturentscheidung treffen: Lasse ich das Projekt weiter aufblähen oder versuche ich es weiter zu modularisieren. Das Projekt wächst täglich, aber es sind noch lange nicht alle Features implementiert, die ich auf der Liste stehen habe. Ich möchte mich früh genug um eine saubere Architektur bemühen, damit ich später nicht in Schwierigkeiten gerate.

Ich setzte bisher auf die Kombination von Grails, GWT und Maven. Die Modularisierung von reinem Java Quellcode ist mit Maven sehr praktisch, doch wie werden Grails Anwendung modularisiert?

Ganz konkret möchte ich einige Domain Klassen auslagern, um sie gleichzeitig in einer Frontend- und einer Backend-Anwendung zu nutzen, muss diese Klasse den Grails Konventionen gehorchen. Sprich sie muss sich unter grails-app/domain befinden. Sie einfach zu kopieren ist eine denkbar dumme Idee, sie in ein Maven Modul zu packen und in ein Repository zu installieren wird leider nicht klappen. Denn wenn Grails zur Laufzeit die Domain Klassen unter grails-app/domain um dynamische Methoden erweitert, wird meine ausgelagerte Domain Klasse übergangen und verfügt folglich nicht über die save(), delete() und find*() Methoden.

Mir scheint ein Grails-Plugin eine sinnvolle Möglichkeit zu sein, meine Anwendung zu strukturieren. Was mich zu Beginn einwenig stocken ließ ist der automatisierte Build-Prozess, der auf meinem TeamCity Server läuft. Alle Module werden mit Maven gebaut. Diese Linie wollte ich beibehalten und habe einwenig geforscht, wie ich die Brücke zwischen meinem Grails-Plugin, der bestehenden Grails Anwendung und meinem Maven-Buildprozess schlagen kann.

Das Maven Publisher Plugin ist ein guter Anfang. Es bündelt das Plugin und installiert es in ein Maven kompatibles Repository.

Als erstes erzeuge ich ein Grails Plugin:
$ grails create-plugin sb-cms-base

Und installiere das Maven Publisher Plugin:
$ cd sb-cms-base
$ grails install-plugin maven-publisher

Da ich gewohnt bin mit Maven zu arbeiten, habe ich eine pom.xml erzeugen lassen, was sich als Fehler herausgestellt hat. Ich habe die install-Phase um die Ausführung von dem maven-install erweitert und versucht das Modul zu installieren. Das hat leider nicht funktioniert. Für jedes Maven Modul wird die Packaging-Methode angegeben <packaging>jar|war|ear|...</packaging> => zip ist aber nicht dabei. Die Folge ist, dass die pom.xml den Dateinamen des Ziel-Artefakts überlädt. Habe ich also also als Packaging-Methode war angegeben, baut mir das maven-publisher Plugin ein Archiv mit dem Namen sb-cms-base.zip, kopiert wird es aber nach sb-cms-base.war

Da ich dieses Plugin trotzdem automatisiert bauen und installieren will, benötige ich ein geeignetes Build-Werkzeug – z.B. das gute alte Ant ;)

Die build.xml und die ivy.xml kann Grails prima selbst erzeugen:
$ grails integrate-with --ant

Und noch die build.xml um ein neues Target ergänzen, welches das Plugin direkt in das lokale Maven Repository als *.zip installiert

...
<target name="maven-install" depends="-init-grails">
  <grails script="MavenInstall"/>
</target>
...

Hier ist übrigens ein netter Post über das Deployment in ein Remote-Repository.

Nun habe ich zwei Dinge erreicht. Erstens kann das neue Plugin in das bestehende Maven Repository deployed werden und zweitens hat mein TeamCity Server dank der Ant-Integration keine Schwierigkeiten dies für mich zu übernehmen.

Jetzt möchte ich dieses Plugin in einer anderen Grails Anwendung nutzen. Dazu definiere ich in der grails-app/conf/BuildConfig.groovy eine Abhängigkeit zu meinem Plugin und….

grails.project.dependency.resolution = {
  repositories {
    ...
    mavenLocal()
    mavenCentral()
    ...
  }
  dependencies {
    plugins {
      compile 'org.grails.plugins:sb-cms-base:0.1'
    }
  }
}

…, da ich mich auf Maven-Boden befinde, installiere es über die Kommandozeile in die zweite Grails Anwendung:
$ mvn grails:install-plugin -DpluginName=sb-cms-base

(Ich weis nicht genau ob das ein Caching Problem meines IDEA’s, oder ob mir ein Fehler unterlaufen ist, aber in der application.properties fehlte nach der Plugin-Installation der plugins.sb-cms-base=0.1 Eintrag – habe ich folglich ergänzt.)

Nun kann ich beginnen Domain Klassen, Services und weitere Komponenten, die von mehreren Grails Anwendungen genutzt werden sollen, in das Plugin auszulagern. Schön ist, dass sich diese Lösung in den Maven Buildprozess integrieren lässt.

Übrigens, sollte eine Exception kommen, die so etwas als Fehlermeldung enthält…

Cause: The name is undefined.
Action: Check the spelling.
Action: Check that any custom tasks/types have been declared.
Action: Check that any / declarations have taken place.

… dann könnte es an der Ant Version liegen. Grails kann mit 1.8.x nicht umgehen und benötigt eine 1.7.x

Update 1

War eine kleine Herausforderung, aber jetzt wird das Plugin auf dem TeamCity Server gebaut. Grails ist nicht installiert und die Abhängigkeiten werden über Ivy aufgelöst. Ich hatte Schwierigkeiten gehabt Spring Abhängigkeiten aufzulösen und kam nicht weiter. Ein ähnliches Problem wird bei stackoverflow.com beschrieben, aber der Lösungsvorschlag brachte bei mir nichts. Für mich hat die folgende Anpassung funktioniert:

Eine wichtige Veränderung ist der Methodenaufruf ebr() in der BuildConfig.groovy, der das SpringSource Enterprise Bundle Repository hinzuzieht. Zusätzlich habe ich ein paar Abhängigkeiten von vornherein ausgeschlossen. Die jsp-api stellt mein Container bereit, mit den xml-apis kollidiert der mit Groovy gebündelte XML-Parser, und Grails loggt über SLF4J, was das commons-logging vieler Apache Bibliotheken überflüssig macht.

grails.project.dependency.resolution = {
  inherits("global") {
    excludes 'jsp-api', 'xml-apis', 'commons-logging'
  }
  ...
  repositories {
    grailsPlugins()
    grailsHome()
    grailsCentral()
    ebr()
 
    mavenLocal()
    mavenCentral()
    mavenRepo "http://repository.codehaus.org"
    mavenRepo "http://download.java.net/maven/2/"
  }
  plugins {
    runtime 'org.grails.plugins:maven-publisher:0.7.5'
  }
  dependencies {
    ...
  }
}

Die ivy.xml habe ich um einige Grails-Abhängigkeiten erweitert, insbesondere die grails-scripts, ohne die das Ant-Buildskript nicht funktionieren würde.

  <dependencies>
    <dependency org="org.grails" name="grails-bootstrap" rev="1.3.6" conf="build"/>
    <dependency org="org.grails" name="grails-core" rev="1.3.6" conf="build"/>
    <dependency org="org.grails" name="grails-crud" rev="1.3.6" conf="build"/>
    <dependency org="org.grails" name="grails-gorm" rev="1.3.6" conf="build"/>
    <dependency org="org.grails" name="grails-web" rev="1.3.6" conf="build"/>
    <dependency org="org.grails" name="grails-test" rev="1.3.6" conf="build"/>
    <dependency org="org.grails" name="grails-scripts" rev="1.3.6" conf="build"/>
  </dependencies>

Und schließlich habe ich das Target maven-install inder der build.xml um die compile Abhängigkeit erweitert, da Grails sonst keine Chance hat das maven-publisher Plugin zu installieren:

  <target name="maven-install" depends="-init-grails, compile">
    <grails script="MavenInstall"/>
  </target>

Update 2
Es wollte einfach nicht… Irgendwie hat sich die slf4j-api:1.5.2 in das deployte WAR eingeschlichen. Lokal auf einem Rechner nicht, aber auf dem TeamCity Server schon. Grund war das hibernate:1.3.4-Plugin, das eine Abhängigkeit auf die slf4j-api:1.5.2 definiert. Egal was ich gemacht habe, ich habe sie nicht rausbekommen und musste schmerzlich erfahren, dass egal wie viel Zeit Grails + Maven + Ivy auch sparen, ein Großteil davon geht auf die Suche nach mysteriösen Fehlern wieder drauf.

Meine derzeitige Lösung sieht so aus, dass ich slf4j-api:1.5.2 global in der BuildConfig.groovy ausschließe:

grails.project.dependency.resolution = {
  inherits("global") {
    excludes 'jsp-api', 'xml-apis', 'commons-logging', 'slf4j-api'
  }
  ...
  dependencies {
    provided 'org.slf4j:slf4j-api:1.5.2'
  }
}
Februar 11th, 2011

Tags:,,,, Posted in Grails,Groovy No Comments »