452

I have a requirement where I need to download a PDF from the website. The PDF needs to be generated within the code, which I thought would be a combination of freemarker and a PDF generation framework like iText. Any better way?

However, my main problem is how do I allow the user to download a file through a Spring Controller?

nbro
  • 15,395
  • 32
  • 113
  • 196
MilindaD
  • 7,533
  • 9
  • 43
  • 63
  • 4
    It worth mentioning that Spring Framework changed a lot since 2011, so you can do it in a reactive way as well - [here](https://stackoverflow.com/a/46766566/2004186) is an example – Krzysztof Skrzynecki Aug 09 '19 at 07:32
  • 1
    With later versions of spring, you just need to return the byte array with appropriate headers in ResponseEntity. Here is a full example: https://allaboutspringframework.com/java-spring-download-file-from-controller/ – Muhammad Asher Toqeer Mar 29 '21 at 13:36

15 Answers15

435
@RequestMapping(value = "/files/{file_name}", method = RequestMethod.GET)
public void getFile(
    @PathVariable("file_name") String fileName, 
    HttpServletResponse response) {
    try {
      // get your file as InputStream
      InputStream is = ...;
      // copy it to response's OutputStream
      org.apache.commons.io.IOUtils.copy(is, response.getOutputStream());
      response.flushBuffer();
    } catch (IOException ex) {
      log.info("Error writing file to output stream. Filename was '{}'", fileName, ex);
      throw new RuntimeException("IOError writing file to output stream");
    }

}

Generally speaking, when you have response.getOutputStream(), you can write anything there. You can pass this output stream as a place to put generated PDF to your generator. Also, if you know what file type you are sending, you can set

response.setContentType("application/pdf");
Jakub Kubrynski
  • 13,724
  • 6
  • 60
  • 85
Infeligo
  • 11,715
  • 8
  • 38
  • 50
  • 4
    This is pretty much what I was about to say, but you should probably also set the response type header to something appropriate for the file. – GaryF Apr 15 '11 at 07:01
  • 2
    Yep, just edited the post. I had various file types generated, so I left it to the browser to determine the content type of the file by its extension. – Infeligo Apr 15 '11 at 07:05
  • Forgot the flushBuffer, thanks to your post, I saw why mine wasn't working :-) – Jan Vladimir Mostert May 27 '12 at 08:10
  • Remember to close InputStream. – Matt Jul 03 '12 at 15:44
  • 42
    Any particular reason to use Apache's `IOUtils` instead of Spring's `FileCopyUtils`? – Powerlord Sep 18 '12 at 16:12
  • Normally yes you close your streams. In this case, though, I think not. http://stackoverflow.com/questions/1159168/should-one-call-close-on-httpservletresponse-getoutputstream-getwriter – Jason Feb 17 '13 at 22:44
  • Can I set the file name? I'm actually interested in the file extension like .pdf or .csv – Marsellus Wallace Sep 16 '13 at 22:17
  • @Gevorg look lobster1234 answer, you can set the header of the response and put any filename you want (the client will get). – рüффп Jan 08 '14 at 16:08
  • 4
    Here is a better solution: http://stackoverflow.com/questions/16652760/return-generated-pdf-using-spring-mvc – Dmytro Plekhotkin Jul 14 '15 at 15:16
  • @Infeligo, i use these code, but my chrome not show a download link for the file. how can i do then? – sendreams Feb 26 '16 at 13:17
  • Same here. Blank PDFs. – Ashley Feb 22 '17 at 21:10
  • Be careful with `response.flushBuffer()`! If your Controller is `@Transactional`, it will send a response even if the transaction should be rolled back, which is most likely not what you want. – Tom Dec 18 '17 at 10:47
  • 2
    @Powerlord Spring method closes the streams, Apache one does not. There are debates if the Servlet response output stream should be closed in the Controller code or by the Servlet container ... – Comencau Mar 07 '18 at 21:04
305

I was able to stream line this by using the built in support in Spring with it's ResourceHttpMessageConverter. This will set the content-length and content-type if it can determine the mime-type

@RequestMapping(value = "/files/{file_name}", method = RequestMethod.GET)
@ResponseBody
public FileSystemResource getFile(@PathVariable("file_name") String fileName) {
    return new FileSystemResource(myService.getFileFor(fileName)); 
}
Scott Carlson
  • 3,764
  • 1
  • 17
  • 11
  • 1
    I think this is the best & most clean way of handling file requests. – Ajay Sep 25 '12 at 23:58
  • 2
    absolutely the best response see also http://stackoverflow.com/questions/3526523/spring-mvc-pathvariable-getting-truncated for pathvariable being truncated (spring 3.0.7 in my case) – lrkwz Dec 04 '12 at 06:43
  • This method looks great but can't get it to work 100%. I don't want to open a question for this but on the off chance someone can help. I can't seem to set any details on what is returned such as the file name. Also on good old IE7 & IE8 the file extension seems to get lost somehow so it doesn't even know what file type is being downloaded. Does anyone have some useful example explaining the usage of this to a Spring noob. I'm currently just using HttpServletResponse but would like to use the above method if I could get to send back a little more details in the response. – Ruairi O'Brien Jun 04 '13 at 09:43
  • 14
    This works. But the file (.csv file) is displayed in the browser and not downloaded - how can I force the browser to download? – chzbrgla Jul 12 '13 at 08:26
  • 48
    You can add produces = MediaType.APPLICATION_OCTET_STREAM_VALUE to the @RequestMapping to force download – David Kago Sep 25 '13 at 17:33
  • 8
    Also You should add **** to messageConverters list () – Enginer Nov 29 '13 at 12:15
  • And can you set content-type with this method? – Kacper86 Jan 02 '14 at 08:35
  • ClassPathResource is a nice alternative to FileSystemResource. – JayL May 07 '14 at 22:52
  • 5
    Is there a way to set the `Content-Disposition` header with this way? – Ralph Apr 15 '15 at 12:54
  • 10
    I didn't have a need for that, but I think you could add HttpResponse as a parameter to the method, and then "response.setHeader("Content-Disposition", "attachment; filename=somefile.pdf");" – Scott Carlson Apr 16 '15 at 13:42
  • @ScottCarlson Can i create a custom class and extend from `FileSystemResource` and use that class type as return type of download controller? – Sajad Aug 08 '16 at 12:52
  • I like this way more than @Infeligo's one but if the file doesn't exist, this doesn't throw any exception and the client download hangs forever (until client times out). Is there something wrong in the builtin ResourceHttpMessageConverter? – Alex Vazquez Fente Jun 27 '17 at 15:43
  • To answer myself: it seems ResourceHttpMessageConverter silently discards Exceptions for some reasons related to SPR-12999 and SPR-13620. To get what I want (an error if the file does not exist or some other error happens trying to stream the file) I had to write a custom converter extending ResourceHttpMessageConverter (overwriting writeContent) and a custom web config extending WebMvcConfigurerAdapter (overwriting configureMessageConverters). – Alex Vazquez Fente Jun 28 '17 at 10:48
  • what is myService? – bharal Feb 10 '20 at 23:57
88

You should be able to write the file on the response directly. Something like

response.setContentType("application/pdf");      
response.setHeader("Content-Disposition", "attachment; filename=\"somefile.pdf\""); 

and then write the file as a binary stream on response.getOutputStream(). Remember to do response.flush() at the end and that should do it.

yglodt
  • 13,807
  • 14
  • 91
  • 127
lobster1234
  • 7,679
  • 26
  • 30
  • 8
    isn't the 'Spring' way to set the content type like this? `@RequestMapping(value = "/foo/bar", produces = "application/pdf")` – Black May 07 '14 at 05:44
  • 4
    @Francis what if your application downloads different file types? Lobster1234's answer enables you to dynamically set the content disposition. – Rose Nov 13 '15 at 02:29
  • 2
    that's true @Rose, but I believe it would be better practice to define different end-points per format – Black Nov 13 '15 at 03:10
  • 3
    I guess not, because it's not scalable. We are currently supporting a dozen types of resources. We might support more file types based on what users want to upload in that case we might end up with so many end points essentially doing the same thing. IMHO there has to be only one download end point and it handles multitude of file types. @Francis – Rose Nov 13 '15 at 05:12
  • 3
    it's absolutely "scalable", but we can agree to disagree whether it's the best practice – Black Nov 16 '15 at 22:42
83

With Spring 3.0 you can use the HttpEntity return object. If you use this, then your controller does not need a HttpServletResponse object, and therefore it is easier to test. Except this, this answer is relative equals to the one of Infeligo.

If the return value of your pdf framework is an byte array (read the second part of my answer for other return values) :

@RequestMapping(value = "/files/{fileName}", method = RequestMethod.GET)
public HttpEntity<byte[]> createPdf(
                 @PathVariable("fileName") String fileName) throws IOException {

    byte[] documentBody = this.pdfFramework.createPdf(filename);

    HttpHeaders header = new HttpHeaders();
    header.setContentType(MediaType.APPLICATION_PDF);
    header.set(HttpHeaders.CONTENT_DISPOSITION,
                   "attachment; filename=" + fileName.replace(" ", "_"));
    header.setContentLength(documentBody.length);

    return new HttpEntity<byte[]>(documentBody, header);
}

If the return type of your PDF Framework (documentBbody) is not already a byte array (and also no ByteArrayInputStream) then it would been wise NOT to make it a byte array first. Instead it is better to use:

example with FileSystemResource:

@RequestMapping(value = "/files/{fileName}", method = RequestMethod.GET)
public HttpEntity<byte[]> createPdf(
                 @PathVariable("fileName") String fileName) throws IOException {

    File document = this.pdfFramework.createPdf(filename);

    HttpHeaders header = new HttpHeaders();
    header.setContentType(MediaType.APPLICATION_PDF);
    header.set(HttpHeaders.CONTENT_DISPOSITION,
                   "attachment; filename=" + fileName.replace(" ", "_"));
    header.setContentLength(document.length());

    return new HttpEntity<byte[]>(new FileSystemResource(document),
                                  header);
}
herau
  • 1,466
  • 2
  • 18
  • 36
Ralph
  • 118,862
  • 56
  • 287
  • 383
  • 15
    -1 this will un-neccessarily load the whole file in memory can easily casue OutOfMemoryErrors. – Faisal Feroz Mar 07 '14 at 12:39
  • 2
    @FaisalFeroz: yes this is right, but the file document is anyway created in memory (see the question: "PDF needs to be generated within the code"). Anyway - what is your solution that overcome this problem? – Ralph Mar 07 '14 at 17:30
  • 1
    You may also use ResponseEntity which is a super of HttpEntity which allows you to specify the response http status code. Example: `return new ResponseEntity(documentBody, headers, HttpStatus.CREATED)` – Amr Mostafa Oct 08 '14 at 11:16
  • @Amr Mostafa: `ResponseEntity` is a subclass of `HttpEntity` (but I get it) on the other hand 201 CREATED is not what I would use when I return just an view to the data. (see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html for 201 CREATED) – Ralph Oct 08 '14 at 12:39
  • 1
    Is there a reason why you are replacing whitespaces with underscore in the filename? You can wrap it in quotes to send the actual name. – Alexandru Severin Oct 22 '18 at 13:15
  • @Alexandru Severin: you are right, I would use quotes nowadays. But when I remember right, there was some problems with Whitespaces (not in the http header) but with the browser or fielsystem (maybe just with some strange file system in our testing environment) – Ralph Oct 27 '18 at 18:12
81

If you:

  • Don't want to load the whole file into a byte[] before sending to the response;
  • Want/need to send/download it via InputStream;
  • Want to have full control of the Mime Type and file name sent;
  • Have other @ControllerAdvice picking up exceptions for you (or not).

The code below is what you need:

@RequestMapping(value = "/stuff/{stuffId}", method = RequestMethod.GET)
public ResponseEntity<FileSystemResource> downloadStuff(@PathVariable int stuffId)
                                                                      throws IOException {
    String fullPath = stuffService.figureOutFileNameFor(stuffId);
    File file = new File(fullPath);
    long fileLength = file.length(); // this is ok, but see note below

    HttpHeaders respHeaders = new HttpHeaders();
    respHeaders.setContentType("application/pdf");
    respHeaders.setContentLength(fileLength);
    respHeaders.setContentDispositionFormData("attachment", "fileNameIwant.pdf");

    return new ResponseEntity<FileSystemResource>(
        new FileSystemResource(file), respHeaders, HttpStatus.OK
    );
}

More on setContentLength(): First of all, the content-length header is optional per the HTTP 1.1 RFC. Still, if you can provide a value, it is better. To obtain such value, know that File#length() should be good enough in the general case, so it is a safe default choice.
In very specific scenarios, though, it can be slow, in which case you should have it stored previously (e.g. in the DB), not calculated on the fly. Slow scenarios include: if the file is very large, specially if it is on a remote system or something more elaborated like that - a database, maybe.



InputStreamResource

If your resource is not a file, e.g. you pick the data up from the DB, you should use InputStreamResource. Example:

InputStreamResource isr = new InputStreamResource(...);
return new ResponseEntity<InputStreamResource>(isr, respHeaders, HttpStatus.OK);
acdcjunior
  • 132,397
  • 37
  • 331
  • 304
  • You don't advise for the use of the FileSystemResource class ? – Stephane Nov 04 '14 at 22:39
  • Actually, I do believe it is OK to use the `FileSystemResource` there. It is even advisable *if your resource is a file*. In this sample, `FileSystemResource` can be used where `InputStreamResource` is. – acdcjunior Apr 27 '15 at 15:35
  • About the file length calculation part: If you are worried, don't be. `File#length()` should be good enough in the general case. I just mentioned it because [it does can be slow](http://stackoverflow.com/questions/116574/), specially if the file is in a remote system or something more elaborated like that - a database, maybe?. But only worry if it becomes a problem (or if you have hard evidence it will become one), not before. The main point is: you are making an effort to stream the file, if you have to preload all of it before, then the streaming ends up making no difference, eh? – acdcjunior May 13 '15 at 15:21
  • why does the above code not working for me ? It downloads 0 bytes file. I checked and made sure ByteArray & ResourceMessage converters are there. Am I missing something ? – coding_idiot Nov 02 '15 at 17:01
  • Why are you worrying about ByteArray & ResourceMessage converters? – acdcjunior Nov 02 '15 at 22:44
  • What would be the stream buffer size of `InputStreamResource`? – Sam YC May 23 '17 at 09:38
  • Specifying `respHeaders.setContentLength(12345678);` makes the download super slow I guess. – Hussain Nov 16 '18 at 07:01
  • `setContentLength()` [is optional](https://stackoverflow.com/q/27228343/355438) so it can be omitted – Ilya Serbis Dec 18 '20 at 10:57
  • what do you mean by - If your resource is not a file, then use `new InputStreamResource(new FileInputStream(file));` .. what is this file? – RamPrakash Aug 21 '21 at 00:25
  • @RamPrakash actually, by "if it is not a file" I mean if you want to send to the user some resource from other source than a file (e.g. you are streaming directly from a database). In this case, you should construct an `InputStreamResource` object. (The example I had left, which I now removed, had an example of constructing an `InputStreamResource` using a file -- which was just an example but you shouldn't do anyway, since if you do want a file you should construct a `FileSystemResource` directly instead). – acdcjunior Aug 26 '21 at 00:37
59

Do

  1. Return ResponseEntity<Resource> from a handler method
  2. Specify Content-Type
  3. Set Content-Disposition if necessary:
    1. filename
    2. type
      1. inline to force preview in a browser
      2. attachment to force a download

Example

@Controller
public class DownloadController {
    @GetMapping("/downloadPdf.pdf")
    // 1.
    public ResponseEntity<Resource> downloadPdf() {
        FileSystemResource resource = new FileSystemResource("/home/caco3/Downloads/JMC_Tutorial.pdf");
        // 2.
        MediaType mediaType = MediaTypeFactory
                .getMediaType(resource)
                .orElse(MediaType.APPLICATION_OCTET_STREAM);
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(mediaType);
        // 3
        ContentDisposition disposition = ContentDisposition
                // 3.2
                .inline() // or .attachment()
                // 3.1
                .filename(resource.getFilename())
                .build();
        headers.setContentDisposition(disposition);
        return new ResponseEntity<>(resource, headers, HttpStatus.OK);
    }
}

Explanation

Return ResponseEntity<Resource>

When you return a ResponseEntity<Resource>, the ResourceHttpMessageConverter writes file contents

Examples of Resource implementations:

Specify Content-Type explicitly:

Reason: see "FileSystemResource is returned with content type json" question

Options:

  • Hardcode the header
  • Use the MediaTypeFactory from Spring. The MediaTypeFactory maps Resource to MediaType using the /org/springframework/http/mime.types file
  • Use a third party library like Apache Tika

Set Content-Disposition if necessary:

About Content-Disposition header:

The first parameter in the HTTP context is either inline (default value, indicating it can be displayed inside the Web page, or as the Web page) or attachment (indicating it should be downloaded; most browsers presenting a 'Save as' dialog, prefilled with the value of the filename parameters if present).

Use ContentDisposition in application:

  • To preview a file in a browser:

    ContentDisposition disposition = ContentDisposition
            .inline()
            .filename(resource.getFilename())
            .build();
    
  • To force a download:

    ContentDisposition disposition = ContentDisposition
            .attachment()
            .filename(resource.getFilename())
            .build();
    

Use InputStreamResource carefully:

Specify Content-Length using the HttpHeaders#setContentLength method if:

  1. The length is known
  2. You use InputStreamResource

Reason: Spring won't write Content-Length for InputStreamResource because Spring can't determine the length of the resource. Here is a snippet of code from ResourceHttpMessageConverter:

@Override
protected Long getContentLength(Resource resource, @Nullable MediaType contentType) throws IOException {
    // Don't try to determine contentLength on InputStreamResource - cannot be read afterwards...
    // Note: custom InputStreamResource subclasses could provide a pre-calculated content length!
    if (InputStreamResource.class == resource.getClass()) {
        return null;
    }
    long contentLength = resource.contentLength();
    return (contentLength < 0 ? null : contentLength);
}

In other cases Spring sets the Content-Length:

~ $ curl -I localhost:8080/downloadPdf.pdf  | grep "Content-Length"
Content-Length: 7554270
Denis Zavedeev
  • 7,627
  • 4
  • 32
  • 53
24

This code is working fine to download a file automatically from spring controller on clicking a link on jsp.

@RequestMapping(value="/downloadLogFile")
public void getLogFile(HttpSession session,HttpServletResponse response) throws Exception {
    try {
        String filePathToBeServed = //complete file name with path;
        File fileToDownload = new File(filePathToBeServed);
        InputStream inputStream = new FileInputStream(fileToDownload);
        response.setContentType("application/force-download");
        response.setHeader("Content-Disposition", "attachment; filename="+fileName+".txt"); 
        IOUtils.copy(inputStream, response.getOutputStream());
        response.flushBuffer();
        inputStream.close();
    } catch (Exception e){
        LOGGER.debug("Request could not be completed at this moment. Please try again.");
        e.printStackTrace();
    }

}
BorisR
  • 514
  • 1
  • 4
  • 19
Sunil
  • 482
  • 6
  • 12
20

Below code worked for me to generate and download a text file.

@RequestMapping(value = "/download", method = RequestMethod.GET)
public ResponseEntity<byte[]> getDownloadData() throws Exception {

    String regData = "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.";
    byte[] output = regData.getBytes();

    HttpHeaders responseHeaders = new HttpHeaders();
    responseHeaders.set("charset", "utf-8");
    responseHeaders.setContentType(MediaType.valueOf("text/html"));
    responseHeaders.setContentLength(output.length);
    responseHeaders.set("Content-disposition", "attachment; filename=filename.txt");

    return new ResponseEntity<byte[]>(output, responseHeaders, HttpStatus.OK);
}
Siva Kumar
  • 560
  • 6
  • 11
6

What I can quickly think of is, generate the pdf and store it in webapp/downloads/< RANDOM-FILENAME>.pdf from the code and send a forward to this file using HttpServletRequest

request.getRequestDispatcher("/downloads/<RANDOM-FILENAME>.pdf").forward(request, response);

or if you can configure your view resolver something like,

  <bean id="pdfViewResolver"
        class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="viewClass"
              value="org.springframework.web.servlet.view.JstlView" />
    <property name="order" value=”2″/>
    <property name="prefix" value="/downloads/" />
    <property name="suffix" value=".pdf" />
  </bean>

then just return

return "RANDOM-FILENAME";
kalyan
  • 3,076
  • 1
  • 22
  • 29
  • 1
    If I need two view resolvers, how can I also return the name of resolver or choose it in controller?? – azerafati Jun 12 '14 at 17:00
4

The following solution work for me

    @RequestMapping(value="/download")
    public void getLogFile(HttpSession session,HttpServletResponse response) throws Exception {
        try {

            String fileName="archivo demo.pdf";
            String filePathToBeServed = "C:\\software\\Tomcat 7.0\\tmpFiles\\";
            File fileToDownload = new File(filePathToBeServed+fileName);

            InputStream inputStream = new FileInputStream(fileToDownload);
            response.setContentType("application/force-download");
            response.setHeader("Content-Disposition", "attachment; filename="+fileName); 
            IOUtils.copy(inputStream, response.getOutputStream());
            response.flushBuffer();
            inputStream.close();
        } catch (Exception exception){
            System.out.println(exception.getMessage());
        }

    }
Jorge Santos Neill
  • 1,635
  • 13
  • 6
3

something like below

@RequestMapping(value = "/download", method = RequestMethod.GET)
public void getFile(HttpServletResponse response) {
    try {
        DefaultResourceLoader loader = new DefaultResourceLoader();
        InputStream is = loader.getResource("classpath:META-INF/resources/Accepted.pdf").getInputStream();
        IOUtils.copy(is, response.getOutputStream());
        response.setHeader("Content-Disposition", "attachment; filename=Accepted.pdf");
        response.flushBuffer();
    } catch (IOException ex) {
        throw new RuntimeException("IOError writing file to output stream");
    }
}

You can display PDF or download it examples here

xxy
  • 1,058
  • 8
  • 14
2

If it helps anyone. You can do what the accepted answer by Infeligo has suggested but just put this extra bit in the code for a forced download.

response.setContentType("application/force-download");
Sagar Nair
  • 107
  • 1
  • 9
1

In my case I'm generating some file on demand, so also url has to be generated.

For me works something like that:

@RequestMapping(value = "/files/{filename:.+}", method = RequestMethod.GET, produces = "text/csv")
@ResponseBody
public FileSystemResource getFile(@PathVariable String filename) {
    String path = dataProvider.getFullPath(filename);
    return new FileSystemResource(new File(path));
}

Very important is mime type in produces and also that, that name of the file is a part of the link so you has to use @PathVariable.

HTML code looks like that:

<a th:href="@{|/dbreport/files/${file_name}|}">Download</a>

Where ${file_name} is generated by Thymeleaf in controller and is i.e.: result_20200225.csv, so that whole url behing link is: example.com/aplication/dbreport/files/result_20200225.csv.

After clicking on link browser asks me what to do with file - save or open.

Tomasz Dzięcielewski
  • 3,829
  • 4
  • 33
  • 47
1

I had to add this to download any file

    response.setContentType("application/octet-stream");
    response.setHeader("Content-Disposition",
            "attachment;filename="+"file.txt");

all code:

@Controller
public class FileController {

@RequestMapping(value = "/file", method =RequestMethod.GET)
@ResponseBody
public FileSystemResource getFile(HttpServletResponse response) {

    final File file = new File("file.txt");
    response.setContentType("application/octet-stream");
    response.setHeader("Content-Disposition",
            "attachment;filename="+"file.txt");
    return new FileSystemResource(file);
 }
}
Inanc Cakil
  • 306
  • 2
  • 9
0

This can be a useful answer.

Is it ok to export data as pdf format in frontend?

Extending to this, adding content-disposition as an attachment(default) will download the file. If you want to view it, you need to set it to inline.

theNextBigThing
  • 131
  • 3
  • 14