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.
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 | |
---|---|
All client classes must be under the same package root. With |
<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>
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.
<?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>
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.
<%@ 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>
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 | |
---|---|
The value returned from the |
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()); } } }