Tworzenie aplikacji Google Web Toolkit z Java Persistence API

Z Jacek Laskowski - Wiki Projektanta Java EE

Google Web Toolkit (GWT) zdaje się być niebagatelnym rozwiązaniem do tworzenia aplikacji internetowej. Jest to nadzwyczaj ciekawy projekt do tworzenia aplikacji uruchamianych w przeglądarce w Javie. Do niedawna to część serwerowa aplikacji była główną częścią z punktu widzenia projektanta rozwiązania, gdzie strona kliencka (uruchamiania w przeglądarce) była generowana dynamicznie po wykonaniu serii operacji na serwerze. Z GWT sprawa jest całkowicie odmienna - to strona kliencka dyktuje warunki stronie serwerowej i jest główną częścią aplikacji korzystając z części serwerowej jako zestawu usług i sprowadzając rolę serwera do roli drugoplanowej (niektórzy przyrównują rolę serwera do terminala). Część kliencka wymaga znajomości JavaScript, CSS, HTML i DHTML, co dla programisty rozeznanego w technologiach serwerowych może sprawić nie lada wyzwanie. Szczęśliwie GWT sprowadza tworzenie całej aplikacji - części klienckiej i serwerowej - do wspólnego mianownika, tj. do języka Java. Tworzenie części klienckiej w GWT to tworzenie interfejsu użytkownika podobne do biblioteki JFC/Swing, a to zazwyczaj znane jest większości programistów Java. Korzystając z możliwości GWT programista strony serwerowej z łatwością może podjąć się wyzwania stworzenia bardzo zaawansowanego interfejsu użytkownika z pomocą GWT wykorzystując usługi serwera aplikacyjnego Java EE. Jedną z usług serwera Java EE jest Java Persistence API (JPA), która mimo, że należy do specyfikacji Enterprise JavaBeans 3.0, może być również uruchomiona poza serwerem, w trybie zarządzanym przez aplikację.

W tym artykule przedstawię sposób integracji GWT z JPA. Zaprezentowane zostanie działanie mechanizmu serializacji GWT, GWT RPC oraz obsługa zdarzeń i kontrolki graficzne GWT. Projekt zarządzany będzie przez Apache Maven 2 i uruchamiany na kontenerze servletów Jetty 6 wraz z Apache Derby. Jako projekt pomocniczny zostanie wykorzystany Dozer.

Rola poszczególnych projektów składowych w aplikacji Rejestr Osobowy:

  • GWT - tworzenie zaawansowanego interfejsu użytkownika uruchamianego jako JavaScript w przeglądarce programując w Javie
  • JPA - mechanizm trwałości danych
  • Apache Maven 2 - narzędzie zarządzające projektem, m.in. budowanie, uruchamianie, testowanie, przygotowanie wersji dystrybucyjnej, itp.
  • Apache OpenJPA - dostawca JPA
  • Apache Derby - relacyjna baza danych
  • Dozer - mapowanie stanu egzemplarza pewnego typu do innego
  • Jetty - kontener servletów, w którym będzie uruchomiona pełna aplikacja

Aplikacja będzie dostarczała funkcjonalność wyszukiwania osób i prezentacji ich danych. Użytkownik ma do dyspozycji panel wyszukiwania i wyników. Wprowadzając pojedyńcze litery nazwiska, aplikacja wywołuje usługę zdalną opartą o mechanizm GWT RPC do pobrania danych z bazy danych Apache Derby za pomocą JPA. Zdalna usługa udostępnia dwie funkcjonalności - pobranie ilości dostępnych osób o nazwisku rozpoczynającym się podanym ciągiem znaków oraz samo pobranie osób do wyświetlenia.

Gotowy projekt do uruchomienia (cała struktura projektu wraz z niezbędnymi elementami składowymi opisanymi poniżej) można pobrać jako gwt-rejestrosobowy.zip. Wystarczy jedynie wykonać polecenie mvn clean jetty:run-exploded.

Spis treści

Utworzenie projektu aplikacji internetowej z GWT - Rejestr Osobowy

Realizacja projektu oparta jest o środowisko opisane w artykule - Tworzenie aplikacji z Google Web Toolkit i Apache Maven 2. Rozpoczynamy od stworzenia projektu przy pomocy M2 i zdefiniowaniu koniecznych zależności GWT.

Rozpoczynamy w wybranym przez siebie katalogu, np. C:\.

$ mvn archetype:create -DarchetypeArtifactId=maven-archetype-webapp -DarchetypeVersion=1.0 -DgroupId=pl.jaceklaskowski.gwt.rejestrosobowy -DartifactId=gwt-rejestrosobowy
[INFO] Scanning for projects...
[INFO] Searching repository for plugin with prefix: 'archetype'.
[INFO] ----------------------------------------------------------------------------
[INFO] Building Maven Default Project
[INFO]    task-segment: [archetype:create] (aggregator-style)
[INFO] ----------------------------------------------------------------------------
...
[INFO] Archetype created in dir: c:\gwt-rejestrosobowy
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 1 second
[INFO] Finished at: Sun May 13 15:42:15 CEST 2007
[INFO] Final Memory: 5M/254M
[INFO] ------------------------------------------------------------------------

Przechodzimy do katalogu gwt-rejestrosobowy.

$ cd gwt-rejestrosobowy/

Kolejne polecenia wydawane będą właśnie z tego katalogu, który reprezentuje nasz projekt aplikacji internetowej.

Zmiany w projekcie najlepiej wykonywać jest za pomocą zintegrowanego środowiska programistycznego (ang. IDE - integrated development environment). Dowolne IDE może być wykorzystane w projekcie, jednakże należy się zdecydować które. Skorzystamy z Eclipse IDE 3.3m7.

Wykonujemy polecenie mvn eclipse:eclipse, aby utworzyć pliku projektu do importu projektu do Eclipse.

$ mvn eclipse:eclipse
[INFO] Scanning for projects...
[INFO] Searching repository for plugin with prefix: 'eclipse'.
[INFO] ----------------------------------------------------------------------------
[INFO] Building gwt-rejestrosobowy Maven Webapp
[INFO]    task-segment: [eclipse:eclipse]
[INFO] ----------------------------------------------------------------------------
...
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 1 second
[INFO] Finished at: Sun May 13 15:46:47 CEST 2007
[INFO] Final Memory: 3M/254M
[INFO] ------------------------------------------------------------------------ 

Komunikat BUILD SUCCESSFUL wskazuje na poprawnie wykonane polecenie. Uruchamiamy Eclipse IDE i importujemy projekt (File > Import > Existing Projects into Workspace).

UWAGA: Może się zdarzyć, że po imporcie pojawi się komunikat ostrzegawczy - Unbound classpath variable: M2_REPO/com/google/gwt/gwt-servlet/1.3.3/gwt-servlet-1.3.3.jar in project gwt-witajswiecie - co oznacza brak definicji zmiennej M2_REPO w wybranej przestrzeni roboczej. Mimo, że Eclipse zgłasza to jako komunikat ostrzegawczy, jest to znaczący błąd, gdyż brak biblioteki gwt-servlet-1.3.3.jar nie pozwoli na tworzenie klas korzystających z GWT. Naprawiamy błąd poprzez Window > Preferences... > Java > Build Path > Classpath Variables > New.... Dodajemy M2_REPO ze wskazaniem na lokalne repozytorium M2, potwierdzamy ponowne przebudowanie projektu i voila - projekt jest czysty, bez jakichkolwiek komunikatów ostrzegawczych, gotowy do dalszego rozwoju.

Modyfikujemy pom.xml o definicje repozytoriów, konfigurację wtyczek, przede wszystkim maven-googlewebtoolkit2-plugin oraz deklarację zależności projektowych.

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>pl.jaceklaskowski.gwt.rejestrosobowy</groupId>
  <artifactId>gwt-rejestrosobowy</artifactId>
  <packaging>war</packaging>
  <version>1.0-SNAPSHOT</version>
  <name>Rejestr Osobowy GWT</name>
  <url>http://www.JacekLaskowski.pl</url>
  <repositories>
    <repository>
      <id>gwt-maven</id>
      <url>http://gwt-maven.googlecode.com/svn/trunk/mavenrepo/</url>
    </repository>
  </repositories>
  <pluginRepositories>
    <pluginRepository>
      <id>gwt-maven</id>
      <url>http://gwt-maven.googlecode.com/svn/trunk/mavenrepo/</url>
    </pluginRepository>
  </pluginRepositories>
  <dependencies>
    <dependency>
      <groupId>com.google.gwt</groupId>
      <artifactId>gwt-user</artifactId>
      <version>1.3.3</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>com.google.gwt</groupId>
      <artifactId>gwt-servlet</artifactId>
      <version>1.3.3</version>
    </dependency>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.1</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
  <build>
    <finalName>gwt-rejestrosobowy</finalName>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>2.0</version>
        <configuration>

Niepoprawny język.

Musisz wybrać język w następujący sposób: <source lang="html4strict">...</source>

Języki obsługiwane w podświetlaniu składni:

abap, actionscript, actionscript3, ada, apache, applescript, apt_sources, asm, asp, autoit, avisynth, bash, basic4gl, bf, bibtex, blitzbasic, bnf, boo, c, c_mac, caddcl, cadlisp, cfdg, cfm, cil, cmake, cobol, cpp, cpp-qt, csharp, css, d, dcs, delphi, diff, div, dos, dot, eiffel, email, erlang, fo, fortran, freebasic, genero, gettext, glsl, gml, gnuplot, groovy, haskell, hq9plus, html4strict, idl, ini, inno, intercal, io, java, java5, javascript, kixtart, klonec, klonecpp, latex, lisp, locobasic, lolcode, lotusformulas, lotusscript, lscript, lsl2, lua, m68k, make, matlab, mirc, modula3, mpasm, mxml, mysql, nsis, oberon2, objc, ocaml, ocaml-brief, oobas, oracle11, oracle8, pascal, per, perl, php, php-brief, pic16, pixelbender, plsql, povray, powershell, progress, prolog, properties, providex, python, qbasic, rails, rebol, reg, robots, ruby, sas, scala, scheme, scilab, sdlbasic, smalltalk, smarty, sql, tcl, teraterm, text, thinbasic, tsql, typoscript, vb, vbnet, verilog, vhdl, vim, visualfoxpro, visualprolog, whitespace, whois, winbatch, xml, xorg_conf, xpp, z80

          <target>1.5</target>
        </configuration>
      </plugin>
      <plugin>
        <groupId>com.totsp.gwt</groupId>
        <artifactId>maven-googlewebtoolkit2-plugin</artifactId>
        <version>1.5.3-SNAPSHOT</version>
        <configuration>
          <runTarget>pl.jaceklaskowski.gwt.rejestrosobowy.RejestrOsobowyGWT/RejestrOsobowyGWT.html</runTarget>
          <compileTarget>
            <param>pl.jaceklaskowski.gwt.rejestrosobowy.RejestrOsobowyGWT</param>
          </compileTarget>
          <gwtHome>c:/apps/gwt</gwtHome>
        </configuration>
        <executions>
          <execution>
            <goals>
              <goal>mergewebxml</goal>
              <goal>compile</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-war-plugin</artifactId>
        <version>2.0.2</version>
        <configuration>
          <webXml>target/web.xml</webXml>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

Po zmianie zależności ponownie generujemy pliki projektu dla wybranego środowiska programistycznego IDE.

$ mvn eclipse:eclipse
[INFO] Scanning for projects...
[INFO] Searching repository for plugin with prefix: 'eclipse'.
[INFO] artifact org.apache.maven.plugins:maven-eclipse-plugin: checking for updates from gwt-maven
[INFO] ----------------------------------------------------------------------------
[INFO] Building Rejestr Osobowy GWT
[INFO]    task-segment: [eclipse:eclipse]
[INFO] ----------------------------------------------------------------------------
...
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 2 seconds
[INFO] Finished at: Sun May 13 16:26:26 CEST 2007
[INFO] Final Memory: 4M/254M
[INFO] ------------------------------------------------------------------------

Z BUILD SUCCESSFUL możemy być pewni, że wszystko jest poprawnie zestawione.

Utworzenie usługi zdalnej aplikacji - Rejestr

Kolejnym krokiem jest stworzenie usługi zdalnej zgodnie z procedurą opisaną w moim poprzednim wpisie o GWT RPC - GWT RPC raz jeszcze, RequestBuilder oraz Timer. Dodatkowo skorzystamy z rad opisanych w artykule Build an Ajax-enabled application using the Google Web Toolkit and Apache Geronimo, Part 1: Run compiled Google Web Toolkit applications on Geronimo, który "idzie" dalej z tematem uruchamiając projekt GWT w środowisku serwera aplikacyjnego Java EE - Apache Geronimo.

Interfejs usługi - Rejestr

Interfejs usługi Rejestr przedstawia się następująco:

package pl.jaceklaskowski.gwt.rejestrosobowy.client;

import java.util.List;

import com.google.gwt.core.client.GWT;
import com.google.gwt.user.client.rpc.RemoteService;
import com.google.gwt.user.client.rpc.ServiceDefTarget;

public interface Rejestr extends RemoteService {

    /**
     * @gwt.typeArgs <pl.jaceklaskowski.gwt.rejestrosobowy.client.Osoba>
     */
    public List getOsoby(String nazwisko);

    public long getIloscOsob(String nazwisko);

    public static class App {
        private static RejestrAsync ourInstance = null;

        public static synchronized RejestrAsync getInstance() {
            if (ourInstance == null) {
                ourInstance = (RejestrAsync) GWT.create(Rejestr.class);
                ((ServiceDefTarget) ourInstance).setServiceEntryPoint(GWT.getModuleBaseURL() + "services/rejestr");
            }
            return ourInstance;
        }
    }
}

UWAGA: Tymczasowo ignorujemy komunikaty błędów związane z brakiem klas w projekcie. Naprawimy to w kolejnych krokach.

Usługa udostępnia dwie metody - getOsoby oraz getIloscOsob. Pierwsza z nich - getOsoby - służy do pobrania użytkowników spełniających zadane kryteria wyszukiwania (w naszym przykładzie będzie to posiadanie nazwiska, które rozpoczyna się podanym wzorcem), a druga - getIloscOsob - zwraca ilość osób, które zostaną zwrócone przez metodę getOsoby. Posiadanie dwóch metod jest podyktowane wydajnością dostarczanej funkcjonalności, w której użytkownik ma możliwość wprowadzania części nazwiska i będzie informowany na bieżąco o ilości dostępnych osób spełniających kryterium, a po wciśnięciu przycisku ENTER bądź wybraniu przycisku Wyszukaj będzie wyświetlona tabela z danymi wyszukanych osób. Gdyby przesyłać informację o ilości osobach w postaci egzemplarzy możliwych do wyszukiwania osób znacząco wydłużyłoby czas reakcji aplikacji.

Interfejs usługi musi rozszerzania interfejsu GWT - RemoteService. Jest to obowiązkowy interfejs każdej usługi zdalnej w GWT RPC.

Na uwagę zasługuje adnotacja GWT - @gwt.typeArgs, który informuje o typie przechowywanym w kolekcji List. Ze względu na ograniczenia GWT odnośnie wersji Java 1.4.2 i poprzednich, z której GWT korzysta, typy generyczne nie są dostępne i jest to jedyny sposób poinstruowania GWT o typie w celu optymalizacji działania. Więcej informacji o adnotacji gwt.typeArgs znajduje się w dokumentacji GWT - Serializable Types.

Plik interfejsu - Rejestr.java - umieszczamy w katalogu src/main/java/pl/jaceklaskowski/gwt/rejestrosobowy/client.

"Transportowa" klasa serializowalna - Osoba

W części klienckiej aplikacji modelujemy byt osoba w postaci klasy Osoba. Klasa będzie przechowywała dane przesyłane z części klienckiej do serwerowej.

package pl.jaceklaskowski.gwt.rejestrosobowy.client;

import com.google.gwt.user.client.rpc.IsSerializable;

public class Osoba implements IsSerializable {

    private String identyfikator;
    private String imie;
    private String nazwisko;

    public String getIdentyfikator() {
        return identyfikator;
    }

    public void setIdentyfikator(String identyfikator) {
        this.identyfikator = identyfikator;
    }

    public String getImie() {
        return imie;
    }

    public void setImie(String imie) {
        this.imie = imie;
    }

    public String getNazwisko() {
        return nazwisko;
    }

    public void setNazwisko(String nazwisko) {
        this.nazwisko = nazwisko;
    }

    public String toString() {
        StringBuffer sb = new StringBuffer();
        sb.append(this.identyfikator);
        sb.append(" - ");
        sb.append(this.imie);
        sb.append(" ");
        sb.append(this.nazwisko);
        return sb.toString();
    }
}

Klasa, która uczestniczy w transporcie danych między usługą zdalną a częścią kliencką aplikacji musi być serializowalna w sensie GWT. Jednym z istotnych elementów w mechaniźmie GWT RPC jest mechanizm serializacji, czyli zamiany postaci obiektowej egzemplarza na postać strumienia bajtów i na odwrót. Jest to mechanizm, który można wykorzystać do zapisywania stanu egzemplarza na dysk czy przesłać strumień bajtów do zdalnego klienta uruchomionego na oddzielnej wirtualnej maszynie. W Javie służy do tego interfejs java.io.Serializable podczas, gdy GWT dostarcza własnej klasy realizującej tę funkcjonalność - IsSerializable. Istnieje wiele powodów, dla których twórcy GWT zdecydowali się odejść od wspierania Serializable na rzecz IsSerializable i dociekliwi mogą znaleźć cały elaborat na ten temat w wątku Treat java.io.Serializable as being equivalent to IsSerializable.

Interfejs IsSerializable pełni pierwszoplanową rolę w realizacji funkcjonalności serializacji w GWT RPC. Przesyłanie stanu obiektów z serwera (usługa) do klienta (przeglądarka z uruchomioną aplikacją GWT jako JavaScript) wymaga protokołu wymiany informacji między uczestnikami konwersacji i IsSerializable jest jego częścią jako interfejs-znacznik. Jedynie serializowalne typy mogą uczestniczyć w komunikacji GWT RPC. Do typów serializowalnych należą:

  • typy proste
  • odpowiedniki obiektowe typów prostych wraz z String oraz Date
  • tablice tych typów
  • klasy, które są typu IsSerializable (implementują typ IsSerializable, a skoro interfejs nie ma żadnych metod poza implements nic nie jest wymagane) i wszystkie pola nie będące transient są serializowalne
  • klasy, które posiadają przynajmniej jedną podklasę serializowalną

Plik klasy - Osoba.java - należy umieścić w katalogu src/main/java/pl/jaceklaskowski/gwt/witajswiecie/client.

Realizacja interfejsu usługi - RejestrImpl

Kolejnym krokiem w tworzeniu zdalnej usługi w GWT jest utworzenie realizacji interfejsu usługi - RejestrImpl. Jest to moment skorzystania z funkcjonalności JPA - zarządzania danymi trwałymi przechowywanymi w bazie danych. JPA będzie wykorzystane w trybie zarządzanym przez aplikację, w którym aplikacja bierze na siebie obowiązek zarządzania życiem zarządcy trwałości zastępując serwer aplikacyjny. Jako dostawcę JPA wybrany jest projekt Apache OpenJPA. Wykorzystujemy M2, więc wybór dostawcy sprowadza się do zadeklarowania zależności w pom.xml.

UWAGA: Po zmianie zależności projektu w pom.xml może być koniecznie ponowne utworzenie plików projektu, np. poleceniem mvn eclipse:eclipse, gdyż dodajemy zależność na poziomie M2, co w zależności od IDE może nie implikować automatycznego uaktualnienia projektu.

Klasa realizująca interfejs usługi jest jednocześnie servletem, więc możemy skorzystać ze wszystkich dobrodziejst płynących z tego, m.in. z przestrzeni application oraz metod init i destroy do zarządzania fabryką zarządcy trwałości.

package pl.jaceklaskowski.gwt.rejestrosobowy.server;

import java.util.ArrayList;
import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Query;
import javax.servlet.ServletException;

import net.sf.dozer.util.mapping.DozerBeanMapper;
import pl.jaceklaskowski.gwt.rejestrosobowy.client.Osoba;
import pl.jaceklaskowski.gwt.rejestrosobowy.client.Rejestr;
import pl.jaceklaskowski.gwt.rejestrosobowy.server.entity.OsobaEncja;

import com.google.gwt.user.client.rpc.InvocationException;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;

public class RejestrImpl extends RemoteServiceServlet implements Rejestr {

    EntityManagerFactory emf = null;
    DozerBeanMapper mapper = null;

    public void init() throws ServletException {
        super.init();
        mapper = new DozerBeanMapper();
        emf = (EntityManagerFactory) getServletContext().getAttribute("entityManagerFactory");
    }

    public long getIloscOsob(String nazwisko) {
        EntityManager em = null;
        try {
            em = emf.createEntityManager();
            Query query = em.createNamedQuery("podajIloscOsobPoNazwisku");
            query.setParameter("nazwisko", nazwisko);
            return ((Long) query.getSingleResult()).longValue();
        } catch (Exception e) {
            throw new InvocationException("Wystąpił wyjątek podczas wykonania zdalnej usługi", e);
        } finally {
            if (em != null) {
                em.close();
            }
        }
    }

    public List getOsoby(String nazwisko) {
        EntityManager em = null;
        try {
            em = emf.createEntityManager();
            Query query = em.createNamedQuery("znajdzOsobyPoNazwisku");
            query.setParameter("nazwisko", nazwisko);
            List<OsobaEncja> osobaEncje = (List<OsobaEncja>) query.getResultList();
            // kopiujemy dane z JPA do struktury dla GWT
            List<Osoba> osoby = null;
            if (osobaEncje != null) {
                osoby = new ArrayList<Osoba>();
                for (OsobaEncja encja : osobaEncje) {
                    osoby.add((Osoba) mapper.map(encja, Osoba.class));
                }
            }
            return osoby;
        } catch (Exception e) {
            throw new InvocationException("Wystąpił wyjątek podczas wykonania zdalnej usługi", e);
        } finally {
            if (em != null) {
                em.close();
            }
        }
    }

    public void destroy() {
        super.destroy();
        emf.close();
    }
}

UWAGA: Tymczasowo ignorujemy błędy związane z niedostępnością typów.

W metodzie getOsoby następuje przekopiowanie danych ze struktury serwerowej - klasa OsobaEncja - do struktury klienckiej - klasa Osoba. Jest to spowodowane przede wszystkim ograniczeniami jakie nakłada GWT w związku ze wspieraną wersją Java 1.4.2 i poprzedniej po stronie klienta. Interfejs Serializable nie jest dostępny w środowisku GWT - JRE Emulation Library oraz funkcjonalność adnotacji. Mimo, że zastosujemy deskryptor XML do definicji mapowania encji - orm.xml, aby m.in. obejść to ograniczenie, wydaje się, że panuje ogólne przekonanie o słuszności stosowania osobnych klas między częściami kliencką i serwerową. Do kopiowania danych z jednej klasy do drugiej wykorzystujemy projekt Dozer, który w naszym zastosowaniu nie wymaga żadnej konfiguracji, gdyż typy są identyczne składniowo.

Plik klasy - RejestrImpl.java - umieszczamy w katalogu src/main/java/pl/jaceklaskowski/gwt/rejestrosobowy/server.

Nie jest to konieczny krok, ale aby zapobiec komunikatom ostrzegawczym o niedostępności konfiguracji zapisywania zdarzeń przez Dozer podczas uruchomienia aplikacji, utworzymy plik konfiguracyjny dla Apache log4j - log4j.properties. log4j jest projektem upraszczającym zapisywanie komunikatów (nie mylić z JMS!) przez aplikacje wykorzystywanym przez Dozer.

log4j.rootLogger = debug, stdout
log4j.category.net.sf.dozer = debug, stdout

log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n

Plik - log4j.properties - umieszczamy w katalogu src/main/resources.

Interfejs asynchroniczny usługi zdalnej - RejestrAsync

Tworząc usługę zdalną w GWT RPC koniecznie należy utworzyć interfejs asynchroniczny usługi zdalnej - RejestrAsync.

package pl.jaceklaskowski.gwt.rejestrosobowy.client;

import com.google.gwt.user.client.rpc.AsyncCallback;

public interface RejestrAsync {

    public void getOsoby(String nazwisko, AsyncCallback callback);

    public void getIloscOsob(String nazwisko, AsyncCallback callback);

}

Różnicą między interfejsem asynchronicznym, a odpowiadającym mu interfejsem usługi jest modyfikacja sygnatury metod interfejsu usługi Rejestr tak, aby metody zwracały typ void oraz ostatnim parametrem wejściowym był egzemplarz typu AsyncCallback.

Plik interfejsu - RejestrAsync.java - umieszczamy w katalogu src/main/java/pl/jaceklaskowski/gwt/rejestrosobowy/client.

Utworzenie modułu - RejestrOsobowyGWT

Punkt początkowy modułu - RejestrOsobowyGWT

Głównym elementem każdej aplikacji GWT jest punkt początkowy modułu (ang. entry-point). W naszym module zdefiniujemy pojedyńczy punkt początkowy - RejestrOsobowyGWT. Punkt będzie realizowany przez klasę pl.jaceklaskowski.gwt.rejestrosobowy.client.RejestrOsobowyGWT.

package pl.jaceklaskowski.gwt.rejestrosobowy.client;

import java.util.List;

import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.ClickListener;
import com.google.gwt.user.client.ui.FlexTable;
import com.google.gwt.user.client.ui.HorizontalPanel;
import com.google.gwt.user.client.ui.KeyboardListenerAdapter;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.TextBox;
import com.google.gwt.user.client.ui.Widget;

public class RejestrOsobowyGWT implements EntryPoint {
    public void onModuleLoad() {

        final Button przyciskSzukaj = new Button("Szukaj");
        final Label wyszukanoLabel = new Label();
        final TextBox nazwiskoTextBox = new TextBox();
        final FlexTable daneTable = new FlexTable();

        nazwiskoTextBox.addKeyboardListener(new KeyboardListenerAdapter() {

            public void onKeyUp(Widget sender, char keyCode, int modifiers) {
                // przechwytywanie zdarzenia kasowania znaku bądź podobnych
                wykonajZdalneGetIloscOsob(sender, (char) 0, modifiers);
            }

            public void onKeyPress(Widget sender, char keyCode, int modifiers) {
                wykonajZdalneGetIloscOsob(sender, keyCode, modifiers);
            }

            private void wykonajZdalneGetIloscOsob(Widget sender, char keyCode, int modifiers) {

                // cyfry niedozwolone w nazwisku
                if (Character.isDigit(keyCode)) {
                    nazwiskoTextBox.cancelKey();
                    return;
                }

                // znaki specjalne, np. DELETE (kasowanie znaku) niedołączane, wyłącznie litery
                StringBuffer wyszukiwanyWzorzecNazwiska = new StringBuffer(nazwiskoTextBox.getText());
                if (Character.isLetter(keyCode)) {
                    wyszukiwanyWzorzecNazwiska.insert(nazwiskoTextBox.getCursorPos(), keyCode);
                }
                wyszukiwanyWzorzecNazwiska.append("%");

                RejestrAsync rejestr = Rejestr.App.getInstance();
                rejestr.getIloscOsob(wyszukiwanyWzorzecNazwiska.toString(), new AsyncCallback() {
                    public void onSuccess(Object result) {
                        wyszukanoLabel.setText(((Long) result).toString());
                    }

                    public void onFailure(Throwable caught) {
                        Window.alert(caught.toString());
                        wyszukanoLabel.setText(caught.toString());
                        caught.printStackTrace();
                    }
                });
                wyszukanoLabel.setText("Pobieram dane...");
            }
        });

        nazwiskoTextBox.addKeyboardListener(new KeyboardListenerAdapter() {
            public void onKeyPress(Widget sender, char keyCode, int modifiers) {
                if (keyCode == '\r') {
                    przyciskSzukaj.click();
                }
            }
        });
        przyciskSzukaj.addClickListener(new ClickListener() {
            public void onClick(Widget sender) {
                RejestrAsync remoteService = Rejestr.App.getInstance();
                remoteService.getOsoby(nazwiskoTextBox.getText() + "%", new AsyncCallback() {
                    public void onSuccess(Object result) {
                        List osobyLista = (List) result;
                        if (osobyLista == null || osobyLista.size() == 0) {
                            daneTable.setVisible(false);
                            return;
                        }

                        // usuń wiersze poza nagłówkiem
                        int iloscWierszy = daneTable.getRowCount();
                        if (iloscWierszy > 1) {
                            do {
                                daneTable.removeRow(iloscWierszy - 1); // wiersze liczone są od 0
                                iloscWierszy--;
                            } while (iloscWierszy != 1);
                        }

                        int rozmiarDanych = osobyLista.size();
                        for (int i = 0; i < rozmiarDanych; i++) {
                            Osoba osoba = (Osoba) osobyLista.get(i);
                            // wypełnij tabelę pamiętając, że pierwszy (zerowy) wiersz jest dla nagłówka
                            daneTable.setText(i + 1, 0, osoba.getIdentyfikator());
                            daneTable.setText(i + 1, 1, osoba.getImie());
                            daneTable.setText(i + 1, 2, osoba.getNazwisko());
                        }
                        daneTable.setVisible(true);
                    }

                    public void onFailure(Throwable caught) {
                        Window.alert(caught.toString());
                        caught.printStackTrace();
                    }
                });
            }
        });

        // początkowo ukryj, ponieważ nie ma danych do wyświetlenia
        daneTable.setBorderWidth(1);
        daneTable.setText(0, 0, "Identyfikator");
        daneTable.setText(0, 1, "Imię");
        daneTable.setText(0, 2, "Nazwisko");
        daneTable.setVisible(false);

        HorizontalPanel przestrzenWyszukiwania = new HorizontalPanel();
        przestrzenWyszukiwania.add(new Label("Nazwisko: "));
        przestrzenWyszukiwania.add(nazwiskoTextBox);
        przestrzenWyszukiwania.add(przyciskSzukaj);
        RootPanel.get("przestrzenWyszukiwania").add(przestrzenWyszukiwania);

        HorizontalPanel poleWyszukano = new HorizontalPanel();
        poleWyszukano.add(new Label("Osób możliwych do wyszukania: "));
        poleWyszukano.add(wyszukanoLabel);
        RootPanel.get("poleWyszukano").add(poleWyszukano);

        RootPanel.get("dane").add(daneTable);
    }
}

Plik klasy punktu początkowego - RejestrOsobowyGWT.java - umieszczamy w katalogu src/main/java/pl/jaceklaskowski/gwt/rejestrosobowy/client.

Część publiczna modułu - RejestrOsobowyGWT.html oraz RejestrOsobowyGWT.css

Jak każda aplikacja internetowa tak i nasza posiada stronę początkową - RejestrOsobowyGWT.html, która uruchamia całą infrastrukturę GWT.

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
  <head>
    <title>Rejestr osobowy GWT</title>
    <meta http-equiv="content-type" content="text/html; charset=UTF-8">
    
    <link rel=stylesheet href="RejestrOsobowyGWT.css" />
    
    <!--                                   -->
    <!--  Łącznik między stroną HTML a GWT -->
    <!--                                   -->
    <meta name='gwt:module' content='pl.jaceklaskowski.gwt.rejestrosobowy.RejestrOsobowyGWT'>
  </head>
  <body>
    <script language="javascript" src="gwt.js"></script>
    
    <iframe id="__gwt_historyFrame" style="width: 0; height: 0; border: 0"></iframe>
    
    <h1>Rejestr osobowy GWT</h1>
    <div id="rejestr">
      <span id="przestrzenWyszukiwania"></span>
    </div>
    <div>
      <span id="poleWyszukano"></span>
    </div>
    <div id="przestrzenInformacyjna">
      <span id="dane"></span>
    </div>    
  </body>
</html>

Plik strony początkowej modułu - RejestrOsobowyGWT.html - umieszczamy w katalogu src/main/webapp/pl.jaceklaskowski.gwt.rejestrosobowy.RejestrOsobowyGWT

Konwencją w tworzeniu aplikacji GWT jest umieszczanie pliku styli obok pliku strony początkowej. Oczywiście plik styli nie jest niczym niezwykłym w sensie tworzenia aplikacji internetowych z JavaScript, HTML i CSS, więc jego położenie może być dowolne. W szczególności plik może nie istnieć w ogóle.

body {
    background-color: white;
    color: black;
    font-family: Arial, sans-serif;
    font-size: small;
    margin: 8px;
}

Plik styli modułu - RejestrOsobowyGWT.css - umieszczamy w katalogu src/main/webapp/pl.jaceklaskowski.gwt.rejestrosobowy.RejestrOsobowyGWT

Konfiguracja modułu - RejestrOsobowyGWT.gwt.xml

Plik RejestrOsobowyGWT.gwt.xml jest konfiguracją modułu GWT, który poza definicją punktu początkowego aplikacji GWT, określa również jakie usługi zdalne są udostępniane.

<module>

  <inherits name='com.google.gwt.user.User' />

  <entry-point class='pl.jaceklaskowski.gwt.rejestrosobowy.client.RejestrOsobowyGWT' />

  <servlet path="/services/rejestr" class="pl.jaceklaskowski.gwt.rejestrosobowy.server.RejestrImpl" />

</module>

Jedyną usługą w module jest rejestr dostępną pod adresem /services/rejestr (atrybut path), która realizowana jest przez klasę pl.jaceklaskowski.gwt.rejestrosobowy.server.RejestrImpl (atrybut class).

Plik konfiguracji modułu - RejestrOsobowyGWT.gwt.xml - umieszczamy w katalogu src/main/resources/pl/jaceklaskowski/gwt/rejestrosobowy.

Warstwa zarządzania danymi oparta o Java Persistence

Klasa encji Osoba - OsobaEncja

Rozdzielając warstwę GWT (przeglądarka) od warstwy serwerowej aplikacji utworzymy klasę OsobaEncja, która będzie obiektem trwałym - encją - zarządzaną przez Java Persistence (JPA).

package pl.jaceklaskowski.gwt.rejestrosobowy.server.entity;

import java.io.Serializable;

public class OsobaEncja implements Serializable {
    private String identyfikator;
    private String imie;
    private String nazwisko;

    // zabronione tworzenie osob bez imienia i nazwiska
    // JPA poradzi sobie z private
    private OsobaEncja() {
    }

    public OsobaEncja(String imie, String nazwisko) {
        super();
        this.imie = imie;
        this.nazwisko = nazwisko;
    }

    public String getIdentyfikator() {
        return identyfikator;
    }

    public void setIdentyfikator(String identyfikator) {
        this.identyfikator = identyfikator;
    }

    public String getImie() {
        return imie;
    }

    public void setImie(String imie) {
        this.imie = imie;
    }

    public String getNazwisko() {
        return nazwisko;
    }

    public void setNazwisko(String nazwisko) {
        this.nazwisko = nazwisko;
    }

    public String toString() {
        StringBuffer sb = new StringBuffer();
        sb.append(this.identyfikator);
        sb.append(" - ");
        sb.append(this.imie);
        sb.append(" ");
        sb.append(this.nazwisko);
        return sb.toString();
    }
}

Jest to klasa identyczna składniowo z klasą wykorzystywaną po stronie przeglądarki w GWT - Osoba. W ten sposób oddzielamy warstwy GWT i JPA umożliwiając ich dalszy samodzielny rozwój bez konieczności modyfikacji obu warstw, jeśli będzie konieczne zmodyfikowanie którejkolwiek z klas. Kopiowaniem danych z warstwy JPA do GWT zajmuje się projekt Dozer.

Plik klasy - OsobaEncja.java - należy umieścić w katalogu src/main/java/pl/jaceklaskowski/gwt/rejestrosobowy/server/entity.

Konfiguracja encji - orm.xml

Chcąc pozostawić możliwość skorzystania z pojedyńczego obiektu, który byłby przesyłany z serwera do przeglądarki i z powrotem tak, aby zapobiec sytuacji korzystania z identycznego obiektu składniowo w GWT i JPA, skorzystamy z możliwości definicji encji przy pomocy deskryptora XML - orm.xml.

<?xml version="1.0" encoding="UTF-8" ?>
<entity-mappings xmlns="http://java.sun.com/xml/ns/persistence/orm"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm http://java.sun.com/xml/ns/persistence/orm_1_0.xsd"
  version="1.0">
  <description>Mapowanie JPA dla aplikacji Rejestr</description>
  <package>pl.jaceklaskowski.gwt.rejestrosobowy.server.entity</package>
  <entity class="OsobaEncja" name="Osoba" metadata-complete="false">
    <description>Encja Osoba</description>
    <named-query name="znajdzOsobyPoNazwisku">
      <query>SELECT o FROM Osoba o WHERE o.nazwisko LIKE :nazwisko</query>
    </named-query>
    <named-query name="podajIloscOsobPoNazwisku">
      <query>SELECT COUNT(o) FROM Osoba o WHERE o.nazwisko LIKE :nazwisko</query>
    </named-query>
    <attributes>
      <id name="identyfikator">
        <generated-value />
      </id>
      <basic name="imie" />
      <basic name="nazwisko" />
    </attributes>
  </entity>
</entity-mappings>

Plik orm.xml jest alternatywnym sposobem definiowania encji w JPA poza mechanizmem adnotacji. Poza uciążliwością utrzymywania dwóch plików - klasy encji oraz orm.xml - wszystkie możliwości adnotacji są dostępne w orm.xml. Korzystając z orm.xml mamy możliwość zaniechania korzystania z adnotacji, które nie są rozumiane przez GWT i które powodowałyby komplikację, gdybyśmy chcieli jednak skorzystać z tej samej klasy w obu warstwach GWT i JPA.

Deskryptor XML definiuje dwa nazwane zapytania JPA - elementy named-query - oraz atrybuty encji. Wszystkie aspekty wydajnościowe pozostawiamy dostawcy JPA i stąd polegamy na ustawieniach domyślnych (jedna z głównym zalet nowej specyfikacji EJB 3.0 - przede wszystkim wartości domyślne nazwana w JPA konfiguracją przez nadpisywanie lub konfiguracją w wyjątkowych sytuacjach - ang. configuration by exception).

Plik - orm.xml - umieszczamy w katalogu src/main/resources/META-INF.

Konfiguracja dostawcy JPA - persistence.xml

Wybór dostawcy JPA jest już bez znaczenia na samą architekturę aplikacji, podobnie jak w przypadku JDBC przy wyborze sterownika bazodanowego. Dostawca JPA jest właśnie sterownikiem JPA i jako taki może być zamieniany bez konieczności reorganizacji aplikacji.

Mimo tego konieczny jest wybór dostawcy JPA. Tym razem stawiamy na Apache OpenJPA. Deklaracja dostawcy JPA odbywa się poprzez plik persistence.xml, który konfiguruje usługę.

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd">
  <persistence-unit name="rejestr" transaction-type="RESOURCE_LOCAL">
    <provider>org.apache.openjpa.persistence.PersistenceProviderImpl</provider>
    <properties>
      <property name="openjpa.ConnectionDriverName" value="org.apache.derby.jdbc.EmbeddedDriver" />
      <property name="openjpa.ConnectionURL" value="jdbc:derby:target/derbyDB;create=true" />
      <property name="openjpa.ConnectionUserName" value="app" />
      <property name="openjpa.ConnectionPassword" value="app" />
      <property name="openjpa.jdbc.DBDictionary" value="derby" />
      <property name="openjpa.jdbc.SynchronizeMappings" value="buildSchema(SchemaAction='add,deleteTableContents')" />
      <property name="openjpa.Log" value="DefaultLevel=TRACE,SQL=TRACE" />
      <property name="openjpa.QueryCompilationCache" value="false" />
    </properties>
  </persistence-unit>
</persistence>

W pliku definiujemy obowiązkową nazwę dla jednostki trwałej (atrybut name elementu persistence-unit), za pomocą której dostaniemy się do trwałych danych.

Plik - persistence.xml - umieszczamy w katalogu src/main/resources/META-INF.

Aplikacja internetowa - gwt-rejestrosobowy

Konfiguracja aplikacji internetowej - web.xml

Konfiguracja aplikacji internetowej jest realizowana za pomocą deskryptora web.xml. Jest on wymagany w naszym projekcie wyłącznie dla celów demonstracyjnych (utworzeniem pliku web.xml z usługą zdalną, która jest de facto servletem zajmuje się również wtyczka maven-googlewebtoolkit2-plugin), aby istniały dane osób do wyświetlenia w aplikacji. W pliku rejestrujemy klasę nasłuchującą - WypelnijBazeDanych - za pomocą elementu listener, która zasili danymi bazę danych.

<?xml version="1.0" encoding="UTF-8"?>

<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://java.sun.com/xml/ns/javaee web-app_2_5.xsd" version="2.5">
  <display-name>Rejestr Osobowy GWT</display-name>
  <listener>
    <listener-class>pl.jaceklaskowski.gwt.rejestrosobowy.server.servlet.WypelnijBazeDanych</listener-class>
  </listener>
</web-app>

Plik - web.xml - umieszczamy w katalogu src/main/webapp/WEB-INF.

Klasa nasłuchująca zdarzeń uruchomienia aplikacji - WypelnijBazeDanych

Na potrzeby aplikacji demonstrującej integrację GWT z JPA stworzymy klasę nasłuchującą zdarzeń uruchomienia aplikacji internetowej. Przy jej pomocy utworzymy przykładowe dane dla demonstracji poprawnego działania aplikacji.

package pl.jaceklaskowski.gwt.rejestrosobowy.server.servlet;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;

import pl.jaceklaskowski.gwt.rejestrosobowy.server.entity.OsobaEncja;

public class WypelnijBazeDanych implements ServletContextListener {

    EntityManagerFactory emf = null;

    public void contextDestroyed(ServletContextEvent evt) {
        emf.close();
    }

    public void contextInitialized(ServletContextEvent evt) {
        emf = Persistence.createEntityManagerFactory("rejestr");
        evt.getServletContext().setAttribute("entityManagerFactory", emf);

        EntityManager em = emf.createEntityManager();
        em.getTransaction().begin();
        OsobaEncja agata = new OsobaEncja("Agata", "Laskowska");
        OsobaEncja jacek = new OsobaEncja("Jacek", "Laskowski");
        OsobaEncja iweta = new OsobaEncja("Iweta", "Laskowska");
        OsobaEncja patryk = new OsobaEncja("Patryk", "Laskowski");

        em.persist(agata);
        em.persist(jacek);
        em.persist(iweta);
        em.persist(patryk);

        em.getTransaction().commit();
        em.close();
    }

}

UWAGA: Tymczasowo ignorujemy błędy związane z niedostępnością wymaganych typów.

Plik - WypelnijBazeDanych.java - umieszczamy w katalogu src/main/java/pl/jaceklaskowski/gwt/rejestrosobowy/server/servlet.

Konfiguracja projektu - pom.xml

Wszystkie zależności projektu są umieszczane w konfiguracji projektu - pom.xml. Jest to centralne miejsce konfiguracji projektu zarządzanego przez Apache Maven 2. Dotychczasowe błędy związane z brakiem typów były związane z brakującymi wpisami w pom.xml. Nadeszła pora nadrabić straty.

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>pl.jaceklaskowski.gwt.rejestrosobowy</groupId>
  <artifactId>gwt-rejestrosobowy</artifactId>
  <packaging>war</packaging>
  <version>1.0-SNAPSHOT</version>
  <name>Rejestr Osobowy GWT</name>
  <url>http://www.JacekLaskowski.pl</url>
  <repositories>
    <repository>
      <id>gwt-maven</id>
      <url>http://gwt-maven.googlecode.com/svn/trunk/mavenrepo</url>
    </repository>
    <repository>
      <id>apache-snapshots</id>
      <name>Apache Snapshots Repository</name>
      <url>http://people.apache.org/maven-snapshot-repository</url>
      <layout>default</layout>
      <snapshots>
        <enabled>true</enabled>
        <updatePolicy>daily</updatePolicy>
        <checksumPolicy>ignore</checksumPolicy>
      </snapshots>
      <releases>
        <enabled>false</enabled>
      </releases>
    </repository>
  </repositories>
  <pluginRepositories>
    <pluginRepository>
      <id>gwt-maven</id>
      <url>http://gwt-maven.googlecode.com/svn/trunk/mavenrepo</url>
    </pluginRepository>
  </pluginRepositories>
  <build>
    <finalName>${pom.artifactId}</finalName>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-antrun-plugin</artifactId>
        <executions>
          <execution>
            <id>openjpa.PCEnhancer</id>
            <phase>process-classes</phase>
            <configuration>
              <tasks>
                <java classname="org.apache.openjpa.enhance.PCEnhancer" classpathref="maven.runtime.classpath"
                  dir="target/classes" fork="true" failonerror="true" />
              </tasks>
            </configuration>
            <goals>
              <goal>run</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>2.0</version>
        <configuration>

Niepoprawny język.

Musisz wybrać język w następujący sposób: <source lang="html4strict">...</source>

Języki obsługiwane w podświetlaniu składni:

abap, actionscript, actionscript3, ada, apache, applescript, apt_sources, asm, asp, autoit, avisynth, bash, basic4gl, bf, bibtex, blitzbasic, bnf, boo, c, c_mac, caddcl, cadlisp, cfdg, cfm, cil, cmake, cobol, cpp, cpp-qt, csharp, css, d, dcs, delphi, diff, div, dos, dot, eiffel, email, erlang, fo, fortran, freebasic, genero, gettext, glsl, gml, gnuplot, groovy, haskell, hq9plus, html4strict, idl, ini, inno, intercal, io, java, java5, javascript, kixtart, klonec, klonecpp, latex, lisp, locobasic, lolcode, lotusformulas, lotusscript, lscript, lsl2, lua, m68k, make, matlab, mirc, modula3, mpasm, mxml, mysql, nsis, oberon2, objc, ocaml, ocaml-brief, oobas, oracle11, oracle8, pascal, per, perl, php, php-brief, pic16, pixelbender, plsql, povray, powershell, progress, prolog, properties, providex, python, qbasic, rails, rebol, reg, robots, ruby, sas, scala, scheme, scilab, sdlbasic, smalltalk, smarty, sql, tcl, teraterm, text, thinbasic, tsql, typoscript, vb, vbnet, verilog, vhdl, vim, visualfoxpro, visualprolog, whitespace, whois, winbatch, xml, xorg_conf, xpp, z80

          <target>1.5</target>
        </configuration>
      </plugin>
      <plugin>
        <groupId>com.totsp.gwt</groupId>
        <artifactId>maven-googlewebtoolkit2-plugin</artifactId>
        <version>1.5.3-SNAPSHOT</version>
        <configuration>
          <runTarget>pl.jaceklaskowski.gwt.rejestrosobowy.RejestrOsobowyGWT/RejestrOsobowyGWT.html</runTarget>
          <compileTarget>
            <param>pl.jaceklaskowski.gwt.rejestrosobowy.RejestrOsobowyGWT</param>
          </compileTarget>
          <gwtHome>c:/apps/gwt</gwtHome>
        </configuration>
        <executions>
          <execution>
            <goals>
              <goal>mergewebxml</goal>
              <goal>compile</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-war-plugin</artifactId>
        <version>2.0.2</version>
        <configuration>
          <webXml>target/web.xml</webXml>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.mortbay.jetty</groupId>
        <artifactId>maven-jetty-plugin</artifactId>
      </plugin>
    </plugins>
  </build>
  <dependencies>
    <dependency>
      <groupId>com.google.gwt</groupId>
      <artifactId>gwt-user</artifactId>
      <version>1.3.3</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>com.google.gwt</groupId>
      <artifactId>gwt-servlet</artifactId>
      <version>1.3.3</version>
    </dependency>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.1</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.apache.openjpa</groupId>
      <artifactId>openjpa-all</artifactId>
      <version>0.9.8-incubating-SNAPSHOT</version>
    </dependency>
    <dependency>
      <groupId>net.sf.dozer</groupId>
      <artifactId>dozer</artifactId>
      <version>3.3.1</version>
    </dependency>
    <dependency>
      <groupId>org.apache.derby</groupId>
      <artifactId>derby</artifactId>
      <version>10.2.2.0</version>
    </dependency>
    <dependency>
      <groupId>log4j</groupId>
      <artifactId>log4j</artifactId>
      <version>1.2.14</version>
    </dependency>
  </dependencies>
</project>

Po zmianie zależności ponownie generujemy pliki projektu dla wybranego środowiska programistycznego IDE - mvn eclipse:eclipse.

Budowanie i uruchomienie aplikacji

Projekt zarządzany jest przez M2, więc wszystkie polecenia wydawane w projekcie są wykonywane z poziomu M2. Nie inaczej będzie i w przypadku uruchomienia aplikacji. Korzystanie z M2 zawsze przynosi dużo uproszczeń w zestawianiu kompletnego środowiska programistycznego. Może zdumiewać prostota uruchomienia aplikacji na serwerze aplikacji - Apache Tomcat bądź Jetty - bez konieczności wstępnej ich konfiguracji. Dla dalszego uproszczenia procesu uruchomienia aplikacji wybieramy serwer Jetty, gdyż wtyczka dla M2 - maven-jetty-plugin - wydaje się być bardziej zaawansowana w funkcjonalności zarządzania aplikacjami na serwerze. Wybór serwera produkcyjnego pozostawiamy na inny czas, polegając tym razem na jedynym, istotnym kryterium - łatwości konfiguracji i uruchomienia.

Zanim przejdziemy do uruchomienia (semi)produkcyjnego na Jetty sprawdźmy uruchomienie aplikacji w dedykowanym środowisku uruchomieniowym GWT w trybie bez kompilacji do JavaScript (ang. GWT hosted mode). Wydajemy polecenie mvn clean gwt:gwt.

$ mvn clean gwt:gwt
[INFO] Scanning for projects...
[INFO] Searching repository for plugin with prefix: 'gwt'.
[INFO] ----------------------------------------------------------------------------
[INFO] Building Rejestr Osobowy GWT
[INFO]    task-segment: [clean, gwt:gwt]
[INFO] ----------------------------------------------------------------------------
...
[INFO] Running GWT with command: c:\apps\java5\jre\bin\java ...com.google.gwt.dev.GWTShell...

W tym momencie uruchomi się środowisko GWT i można przetestować funkcjonalność aplikacji.

Grafika:gwt-rejestrosobowy-hostedmode.png

Zamykamy środowisko uruchomieniowe GWT i przechodzimy do wykonania kolejnego etapu w życiu naszej aplikacji - uruchomienia aplikacji w ramach serwera Jetty, co sprowadza się do pojedyńczego polecenia mvn clean jetty:run-exploded.

$ mvn clean jetty:run-exploded
[INFO] Scanning for projects...
[INFO] Searching repository for plugin with prefix: 'jetty'.
[INFO] ----------------------------------------------------------------------------
[INFO] Building Rejestr Osobowy GWT
[INFO]    task-segment: [clean, jetty:run-exploded]
[INFO] ----------------------------------------------------------------------------
...
[INFO] [jetty:run-exploded]
[INFO] Configuring Jetty for project: Rejestr Osobowy GWT
2007-05-13 23:51:08.906::INFO:  Logging to STDERR via org.mortbay.log.StdErrLog
[INFO] Context path = /gwt-rejestrosobowy
[INFO] Tmp directory = c:\gwt-rejestrosobowy\target\work
[INFO] Web defaults =  jetty default
[INFO] Starting jetty 6.1.0pre0 ...
2007-05-13 23:51:08.968::INFO:  jetty-6.1.0pre0
...
2007-05-13 23:51:14.000::INFO:  Started SelectChannelConnector @ 0.0.0.0:8080
[INFO] Started Jetty Server

W tym momencie mamy możliwość uruchomienia aplikacji w przeglądarce otwierając stronę pod adresem http://localhost:8080/gwt-rejestrosobowy/pl.jaceklaskowski.gwt.rejestrosobowy.RejestrOsobowyGWT/RejestrOsobowyGWT.html.

Grafika:gwt-rejestrosobowy.PNG

Zatrzymanie serwera i zamknięcie aplikacji to wciśnięcie kombinacji klawiszy Ctrl-C na konsoli, gdzie uruchomiono polecenie mvn clean jetty:run-exploded.

Przygotowanie wersji dystrybucyjnej aplikacji to wydanie polecenia mvn clean package, które utworzy plik gwt-rejestrosobowy.war w katalogu target projektu.

$ mvn clean package
[INFO] Scanning for projects...
[INFO] ----------------------------------------------------------------------------
[INFO] Building Rejestr Osobowy GWT
[INFO]    task-segment: [clean, package]
[INFO] ----------------------------------------------------------------------------
...
[INFO] Building war: c:\gwt-rejestrosobowy\target\gwt-rejestrosobowy.war
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 17 seconds
[INFO] Finished at: Sun May 13 23:57:48 CEST 2007
[INFO] Final Memory: 13M/254M
[INFO] ------------------------------------------------------------------------
Osobiste