|
|
|
@@ -18,18 +18,29 @@ import org.springframework.web.reactive.function.client.WebClient |
|
|
|
import reactor.core.publisher.Mono |
|
|
|
import kotlin.reflect.full.memberProperties |
|
|
|
import org.springframework.context.annotation.Lazy |
|
|
|
import org.springframework.http.HttpMethod |
|
|
|
import org.springframework.http.HttpRequest |
|
|
|
import org.springframework.http.HttpStatusCode |
|
|
|
import org.springframework.web.reactive.function.BodyInserters |
|
|
|
import org.springframework.web.reactive.function.client.ClientRequest |
|
|
|
import org.springframework.web.reactive.function.client.ExchangeFilterFunction |
|
|
|
import org.springframework.web.reactive.function.client.WebClientResponseException |
|
|
|
import org.springframework.web.service.invoker.HttpRequestValues |
|
|
|
import reactor.util.retry.Retry |
|
|
|
import java.time.Duration |
|
|
|
import java.util.concurrent.atomic.AtomicReference |
|
|
|
|
|
|
|
@Service |
|
|
|
open class ApiCallerService( |
|
|
|
val m18Config: M18Config, |
|
|
|
) { |
|
|
|
val logger: Logger = LoggerFactory.getLogger(JwtTokenUtil::class.java) |
|
|
|
val logger: Logger = LoggerFactory.getLogger(ApiCallerService::class.java) |
|
|
|
private val MAX_IN_MEMORY_SIZE = 10 * 1024 * 1024 |
|
|
|
val DEFAULT_RETRY_ATTEMPTS = 5L |
|
|
|
|
|
|
|
val webClient: WebClient = WebClient.builder() |
|
|
|
.baseUrl(m18Config.BASE_URL) |
|
|
|
// .baseUrl(m18Config.BASE_URL_UAT) |
|
|
|
.defaultHeaders { headers -> |
|
|
|
headers.set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) |
|
|
|
// headers.set(HttpHeaders.AUTHORIZATION, "Bearer ${m18Config.ACCESS_TOKEN}") |
|
|
|
@@ -42,15 +53,22 @@ open class ApiCallerService( |
|
|
|
.build() |
|
|
|
next.exchange(updatedRequest) |
|
|
|
} |
|
|
|
.codecs { configurer -> configurer.defaultCodecs().maxInMemorySize(10 * 1024 * 1024) } |
|
|
|
.filter(ExchangeFilterFunction.ofRequestProcessor { request -> |
|
|
|
// logger.info("Request: ${request.method()} ${request.url()}") |
|
|
|
Mono.just(request) |
|
|
|
}.andThen(ExchangeFilterFunction.ofResponseProcessor { response -> |
|
|
|
// logger.info("Response: ${response.statusCode()}") |
|
|
|
Mono.just(response) |
|
|
|
})) |
|
|
|
.codecs { configurer -> configurer.defaultCodecs().maxInMemorySize(MAX_IN_MEMORY_SIZE) } |
|
|
|
.build() |
|
|
|
|
|
|
|
@PostConstruct |
|
|
|
fun init() { |
|
|
|
updateToken() |
|
|
|
updateToken().subscribe() |
|
|
|
} |
|
|
|
|
|
|
|
fun updateToken() { |
|
|
|
fun updateToken() : Mono<String>{ |
|
|
|
val params = M18TokenRequest( |
|
|
|
grant_type = m18Config.GRANT_TYPE, |
|
|
|
client_id = m18Config.CLIENT_ID, |
|
|
|
@@ -59,96 +77,69 @@ open class ApiCallerService( |
|
|
|
password = m18Config.PASSWORD |
|
|
|
) |
|
|
|
|
|
|
|
get<M18TokenResponse, M18TokenRequest>("/oauth/token", params, null) |
|
|
|
.subscribe( |
|
|
|
{ response -> |
|
|
|
return get<M18TokenResponse, M18TokenRequest>("/oauth/token", params, null) |
|
|
|
.doOnSuccess { response -> |
|
|
|
if (m18Config.ACCESS_TOKEN != response.access_token) { |
|
|
|
m18Config.ACCESS_TOKEN = response.access_token |
|
|
|
println("WebClient Response stored: $response") |
|
|
|
}, |
|
|
|
{ error -> println("WebClient Error: ${error.message}") } |
|
|
|
) |
|
|
|
logger.info("Token updated: ${response.access_token}") |
|
|
|
} |
|
|
|
} |
|
|
|
.doOnError { error -> logger.error("Token update failed: ${error.message}") } |
|
|
|
.map { it.access_token } |
|
|
|
} |
|
|
|
|
|
|
|
// ------------------------------------ GET ------------------------------------ // |
|
|
|
/** |
|
|
|
* Performs a GET HTTP request to the specified URL path. |
|
|
|
* Performs a GET HTTP request to the specified URL path with parameters. |
|
|
|
* T: Response, U: Request |
|
|
|
* |
|
|
|
* @param urlPath The path to send the GET request to (after /jsf/rfws) |
|
|
|
* @param params Optional query parameters to include in the request |
|
|
|
* @param customHeaders Optional custom headers to include in the request. The default header includes CONTENT_TYPE, AUTHORIZATION, client_id |
|
|
|
* @param params Optional query parameters (supports Map, MultiValueMap, or custom object) |
|
|
|
* @param customHeaders Optional custom headers to include in the request |
|
|
|
* @return A Mono that emits the response body converted to type T |
|
|
|
*/ |
|
|
|
inline fun <reified T : Any> get( |
|
|
|
inline fun <reified T : Any, reified U : Any> get( |
|
|
|
urlPath: String, |
|
|
|
params: MultiValueMap<String, String>, |
|
|
|
customHeaders: Map<String, String>? |
|
|
|
params: U? = null, |
|
|
|
customHeaders: Map<String, String>? = null |
|
|
|
): Mono<T> { |
|
|
|
println("ACCESS TOKEN: ${m18Config.ACCESS_TOKEN}") |
|
|
|
return webClient.get() |
|
|
|
.uri { uriBuilder -> |
|
|
|
uriBuilder |
|
|
|
.apply { |
|
|
|
path(urlPath) |
|
|
|
|
|
|
|
// convert to multiValueMap |
|
|
|
queryParams(params) |
|
|
|
val queryParams = when (params) { |
|
|
|
is Map<*, *> -> LinkedMultiValueMap<String, String>().apply { |
|
|
|
params.forEach { (k, v) -> |
|
|
|
when (v) { |
|
|
|
is Collection<*> -> addAll(k.toString(), v.map { it.toString() }) |
|
|
|
else -> add(k.toString(), v.toString()) |
|
|
|
} |
|
|
|
.build() |
|
|
|
} |
|
|
|
.headers { headers -> |
|
|
|
customHeaders?.forEach { (k, v) -> headers.set(k, v) } |
|
|
|
} |
|
|
|
.retrieve() |
|
|
|
.bodyToMono(T::class.java) |
|
|
|
// Below is for test bug (200 but error). Need to comment out bodyToMono and add jsonIgnore to data class |
|
|
|
// .toEntity(String::class.java) // Get raw body as String |
|
|
|
// .flatMap { entity -> |
|
|
|
// logger.info("Response Status: ${entity.statusCode}") |
|
|
|
// logger.info("Response Headers: ${entity.headers}") |
|
|
|
// logger.info("Raw Response Body: ${entity.body}") |
|
|
|
// try { |
|
|
|
// val objectMapper = jacksonObjectMapper() |
|
|
|
// val deserialized = objectMapper.readValue(entity.body, T::class.java) |
|
|
|
// Mono.just(deserialized) |
|
|
|
// } catch (e: Exception) { |
|
|
|
// logger.error("Deserialization failed: ${e.message}") |
|
|
|
// Mono.error(e) |
|
|
|
// } |
|
|
|
// } |
|
|
|
.doOnError { error -> |
|
|
|
println("Error occurred: ${error.message}") |
|
|
|
} |
|
|
|
.onErrorResume(WebClientResponseException::class.java) { error -> |
|
|
|
logger.error("WebClientResponseException") |
|
|
|
logger.error("Error Status: ${error.statusCode} - ${error.statusText}") |
|
|
|
logger.error("Error Message: ${error.message}") |
|
|
|
logger.error("Error Response: ${error.responseBodyAsString}") |
|
|
|
if (error.statusCode == HttpStatusCode.valueOf(400)) { |
|
|
|
updateToken() |
|
|
|
} |
|
|
|
Mono.error(error) |
|
|
|
} |
|
|
|
.onErrorResume { error -> |
|
|
|
logger.error("Exception") |
|
|
|
logger.error("Error Message: ${error.message}") |
|
|
|
Mono.error(error) |
|
|
|
null -> LinkedMultiValueMap<String, String>() |
|
|
|
else -> LinkedMultiValueMap<String, String>().apply { |
|
|
|
U::class.memberProperties.forEach { property -> |
|
|
|
val key = property.name |
|
|
|
val value = property.get(params) |
|
|
|
when (value) { |
|
|
|
is Collection<*> -> value.forEach { item -> add(key, item.toString()) } |
|
|
|
else -> add(key, value.toString()) |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
.retry(5) |
|
|
|
} |
|
|
|
|
|
|
|
return executeGet<T>(urlPath, queryParams, customHeaders) |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Performs a GET HTTP request to the specified URL path. |
|
|
|
* Performs a GET HTTP request to the specified URL path without parameters. |
|
|
|
* |
|
|
|
* @param urlPath The path to send the GET request to (after /jsf/rfws) |
|
|
|
* @param params Optional query parameters to include in the request |
|
|
|
* @param customHeaders Optional custom headers to include in the request. The default header includes CONTENT_TYPE, AUTHORIZATION, client_id |
|
|
|
* @param customHeaders Optional custom headers to include in the request |
|
|
|
* @return A Mono that emits the response body converted to type T |
|
|
|
*/ |
|
|
|
inline fun <reified T : Any> get( |
|
|
|
urlPath: String, |
|
|
|
params: Map<String, Any>?, |
|
|
|
customHeaders: Map<String, String>? |
|
|
|
customHeaders: Map<String, String>? = null |
|
|
|
): Mono<T> { |
|
|
|
|
|
|
|
// convert to multiValueMap<String, String> |
|
|
|
val queryParams = params?.let { paramMap -> |
|
|
|
LinkedMultiValueMap<String, String>().apply { |
|
|
|
@@ -161,7 +152,7 @@ open class ApiCallerService( |
|
|
|
} |
|
|
|
} ?: LinkedMultiValueMap<String, String>() |
|
|
|
|
|
|
|
return get<T>(urlPath, queryParams, customHeaders) |
|
|
|
return executeGet<T>(urlPath, queryParams, customHeaders) |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
@@ -169,33 +160,87 @@ open class ApiCallerService( |
|
|
|
* |
|
|
|
* @param urlPath The path to send the GET request to (after /jsf/rfws) |
|
|
|
* @param params Optional query parameters to include in the request |
|
|
|
* @param customHeaders Optional custom headers to include in the request. The default header includes CONTENT_TYPE, AUTHORIZATION, client_id |
|
|
|
* @return A Mono that emits the response body converted to type T |
|
|
|
*/ |
|
|
|
inline fun <reified T : Any> get( |
|
|
|
inline fun <reified T : Any> executeGet( |
|
|
|
urlPath: String, |
|
|
|
params: Map<String, Any>? |
|
|
|
params: MultiValueMap<String, String>, |
|
|
|
customHeaders: Map<String, String>? |
|
|
|
): Mono<T> { |
|
|
|
return get<T>(urlPath, params, null) |
|
|
|
println("ACCESS TOKEN: ${m18Config.ACCESS_TOKEN}") |
|
|
|
|
|
|
|
return webClient.get() |
|
|
|
.uri { uriBuilder -> uriBuilder.path(urlPath).queryParams(params).build() } |
|
|
|
.headers { headers -> customHeaders?.forEach { (k, v) -> headers.set(k, v) } } |
|
|
|
.retrieve() |
|
|
|
.bodyToMono(T::class.java) |
|
|
|
// Below is for test bug (200 but error). Need to comment out bodyToMono and add jsonIgnore to data class |
|
|
|
// .toEntity(String::class.java) // Get raw body as String |
|
|
|
// .flatMap { entity -> |
|
|
|
// logger.info("Response Status: ${entity.statusCode}") |
|
|
|
// logger.info("Response Headers: ${entity.headers}") |
|
|
|
// logger.info("Raw Response Body: ${entity.body}") |
|
|
|
// try { |
|
|
|
// val objectMapper = jacksonObjectMapper() |
|
|
|
// val deserialized = objectMapper.readValue(entity.body, T::class.java) |
|
|
|
// Mono.just(deserialized) |
|
|
|
// } catch (e: Exception) { |
|
|
|
// logger.error("Deserialization failed: ${e.message}") |
|
|
|
// Mono.error(e) |
|
|
|
// } |
|
|
|
// } |
|
|
|
.doOnError { error -> println("Error occurred: ${error.message}") } |
|
|
|
.onErrorResume(WebClientResponseException::class.java) { error -> |
|
|
|
logger.error("WebClientResponseException: ${error.statusCode} - ${error.statusText}, Body: ${error.responseBodyAsString}") |
|
|
|
if (error.statusCode == HttpStatusCode.valueOf(400) || error.statusCode == HttpStatusCode.valueOf(401)) { |
|
|
|
updateToken().flatMap { newToken -> |
|
|
|
webClient.get() |
|
|
|
.uri { uriBuilder -> uriBuilder.path(urlPath).queryParams(params).build() } |
|
|
|
.headers { headers -> |
|
|
|
headers.set(HttpHeaders.AUTHORIZATION, "Bearer $newToken") |
|
|
|
customHeaders?.forEach { (k, v) -> headers.set(k, v) } |
|
|
|
} |
|
|
|
.retrieve() |
|
|
|
.bodyToMono(T::class.java) |
|
|
|
} |
|
|
|
} else { |
|
|
|
Mono.error(error) |
|
|
|
} |
|
|
|
} |
|
|
|
.retryWhen( |
|
|
|
Retry.backoff(DEFAULT_RETRY_ATTEMPTS, Duration.ofSeconds(1)) |
|
|
|
// .filter { it is WebClientResponseException && it.statusCode != HttpStatusCode.valueOf(400) } |
|
|
|
.doBeforeRetry { signal -> logger.info("Retrying due to: ${signal.failure().message}") } |
|
|
|
) |
|
|
|
} |
|
|
|
|
|
|
|
// ------------------------------------ POST ------------------------------------ // |
|
|
|
/** |
|
|
|
* Performs a GET HTTP request to the specified URL path. |
|
|
|
* Performs a POST HTTP request to the specified URL path with parameters. |
|
|
|
* T: Response, U: Request |
|
|
|
* |
|
|
|
* @param urlPath The path to send the GET request to (after /jsf/rfws) |
|
|
|
* @param params Optional query parameters to include in the request |
|
|
|
* @param customHeaders Optional custom headers to include in the request. The default header includes CONTENT_TYPE, AUTHORIZATION, client_id |
|
|
|
* @param urlPath The path to send the POST request to (after /jsf/rfws) |
|
|
|
* @param params Optional query parameters (supports Map, MultiValueMap, or custom object) |
|
|
|
* @param customHeaders Optional custom headers to include in the request |
|
|
|
* @return A Mono that emits the response body converted to type T |
|
|
|
*/ |
|
|
|
inline fun <reified T : Any, reified U : Any> get( |
|
|
|
inline fun <reified T : Any, reified U : Any> post( |
|
|
|
urlPath: String, |
|
|
|
params: U?, |
|
|
|
customHeaders: Map<String, String>? |
|
|
|
params: U? = null, |
|
|
|
customHeaders: Map<String, String>? = null |
|
|
|
): Mono<T> { |
|
|
|
|
|
|
|
// convert to multiValueMap<String, String> |
|
|
|
val queryParams = params?.let { |
|
|
|
LinkedMultiValueMap<String, String>().apply { |
|
|
|
val queryParams = when (params) { |
|
|
|
is Map<*, *> -> LinkedMultiValueMap<String, String>().apply { |
|
|
|
params.forEach { (k, v) -> |
|
|
|
when (v) { |
|
|
|
is Collection<*> -> addAll(k.toString(), v.map { it.toString() }) |
|
|
|
else -> add(k.toString(), v.toString()) |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
null -> LinkedMultiValueMap<String, String>() |
|
|
|
else -> LinkedMultiValueMap<String, String>().apply { |
|
|
|
U::class.memberProperties.forEach { property -> |
|
|
|
val key = property.name |
|
|
|
val value = property.get(params) |
|
|
|
@@ -205,25 +250,80 @@ open class ApiCallerService( |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
return executePost<T>(urlPath, queryParams, customHeaders) |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Performs a POST HTTP request to the specified URL path without parameters. |
|
|
|
* |
|
|
|
* @param urlPath The path to send the POST request to (after /jsf/rfws) |
|
|
|
* @param customHeaders Optional custom headers to include in the request |
|
|
|
* @return A Mono that emits the response body converted to type T |
|
|
|
*/ |
|
|
|
inline fun <reified T : Any> post( |
|
|
|
urlPath: String, |
|
|
|
params: Map<String, Any>?, |
|
|
|
customHeaders: Map<String, String>? = null |
|
|
|
): Mono<T> { |
|
|
|
// convert to multiValueMap<String, String> |
|
|
|
val queryParams = params?.let { paramMap -> |
|
|
|
LinkedMultiValueMap<String, String>().apply { |
|
|
|
paramMap.forEach { (key, value) -> |
|
|
|
when (value) { |
|
|
|
is Collection<*> -> addAll(key, value.map { it.toString() }) |
|
|
|
else -> add(key, value.toString()) |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} ?: LinkedMultiValueMap<String, String>() |
|
|
|
|
|
|
|
return get<T>(urlPath, queryParams, customHeaders) |
|
|
|
return executePost<T>(urlPath, queryParams, customHeaders) |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Performs a GET HTTP request to the specified URL path. |
|
|
|
* T: Response, U: Request |
|
|
|
* Performs a POST HTTP request to the specified URL path. |
|
|
|
* |
|
|
|
* @param urlPath The path to send the GET request to (after /jsf/rfws) |
|
|
|
* @param params Optional query parameters to include in the request |
|
|
|
* @param customHeaders Optional custom headers to include in the request. The default header includes CONTENT_TYPE, AUTHORIZATION, client_id |
|
|
|
* @return A Mono that emits the response body converted to type T |
|
|
|
*/ |
|
|
|
inline fun <reified T : Any, reified U : Any> get( |
|
|
|
inline fun <reified T : Any> executePost( |
|
|
|
urlPath: String, |
|
|
|
params: U?, |
|
|
|
params: MultiValueMap<String, String>, |
|
|
|
customHeaders: Map<String, String>? |
|
|
|
): Mono<T> { |
|
|
|
println("ACCESS TOKEN: ${m18Config.ACCESS_TOKEN}") |
|
|
|
|
|
|
|
return get<T, U>(urlPath, params, null) |
|
|
|
return webClient.post() |
|
|
|
.uri { uriBuilder -> uriBuilder.path(urlPath).build() } |
|
|
|
.headers { headers -> customHeaders?.forEach { (k, v) -> headers.set(k, v) } } |
|
|
|
.body(BodyInserters.fromValue(params)) |
|
|
|
.retrieve() |
|
|
|
.bodyToMono(T::class.java) |
|
|
|
.doOnError { error -> println("Error occurred: ${error.message}") } |
|
|
|
.onErrorResume(WebClientResponseException::class.java) { error -> |
|
|
|
logger.error("WebClientResponseException: ${error.statusCode} - ${error.statusText}, Body: ${error.responseBodyAsString}") |
|
|
|
if (error.statusCode == HttpStatusCode.valueOf(400) || error.statusCode == HttpStatusCode.valueOf(401)) { |
|
|
|
updateToken().flatMap { newToken -> |
|
|
|
webClient.get() |
|
|
|
.uri { uriBuilder -> uriBuilder.path(urlPath).queryParams(params).build() } |
|
|
|
.headers { headers -> |
|
|
|
headers.set(HttpHeaders.AUTHORIZATION, "Bearer $newToken") |
|
|
|
customHeaders?.forEach { (k, v) -> headers.set(k, v) } |
|
|
|
} |
|
|
|
.retrieve() |
|
|
|
.bodyToMono(T::class.java) |
|
|
|
} |
|
|
|
} else { |
|
|
|
Mono.error(error) |
|
|
|
} |
|
|
|
} |
|
|
|
.retryWhen( |
|
|
|
Retry.backoff(DEFAULT_RETRY_ATTEMPTS, Duration.ofSeconds(1)) |
|
|
|
.doBeforeRetry { signal -> logger.info("Retrying due to: ${signal.failure().message}") } |
|
|
|
) |
|
|
|
} |
|
|
|
|
|
|
|
} |