diff --git a/src/main/java/com/ffii/fpsms/api/service/ApiCallerService.kt b/src/main/java/com/ffii/fpsms/api/service/ApiCallerService.kt index 8a99759..ffd3257 100644 --- a/src/main/java/com/ffii/fpsms/api/service/ApiCallerService.kt +++ b/src/main/java/com/ffii/fpsms/api/service/ApiCallerService.kt @@ -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{ val params = M18TokenRequest( grant_type = m18Config.GRANT_TYPE, client_id = m18Config.CLIENT_ID, @@ -59,96 +77,69 @@ open class ApiCallerService( password = m18Config.PASSWORD ) - get("/oauth/token", params, null) - .subscribe( - { response -> + return get("/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 get( + inline fun get( urlPath: String, - params: MultiValueMap, - customHeaders: Map? + params: U? = null, + customHeaders: Map? = null ): Mono { - 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().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() + else -> LinkedMultiValueMap().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(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 get( urlPath: String, params: Map?, - customHeaders: Map? + customHeaders: Map? = null ): Mono { - // convert to multiValueMap val queryParams = params?.let { paramMap -> LinkedMultiValueMap().apply { @@ -161,7 +152,7 @@ open class ApiCallerService( } } ?: LinkedMultiValueMap() - return get(urlPath, queryParams, customHeaders) + return executeGet(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 get( + inline fun executeGet( urlPath: String, - params: Map? + params: MultiValueMap, + customHeaders: Map? ): Mono { - return get(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 get( + inline fun post( urlPath: String, - params: U?, - customHeaders: Map? + params: U? = null, + customHeaders: Map? = null ): Mono { - - // convert to multiValueMap - val queryParams = params?.let { - LinkedMultiValueMap().apply { + val queryParams = when (params) { + is Map<*, *> -> LinkedMultiValueMap().apply { + params.forEach { (k, v) -> + when (v) { + is Collection<*> -> addAll(k.toString(), v.map { it.toString() }) + else -> add(k.toString(), v.toString()) + } + } + } + null -> LinkedMultiValueMap() + else -> LinkedMultiValueMap().apply { U::class.memberProperties.forEach { property -> val key = property.name val value = property.get(params) @@ -205,25 +250,80 @@ open class ApiCallerService( } } } + } + + return executePost(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 post( + urlPath: String, + params: Map?, + customHeaders: Map? = null + ): Mono { + // convert to multiValueMap + val queryParams = params?.let { paramMap -> + LinkedMultiValueMap().apply { + paramMap.forEach { (key, value) -> + when (value) { + is Collection<*> -> addAll(key, value.map { it.toString() }) + else -> add(key, value.toString()) + } + } + } } ?: LinkedMultiValueMap() - return get(urlPath, queryParams, customHeaders) + return executePost(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 get( + inline fun executePost( urlPath: String, - params: U?, + params: MultiValueMap, + customHeaders: Map? ): Mono { + println("ACCESS TOKEN: ${m18Config.ACCESS_TOKEN}") - return get(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}") } + ) } - } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/m18/M18Config.kt b/src/main/java/com/ffii/fpsms/m18/M18Config.kt index 92008e9..c6df36b 100644 --- a/src/main/java/com/ffii/fpsms/m18/M18Config.kt +++ b/src/main/java/com/ffii/fpsms/m18/M18Config.kt @@ -28,6 +28,9 @@ open class M18Config { @Value("\${m18.config.base-url}") lateinit var BASE_URL: String; + @Value("\${m18.config.base-url-uat}") + lateinit var BASE_URL_UAT: String; + // Supplier // var MATERIAL_PO_SUPPLIER_NOT: List = listOf("P06", "P07", "T62"); // If need oem type diff --git a/src/main/java/com/ffii/fpsms/m18/service/M18SchedulerService.kt b/src/main/java/com/ffii/fpsms/m18/service/M18SchedulerService.kt deleted file mode 100644 index 1a0a239..0000000 --- a/src/main/java/com/ffii/fpsms/m18/service/M18SchedulerService.kt +++ /dev/null @@ -1,113 +0,0 @@ -package com.ffii.fpsms.m18.service - -import com.ffii.core.utils.JwtTokenUtil -import com.ffii.fpsms.m18.web.models.M18CommonRequest -import com.ffii.fpsms.modules.common.SettingNames -import com.ffii.fpsms.modules.settings.service.SettingsService -import jakarta.annotation.PostConstruct -import org.slf4j.Logger -import org.slf4j.LoggerFactory -import org.springframework.scheduling.TaskScheduler -//import org.springframework.scheduling.annotation.Async -import org.springframework.scheduling.annotation.Scheduled -import org.springframework.scheduling.support.CronTrigger -import org.springframework.stereotype.Service -import java.time.LocalDateTime -import java.time.format.DateTimeFormatter -import java.util.concurrent.ScheduledFuture -import kotlin.concurrent.Volatile -import kotlin.jvm.optionals.getOrNull - -@Service -open class M18SchedulerService( - val m18PurchaseOrderService: M18PurchaseOrderService, - val m18DeliveryOrderService: M18DeliveryOrderService, - val m18MasterDataService: M18MasterDataService, - val settingsService: SettingsService, - val taskScheduler: TaskScheduler, -) { - var logger: Logger = LoggerFactory.getLogger(JwtTokenUtil::class.java) - val dataStringFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd") - val defaultCronExpression = "0 0 2 * * *"; - - @Volatile - var scheduledM18Po: ScheduledFuture<*>? = null - - @Volatile - var scheduledM18Master: ScheduledFuture<*>? = null - - fun isValidCronExpression(cronExpression: String): Boolean { - return try { - CronTrigger(cronExpression) - true - } catch (e: IllegalArgumentException) { - false - } - } - @PostConstruct - fun init() { - scheduleM18PoTask() - scheduleM18MasterData() - } - - fun commonSchedule(scheduled: ScheduledFuture<*>?, settingName: String, scheduleFunc: () -> Unit) { - scheduled?.cancel(false) - - var cron = settingsService.findByName(settingName).getOrNull()?.value ?: defaultCronExpression; - - if (!isValidCronExpression(cron)) { - cron = defaultCronExpression - } - scheduledM18Po = taskScheduler.schedule( - { - scheduleFunc() - }, - CronTrigger(cron) - ) - } - - // Scheduler - fun scheduleM18PoTask() { - commonSchedule(scheduledM18Po, SettingNames.SCHEDULE_M18_PO, ::getM18Pos) - } - - fun scheduleM18MasterData() { - commonSchedule(scheduledM18Master, SettingNames.SCHEDULE_M18_MASTER, ::getM18MasterData) - } - - // Tasks -// @Async -// @Scheduled(cron = "0 0 2 * * *") // (SS/MM/HH/DD/MM/YY) - open fun getM18Pos() { - logger.info("Daily Scheduler - PO") - val currentTime = LocalDateTime.now() - val today = currentTime.toLocalDate().atStartOfDay() - val yesterday = today.minusDays(1L) - val request = M18CommonRequest( - modifiedDateTo = today.format(dataStringFormat), - modifiedDateFrom = yesterday.format(dataStringFormat) - ) - m18PurchaseOrderService.savePurchaseOrders(request); - m18DeliveryOrderService.saveDeliveryOrders(request); - logger.info("today: ${today.format(dataStringFormat)}") - logger.info("yesterday: ${yesterday.format(dataStringFormat)}") - } - - open fun getM18MasterData() { - logger.info("Daily Scheduler - Master Data") - val currentTime = LocalDateTime.now() - val today = currentTime.toLocalDate().atStartOfDay() - val yesterday = today.minusDays(1L) - val request = M18CommonRequest( - modifiedDateTo = today.format(dataStringFormat), - modifiedDateFrom = yesterday.format(dataStringFormat) - ) - m18MasterDataService.saveUnits(request) - m18MasterDataService.saveProducts(request) - m18MasterDataService.saveVendors(request) - m18MasterDataService.saveBusinessUnits(request) - m18MasterDataService.saveCurrencies(request) - logger.info("today: ${today.format(dataStringFormat)}") - logger.info("yesterday: ${yesterday.format(dataStringFormat)}") - } -} \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index d129fd4..94fc5fb 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -35,6 +35,7 @@ m18: username: testingMTMS password: db25f2fc14cd2d2b1e7af307241f548fb03c312a base-url: https://toa.m18saas.com/jsf/rfws + base-url-uat: https://toauat.m18saas.com/jsf/rfws base-password: qwer1234 supplier: shop-po: P06, P07