Spring MVC - Path Extension Content Negotiation Strategy

[Updated: Jul 18, 2017, Created: Jul 13, 2017]

In the last tutorial, we saw how content-negotiation works in Spring. In this example, we will utilize ServletPathExtensionContentNegotiationStrategy by using a path extension with the request. We are going to use XML media type. This strategy is enabled by default, we just need to use file extension to make use of it.

Example

Writing Controller

@Controller
@RequestMapping("user")
public class UserController {
  
  @RequestMapping(produces = MediaType.APPLICATION_XML_VALUE)
  @ResponseBody
  public User getUserById (@RequestParam("id") long userId) {
      //creating dummy user
      User user = new User();
      user.setId(userId);
      user.setName("joe");
      user.setEmailAddress("joe@example.com");
      return user;
  }
  
  @RequestMapping
  @ResponseBody
  public String getUserStringById (@RequestParam("id") long userId) {
      return "joe, id: "+userId;
  }
}
@XmlRootElement
public class User implements Serializable {
  private Long id;
  private String name;
  private String password;
  private String emailAddress;
    .............
}

Writing JUnit tests

In this test, we are not yet going to use xml extension.

@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration(classes = MyWebConfig.class)
public class UserTests {
  @Autowired
  private WebApplicationContext wac;
  private MockMvc mockMvc;
    .............
  @Test
  public void testUserRequest () throws Exception {
      
      MockHttpServletRequestBuilder builder =
                MockMvcRequestBuilders.get("/user")
                                      .param("id", "100");
      
      this.mockMvc.perform(builder)
                  .andExpect(MockMvcResultMatchers.status()
                                                  .isOk())
                  .andDo(MockMvcResultHandlers.print());
  }
    .............
}

Output

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /user
       Parameters = {id=[100]}
          Headers = {}

Handler:
             Type = com.logicbig.example.UserController
           Method = public java.lang.String com.logicbig.example.UserController.getUserStringById(long)

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = {Content-Type=[text/plain;charset=ISO-8859-1], Content-Length=[12]}
     Content type = text/plain;charset=ISO-8859-1
             Body = joe, id: 100
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

The test request maps to the handler which returns String.

Now let's use xml file extension with the request:

@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration(classes = MyWebConfig.class)
public class SuperUserTests {
  @Autowired
  private WebApplicationContext wac;
  private MockMvc mockMvc;
    .............
  @Test
  public void testSuperUserWithExtension () throws Exception {
      
      MockHttpServletRequestBuilder builder =
                MockMvcRequestBuilders.get("/super-user.xml")
                                      .param("id", "200");
      
      this.mockMvc.perform(builder)
                  .andExpect(MockMvcResultMatchers.status()
                                                  .isOk())
                  .andDo(MockMvcResultHandlers.print());
  }
}

Running the test

 mvn -q test -Dtest=SuperUserTests#testSuperUserWithExtension

Note that, we still did not specify 'Accept' header with the test request.

Output

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /user.xml
       Parameters = {id=[100]}
          Headers = {}

Handler:
             Type = com.logicbig.example.UserController
           Method = public com.logicbig.example.User com.logicbig.example.UserController.getUserById(long)

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = {Content-Type=[application/xml]}
     Content type = application/xml
             Body = <?xml version="1.0" encoding="UTF-8" standalone="yes"?><user><emailAddress>joe@example.com</emailAddress><id>100</id><name>joe</name></user>
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

This time we got xml response, even though we did not specify 'Accept' header with the request. The extension xml with the request caused it to map to the handler which produces xml response.

Specifying the extension with Controller Mapping

Now let's use the path extension with @RequestMapping without 'produces' element.

@Controller
@RequestMapping("super-user.xml")
public class SuperUserController {
  
  @RequestMapping
  @ResponseBody
  public User getUserById (@RequestParam("id") long userId) {
      User user = new User();
      user.setId(userId);
      user.setName("tim");
      user.setEmailAddress("tim@example.com");
      return user;
  }
}
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration(classes = MyWebConfig.class)
public class SuperUserTests {
  @Autowired
  private WebApplicationContext wac;
  private MockMvc mockMvc;
    .............
  @Test
  public void testSuperUserWithoutExtension () throws Exception {
      
      MockHttpServletRequestBuilder builder =
                MockMvcRequestBuilders.get("/super-user")
                                      .param("id", "200");
      
      this.mockMvc.perform(builder)
                  .andExpect(MockMvcResultMatchers.status()
                                                  .isOk())
                  .andDo(MockMvcResultHandlers.print());
  }
    .............
}

Running the test

 mvn -q test -Dtest=SuperUserTests#testSuperUserWithoutExtension

Output

The test fails:

java.lang.AssertionError: Status
Expected :200
Actual   :404

Let's use the file extension with the request:

@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration(classes = MyWebConfig.class)
public class UserTests {
  @Autowired
  private WebApplicationContext wac;
  private MockMvc mockMvc;
    .............
  @Test
  public void testUserRequestWithXmlExtension () throws Exception {
      
      MockHttpServletRequestBuilder builder =
                MockMvcRequestBuilders.get("/user.xml")
                                      .param("id", "100");
      
      this.mockMvc.perform(builder)
                  .andExpect(MockMvcResultMatchers.status()
                                                  .isOk())
                  .andDo(MockMvcResultHandlers.print());
  }
    .............
}

Running the test

 mvn -q test -Dtest=UserTests#testUserRequestWithXmlExtension

Output

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /super-user.xml
       Parameters = {id=[200]}
          Headers = {}

Handler:
             Type = com.logicbig.example.SuperUserController
           Method = public com.logicbig.example.User com.logicbig.example.SuperUserController.getUserById(long)

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = {Content-Type=[application/xml]}
     Content type = application/xml
             Body = <?xml version="1.0" encoding="UTF-8" standalone="yes"?><user><emailAddress>tim@example.com</emailAddress><id>200</id><name>tim</name></user>
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

Conclusion

If we don't specify 'Accept' header with the request, instead use the file extension (xml) then the request will map to the handler which 'produces' XML response. That happens because of Path extension content-negotiation Strategy. Also, on the handler method, using file extension with @RequestMapping#path instead of specifying 'produces' element will also work and Path extension strategy will be used. In both cases, we need to use file extension with the request URI (e.g. /user.xml).

Note that, path extension strategy works for the extensions json, xml, rss, atom by default if corresponding dependencies are in the classpath.

Example Project

Dependencies and Technologies Used :

  • spring-webmvc 4.3.9.RELEASE: Spring Web MVC.
  • spring-test 4.3.9.RELEASE: Spring TestContext Framework.
  • javax.servlet-api 3.1.0 Java Servlet API
  • junit 4.12: JUnit is a unit testing framework for Java, created by Erich Gamma and Kent Beck..
  • JDK 1.8
  • Maven 3.3.9

Path Extension Content-Negotiation Strategy Example Select All Download
  • content-negotiation-path-extension-strategy
    • src
      • main
        • java
          • com
            • logicbig
              • example
      • test
        • java
          • com
            • logicbig
              • example

See Also