diff --git a/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/fints4kResource.kt b/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/fints4kResource.kt index 21bb023d..05bd958e 100644 --- a/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/fints4kResource.kt +++ b/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/fints4kResource.kt @@ -1,16 +1,25 @@ package net.dankito.banking.fints.rest +import net.dankito.banking.fints.response.client.AddAccountResponse +import net.dankito.banking.fints.response.client.GetTransactionsResponse import net.dankito.banking.fints.rest.model.dto.request.AddAccountRequestDto import net.dankito.banking.fints.rest.mapper.DtoMapper import net.dankito.banking.fints.rest.model.dto.request.GetAccountsTransactionsRequestDto +import net.dankito.banking.fints.rest.model.dto.request.TanResponseDto +import net.dankito.banking.fints.rest.model.dto.response.AddAccountResponseDto import net.dankito.banking.fints.rest.model.dto.response.GetAccountsTransactionsResponseDto +import net.dankito.banking.fints.rest.model.dto.response.RestResponse import net.dankito.banking.fints.rest.service.fints4kService +import net.dankito.banking.fints.rest.service.model.GetAccountsTransactionsResponse +import org.slf4j.LoggerFactory import javax.inject.Inject import javax.ws.rs.* import javax.ws.rs.core.MediaType @Path("/fints/v1") +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) class fints4kResource { @Inject @@ -20,31 +29,44 @@ class fints4kResource { @POST - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) @Path("addaccount") - fun addAccount( - request: AddAccountRequestDto, - @DefaultValue("false") @QueryParam("showRawResponse") showRawResponse: Boolean - ): Any { - val clientResponse = service.getAddAccountResponse(request) + fun addAccount(request: AddAccountRequestDto): RestResponse { + val response = service.getAddAccountResponse(request) - if (showRawResponse) { - return clientResponse - } - - return mapper.map(clientResponse) + return mapper.createRestResponse(response) { successResponse -> mapper.map(successResponse) } } @POST - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) @Path("transactions") fun getAccountTransactions(request: GetAccountsTransactionsRequestDto): GetAccountsTransactionsResponseDto { - val accountsTransactions = service.getAccountTransactions(request) + val response = service.getAccountTransactions(request) - return mapper.mapTransactions(accountsTransactions) + return mapper.map(response) + } + + + @POST + @Path("tanresponse") + fun tanResponse(dto: TanResponseDto): RestResponse { + val response = service.handleTanResponse(dto) + + // couldn't make it that compiler access ResponseHolder<*> for mapper.createRestResponse(), resulted in very cryptic "{"arity":0}" response -> handle it manually + response.response?.let { successResponse -> + return RestResponse.success(mapSuccessResponse(successResponse)) + } + + // all other cases map here, the responseMapper callback has no function + return mapper.createRestResponse(response) { it!! } + } + + private fun mapSuccessResponse(successResponse: Any): Any { + return when (successResponse) { + is AddAccountResponse -> mapper.map(successResponse) + is GetAccountsTransactionsResponse -> mapper.map(successResponse) + is GetTransactionsResponse -> mapper.mapTransactions(successResponse) + else -> successResponse // add others / new ones here + } } } \ No newline at end of file diff --git a/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/mapper/DtoMapper.kt b/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/mapper/DtoMapper.kt index b54e4059..50187a4c 100644 --- a/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/mapper/DtoMapper.kt +++ b/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/mapper/DtoMapper.kt @@ -4,13 +4,28 @@ import net.dankito.banking.fints.model.* import net.dankito.banking.fints.response.client.AddAccountResponse import net.dankito.banking.fints.response.client.FinTsClientResponse import net.dankito.banking.fints.response.client.GetTransactionsResponse +import net.dankito.banking.fints.rest.model.ResponseHolder import net.dankito.banking.fints.rest.model.dto.response.* +import net.dankito.banking.fints.rest.service.model.GetAccountsTransactionsResponse import java.math.BigDecimal import javax.ws.rs.InternalServerErrorException open class DtoMapper { + fun createRestResponse(responseHolder: ResponseHolder, responseMapper: (DomainType) -> DtoType): RestResponse { + responseHolder.response?.let { response -> + return RestResponse.success(responseMapper(response)) + } + + responseHolder.enterTanRequest?.let { enterTanRequest -> + return RestResponse.requiresTan(enterTanRequest) + } + + return RestResponse.error(responseHolder.error ?: "Unknown error") + } + + open fun map(response: AddAccountResponse): AddAccountResponseDto { return AddAccountResponseDto( response.successful, @@ -38,20 +53,21 @@ open class DtoMapper { } - open fun mapTransactions(accountsTransactions: List?): GetAccountsTransactionsResponseDto { + open fun map(response: GetAccountsTransactionsResponse?): GetAccountsTransactionsResponseDto { + // TODO: is this still the case? // TODO: if a TAN is required then accountsTransactions contains null value(s) (but why?) -> application crashes - if (accountsTransactions == null) { + if (response == null) { throw InternalServerErrorException("Could not fetch account transactions. Either TAN hasn't been entered or developers made a mistake.") } - return GetAccountsTransactionsResponseDto(accountsTransactions.mapNotNull { map(it) }) // TODO: is this correct removing accounts from result for which no transactions have been retrieved? + return GetAccountsTransactionsResponseDto( + // TODO: is this correct removing accounts from result for which no transactions have been retrieved? + response.transactionsPerAccount.filter { it.response?.retrievedData?.isNotEmpty() != false } + .map { createRestResponse(it) { transactionsResponse -> mapTransactions(transactionsResponse) } } + ) } - open fun map(accountTransactions: GetTransactionsResponse): GetAccountTransactionsResponseDto? { - if (accountTransactions.retrievedData.isEmpty()) { - return null - } - + open fun mapTransactions(accountTransactions: GetTransactionsResponse): GetAccountTransactionsResponseDto { val retrievedData = accountTransactions.retrievedData.first() val balance = mapNullable(retrievedData.balance) val bookedTransactions = map(retrievedData.bookedTransactions) diff --git a/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/EnterTanContext.kt b/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/EnterTanContext.kt index 841d874e..2fe11498 100644 --- a/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/EnterTanContext.kt +++ b/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/EnterTanContext.kt @@ -6,8 +6,9 @@ import java.util.concurrent.CountDownLatch import java.util.concurrent.atomic.AtomicReference -open class EnterTanContext( - open val enterTanResult: AtomicReference, - open val countDownLatch: CountDownLatch, - open val tanRequestedTimeStamp: Date = Date() +class EnterTanContext( + val enterTanResult: AtomicReference, + val responseHolder: ResponseHolder<*>, + val countDownLatch: CountDownLatch, + val tanRequestedTimeStamp: Date = Date() ) \ No newline at end of file diff --git a/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/EnteringTanRequested.kt b/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/EnteringTanRequested.kt index 749a262a..7adcfdd2 100644 --- a/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/EnteringTanRequested.kt +++ b/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/EnteringTanRequested.kt @@ -1,11 +1,9 @@ package net.dankito.banking.fints.rest.model -import net.dankito.banking.fints.model.BankData import net.dankito.banking.fints.model.TanChallenge -open class EnteringTanRequested( - open val tanRequestId: String, - open val bank: BankData, - open val tanChallenge: TanChallenge +class EnteringTanRequested( + val tanRequestId: String, + val tanChallenge: TanChallenge ) \ No newline at end of file diff --git a/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/ResponseHolder.kt b/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/ResponseHolder.kt new file mode 100644 index 00000000..02264988 --- /dev/null +++ b/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/ResponseHolder.kt @@ -0,0 +1,63 @@ +package net.dankito.banking.fints.rest.model + +import java.util.concurrent.CountDownLatch + + +class ResponseHolder() { + + private var responseReceivedLatch = CountDownLatch(1) + + constructor(error: String) : this() { + setError(error) + } + + + var response: T? = null + private set + + var error: String? = null + private set + + var enterTanRequest: EnteringTanRequested? = null + private set + + + fun setResponse(response: T) { + this.response = response + + signalResponseReceived() + } + + fun setError(error: String) { + this.error = error + + signalResponseReceived() + } + + fun setEnterTanRequest(enterTanRequest: EnteringTanRequested) { + this.enterTanRequest = enterTanRequest + + signalResponseReceived() + } + + + fun waitForResponse() { + responseReceivedLatch.await() + } + + fun resetAfterEnteringTan() { + this.enterTanRequest = null + + responseReceivedLatch = CountDownLatch(1) + } + + private fun signalResponseReceived() { + responseReceivedLatch.countDown() + } + + + override fun toString(): String { + return "Error: $error, TAN requested: $enterTanRequest, success: $response" + } + +} \ No newline at end of file diff --git a/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/dto/request/TanResponseDto.kt b/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/dto/request/TanResponseDto.kt new file mode 100644 index 00000000..d41f383a --- /dev/null +++ b/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/dto/request/TanResponseDto.kt @@ -0,0 +1,9 @@ +package net.dankito.banking.fints.rest.model.dto.request + +import net.dankito.banking.fints.model.EnterTanResult + + +class TanResponseDto( + val tanRequestId: String, + val enterTanResult: EnterTanResult +) \ No newline at end of file diff --git a/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/dto/response/GetAccountsTransactionsResponseDto.kt b/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/dto/response/GetAccountsTransactionsResponseDto.kt index b6b7f228..dbda2749 100644 --- a/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/dto/response/GetAccountsTransactionsResponseDto.kt +++ b/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/dto/response/GetAccountsTransactionsResponseDto.kt @@ -2,5 +2,5 @@ package net.dankito.banking.fints.rest.model.dto.response open class GetAccountsTransactionsResponseDto( - open val accounts: List + open val transactionsPerAccount: List> ) \ No newline at end of file diff --git a/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/dto/response/ResponseType.kt b/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/dto/response/ResponseType.kt new file mode 100644 index 00000000..bed09865 --- /dev/null +++ b/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/dto/response/ResponseType.kt @@ -0,0 +1,12 @@ +package net.dankito.banking.fints.rest.model.dto.response + + +enum class ResponseType { + + Success, + + Error, + + TanRequired + +} \ No newline at end of file diff --git a/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/dto/response/RestResponse.kt b/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/dto/response/RestResponse.kt new file mode 100644 index 00000000..291b5b5c --- /dev/null +++ b/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/dto/response/RestResponse.kt @@ -0,0 +1,29 @@ +package net.dankito.banking.fints.rest.model.dto.response + +import net.dankito.banking.fints.rest.model.EnteringTanRequested + + +class RestResponse( + val status: ResponseType, + val errorMessage: String?, + val successResponse: T?, + val enteringTanRequested: EnteringTanRequested? = null +) { + + companion object { + + fun success(result: T): RestResponse { + return RestResponse(ResponseType.Success, null, result, null) + } + + fun error(errorMessage: String): RestResponse { + return RestResponse(ResponseType.Error, errorMessage, null, null) + } + + fun requiresTan(enteringTanRequested: EnteringTanRequested): RestResponse { + return RestResponse(ResponseType.TanRequired, null, null, enteringTanRequested) + } + + } + +} \ No newline at end of file diff --git a/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/service/fints4kService.kt b/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/service/fints4kService.kt index e249adae..db759449 100644 --- a/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/service/fints4kService.kt +++ b/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/service/fints4kService.kt @@ -2,15 +2,18 @@ package net.dankito.banking.fints.rest.service import net.dankito.banking.bankfinder.InMemoryBankFinder import net.dankito.banking.fints.FinTsClientForCustomer +import net.dankito.banking.fints.callback.FinTsClientCallback import net.dankito.banking.fints.callback.SimpleFinTsClientCallback import net.dankito.banking.fints.model.* -import net.dankito.banking.fints.response.BankResponse import net.dankito.banking.fints.response.client.AddAccountResponse import net.dankito.banking.fints.response.client.GetTransactionsResponse import net.dankito.banking.fints.rest.model.BankAccessData import net.dankito.banking.fints.rest.model.EnterTanContext -import net.dankito.banking.fints.rest.model.dto.request.AccountRequestDto +import net.dankito.banking.fints.rest.model.EnteringTanRequested +import net.dankito.banking.fints.rest.model.ResponseHolder import net.dankito.banking.fints.rest.model.dto.request.GetAccountsTransactionsRequestDto +import net.dankito.banking.fints.rest.model.dto.request.TanResponseDto +import net.dankito.banking.fints.rest.service.model.GetAccountsTransactionsResponse import java.util.* import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.CountDownLatch @@ -25,21 +28,21 @@ class fints4kService { protected val clientCache = ConcurrentHashMap() - protected val tanRequests = mutableMapOf() + // TODO: create clean up job for timed out TAN requests + protected val tanRequests = ConcurrentHashMap() - fun getAddAccountResponse(accessData: BankAccessData): AddAccountResponse { + fun getAddAccountResponse(accessData: BankAccessData): ResponseHolder { val (bank, errorMessage) = mapToBankData(accessData) if (errorMessage != null) { - return AddAccountResponse(BankResponse(false, errorMessage = errorMessage), bank) + return ResponseHolder(errorMessage) } return getAccountData(bank) } - // TODO: as in most cases we really just want the account data, so just retrieve these without balances and transactions - protected fun getAccountData(bank: BankData): AddAccountResponse { + protected fun getAccountData(bank: BankData): ResponseHolder { return getAsyncResponse(bank) { client, responseRetrieved -> client.addAccountAsync(AddAccountParameter(bank)) { response -> responseRetrieved(response) @@ -48,28 +51,32 @@ class fints4kService { } - fun getAccountTransactions(dto: GetAccountsTransactionsRequestDto): List { + fun getAccountTransactions(dto: GetAccountsTransactionsRequestDto): GetAccountsTransactionsResponse { val (bank, errorMessage) = mapToBankData(dto.credentials) if (errorMessage != null) { - return listOf(GetTransactionsResponse(BankResponse(false, errorMessage = errorMessage))) + return GetAccountsTransactionsResponse(listOf(ResponseHolder(errorMessage))) } - return dto.accounts.map { accountDto -> - val account = findAccount(dto.credentials, accountDto) + val retrievedAccounts = getAccounts(bank) + + val transactionsPerAccount = dto.accounts.map { accountDto -> + val account = retrievedAccounts?.firstOrNull { it.accountIdentifier == accountDto.identifier } return@map if (account != null) { val parameter = GetTransactionsParameter(account, dto.alsoRetrieveBalance, dto.fromDate, dto.toDate, abortIfTanIsRequired = dto.abortIfTanIsRequired) getAccountTransactions(bank, parameter) } else { - GetTransactionsResponse(BankResponse(false, errorMessage = "Account with identifier '${accountDto.identifier}' not found. Available accounts: " + - "${getCachedClient(dto.credentials)?.bank?.accounts?.map { it.accountIdentifier }?.joinToString(", ")}")) + ResponseHolder("Account with identifier '${accountDto.identifier}' not found. Available accounts: " + + "${retrievedAccounts?.joinToString(", ") { it.accountIdentifier }}") } } + + return GetAccountsTransactionsResponse(transactionsPerAccount) } - fun getAccountTransactions(bank: BankData, parameter: GetTransactionsParameter): GetTransactionsResponse { + fun getAccountTransactions(bank: BankData, parameter: GetTransactionsParameter): ResponseHolder { return getAsyncResponse(bank) { client, responseRetrieved -> client.getTransactionsAsync(parameter) { response -> responseRetrieved(response) @@ -78,50 +85,68 @@ class fints4kService { } - protected fun getAsyncResponse(bank: BankData, executeRequest: (FinTsClientForCustomer, ((T) -> Unit)) -> Unit): T { - val result = AtomicReference() - val countDownLatch = CountDownLatch(1) + fun handleTanResponse(dto: TanResponseDto): ResponseHolder<*> { + tanRequests.remove(dto.tanRequestId)?.let { enterTanContext -> + val responseHolder = enterTanContext.responseHolder + responseHolder.resetAfterEnteringTan() - val client = getClient(bank, result, countDownLatch) + enterTanContext.enterTanResult.set(dto.enterTanResult) + enterTanContext.countDownLatch.countDown() - executeRequest(client) { response -> - result.set(response) - countDownLatch.countDown() + responseHolder.waitForResponse() + + return responseHolder } - countDownLatch.await() - - return result.get() + return ResponseHolder("No TAN request found for TAN Request ID '${dto.tanRequestId}'") } - private fun getClient(bank: BankData, result: AtomicReference, countDownLatch: CountDownLatch): FinTsClientForCustomer { + + protected fun getAsyncResponse(bank: BankData, executeRequest: (FinTsClientForCustomer, ((T) -> Unit)) -> Unit): ResponseHolder { + val responseHolder = ResponseHolder() + + val client = getClient(bank, responseHolder) + + executeRequest(client) { response -> + responseHolder.setResponse(response) + } + + responseHolder.waitForResponse() + + return responseHolder + } + + private fun getClient(bank: BankData, responseHolder: ResponseHolder): FinTsClientForCustomer { val cacheKey = getCacheKey(bank.bankCode, bank.customerId) clientCache[cacheKey]?.let { + // TODO: this will not work for two parallel calls for the same account if both calls require entering a TAN as second one overwrites callback and ResponseHolder of first one -> first one blocks forever + it.setCallback(createFinTsClientCallback(responseHolder)) // we have to newly create callback otherwise ResponseHolder instance of when client was created is used -> its CountDownLatch would never signal return it } -// val client = FinTsClient(SimpleFinTsClientCallback { supportedTanMethods: List, suggestedTanMethod: TanMethod? -> - val client = FinTsClientForCustomer(bank, SimpleFinTsClientCallback({ bank, tanChallenge -> handleEnterTan(bank, tanChallenge, countDownLatch, result) }) { supportedTanMethods, suggestedTanMethod -> - suggestedTanMethod - }) + val client = FinTsClientForCustomer(bank, createFinTsClientCallback(responseHolder)) clientCache[cacheKey] = client return client } - protected fun handleEnterTan(bank: BankData, tanChallenge: TanChallenge, originatingRequestLatch: CountDownLatch, originatingRequestResult: AtomicReference): EnterTanResult { + private fun createFinTsClientCallback(responseHolder: ResponseHolder): FinTsClientCallback { + return SimpleFinTsClientCallback({ bank, tanChallenge -> handleEnterTan(bank, tanChallenge, responseHolder) }) { supportedTanMethods, suggestedTanMethod -> + suggestedTanMethod + } + } + + protected fun handleEnterTan(bank: BankData, tanChallenge: TanChallenge, responseHolder: ResponseHolder): EnterTanResult { val enterTanResult = AtomicReference() val enterTanLatch = CountDownLatch(1) val tanRequestId = UUID.randomUUID().toString() - // TODO: find a solution for returning TAN challenge to caller - //originatingRequestResult.set(EnteringTanRequested(tanRequestId, bank, tanChallenge)) - originatingRequestLatch.countDown() + tanRequests.put(tanRequestId, EnterTanContext(enterTanResult, responseHolder, enterTanLatch)) - tanRequests.put(tanRequestId, EnterTanContext(enterTanResult, enterTanLatch)) + responseHolder.setEnterTanRequest(EnteringTanRequested(tanRequestId, tanChallenge)) enterTanLatch.await() @@ -146,11 +171,24 @@ class fints4kService { return Pair(bank, null) } - protected fun findAccount(credentials: BankAccessData, accountDto: AccountRequestDto): AccountData? { - return getCachedClient(credentials)?.bank?.accounts?.firstOrNull { it.accountIdentifier == accountDto.identifier } + + protected fun getAccounts(bank: BankData): List? { + getCachedClient(bank)?.bank?.accounts?.let { + return it + } + + val addAccountResponse = getAccountData(bank) + + return addAccountResponse.response?.bank?.accounts } + private fun getCachedClient(bank: BankData): FinTsClientForCustomer? { + val cacheKey = getCacheKey(bank.bankCode, bank.customerId) + + return clientCache[cacheKey] + } + private fun getCachedClient(credentials: BankAccessData): FinTsClientForCustomer? { val cacheKey = getCacheKey(credentials.bankCode, credentials.loginName) diff --git a/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/service/model/GetAccountsTransactionsResponse.kt b/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/service/model/GetAccountsTransactionsResponse.kt new file mode 100644 index 00000000..c686e3e1 --- /dev/null +++ b/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/service/model/GetAccountsTransactionsResponse.kt @@ -0,0 +1,9 @@ +package net.dankito.banking.fints.rest.service.model + +import net.dankito.banking.fints.response.client.GetTransactionsResponse +import net.dankito.banking.fints.rest.model.ResponseHolder + + +class GetAccountsTransactionsResponse( + val transactionsPerAccount: List> +) \ No newline at end of file