How to stream video from GridFS with Spring Webflux? - spring

I want to get video from GridFS server and then stream it as .mp4 to user via REST endpoint. I am using Spring Webflux. As for now I have used code from some tutorial but the result isn't what I wanted. After sending GET request I get a page filled with weird symbols, instead of mp4 video. This is code for my logic:
public Flux<Void> read(#PathVariable String id, ServerWebExchange exchange) {
return this.reactiveGridFsTemplate.findOne(new Query(Criteria.where("filename").is(id)))
.flatMap(reactiveGridFsTemplate::getResource)
.flatMapMany(r -> exchange.getResponse().writeWith(r.getDownloadStream()));
}
and for my controller:
#GetMapping(value = "/videos/{name}")
public Flux<?> getVideo(#PathVariable String name, ServerWebExchange exchange){
return gridStorageService.read(name, exchange);
}

You have to set Content-Disposition header in the response with the value attachment and the file name, so that the client can identify that it should be downloaded as a file, also set produces to MediaType.APPLICATION_OCTET_STREAM_VALUE) or "video/mp4".
#GetMapping(value = "/videos/{name}", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public Flux<?> getVideo(#PathVariable String name, ServerWebExchange exchange){
exchange.getResponse().getHeaders().add(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + name);
return gridStorageService.read(name, exchange);
}

Related

Simple logging dump of WebClient request?

I'm trying to use Spring WebClient to make some basic REST API calls. I'm getting an error that the request is malformed, but I can't tell exactly why. Is there any way to easily log the contents of the request (really, just the request body)? Everything I find online is super complicated. Here's what I have:
LinkedMultiValueMap params = new LinkedMultiValueMap();
params.add("app_id", getOneSignalAppId());
params.add("included_segments", inSegment);
params.add("content_available", true);
params.add("contents", new LinkedMultiValueMap() {{
add("en", inTitle);
}});
BodyInserters.MultipartInserter inserter = BodyInserters.fromMultipartData(params);
WebClient client = WebClient.builder()
.baseUrl("https://onesignal.com")
.defaultHeader(HttpHeaders.AUTHORIZATION, "Basic " + getOneSignalKey())
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
.build();
Mono<NotificationResponse> result = client
.post()
.uri("/api/v1/notifications")
.body(inserter)
.retrieve()
.bodyToMono(NotificationResponse.class);
I just want a string of the JSON that will be inserted into the request body.
You can create your own wrapper/proxy class around the JSON encoder (assuming you're using JSON) and intercept the serialized body before it is sent into the intertubes.
If your request is going to send JSON.
Specifically, you would extend the encodeValue method (or encodeValues in case of streaming data) of Jackson2JsonEncoder (the default encoder). Then you can do with that data what you wish, such as logging etc. And you could even do this conditionally based on environment/profile.
This custom logging-encoder can be specified when creating the WebClient, by providing it as a codec:
CustomBodyLoggingEncoder bodyLoggingEncoder = new CustomBodyLoggingEncoder();
WebClient.builder()
.codecs(clientDefaultCodecsConfigurer -> {
clientDefaultCodecsConfigurer.defaultCodecs().jackson2JsonEncoder(bodyLoggingEncoder);
clientDefaultCodecsConfigurer.defaultCodecs().jackson2JsonDecoder(new Jackson2JsonDecoder(new ObjectMapper(), MediaType.APPLICATION_JSON));
})
...
I made a blog post about this. You might be able to find the encoder for Multipart data and apply similar principles
For completeness, the encoder might look something like this:
public class CustomBodyLoggingEncoder extends Jackson2JsonEncoder {
#Override
public DataBuffer encodeValue(final Object value, final DataBufferFactory bufferFactory,
final ResolvableType valueType, #Nullable final MimeType mimeType, #Nullable final Map<String, Object> hints) {
// Encode/Serialize data to JSON
final DataBuffer data = super.encodeValue(value, bufferFactory, valueType, mimeType, hints);
// This is your code:
SomethingAmazing.doItWithThisData(extractBytes(data));
// Return the data as normal
return data;
}
private byte[] extractBytes(final DataBuffer data) {
final byte[] bytes = new byte[data.readableByteCount()];
data.read(bytes);
// We've copied the data above to our array, but must reset the buffer for actual usage
data.readPosition(0);
return bytes;
}
}
Hope that helps somehow!

In Spring, how do I POST to a URL using WebClient?

Right now, I have a class Message representing a JSON object as so
#Value
#Builder
public class Message {
#JsonProperty("#msgType")
String msgType;
#JsonProperty("#type")
String type;
String category;
#Singular("characteristic") List<CharacteristicItem> characteristic;
#Singular("receiver") List<ReceiverItem> receiver;
Sender sender;
}
I would like to send an instance of the class Message as a JSON to remote URL https:example.com/message. How can I create a method that can POST to this URL with the JSON? Note, I am not creating a mapping, I just want to POST to URL and retrieve the HTTP response and convert to string. I'm thinking of using WebClient. Note, I am only interested in seeing if the response is 200, 400, etc., I don't need to handle the response JSON.
If you want to use WebClient you can write below code to post JSON data to URL and retrieve response status code.
WebClient client = WebClient.builder()
.baseUrl("https://example.com")
.build();
Message m = Message.builder().build();
Mono<HttpStatus> httpStatusMono = client.post().uri("/createMessage")
.body(Mono.just(m), Message.class)
.exchange().map(r -> r.statusCode());
// s.value() will give you response status code
httpStatusMono.subscribe(s -> s.value());
If you dont wan't to use WebClient another alternative is RestTemplate
below is the code to achieve the same thing with RestTemplate
RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder();
ResponseExtractor<Integer> responseExtractor = clientHttpResponse -> {
return Integer.valueOf(clientHttpResponse.getStatusCode().value());
};
Message m = Message.builder().build();
RequestCallback requestCallback = clientHttpRequest -> {
objectMapper.writeValue(clientHttpRequest.getBody(), m);
};
Integer status = restTemplateBuilder.build()
.execute("https://example.com/createMessage",
HttpMethod.POST, requestCallback, responseExtractor);
System.out.println(status);

POST byte array in multipart using Spring RestTemplate

I'm trying to POST a multipart/form-data using Spring RestTemplate with a byte array as the file to upload and it keeps failing (Server rejects with different kinds of errors).
I'm using a MultiValueMap with ByteArrayResource. Is there something I'm missing?
Yes there is something missing.
I have found this article:
https://medium.com/#voziv/posting-a-byte-array-instead-of-a-file-using-spring-s-resttemplate-56268b45140b
The author mentions that in order to POST a byte array using Spring RestTemplate one needs to override getFileName() of the ByteArrayResource.
Here is the code example from the article:
private static void uploadWordDocument(byte[] fileContents, final String filename) {
RestTemplate restTemplate = new RestTemplate();
String fooResourceUrl = "http://localhost:8080/spring-rest/foos"; // Dummy URL.
MultiValueMap<String, Object> map = new LinkedMultiValueMap<String, Object>();
map.add("name", filename);
map.add("filename", filename);
// Here we
ByteArrayResource contentsAsResource = new ByteArrayResource(fileContents) {
#Override
public String getFilename() {
return filename; // Filename has to be returned in order to be able to post.
}
};
map.add("file", contentsAsResource);
// Now you can send your file along.
String result = restTemplate.postForObject(fooResourceUrl, map, String.class);
// Proceed as normal with your results.
}
I tried it and it works!
I added an issue to send a request from java client to Python service in FastApi and sending a ByteArrayResource instaead of simple byte[] fixed the issue.
FastAPI server returned: "Expected UploadFile, received: <class 'str'>","type":"value_error""

Encoding for downloaded files in Spring

I want to create a controller which will sent to client a CSV file, and I created the next controller:
#ResponseStatus(value = HttpStatus.OK)
#RequestMapping(value = "/csv", method = RequestMethod.GET)
public ResponseEntity downloadCsvAllInvoiceTransactionsByFilter(
#PageableDefault(direction = DESC, sort = "invoiceDate", size = 30) Pageable pageRequest) throws IOException {
String someVariable = "Some text";
byte[] out = someVariable.getBytes(Charset.forName("UTF-8"));
HttpHeaders responseHeaders = new HttpHeaders();
LOGGER.info(new String(out));
responseHeaders.add("content-disposition", "attachment; filename=transactions.csv" );
responseHeaders.add("Content-Type","text/csv; charset=utf-8");
return new ResponseEntity<>(out,responseHeaders,HttpStatus.OK);
}
Logger is displaying the correct string:
Some text
but in downloaded file there is another one
U29tZSB0ZXh0
How can I fix this?
Body of ResponseEntity goes through a message converter before it gets sent. The choice of the particular converter depends on class of the body and response and request headers.
I tried to reproduce the issue with your code snippet and got expected text in csv file. So I assume that you got a message converter registered that converts byte arrays the way you observe.
You can debug AbstractMessageConverterMethodProcessor#writeWithMessageConverters and see which converter is chosen and why.

Spring Boot - Writing media (image, mp3, mp4) file to response output stream

I am new to Servlets and Spring Framework.
I try to get media files from directory through Rest Service.
For videos/mp4 I couldn't find anything.
For audio I did this:
Writing mp3 file to response output stream
For images I did this:
#RequestMapping("/tmp/{uuid}")
#ResponseBody
public ResponseEntity<InputStreamResource> getTmp(#PathVariable("uuid") String uuid)
throws IOException {
Path path = Paths.get("/media/psmaster/HDD/TUC-IPS/" + uuid);
String contentType = Files.probeContentType(path);
FileSystemResource file = new FileSystemResource("/media/psmaster/HDD/TUC-IPS/" + uuid);
return ResponseEntity
.ok()
.contentLength(file.contentLength())
.contentType(
MediaType.parseMediaType(contentType))
.body(new InputStreamResource(file.getInputStream()));
}
Can someone please help to figure out the problem?
If you are using Spring 4.2 you can use StreamingResponseBody, Have a look at this post
You can also give Spring Content a look. It allows you to build content services very quickly and easily using similar programming techniques to Spring Data. You can also pair it with Spring Data to additionally store and search metadata for your videos. By defining a single interface and including the appropriate Spring Content dependency in your project you can create a set of REST endpoints that allow you to manage the full lifecycle of a video including streaming.
You can write media using streams and HttpServletResponse:
#RequestMapping(value = "/image/{imgName}", method = RequestMethod.GET)
public void getImageAsByteArray(#PathVariable String imgName , HttpServletResponse response) throws IOException {
InputStream in = servletContext.getResourceAsStream("/WEB-INF/images/" + imgName);
response.setContentType(MediaType.IMAGE_JPEG_VALUE);
IOUtils.copy(in, response.getOutputStream());
}
The example above serves an image file.
Hope this helps

Resources