diff --git a/README.md b/README.md
index ef23132..cd1c65f 100644
--- a/README.md
+++ b/README.md
@@ -8,3 +8,4 @@ module for each Spring feature that I test.
## Table of contents
- [Events in Spring](spring-events/README.md);
- [SpEL permission evaluator](spring-el-permission-evaluator/README.md)
+- [AWS S3](spring-aws-s3/README.md)
diff --git a/pom.xml b/pom.xml
index 84f9bf7..bbde797 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,23 +1,23 @@
- 4.0.0
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+ 4.0.0
- com.pedrodovale.experimentingwith.spring
- experimenting-with-spring-parent
- 0.1-SNAPSHOT
-
- spring-events
- spring-el-permission-evaluator
-
- pom
+ com.pedrodovale.experimentingwith.spring
+ experimenting-with-spring-parent
+ 0.1-SNAPSHOT
+
+ spring-events
+ spring-aws-s3
+
+ pom
-
- UTF-8
- UTF-8
- 11
- 11
-
+
+ UTF-8
+ UTF-8
+ 17
+ 17
+
\ No newline at end of file
diff --git a/spring-aws-s3/README.md b/spring-aws-s3/README.md
new file mode 100644
index 0000000..5f32af8
--- /dev/null
+++ b/spring-aws-s3/README.md
@@ -0,0 +1,21 @@
+# AWS S3
+
+Playing around with AWS S3 in a Springboot application based
+on [this documentation](https://docs.awspring.io/spring-cloud-aws/docs/3.2.0/reference/html/index.html#starter-dependencies)
+
+## How to test it
+
+From the root folder, execute:
+
+> ./mvnw clean package \
+> java -jar spring-aws-s3/target/app.jar --spring.config.additional-location=spring-aws-s3/src/main/resources/aws_credentials.properties
+
+This will start the Springboot application on port 8080.
+To interact with the API:
+
+> curl -v --include --form books=@spring-aws-s3/src/test/resources/books.json http://localhost:8080/books/import
+> --data '{"title":"A Study in Scarlet"}'
+
+> curl http://localhost:8080/books
+
+The response body should contain the imported books, populated with an `id`.
\ No newline at end of file
diff --git a/spring-aws-s3/pom.xml b/spring-aws-s3/pom.xml
new file mode 100644
index 0000000..9613d82
--- /dev/null
+++ b/spring-aws-s3/pom.xml
@@ -0,0 +1,81 @@
+
+
+ 4.0.0
+
+ com.pedrodovale.experimentingwith.spring
+ experimenting-with-spring-parent
+ 0.1-SNAPSHOT
+
+
+ spring-aws-s3
+
+ 3.3.5
+ 3.2.0
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-dependencies
+ ${spring-boot-dependencies.version}
+ pom
+ import
+
+
+ io.awspring.cloud
+ spring-cloud-aws-dependencies
+ ${spring-cloud-aws-dependencies.version}
+ pom
+ import
+
+
+
+
+
+
+ io.awspring.cloud
+ spring-cloud-aws-starter-s3
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-autoconfigure
+
+
+ org.projectlombok
+ lombok
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+
+
+
+
+ app
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+ 3.3.1
+
+
+ package-with-build-info
+
+ repackage
+ build-info
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/spring-aws-s3/src/main/java/com/pedrodovale/experimentingwith/spring/awss3/AwsS3Application.java b/spring-aws-s3/src/main/java/com/pedrodovale/experimentingwith/spring/awss3/AwsS3Application.java
new file mode 100644
index 0000000..e4dc5d6
--- /dev/null
+++ b/spring-aws-s3/src/main/java/com/pedrodovale/experimentingwith/spring/awss3/AwsS3Application.java
@@ -0,0 +1,13 @@
+package com.pedrodovale.experimentingwith.spring.awss3;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+
+@SpringBootApplication
+@EnableConfigurationProperties(BooksProperties.class)
+public class AwsS3Application {
+ public static void main(String[] args) {
+ SpringApplication.run(AwsS3Application.class, args);
+ }
+}
diff --git a/spring-aws-s3/src/main/java/com/pedrodovale/experimentingwith/spring/awss3/BooksProperties.java b/spring-aws-s3/src/main/java/com/pedrodovale/experimentingwith/spring/awss3/BooksProperties.java
new file mode 100644
index 0000000..792ce66
--- /dev/null
+++ b/spring-aws-s3/src/main/java/com/pedrodovale/experimentingwith/spring/awss3/BooksProperties.java
@@ -0,0 +1,18 @@
+package com.pedrodovale.experimentingwith.spring.awss3;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import software.amazon.awssdk.annotations.NotNull;
+
+import static com.pedrodovale.experimentingwith.spring.awss3.BooksProperties.PREFIX;
+
+@ConfigurationProperties(prefix = PREFIX)
+@Getter
+@Setter
+public class BooksProperties {
+
+ public static final String PREFIX = "com.pedrodovale.experimentingwith.spring.books";
+
+ @NotNull private String bucket;
+}
diff --git a/spring-aws-s3/src/main/java/com/pedrodovale/experimentingwith/spring/awss3/controller/BooksController.java b/spring-aws-s3/src/main/java/com/pedrodovale/experimentingwith/spring/awss3/controller/BooksController.java
new file mode 100644
index 0000000..75f4d7b
--- /dev/null
+++ b/spring-aws-s3/src/main/java/com/pedrodovale/experimentingwith/spring/awss3/controller/BooksController.java
@@ -0,0 +1,29 @@
+package com.pedrodovale.experimentingwith.spring.awss3.controller;
+
+import com.pedrodovale.experimentingwith.spring.awss3.model.Book;
+import com.pedrodovale.experimentingwith.spring.awss3.service.BooksService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestPart;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.util.List;
+
+import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
+import static org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE;
+
+@RestController
+@RequiredArgsConstructor
+public class BooksController {
+
+ private final BooksService booksService;
+
+ @PostMapping(
+ value = "/books/import",
+ consumes = MULTIPART_FORM_DATA_VALUE,
+ produces = APPLICATION_JSON_VALUE)
+ public List importBooks(@RequestPart(name = "books") MultipartFile multipartFile) {
+ return booksService.processAndBackup(multipartFile);
+ }
+}
diff --git a/spring-aws-s3/src/main/java/com/pedrodovale/experimentingwith/spring/awss3/model/Book.java b/spring-aws-s3/src/main/java/com/pedrodovale/experimentingwith/spring/awss3/model/Book.java
new file mode 100644
index 0000000..f155e58
--- /dev/null
+++ b/spring-aws-s3/src/main/java/com/pedrodovale/experimentingwith/spring/awss3/model/Book.java
@@ -0,0 +1,13 @@
+package com.pedrodovale.experimentingwith.spring.awss3.model;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import java.io.Serializable;
+
+@Getter
+@Setter
+public class Book implements Serializable {
+ private String id;
+ private String title;
+}
diff --git a/spring-aws-s3/src/main/java/com/pedrodovale/experimentingwith/spring/awss3/service/BooksService.java b/spring-aws-s3/src/main/java/com/pedrodovale/experimentingwith/spring/awss3/service/BooksService.java
new file mode 100644
index 0000000..1854e1e
--- /dev/null
+++ b/spring-aws-s3/src/main/java/com/pedrodovale/experimentingwith/spring/awss3/service/BooksService.java
@@ -0,0 +1,47 @@
+package com.pedrodovale.experimentingwith.spring.awss3.service;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.pedrodovale.experimentingwith.spring.awss3.BooksProperties;
+import com.pedrodovale.experimentingwith.spring.awss3.model.Book;
+import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
+import software.amazon.awssdk.core.sync.RequestBody;
+import software.amazon.awssdk.services.s3.S3Client;
+import software.amazon.awssdk.services.s3.model.PutObjectRequest;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+import java.util.UUID;
+
+@Service
+public class BooksService {
+
+ public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+ private final S3Client s3Client;
+ private final String bucket;
+
+ public BooksService(S3Client s3Client, BooksProperties booksProperties) {
+ this.s3Client = s3Client;
+ this.bucket = booksProperties.getBucket();
+ }
+
+ public List processAndBackup(MultipartFile multipartFile) {
+ try (InputStream inputStream = multipartFile.getInputStream()) {
+ byte[] bytes = inputStream.readAllBytes();
+ List books = OBJECT_MAPPER.readValue(bytes, new TypeReference<>() {});
+ books.forEach(
+ book -> {
+ String bookId = UUID.randomUUID().toString();
+ book.setId(bookId);
+ s3Client.putObject(
+ PutObjectRequest.builder().key(bookId).bucket(bucket).build(),
+ RequestBody.fromBytes(bytes));
+ });
+ return books;
+ } catch (IOException e) {
+ throw new RuntimeException("problem retrieving local file", e);
+ }
+ }
+}
diff --git a/spring-aws-s3/src/main/resources/application.yml b/spring-aws-s3/src/main/resources/application.yml
new file mode 100644
index 0000000..e89030d
--- /dev/null
+++ b/spring-aws-s3/src/main/resources/application.yml
@@ -0,0 +1,20 @@
+spring:
+ cloud:
+ aws:
+ region:
+ static: eu-central-1
+ s3:
+ path-style-access-enabled: false
+
+com:
+ pedrodovale:
+ experimentingwith:
+ spring:
+ books:
+ bucket: books-123
+
+logging:
+ level:
+ io:
+ awspring:
+ cloud: debug
\ No newline at end of file
diff --git a/spring-aws-s3/src/test/java/com/pedrodovale/experimentingwith/spring/awss3/controller/BooksControllerTest.java b/spring-aws-s3/src/test/java/com/pedrodovale/experimentingwith/spring/awss3/controller/BooksControllerTest.java
new file mode 100644
index 0000000..62e93d9
--- /dev/null
+++ b/spring-aws-s3/src/test/java/com/pedrodovale/experimentingwith/spring/awss3/controller/BooksControllerTest.java
@@ -0,0 +1,44 @@
+package com.pedrodovale.experimentingwith.spring.awss3.controller;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.core.io.Resource;
+import org.springframework.mock.web.MockPart;
+import org.springframework.test.web.servlet.MockMvc;
+import software.amazon.awssdk.services.s3.S3Client;
+
+import static org.springframework.http.MediaType.APPLICATION_JSON;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@SpringBootTest
+@AutoConfigureMockMvc
+class BooksControllerTest {
+
+ @Autowired private MockMvc mockMvc;
+
+ @MockBean private S3Client s3Client;
+
+ @Value("classpath:books.json")
+ private Resource sampleFile;
+
+ @Test
+ void importBooks() throws Exception {
+ mockMvc
+ .perform(
+ multipart("/books/import")
+ .part(
+ new MockPart(
+ "books", sampleFile.getFilename(), sampleFile.getContentAsByteArray())))
+ .andExpect(status().isOk())
+ .andExpect(content().contentType(APPLICATION_JSON))
+ .andExpect(jsonPath("$[0].id").isNotEmpty())
+ .andExpect(jsonPath("$[0].title").value("A Study In Scarlet"));
+ }
+}
diff --git a/spring-aws-s3/src/test/resources/books.json b/spring-aws-s3/src/test/resources/books.json
new file mode 100644
index 0000000..5273763
--- /dev/null
+++ b/spring-aws-s3/src/test/resources/books.json
@@ -0,0 +1,5 @@
+[
+ {
+ "title": "A Study In Scarlet"
+ }
+]
\ No newline at end of file