diff --git a/spring-core-4/pom.xml b/spring-core-4/pom.xml index 1bbbb2dd5dfc..15c2e1037617 100644 --- a/spring-core-4/pom.xml +++ b/spring-core-4/pom.xml @@ -5,6 +5,18 @@ 4.0.0 spring-core-4 spring-core-4 + + + + org.apache.maven.plugins + maven-compiler-plugin + + 17 + 17 + + + + com.baeldung @@ -70,6 +82,44 @@ commons-text ${commons-text.version} + + org.springframework + spring-webflux + ${spring-webflux.version} + + + org.mockito + mockito-junit-jupiter + ${mockito.version} + test + + + io.projectreactor + reactor-test + ${reactor.version} + test + + + com.github.tomakehurst + wiremock-jre8-standalone + ${wiremock.version} + test + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + com.fasterxml.jackson.core + jackson-core + ${jackson.version} + + + com.fasterxml.jackson.core + jackson-annotations + ${jackson.version} + @@ -77,6 +127,11 @@ 6.1.0 3.0.0 1.10.0 + 6.2.9 + 5.18.0 + 3.7.8 + 2.35.0 + 2.19.1 diff --git a/spring-core-4/src/main/java/com/baeldung/parametrizedtypereference/ApiException.java b/spring-core-4/src/main/java/com/baeldung/parametrizedtypereference/ApiException.java new file mode 100644 index 000000000000..a6710f5eb590 --- /dev/null +++ b/spring-core-4/src/main/java/com/baeldung/parametrizedtypereference/ApiException.java @@ -0,0 +1,12 @@ +package com.baeldung.parametrizedtypereference; + +public class ApiException extends RuntimeException { + + public ApiException(String message) { + super(message); + } + + public ApiException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/spring-core-4/src/main/java/com/baeldung/parametrizedtypereference/ApiResponse.java b/spring-core-4/src/main/java/com/baeldung/parametrizedtypereference/ApiResponse.java new file mode 100644 index 000000000000..290fff4ff186 --- /dev/null +++ b/spring-core-4/src/main/java/com/baeldung/parametrizedtypereference/ApiResponse.java @@ -0,0 +1,5 @@ +package com.baeldung.parametrizedtypereference; + +public record ApiResponse(boolean success, String message, T data) { + +} diff --git a/spring-core-4/src/main/java/com/baeldung/parametrizedtypereference/ApiService.java b/spring-core-4/src/main/java/com/baeldung/parametrizedtypereference/ApiService.java new file mode 100644 index 000000000000..5f1e2812a6f8 --- /dev/null +++ b/spring-core-4/src/main/java/com/baeldung/parametrizedtypereference/ApiService.java @@ -0,0 +1,70 @@ +package com.baeldung.parametrizedtypereference; + +import static com.baeldung.parametrizedtypereference.TypeReferences.USER_LIST; + +import java.util.List; + +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +@Service +public class ApiService { + + private final RestTemplate restTemplate; + private final String baseUrl; + + public ApiService(RestTemplate restTemplate, String baseUrl) { + this.restTemplate = restTemplate; + this.baseUrl = baseUrl; + } + + public List fetchUserList() { + ParameterizedTypeReference> typeRef = new ParameterizedTypeReference>() { + }; + + ResponseEntity> response = restTemplate.exchange(baseUrl + "/api/users", HttpMethod.GET, null, typeRef); + + return response.getBody(); + } + + public List fetchUsersWrongApproach() { + ResponseEntity response = restTemplate.getForEntity(baseUrl + "/api/users", List.class); + + return (List) response.getBody(); + } + + public List fetchUsersCorrectApproach() { + ParameterizedTypeReference> typeRef = new ParameterizedTypeReference>() { + }; + + ResponseEntity> response = restTemplate.exchange(baseUrl + "/api/users", HttpMethod.GET, null, typeRef); + + return response.getBody(); + } + + public User fetchUser(Long id) { + return restTemplate.getForObject(baseUrl + "/api/users/" + id, User.class); + } + + public User[] fetchUsersArray() { + return restTemplate.getForObject(baseUrl + "/api/users", User[].class); + } + + public List fetchUsersList() { + ParameterizedTypeReference> typeRef = new ParameterizedTypeReference>() { + }; + + ResponseEntity> response = restTemplate.exchange(baseUrl + "/api/users", HttpMethod.GET, null, typeRef); + + return response.getBody(); + } + + public List fetchUsersListWithExistingReference() { + ResponseEntity> response = restTemplate.exchange(baseUrl + "/api/users", HttpMethod.GET, null, USER_LIST); + + return response.getBody(); + } +} \ No newline at end of file diff --git a/spring-core-4/src/main/java/com/baeldung/parametrizedtypereference/ReactiveApiService.java b/spring-core-4/src/main/java/com/baeldung/parametrizedtypereference/ReactiveApiService.java new file mode 100644 index 000000000000..d0fbf2e50b76 --- /dev/null +++ b/spring-core-4/src/main/java/com/baeldung/parametrizedtypereference/ReactiveApiService.java @@ -0,0 +1,43 @@ +package com.baeldung.parametrizedtypereference; + +import java.util.List; +import java.util.Map; + +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; + +import reactor.core.publisher.Mono; + +@Service +public class ReactiveApiService { + + private final WebClient webClient; + + public ReactiveApiService(String baseUrl) { + this.webClient = WebClient.builder() + .baseUrl(baseUrl) + .build(); + } + + public Mono>> fetchUsersByDepartment() { + ParameterizedTypeReference>> typeRef = new ParameterizedTypeReference>>() { + }; + + return webClient.get() + .uri("/users/by-department") + .retrieve() + .bodyToMono(typeRef); + } + + public Mono>> fetchUsersWithWrapper() { + ParameterizedTypeReference>> typeRef = new ParameterizedTypeReference>>() { + }; + + return webClient.get() + .uri("/users/wrapped") + .retrieve() + .bodyToMono(typeRef); + } + +} \ No newline at end of file diff --git a/spring-core-4/src/main/java/com/baeldung/parametrizedtypereference/TypeReferences.java b/spring-core-4/src/main/java/com/baeldung/parametrizedtypereference/TypeReferences.java new file mode 100644 index 000000000000..fd5e888a4ce4 --- /dev/null +++ b/spring-core-4/src/main/java/com/baeldung/parametrizedtypereference/TypeReferences.java @@ -0,0 +1,16 @@ +package com.baeldung.parametrizedtypereference; + +import java.util.List; +import java.util.Map; + +import org.springframework.core.ParameterizedTypeReference; + +public class TypeReferences { + + public static final ParameterizedTypeReference> USER_LIST = new ParameterizedTypeReference>() { + }; + + public static final ParameterizedTypeReference>> USER_MAP = new ParameterizedTypeReference>>() { + }; + +} diff --git a/spring-core-4/src/main/java/com/baeldung/parametrizedtypereference/User.java b/spring-core-4/src/main/java/com/baeldung/parametrizedtypereference/User.java new file mode 100644 index 000000000000..037f0d8fbeda --- /dev/null +++ b/spring-core-4/src/main/java/com/baeldung/parametrizedtypereference/User.java @@ -0,0 +1,52 @@ +package com.baeldung.parametrizedtypereference; + +public class User { + + private Long id; + private String name; + private String email; + private String department; + + public User() { + } + + public User(Long id, String name, String email, String department) { + this.id = id; + this.name = name; + this.email = email; + this.department = department; + } + + // Getters and setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getDepartment() { + return department; + } + + public void setDepartment(String department) { + this.department = department; + } +} diff --git a/spring-core-4/src/test/java/com/baeldung/parametrizedtypereference/ApiServiceUnitTest.java b/spring-core-4/src/test/java/com/baeldung/parametrizedtypereference/ApiServiceUnitTest.java new file mode 100644 index 000000000000..db090aeae2d3 --- /dev/null +++ b/spring-core-4/src/test/java/com/baeldung/parametrizedtypereference/ApiServiceUnitTest.java @@ -0,0 +1,154 @@ +package com.baeldung.parametrizedtypereference; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.web.client.RestTemplate; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; + +class ApiServiceUnitTest { + + private WireMockServer wireMockServer; + private ApiService apiService; + private String baseUrl; + + @BeforeEach + void setUp() { + wireMockServer = new WireMockServer(8089); + wireMockServer.start(); + WireMock.configureFor("localhost", 8089); + + baseUrl = "http://localhost:8089"; + apiService = new ApiService(new RestTemplate(), baseUrl); + } + + @AfterEach + void tearDown() { + wireMockServer.stop(); + } + + @Test + void whenFetchingUserList_thenReturnsCorrectType() { + // given + wireMockServer.stubFor(get(urlEqualTo("/api/users")).willReturn(aResponse().withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(""" + [ + {"id": 1, "name": "John Doe", "email": "john@example.com", "department": "Engineering"}, + {"id": 2, "name": "Jane Smith", "email": "jane@example.com", "department": "Marketing"} + ] + """))); + + // when + List result = apiService.fetchUserList(); + + // then + assertEquals(2, result.size()); + assertEquals("John Doe", result.get(0) + .getName()); + assertEquals("jane@example.com", result.get(1) + .getEmail()); + assertEquals("Engineering", result.get(0) + .getDepartment()); + assertEquals("Marketing", result.get(1) + .getDepartment()); + } + + @Test + void whenFetchingUsersCorrectApproach_thenReturnsTypedList() { + // given + wireMockServer.stubFor(get(urlEqualTo("/api/users")).willReturn(aResponse().withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(""" + [ + {"id": 1, "name": "John Doe", "email": "john@example.com", "department": "Engineering"} + ] + """))); + + // when + List result = apiService.fetchUsersCorrectApproach(); + + // then + assertEquals(1, result.size()); + assertEquals("John Doe", result.get(0) + .getName()); + assertEquals("Engineering", result.get(0) + .getDepartment()); + } + + @Test + void whenFetchingSingleUser_thenReturnsUser() { + // given + wireMockServer.stubFor(get(urlEqualTo("/api/users/1")).willReturn(aResponse().withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(""" + {"id": 1, "name": "John Doe", "email": "john@example.com", "department": "Engineering"} + """))); + + // when + User result = apiService.fetchUser(1L); + + // then + assertEquals("John Doe", result.getName()); + assertEquals("john@example.com", result.getEmail()); + assertEquals("Engineering", result.getDepartment()); + } + + @Test + void whenFetchingUsersArray_thenReturnsArray() { + // given + wireMockServer.stubFor(get(urlEqualTo("/api/users")).willReturn(aResponse().withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(""" + [ + {"id": 1, "name": "John Doe", "email": "john@example.com", "department": "Engineering"}, + {"id": 2, "name": "Jane Smith", "email": "jane@example.com", "department": "Marketing"} + ] + """))); + + // when + User[] result = apiService.fetchUsersArray(); + + // then + assertEquals(2, result.length); + assertEquals("John Doe", result[0].getName()); + assertEquals("Jane Smith", result[1].getName()); + } + + @Test + void whenFetchingUsersList_thenReturnsTypedList() { + // given + wireMockServer.stubFor(get(urlEqualTo("/api/users")).willReturn(aResponse().withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(""" + [ + {"id": 1, "name": "John Doe", "email": "john@example.com", "department": "Engineering"}, + {"id": 2, "name": "Jane Smith", "email": "jane@example.com", "department": "Marketing"} + ] + """))); + + // when + List result = apiService.fetchUsersList(); + + // then + assertEquals(2, result.size()); + assertEquals("John Doe", result.get(0) + .getName()); + assertEquals("Jane Smith", result.get(1) + .getName()); + + // Verify that we actually get a typed List, not List + // This test would fail with ClassCastException if ParameterizedTypeReference wasn't working + User firstUser = result.get(0); + assertEquals(Long.valueOf(1L), firstUser.getId()); + } +} \ No newline at end of file diff --git a/spring-core-4/src/test/java/com/baeldung/parametrizedtypereference/ReactiveApiServiceUnitTest.java b/spring-core-4/src/test/java/com/baeldung/parametrizedtypereference/ReactiveApiServiceUnitTest.java new file mode 100644 index 000000000000..21efc3e60a6a --- /dev/null +++ b/spring-core-4/src/test/java/com/baeldung/parametrizedtypereference/ReactiveApiServiceUnitTest.java @@ -0,0 +1,159 @@ +package com.baeldung.parametrizedtypereference; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; + +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +class ReactiveApiServiceUnitTest { + + private WireMockServer wireMockServer; + private ReactiveApiService reactiveApiService; + private String baseUrl; + + @BeforeEach + void setUp() { + wireMockServer = new WireMockServer(8090); + wireMockServer.start(); + WireMock.configureFor("localhost", 8090); + + baseUrl = "http://localhost:8090"; + reactiveApiService = new ReactiveApiService(baseUrl); + } + + @AfterEach + void tearDown() { + wireMockServer.stop(); + } + + @Test + void whenFetchingUsersByDepartment_thenReturnsCorrectMap() { + // given + wireMockServer.stubFor(get(urlEqualTo("/users/by-department")).willReturn(aResponse().withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(""" + { + "Engineering": [ + {"id": 1, "name": "John Doe", "email": "john@example.com", "department": "Engineering"} + ], + "Marketing": [ + {"id": 2, "name": "Jane Smith", "email": "jane@example.com", "department": "Marketing"} + ] + } + """))); + + // when + Mono>> result = reactiveApiService.fetchUsersByDepartment(); + + // then + StepVerifier.create(result) + .assertNext(map -> { + assertTrue(map.containsKey("Engineering")); + assertTrue(map.containsKey("Marketing")); + assertEquals("John Doe", map.get("Engineering") + .get(0) + .getName()); + assertEquals("Jane Smith", map.get("Marketing") + .get(0) + .getName()); + + // Verify proper typing - this would fail if ParameterizedTypeReference didn't work + List engineeringUsers = map.get("Engineering"); + User firstUser = engineeringUsers.get(0); + assertEquals(Long.valueOf(1L), firstUser.getId()); + }) + .verifyComplete(); + } + + @Test + void whenFetchingUsersWithWrapper_thenReturnsApiResponse() { + // given + wireMockServer.stubFor(get(urlEqualTo("/users/wrapped")).willReturn(aResponse().withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(""" + { + "success": true, + "message": "Success", + "data": [ + {"id": 1, "name": "John Doe", "email": "john@example.com", "department": "Engineering"}, + {"id": 2, "name": "Jane Smith", "email": "jane@example.com", "department": "Marketing"} + ] + } + """))); + + // when + Mono>> result = reactiveApiService.fetchUsersWithWrapper(); + + // then + StepVerifier.create(result) + .assertNext(response -> { + assertTrue(response.success()); + assertEquals("Success", response.message()); + assertEquals(2, response.data() + .size()); + assertEquals("John Doe", response.data() + .get(0) + .getName()); + assertEquals("Jane Smith", response.data() + .get(1) + .getName()); + + // Verify proper generic typing - this ensures ParameterizedTypeReference worked + List users = response.data(); + User firstUser = users.get(0); + assertEquals(Long.valueOf(1L), firstUser.getId()); + assertEquals("Engineering", firstUser.getDepartment()); + }) + .verifyComplete(); + } + + @Test + void whenApiReturnsError_thenHandlesGracefully() { + // given + wireMockServer.stubFor(get(urlEqualTo("/users/by-department")).willReturn(aResponse().withStatus(500) + .withHeader("Content-Type", "application/json") + .withBody(""" + {"error": "Internal server error"} + """))); + + // when + Mono>> result = reactiveApiService.fetchUsersByDepartment(); + + // then + StepVerifier.create(result) + .expectError() + .verify(); + } + + @Test + void whenEmptyResponse_thenHandlesCorrectly() { + // given + wireMockServer.stubFor(get(urlEqualTo("/users/by-department")).willReturn(aResponse().withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{}"))); + + // when + Mono>> result = reactiveApiService.fetchUsersByDepartment(); + + // then + StepVerifier.create(result) + .assertNext(map -> { + assertTrue(map.isEmpty()); + }) + .verifyComplete(); + } +} \ No newline at end of file