Gillius's Programming

Adapting Smartclient DataSource to RequestFactory

SmartGWT controls are beautiful.  Unfortunately, using them is an all-in proposition - you miss out on most of the benefits (and make a lot of extra work for yourself) if you don't use the entire stack from GUI to backend.  I had some good reasons for not using their backend, namely that my GWT application already used Jersey and GWT RequestFactory, and I didn't want to add yet a third servlet to feed data in a form of SmartGWT's liking.  This post shows a simple way to create your own SmartGWT compatible DataSource to back their sexy controls and widgets but mapped to your existing backend.

A very nice thing about SmartGWT is the data binding model.  Their controls are meant to work with their own concept of a DataSource.  Typically it is backed by their own servlet that you configure in web.xml.  Very little client-side code is required to get nice features like lazy fetching and paging.  You place a table on a screen, point it to the DataSource and it will handle CRUD operations and paging for you.

To get this functionality you are encouraged to use smartclient's backend servlet as part of your project.  Alternatively, you can create a RESTful service of your own, but it has to conform to SmartClient's REST data source object model and schema.  Their standard servlet expets to be backed by Hibernate or SQL.

For me, neither of these solutions is particularly optimal.  I already have my own backend (two, in fact - one RESTful, the other RequestFactory).  I don't want to add a third.  Furthermore, my data lives in a combination of S3 and SimpleDB on the Amazon cloud.  The interface is not SQL.

Fortunately, it's not that hard to create your own SmartGWT DataSource backed by whatever you want.  A Smartclient forum post explains the concept of mapping to a GwtRpc backend..  I found that example to be general enough to be confusing, and thought developers would benefit from a less generic example.

SmartGWT DataSources are built using four basic operations, fetch, add, update, and remove - CRUD with a few required paremeters.  Requests are expressed by DSRequest and responses by DSResponse.  For simple paging the DSRequest object includes startRow and endRow parameters.  More robust fetches can be implemented to allow things like server-side-filtered requests.

As an example I have implemented a SmartGWT DataSource backed by a GWT RequestFactory. CustomerRequestFactory has factory has one request (though the remainder of the CRUD model would have to be implemented for this to be complete):

public interface CustomerRequestFactory extends RequestFactory {
	@Service(value = CustomerService.class, locator = CustomerServiceLocator.class)
	public interface CustomerRequest extends RequestContext {
		public Request<List<CustomerProxy>> fetchCustomers(int startRow, int endRow);
	}
	CustomerRequest custRequest();
}

The DataSource that uses this RequestFactory looks like this:

/**
 * An example DataSource backed by a GWT RequestFactory service that
 * gets a list of customers.
 * @author chrisp
 */
public class CustomerDataSource extends DataSource {

	/**
	 * Customer RequestFactory, which will be injected in by GIN
	 */
	final CustomerRequestFactory requestFactory;

	@Inject
	public CustomerDataSource( CustomerRequestFactory requestFactory ) {
		this.requestFactory = requestFactory;

		/*
		 * The following two settings tell the base class that we will
		 * be using a custom protocol, and that SmartGWT should not
		 * attempt to contact the server directly.  This is necessary
		 * for us to hijack all requests.
		 */
		setDataProtocol( DSProtocol.CLIENTCUSTOM );
		setClientOnly( true );

		/*
		 * Add a couple of fields - this data source is specific to one
		 * object, so I'm going to just do this statically
		 */
		DataSourceTextField idField = new DataSourceTextField( "id", "Customer ID", 128 );
		idField.setPrimaryKey( true );
		idField.setRequired( true );
		addField( idField );

		DataSourceTextField nameField = new DataSourceTextField( "name", "Customer Name", 40 );
		nameField.setRequired( true );
		addField( nameField );
	}

	/**
	 * Transform a request.  This is our hijacking point.  We'll see the
	 * request that the DataSource wishes to make, but since we're in client
	 * only mode it won't actually do it.  We'll make the request ourselves,
	 * using RequestFactory.
	 * @param request
	 * @return 
	 */
	@Override
	protected Object transformRequest( DSRequest request ) {
		String requestId = request.getRequestId();
		DSResponse response = new DSResponse();
		response.setAttribute( "clientContext", request.getAttributeAsObject( "clientContext" ) );

		/*
		 * Figure out what kind of request this is, and delegate to
		 * one of our four handlers.
		 */
		switch ( request.getOperationType() ) {
			case FETCH:
				executeFetch( requestId, request, response );
				break;
			case ADD:
				executeAdd( requestId, request, response );
				break;
			case UPDATE:
				executeUpdate( requestId, request, response );
				break;
			case REMOVE:
				executeRemove( requestId, request, response );
				break;
			default:
				// Operation not implemented.
				break;
		}
		return request.getData();
	}

	private void executeFetch( final String requestId, final DSRequest dsRequest, final DSResponse dsResponse ) {
		final CustomerRequestFactory.CustomerRequest ourRequest = requestFactory.custRequest();

		int startRow = dsRequest.getStartRow();
		int endRow = dsRequest.getEndRow();

		ourRequest.fetchCustomers( startRow, endRow ).fire( new Receiver<List<CustomerProxy>>() {

			@Override
			public void onSuccess( List<CustomerProxy> customers ) {
				
				/*
				 * Our request factory request returns a list of CustomerProxy
				 * records.  These need to be put into a DataSource Record
				 * format.
				 */
				RecordList records = new RecordList();
				for ( CustomerProxy customer : customers ) {
					Record r = new Record();
					r.setAttribute( "id", customer.getId() );
					r.setAttribute( "name", customer.getCustomerName() );
					records.add( r );
				}
				
				/*
				 * Add the results to the DSResponse
				 */
				Record[] arr = records.toArray();
				dsResponse.setData( arr );
				dsResponse.setStatus(RPCResponse.STATUS_SUCCESS);

				/*
				 * Notify the base class that we've received this
				 * response.
				 */
				processResponse( requestId, dsResponse );
			}
			
			@Override
			public void onFailure(ServerFailure error) {
				GWT.log("FAIL: " + error.getMessage());
				dsResponse.setStatus( RPCResponse.STATUS_FAILURE );
				processResponse( requestId, dsResponse );
			}
		} );
	}
}

In the way of the better explanation I promised earlier let me explain a little bit of what's going on here. The transformRequest() method intercepts the request and delegates to one of our helper, executeFetch(). This method does the actual work of launching the request to the wire.  Note that it needs to keep hold of the requestId - SmartGWT needs this to match a response with a request. The response comes some time later (we're making an asynchronous request). Now, because we have set clientOnly mode, the base class knows that it should do no further work. When we get a response to our RequestFactory request we simply process it and formulate a SmartGWT Record list, then call processResponse() to push the new data into SmartGWT. Everything will still act as if the DataSource had made a request and gotten a response in its usual way, but we have hijacked the request and injected our own response.

The GwtRpc example mentioned earlier is a much more general solution and, as they said, it could be modified to use a RequestFactory backend.  The issue I had (and my  motivation for writing this) is that expressing the solution in terms of a number of generic classes makes it harder to follow if you're just figuring out how to do this.

In general terms, it has always bothered me to be forced into using some particular servlet.  Being able to intercept DataSource requests and responses means you can adapt to whatever backend you already have.  If that happens to be RequestFactory it's fairly straight forward, but even if you want to use a custom RESTful service this example shows how you'd do it.  It's not at all unreasonable to back SmartGWT controls using, for example, a CSV or Protocol Buffers service.

Good luck!