Browse Source

[API] Simplify (Rewrite) ApiCallerService, and add "post" function.

master
cyril.tsui 1 month ago
parent
commit
5672cf3364
4 changed files with 195 additions and 204 deletions
  1. +191
    -91
      src/main/java/com/ffii/fpsms/api/service/ApiCallerService.kt
  2. +3
    -0
      src/main/java/com/ffii/fpsms/m18/M18Config.kt
  3. +0
    -113
      src/main/java/com/ffii/fpsms/m18/service/M18SchedulerService.kt
  4. +1
    -0
      src/main/resources/application.yml

+ 191
- 91
src/main/java/com/ffii/fpsms/api/service/ApiCallerService.kt View File

@@ -18,18 +18,29 @@ import org.springframework.web.reactive.function.client.WebClient
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
import kotlin.reflect.full.memberProperties import kotlin.reflect.full.memberProperties
import org.springframework.context.annotation.Lazy import org.springframework.context.annotation.Lazy
import org.springframework.http.HttpMethod
import org.springframework.http.HttpRequest
import org.springframework.http.HttpStatusCode 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.ClientRequest
import org.springframework.web.reactive.function.client.ExchangeFilterFunction
import org.springframework.web.reactive.function.client.WebClientResponseException 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 @Service
open class ApiCallerService( open class ApiCallerService(
val m18Config: M18Config, 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() val webClient: WebClient = WebClient.builder()
.baseUrl(m18Config.BASE_URL) .baseUrl(m18Config.BASE_URL)
// .baseUrl(m18Config.BASE_URL_UAT)
.defaultHeaders { headers -> .defaultHeaders { headers ->
headers.set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) headers.set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
// headers.set(HttpHeaders.AUTHORIZATION, "Bearer ${m18Config.ACCESS_TOKEN}") // headers.set(HttpHeaders.AUTHORIZATION, "Bearer ${m18Config.ACCESS_TOKEN}")
@@ -42,15 +53,22 @@ open class ApiCallerService(
.build() .build()
next.exchange(updatedRequest) 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() .build()


@PostConstruct @PostConstruct
fun init() { fun init() {
updateToken()
updateToken().subscribe()
} }


fun updateToken() {
fun updateToken() : Mono<String>{
val params = M18TokenRequest( val params = M18TokenRequest(
grant_type = m18Config.GRANT_TYPE, grant_type = m18Config.GRANT_TYPE,
client_id = m18Config.CLIENT_ID, client_id = m18Config.CLIENT_ID,
@@ -59,96 +77,69 @@ open class ApiCallerService(
password = m18Config.PASSWORD 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 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 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 * @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, urlPath: String,
params: MultiValueMap<String, String>,
customHeaders: Map<String, String>?
params: U? = null,
customHeaders: Map<String, String>? = null
): Mono<T> { ): 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 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 * @return A Mono that emits the response body converted to type T
*/ */
inline fun <reified T : Any> get( inline fun <reified T : Any> get(
urlPath: String, urlPath: String,
params: Map<String, Any>?, params: Map<String, Any>?,
customHeaders: Map<String, String>?
customHeaders: Map<String, String>? = null
): Mono<T> { ): Mono<T> {

// convert to multiValueMap<String, String> // convert to multiValueMap<String, String>
val queryParams = params?.let { paramMap -> val queryParams = params?.let { paramMap ->
LinkedMultiValueMap<String, String>().apply { LinkedMultiValueMap<String, String>().apply {
@@ -161,7 +152,7 @@ open class ApiCallerService(
} }
} ?: LinkedMultiValueMap<String, String>() } ?: 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 urlPath The path to send the GET request to (after /jsf/rfws)
* @param params Optional query parameters to include in the request * @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 * @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, urlPath: String,
params: Map<String, Any>?
params: MultiValueMap<String, String>,
customHeaders: Map<String, String>?
): Mono<T> { ): 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 * 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 * @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, urlPath: String,
params: U?,
customHeaders: Map<String, String>?
params: U? = null,
customHeaders: Map<String, String>? = null
): Mono<T> { ): 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 -> U::class.memberProperties.forEach { property ->
val key = property.name val key = property.name
val value = property.get(params) 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>() } ?: 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 urlPath The path to send the GET request to (after /jsf/rfws)
* @param params Optional query parameters to include in the request * @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 * @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, urlPath: String,
params: U?,
params: MultiValueMap<String, String>,
customHeaders: Map<String, String>?
): Mono<T> { ): 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}") }
)
} }

} }

+ 3
- 0
src/main/java/com/ffii/fpsms/m18/M18Config.kt View File

@@ -28,6 +28,9 @@ open class M18Config {
@Value("\${m18.config.base-url}") @Value("\${m18.config.base-url}")
lateinit var BASE_URL: String; lateinit var BASE_URL: String;


@Value("\${m18.config.base-url-uat}")
lateinit var BASE_URL_UAT: String;

// Supplier // Supplier
// var MATERIAL_PO_SUPPLIER_NOT: List<String> = listOf("P06", "P07", "T62"); // If need oem type // var MATERIAL_PO_SUPPLIER_NOT: List<String> = listOf("P06", "P07", "T62"); // If need oem type




+ 0
- 113
src/main/java/com/ffii/fpsms/m18/service/M18SchedulerService.kt View File

@@ -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)}")
}
}

+ 1
- 0
src/main/resources/application.yml View File

@@ -35,6 +35,7 @@ m18:
username: testingMTMS username: testingMTMS
password: db25f2fc14cd2d2b1e7af307241f548fb03c312a password: db25f2fc14cd2d2b1e7af307241f548fb03c312a
base-url: https://toa.m18saas.com/jsf/rfws base-url: https://toa.m18saas.com/jsf/rfws
base-url-uat: https://toauat.m18saas.com/jsf/rfws
base-password: qwer1234 base-password: qwer1234
supplier: supplier:
shop-po: P06, P07 shop-po: P06, P07


Loading…
Cancel
Save