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