26

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.

1
  • Found the issue. I was trying to process the response data when it was already in the correct format. The above code works when the site simply hrefs to getFullData.html. In short, nothing to see here...move along :):) Commented Mar 28, 2012 at 19:25

2 Answers 2

31

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;
       }
    }
    
Sign up to request clarification or add additional context in comments.

6 Comments

Shouldn't the @RequestMapping tag have a produces = "text/csv" rather than a consumes? It is producing data to the client not consuming it.
I also needed to configure a bean for the CsvMessageConverter, e.g. <annotation-driven> <message-converters> <beans:bean class="blah.blah.blah.export.CsvMessageConverter" /> </message-converters> </annotation-driven>
googlers: the CsVWriter needs to be closed in the message converter - writer.close(), also, you may want to add a header row to the file in the writeInternal method. For example, before iterating over the list of CompositeRequirement objects, add a line: writer.write("col1header,col2header,col3header");
for CsvMessageConverter: import au.com.bytecode.opencsv.CSVWriter; 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.OutputStream; import java.io.OutputStreamWriter; import java.nio.charset.Charset;
I have posted a complete answer, with some reflection to respond to any object passed.
|
8

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");
    }

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.