-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(FSADT1-1307): adding search by ids
As a forest client api user I want to be able to submit a list of forest client ids So that the endpoint returns me a list of matching forest clients. Closes #217
- Loading branch information
1 parent
7c3fd7f
commit cc4f2d7
Showing
5 changed files
with
403 additions
and
1 deletion.
There are no files selected for viewing
120 changes: 120 additions & 0 deletions
120
src/main/java/ca/bc/gov/api/oracle/legacy/controller/ClientSearchController.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
package ca.bc.gov.api.oracle.legacy.controller; | ||
|
||
import ca.bc.gov.api.oracle.legacy.ApplicationConstants; | ||
import ca.bc.gov.api.oracle.legacy.dto.ClientPublicViewDto; | ||
import ca.bc.gov.api.oracle.legacy.service.ClientSearchService; | ||
import io.swagger.v3.oas.annotations.Operation; | ||
import io.swagger.v3.oas.annotations.Parameter; | ||
import io.swagger.v3.oas.annotations.headers.Header; | ||
import io.swagger.v3.oas.annotations.media.ArraySchema; | ||
import io.swagger.v3.oas.annotations.media.Content; | ||
import io.swagger.v3.oas.annotations.media.Schema; | ||
import io.swagger.v3.oas.annotations.responses.ApiResponse; | ||
import io.swagger.v3.oas.annotations.tags.Tag; | ||
import java.util.Arrays; | ||
import java.util.List; | ||
import lombok.RequiredArgsConstructor; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.springframework.http.MediaType; | ||
import org.springframework.http.server.reactive.ServerHttpResponse; | ||
import org.springframework.web.bind.annotation.GetMapping; | ||
import org.springframework.web.bind.annotation.RequestMapping; | ||
import org.springframework.web.bind.annotation.RequestParam; | ||
import org.springframework.web.bind.annotation.RestController; | ||
import reactor.core.publisher.Flux; | ||
|
||
/** | ||
* This is the main controller for the Client Search API. It is a REST controller that handles | ||
* requests to the /api/clients/search endpoint. It uses the ClientSearchService to perform | ||
* operations related to client search. It is annotated with @RestController, indicating that it is | ||
* a controller where every method returns a domain object instead of a view. It is also annotated | ||
* with @Slf4j, which provides a logger for the class to log information. The @Tag annotation | ||
* provides additional metadata for the API documentation. The @RequestMapping annotation maps | ||
* requests to the /api/clients/search endpoint to this controller. The @RequiredArgsConstructor | ||
* annotation generates a constructor with 1 parameter for each field that requires special | ||
* handling. | ||
*/ | ||
@RestController | ||
@Slf4j | ||
@Tag( | ||
name = "Client Search API", | ||
description = "Deals with search on client data" | ||
) | ||
@RequestMapping(value = "/api/clients/search", produces = MediaType.APPLICATION_JSON_VALUE) | ||
@RequiredArgsConstructor | ||
public class ClientSearchController { | ||
|
||
private final ClientSearchService clientSearchService; | ||
|
||
/** | ||
* This is a GET mapping for the searchClients endpoint. It searches for clients based on the | ||
* provided client IDs, page number, and page size. It first logs the IDs of the clients to be | ||
* searched. Then, it calls the searchClientByQuery method of the clientSearchService to retrieve | ||
* the clients. The searchClientByQuery method takes in a search criteria created by the | ||
* searchById method of the clientSearchService, the page number, and the page size. It logs the | ||
* client number of each retrieved client. It also sets the X_TOTAL_COUNT header of the server | ||
* response to the count of the retrieved clients. Finally, it returns a Flux stream of | ||
* ClientPublicViewDto objects. | ||
* | ||
* @param page The one index page number, defaults to 0. | ||
* @param size The amount of data to be returned per page, defaults to 10. | ||
* @param id The IDs of the clients to be searched. | ||
* @param serverResponse The server response to which the X_TOTAL_COUNT header is to be set. | ||
* @return A Flux stream of ClientPublicViewDto objects. | ||
*/ | ||
@GetMapping | ||
@Operation( | ||
summary = "Search for clients", | ||
description = "Search for clients based on the provided client IDs", | ||
tags = {"Client Search API"}, | ||
responses = { | ||
@ApiResponse( | ||
responseCode = "200", | ||
description = "Successfully retrieved clients", | ||
content = @Content( | ||
mediaType = MediaType.APPLICATION_JSON_VALUE, | ||
array = @ArraySchema( | ||
schema = @Schema( | ||
name = "ClientView", | ||
implementation = ClientPublicViewDto.class | ||
) | ||
) | ||
), | ||
headers = { | ||
@Header( | ||
name = ApplicationConstants.X_TOTAL_COUNT, | ||
description = "Total number of records found" | ||
) | ||
} | ||
) | ||
} | ||
) | ||
public Flux<ClientPublicViewDto> searchClients( | ||
@Parameter(description = "The one index page number, defaults to 0", example = "0") | ||
@RequestParam(value = "page", required = false, defaultValue = "0") | ||
Integer page, | ||
|
||
@Parameter(description = "The amount of data to be returned per page, defaults to 10", | ||
example = "10") | ||
@RequestParam(value = "size", required = false, defaultValue = "10") | ||
Integer size, | ||
|
||
@Parameter(description = "Id of the client you're searching", example = "00000001") | ||
@RequestParam(value = "id", required = false) | ||
List<String> id, | ||
|
||
ServerHttpResponse serverResponse | ||
) { | ||
log.info("Searching for clients with ids {}", id); | ||
return clientSearchService | ||
.searchClientByQuery( | ||
clientSearchService.searchById(id), | ||
page, | ||
size | ||
) | ||
.doOnNext(client -> log.info("Found client with id {}", client.getClientNumber())) | ||
.doOnNext(dto -> serverResponse.getHeaders() | ||
.putIfAbsent(ApplicationConstants.X_TOTAL_COUNT, List.of(dto.getCount().toString()))); | ||
} | ||
|
||
} |
106 changes: 106 additions & 0 deletions
106
src/main/java/ca/bc/gov/api/oracle/legacy/service/ClientSearchService.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
package ca.bc.gov.api.oracle.legacy.service; | ||
|
||
import static org.springframework.data.relational.core.query.Criteria.where; | ||
|
||
import ca.bc.gov.api.oracle.legacy.dto.ClientPublicViewDto; | ||
import ca.bc.gov.api.oracle.legacy.entity.ForestClientEntity; | ||
import ca.bc.gov.api.oracle.legacy.util.ClientMapper; | ||
import java.util.List; | ||
import lombok.RequiredArgsConstructor; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.springframework.data.domain.PageRequest; | ||
import org.springframework.data.domain.Sort; | ||
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate; | ||
import org.springframework.data.relational.core.query.Criteria; | ||
import org.springframework.data.relational.core.query.Query; | ||
import org.springframework.stereotype.Service; | ||
import reactor.core.publisher.Flux; | ||
|
||
@Service | ||
@RequiredArgsConstructor | ||
@Slf4j | ||
public class ClientSearchService { | ||
|
||
private final R2dbcEntityTemplate template; | ||
|
||
/** | ||
* This method is used to create a search criteria based on a list of client IDs. It first logs | ||
* the IDs of the clients to be searched. Then, it creates an empty query criteria. If the list of | ||
* IDs is not null and not empty, it adds a query criteria to search for clients with client | ||
* numbers in the IDs list. Finally, it returns the created search criteria. | ||
* | ||
* @param ids The list of client IDs to be used in the search criteria. | ||
* @return The created search criteria. | ||
*/ | ||
public Criteria searchById(List<String> ids) { | ||
log.info("Searching for clients with ids {}", ids); | ||
|
||
// Create an empty query criteria. | ||
Criteria queryCriteria = Criteria.empty(); | ||
|
||
// If the ids list is not empty, add a query criteria to search for clients with client numbers in the ids list. | ||
if (ids != null && !ids.isEmpty()) { | ||
queryCriteria = queryCriteria | ||
.and(where("clientNumber").in(ids)); | ||
} | ||
|
||
// Return search criteria | ||
return queryCriteria; | ||
} | ||
|
||
/** | ||
* This method is used to search for clients based on a given query criteria, page number, and | ||
* page size. It first creates a query based on the provided query criteria. Then, it counts the | ||
* total number of clients that match the search query. It retrieves the specific page of clients | ||
* based on the page number and size. The clients are sorted in ascending order by client number | ||
* and then by client name. Each retrieved client entity is then mapped to a DTO (Data Transfer | ||
* Object). The count of total matching clients is also set in each client DTO. Finally, it logs | ||
* the client number of each retrieved client. | ||
* | ||
* @param queryCriteria The criteria used to search for clients. | ||
* @param page The page number of the clients to retrieve. | ||
* @param size The number of clients to retrieve per page. | ||
* @return A Flux stream of ClientPublicViewDto objects. | ||
*/ | ||
public Flux<ClientPublicViewDto> searchClientByQuery( | ||
final Criteria queryCriteria, | ||
final Integer page, | ||
final Integer size | ||
) { | ||
// Create a query based on the query criteria. | ||
Query searchQuery = Query.query(queryCriteria); | ||
|
||
log.info("Searching for clients with query {} {}", | ||
queryCriteria, | ||
queryCriteria.isEmpty() | ||
); | ||
|
||
if(queryCriteria.isEmpty()) { | ||
return Flux.empty(); | ||
} | ||
|
||
// Count the total number of clients that match the search query. | ||
return template | ||
.count(searchQuery, ForestClientEntity.class) | ||
.doOnNext(count -> log.info("Found {} clients", count)) | ||
// Retrieve the clients based on the search query, page number, and size. | ||
.flatMapMany(count -> | ||
template | ||
.select( | ||
searchQuery | ||
.with(PageRequest.of(page, size)) | ||
.sort( | ||
Sort | ||
.by(Sort.Order.asc("clientNumber")) | ||
.and(Sort.by(Sort.Order.asc("clientName"))) | ||
), | ||
ForestClientEntity.class | ||
) | ||
// Map each client entity to a DTO and set the count of total matching clients. | ||
.map(ClientMapper::mapEntityToDto) | ||
// Add the total count on each retrieved client. | ||
.doOnNext(client -> client.setCount(count)) | ||
) | ||
.doOnNext(client -> log.info("Found client with id {}", client.getClientNumber())); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
49 changes: 49 additions & 0 deletions
49
...st/java/ca/bc/gov/api/oracle/legacy/controller/ClientSearchControllerIntegrationTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
package ca.bc.gov.api.oracle.legacy.controller; | ||
|
||
import ca.bc.gov.api.oracle.legacy.AbstractTestContainerIntegrationTest; | ||
import ca.bc.gov.api.oracle.legacy.dto.ClientPublicViewDto; | ||
import java.util.stream.Stream; | ||
import org.junit.jupiter.api.DisplayName; | ||
import org.junit.jupiter.params.ParameterizedTest; | ||
import org.junit.jupiter.params.provider.Arguments; | ||
import org.junit.jupiter.params.provider.MethodSource; | ||
import org.springframework.beans.factory.annotation.Autowired; | ||
import org.springframework.test.web.reactive.server.WebTestClient; | ||
|
||
@DisplayName("Integration Test | Client Search Handler") | ||
class ClientSearchControllerIntegrationTest extends AbstractTestContainerIntegrationTest { | ||
|
||
@Autowired | ||
private WebTestClient webTestClient; | ||
|
||
@ParameterizedTest | ||
@MethodSource("searchById") | ||
@DisplayName("Search clients by ID") | ||
void shouldSearchClientsById(Integer returnSize, Object[] ids) { | ||
|
||
System.out.printf("returnSize: %d, ids: %s%n", returnSize, ids); | ||
|
||
webTestClient | ||
.get() | ||
.uri(uriBuilder -> uriBuilder | ||
.path("/api/clients/search") | ||
.queryParam("id", ids) | ||
.build() | ||
) | ||
.exchange() | ||
.expectStatus().isOk() | ||
.expectBodyList(ClientPublicViewDto.class) | ||
.hasSize(returnSize); | ||
} | ||
|
||
|
||
private static Stream<Arguments> searchById() { | ||
return Stream.of( | ||
Arguments.of(1, new Object[]{"00000001"}), | ||
Arguments.of(1, new Object[]{"00000001", "1", "4"}), | ||
Arguments.of(0, new Object[]{"00000999"}), | ||
Arguments.of(0, null) | ||
); | ||
} | ||
|
||
} |
Oops, something went wrong.