Spring MVC - HttpMessageConverter for CSV conversion

[Updated: Sep 2, 2017, Created: May 24, 2017]

In this tutorial, we will learn how to create and register a custom HttpMessageConverter. The converter will convert the request csv body data to user defined object. The converter will also convert the response user object to csv data. We are going to use OpenCSV parser library for Java object to CSV (and vice-versa) conversion.

Converting request body CSV to Java object list

Creating the Controller

@Controller
public class ExampleController {

  @RequestMapping(
            value = "/newEmployee",
            consumes = "text/csv",
            produces = MediaType.TEXT_PLAIN_VALUE,
            method = RequestMethod.POST)
  @ResponseBody
  @ResponseStatus(HttpStatus.OK)
  public String handleRequest (@RequestBody EmployeeList employeeList) {
      System.out.printf("In handleRequest method, employeeList: %s%n", employeeList.getList());
      String s = String.format("size: " + employeeList.getList().size());
      System.out.println(s);
      return s;
  }
    .............
}
public class EmployeeList extends ListParam<Employee> {
}
public class ListParam<T> {

  private List<T> list;

  public List<T> getList () {
      return list;
  }
  
  public void setList (List<T> list) {
      this.list = list;
  }

}
package com.logicbig.example;

import com.opencsv.bean.CsvBindByName;

public class Employee {
  @CsvBindByName
  private String id;
  @CsvBindByName
  private String name;
  @CsvBindByName
  private String phoneNumber;
    .............
}

Creating the Converter

package com.logicbig.example;


import com.opencsv.CSVReader;
import com.opencsv.CSVWriter;
import com.opencsv.bean.CsvToBean;
import com.opencsv.bean.HeaderColumnNameMappingStrategy;
import com.opencsv.bean.StatefulBeanToCsv;
import com.opencsv.bean.StatefulBeanToCsvBuilder;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.AbstractHttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;

import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.List;

public class CsvHttpMessageConverter<T, L extends ListParam<T>>
        extends AbstractHttpMessageConverter<L> {
  
  public CsvHttpMessageConverter () {
      super(new MediaType("text", "csv"));
  }
  
  @Override
  protected boolean supports (Class<?> clazz) {
      return ListParam.class.isAssignableFrom(clazz);
  }
  
  @Override
  protected L readInternal (Class<? extends L> clazz, HttpInputMessage inputMessage)
            throws IOException, HttpMessageNotReadableException {
      HeaderColumnNameMappingStrategy<T> strategy = new HeaderColumnNameMappingStrategy<>();
      Class<T> t = toBeanType(clazz.getGenericSuperclass());
      strategy.setType(t);
      
      CSVReader csv = new CSVReader(new InputStreamReader(inputMessage.getBody()));
      CsvToBean<T> csvToBean = new CsvToBean<>();
      List<T> beanList = csvToBean.parse(strategy, csv);
      try {
          L l = clazz.newInstance();
          l.setList(beanList);
          return l;
      } catch (Exception e) {
          throw new RuntimeException(e);
      }
  }
  
  @SuppressWarnings("unchecked")
  @Override
  protected void writeInternal (L l, HttpOutputMessage outputMessage)
            throws IOException, HttpMessageNotWritableException {
      
      HeaderColumnNameMappingStrategy<T> strategy = new HeaderColumnNameMappingStrategy<>();
      strategy.setType(toBeanType(l.getClass().getGenericSuperclass()));
      
      OutputStreamWriter outputStream = new OutputStreamWriter(outputMessage.getBody());
      StatefulBeanToCsv<T> beanToCsv =
                new StatefulBeanToCsvBuilder(outputStream)
                          .withQuotechar(CSVWriter.NO_QUOTE_CHARACTER)
                          .withMappingStrategy(strategy)
                          .build();
      try {
          beanToCsv.write(l.getList());
          outputStream.close();
      } catch (Exception e) {
          throw new RuntimeException(e);
      }
  }
  
  @SuppressWarnings("unchecked")
  private Class<T> toBeanType (Type type) {
      return (Class<T>) ((ParameterizedType) type).getActualTypeArguments()[0];
  }
}

Registering the Converter

@EnableWebMvc
@ComponentScan("com.logicbig.example")
public class AppConfig extends WebMvcConfigurerAdapter {
 
  
  @Override
  public void extendMessageConverters (List<HttpMessageConverter<?>> converters) {
      converters.add(new CsvHttpMessageConverter<>());
  }
}

Writing JUnit test

@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration(classes = AppConfig.class)
public class ControllerTest {
  @Autowired
  private WebApplicationContext wac;
  private MockMvc mockMvc;
    .............
  @Test
  public void testConsumerController () throws Exception {
      MockHttpServletRequestBuilder builder =
                MockMvcRequestBuilders.post("/newEmployee")
                                      .contentType("text/csv")
                                      .accept(MediaType.TEXT_PLAIN_VALUE)
                                      .content(getNewEmployeeListInCsv());
      this.mockMvc.perform(builder)
                  .andExpect(MockMvcResultMatchers.status()
                                                  .isOk())
                  .andExpect(MockMvcResultMatchers.content()
                                                  .string("size: 3"))
                  .andDo(MockMvcResultHandlers.print());
      ;
  }
    .............
  public String getNewEmployeeListInCsv () {
      return "id, name, phoneNumber\n1,Joe,123-212-3233\n2,Sara,132,232,3111\n" +
                "3,Mike,111-222-3333\n";
  }
}

Output


In handleRequest method, employeeList: [Employee{id='1', name='Joe', phoneNumber='123-212-3233'}, Employee{id='2', name='Sara', phoneNumber='132'}, Employee{id='3', name='Mike', phoneNumber='111-222-3333'}]
size: 3

MockHttpServletRequest:
HTTP Method = POST
Request URI = /newEmployee
Parameters = {}
Headers = {Content-Type=[text/csv], Accept=[text/plain]}

Handler:
Type = com.logicbig.example.ExampleController
Method = public java.lang.String com.logicbig.example.ExampleController.handleRequest(com.logicbig.example.EmployeeList)

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=[7]}
Content type = text/plain;charset=ISO-8859-1
Body = size: 3
Forwarded URL = null
Redirected URL = null
Cookies = []

Returning HTTP response in CSV

@Controller
public class ExampleController {
    .............
  @RequestMapping(
            value = "/employeeList",
            produces = "text/csv",
            method = RequestMethod.GET)
  @ResponseBody
  @ResponseStatus(HttpStatus.OK)
  public EmployeeList handleRequest2 () {
      List<Employee> list = Arrays.asList(
                new Employee("1", "Tina", "111-111-1111"),
                new Employee("2", "John", "222-222-2222")
      );
      EmployeeList employeeList = new EmployeeList();
      employeeList.setList(list);
      return employeeList;
  }
}
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration(classes = AppConfig.class)
public class ControllerTest {
  @Autowired
  private WebApplicationContext wac;
  private MockMvc mockMvc;
    .............
  @Test
  public void testProducerController () throws Exception {
      MockHttpServletRequestBuilder builder =
                MockMvcRequestBuilders.get("/employeeList")
                                      .accept("text/csv");
      this.mockMvc.perform(builder)
                  .andExpect(MockMvcResultMatchers.status()
                                                  .isOk())
                  .andDo(MockMvcResultHandlers.print());
  }
    .............
}

Output



MockHttpServletRequest:
HTTP Method = GET
Request URI = /employeeList
Parameters = {}
Headers = {Accept=[text/csv]}

Handler:
Type = com.logicbig.example.ExampleController
Method = public com.logicbig.example.EmployeeList com.logicbig.example.ExampleController.handleRequest2()

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/csv]}
Content type = text/csv
Body = ID,NAME,PHONENUMBER
1,Tina,111-111-1111
2,John,222-222-2222

Forwarded URL = null
Redirected URL = null
Cookies = []

Example Project

Dependencies and Technologies Used :

  • spring-webmvc 4.3.8.RELEASE: Spring Web MVC.
  • spring-test 4.3.8.RELEASE: Spring TestContext Framework.
  • javax.servlet-api 3.0.1 Java Servlet API
  • junit 4.12: JUnit is a unit testing framework for Java, created by Erich Gamma and Kent Beck.
  • opencsv 3.9: A simple library for reading and writing CSV in Java.
  • JDK 1.8
  • Maven 3.3.9

Custom HttpMessageConverter for CSV Conversion Select All Download
  • custom-message-converter
    • src
      • main
        • java
          • com
            • logicbig
              • example
      • test
        • java
          • com
            • logicbig
              • example

See Also