Skip to content

Commit

Permalink
feat(FSADT1-1307): adding search by ids
Browse files Browse the repository at this point in the history
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
paulushcgcj committed Apr 23, 2024
1 parent 7c3fd7f commit cc4f2d7
Show file tree
Hide file tree
Showing 5 changed files with 403 additions and 1 deletion.
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())));
}

}
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()));
}
}
29 changes: 28 additions & 1 deletion src/main/java/ca/bc/gov/api/oracle/legacy/util/ClientMapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,20 @@
import lombok.AccessLevel;
import lombok.NoArgsConstructor;

/**
* This is a utility class that provides methods to map ForestClientEntity objects to ClientPublicViewDto and ClientViewDto objects.
* It is annotated with @NoArgsConstructor(access = AccessLevel.PRIVATE) to prevent instantiation of this utility class.
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class ClientMapper {

/**
* This method maps a ForestClientEntity object to a ClientPublicViewDto object.
* It takes in a ForestClientEntity object and returns a ClientPublicViewDto object with the same client details.
*
* @param clientEntity The ForestClientEntity object to be mapped.
* @return A ClientPublicViewDto object with the same client details as the provided ForestClientEntity object.
*/
public static ClientPublicViewDto mapEntityToDto(ForestClientEntity clientEntity) {
return ClientPublicViewDto
.builder()
Expand All @@ -21,6 +33,14 @@ public static ClientPublicViewDto mapEntityToDto(ForestClientEntity clientEntity
.build();
}

/**
* This method maps a ForestClientEntity object to a ClientPublicViewDto object and sets the count of total matching clients.
* It takes in a ForestClientEntity object and a count of total matching clients and returns a ClientPublicViewDto object with the same client details and the count.
*
* @param clientEntity The ForestClientEntity object to be mapped.
* @param count The count of total matching clients.
* @return A ClientPublicViewDto object with the same client details as the provided ForestClientEntity object and the count of total matching clients.
*/
public static ClientPublicViewDto mapEntityToDto(ForestClientEntity clientEntity, Long count) {
return ClientPublicViewDto
.builder()
Expand All @@ -35,6 +55,13 @@ public static ClientPublicViewDto mapEntityToDto(ForestClientEntity clientEntity
.build();
}

/**
* This method maps a ForestClientEntity object to a ClientViewDto object.
* It takes in a ForestClientEntity object and returns a ClientViewDto object with the same client details.
*
* @param clientEntity The ForestClientEntity object to be mapped.
* @return A ClientViewDto object with the same client details as the provided ForestClientEntity object.
*/
public static ClientViewDto mapEntityToClientViewDto(ForestClientEntity clientEntity) {
return ClientViewDto
.builder()
Expand All @@ -47,4 +74,4 @@ public static ClientViewDto mapEntityToClientViewDto(ForestClientEntity clientEn
.acronym(clientEntity.getClientAcronym())
.build();
}
}
}
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)
);
}

}
Loading

0 comments on commit cc4f2d7

Please sign in to comment.