From 4ddb55e6128bdaddced721800f569699272f48d0 Mon Sep 17 00:00:00 2001 From: dankito Date: Tue, 22 Dec 2020 14:15:41 +0100 Subject: [PATCH] Extracted RequestExecutor --- .../banking/fints/FinTsClientForCustomer.kt | 16 +- .../dankito/banking/fints/FinTsJobExecutor.kt | 177 +--------------- .../dankito/banking/fints/RequestExecutor.kt | 197 ++++++++++++++++++ 3 files changed, 216 insertions(+), 174 deletions(-) create mode 100644 fints4k/src/commonMain/kotlin/net/dankito/banking/fints/RequestExecutor.kt diff --git a/fints4k/src/commonMain/kotlin/net/dankito/banking/fints/FinTsClientForCustomer.kt b/fints4k/src/commonMain/kotlin/net/dankito/banking/fints/FinTsClientForCustomer.kt index dabd2f98..ff252059 100644 --- a/fints4k/src/commonMain/kotlin/net/dankito/banking/fints/FinTsClientForCustomer.kt +++ b/fints4k/src/commonMain/kotlin/net/dankito/banking/fints/FinTsClientForCustomer.kt @@ -1,11 +1,9 @@ package net.dankito.banking.fints import net.dankito.banking.fints.callback.FinTsClientCallback -import net.dankito.banking.fints.log.MessageLogCollector import net.dankito.banking.fints.messages.MessageBuilder import net.dankito.banking.fints.model.* import net.dankito.banking.fints.model.mapper.ModelMapper -import net.dankito.banking.fints.response.ResponseParser import net.dankito.banking.fints.response.client.AddAccountResponse import net.dankito.banking.fints.response.client.FinTsClientResponse import net.dankito.banking.fints.response.client.GetTransactionsResponse @@ -20,17 +18,19 @@ import net.dankito.banking.fints.webclient.KtorWebClient open class FinTsClientForCustomer( val bank: BankData, callback: FinTsClientCallback, - webClient: IWebClient = KtorWebClient(), - base64Service: IBase64Service = PureKotlinBase64Service(), + requestExecutor: RequestExecutor = RequestExecutor(), messageBuilder: MessageBuilder = MessageBuilder(), - responseParser: ResponseParser = ResponseParser(), mt940Parser: IAccountTransactionsParser = Mt940AccountTransactionsParser(), - messageLogCollector: MessageLogCollector = MessageLogCollector(), modelMapper: ModelMapper = ModelMapper(messageBuilder), - product: ProductData = ProductData("15E53C26816138699C7B6A3E8", "1.0.0") // TODO: get version dynamically){} + product: ProductData = ProductData("15E53C26816138699C7B6A3E8", "1.0.0") // TODO: get version dynamically) ) { - protected val client = FinTsClient(FinTsJobExecutor(callback, webClient, base64Service, messageBuilder, responseParser, mt940Parser, messageLogCollector, modelMapper, product)) + constructor(bank: BankData, callback: FinTsClientCallback, webClient: IWebClient = KtorWebClient(), base64Service: IBase64Service = PureKotlinBase64Service(), + product: ProductData = ProductData("15E53C26816138699C7B6A3E8", "1.0.0")) // TODO: get version dynamically) + : this(bank, callback, RequestExecutor(MessageBuilder(), webClient, base64Service)) + + + protected val client = FinTsClient(FinTsJobExecutor(callback, requestExecutor, messageBuilder, mt940Parser, modelMapper, product)) open val messageLogWithoutSensitiveData: List diff --git a/fints4k/src/commonMain/kotlin/net/dankito/banking/fints/FinTsJobExecutor.kt b/fints4k/src/commonMain/kotlin/net/dankito/banking/fints/FinTsJobExecutor.kt index 0f89349e..07e65a7b 100644 --- a/fints4k/src/commonMain/kotlin/net/dankito/banking/fints/FinTsJobExecutor.kt +++ b/fints4k/src/commonMain/kotlin/net/dankito/banking/fints/FinTsJobExecutor.kt @@ -1,8 +1,6 @@ package net.dankito.banking.fints import net.dankito.banking.fints.callback.FinTsClientCallback -import net.dankito.banking.fints.log.IMessageLogAppender -import net.dankito.banking.fints.log.MessageLogCollector import net.dankito.banking.fints.messages.MessageBuilder import net.dankito.banking.fints.messages.MessageBuilderResult import net.dankito.banking.fints.messages.datenelemente.implementierte.signatur.VersionDesSicherheitsverfahrens @@ -13,7 +11,6 @@ import net.dankito.banking.fints.model.* import net.dankito.banking.fints.model.mapper.ModelMapper import net.dankito.banking.fints.response.BankResponse import net.dankito.banking.fints.response.InstituteSegmentId -import net.dankito.banking.fints.response.ResponseParser import net.dankito.banking.fints.response.client.FinTsClientResponse import net.dankito.banking.fints.response.client.GetTanMediaListResponse import net.dankito.banking.fints.response.client.GetTransactionsResponse @@ -23,15 +20,8 @@ import net.dankito.banking.fints.tan.FlickerCodeDecoder import net.dankito.banking.fints.tan.TanImageDecoder import net.dankito.banking.fints.transactions.IAccountTransactionsParser import net.dankito.banking.fints.transactions.Mt940AccountTransactionsParser -import net.dankito.banking.fints.util.IBase64Service -import net.dankito.banking.fints.util.PureKotlinBase64Service -import net.dankito.utils.multiplatform.log.Logger import net.dankito.utils.multiplatform.log.LoggerFactory -import net.dankito.banking.fints.webclient.IWebClient -import net.dankito.banking.fints.webclient.KtorWebClient -import net.dankito.banking.fints.webclient.WebClientResponse import net.dankito.utils.multiplatform.Date -import net.dankito.utils.multiplatform.getInnerExceptionMessage import net.dankito.utils.multiplatform.ObjectReference @@ -42,12 +32,9 @@ import net.dankito.utils.multiplatform.ObjectReference */ open class FinTsJobExecutor( protected open val callback: FinTsClientCallback, - protected open val webClient: IWebClient = KtorWebClient(), - protected open val base64Service: IBase64Service = PureKotlinBase64Service(), + protected open val requestExecutor: RequestExecutor = RequestExecutor(), protected open val messageBuilder: MessageBuilder = MessageBuilder(), - protected open val responseParser: ResponseParser = ResponseParser(), protected open val mt940Parser: IAccountTransactionsParser = Mt940AccountTransactionsParser(), - protected open val messageLogCollector: MessageLogCollector = MessageLogCollector(), protected open val modelMapper: ModelMapper = ModelMapper(messageBuilder), protected open val product: ProductData = ProductData("15E53C26816138699C7B6A3E8", "1.0.0") // TODO: get version dynamically ) { @@ -58,20 +45,11 @@ open class FinTsJobExecutor( open val messageLogWithoutSensitiveData: List - get() = messageLogCollector.messageLogWithoutSensitiveData - - protected open val messageLogAppender: IMessageLogAppender = object : IMessageLogAppender { - - override fun logError(message: String, e: Exception?, logger: Logger?, bank: BankData?) { - messageLogCollector.logError(message, e, logger, bank) - } - - } + get() = requestExecutor.messageLogWithoutSensitiveData init { - responseParser.logAppender = messageLogAppender - mt940Parser.logAppender = messageLogAppender + mt940Parser.logAppender = requestExecutor.messageLogAppender // TODO: find a better solution to append messages to MessageLog } @@ -382,142 +360,17 @@ open class FinTsJobExecutor( protected open fun getAndHandleResponseForMessage(message: MessageBuilderResult, dialogContext: DialogContext, callback: (BankResponse) -> Unit) { - if (message.createdMessage == null) { - callback(BankResponse(false, messageCreationError = message)) - } - else { - getAndHandleResponseForMessage(message.createdMessage, dialogContext) { response -> - handleMayRequiresTan(response, dialogContext) { handledResponse -> - // if there's a Aufsetzpunkt (continuationId) set, then response is not complete yet, there's more information to fetch by sending this Aufsetzpunkt - handledResponse.aufsetzpunkt?.let { continuationId -> - if (handledResponse.followUpResponse == null) { // for re-sent messages followUpResponse is already set and dialog already closed -> would be overwritten with an error response that dialog is closed - if (message.isSendEnteredTanMessage() == false) { // for sending TAN no follow up message can be created -> filter out, otherwise chunkedResponseHandler would get called twice for same response - dialogContext.chunkedResponseHandler?.invoke(handledResponse) - } - - getFollowUpMessageForContinuationId(handledResponse, continuationId, message, dialogContext) { followUpResponse -> - handledResponse.followUpResponse = followUpResponse - handledResponse.hasFollowUpMessageButCouldNotReceiveIt = handledResponse.followUpResponse == null - - callback(handledResponse) - } - } - else { - callback(handledResponse) - } - } - ?: run { - // e.g. response = enter TAN response, but handledResponse is then response after entering TAN, e.g. account transactions - // -> chunkedResponseHandler would get called for same handledResponse multiple times - if (response == handledResponse) { - dialogContext.chunkedResponseHandler?.invoke(handledResponse) - } - - callback(handledResponse) - } - } - } - } - } - - protected open fun getAndHandleResponseForMessage(requestBody: String, dialogContext: DialogContext, callback: (BankResponse) -> Unit) { - addMessageLog(requestBody, MessageLogEntryType.Sent, dialogContext) - - getResponseForMessage(requestBody, dialogContext.bank.finTs3ServerAddress) { webResponse -> - val response = handleResponse(webResponse, dialogContext) - - dialogContext.response = response - - response.messageHeader?.let { header -> dialogContext.dialogId = header.dialogId } - dialogContext.didBankCloseDialog = response.didBankCloseDialog - - callback(response) - } - } - - protected open fun getResponseForMessage(requestBody: String, finTs3ServerAddress: String, callback: (WebClientResponse) -> Unit) { - val encodedRequestBody = base64Service.encode(requestBody) - - webClient.post(finTs3ServerAddress, encodedRequestBody, "application/octet-stream", IWebClient.DefaultUserAgent, callback) + requestExecutor.getAndHandleResponseForMessage( + message, + dialogContext, + { tanResponse, bankResponse, tanRequiredCallback -> handleEnteringTanRequired(tanResponse, bankResponse, dialogContext, tanRequiredCallback) }, + callback) } protected open fun fireAndForgetMessage(message: MessageBuilderResult, dialogContext: DialogContext) { - message.createdMessage?.let { requestBody -> - addMessageLog(requestBody, MessageLogEntryType.Sent, dialogContext) - - getResponseForMessage(requestBody, dialogContext.bank.finTs3ServerAddress) { } - - // if really needed add received response to message log here - } + requestExecutor.fireAndForgetMessage(message, dialogContext) } - protected open fun handleResponse(webResponse: WebClientResponse, dialogContext: DialogContext): BankResponse { - val responseBody = webResponse.body - - if (webResponse.successful && responseBody != null) { - - try { - val decodedResponse = decodeBase64Response(responseBody) - - addMessageLog(decodedResponse, MessageLogEntryType.Received, dialogContext) - - return responseParser.parse(decodedResponse) - } catch (e: Exception) { - logError("Could not decode responseBody:\r\n'$responseBody'", dialogContext, e) - - return BankResponse(false, errorMessage = e.getInnerExceptionMessage()) - } - } - else { - val bank = dialogContext.bank - logError("Request to $bank (${bank.finTs3ServerAddress}) failed", dialogContext, webResponse.error) - } - - return BankResponse(false, errorMessage = webResponse.error?.getInnerExceptionMessage()) - } - - protected open fun decodeBase64Response(responseBody: String): String { - return base64Service.decode(responseBody.replace("\r", "").replace("\n", "")) - } - - - protected open fun getFollowUpMessageForContinuationId(response: BankResponse, continuationId: String, message: MessageBuilderResult, - dialogContext: DialogContext, callback: (BankResponse?) -> Unit) { - - messageBuilder.rebuildMessageWithContinuationId(message, continuationId, dialogContext)?.let { followUpMessage -> - getAndHandleResponseForMessage(followUpMessage, dialogContext, callback) - } - ?: run { callback(null) } - } - - - protected open fun handleMayRequiresTan(response: BankResponse, dialogContext: DialogContext, callback: (BankResponse) -> Unit) { // TODO: use response from DialogContext - - if (response.isStrongAuthenticationRequired) { - if (dialogContext.abortIfTanIsRequired) { - response.tanRequiredButWeWereToldToAbortIfSo = true - - callback(response) - return - } - else if (response.tanResponse != null) { - response.tanResponse?.let { tanResponse -> - handleEnteringTanRequired(tanResponse, response, dialogContext, callback) - } - - return - } - } - - // TODO: check if response contains '3931 TAN-Generator gesperrt, Synchronisierung erforderlich' or - // '3933 TAN-Generator gesperrt, Synchronisierung erforderlich Kartennummer ##########' message, - // call callback.enterAtc() and implement and call HKTSY job (p. 77) - - // TODO: also check '9931 Sperrung des Kontos nach %1 Fehlversuchen' -> if %1 == 3 synchronize TAN generator - // as it's quite unrealistic that user entered TAN wrong three times, in most cases TAN generator is not synchronized - - callback(response) - } protected open fun handleEnteringTanRequired(tanResponse: TanResponse, response: BankResponse, dialogContext: DialogContext, callback: (BankResponse) -> Unit) { val bank = dialogContext.bank // TODO: copy required data to TanChallenge @@ -555,7 +408,8 @@ open class FinTsJobExecutor( } protected open fun mayRetrieveAutomaticallyIfUserEnteredDecoupledTan(tanChallenge: TanChallenge, tanResponse: TanResponse, - userDidCancelEnteringTan: ObjectReference, dialogContext: DialogContext) { + userDidCancelEnteringTan: ObjectReference, dialogContext: DialogContext + ) { dialogContext.bank.selectedTanMethod.decoupledParameters?.let { decoupledTanMethodParameters -> if (tanResponse.tanProcess == TanProcess.AppTan && decoupledTanMethodParameters.periodicStateRequestsAllowed) { automaticallyRetrieveIfUserEnteredDecoupledTan(tanChallenge, userDidCancelEnteringTan, dialogContext) @@ -840,13 +694,4 @@ open class FinTsJobExecutor( return modelMapper.isJobSupported(bank, segmentId) } - - protected open fun addMessageLog(message: String, type: MessageLogEntryType, dialogContext: DialogContext) { - messageLogCollector.addMessageLog(message, type, dialogContext.bank) - } - - protected open fun logError(message: String, dialogContext: DialogContext, e: Exception?) { - messageLogAppender.logError(message, e, log, dialogContext.bank) - } - } \ No newline at end of file diff --git a/fints4k/src/commonMain/kotlin/net/dankito/banking/fints/RequestExecutor.kt b/fints4k/src/commonMain/kotlin/net/dankito/banking/fints/RequestExecutor.kt new file mode 100644 index 00000000..2d8126b5 --- /dev/null +++ b/fints4k/src/commonMain/kotlin/net/dankito/banking/fints/RequestExecutor.kt @@ -0,0 +1,197 @@ +package net.dankito.banking.fints + +import net.dankito.banking.fints.log.IMessageLogAppender +import net.dankito.banking.fints.log.MessageLogCollector +import net.dankito.banking.fints.messages.MessageBuilder +import net.dankito.banking.fints.messages.MessageBuilderResult +import net.dankito.banking.fints.model.* +import net.dankito.banking.fints.response.BankResponse +import net.dankito.banking.fints.response.ResponseParser +import net.dankito.banking.fints.response.segments.TanResponse +import net.dankito.banking.fints.util.IBase64Service +import net.dankito.banking.fints.util.PureKotlinBase64Service +import net.dankito.banking.fints.webclient.IWebClient +import net.dankito.banking.fints.webclient.KtorWebClient +import net.dankito.banking.fints.webclient.WebClientResponse +import net.dankito.utils.multiplatform.getInnerExceptionMessage +import net.dankito.utils.multiplatform.log.Logger +import net.dankito.utils.multiplatform.log.LoggerFactory + + +open class RequestExecutor( + protected open val messageBuilder: MessageBuilder = MessageBuilder(), + protected open val webClient: IWebClient = KtorWebClient(), + protected open val base64Service: IBase64Service = PureKotlinBase64Service(), + protected open val responseParser: ResponseParser = ResponseParser(), + protected open val messageLogCollector: MessageLogCollector = MessageLogCollector() +) { + + companion object { + private val log = LoggerFactory.getLogger(FinTsJobExecutor::class) + } + + + open val messageLogWithoutSensitiveData: List + get() = messageLogCollector.messageLogWithoutSensitiveData + + internal open val messageLogAppender: IMessageLogAppender = object : IMessageLogAppender { + + override fun logError(message: String, e: Exception?, logger: Logger?, bank: BankData?) { + messageLogCollector.logError(message, e, logger, bank) + } + + } + + + open fun getAndHandleResponseForMessage(message: MessageBuilderResult, dialogContext: DialogContext, + tanRequiredCallback: (TanResponse, BankResponse, callback: (BankResponse) -> Unit) -> Unit, callback: (BankResponse) -> Unit) { + if (message.createdMessage == null) { + callback(BankResponse(false, messageCreationError = message)) + } + else { + getAndHandleResponseForMessage(message.createdMessage, dialogContext) { response -> + handleMayRequiresTan(response, dialogContext, tanRequiredCallback) { handledResponse -> + // if there's a Aufsetzpunkt (continuationId) set, then response is not complete yet, there's more information to fetch by sending this Aufsetzpunkt + handledResponse.aufsetzpunkt?.let { continuationId -> + if (handledResponse.followUpResponse == null) { // for re-sent messages followUpResponse is already set and dialog already closed -> would be overwritten with an error response that dialog is closed + if (message.isSendEnteredTanMessage() == false) { // for sending TAN no follow up message can be created -> filter out, otherwise chunkedResponseHandler would get called twice for same response + dialogContext.chunkedResponseHandler?.invoke(handledResponse) + } + + getFollowUpMessageForContinuationId(handledResponse, continuationId, message, dialogContext, tanRequiredCallback) { followUpResponse -> + handledResponse.followUpResponse = followUpResponse + handledResponse.hasFollowUpMessageButCouldNotReceiveIt = handledResponse.followUpResponse == null + + callback(handledResponse) + } + } + else { + callback(handledResponse) + } + } + ?: run { + // e.g. response = enter TAN response, but handledResponse is then response after entering TAN, e.g. account transactions + // -> chunkedResponseHandler would get called for same handledResponse multiple times + if (response == handledResponse) { + dialogContext.chunkedResponseHandler?.invoke(handledResponse) + } + + callback(handledResponse) + } + } + } + } + } + + protected open fun getAndHandleResponseForMessage(requestBody: String, dialogContext: DialogContext, callback: (BankResponse) -> Unit) { + addMessageLog(requestBody, MessageLogEntryType.Sent, dialogContext) + + getResponseForMessage(requestBody, dialogContext.bank.finTs3ServerAddress) { webResponse -> + val response = handleResponse(webResponse, dialogContext) + + dialogContext.response = response + + response.messageHeader?.let { header -> dialogContext.dialogId = header.dialogId } + dialogContext.didBankCloseDialog = response.didBankCloseDialog + + callback(response) + } + } + + protected open fun getResponseForMessage(requestBody: String, finTs3ServerAddress: String, callback: (WebClientResponse) -> Unit) { + val encodedRequestBody = base64Service.encode(requestBody) + + webClient.post(finTs3ServerAddress, encodedRequestBody, "application/octet-stream", IWebClient.DefaultUserAgent, callback) + } + + open fun fireAndForgetMessage(message: MessageBuilderResult, dialogContext: DialogContext) { + message.createdMessage?.let { requestBody -> + addMessageLog(requestBody, MessageLogEntryType.Sent, dialogContext) + + getResponseForMessage(requestBody, dialogContext.bank.finTs3ServerAddress) { } + + // if really needed add received response to message log here + } + } + + protected open fun handleResponse(webResponse: WebClientResponse, dialogContext: DialogContext): BankResponse { + val responseBody = webResponse.body + + if (webResponse.successful && responseBody != null) { + + try { + val decodedResponse = decodeBase64Response(responseBody) + + addMessageLog(decodedResponse, MessageLogEntryType.Received, dialogContext) + + return responseParser.parse(decodedResponse) + } catch (e: Exception) { + logError("Could not decode responseBody:\r\n'$responseBody'", dialogContext, e) + + return BankResponse(false, errorMessage = e.getInnerExceptionMessage()) + } + } + else { + val bank = dialogContext.bank + logError("Request to $bank (${bank.finTs3ServerAddress}) failed", dialogContext, webResponse.error) + } + + return BankResponse(false, errorMessage = webResponse.error?.getInnerExceptionMessage()) + } + + protected open fun decodeBase64Response(responseBody: String): String { + return base64Service.decode(responseBody.replace("\r", "").replace("\n", "")) + } + + + protected open fun getFollowUpMessageForContinuationId(response: BankResponse, continuationId: String, message: MessageBuilderResult, dialogContext: DialogContext, + tanRequiredCallback: (TanResponse, BankResponse, callback: (BankResponse) -> Unit) -> Unit, + callback: (BankResponse?) -> Unit) { + + messageBuilder.rebuildMessageWithContinuationId(message, continuationId, dialogContext)?.let { followUpMessage -> + getAndHandleResponseForMessage(followUpMessage, dialogContext, tanRequiredCallback, callback) + } + ?: run { callback(null) } + } + + + protected open fun handleMayRequiresTan(response: BankResponse, dialogContext: DialogContext, + tanRequiredCallback: (TanResponse, BankResponse, callback: (BankResponse) -> Unit) -> Unit, + callback: (BankResponse) -> Unit) { // TODO: use response from DialogContext + + if (response.isStrongAuthenticationRequired) { + if (dialogContext.abortIfTanIsRequired) { + response.tanRequiredButWeWereToldToAbortIfSo = true + + callback(response) + return + } + else if (response.tanResponse != null) { + response.tanResponse?.let { tanResponse -> + tanRequiredCallback(tanResponse, response, callback) + } + + return + } + } + + // TODO: check if response contains '3931 TAN-Generator gesperrt, Synchronisierung erforderlich' or + // '3933 TAN-Generator gesperrt, Synchronisierung erforderlich Kartennummer ##########' message, + // call callback.enterAtc() and implement and call HKTSY job (p. 77) + + // TODO: also check '9931 Sperrung des Kontos nach %1 Fehlversuchen' -> if %1 == 3 synchronize TAN generator + // as it's quite unrealistic that user entered TAN wrong three times, in most cases TAN generator is not synchronized + + callback(response) + } + + + protected open fun addMessageLog(message: String, type: MessageLogEntryType, dialogContext: DialogContext) { + messageLogCollector.addMessageLog(message, type, dialogContext.bank) + } + + protected open fun logError(message: String, dialogContext: DialogContext, e: Exception?) { + messageLogAppender.logError(message, e, log, dialogContext.bank) + } + +} \ No newline at end of file