From aed94f94be9e1ca1c300870985edb9168673e03c Mon Sep 17 00:00:00 2001 From: Raja Kolli Date: Mon, 23 Oct 2023 14:25:01 +0000 Subject: [PATCH] feat : handle service calls efficiently --- .vscode/launch.json | 10 ++ .../proxy/client/JsonPlaceholderService.java | 19 +++- .../exception/PostNotFoundException.java | 25 +++++ .../rest/proxy/services/PostService.java | 31 +++++- .../src/main/resources/application.properties | 9 -- .../src/main/resources/logback-spring.xml | 8 +- .../proxy/ApplicationIntegrationTest.java | 10 -- .../example/rest/proxy/TestApplication.java | 18 +++- .../proxy/common/AbstractIntegrationTest.java | 6 +- .../config/DBTestContainersConfiguration.java | 18 ---- .../rest/proxy/services/PostServiceTest.java | 95 ------------------- 11 files changed, 103 insertions(+), 146 deletions(-) create mode 100644 httpClients/boot-http-proxy/src/main/java/com/example/rest/proxy/exception/PostNotFoundException.java delete mode 100644 httpClients/boot-http-proxy/src/test/java/com/example/rest/proxy/ApplicationIntegrationTest.java delete mode 100644 httpClients/boot-http-proxy/src/test/java/com/example/rest/proxy/config/DBTestContainersConfiguration.java delete mode 100644 httpClients/boot-http-proxy/src/test/java/com/example/rest/proxy/services/PostServiceTest.java diff --git a/.vscode/launch.json b/.vscode/launch.json index 0d1eb40ef..93702c1c5 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -83,6 +83,16 @@ "projectName": "boot-ultimate-redis", "args": "", "envFile": "${workspaceFolder}/.env" + }, + { + "type": "java", + "name": "Spring Boot-TestApplication", + "request": "launch", + "cwd": "${workspaceFolder}", + "mainClass": "com.example.rest.proxy.TestApplication", + "projectName": "boot-http-proxy", + "args": "", + "envFile": "${workspaceFolder}/.env" } ] } \ No newline at end of file diff --git a/httpClients/boot-http-proxy/src/main/java/com/example/rest/proxy/client/JsonPlaceholderService.java b/httpClients/boot-http-proxy/src/main/java/com/example/rest/proxy/client/JsonPlaceholderService.java index b9d8e788e..55c8e48c6 100644 --- a/httpClients/boot-http-proxy/src/main/java/com/example/rest/proxy/client/JsonPlaceholderService.java +++ b/httpClients/boot-http-proxy/src/main/java/com/example/rest/proxy/client/JsonPlaceholderService.java @@ -2,12 +2,27 @@ import com.example.rest.proxy.entities.Post; import java.util.List; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.service.annotation.DeleteExchange; import org.springframework.web.service.annotation.GetExchange; -import org.springframework.web.service.annotation.HttpExchange; +import org.springframework.web.service.annotation.PostExchange; +import org.springframework.web.service.annotation.PutExchange; -@HttpExchange(url = "https://jsonplaceholder.typicode.com") public interface JsonPlaceholderService { @GetExchange("/posts") List loadAllPosts(); + + @GetExchange("/posts/{id}") + Post loadPostById(@PathVariable Long id); + + @PostExchange("/posts") + Post createPost(@RequestBody Post post); + + @PutExchange("/posts/{id}") + Post updatePostById(@PathVariable Long id, @RequestBody Post post); + + @DeleteExchange("/posts/{id}") + Post deletePostById(@PathVariable Long id); } diff --git a/httpClients/boot-http-proxy/src/main/java/com/example/rest/proxy/exception/PostNotFoundException.java b/httpClients/boot-http-proxy/src/main/java/com/example/rest/proxy/exception/PostNotFoundException.java new file mode 100644 index 000000000..c1db80e0f --- /dev/null +++ b/httpClients/boot-http-proxy/src/main/java/com/example/rest/proxy/exception/PostNotFoundException.java @@ -0,0 +1,25 @@ +package com.example.rest.proxy.exception; + +import java.net.URI; +import java.time.Instant; +import org.springframework.http.HttpStatus; +import org.springframework.http.ProblemDetail; +import org.springframework.web.ErrorResponseException; + +public class PostNotFoundException extends ErrorResponseException { + + public PostNotFoundException(Long id) { + super(HttpStatus.NOT_FOUND, asProblemDetail(id), null); + } + + private static ProblemDetail asProblemDetail(Long id) { + ProblemDetail problemDetail = + ProblemDetail.forStatusAndDetail( + HttpStatus.NOT_FOUND, "Post with Id '%d' not found".formatted(id)); + problemDetail.setTitle("Post Not Found"); + problemDetail.setType(URI.create("http://api.posts.com/errors/not-found")); + problemDetail.setProperty("errorCategory", "Generic"); + problemDetail.setProperty("timestamp", Instant.now()); + return problemDetail; + } +} diff --git a/httpClients/boot-http-proxy/src/main/java/com/example/rest/proxy/services/PostService.java b/httpClients/boot-http-proxy/src/main/java/com/example/rest/proxy/services/PostService.java index 17afe473c..1e94e1e73 100644 --- a/httpClients/boot-http-proxy/src/main/java/com/example/rest/proxy/services/PostService.java +++ b/httpClients/boot-http-proxy/src/main/java/com/example/rest/proxy/services/PostService.java @@ -1,16 +1,21 @@ package com.example.rest.proxy.services; +import com.example.rest.proxy.client.JsonPlaceholderService; import com.example.rest.proxy.entities.Post; +import com.example.rest.proxy.exception.PostNotFoundException; import com.example.rest.proxy.model.response.PagedResult; import com.example.rest.proxy.repositories.PostRepository; import java.util.Optional; +import java.util.function.Function; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; +import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.client.HttpClientErrorException; @Service @Transactional @@ -18,6 +23,7 @@ public class PostService { private final PostRepository postRepository; + private final JsonPlaceholderService jsonPlaceholderService; public PagedResult findAllPosts(int pageNo, int pageSize, String sortBy, String sortDir) { Sort sort = @@ -33,14 +39,35 @@ public PagedResult findAllPosts(int pageNo, int pageSize, String sortBy, S } public Optional findPostById(Long id) { - return postRepository.findById(id); + Optional optionalPost = postRepository.findById(id); + if (optionalPost.isPresent()) { + return optionalPost; + } else { + Function loadPostById = jsonPlaceholderService::loadPostById; + return Optional.of(callService(id, loadPostById.andThen(this::savePost))); + } } public Post savePost(Post post) { - return postRepository.save(post); + Post savedPost = jsonPlaceholderService.createPost(post); + return postRepository.save(savedPost); } public void deletePostById(Long id) { + jsonPlaceholderService.deletePostById(id); postRepository.deleteById(id); } + + private T callService(Long id, Function serviceFunction) { + T result; + try { + result = serviceFunction.apply(id); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + throw new PostNotFoundException(id); + } + throw exception; + } + return result; + } } diff --git a/httpClients/boot-http-proxy/src/main/resources/application.properties b/httpClients/boot-http-proxy/src/main/resources/application.properties index 378283b55..98053acea 100644 --- a/httpClients/boot-http-proxy/src/main/resources/application.properties +++ b/httpClients/boot-http-proxy/src/main/resources/application.properties @@ -4,15 +4,6 @@ server.shutdown=graceful spring.main.allow-bean-definition-overriding=true spring.jmx.enabled=false -################ Logging ##################### -logging.file.name=logs/${spring.application.name}.log -logging.level.web=INFO -logging.level.sql=INFO -## To enable transaction details logging -#logging.level.org.springframework.orm.jpa=DEBUG -#logging.level.org.springframework.transaction=DEBUG -#logging.level.org.hibernate.engine.transaction.internal.TransactionImpl=DEBUG - ################ Actuator ##################### management.endpoints.web.exposure.include=configprops,env,health,info,logfile,loggers,metrics management.endpoint.health.show-details=always diff --git a/httpClients/boot-http-proxy/src/main/resources/logback-spring.xml b/httpClients/boot-http-proxy/src/main/resources/logback-spring.xml index 5fae54896..04f130c99 100644 --- a/httpClients/boot-http-proxy/src/main/resources/logback-spring.xml +++ b/httpClients/boot-http-proxy/src/main/resources/logback-spring.xml @@ -1,16 +1,18 @@ - - + + + + @@ -19,5 +21,7 @@ + + diff --git a/httpClients/boot-http-proxy/src/test/java/com/example/rest/proxy/ApplicationIntegrationTest.java b/httpClients/boot-http-proxy/src/test/java/com/example/rest/proxy/ApplicationIntegrationTest.java deleted file mode 100644 index fa6539709..000000000 --- a/httpClients/boot-http-proxy/src/test/java/com/example/rest/proxy/ApplicationIntegrationTest.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.example.rest.proxy; - -import com.example.rest.proxy.common.AbstractIntegrationTest; -import org.junit.jupiter.api.Test; - -class ApplicationIntegrationTest extends AbstractIntegrationTest { - - @Test - void contextLoads() {} -} diff --git a/httpClients/boot-http-proxy/src/test/java/com/example/rest/proxy/TestApplication.java b/httpClients/boot-http-proxy/src/test/java/com/example/rest/proxy/TestApplication.java index 386ff9b2a..c3d4061f8 100644 --- a/httpClients/boot-http-proxy/src/test/java/com/example/rest/proxy/TestApplication.java +++ b/httpClients/boot-http-proxy/src/test/java/com/example/rest/proxy/TestApplication.java @@ -1,13 +1,23 @@ package com.example.rest.proxy; -import com.example.rest.proxy.config.DBTestContainersConfiguration; import org.springframework.boot.SpringApplication; +import org.springframework.boot.devtools.restart.RestartScope; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.context.annotation.Bean; +import org.testcontainers.containers.PostgreSQLContainer; +@TestConfiguration(proxyBeanMethods = false) public class TestApplication { + @Bean + @ServiceConnection + @RestartScope + PostgreSQLContainer postgresContainer() { + return new PostgreSQLContainer<>("postgres:16.0-alpine"); + } + public static void main(String[] args) { - SpringApplication.from(Application::main) - .with(DBTestContainersConfiguration.class) - .run(args); + SpringApplication.from(Application::main).with(TestApplication.class).run(args); } } diff --git a/httpClients/boot-http-proxy/src/test/java/com/example/rest/proxy/common/AbstractIntegrationTest.java b/httpClients/boot-http-proxy/src/test/java/com/example/rest/proxy/common/AbstractIntegrationTest.java index 8d4c8a1bd..fa24f979b 100644 --- a/httpClients/boot-http-proxy/src/test/java/com/example/rest/proxy/common/AbstractIntegrationTest.java +++ b/httpClients/boot-http-proxy/src/test/java/com/example/rest/proxy/common/AbstractIntegrationTest.java @@ -3,18 +3,16 @@ import static com.example.rest.proxy.utils.AppConstants.PROFILE_TEST; import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; -import com.example.rest.proxy.config.DBTestContainersConfiguration; +import com.example.rest.proxy.TestApplication; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.context.annotation.Import; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; @ActiveProfiles({PROFILE_TEST}) -@SpringBootTest(webEnvironment = RANDOM_PORT) -@Import(DBTestContainersConfiguration.class) +@SpringBootTest(webEnvironment = RANDOM_PORT, classes = TestApplication.class) @AutoConfigureMockMvc public abstract class AbstractIntegrationTest { diff --git a/httpClients/boot-http-proxy/src/test/java/com/example/rest/proxy/config/DBTestContainersConfiguration.java b/httpClients/boot-http-proxy/src/test/java/com/example/rest/proxy/config/DBTestContainersConfiguration.java deleted file mode 100644 index 0847ef2e0..000000000 --- a/httpClients/boot-http-proxy/src/test/java/com/example/rest/proxy/config/DBTestContainersConfiguration.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.example.rest.proxy.config; - -import org.springframework.boot.devtools.restart.RestartScope; -import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.boot.testcontainers.service.connection.ServiceConnection; -import org.springframework.context.annotation.Bean; -import org.testcontainers.containers.PostgreSQLContainer; - -@TestConfiguration(proxyBeanMethods = false) -public class DBTestContainersConfiguration { - - @Bean - @ServiceConnection - @RestartScope - PostgreSQLContainer postgresContainer() { - return new PostgreSQLContainer<>("postgres:16.0-alpine"); - } -} diff --git a/httpClients/boot-http-proxy/src/test/java/com/example/rest/proxy/services/PostServiceTest.java b/httpClients/boot-http-proxy/src/test/java/com/example/rest/proxy/services/PostServiceTest.java deleted file mode 100644 index 43b782e4c..000000000 --- a/httpClients/boot-http-proxy/src/test/java/com/example/rest/proxy/services/PostServiceTest.java +++ /dev/null @@ -1,95 +0,0 @@ -package com.example.rest.proxy.services; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.times; -import static org.mockito.BDDMockito.verify; -import static org.mockito.BDDMockito.willDoNothing; - -import com.example.rest.proxy.entities.Post; -import com.example.rest.proxy.model.response.PagedResult; -import com.example.rest.proxy.repositories.PostRepository; -import java.util.List; -import java.util.Optional; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; - -@ExtendWith(MockitoExtension.class) -class PostServiceTest { - - @Mock private PostRepository postRepository; - - @InjectMocks private PostService postService; - - @Test - void findAllPosts() { - // given - Pageable pageable = PageRequest.of(0, 10, Sort.by(Sort.Direction.ASC, "id")); - Page postPage = new PageImpl<>(List.of(getPost())); - given(postRepository.findAll(pageable)).willReturn(postPage); - - // when - PagedResult pagedResult = postService.findAllPosts(0, 10, "id", "asc"); - - // then - assertThat(pagedResult).isNotNull(); - assertThat(pagedResult.data()).isNotEmpty().hasSize(1); - assertThat(pagedResult.hasNext()).isFalse(); - assertThat(pagedResult.pageNumber()).isEqualTo(1); - assertThat(pagedResult.totalPages()).isEqualTo(1); - assertThat(pagedResult.isFirst()).isTrue(); - assertThat(pagedResult.isLast()).isTrue(); - assertThat(pagedResult.hasPrevious()).isFalse(); - assertThat(pagedResult.totalElements()).isEqualTo(1); - } - - @Test - void findPostById() { - // given - given(postRepository.findById(1L)).willReturn(Optional.of(getPost())); - // when - Optional optionalPost = postService.findPostById(1L); - // then - assertThat(optionalPost).isPresent(); - Post post = optionalPost.get(); - assertThat(post.getId()).isEqualTo(1L); - assertThat(post.getTitle()).isEqualTo("junitTest"); - } - - @Test - void savePost() { - // given - given(postRepository.save(getPost())).willReturn(getPost()); - // when - Post persistedPost = postService.savePost(getPost()); - // then - assertThat(persistedPost).isNotNull(); - assertThat(persistedPost.getId()).isEqualTo(1L); - assertThat(persistedPost.getTitle()).isEqualTo("junitTest"); - } - - @Test - void deletePostById() { - // given - willDoNothing().given(postRepository).deleteById(1L); - // when - postService.deletePostById(1L); - // then - verify(postRepository, times(1)).deleteById(1L); - } - - private Post getPost() { - Post post = new Post(); - post.setId(1L); - post.setTitle("junitTest"); - return post; - } -}