Maven Project with Multiple GWT Entry Points + Faking REST

August 8, 2009 - zenoconsultingzenoconsulting


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:

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:

url content comes from user sees
http://localhost:8080/admin http://localhost:8080/com.example.Admin/Admin.html http://localhost:8080/admin
http://localhost:8080/user/abc http://localhost:8080/com.example.User/User.html http://localhost:8080/user/abc

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.

screenshot.png

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 a New Comment
or Sign in as Wikidot user
(will not be published)
- +
Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License