Reputation: 795
I cannot seem to get a simple REST client integration test going. I am using Spring 3.1 MVC with a JavaConfig setup. I use Maven, and I can build, run and deploy my current webapp with no problems.
First, here's some code and config
My controller
@Controller
@RequestMapping("/rest")
public class StubRestController {
@Inject
private TestData testData;
@RequestMapping(value=Endpoints.GET_RESOURCES, method=RequestMethod.GET, produces="application/json")
@ResponseStatus(HttpStatus.OK)
public @ResponseBody JSONObject getResources(@RequestParam(value="q") String query, @RequestParam int indexFrom, @RequestParam int indexTo) throws JSONException {
return makeRequest(query, indexFrom, indexTo, testData.getResources());
}
@RequestMapping(value=Endpoints.GET_LOCATIONS, method=RequestMethod.GET, produces="application/json")
@ResponseStatus(HttpStatus.OK)
public @ResponseBody JSONObject getLocations(@RequestParam(value="q") String query, @RequestParam int indexFrom, @RequestParam int indexTo) throws JSONException {
return makeRequest(query, indexFrom, indexTo, testData.getLocations());
}
private JSONObject makeRequest(String query, int indexFrom, int indexTo, String[] data) throws JSONException {
int count = 0;
final JSONArray resources = new JSONArray();
for (final String resourceName: data) {
final String lowerColor = resourceName.toLowerCase();
final int has = lowerColor.indexOf(query.toLowerCase());
if (!query.isEmpty() && (query.equals("*") || has >= 0)) {
final JSONObject resource = new JSONObject();
resource.put("DisplayName", resourceName);
resource.put("Value", resourceName); // shouldn't this be a unique id? e.g., resourceid
resources.put(resource);
count++;
}
}
final JSONArray partial = new JSONArray();
if (resources.length() > 0) {
final int end = count - 1 > indexTo ? indexTo : count - 1;
for (int i = indexFrom; i <= end; i++) {
partial.put(resources.get(i));
}
}
final JSONObject result = new JSONObject();
result.put("TotalSize", count);
result.put("Options", partial);
return result;
}
@ExceptionHandler(JSONException.class)
@ResponseStatus(value = HttpStatus.NOT_FOUND, reason="No data found matching criteria")
public void notFound() { }
}
My test
@ContextConfiguration(classes={ RestClientContext.class }, loader=AnnotationConfigContextLoader.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class StubRestClientITCase {
private static final String SCHEME = "http";
private static final String HOST = "localhost";
private static final int PORT = 8081;
// requests must match <url-pattern> in order to be handled by DispatcherServlet
private static final String ENDPOINT_PREFIX = "spp-emkt-mui-experimental/EMKT/services/rest";
private static final String QUERY_KEY = "q";
private static final String INDEX_FROM_KEY = "indexFrom";
private static final String INDEX_TO_KEY = "indexTo";
private static final String TOTAL_SIZE_KEY = "TotalSize";
private Logger log = LoggerFactory.getLogger(StubRestClientITCase.class);
@Inject
RestTemplate restTemplate;
@Test
public void testGetResources() {
// Case 1: Discover all resources using * (asterisk), first 25
final URI uri = buildUri("*", 0, 24, Endpoints.GET_RESOURCES);
final HttpEntity<JSONObject> response = obtainResponse(uri);
try {
Assert.assertTrue(response.hasBody());
Assert.assertEquals(25, Integer.parseInt(response.getBody().getString(TOTAL_SIZE_KEY)));
} catch (final JSONException je) {
fail("Could not obtain \"" + TOTAL_SIZE_KEY + "\" from JSON payload for getResources().\n" + je.getMessage());
}
}
private URI buildUri(String query, int indexFrom, int indexTo, String endPointUrl) {
final UriComponents uriComponents =
UriComponentsBuilder.newInstance()
.scheme(SCHEME).host(HOST).port(PORT).path(ENDPOINT_PREFIX + endPointUrl)
.queryParam(QUERY_KEY, query)
.queryParam(INDEX_FROM_KEY, indexFrom)
.queryParam(INDEX_TO_KEY, indexTo)
.build()
.encode();
final URI uri = uriComponents.toUri();
return uri;
}
private HttpEntity<JSONObject> obtainResponse(URI uri) {
final HttpHeaders requestHeaders = new HttpHeaders();
requestHeaders.setAccept(Arrays.asList(new MediaType[] {MediaType.APPLICATION_JSON}));
requestHeaders.setAcceptCharset(Arrays.asList(new Charset[] {Charset.forName("UTF-8")}));
final HttpEntity<?> requestEntity = new HttpEntity(requestHeaders);
final HttpEntity<JSONObject> response = restTemplate.exchange(uri, HttpMethod.GET, requestEntity, JSONObject.class);
return response;
}
}
My web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<context-param>
<param-name>contextClass</param-name>
<param-value>
org.springframework.web.context.support.AnnotationConfigWebApplicationContext
</param-value>
</context-param>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>com.spp.mui.gwt.server.config.WebAppContextExperimental</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<listener>
<listener-class>
org.springframework.web.context.request.RequestContextListener
</listener-class>
</listener>
<filter>
<filter-name>characterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>characterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<servlet>
<servlet-name>gwt</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextClass</param-name>
<param-value>
org.springframework.web.context.support.AnnotationConfigWebApplicationContext
</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>gwt</servlet-name>
<url-pattern>/EMKT/service/*</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
<session-config>
<session-timeout>0</session-timeout>
</session-config>
</web-app>
I can see my Controller methods mapped on server startup...
17:48:58,651 INFO [RequestMappingHandlerMapping] Mapped "{[/rest/resources],methods=[GET],params=[],headers=[],consumes=[],produces=[application/json],custom=[]}"
17:48:58,651 INFO [RequestMappingHandlerMapping] Mapped "{[/rest/locations],methods=[GET],params=[],headers=[],consumes=[],produces=[application/json],custom=[]}"
When the test runs, I get...
17:49:00,617 DEBUG [AutowiredAnnotationBeanPostProcessor] Autowiring by type from bean name 'com.spp.mui.gwt.server.controller.stub.StubRestClientITCase' to bean named 'restTemplate'
17:49:00,648 DEBUG [RestTemplate] Created GET request for "http://localhost:8080/EMKT/services/rest/resources?q=*&indexFrom=0&indexTo=24"
17:49:00,680 DEBUG [RestTemplate] Setting request Accept header to [application/json]
17:49:00,742 WARN [RestTemplate] GET request for "http://localhost:8080/EMKT/services/rest/resources?q=*&indexFrom=0&indexTo=24" resulted in 404 (Not Found); invoking error handler
Finally...
Tests in error:
testGetResources(com.spp.mui.gwt.server.controller.stub.StubRestClientITCase): 404 Not Found
What could be the matter? Thinking it has something to do with url-pattern in servlet-mapping. I have tried to follow Spring docs on setup but have been unsuccessful. Note: I am inflexible on that though as I have a GWT-SL setup to map RPC service interfaces.
UPDATE #1
If I try using
curl --verbose -H "Accept: application/json" "localhost:8081/spp-emkt-mui-experimental/EMKT/service/rest/resources?q=*&indexFrom=0&indexTo=24"
against a Tomcat 6 deployment (using cargo plugin), I get a different result:
* About to connect() to localhost port 8081 (#0)
* Trying 127.0.0.1... connected
* Connected to localhost (127.0.0.1) port 8081 (#0)
> GET /spp-emkt-mui-experimental/EMKT/service/rest/resources?q=*&indexFrom=0&indexTo=24 HTTP/1.1
> User-Agent: curl/7.21.1 (i686-pc-mingw32) libcurl/7.21.1 OpenSSL/0.9.8r zlib/1.2.3
> Host: localhost:8081
> Accept: */*
>
< HTTP/1.1 406 Not Acceptable
< Server: Apache-Coyote/1.1
< Content-Type: text/html;charset=utf-8
< Content-Length: 1070
< Date: Tue, 28 Feb 2012 05:00:26 GMT
Which seems to indicate I'm not sending appropriate header info in my request. Hmmmmmm.
UPDATE #2
The test as written returns a 404. And the curl a 406.
I've scoured so many seemingly related posts and tried so many things that I'm wondering if Spring MVC, GWT-RPC and GWT-SL are able to be combined in the same container. Considering a move to RestyGWT with server-side being Spring MVC. Comments?
Upvotes: 3
Views: 17368
Reputation: 795
This was definitely a learning exercise on my part. I had to clear a number of hurdles with respect to configuration so that GWT RPC and Spring MVC play nicely together.
I now have a working test. The "faux pas" on my part was trying to return an @ResponseBody annotated JSONObject. Don't do this! I crafted a custom DTO and annotated with Jackson annotations like @JsonProperty, @JsonSerialize and @JsonDeserialize to get the I/O the way I wanted it.
I am presenting my working test and configuration updates below in case anyone is interested...
TEST
@ContextConfiguration(classes={ RestClientContext.class }, loader=AnnotationConfigContextLoader.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class SuggestionsClientITCase {
private static final String SCHEME = "http";
private static final String HOST = "localhost";
private static final int PORT = 8080;
private static final String QUERY_KEY = "q";
private static final String INDEX_FROM_KEY = "indexFrom";
private static final String INDEX_TO_KEY = "indexTo";;
private Logger log = LoggerFactory.getLogger(SuggestionsClientITCase.class);
@Inject
RestTemplate restTemplate;
@Test
public void testGetResources() {
// Case 1: Discover all resources using * (asterisk), first 25
// -- the total # of resources in TestData is 250
// -- the total # of options returned should be constrained by indexTo - indexFrom = 25
happyPathAssertions(Endpoints.GET_RESOURCES, "*", 0, 24, 250, 25);
}
@Test
public void testGetLocations() {
// Case 1: Discover all resources using * (asterisk), first 25
// -- the total # of locations in TestData is 4316
// -- the total # of options returned should be constrained by indexTo - indexFrom = 25
happyPathAssertions(Endpoints.GET_LOCATIONS, "*", 0, 24, 4316, 25);
}
private void happyPathAssertions(String endpointUrl, String query, int indexFrom, int indexTo, int expectedTotal, int expectedOptionsPerPage) {
final URI uri = buildUri(query, indexFrom, indexTo, endpointUrl);
final HttpEntity<SuggestionsPayload> response = obtainResponse(uri);
Assert.assertTrue(response.hasBody());
Assert.assertEquals(expectedTotal, response.getBody().getTotalSize());
Assert.assertEquals(expectedOptionsPerPage, response.getBody().getOptions().size());
}
private URI buildUri(String query, int indexFrom, int indexTo, String endPointUrl) {
final UriComponents uriComponents =
UriComponentsBuilder.newInstance()
.scheme(SCHEME).host(HOST).port(PORT).path(Endpoints.REST_PREFIX + endPointUrl)
.queryParam(QUERY_KEY, query)
.queryParam(INDEX_FROM_KEY, indexFrom)
.queryParam(INDEX_TO_KEY, indexTo)
.build()
.encode();
final URI uri = uriComponents.toUri();
return uri;
}
private HttpEntity<SuggestionsPayload> obtainResponse(URI uri) {
final HttpHeaders requestHeaders = new HttpHeaders();
requestHeaders.setAccept(Arrays.asList(new MediaType[] {MediaType.APPLICATION_JSON}));
requestHeaders.setAcceptCharset(Arrays.asList(new Charset[] {Charset.forName("UTF-8")}));
final HttpEntity<?> requestEntity = new HttpEntity(requestHeaders);
final HttpEntity<SuggestionsPayload> response = restTemplate.exchange(uri, HttpMethod.GET, requestEntity, SuggestionsPayload.class);
return response;
}
}
CONTROLLER
@Controller
@RequestMapping("/" + Endpoints.REST_PREFIX)
public class StubSuggestionsController {
@Inject
private TestData testData;
@RequestMapping(value=Endpoints.GET_RESOURCES, method=RequestMethod.GET, produces = {MediaType.APPLICATION_JSON_VALUE})
@ResponseStatus(HttpStatus.OK)
public @ResponseBody SuggestionsPayload getResources(@RequestParam(value="q") String query, @RequestParam int indexFrom, @RequestParam int indexTo) {
return makeRequest(query, indexFrom, indexTo, testData.getResources());
}
@RequestMapping(value=Endpoints.GET_LOCATIONS, method=RequestMethod.GET, produces = {MediaType.APPLICATION_JSON_VALUE})
@ResponseStatus(HttpStatus.OK)
public @ResponseBody SuggestionsPayload getLocations(@RequestParam(value="q") String query, @RequestParam int indexFrom, @RequestParam int indexTo) {
return makeRequest(query, indexFrom, indexTo, testData.getLocations());
}
private SuggestionsPayload makeRequest(String query, int indexFrom, int indexTo, String[] data) {
int count = 0;
final List<SuggestionOption> possibilities = new ArrayList<SuggestionOption>();
for (final String resourceName: data) {
final String key = resourceName.toLowerCase();
final int has = key.indexOf(query.toLowerCase());
if (!query.isEmpty() && (query.equals("*") || has >= 0)) {
final SuggestionOption possibility = new SuggestionOption();
possibility.setDisplayName(resourceName);
possibility.setValue(resourceName); // shouldn't this be a unique id? e.g., resourceid
possibilities.add(possibility);
count++;
}
}
final List<SuggestionOption> options = new ArrayList<SuggestionOption>();
if (possibilities.size() > 0) {
final int end = count - 1 > indexTo ? indexTo : count - 1;
for (int i = indexFrom; i <= end; i++) {
options.add(possibilities.get(i));
}
// sort the suggestions by display name
Collections.sort(options, new Comparator<SuggestionOption>() {
@Override
public int compare(SuggestionOption o1, SuggestionOption o2) {
final int comparison = o1.getDisplayName().compareTo(o2.getDisplayName());
return comparison;
}
});
}
final SuggestionsPayload result = new SuggestionsPayload();
result.setTotalSize(count);
result.setOptions(options);
return result;
}
}
PAYLOAD
public class SuggestionsPayload {
@JsonProperty("TotalSize")
@JsonSerialize @JsonDeserialize
private int totalSize;
@JsonProperty("Options")
@JsonSerialize @JsonDeserialize
private List<SuggestionOption> options;
public int getTotalSize() {
return totalSize;
}
public void setTotalSize(int totalSize) {
this.totalSize = totalSize;
}
public List<SuggestionOption> getOptions() {
return options;
}
public void setOptions(List<SuggestionOption> options) {
this.options = options;
}
public static class SuggestionOption {
@JsonProperty("Value")
@JsonSerialize @JsonDeserialize
private String value;
@JsonProperty("DisplayName")
@JsonSerialize @JsonDeserialize
private String displayName;
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public String getDisplayName() {
return displayName;
}
public void setDisplayName(String displayName) {
this.displayName = displayName;
}
}
}
WEB.XML
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<!-- Java-based annotation-driven Spring container definition -->
<context-param>
<param-name>contextClass</param-name>
<param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
</context-param>
<!-- Location of Java @Configuration classes that configure the components that makeup this application -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>com.spp.mui.gwt.server.config.WebAppContextExperimental</param-value>
</context-param>
<!-- Specifies the default mode of this application, to be activated if no other profile (or mode) is specified -->
<context-param>
<param-name>spring.profiles.default</param-name>
<param-value>standard</param-value>
</context-param>
<!-- Creates the Spring Container shared by all Servlets and Filters -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- Exposes request on current thread of execution, required for beans
employing <aop:scoped-proxy /> or e.g., @Scope(value="request", proxyMode=ScopedProxyMode.TARGET_CLASS) -->
<listener>
<listener-class>
org.springframework.web.context.request.RequestContextListener
</listener-class>
</listener>
<!-- Reads request input using UTF-8 encoding -->
<filter>
<filter-name>characterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>characterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- Enables support for DELETE and PUT request methods with web browser clients -->
<filter>
<filter-name>hiddenHttpMethodFilter</filter-name>
<filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>hiddenHttpMethodFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- Secures the application -->
<!--
<filter>
<filter-name>securityFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<init-param>
<param-name>targetBeanName</param-name>
<param-value>springSecurityFilterChain</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>securityFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
-->
<!-- Handles requests into the application -->
<servlet>
<servlet-name>appServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<!-- No explicit configuration file reference here: everything is configured in the root container for simplicity -->
<param-name>contextConfigLocation</param-name>
<param-value></param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>appServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>Application.html</welcome-file>
</welcome-file-list>
<session-config>
<session-timeout>0</session-timeout>
</session-config>
CONFIGURATION
@Configuration
@Import(value={ AopConfig.class, GwtServiceConfig.class, ComponentConfig.class, MvcConfig.class })
public class WebAppContextExperimental {
// context used for web application
}
and within my MvcConfig I had to make sure to add these (particularly the registry entries for GWT support so Spring's DispatcherServlet would allow requests for code gen'd resources)
// serve static resources
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// i.e., images, JS, and CSS
registry.addResourceHandler("/resources/**").addResourceLocations("/resources/");
// i.e., GWT module code-generated resources
registry.addResourceHandler("/*").addResourceLocations("/");
registry.addResourceHandler("/EMKT/**").addResourceLocations("/EMKT/");
}
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new MappingJacksonHttpMessageConverter());
}
Upvotes: 8