Saturday, September 28, 2013

Dynamic REST Client & Controller Generation


Here is the example from SBE REST Modules on GitHub.

It let's you create an interface and put your Spring MVC REST annotations on it if you follow certain patterns. The REST client and controller can be generated using Spring beans. It's expected that your controllers would just delegate to a service layer, so the interface to that service can be registered using @RestResource on the class. The base URI and default response class are also defined here. By default, a method with @RequestMapping will expect the same method and method signature to be available to delegate to in the service class.

Another feature is to turn off creating a relative URI using the class' path, is to specify @RestRequestResource on a method and setting 'relative' to false. If the interface overloads a method to perform further conversion on the results, the method name can be specified. The framework still expects the method signatures to match. A converter can be specified to change the result before it's sent to the client. A good example of this is creating smaller models to match different needs without doing further customizations on the backend services or queries.

This all needs more work, but all of the basics are working now. I'll write this up soon on Spring by Example.



public interface PersistenceFindMarshallingService<R extends EntityResponseResult, FR extends EntityFindResponseResult> {

     public final static String PATH_DELIM = "/";
     public final static String PARAM_DELIM = "?";
     public final static String PARAM_VALUE_DELIM = "&";

     public final static String ID_VAR = "id";
     public final static String PAGE_VAR = "page";
     public final static String PAGE_SIZE_VAR = "page-size";

     public final static String PAGE_PATH = PATH_DELIM + PAGE_VAR;
     public final static String PAGE_SIZE_PATH = PATH_DELIM + PAGE_SIZE_VAR;
     public final static String PAGINATED = PAGE_PATH + PATH_DELIM + "{" + PAGE_VAR + "}" + PAGE_SIZE_PATH + PATH_DELIM + "{" + PAGE_SIZE_VAR + "}";

     public final static String ROOT_URI = PATH_DELIM;
     public final static String FIND_BY_ID_URI = PATH_DELIM + "{" + ID_VAR + "}";

     /**
      * Find by primary key.
      */
     @RequestMapping(value = FIND_BY_ID_URI, method = RequestMethod.GET)
     public R findById(@PathVariable(ID_VAR) Integer id);

     /**
      * Find a paginated record set.
      */
     @RequestMapping(value = PAGINATED, method = RequestMethod.GET)
     public FR find(@PathVariable(PAGE_VAR) int page, @PathVariable(PAGE_SIZE_VAR) int pageSize);

     /**
      * Find all records.
      */
     @RequestMapping(value = ROOT_URI, method = RequestMethod.GET)
     public FR find();

}


public interface PersistenceMarshallingService<R extends EntityResponseResult, FR extends EntityFindResponseResult, S extends PkEntityBase>
         extends PersistenceFindMarshallingService<R, FR> {

     public final static String DELETE_URI = ROOT_URI + "remove";

     /**
      * Save record.
      */
     @RequestMapping(value = ROOT_URI, method = RequestMethod.POST)
     public R create(@RequestBody S request);

     /**
      * Update record.
      */
     @RequestMapping(value = ROOT_URI, method = RequestMethod.PUT)
     public R update(@RequestBody S request);

     /**
      * Delete record.
      */
     // FIXME: server has marshalling error if DELETE
     @RequestMapping(value = DELETE_URI, method = RequestMethod.PUT)
     public R delete(@RequestBody S request);

}



@RestResource(service=ContactService.class, path=PATH, responseClass=PersonResponse.class)
public interface ContactMarshallingService extends PersistenceMarshallingService<PersonResponse, PersonFindResponse, Person> {

     final static String PATH = "/person-test";
     final static String SMALL_URI = "/small";
     final static String SMALL_PATH = "/small-person";

     public final static String SMALL_FIND_BY_ID_REQUEST = SMALL_PATH + PATH_DELIM + "{" + ID_VAR + "}";

     public final static String SMALL_FIND_PAGINATED_REQUEST = SMALL_PATH + PAGINATED;

     public final static String LAST_NAME_VAR = "lastName";
     public final static String LAST_NAME_PARAMS = PARAM_DELIM + LAST_NAME_VAR + "={" + LAST_NAME_VAR + "}";
     public final static String FIND_BY_LAST_NAME_CLIENT_REQUEST = PATH + LAST_NAME_PARAMS;
     public final static String SMALL_FIND_BY_LAST_NAME_CLIENT_REQUEST = PATH + SMALL_URI + LAST_NAME_PARAMS;

     @RequestMapping(value = SMALL_FIND_BY_ID_REQUEST, method = RequestMethod.GET)
     @RestRequestResource(relative=false, methodName="findById", converter=SmallContactConverter.class)
     public PersonResponse smallFindById(@PathVariable(ID_VAR) Integer id);

     @RequestMapping(value=SMALL_FIND_PAGINATED_REQUEST, method = RequestMethod.GET)
     @RestRequestResource(relative=false, methodName="find", converter=SmallContactConverter.class)
     public PersonFindResponse smallFind(@PathVariable(PAGE_VAR) int page, @PathVariable(PAGE_SIZE_VAR) int pageSize);

     @RequestMapping(value = PATH_DELIM, method = RequestMethod.GET, params= { LAST_NAME_VAR })
     public PersonFindResponse findByLastName(@RequestParam(LAST_NAME_VAR) String lastName);

     @RequestMapping(value = SMALL_URI, method = RequestMethod.GET, params= { LAST_NAME_VAR })
     @RestRequestResource(methodName="findByLastName", converter=SmallContactConverter.class)
     public PersonFindResponse smallFindByLastName(@RequestParam(LAST_NAME_VAR) String lastName);

}


Spring by Example Update (1.3)


I've been working on this off and on for a long time, but I finally have a Spring by Example site update ready. It's using Spring Framework 3.2.x and most major libraries are all upgraded. The biggest changes are to the Contact Application, which now references SBE REST Modules (available on Spring by Example's GitHub), which I will document on the site shortly. The Contact Application also has a better production ready DB connection pool configuration and upgrades the Jackson JSON mapper & view.

I did also try to create a shared base for messages/response for the JAXB beans, but I ran into some issues generating them in the Contact Application. The Fluent API doesn't look at parent classes when generating the '.withXXX' methods. It does look like it would be simple to customize the JAXB plugin to fix this, but I didn't want to take the time right now.

Below are the Contact Application modules.
  • DAO - DB Schema, JPA Entities, Spring Data JPA repositories.
  • Web Service Beans - JAXB beans generated from XSDs.
  • Services - APIs use JAXB beans and Dozer is used to convert between this beans and the JPA entities. Security and transactions are configured in this layer.
  • REST Services - The module has clients & controllers, as well as their Spring configurations. JSON and XML views are supported for requests.
  • Webapp - The webapp as a standard JSP UI, Sencha ExtJS, and also a Sencha Touch UI.
  • Test - The DAO, Services, and REST Services all have an abstract test class for each module that each test extends. This way within each module, all tests have a shared context so Spring only has to load once. All of these tests use an in memory database and the REST Services have an embedded jetty server. REST Services tests can be run with clients using JSON or XML for marshalling.