Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Configuring Spring MVC controller to send file to client

Tags:

I think my scenario is pretty common. I have a database and I want my Spring MVC app to accept a request in the controller, invoke the DB service to get data and send that data to the client as a CSV file. I'm using the JavaCSV library found here to assist in the process: http://sourceforge.net/projects/javacsv/

I've found several examples of people doing similar things and cobbled together something that looks correct-ish. When I hit the method, though, nothing is really happening.

I thought writing the data to the HttpServletResponse's outputStream would be sufficient, but apparently, I'm missing something.

Here's my controller code:

@RequestMapping(value="/getFullData.html", method = RequestMethod.GET) public void getFullData(HttpSession session, HttpServletRequest request, HttpServletResponse response) throws IOException{     List<CompositeRequirement> allRecords = compReqServ.getFullDataSet((String)session.getAttribute("currentProject"));      response.setContentType("data:text/csv;charset=utf-8");      response.setHeader("Content-Disposition","attachment; filename=\yourData.csv\"");     OutputStream resOs= response.getOutputStream();       OutputStream buffOs= new BufferedOutputStream(resOs);        OutputStreamWriter outputwriter = new OutputStreamWriter(buffOs);        CsvWriter writer = new CsvWriter(outputwriter, '\u0009');       for(int i=1;i <allRecords.size();i++){                       CompositeRequirement aReq=allRecords.get(i);           writer.write(aReq.toString());       }          outputwriter.flush();        outputwriter.close();  }; 

What step am I missing here? Basically, the net effect is... nothing. I would have thought setting the header and content type would cause my browser to pick up on the response and trigger a file download action.

like image 578
Raevik Avatar asked Mar 28 '12 18:03

Raevik


2 Answers

It seems to be because your Content-type is set incorrectly, it should be response.setContentType("text/csv;charset=utf-8") instead of response.setContentType("data:text/csv;charset=utf-8").

Additionally, if you are using Spring 3, you should probably use a @ResponseBody HttpMessageConverter for code reuse. For example:

  • In the controller:

    @RequestMapping(value = "/getFullData2.html", method = RequestMethod.GET, consumes = "text/csv") @ResponseBody // indicate to use a compatible HttpMessageConverter public CsvResponse getFullData(HttpSession session) throws IOException {       List<CompositeRequirement> allRecords = compReqServ.getFullDataSet((String) session.getAttribute("currentProject"));       return new CsvResponse(allRecords, "yourData.csv"); } 
  • plus a simple HttpMessageConverter:

    public class CsvMessageConverter extends AbstractHttpMessageConverter<CsvResponse> {    public static final MediaType MEDIA_TYPE = new MediaType("text", "csv", Charset.forName("utf-8"));    public CsvMessageConverter() {        super(MEDIA_TYPE);    }     protected boolean supports(Class<?> clazz) {        return CsvResponse.class.equals(clazz);    }     protected void writeInternal(CsvResponse response, HttpOutputMessage output) throws IOException, HttpMessageNotWritableException {        output.getHeaders().setContentType(MEDIA_TYPE);        output.getHeaders().set("Content-Disposition", "attachment; filename=\"" + response.getFilename() + "\"");        OutputStream out = output.getBody();        CsvWriter writer = new CsvWriter(new OutputStreamWriter(out), '\u0009');        List<CompositeRequirement> allRecords = response.getRecords();        for (int i = 1; i < allRecords.size(); i++) {             CompositeRequirement aReq = allRecords.get(i);             writer.write(aReq.toString());        }        writer.close();    } } 
  • and a simple object to bind everything together:

    public class CsvResponse {        private final String filename;    private final List<CompositeRequirement> records;     public CsvResponse(List<CompositeRequirement> records, String filename) {        this.records = records;        this.filename = filename;    }    public String getFilename() {        return filename;    }    public List<CompositeRequirement> getRecords() {        return records;    } } 
like image 189
Pierre Avatar answered Oct 13 '22 19:10

Pierre


Based on Pierre answer, i did a converter. Here is the full code, that works with any Object passed:

TsvMessageConverter.java

public class TsvMessageConverter extends AbstractHttpMessageConverter<TsvResponse> {      public static final MediaType MEDIA_TYPE = new MediaType("text", "tsv", Charset.forName("utf-8"));     private static final Logger logger = LoggerFactory.getLogger(TsvMessageConverter.class);      public TsvMessageConverter() {         super(MEDIA_TYPE);     }      protected boolean supports(Class<?> clazz) {         return TsvResponse.class.equals(clazz);     }      @Override     protected TsvResponse readInternal(Class<? extends TsvResponse> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {         return null;     }      protected void writeInternal(TsvResponse tsvResponse, HttpOutputMessage output) throws IOException, HttpMessageNotWritableException {         output.getHeaders().setContentType(MEDIA_TYPE);         output.getHeaders().set("Content-Disposition", "attachment; filename=\"" + tsvResponse.getFilename() + "\"");         final OutputStream out = output.getBody();          writeColumnTitles(tsvResponse, out);          if (tsvResponse.getRecords() != null && tsvResponse.getRecords().size() != 0) {             writeRecords(tsvResponse, out);         }          out.close();     }      private void writeRecords(TsvResponse response, OutputStream out) throws IOException {         List<String> getters = getObjectGetters(response);         for (final Object record : response.getRecords()) {             for (String getter : getters) {                 try {                     Method method = ReflectionUtils.findMethod(record.getClass(), getter);                     out.write(method.invoke(record).toString().getBytes(Charset.forName("utf-8")));                     out.write('\t');                 } catch (IllegalAccessException | InvocationTargetException e) {                     logger.error("Erro ao transformar em CSV", e);                 }             }             out.write('\n');         }     }      private List<String> getObjectGetters(TsvResponse response) {         List<String> getters = new ArrayList<>();         for (Method method : ReflectionUtils.getAllDeclaredMethods(response.getRecords().get(0).getClass())) {             String methodName = method.getName();             if (methodName.startsWith("get") && !methodName.equals("getClass")) {                 getters.add(methodName);             }         }         sort(getters);         return getters;     }      private void writeColumnTitles(TsvResponse response, OutputStream out) throws IOException {         for (String columnTitle : response.getColumnTitles()) {             out.write(columnTitle.getBytes());             out.write('\t');         }         out.write('\n');     } } 

TsvResponse.java

public class TsvResponse {    private final String filename;    private final List records;     private final String[] columnTitles;     public TsvResponse(List records, String filename, String ... columnTitles) {        this.records = records;        this.filename = filename;        this.columnTitles = columnTitles;    }    public String getFilename() {        return filename;    }    public List getRecords() {        return records;    }      public String[] getColumnTitles() {         return columnTitles;     } } 

And on SpringContext.xml add the following:

<mvc:annotation-driven>         <mvc:message-converters register-defaults="true">             <bean class="com.mypackage.TsvMessageConverter"/>         </mvc:message-converters>     </mvc:annotation-driven> 

So, you can use on your controller like this:

@RequestMapping(value="/tsv", method= RequestMethod.GET, produces = "text/tsv")     @ResponseBody     public TsvResponse tsv() {         return new TsvResponse(myListOfPojos, "fileName.tsv",                 "Name", "Email", "Phone", "Mobile");     } 
like image 35
renanleandrof Avatar answered Oct 13 '22 19:10

renanleandrof