Skip to content

Commit

Permalink
chore: add docs
Browse files Browse the repository at this point in the history
  • Loading branch information
hantsy committed Sep 23, 2024
1 parent fd41967 commit e21667e
Show file tree
Hide file tree
Showing 4 changed files with 391 additions and 13 deletions.
8 changes: 2 additions & 6 deletions boot-filepart/src/main/java/com/example/demo/PostSummary.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
package com.example.demo;

import lombok.Value;

import java.util.UUID;

public record PostSummary (
UUID id,
String title
){}
public record PostSummary(UUID id, String title) {
}

189 changes: 188 additions & 1 deletion docs/filepart.md
Original file line number Diff line number Diff line change
@@ -1 +1,188 @@
# Handling File Upload/Download with Spring WebFlux and Spring Data R2dbc
# Handling File Upload/Download with Spring WebFlux and Spring Data R2dbc

In [last post](https://hantsy.medium.com/persisting-binary-data-into-postgres-using-spring-data-r2dbc-cefe6afb3e1c), we have explore how to persist binary data into Postgres database using R2dbc/Spring Data R2dbc. In this post, we continue to discuss how to upload file and download file via Spring WebFlux stack, and of course handling the persistence using the former usages we have done in previous post.

Let's work on the existing project we used in the previous post.

Create a controller for `Post`.
* A method to retrieve all posts.
* A method to save new Post.
* A method to retrieve Post by id.

```java
@RestController
@RequestMapping("/posts")
@Slf4j
@RequiredArgsConstructor
class PostController {

private final PostRepository posts;

@GetMapping
public Flux<PostSummary> all(@RequestParam(required = true, defaultValue = "") String title,
@RequestParam(required = true, defaultValue = "0") Integer page,
@RequestParam(required = true, defaultValue = "10") Integer size
) {
return posts.findByTitleLike("%" + title + "%", PageRequest.of(page, size));
}

@PostMapping
public Mono<ResponseEntity> create(@RequestBody CreatePostCommand data) {
return posts.save(Post.builder().title(data.title()).content(data.content()).build())
.map(saved -> ResponseEntity.created(URI.create("/posts/" + saved.getId())).build());
}

@GetMapping("{id}")
public Mono<ResponseEntity<Post>> get(@PathVariable UUID id) {
return this.posts.findById(id)
.map(post -> ResponseEntity.ok().body(post))
.switchIfEmpty(Mono.just(ResponseEntity.notFound().build()));
}

}
```

The `@RequiredArgsConstructor` will append a constructor with the `final` fields as parameters at compile time.

```java
public PostController(PostRepository posts;) {
this.posts = posts;
}
```

Spring supports injection via constructor for the constructor with parameters if the class has one and only such one constructor.

The `PostSummary` contains the essential info of a `Post`.

```java
public record PostSummary(UUID id, String title) {
}
```

Add a method into the `PostRepository`.

```java
interface PostRepository extends R2dbcRepository<Post, UUID>{

public Flux<PostSummary> findByTitleLike(String title, Pageable pageable);
}
```

Notes, in the *reactive* `Repository`, it accepts a `Pageable` as the last parameter in the method parameter list, but it does not return a `Page` object as result. To get the count of the total elements, you should use another count query like the following.

```java
interface PostRepository extends R2dbcRepository<Post, UUID>{
// ...
public Long countByTitleLike(String title);
}
```

The `/posts` endpoint get all posts that only includes post titles, binary data in the JSON will be displayed as unrecognized characters.

It is better to use a standalone endpoint to save and read binary data.

Add two methods to handle attachment upload and download.

```java
@PutMapping("{id}/attachment")
public Mono<ResponseEntity> upload(@PathVariable UUID id,
@RequestPart Mono<FilePart> fileParts) {

return Mono
.zip(objects -> {
var post = (Post) objects[0];
var filePart = (DataBuffer) objects[1];
post.setAttachment(filePart.toByteBuffer());
return post;
},
this.posts.findById(id),
fileParts.flatMap(filePart -> DataBufferUtils.join(filePart.content()))
)
.flatMap(this.posts::save)
.map(saved -> ResponseEntity.noContent().build());
}

@GetMapping("{id}/attachment")
public Mono<Void> read(@PathVariable UUID id, ServerWebExchange exchange) {
return this.posts.findById(id)
.log()
.map(post -> Mono.just(new DefaultDataBufferFactory().wrap(post.getAttachment())))
.flatMap(r -> exchange.getResponse().writeWith(r));
}
```

The `GET posts/{id}/attachment` endpoint writes the binary data into HTTP response directly.

Create an integration test to the file upload and download progress.

```java
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Slf4j
public class IntegrationTests {

@LocalServerPort
private int port;

private WebTestClient webClient;

@BeforeEach
public void setup() {
this.webClient = WebTestClient.bindToServer()
.baseUrl("http://localhost:" + this.port)
.codecs(clientCodecConfigurer ->
clientCodecConfigurer.defaultCodecs().enableLoggingRequestDetails(true)
)
.build();
}

@Test
public void willLoadPosts() {
this.webClient.get().uri("/posts")
.exchange()
.expectStatus().is2xxSuccessful();
}

@Test
public void testUploadAndDownload() {
var locationUri = this.webClient.post()
.uri("/posts")
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(new CreatePostCommand("test title", "test content"))
.exchange()
.expectStatus().isCreated()
.expectHeader().exists(HttpHeaders.LOCATION)
.returnResult(ParameterizedTypeReference.forType(Void.class))
.getResponseHeaders().getLocation();

log.debug("location uri: {}", locationUri);
assertThat(locationUri).isNotNull();

var attachmentUri = locationUri + "/attachment";
this.webClient.put()
.uri(attachmentUri)
.bodyValue(generateBody())
.exchange()
.expectStatus().isNoContent();

var responseContent = this.webClient.get()
.uri(attachmentUri)
.accept(MediaType.APPLICATION_OCTET_STREAM)
.exchange()
.expectStatus().isOk()
.returnResult(ParameterizedTypeReference.forType(byte[].class))
.getResponseBodyContent();

assertThat(responseContent).isNotNull();
assertThat(new String(responseContent)).isEqualTo("test");
}

private MultiValueMap<String, HttpEntity<?>> generateBody() {
MultipartBodyBuilder builder = new MultipartBodyBuilder();
builder.part("fileParts", new ClassPathResource("/foo.txt", IntegrationTests.class));
return builder.build();
}

}
```

The complete [sample codes](https://github.com/hantsy/spring-r2dbc-sample/blob/master/filepart) are shared on my Github account.
8 changes: 2 additions & 6 deletions docs/index.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
* [Introduction to R2dbc](./intro.md)

* [Working with Relational Database using R2dbc DatabaseClient](./database-client.md)

* [*Update*: Accessing RDBMS with Spring Data R2dbc](./data-r2dbc.md)

* [Data Auditing with Spring Data R2dbc](./auditing.md)

* [Dealing with Postgres specific Json/Enum type and NOTIFY/LISTEN with R2dbc](./pg.md)


* [Persisting Binary Data into Postgres using Spring Data R2dbc](./docs/persist-bin.md)
* [Handling File Upload/Download with Spring WebFlux and Spring Data R2dbc](./docs/filepart.md)
Loading

0 comments on commit e21667e

Please sign in to comment.