2. GWT

For GWT integration a Maven plugin that generates the GWT static HTML and JavaScript files as part of the compilation step is being used. The GWTApplication.launch can be used to launch GWT inside Eclipse either in standard or debug mode. Unfortunately GWT was made to extend it's RemoteServiceServlet class. It was simplest to have a mock/test implementation to return some static data to use with GWT debugging and a Spring controller for development within Spring. In order to develop in Spring, there is a small manual step in the current project setup. Which is running Maven's compile and then copying the generated files into 'src/main/webapp' after removing the existing files. GWT generates unique file names each time for most files, so old ones will build up if they aren't removed.

            mvn compile
            # remove old static files from src/main/webapp
            cp -R target/simple-gwt-1.0/org.springbyexample.web.gwt.App src/main/webapp/
        

The Spring controller extends GwtController within the project and is very simple. It extends RemoteServiceServlet and also implements the Spring Controller interface, which then just calls doPost in the handleRequest method. The doPost method will deserialize any incoming IsSerializable JavaBeans sent by the client request, call the method specified in the RPC request, and serialize any results returned to the client. Also see the GWTSpringController GWT Widget Library to not have to maintain your own controller class.

GWT Configuration

This is used during the compilation process and when running GWT with GWTApplication.launch in Eclipse. There is a static HTML page in a directory called 'public' just under the XML file's directory.

All normal projects inherit com.google.gwt.user.User, but this example also uses com.google.gwt.i18n.I18N for internationalization and com.google.gwt.http.HTTP for getting the edit/delete links from a JSP fragment for each table row. The entry-point element is the class that initializes the GWT application. Typically client code is under a client package and server is under a server package.

[Note]Note

All client classes must be under the same package root. With org.springbyexample.web.gwt.client.App, org.springbyexample.web.gwt.client.bean.Person is ok, but org.springbyexample.web.bean.Person would not be.

App.gwt.xml
                    
<module>

    <!-- Inherit the core Web Toolkit stuff. -->
    <inherits name='com.google.gwt.user.User'/>
    <inherits name="com.google.gwt.i18n.I18N"/>
    <inherits name="com.google.gwt.http.HTTP"/>

    <!-- Specify the app entry point class. -->
    <entry-point class='org.springbyexample.web.gwt.client.App'/>

    <servlet path='/service.do' class='org.springbyexample.web.gwt.server.ServiceImpl'/>
  
</module>
                    
                

Spring Configuration

The ServiceController is loaded by the context:component-scan and configured on the SimpleUrlHandlerMapping bean to map to '/person/service.do' to handle calls from the GWT client.

/WEB-INF/gwt-controller-servlet.xml
                    
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans 
                           http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context 
                           http://www.springframework.org/schema/context/spring-context.xsd">

    <context:component-scan base-package="org.springbyexample.web.gwt.server" />

    <bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping">
        <!-- <property name="interceptors" ref="localeChangeInterceptor"/> -->
    </bean>

    <!-- Enables annotated POJO @Controllers -->
    <bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter" />

    <!-- Enables plain Controllers -->
    <bean class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter" />

    <bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="order" value="0" />
        <property name="mappings">
            <value>
                /person/service.do=serviceController
            </value>
        </property>
    </bean>
    
</beans>
                    
                

JSP Example

The script element references the generated script entry point and the 'search-table' div is where the script will insert the table widget.

GWT has built in internationalization (i18n) support, but to integrate it in with the web application's i18n support a JavaScript object must be created on the page that GWT will read from by calling Dictionary messageResource = Dictionary.getDictionary("MessageResource"). Then they can be retrieved by calling messageResource.get("person.form.firstName") with the message key.

/WEB-INF/jsp/person/search.jsp
                    
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>

<script language="javascript" src="<c:url value="/org.springbyexample.web.gwt.App/org.springbyexample.web.gwt.App.nocache.js"/>"></script>

<!-- OPTIONAL: include this if you want history support -->
<iframe id="__gwt_historyFrame" style="width:0;height:0;border:0"></iframe>

<%-- Used by GWT Search Table Widget for Internationalization --%>
<script language="javascript">
//<!--        
    var MessageResource = {
        "person.form.firstName": "<fmt:message key="person.form.firstName"/>",
        "person.form.lastName": "<fmt:message key="person.form.lastName"/>"
    };
// -->
</script>
        
<h1><fmt:message key="person.search.title"/></h1>

<div id="search-table"></div>
                    
                

Code Example

GWT requires an interface and a matching asynchronous interface for clients to use. Whatever the main interface name is, the asynchronous name should match with a suffix of 'Async' ('Service' --> 'ServiceAsync').

Example 1. Service

Server interface that must extend GWT's RemoteService interface.

                    
public interface Service extends RemoteService {

    /**
     * Finds person within a range.
     */
    public Person[] findPersons(int startIndex, int maxResults);

}
                    
                

Example 2. ServiceAsync

Client asynchronous interface that matches the Service interface, but returns void and has AsyncCallback<Person[]> added as a final parameter.

[Note]Note

The value returned from the Service interface is now the generic value passed into the AsyncCallback.

                    
public interface ServiceAsync {

    /**
     * Finds person within a range.
     */
    public void findPersons(int startIndex, int maxResults, AsyncCallback<Person[]> callback);

}
                        
                    

Example 3. ServiceController

This class extends GwtController and implements the Service interface. The person DAO is used to get the current page of results and copies them into another Person instance that implements IsSerializable (which indicates that this is serializable by GWT RPC requests). The instance returned by the DAO can't be used because it has annotations that the GWT compiler can't convert to JavaScript.

                    
@Controller
public class ServiceController extends GwtController implements Service {

    final Logger logger = LoggerFactory.getLogger(ServiceController.class);

    private static final long serialVersionUID = -2103209407529882816L;

    @Autowired
    private PersonDao personDao = null;
    
    /**
     * Finds person within a range.
     */
    public Person[] findPersons(int startIndex, int maxResults) {
        Person[] results = null;

        List<Person> lResults = new ArrayList<Person>();

        Collection<org.springbyexample.orm.hibernate3.annotation.bean.Person> lPersons = personDao.findPersons(startIndex, maxResults);
        
        for (org.springbyexample.orm.hibernate3.annotation.bean.Person person : lPersons) {
            Person result = new Person();
            result.setId(person.getId());
            result.setFirstName(person.getFirstName());
            result.setLastName(person.getLastName());
            
            lResults.add(result);
        }

        return lResults.toArray(new Person[]{});
    }

}
                    
                

Example 4. SearchTableWidget.PersonProvider

The constructor sets up the asynchronous service. The URL base is set by using GWT.getHostPageBaseURL() so the generated JavaScript can be hosted in other pages as shown in the search.jsp above. The callback is used to process the results or a failure if there is a problem with the request.

The processLinkRequest method is used to populate the table cell with the edit/delete link by making an HTTP get request to a JSP fragment with the links in the pages. This isn't the best for performance, but seemed the easiest way to leverage Spring Security's JSP tags for evaluating whether or not someone has permission to delete a record.

                    
private class PersonProvider implements SearchTableDataProvider {

    private final ServiceAsync service;
    private int lastMaxRows = -1;
    private Person[] lastPeople;
    private int lastStartRow = -1;

    /**
     * Constructor
     */
    public PersonProvider() {
        service = (ServiceAsync) GWT.create(Service.class);
        ServiceDefTarget target = (ServiceDefTarget) service;
        String moduleRelativeURL = GWT.getHostPageBaseURL() + "service.do";
        target.setServiceEntryPoint(moduleRelativeURL);
    }

    public void updateRowData(final int startRow, final int maxRows,
                              final RowDataAcceptor acceptor) {
        final StringBuffer sb = new StringBuffer();
        
        // Check the simple cache first.
        if (startRow == lastStartRow) {
            if (maxRows == lastMaxRows) {
                // Use the cached batch.
                pushResults(acceptor, startRow, lastPeople);
            }
        }

        // Fetch the data remotely.
        service.findPersons(startRow, maxRows, new AsyncCallback<Person[]>() {
            public void onFailure(Throwable caught) {
                acceptor.failed(caught);
            }

            public void onSuccess(Person[] result) {
                lastStartRow = startRow;
                lastMaxRows = maxRows;
                lastPeople = result;
                
                pushResults(acceptor, startRow, result);
            }

        });
    }

    private void pushResults(RowDataAcceptor acceptor, int startRow, Person[] people) {
        String[][] rows = new String[people.length][];
        for (int i = 0, n = rows.length; i < n; i++) {
            Person person = people[i];
            rows[i] = new String[3];
            rows[i][0] = person.getFirstName();
            rows[i][1] = person.getLastName();
            rows[i][2] = "";

            // increment by one because of table header
            int row = i + 1;
            processLinkRequest(person.getId(), acceptor, row);
        }

        String message = acceptor.accept(startRow, rows);
        dynamicTable.setStatusText(message);
    }

    private void processLinkRequest(Integer personId,
            final RowDataAcceptor acceptor, final int row) {
        String url = GWT.getHostPageBaseURL() + "fragment/search_link.htm" + 
                     "?ajaxSource=true&fragments=content&" + "id=" + personId;
        url = URL.encode(url);

        RequestBuilder rb = new RequestBuilder(RequestBuilder.GET, url);

        try {
            Request response = rb.sendRequest(null, new RequestCallback() {
                public void onError(Request request, Throwable exception) {
                }

                public void onResponseReceived(Request request, Response response) {
                    dynamicTable.setCell(row, 2, response.getText());
                }
            });
        } catch (RequestException e) {
            Window.alert("Failed to send the request: " + e.getMessage());
        }
    }
}