Friday, October 1, 2010

Basic GWT-RPC Application

After stepping through this introduction to a simple GWT-RPC example, I had to experiment a bit to (1) get it running and (2) get some clarity around the naming hooks. What I'll present here is my own variation on that article's code examples, and the understanding I gained around what to keep straight around the names of the various classes and files.

Here are the moving parts used to test things in hosted mode:

  1. Client-side: entry point, remote service interface and async-remote service interface
  2. Server-side remote service implementation
  3. GWT module file
  4. Public folder with a startup HTML file
  5. Web-side deployment descriptor (web.xml)
  6. POM

Using a standard Maven structure, here's what it looks like:

This is for all intents the same thing as the article's layout, but I've changed some names around to make it clearer (for me, at least ) how the names are coupled. To elaborate on that, here are the things that get named and must be managed with care:

  1. The GWT module file (in this case, ProtoRPC.gwt.xml)
  2. The GWT module
  3. The servlet context
  4. The relative path that follows the servlet context
  5. The JavaScript file invoked at startup

Each of these is addressed in more than one place, either explicitly or implicitly - hence, the need to manage things carefully. What I'll do is simply present code snippets that have embedded comments explaining the coupling as we go. First, the client-side remote service interfaces and the server-side implementation:


/**
* Client-side remote interface used by top-level entry point to generate an async version
* that is used for RPC, and implemented by server side to provide service behavior. Note that
* the RemoteServiceRelativePath must match up with the url-pattern 2nd part in the web.xml and
* with the servlet path value in the <module>.gwt.xml.
*/
@RemoteServiceRelativePath("relative-path")
public interface MyService extends RemoteService
{
public String myMethod(String s);
}

/**
* The async-remote service interface, which must specify all methods to be used by the
* client, exactly as specified in the remote service interface, but with the addition of
* the AsyncCallback argument and substituting void return values.
*/
public interface MyServiceAsync {
public void myMethod(String s, AsyncCallback callback);
}

public class MyServiceImpl extends RemoteServiceServlet implements MyService {
public String myMethod(String s) {
return "Message received: '" + s + "'";
}
}

...the client-side entry point implementation:


/**
* Top-level entry point loaded at startup time, as configured in the module gwt.xml file; this
* class invokes the proxy for the async-remote service, executes a method via that proxy and
* establishes on-success and on-failure methods that the remote service will call back to.
*/
public class MyApplication implements EntryPoint {

public void onModuleLoad() {

MyServiceAsync svc = (MyServiceAsync) GWT.create(MyService.class);

/* If you don't specify the @RemoteServiceRelativePath on the service interface,
you should set it here. But, do NOT preface the relative path with a slash, as
instructed in the GWT-RPC article, or you'll get a 404:

ServiceDefTarget endpoint = (ServiceDefTarget) svc;
endpoint.setServiceEntryPoint("/relative-path");

Using the leading slash yields this:

com.google.gwt.user.client.rpc.StatusCodeException:
HTTP ERROR:404 NOT_FOUND
RequestURI=/relative-path

Instead, do this:

endpoint.setServiceEntryPoint("relative-path");

In this example, I use the annotation on the remote service interface instead.
*/

AsyncCallback callback = new AsyncCallback() {
public void onSuccess(Object result) {
RootPanel.get().add(new HTML(result.toString()));
}

public void onFailure(Throwable ex) {
RootPanel.get().add(new HTML(ex.toString()));
}
};

svc.myMethod("Do some Stuff...", callback);
}
}

The GWT module file specifies the entry point, the servlet relative path and the service implementation:


<?xml version="1.0" encoding="UTF-8"?>
<!--
Module name (either defaulted or changed via rename-to) must match up with the
servlet-mapping/url-pattern first part in web.xml; and with the gwt-maven configuration/runTarget value in the pom.xml. Also match it up with
the name of the JS file referenced in the startup HTML file.

Finally, the convention is for that startup HTML file to reference this file, as a
fully-qualified Java classname, as the gwt:module:content value.
-->
<module rename-to="module">
<inherits name="com.google.gwt.user.User"/>
<entry-point class="com.mybiz.proto.gwt.rpc.client.MyApplication"/>
<!--
The servlet path value must match up with the 2nd part of the ur-pattern in web.xml and
the @RemoteServiceRelativePath annotation value in service interface.
-->
<servlet path="/relative-path" class="com.mybiz.proto.gwt.rpc.server.MyServiceImpl"/>
</module>

The startup HTML file references the GWT module and a JavaScript file:


<html>
<head>
<title>Wrapper for Prototype GWT-RPC</title>
<!--
name of meta/content should match up with name of <module>.gwt.xml file, specified
as a fully-qualified Java classname.
-->
<meta name='gwt:module' content='com.twc.atg.msb.ProtoRPC' />
</head>
<body>
<!--
name of javascript src file should match up with name of GWT module, as configured
in the <module>.gwt.xml file
-->
<script language="javascript" src="module.nocache.js"></script>
</body>
</html>

The web.xml configures the servlet:


<?xml version="1.0" encoding="ISO-8859-1"?>
<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">

<servlet>
<servlet-name>service</servlet-name>
<servlet-class>com.mybiz.proto.gwt.rpc.server.MyServiceImpl</servlet-class>
</servlet>

<servlet-mapping>
<servlet-name>service</servlet-name>
<!--
The servlet-mapping/url-pattern first part must match up with module name in <module>.gwt.xml
file; and the gwt-maven configuration/runTarget value in the pom.xml.

The url-pattern 2nd part must match up with @RemoteServiceRelativePath annotation value
in service interface and servlet path value in the <module>.gwt.xml.
-->
<url-pattern>/module/relative-path</url-pattern>
</servlet-mapping>

</web-app>

And the POM configures the gwt-maven-plugin to facilitate testing. Though I omit dependencies here, you'll want to provide the gwt-user and gwt-servlet artifacts from com.google.gwt. This is the build tag entry, which also provides a workaround to GWTTestCase problems as found in this forum discussion:


<build>
<!-- outputDirectory must match up with first part of maven-war-plugin/webappDirectory -->
<outputDirectory>${basedir}/war/WEB-INF/classes</outputDirectory>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>gwt-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<!-- omit async generation if creating your own RPC classes -->
<goal>compile</goal>
</goals>
<configuration/>
</execution>
</executions>
<configuration>
<!--
Keep the context path here in sync with web.xml and
<module>.gwt.xml files.
-->
<runTarget>http://localhost:8080/module/index.html</runTarget>
<sourceDirectory>${basedir}/src/main/java</sourceDirectory>
<warSourceDirectory>${basedir}/src/main/webapp</warSourceDirectory>
<inplace>true</inplace>
<port>8080</port>
</configuration>
</plugin>

<plugin>
<groupId>com.springsource.bundlor</groupId>
<artifactId>com.springsource.bundlor.maven</artifactId>
</plugin>

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<configuration>
<webappDirectory>${basedir}/war</webappDirectory>
<warSourceDirectory>${basedir}/src/main/webapp</warSourceDirectory>
<webXml>${basedir}/src/main/webapp/WEB-INF/web.xml</webXml>
</configuration>
</plugin>

<!--
As per http://stackoverflow.com/questions/2737173/error-when-running-a-gwttestcase-using-maven-gwt-plugin,
the last piece of advice on that page offers this kind of configuration to prevent the problem:

No source path entries. Expect subsequent failures.

...when running GWTTestCase tests. The problem is how GWT loads
classes at Junit test time - it loads from the network vs from the system classloader. See that
stackoverflow question for full explanation.
-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<useSystemClassLoader>false</useSystemClassLoader>
<additionalClasspathElements>
<additionalClasspathElement>${basedir}/src/main/java</additionalClasspathElement>
<additionalClasspathElement>${basedir}/src/test/java</additionalClasspathElement>
</additionalClasspathElements>
</configuration>
<executions>
<execution>
<phase>integration-test</phase>
<goals>
<goal>test</goal>
</goals>
</execution>
</executions>
</plugin>

</plugins>
</build>

Launch this via the gwt-maven-plugin run goal. Phew.

2 comments:

  1. everything works apart from module.nocache.js which need to be module/module.nocache.js (at least it happened to me, not even GWT tutorial website mention this)

    ReplyDelete
  2. That's a bit surprising - are you sure your index.html file is in the src/main/java/.../public folder? In my case, structuring things as in my image for the "standard Maven structure" in the article resulted in both index.html and a module.nocache.js file going to the src/main/webapp/admin folder. So, while yes you're right that the module.nocache.js file is in that ./admin folder, so too should the index.html file be there. Now if the index.html somehow ended up in the directory above that, then you would need to reference the JS file as you stated, i.e. as module/module.nocache.js.

    So, I'm not quite sure what's going on here.

    ReplyDelete