From a81411cbb6e4d9d2fb79f808f14a964fcb6837e5 Mon Sep 17 00:00:00 2001 From: Robin Wilkins Date: Fri, 9 Aug 2024 17:13:43 +0100 Subject: [PATCH] Add file download and XML API Add a controller streaming a pdf file with file name and headers. Add further changes to provide xml api endpoints --- build.gradle | 5 ++ .../wilkins/showcase/ShowcaseApplication.java | 3 + .../java/com/wilkins/showcase/WebConfig.java | 15 ++++ .../controller/DownloadController.java | 37 ++++++++++ .../wilkins/showcase/controller/Person.java | 6 ++ .../showcase/controller/UploadController.java | 33 +++++++++ src/main/resources/Test.pdf | Bin 0 -> 5446 bytes src/main/resources/application.yml | 5 ++ src/main/resources/public/index.html | 4 + .../controller/DownloadControllerTest.java | 30 ++++++++ .../controller/UploadControllerTest.java | 69 ++++++++++++++++++ 11 files changed, 207 insertions(+) create mode 100644 src/main/java/com/wilkins/showcase/WebConfig.java create mode 100644 src/main/java/com/wilkins/showcase/controller/DownloadController.java create mode 100644 src/main/java/com/wilkins/showcase/controller/Person.java create mode 100644 src/main/java/com/wilkins/showcase/controller/UploadController.java create mode 100644 src/main/resources/Test.pdf create mode 100644 src/main/resources/public/index.html create mode 100644 src/test/java/com/wilkins/showcase/controller/DownloadControllerTest.java create mode 100644 src/test/java/com/wilkins/showcase/controller/UploadControllerTest.java diff --git a/build.gradle b/build.gradle index 393e73f..7a0f03b 100644 --- a/build.gradle +++ b/build.gradle @@ -2,6 +2,7 @@ plugins { id 'org.springframework.boot' version '3.1.2' id 'io.spring.dependency-management' version '1.1.0' id 'java' + id("io.freefair.lombok") version "8.7.1" } targetCompatibility = '17' @@ -14,6 +15,10 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' testImplementation('org.springframework.boot:spring-boot-starter-test') + + implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.15.2' + implementation 'com.fasterxml.jackson:jackson-bom:2.15.2' +// implementation 'jakarta.xml.bind:jakarta.xml.bind-api:4.0.0' } test { diff --git a/src/main/java/com/wilkins/showcase/ShowcaseApplication.java b/src/main/java/com/wilkins/showcase/ShowcaseApplication.java index e2e2586..a2a21eb 100644 --- a/src/main/java/com/wilkins/showcase/ShowcaseApplication.java +++ b/src/main/java/com/wilkins/showcase/ShowcaseApplication.java @@ -1,12 +1,15 @@ package com.wilkins.showcase; +import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +@Slf4j @SpringBootApplication public class ShowcaseApplication { public static void main(String[] args) { SpringApplication.run(ShowcaseApplication.class, args); } + } diff --git a/src/main/java/com/wilkins/showcase/WebConfig.java b/src/main/java/com/wilkins/showcase/WebConfig.java new file mode 100644 index 0000000..2f58d8e --- /dev/null +++ b/src/main/java/com/wilkins/showcase/WebConfig.java @@ -0,0 +1,15 @@ +package com.wilkins.showcase; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.web.client.RestTemplateCustomizer; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Slf4j +@Configuration +public class WebConfig implements RestTemplateCustomizer { + @Override + public void customize(RestTemplate restTemplate) { + log.info("Rest template message converters: {}", restTemplate.getMessageConverters()); + } +} diff --git a/src/main/java/com/wilkins/showcase/controller/DownloadController.java b/src/main/java/com/wilkins/showcase/controller/DownloadController.java new file mode 100644 index 0000000..47036b4 --- /dev/null +++ b/src/main/java/com/wilkins/showcase/controller/DownloadController.java @@ -0,0 +1,37 @@ +package com.wilkins.showcase.controller; + +import org.springframework.http.ContentDisposition; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; + +import java.io.IOException; +import java.io.OutputStream; + +import static org.springframework.http.HttpHeaders.CONTENT_DISPOSITION; +import static org.springframework.http.ResponseEntity.ok; + +@RestController +@RequestMapping("/download") +public class DownloadController { + @GetMapping(produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) + public ResponseEntity download() { + return ok() + .header(CONTENT_DISPOSITION, ContentDisposition + .builder("pdf") + .filename("demo.pdf") + .build().toString()) + .body(this::writeFileToStream); + } + + private void writeFileToStream(OutputStream outputStream) { + try (var inputStream = DownloadController.class.getClassLoader().getResourceAsStream("Test.pdf")) { + outputStream.write(inputStream.readAllBytes()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/com/wilkins/showcase/controller/Person.java b/src/main/java/com/wilkins/showcase/controller/Person.java new file mode 100644 index 0000000..088a057 --- /dev/null +++ b/src/main/java/com/wilkins/showcase/controller/Person.java @@ -0,0 +1,6 @@ +package com.wilkins.showcase.controller; + +import java.math.BigDecimal; + +public record Person(String firstName, String lastName, BigDecimal salary) { +} diff --git a/src/main/java/com/wilkins/showcase/controller/UploadController.java b/src/main/java/com/wilkins/showcase/controller/UploadController.java new file mode 100644 index 0000000..eb03080 --- /dev/null +++ b/src/main/java/com/wilkins/showcase/controller/UploadController.java @@ -0,0 +1,33 @@ +package com.wilkins.showcase.controller; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.math.BigDecimal; + +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import static org.springframework.http.MediaType.APPLICATION_XML_VALUE; + +@RequestMapping("/upload") +@RestController +@Slf4j +public class UploadController { + + @PostMapping(consumes = {APPLICATION_XML_VALUE, "application/xml;charset=UTF-8"}) + public ResponseEntity uploadXml(@RequestBody Person person) { + log.info("xml person: {}", person); + var personWithSalary = new Person(person.firstName(), person.lastName(), BigDecimal.valueOf(123456789000000.0)); + return ResponseEntity.ok(personWithSalary); + } + + @PostMapping(consumes = APPLICATION_JSON_VALUE, produces = APPLICATION_JSON_VALUE) + public ResponseEntity uploadJson(@RequestBody Person person) { + log.info("json person: {}", person); + var personWithSalary = new Person(person.firstName(), person.lastName(), BigDecimal.valueOf(123456789.0)); + return ResponseEntity.ok(personWithSalary); + } +} diff --git a/src/main/resources/Test.pdf b/src/main/resources/Test.pdf new file mode 100644 index 0000000000000000000000000000000000000000..cef2df05bd5e5c3fab8cb591d167940952b947f0 GIT binary patch literal 5446 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 Uz<}>K9|c3AA>!hCCi;;70DkZeL;wH) literal 0 HcmV?d00001 diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 8d50475..664b58c 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,3 +1,8 @@ +spring: + jackson: + serialization: + WRITE_BIGDECIMAL_AS_PLAIN: true + server: port: 8080 servlet: diff --git a/src/main/resources/public/index.html b/src/main/resources/public/index.html new file mode 100644 index 0000000..dc15662 --- /dev/null +++ b/src/main/resources/public/index.html @@ -0,0 +1,4 @@ + +

Demo

+
Download + \ No newline at end of file diff --git a/src/test/java/com/wilkins/showcase/controller/DownloadControllerTest.java b/src/test/java/com/wilkins/showcase/controller/DownloadControllerTest.java new file mode 100644 index 0000000..06574c2 --- /dev/null +++ b/src/test/java/com/wilkins/showcase/controller/DownloadControllerTest.java @@ -0,0 +1,30 @@ +package com.wilkins.showcase.controller; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.log; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class DownloadControllerTest { + + MockMvc mockMvc; + + @BeforeEach + void beforeEach() { + mockMvc = MockMvcBuilders.standaloneSetup(new DownloadController()).build(); + } + + @Test + void canDownloadFile() throws Exception { + var result = mockMvc.perform(MockMvcRequestBuilders.get("/download")) + .andExpect(status().isOk()) + .andExpect(request().asyncStarted()) + .andDo(log()) + .andReturn(); + } +} \ No newline at end of file diff --git a/src/test/java/com/wilkins/showcase/controller/UploadControllerTest.java b/src/test/java/com/wilkins/showcase/controller/UploadControllerTest.java new file mode 100644 index 0000000..6b1efec --- /dev/null +++ b/src/test/java/com/wilkins/showcase/controller/UploadControllerTest.java @@ -0,0 +1,69 @@ +package com.wilkins.showcase.controller; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; +import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class UploadControllerTest { + + MockMvc mockMvc; + + @BeforeEach + void setUp() { + var messageConverter = new MappingJackson2XmlHttpMessageConverter(); + var xmlMapper = new XmlMapper(); + xmlMapper.enable(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN); + xmlMapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY); + messageConverter.setObjectMapper(xmlMapper); + mockMvc = MockMvcBuilders + .standaloneSetup(new UploadController()) + .setMessageConverters(messageConverter) + .build(); + } + + @Test + void canUploadXml() throws Exception { + var response = mockMvc.perform(post("/upload") + .contentType(MediaType.APPLICATION_XML) + .content(""" + + Bob + Fish + + """)) + .andExpect(status().isOk()) + .andReturn() + .getResponse() + .getContentAsString(); + + assertThat(response).doesNotContain("E+"); + } + + @Test + void canUploadXmlWithEmptyValues() throws Exception { + var response = mockMvc.perform(post("/upload") + .contentType(MediaType.APPLICATION_XML) + .content(""" + + Bob + + + """)) + .andExpect(status().isOk()) + .andReturn() + .getResponse() + .getContentAsString(); + + assertThat(response).doesNotContain("E+"); + } +} \ No newline at end of file