August 8, 2009 - zenoconsulting
Introduction
It has been a good while since I wrote any tech blogs — I've been busy with my new son, Davis Jr. :). That said, I wanted to post something interesting here related to the Google Web Toolkit (GWT). Recently, I've built an app from scratch with GWT. Having never used it before, it takes some time to get used to. The programming model is very simple — that isn't what you need to wrap your head around. The challenge is more in how to construct a decent project structure so it will build seamlessly for other colleagues, and continuous integration, and integrate smoothly with other frameworks. Other challenges I had were unique to this project. One of the requirements is that we have multiple user roles for the system, and each user will have a custom login page, and custom behavior.
This is kind of tricky to do in GWT. You can read this whole thread and get an idea. I didn't really like any of the suggestions in that thread, so I took another path.
With GWT, you typically have a single entry point to your application (like a main() method). The entry point bootstraps all the JavaScript for you in the client browser. So, if you need custom entry points to your application, you have a few options. You could build custom GWT widgets and include them in your project. This is essentially how you pull in 3rd party GWT libraries, but this doesn't really solve my problem. My problem is that when the application is bootstrapped — that is, when the client browser loads a static HTML file that pulls in the JavaScript, I want the application to look and act differently based principally on the URL.
You can have multiple entry points, but there are very few examples out there. I could solve this by making a multi-module maven project, and while I'm very adept in Maven, and have set these up many times before, I realized that this is just added complexity for what I wanted. Instead, I figured out a way to have it all in the same project, which simplifies life greatly, and provides me with any number of unique entry points to my application driven by URL.
GWT by default sets up ugly URL paths like
http://hostname/context/com.mycompany.myapplication.Application/Application.html
There are lots of ways to rewrite URLs, but I found a way using UrlRewriteFilter that makes it really nice — and you can even make fake REST urls.
About The App
There is absolutely nothing fancy about the actual application(s) themselves, but this is meant more as a good starting skeleton for someone that wants to get rolling with GWT and not have to deal with a lot of the initial setup headache.
There are basically two independent GWT applications here. One of them is called User, and one of them is called Admin. Let's assume you want a separate Admin application that has unique characteristics like logging in as an admin and doing stuff. You could just add a panel on a single GWT application and try to figure out whether you should show the panel or not based on somehow identifying who a user is. You can read this whole thread and get an idea how much of a pain this kind of thing can be with GWT.
Fortunately, there is a better way. You make two GWT entry points — one for the User app and one for the Admin app. They can share GWT components you custom build, but you can also make them have distinct, unique look-and-feel and content. Better yet, you can restrict access to them based on URL filtering — and host filtering, etc. For example, you could set up a filter to only allow the /admin url to be accessed by localhost.
Setup The Project
You can download the complete, working sample project here. The following commands will get you started:
Build Eclipse Project Files
$ mvn eclipse:eclipse
Now, just do Eclipse -> File -> Import -> Existing Project Into Workspace, etc., etc.
Run The User App In Hosted Mode
$ mvn gwt:run -DrunTarget=com.example.User/User.html
Run The Admin App In Hosted Mode
$ mvn gwt:run -DrunTarget=com.example.Admin/Admin.html
Project Structure
Typical mvn webapp projects follow a standard structure that looks like this:
+- pom.xml
+- my-app
| +- pom.xml
| +- src
| +- main
| +- java
+- my-webapp
| +- pom.xml
| +- src
| +- main
| +- webapp
| +- WEB-INF
| +- web.xml
We're going to tweak this slightly to deal with GWT-isms. We'll use the maven-gwt-plugin. If you searched Google for a maven GWT plugin, you might get confused because there are three (?) of them:
- maven-xgwt-plugin — yet another plugin; haven't tried it
- gwt-maven — this one has had its codebase merged into the Codehaus plugin and is only supporting bug fixes - no new features
- codehaus maven-gwt-plugin the codehaus plugin
I'm going to save you some time. Use the codehaus plugin. It is poorly documented, but it works well once you get the hang of it, and it is maintained.
So, instead of using the typical {src/main/webapp} folder for the webapp, we create a root-level directory called war and run in-place. The directory structure looks like this:
my-app
|-- pom.xml
`-- src
| |-- main
| | |-- java
| | | `-- com
| | | `-- example
| | | `-- client
| | | `-- Admin.java
| | | `-- User.java
| | | `-- server/
| | `-- resources
| | `-- com
| | |-- example
| | | ` Admin.gwt.xml
| | | ` Client.gwt.xml
| | |-- public
| | | `-- Admin.html
| | | `-- Client.html
| | |-- css/
| | |-- images/
| `-- test
| |-- java
| | `-- com
| | `-- example/
`-- war
|
`-- WEB-INF
`-- web.xml
Under the package namespace com.example we have a client/server delineation. All GWT code that is compile-able from Java into JavaScript has to be under client. This is a GWT-ism. The server package can be named whatever you want, but I name it that to indicate the difference. You can put any Java code you want under server or outside the client package…but you are limited to a subset of J2SE under client.
Admin.java and User.java both implement EntryPoint. This is also why we need both Admin.gwt.xml and User.gwt.xml, as well as Admin.html and User.html.
Now we have to configure the plugins.
The pom.xml file
Here's most of it. The pom.xml in the full project has some other fun stuff in it like using the cargo plugin to deploy to local JBoss.
<?xml version="1.0" encoding="UTF-8"?> <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>com.example</groupId> <artifactId>gwt-prototype</artifactId> <packaging>war</packaging> <version>1.0-SNAPSHOT</version> <name>GWT Maven Prototype</name> <properties> <!-- versions of frameworks we depend on --> <gwt.version>1.7.0</gwt.version> <spring.version>2.5.6</spring.version> <!-- java version we use --> <maven.compiler.source>1.6</maven.compiler.source> <maven.compiler.target>1.6</maven.compiler.target> <!-- define a transient output directory for gwt --> <gwt.output.directory>${basedir}/war</gwt.output.directory> <!-- this will end up being the war name --> <final.build.name>gwt-prototype</final.build.name> </properties> <repositories> <repository> <id>jboss.org</id> <name>JBoss.org maven2 repo</name> <url>http://repository.jboss.org/maven2</url> </repository> </repositories> <pluginRepositories> <pluginRepository> <id>codehaus</id> <name>Codehaus Release Repo</name> <url>http://repository.codehaus.org</url> </pluginRepository> <pluginRepository> <id>codehaus-snapshot</id> <name>Codehaus Snapshot Repo</name> <url>http://snapshots.repository.codehaus.org</url> </pluginRepository> </pluginRepositories> <dependencies> <dependency> <!-- GWT servlet --> <groupId>com.google.gwt</groupId> <artifactId>gwt-servlet</artifactId> <version>${gwt.version}</version> <scope>runtime</scope> </dependency> <dependency> <!-- main GWT classes --> <groupId>com.google.gwt</groupId> <artifactId>gwt-user</artifactId> <version>${gwt.version}</version> <scope>provided</scope> </dependency> <dependency> <!-- for logging --> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.13</version> <type>jar</type> </dependency> <dependency> <!-- http://urlrewritefilter.googlecode.com --> <groupId>org.tuckey</groupId> <artifactId>urlrewrite</artifactId> <version>3.2.0</version> <scope>system</scope> <systemPath>${basedir}/lib/urlrewrite-3.2.0.jar</systemPath> </dependency> <!-- TEST SCOPE DEPENDENCIES --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.4</version> <scope>test</scope> </dependency> </dependencies> <build> <finalName>${final.build.name}</finalName> <!-- compile into war transient directory for hosted mode live editing --> <outputDirectory>${gwt.output.directory}/WEB-INF/classes</outputDirectory> <plugins> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>gwt-maven-plugin</artifactId> <version>1.1</version> <configuration> <output>${basedir}/war</output> <webXml>${basedir}/war/WEB-INF/web.xml</webXml> <hostedWebapp>${basedir}/war</hostedWebapp> </configuration> <executions> <execution> <phase>process-classes</phase> <goals> <goal>compile</goal> <goal>eclipse</goal> <goal>eclipseTest</goal> </goals> </execution> <execution> <id>integration-test-gwt</id> <phase>integration-test</phase> <goals> <goal>test</goal> </goals> </execution> </executions> </plugin> <plugin> <artifactId>maven-surefire-plugin</artifactId> <version>2.4.3</version> <configuration> <excludes> <exclude>**/GwtTest*.java</exclude> <exclude>**/*IntegrationTest.java</exclude> </excludes> <argLine>-Xmx256m</argLine> </configuration> <executions> <execution> <id>integration-test</id> <goals> <goal>test</goal> </goals> <phase>integration-test</phase> <configuration> <excludes> <exclude>none</exclude> </excludes> <includes> <include>**/*IntegrationTest*.java</include> </includes> </configuration> </execution> </executions> </plugin> <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> <encoding>UTF-8</encoding> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-resources-plugin</artifactId> <version>2.3</version> <!-- set encoding to something not platform dependent --> <configuration> <encoding>UTF-8</encoding> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-war-plugin</artifactId> <configuration> <warSourceDirectory>${basedir}/war</warSourceDirectory> <warSourceExcludes>.gwt-tmp/**,WEB-INF/lib/gwt-user*,.svn</warSourceExcludes> <webXml>${basedir}/war/WEB-INF/web.xml</webXml> </configuration> </plugin> </plugins> </build> </project>
The urlrewrite dependency wasn't available in any public repo I found at version 3.2.0, so I include it in a /lib dir. You should deploy it to your local maven repo. I've configured maven-gwt-plugin here to compile its output to the /war directory, and I've also told the maven-war-plugin that this is the warSourceDirectory. This has the nice benefit of building everything in-place. It has the annoying side-effect of copying temporary files into version-controlled directories. This is easily solved. If you are using subversion, for example, check in war/WEB-INF/web.xml and set the svn:ignore property on the generated directories like war/WEB-INF/classes.
Testing
I've also set this up to enable unit and integration testing as separate maven phases. The good news is that you can unit test the GWT user interface you build with simple JUnit. The bad news is that it is really slow and this is annoying, so let's make it run in maven's integration-test phase only. We'll do the same for any other integration tests we run (e.g. if you add tests that hit a database). If you create tests and add them, and follow this naming standard, maven is already configured for you:
Test Class Name | Maven Execution Phase | Notes |
---|---|---|
MyClassTest.java | test | extends TestCase or use JUnit 4 annotations |
GwtTestMyClass.java | integration-test | extends AbstractGwtTest (see below) |
MyClassIntegrationTest.java | integration-test |
In order to write a unit test for GWT, you can create one simple Abstract class like this:
package com.example; import com.google.gwt.junit.client.GWTTestCase; /** * Abstract test class that all other GWT test cases should inherit * from. */ public abstract class AbstractGwtTest extends GWTTestCase { /** * (non-Javadoc) * @see com.google.gwt.junit.client.GWTTestCase#getModuleName() */ @Override public String getModuleName() { return "com.example.User"; } }
Now, if you write some class that extends GWT Composite or some GWT Widget like VerticalPanel, you can create a unit test for it. Just extends AbstractGwtTest and name it GwtTestMyClass.java, and maven will run it under
$ mvn integration-test
…and if you want to run just standard unit tests, it is the same ol'
$ mvn test
GWT Design
So, now that we have these different entry points, I know they will share a lot of the same widgets that I will create, and I don't want to duplicate the Java code. The recommended approach is to design GWT classes that extend Composite. Design them so they are re-usable. In the entry points, you decide what to load. For example, here is the code for Admin.java
package com.example.client; import com.example.client.view.AdminMiddlePanel; import com.example.client.view.FooterPanel; import com.example.client.view.HeaderPanel; import com.google.gwt.core.client.EntryPoint; import com.google.gwt.user.client.ui.RootPanel; /** * Entry point classes define <code>onModuleLoad()</code>. */ public class Admin implements EntryPoint { public void onModuleLoad() { RootPanel.get("header").add(new HeaderPanel()); RootPanel.get("content").add(new AdminMiddlePanel()); RootPanel.get("footer").add(new FooterPanel()); } }
I created two boring widgets called HeaderPanel and FooterPanel and I stuff them in the Admin.html DIV ids "header" and "footer". I do the same with the User.java class. However, I want the middle DIV id (called "content") to be different. So, I have a custom AdminMiddlePanel that I put here. For the User.java class, I insert a different middle panel.
URL filtering
So, if you run the app in hosted mode, you'll notice that it brings up the Host shell at a URL like
http://localhost:8080/com.example.User/User.html
Let's get rid of that. Furthermore, what if I have unique requirements where I want to fake a RESTful style URL, and use that path to drive some underlying logic. For example, what if I want a URL like:
http://localhost:8080/user/abc
If you go to this URL, you know that this is the "abc" user. It's a simple, contrived example, but you can change it to fit your needs. Here, you need to setup the fantastic UrlRewriteFilter. Since we build the project with maven, when you build the webapp either for a real container or in hosted mode, the required jar file will automatically get put under /war/WEB-INF/lib/. You also need to have the file /war/WEB-INF/urlrewrite.xml and configure a servlet filter to filter all incoming requests. The web.xml looks like this:
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd" > <web-app> <display-name>GWT Prototype</display-name> <session-config> <session-timeout>30</session-timeout> </session-config> <!-- FILTERS --> <filter> <filter-name>UrlRewriteFilter</filter-name> <filter-class>org.tuckey.web.filters.urlrewrite.UrlRewriteFilter</filter-class> </filter> <filter-mapping> <filter-name>UrlRewriteFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <!-- SERVLET DEFINITIONS --> <servlet> <servlet-name>userServlet</servlet-name> <servlet-class>com.example.server.MyServiceImpl</servlet-class> </servlet> <!-- SERVLET MAPPINGS --> <!-- stupid JBoss does not allow multiple <url-pattern> per <servlet-name> --> <servlet-mapping> <servlet-name>userServlet</servlet-name> <url-pattern>/com.example.User/MyService</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>userServlet</servlet-name> <url-pattern>/com.example.Admin/MyService</url-pattern> </servlet-mapping> <!-- WELCOME FILE --> <welcome-file-list> <welcome-file>index.html</welcome-file> </welcome-file-list> </web-app>
All incoming requests will be run through our filter rules which are defined in urlrewrite.xml. The other servlets are examples of GWT-RPC that I put in the project. You can see how you can still get GWT-RPC working for both the Admin or the User application. A better approach, I think would be to use spring-webmvc, but this is a simple example, and I'll save that for another day.
Consult the urlrewrite docs for how to set it up. You just basically use regular expressions. The project example is already pre-configured to do the following:
<?xml version="1.0" encoding="utf-8"?> <!DOCTYPE urlrewrite PUBLIC "-//tuckey.org//DTD UrlRewrite 3.2//EN" "http://tuckey.org/res/dtds/urlrewrite3.2.dtd"> <!-- Configuration file for UrlRewriteFilter http://tuckey.org/urlrewrite/ --> <urlrewrite> <!-- USER RULES 3-letter-code is appended to url like: /user/abc --> <rule> <note>must specify 3-letter-code or redirect to homepage</note> <from>^/user/$</from> <to last="true" type="redirect">%{context-path}/index.html</to> </rule> <rule> <note>no trailing slash; must specify 3-letter-code or redirect to homepage</note> <from>^/user$</from> <to last="true" type="redirect">%{context-path}/index.html</to> </rule> <rule> <from>^/user/([a-z]{3})$</from> <to>/com.example.User/User.html?c=$1</to> </rule> <outbound-rule> <from>^/com.example.User/User.html?c=([a-z]{3})$</from> <to encode="false">/user/$1</to> </outbound-rule> <rule> <from>^/user/(.*)$</from> <to>/com.example.User/$1</to> </rule> <outbound-rule> <from>^/com.example.User/(.*)$</from> <to encode="true">/user/$1</to> </outbound-rule> <!-- ADMIN RULES admin page --> <rule> <note>trailing slash problem - redirect to trailing slash</note> <from>^/admin$</from> <to last="true" type="redirect">%{context-path}/admin/</to> </rule> <rule> <from>^/admin/*$</from> <to>/com.example.Admin/Admin.html</to> </rule> <rule> <from>^/admin/(.*)$</from> <to>/com.example.Admin/$1</to> </rule> <outbound-rule> <from>^/com.example.Admin/Admin.html</from> <to>/admin/</to> </outbound-rule> <outbound-rule> <from>^/com.example.Admin/(.*)$</from> <to>/admin/$1</to> </outbound-rule> </urlrewrite>
Here are some examples of what you can do:
In the latter case, we store the "abc" string and pre-populate a form-field in the UserMiddlePanel.
If you look at the urlrewrite rules carefully, you'll notice I'm trying to pass the "abc" string as a URL parameter. I could not get this to work. You should be able to fetch this via
String code = Window.Location.getParameter("c");
…but this always returns null for me. In any event, I just use a regex to parse the path itself. If someone knows how to get the rules to work with URL params, I'd like to hear it.

Drawbacks To This Approach
Everything has pros and cons. I hope you see some of the pros:
- Single project structure — easier to setup and maintain
- Widget re-usability
- Unique, customized entry points
- URL filtering to customize your app
Here are some of the drawbacks.
- Compile time is slow. You are essentially doubling your compilation time by adding another module. GWT compilation isn't super fast.
- You have some JavaScript code duplication in the deployed war file. Essentially the war contains a dupe for each com.example.* directory, but the JavaScript is custom for that app. The only drawback here is it bloats the size of the war a little, but who cares? It doesn't make the app load any slower for the client. If the client switches urls from /user to /admin it is true that they switch completely over and have to go fetch the resources for the other application, but I don't find this to be much of a problem.
- Debugging in hosted mode is a little more of a pain. If you try to just switch URLs in the hosted browser it will fail if you haven't yet compiled the other application. This is a minor inconvenience.
Deploying
To deploy, you can run the full command:
$ mvn clean gwt:compile -Dinplace=false package
That will build you a war file that you can deploy to any container. If you have JBoss, you can probably figure out the pom.xml cargo stuff. You basically just need to fill out the file src/main/filter/developer.properties to point to your local JBoss home directory, and then you can run the command:
$ mvn clean gwt:compile -Dinplace=false package cargo:deploy cargo:start
Backlinks
Add one blog page
Tags
Hi Davis, how are you? Wanted to take a look but couldn't find the link to download the sample app… jeeez ;-)
Hey Pieter — I am doing well. You caught me…I wasn't finished yet. It is getting harder to do these things with a little one running around consuming all my time. I will finish it tonight (hopefully) :)
Great post - I found it very useful.
super cool! take care of your little one… thx and greets, p
Very nice material.
Thanks for your time.
Nice post. Great resource for getting one project multiple entry points.
URL ReWrite is hack sure there has to be a better way .
Hi,
this is really a nice post. I was looking for something similar to this. But this project seems to work with maven plugin and other dependentcies.
Can i use google GWT plugin and achieve this URL rewriting and REST ful hack.
I wanted something like this for example in this case the admin page content (only html data would be returned, real REST) will be returned with a Rest URL like http://localhost:8080/admin, It should not redirect to that page !!! so that the data could be used for replacing some div element in someother page and can be displayed like a widget over there !!!
In short Can we achieve urlrewrite and REST way (urlrewrite.xml rules addition) with out using maven plugin ?
Gwt has an option in gwt.xml files to rename the module:
<module rename-to="somethingElse">
This way you can avoid having the whole package name in the path. So I don't think you need the URL rewriting ;)
@zeratus — not the same thing. my code required different urls for different entry points. url rewrite isn't required if you have a single entry point, but it is if you have mutliple. in any event, i wrote this post back in 2009 when i was building a GWT app. GWT has gone through several major releases since then. not sure what has changed.
Post preview:
Close preview