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

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.

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.

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.

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.

Related

Spring not generating Content Type

I'm using SpringBoot 3.0.1 and I'm trying to get a file stored in the backend using Axios.
The controller is the following:
#GetMapping(value = "/api/files/{fileName}", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public ResponseEntity<?> getFile(final #PathVariable("fileName") String fileName) {
try {
Path filePath = Path.of(fileName);
File file = filePath.toFile();
HttpHeaders responseHeaders = new HttpHeaders();
String filename = filePath.getFileName().toString();
responseHeaders
.setContentDisposition(ContentDisposition.builder("attachment")
.filename(filename, StandardCharsets.UTF_8)
.build());
FileSystemResource fileSystemResource = new FileSystemResource(file);
return ResponseEntity
.ok()
.headers(responseHeaders)
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.contentLength(file.length())
.lastModified(file.lastModified())
.body(fileSystemResource);
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
When I get the answer (status is 200), the header I've set in the controller is not given. In particular, the Content-Disposition header is not defined in the answer.
I'm wondering if there is any missing configuration that must be set in Sprint Boot in order to be allowed to set a custom header. Anyone who knows what can cause this and how to fix it?

How to Invoke Multiple webmethod URLs with different filename at once in spring boot

I am very new to Spring Boot project.
I am writing backend code where I have a webmethod url which can download only one file at a time based on fileNo. It is invoked from the front-end when user enters fileNo and submits.
User can enter maximum 5 fileNo(comma-separated) at one time.
In that case I have to take each file no and and set it into my url, invoke it which will download 5 files and put it in one common folder.
Below code is working for one FileNo and downloading the file,
Is there anyway where I can set and invoke all 5 URLs concurrently, download all 5 files and put it in a same folder.
Or If I have to set it one by one in my URL then how to do it. What is the best way to do this. (went through few similar posts but couldn't fine anything for my solution). Looking for a solution here. Thanks
#SneakyThrows
#RequestMapping(value = "/{fileName:.+}", method = RequestMethod.GET)
#ApiResponses(value = {
#ApiResponse(code = 200, message = "Success"),
#ApiResponse(code = 500, message = "Server Error")
})
public ResponseEntity getCollateralDownloadData(#PathVariable("fileName") String fileName) {
String wmURL = "https://www.qa.referencesite.com/invoke/File_appDesigns.process:processTpfQuery?prdType=PTO&tpf_aif=one&SALESNO=&PRODNO=&OASN="+fileName;
try {
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Arrays.asList(MediaType.APPLICATION_OCTET_STREAM));
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<byte[]> response = restTemplate.build()
.exchange(wmURL, HttpMethod.GET, entity, byte[].class);
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType("application/octet-stream"))
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileName + "\"")
.body(response.getBody());
} catch (Exception e) {
e.printStackTrace();
}
return ResponseEntity.status(HttpStatus.OK).body("Collateral Download Created successfully");
}
There are 2 approaches depending on your need :
The client may trigger requests concurrently, so the client has to send a request for each fileNo and you backend deals with it
The client may trigger only one request, so your backend should be modified to trigger subsequent calls, wait for result and build a common response. You may use Spring WebFlux facilities or Java ExecutorService.invokeAll() method to manage subsequent parallels requests.
Regards.
Here is complete Flow of code what I have done.
#RestController
public class DownloadOAFileController {
#Autowired
private RestTemplateBuilder restTemplate;
private static final int MYTHREADS = 30;
#SneakyThrows
#RequestMapping(value = "/downloadzippedFile/{fileName}", method = RequestMethod.GET)
#ApiResponses(value = {
#ApiResponse(code = 200, message = "Success"),
#ApiResponse(code = 500, message = "Server Error")
})
public ResponseEntity<Object> downloadzipFiles(#PathVariable("fileName") String fileName) throws IOException, ExecutionException, InterruptedException {
downloadMultipleFilesToRemoteDiskUsingThread(fileName);
String zipFilename = "/Users/kumars22/Downloads/Files.zip";
File file = new File(zipFilename);
InputStreamResource resource = new InputStreamResource(new FileInputStream(file));
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Disposition",
String.format("attachment; filename=\"%s\"",file.getName()));
headers.add("Cache-Control","no-cache, no-store, no-revalidate");
headers.add("pragma","no-cache");
headers.add("Expires","0");
ResponseEntity<Object> responseEntity = ResponseEntity.ok().headers(headers)
.contentLength(file.length())
.contentType(MediaType.parseMediaType("application/octet-stream"))
.body(resource);
return responseEntity;
}
//Created a Directory where all files will be downloaded (Got some space in AWS for actual implementation.
public static void createDirectory() {
File file = new File("/Users/kumars22/Downloads/Files");
if (!file.exists()) {
file.mkdir();
}
}
public static void downloadMultipleFilesToRemoteDiskUsingThread( String fileName ) throws ExecutionException, InterruptedException, IOException {
createDirectory();
ExecutorService executor = Executors.newFixedThreadPool(MYTHREADS);
String[] serials = fileName.split(",");
String[] wmURLs = new String[serials.length];
for (int i =0;i<serials.length;i++) {
wmURLs[i] = "https://www.qa.referencesite.com/invoke/File_appDesigns.process:processTpfQuery?prdType=PTO&tpf_aif=one&SALESNO=&PRODNO=&OASN="+serials[i];
}
for (int i = 0; i < wmURLs.length; i++) {
String url = wmURLs[i];
Runnable worker = new MultipleCallThreadController(url,fileName,"James");
executor.execute(worker);
}
executor.shutdown();
// Wait until all threads are finish
while (!executor.isTerminated()) {
}
zipFiles("/Users/kumars22/Downloads/Files","/Users/kumars22/Downloads/Files.zip");
System.out.println("\nFinished all threads");
}
//Zip the Folder having all files
public static void zipFiles(String sourceDirPath, String zipFilePath) throws IOException {
Path p = Files.createFile(Paths.get(zipFilePath));
Path pp = Paths.get(sourceDirPath);
try (ZipOutputStream zs = new ZipOutputStream(Files.newOutputStream(p));
Stream<Path> paths = Files.walk(pp)) {
paths
.filter(path -> !Files.isDirectory(path))
.forEach(path -> {
ZipEntry zipEntry = new ZipEntry(pp.relativize(path).toString());
try {
zs.putNextEntry(zipEntry);
Files.copy(path, zs);
zs.closeEntry();
} catch (IOException e) {
System.err.println(e);
}
});
}
}

How to upload multipart/form-data to AmazonS3 using java sdk and web-flux?

I have a java spring-boot project with spring-boot-starter-webflux. I have a rest controller:
import software.amazon.awssdk.services.s3.S3AsyncClient;
import software.amazon.awssdk.core.async.AsyncRequestBody;
...
#PostMapping(path = "/path", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = APPLICATION_JSON_VALUE)
public Mono<String> submitMultipartInstance(#RequestPart Flux<FilePart> partFlux) {
String bucketName = ...
String key = ...
PutObjectRequest request = PutObjectRequest.builder()
.bucket(bucketName)
.key(key)
.build();
CompletableFuture<PutObjectResponse> completableFuture = s3AsyncClient.putObject(request, createAsyncBody(partFlux));
return Mono.defer(() -> Mono.fromFuture(completableFuture))
.map(PutObjectResponse::toString)
}
private AsyncRequestBody createAsyncBody(Flux<FilePart> part) {
return new AsyncRequestBody() {
#Override
public Optional<Long> contentLength() {
return Optional.empty(); // what value to return from here?
}
#Override
public void subscribe(Subscriber<? super ByteBuffer> s) {
part.flatMap(FilePart::content).map(toByteBuffer()).subscribe(s);
}
private Function<DataBuffer, ByteBuffer> toByteBuffer() {
return (buffer) -> {
byte[] bytes = new byte[buffer.asByteBuffer().remaining()];
try {
return ByteBuffer.wrap(bytes);
} finally {
DataBufferUtils.release(buffer.read(bytes));
}
};
}
};
}
and I receive message (expectedly)
Caused by: software.amazon.awssdk.services.s3.model.S3Exception: You
must provide the Content-Length HTTP header. (Service: S3, Status
Code: 411, Request ID: ...)
How to upload multipart to s3 in reactive style?
You can't use the put object operation in this case. You need to to do this with multipart upload.
Unfortunately, the new AWS Java SDK support for this feature is very cumbersome.
You need to use the following operations:
createMultipartUpload
uploadPart
completeMultipartUpload
Hopefully, one day we will get a high-level API for this.

Sending file in Spring Boot

I'm creating spring boot application that send a file in body response, to this i use this code :
FileSystemResource pdfFile = new FileSystemResource(outputFile);
return ResponseEntity
.ok()
.contentLength(pdfFile.contentLength())
.contentType(MediaType.parseMediaType("application/pdf"))
.body(new ByteArrayResource(IOUtils.toByteArray(pdfFile.getInputStream())));
I'm wondering if there's any alternative way for send file other than using FileSystemResource ?
Please, If there's any suggestion, do not hesitate.
Thank You !
This is a simplified version of how I usually do it, but it does pretty much the same thing:
#RequestMapping(method = RequestMethod.GET, value = "/{id}")
public ResponseEntity<byte[]> getPdf(#PathVariable Long id) throws IOException {
final String filePath = pdfFilePathFinder.find(id);
final byte[] pdfBytes = Files.readAllBytes(Paths.get(filePath));
final HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.parseMediaType("application/pdf"));
headers.setContentDispositionFormData("attachment", null);
headers.setCacheControl("no-cache");
return new ResponseEntity<>(pdfBytes, headers, HttpStatus.OK);
}

Send received spring FilePart without saving

I need to send FilePart received in RestController to API using WebClient,
how can I do this?
Found an example, which saves image to disk.
private static String UPLOAD_ROOT = "C:\\pics\\";
public Mono<Void> checkInTest(#RequestPart("photo") Flux<FilePart> photoParts,
#RequestPart("data") CheckInParams params, Principal principal) {
return saveFileToDisk(photoParts);
}
private Mono<Void> saveFileToDisk(Flux<FilePart> parts) {
return parts
.log("createImage-files")
.flatMap(file -> {
Mono<Void> copyFile = Mono.just(Paths.get(UPLOAD_ROOT, file.filename()).toFile())
.log("createImage-picktarget")
.map(destFile -> {
try {
destFile.createNewFile();
return destFile;
} catch (IOException e) {
throw new RuntimeException(e);
}
})
.log("createImage-newfile")
.flatMap(file::transferTo)
.log("createImage-copy");
return Mono.when(copyFile)
.log("createImage-when");
})
.log("createImage-flatMap")
.then()
.log("createImage-done");
}
Then read it again and send to anoter server
.map(destFile -> {
MultiValueMap<String, Object> map = new LinkedMultiValueMap<>();
try {
map.set("multipartFile", new ByteArrayResource(FileUtils.readFileToByteArray(destFile)));
} catch (IOException ignored) {
}
map.set("fileName", "test.txt");
WebClient client = WebClient.builder().baseUrl("http://localhost:8080").build();
return client.post()
.uri("/upload")
.contentType(MediaType.MULTIPART_FORM_DATA)
.syncBody(map)
.exchange(); //todo handle errors???
}).then()
Is there way to avoid saving file?
I will mention solution by #Abhinaba Chakraborty
provided in https://stackoverflow.com/a/62745370/4551411
Probably something like this:
#PostMapping(value = "/images/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public Mono<ResponseEntity<Void>> uploadImages(#RequestPart("files") Flux<FilePart> fileParts) {
return fileParts
.flatMap(filePart -> {
return webClient.post()
.uri("/someOtherService")
.body(BodyInserters.fromPublisher(filePart.content(), DataBuffer.class))
.exchange()
.flatMap(clientResponse -> {
//some logging
return Mono.empty();
});
})
.collectList()
.flatMap(response -> Mono.just(ResponseEntity.accepted().build()));
}
This accepts MULTIPART FORM DATA where you can attach multiple image files and upload them to another service.

Categories