Bk Santiago
Bk Santiago

Reputation: 1573

How to serve files/PDF files the reactive way in spring

I have the following endpoint code to serve PDF files.

@RequestMapping
ResponseEntity<byte[]> getPDF() {
  File file = ...;
  byte[] contents = null;
  try {
    try (FileInputStream fis = new FileInputStream(file)) {
      contents = new byte[(int) file.length()];
      fis.read(contents);
    }
  } catch(Exception e) {
    // error handling
  }
  HttpHeaders headers = new HttpHeaders();
  headers.setContentDispositionFormData(file.getName(), file.getName());
  headeres.setCacheControl("must-revalidate, post-check=0, pre-check=0");
  return new ResponseEntity<>(contents, headers, HttpStatus.OK);
}

How can I convert above into a reactive type Flux/Mono and DataBuffer.

I have check DataBufferUtils but It doesn't seem to offer what I needed. I didn't find any example either.

Upvotes: 8

Views: 12848

Answers (3)

bamossza
bamossza

Reputation: 3926

Same Problem with me.

I use Webflux Spring WebClient

I write style RouterFunction

My solution below,

ETaxServiceClient.java

final WebClient defaultWebClient;


public Mono<byte[]> eTaxPdf(String id) {
    return defaultWebClient
            .get()
            .uri("-- URL PDF File --")
            .accept(MediaType.APPLICATION_OCTET_STREAM)
            .exchange()
            .log("eTaxPdf -> call other service")
            .flatMap(response -> response.toEntity(byte[].class))
            .flatMap(responseEntity -> Mono.just(Objects.requireNonNull(responseEntity.getBody())));
}

ETaxHandle.java

@NotNull
public Mono<ServerResponse> eTaxPdf(ServerRequest sr) {
    Consumer<HttpHeaders> headers = httpHeaders -> {
        httpHeaders.setCacheControl(CacheControl.noCache());
        httpHeaders.setContentDisposition(
                ContentDisposition.builder("inline")
                        .filename(sr.pathVariable("id") + ".pdf")
                        .build()
        );
    };
    return successPDF(eTaxServiceClient
            .eTaxPdf(sr.pathVariable("id"))
            .switchIfEmpty(Mono.empty()), headers);
}

ETaxRouter.java

@Bean
public RouterFunction<ServerResponse> routerFunctionV1(ETaxHandle handler) {
    return route()
            .path("/api/v1/e-tax-invoices", builder -> builder
                    .GET("/{id}", handler::eTaxPdf)
            )
            .build();
}

CommonHandler.java

Mono<ServerResponse> successPDF(Mono<?> mono, Consumer<HttpHeaders> headers) {
    return ServerResponse.ok()
            .headers(headers)
            .contentType(APPLICATION_PDF)
            .body(mono.map(m -> m)
                    .subscribeOn(Schedulers.elastic()), byte[].class);
}

Result: Successfully displayed on the browser.

Work for me.

enter image description here

Upvotes: 0

Sudharsan Thumatti
Sudharsan Thumatti

Reputation: 2895

Below is the code to return the attachment as byte stream:

@GetMapping(
        path = "api/v1/attachment",
        produces = APPLICATION_OCTET_STREAM_VALUE
)
public Mono<byte[]> getAttachment(String url) {
    return rest.get()
            .uri(url)
            .exchange()
            .flatMap(response -> response.toEntity(byte[].class));
}

This approach is very simple but the disadvantage is it will the load the entire attachment into memory. If the file size is larger, then it will be a problem.

To overcome we can use DataBuffer which will send the data in chunks. This is an efficient solution and it will work for any large size file. Below is the modified code using DataBuffer:

@GetMapping(
        path = "api/v1/attachment",
        produces = APPLICATION_OCTET_STREAM_VALUE
)
public Flux<DataBuffer> getAttachment(String url) {
    return rest.get()
            .uri(url)
            .exchange()
            .flatMapMany(response -> response.toEntity(DataBuffer.class));
}

In this way, we can send attachments in a reactive fashion.

Upvotes: 2

Brian Clozel
Brian Clozel

Reputation: 59201

The easiest way to achieve that would be with a Resource.

@GetMapping(path = "/pdf", produces = "application/pdf")
ResponseEntity<Resource> getPDF() {
  Resource pdfFile = ...;
  HttpHeaders headers = new HttpHeaders();
  headers.setContentDispositionFormData(file.getName(), file.getName());
  return ResponseEntity
    .ok().cacheControl(CacheControl.noCache())
    .headers(headers).body(resource);
}

Note that DataBufferUtils has some useful methods there that convert an InputStream to a Flux<DataBuffer>, like DataBufferUtils#read(). But dealing with a Resource is still superior.

Upvotes: 16

Related Questions