As a part of our Free Dojo Support initiative, we received the following question about using the new dojo.store.JsonRest API with Spring:

The Question

There are no examples of the server-side of the new dojo.store.JsonRest APIs using spring. There are a few detailing the older dojox.data interfaces, but nothing combining the latest dojo.store with annotated spring controllers. This would be a good jump off point for any new app given the prevalence of both of these technologies.

The Answer

To answer this question, we discussed this with Jeremy Grelle at vmware/Spring, who was nice enough to put together an example using dojo.store.JsonRest with Spring on GitHub.
The readme file contains complete details on how to make this work, and also notes a couple of current limitations between the dojo.store.JsonRest API and Spring, which will be addressed in the near future.
The example is reasonably self-explanatory. The client-side portion of the code is concise. Just create a store, add a caching wrapper, target the URL of the usstates service, create a grid, connect it to the store, define the structure for displaying data in the grid, bind it to a node, render the grid, and then connect save events to the save command on the store:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
myStore = dojo.store.Cache(
    dojo.store.JsonRest({target:"usstates/"}),
    dojo.store.Memory());
grid = new dojox.grid.DataGrid({
    store: dataStore = dojo.data.ObjectStore({
        objectStore: myStore}),
    structure: [
        {name:"State Name", field:"name", width: "200px"},
        {name:"Abbreviation", field:"abbreviation",
            width: "200px", editable: true}
    ]
    // make sure you have a target HTML element with this id
    }, "target-node-id");
grid.startup();
dojo.query("#save").onclick(function(){
dataStore.save();
});
The server-side code is also to the point. After including the necessary modules, you connect a Spring Roo WebScaffold as the URL path available to the client-side store, to the USState for backing class. You then map the URL to an HTTPRequest, define HTTP settings for the controller object to deliver JSON, etc., and then define various methods for delivering a response based on the request type (GET/PUT/POST/DELETE, etc.), and available data. Finally there are helper methods to support server-side sorting, and requesting data in blocks to support pagination of data loading:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
@RooWebScaffold(
    path = "usstates", formBackingObject = USState.class)
@RequestMapping("/usstates")
@Controller
public class USStateController {   
    private static final String RANGE_PREFIX = "items=";
    private static final String CONTENT_RANGE_HEADER = "Content-Range";
    private static final String ACCEPT_JSON = "Accept=application/json";
    @RequestMapping(value="/{id}", method=RequestMethod.GET, headers=ACCEPT_JSON)
 
    public @ResponseBody USState getJson(@PathVariable("id") Long id) {
        return USState.findUSState(id);
    }
     
    /**
* TODO - Should probably be stricter here in following the HTTP spec,
* i.e. returning the proper 20x response instead of an explicit redirect
*/
    @RequestMapping(method=RequestMethod.POST, headers=ACCEPT_JSON)
    public String createJson(@Valid @RequestBody USState state) {
        state.persist();
        return "redirect:/usstates/"+state.getId();
    }
     
    @RequestMapping(value="/{id}", method={RequestMethod.POST, RequestMethod.PUT}, headers={ACCEPT_JSON,"If-None-Match=*"})
    public String createJsonWithId(@Valid @RequestBody USState state, @PathVariable("id") Long id) {
        Assert.isTrue(USState.findUSState(id) == null);
        return updateJson(state, id);
    }
     
    @RequestMapping(value="/{id}", method={RequestMethod.POST, RequestMethod.PUT}, headers={ACCEPT_JSON,"If-Match=*"})
    public String overWriteJson(@Valid @RequestBody USState state, @PathVariable("id") Long id) {
        Assert.isTrue(USState.findUSState(id) != null);
        return updateJson(state, id);
    }
     
    /**
* TODO - Should probably be stricter here in following the HTTP spec,
* i.e. returning the proper 20x response instead of an automatic redirect
*/
    @RequestMapping(value="/{id}", method=RequestMethod.PUT, headers=ACCEPT_JSON)
    public String updateJson(@Valid @RequestBody USState state, @PathVariable("id") Long id) {
        Assert.isTrue(id.equals(state.getId()));
        state.merge();
        return "redirect:/usstates/"+state.getId();
    }
     
    @RequestMapping(method=RequestMethod.DELETE, headers=ACCEPT_JSON)
    public @ResponseBody ResponseEntity deleteJson(@PathVariable("id") Long id) {
        USState.findUSState(id).remove();
        return new ResponseEntity(HttpStatus.NO_CONTENT);
    }
     
    @RequestMapping(method=RequestMethod.GET, headers=ACCEPT_JSON)
    public @ResponseBody HttpEntity> listJson() {
        HttpHeaders headers = new HttpHeaders();
        List body = null;
        body = USState.findAllUSStates();
        headers.add(CONTENT_RANGE_HEADER, getContentRangeValue(0, body.size(), new Integer(body.size()).longValue()));
        return new HttpEntity>(body, headers);
    }
     
    @RequestMapping(method=RequestMethod.GET, headers={ACCEPT_JSON, "Range"})
    public @ResponseBody HttpEntity> listJsonForRange(@RequestHeader(value="Range") String range, HttpServletRequest request) {
        HttpHeaders headers = new HttpHeaders();
        List body = null;
        Range parsedRange = new Range(range.replaceAll(RANGE_PREFIX, ""));
        long count = USState.countUSStates();
        body = USState.findUSStateEntries(parsedRange.getFirstResult(), parsedRange.getMaxResults());
        headers.add(CONTENT_RANGE_HEADER, getContentRangeValue(parsedRange.getFirstResult(), body.size(), count));
        return new HttpEntity>(body, headers);
    }
     
    /**
* TODO - This doesn't actually get selected since the query param is in the form of 'sort(...)' instead of 'sort=(...)'
*/
    @RequestMapping(method=RequestMethod.GET, headers={ACCEPT_JSON, "Range"}, params="sort")
    public @ResponseBody HttpEntity> listJsonForRangeSorted(@RequestHeader("Range") String range, @RequestParam("sort") String sort) {
        HttpHeaders headers = new HttpHeaders();
        List body = null;
        Range parsedRange = new Range(range.replaceAll(RANGE_PREFIX, ""));
        long count = USState.countUSStates();
        //TODO - Implement sort param parsing
        body = USState.findOrderedUSStateEntries(parsedRange.getFirstResult(), parsedRange.getMaxResults(), "");
        headers.add(CONTENT_RANGE_HEADER, getContentRangeValue(parsedRange.getFirstResult(), body.size(), count));
        return new HttpEntity>(body, headers);
    }
     
    private String getContentRangeValue(Integer firstResult, Integer resultCount, Long totalCount) {
        StringBuilder value = new StringBuilder("items "+firstResult+"-");
        if (resultCount == 0) {
            value.append("0");
        } else {
            value.append(firstResult + resultCount - 1);
        }
        value.append("/"+totalCount);
        return value.toString();
    }
     
    private static final class Range {
         
        private Integer firstResult = 0;
        private Integer maxResults = 0;
         
        public Range(String range) {
            String[] parsed = range.split("-");
            Assert.isTrue(parsed.length == 2, "Range header in an unexpected format.");
            this.firstResult = new Integer(parsed[0]);
            Integer end = new Integer(parsed[1]);
            this.maxResults = end - firstResult + 1;
        }
         
        public Integer getFirstResult() {
            return this.firstResult;
        }
         
        public Integer getMaxResults() {
            return this.maxResults;
        }
    }
     
}