Skip to content

Commit

Permalink
Dheeraj | Implement facility search APIs using Facility Registry (#146)
Browse files Browse the repository at this point in the history
* Dheeraj | Basic methods to search facility

* Dheeraj | Figure out if a facility is HIP or not

* Dheeraj | Store facility registry auth token in cache

* Dheeraj | Add API to fetch facility by ID

* Dheeraj | Add current API version to facility search APIs

* Dheeraj | Add tests

* Dheeraj | Check if the facility is active

* Dheeraj | Change Properties.class to String.class in all web client calls

* Dheeraj | Apply PR feedbacks
  • Loading branch information
dheeraj-p authored Nov 26, 2020
1 parent a15ffd9 commit d38f9d1
Show file tree
Hide file tree
Showing 19 changed files with 621 additions and 17 deletions.
4 changes: 3 additions & 1 deletion src/main/java/in/projecteka/gateway/GatewayApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import in.projecteka.gateway.common.cache.ServiceOptions;
import in.projecteka.gateway.common.heartbeat.RabbitmqOptions;
import in.projecteka.gateway.common.heartbeat.CacheMethodProperty;
import in.projecteka.gateway.registry.FacilityRegistryProperties;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
Expand All @@ -18,7 +19,8 @@
DbOptions.class,
WebClientOptions.class,
CacheMethodProperty.class,
ShareProfile.class})
ShareProfile.class,
FacilityRegistryProperties.class})
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
Expand Down
38 changes: 35 additions & 3 deletions src/main/java/in/projecteka/gateway/GatewayConfiguration.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import in.projecteka.gateway.clients.ConsentStatusServiceClient;
import in.projecteka.gateway.clients.DataFlowRequestServiceClient;
import in.projecteka.gateway.clients.DiscoveryServiceClient;
import in.projecteka.gateway.clients.FacilityRegistryClient;
import in.projecteka.gateway.clients.GlobalExceptionHandler;
import in.projecteka.gateway.clients.HealthInfoNotificationServiceClient;
import in.projecteka.gateway.clients.HipConsentNotifyServiceClient;
Expand Down Expand Up @@ -53,6 +54,7 @@
import in.projecteka.gateway.common.heartbeat.RabbitmqOptions;
import in.projecteka.gateway.registry.BridgeRegistry;
import in.projecteka.gateway.registry.CMRegistry;
import in.projecteka.gateway.registry.FacilityRegistryProperties;
import in.projecteka.gateway.registry.RegistryRepository;
import in.projecteka.gateway.registry.RegistryService;
import in.projecteka.gateway.registry.ServiceType;
Expand Down Expand Up @@ -102,6 +104,8 @@

@Configuration
public class GatewayConfiguration {
@Value("${webclient.maxInMemorySize}")
private int maxInMemorySize;

@ConditionalOnProperty(value = "gateway.cacheMethod", havingValue = "guava", matchIfMissing = true)
@Bean("accessToken")
Expand Down Expand Up @@ -131,6 +135,23 @@ public CacheAdapter<String, String> createRedisCacheAdapterForAccessToken(
redisOptions.getRetry());
}

@ConditionalOnProperty(value = "gateway.cacheMethod", havingValue = "guava", matchIfMissing = true)
@Bean("facilityTokenCache")
public CacheAdapter<String, String> createLoadingCacheAdapterForFacilityAccessToken() {
return new LoadingCacheAdapter<>(stringStringLoadingCache(5));
}

@ConditionalOnProperty(value = "gateway.cacheMethod", havingValue = "redis")
@Bean("facilityTokenCache")
public CacheAdapter<String, String> createRedisCacheAdapterForFacilityAccessToken(
@Qualifier("Lettuce") RedisClient redisClient,
RedisOptions redisOptions,
FacilityRegistryProperties facilityRegistryProperties) {
return new RedisCacheAdapter(redisClient,
facilityRegistryProperties.getTokenExpiry(),
redisOptions.getRetry());
}

@ConditionalOnProperty(value = "gateway.cacheMethod", havingValue = "redis")
@Bean({"requestIdMappings", "requestIdTimestampMappings"})
public CacheAdapter<String, String> createRedisCacheAdapter(@Qualifier("Lettuce") RedisClient redisClient,
Expand Down Expand Up @@ -972,7 +993,10 @@ public WebClient.Builder webClient(
return WebClient
.builder()
.exchangeStrategies(exchangeStrategies(objectMapper))
.clientConnector(clientHttpConnector);
.clientConnector(clientHttpConnector)
.codecs(configurer -> configurer
.defaultCodecs()
.maxInMemorySize(maxInMemorySize));
}

private ExchangeStrategies exchangeStrategies(ObjectMapper objectMapper) {
Expand Down Expand Up @@ -1048,8 +1072,9 @@ public RegistryRepository registryRepository(@Qualifier("readWriteClient") PgPoo
public RegistryService registryService(RegistryRepository registryRepository,
CacheAdapter<String, String> consentManagerMappings,
CacheAdapter<Pair<String, ServiceType>, String> bridgeMappings,
AdminServiceClient adminServiceClient) {
return new RegistryService(registryRepository, consentManagerMappings, bridgeMappings, adminServiceClient);
AdminServiceClient adminServiceClient,
FacilityRegistryClient facilityRegistryClient) {
return new RegistryService(registryRepository, consentManagerMappings, bridgeMappings, adminServiceClient, facilityRegistryClient);
}

@Bean("userAuthenticatorClient")
Expand Down Expand Up @@ -1331,4 +1356,11 @@ public RequestOrchestrator<HiuSubscriptionNotifyServiceClient> hiuSubscriptionNo
hiuConsentNotifyServiceClient,
hiuSubscriptionNotifyRequestAction);
}

@Bean("facilityRegistryClient")
public FacilityRegistryClient facilityRegistryClient(@Qualifier("customBuilder") WebClient.Builder builder,
FacilityRegistryProperties facilityRegistryProperties,
@Qualifier("facilityTokenCache") CacheAdapter<String, String> facilityTokenCache){
return new FacilityRegistryClient(builder, facilityRegistryProperties, facilityTokenCache);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
import static in.projecteka.gateway.common.Constants.INTERNAL_BRIDGES;
import static in.projecteka.gateway.common.Constants.INTERNAL_BRIDGES_BRIDGE_ID_SERVICES;
import static in.projecteka.gateway.common.Constants.INTERNAL_CM;
import static in.projecteka.gateway.common.Constants.INTERNAL_GET_FACILITY_BY_ID;
import static in.projecteka.gateway.common.Constants.INTERNAL_SEARCH_FACILITY_BY_NAME;
import static in.projecteka.gateway.common.Constants.PATH_ADD_CARE_CONTEXTS;
import static in.projecteka.gateway.common.Constants.PATH_CARE_CONTEXTS_DISCOVER;
import static in.projecteka.gateway.common.Constants.PATH_CARE_CONTEXTS_ON_DISCOVER;
Expand Down Expand Up @@ -143,7 +145,9 @@ public class SecurityConfiguration {
PATH_PATIENT_SHARE,
PATH_ON_FETCH_AUTH_MODES,
PATH_HIU_SUBSCRIPTION_NOTIFY,
PATH_SUBSCRIPTION_REQUESTS_NOTIFY
PATH_SUBSCRIPTION_REQUESTS_NOTIFY,
INTERNAL_SEARCH_FACILITY_BY_NAME,
INTERNAL_GET_FACILITY_BY_ID
};

protected static final String[] ALLOW_LIST_APIS = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ private Mono<Void> createClient(ClientRepresentation clientRepresentation) {
logger.error(keyCloakError.getErrorMessage(), keyCloakError);
return error(clientAlreadyExists(keyCloakError.getErrorMessage()));
}))
.onStatus(HttpStatus::isError, clientResponse -> clientResponse.bodyToMono(Properties.class)
.onStatus(HttpStatus::isError, clientResponse -> clientResponse.bodyToMono(String.class)
.doOnNext(properties -> logger.error(ERROR_MESSAGE,
clientResponse.statusCode(),
properties))
Expand Down Expand Up @@ -123,7 +123,7 @@ public Mono<ServiceAccount> getServiceAccount(String id) {
logger.error(keyCloakError.getError(), keyCloakError);
return error(notFound(keyCloakError.getError()));
}))
.onStatus(HttpStatus::isError, clientResponse -> clientResponse.bodyToMono(Properties.class)
.onStatus(HttpStatus::isError, clientResponse -> clientResponse.bodyToMono(String.class)
.doOnNext(properties -> logger.error(ERROR_MESSAGE,
clientResponse.statusCode(),
properties))
Expand Down Expand Up @@ -153,7 +153,7 @@ public Mono<List<RealmRole>> getAvailableRealmRoles(String serviceAccountId) {
logger.error(keyCloakError.getError(), keyCloakError);
return error(notFound(keyCloakError.getError()));
}))
.onStatus(HttpStatus::isError, clientResponse -> clientResponse.bodyToMono(Properties.class)
.onStatus(HttpStatus::isError, clientResponse -> clientResponse.bodyToMono(String.class)
.doOnNext(properties -> logger.error(ERROR_MESSAGE,
clientResponse.statusCode(),
properties))
Expand Down Expand Up @@ -186,7 +186,7 @@ public Mono<Void> assignRoleToClient(List<RealmRole> realmRoles, String serviceA
logger.error(keyCloakError.getError(), keyCloakError);
return error(notFound(keyCloakError.getError()));
}))
.onStatus(HttpStatus::isError, clientResponse -> clientResponse.bodyToMono(Properties.class)
.onStatus(HttpStatus::isError, clientResponse -> clientResponse.bodyToMono(String.class)
.doOnNext(properties -> logger.error(ERROR_MESSAGE,
clientResponse.statusCode(),
properties))
Expand Down Expand Up @@ -217,7 +217,7 @@ public Mono<Void> deleteClient(String id) {
logger.error(keyCloakError.getError(), keyCloakError);
return error(notFound(keyCloakError.getError()));
}))
.onStatus(HttpStatus::isError, clientResponse -> clientResponse.bodyToMono(Properties.class)
.onStatus(HttpStatus::isError, clientResponse -> clientResponse.bodyToMono(String.class)
.doOnNext(properties -> logger.error(ERROR_MESSAGE,
clientResponse.statusCode(),
properties))
Expand Down Expand Up @@ -248,7 +248,7 @@ public Mono<ClientSecret> getClientSecret(String clientId) {
logger.error(keyCloakError.getError(), keyCloakError);
return error(notFound(keyCloakError.getError()));
}))
.onStatus(HttpStatus::isError, clientResponse -> clientResponse.bodyToMono(Properties.class)
.onStatus(HttpStatus::isError, clientResponse -> clientResponse.bodyToMono(String.class)
.doOnNext(properties -> logger.error(ERROR_MESSAGE,
clientResponse.statusCode(),
properties))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package in.projecteka.gateway.clients;

import in.projecteka.gateway.clients.model.FacilitySearchResponse;
import in.projecteka.gateway.clients.model.FindFacilityByIDResponse;
import in.projecteka.gateway.clients.model.Session;
import in.projecteka.gateway.common.cache.CacheAdapter;
import in.projecteka.gateway.registry.FacilityRegistryProperties;
import lombok.AllArgsConstructor;
import lombok.Value;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.HashMap;
import java.util.UUID;

import static in.projecteka.gateway.clients.ClientError.unableToConnect;
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static reactor.core.publisher.Mono.error;


public class FacilityRegistryClient {
private static final Logger logger = LoggerFactory.getLogger(FacilityRegistryClient.class);
public static final String FACILITY_SEARCH_INCLUDE_PHOTO = "N"; //"N" for no, "Y" for yes
public static final String FACILITY_TOKEN_CACHE_KEY = "facilityRegistry:accessToken";

private final WebClient registryWebClient;
private final WebClient authWebClient;
private final FacilityRegistryProperties properties;
private final CacheAdapter<String, String> facilityTokenCache;


public FacilityRegistryClient(WebClient.Builder webClientBuilder, FacilityRegistryProperties properties,
CacheAdapter<String, String> facilityTokenCache) {
this.registryWebClient = webClientBuilder.baseUrl(properties.getUrl()).build();
this.authWebClient = webClientBuilder.baseUrl(properties.getAuthUrl()).build();
this.properties = properties;
this.facilityTokenCache = facilityTokenCache;
}

private Mono<String> getTokenForFacilityRegistry() {
return authWebClient
.post()
.uri("/sessions")
.contentType(APPLICATION_JSON)
.accept(APPLICATION_JSON)
.body(BodyInserters.fromValue(requestWith(properties.getClientId(), properties.getClientSecret())))
.retrieve()
.onStatus(HttpStatus::is4xxClientError, clientResponse -> clientResponse.bodyToMono(String.class)
.doOnNext(logger::error).then(Mono.error(ClientError.unknownUnAuthorizedError("Unable to get token for facility registry"))))
.onStatus(HttpStatus::isError, clientResponse -> clientResponse.bodyToMono(String.class)
.doOnNext(logger::error)
.then(Mono.error(ClientError.unableToConnect())))
.bodyToMono(Session.class)
.flatMap(session -> facilityTokenCache.put(FACILITY_TOKEN_CACHE_KEY, session.getAccessToken())
.thenReturn(session.getAccessToken()))
.doOnSubscribe(subscription -> logger.info("About to get token for facility registry"));
}

public Mono<String> getToken() {
return facilityTokenCache.get(FACILITY_TOKEN_CACHE_KEY)
.switchIfEmpty(getTokenForFacilityRegistry())
.map(token -> String.format("%s %s", "Bearer", token));
}

public Mono<FacilitySearchResponse> searchFacilityByName(String name, String state, String district) {
return getToken()
.flatMap(token -> registryWebClient.post()
.uri("/v1.0/facility/search-facilities")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.header("Authorization", token)
.body(BodyInserters.fromValue(searchByNameRequest(name, state, district)))
.retrieve()
.onStatus(HttpStatus::isError, clientResponse -> clientResponse.bodyToMono(String.class)
.doOnNext(properties -> logger.error("Error Status Code: {} and error: {} ",
clientResponse.statusCode(),
properties))
.then(error(unableToConnect())))
.bodyToMono(FacilitySearchResponse.class));
}

public Mono<FindFacilityByIDResponse> getFacilityById(String facilityId) {
return getToken()
.flatMap(token -> registryWebClient.post()
.uri("/v1.0/facility/fetch-facility-info")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.header("Authorization", token)
.body(BodyInserters.fromValue(getFacilityByIdRequest(facilityId)))
.retrieve()
.onStatus(HttpStatus::isError, clientResponse -> clientResponse.bodyToMono(String.class)
.doOnNext(properties -> logger.error("Error Status Code: {} and error: {} ",
clientResponse.statusCode(),
properties))
.then(error(unableToConnect())))
.bodyToMono(FindFacilityByIDResponse.class));
}

private HashMap<String, Object> searchByNameRequest(String name, String state, String district) {
var requestData = new HashMap<String, Object>();
var facilityInfo = new HashMap<String, String>();
facilityInfo.put("facilityName", name);
facilityInfo.put("photo", FACILITY_SEARCH_INCLUDE_PHOTO);

if (!StringUtils.isEmpty(state)) {
facilityInfo.put("state", state);
}
if (!StringUtils.isEmpty(district)) {
facilityInfo.put("district", district);
}

requestData.put("requestId", UUID.randomUUID().toString());
requestData.put("timestamp", LocalDateTime.now(ZoneOffset.UTC));
requestData.put("facility", facilityInfo);
return requestData;
}

private HashMap<String, Object> getFacilityByIdRequest(String facilityId) {
var requestData = new HashMap<String, Object>();
var facilityInfo = new HashMap<String, String>();
facilityInfo.put("id", facilityId);
requestData.put("requestId", UUID.randomUUID().toString());
requestData.put("timestamp", LocalDateTime.now(ZoneOffset.UTC));
requestData.put("facility", facilityInfo);
return requestData;
}

private FacilityRegistryClient.SessionRequest requestWith(String clientId, String clientSecret) {
return new FacilityRegistryClient.SessionRequest(clientId, clientSecret);
}

@AllArgsConstructor
@Value
private static class SessionRequest {
String clientId;
String clientSecret;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ private Mono<Session> getToken(MultiValueMap<String, String> formData) {
logger.error(keyCloakError.getError(), keyCloakError);
return error(invalidRequest(keyCloakError.getErrorDescription()));
}))
.onStatus(HttpStatus::isError, clientResponse -> clientResponse.bodyToMono(Properties.class)
.onStatus(HttpStatus::isError, clientResponse -> clientResponse.bodyToMono(String.class)
.doOnNext(properties -> logger.error("Error Status Code: {} and error: {} ",
clientResponse.statusCode(),
properties))
Expand All @@ -84,7 +84,7 @@ public Mono<JsonNode> certs() {
uriBuilder.path("/realms/{realm}/protocol/openid-connect/certs").build(Map.of("realm", realm)))
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.onStatus(HttpStatus::isError, clientResponse -> clientResponse.bodyToMono(Properties.class)
.onStatus(HttpStatus::isError, clientResponse -> clientResponse.bodyToMono(String.class)
.doOnNext(properties -> logger.error("Error Status Code: {} and error: {} ",
clientResponse.statusCode(),
properties))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package in.projecteka.gateway.clients.model;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;

import java.util.List;

@AllArgsConstructor
@Builder
@Data
public class FacilitySearchResponse {
private String referenceNumber;
private List<HFRFacilityRepresentation> facilities;
}
Loading

0 comments on commit d38f9d1

Please sign in to comment.