Converted fints4k to a Kotlin multi platform project

This commit is contained in:
dankito 2020-06-03 17:49:29 +02:00
parent d1bb7d81c3
commit e44a68addc
347 changed files with 2161 additions and 1138 deletions

View File

@ -4,9 +4,24 @@ ext {
appVersionCode = 3
/* MPP / basic dependencies */
kotlinVersion = '1.3.72'
kotlinCoroutinesVersion = "1.3.5"
serializationVersion = "0.20.0"
ktorVersion = "1.3.1"
klockVersion = "1.8.4"
bigNumVersion = "0.1.5"
uuidVersion = "0.1.0"
javaUtilsVersion = '1.0.16'
luceneUtilsVersion = "0.5.1-SNAPSHOT"
@ -17,6 +32,15 @@ ext {
/* Android */
androidCompileSdkVersion = 28
androidBuildToolsVersion = "29.0.3"
androidMinSdkVersion = 16
androidTargetSdkVersion = 28
androidUtilsVersion = '1.1.1-SNAPSHOT'
materialDrawerVersion = "8.0.1"
@ -44,6 +68,10 @@ ext {
/* Test */
junitVersion = '4.12'
junit5Version = '5.5.2'
atriumVersion = "0.12.0"
assertJVersion = '3.12.2'
mockitoVersion = '2.22.0'
@ -54,15 +82,16 @@ ext {
}
buildscript {
ext.kotlin_version = '1.3.61'
ext.kotlin_version = '1.3.72'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.1'
classpath 'com.android.tools.build:gradle:4.0.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
// Nexus staging plugin has to be downgraded to 0.10.0 to be applicable to sub projects, see https://github.com/UweTrottmann/SeriesGuide/commit/ca33e8ad2fa6cc5c426450c8aef3417ba073ca7f
classpath "io.codearte.gradle.nexus:gradle-nexus-staging-plugin:0.10.0"

10
fints4k-jvm/build.gradle Normal file
View File

@ -0,0 +1,10 @@
apply plugin: 'java-library'
apply plugin: 'kotlin'
sourceCompatibility = "8"
targetCompatibility = "8"
dependencies {
api project(":fints4k")
}

View File

@ -1,36 +1,144 @@
apply plugin: 'java-library'
apply plugin: 'kotlin'
buildscript {
repositories {
jcenter()
}
}
sourceCompatibility = "1.7"
targetCompatibility = "1.7"
compileKotlin.kotlinOptions.jvmTarget = "1.6"
compileTestKotlin.kotlinOptions.jvmTarget = "1.8"
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
}
dependencies {
api "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
api "net.dankito.utils:java-utils:$javaUtilsVersion"
implementation "net.dankito.search:lucene-4-utils:$luceneUtilsVersion"
testImplementation "junit:junit:$junitVersion"
testImplementation "org.assertj:assertj-core:$assertJVersion"
testImplementation "org.mockito:mockito-core:$mockitoVersion"
testImplementation "com.nhaarman:mockito-kotlin:$mockitoKotlinVersion" // so that Mockito.any() doesn't return null which null-safe Kotlin parameter don't like
// for how to enable mocking final class (which is standard in Kotlin) with Mockito see https://github.com/mockito/mockito/wiki/What's-new-in-Mockito-2#mock-the-unmockable-opt-in-mocking-of-final-classesmethods
testImplementation "ch.qos.logback:logback-classic:$logbackVersion"
testImplementation("net.dankito.utils:java-fx-utils:$javaFxUtilsVersion") {
exclude group: "org.controlsfx"
kotlin {
jvm("jvm6") {
compilations.main.kotlinOptions {
jvmTarget = "1.6"
}
}
android()
sourceSets {
commonMain {
dependencies {
implementation kotlin("stdlib-common")
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core-common:$kotlinCoroutinesVersion"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime-common:$serializationVersion"
implementation "io.ktor:ktor-client-core:$ktorVersion"
//
// implementation("io.ktor:ktor-client-serialization:$ktor_version")
api "com.soywiz.korlibs.klock:klock:$klockVersion"
api("com.ionspin.kotlin:bignum:$bigNumVersion")
implementation "com.benasher44:uuid:$uuidVersion"
}
}
commonTest {
dependencies {
implementation kotlin("test-common")
implementation kotlin("test-annotations-common")
implementation project(":BankingUiCommon")
implementation project(":BankFinder")
implementation project(":fints4kBankingClient")
implementation "ch.tutteli.atrium:atrium-fluent-en_GB:$atriumVersion"
}
}
jvm6Main {
dependencies {
// implementation "io.ktor:ktor-client-cio:$ktorVersion"
implementation "io.ktor:ktor-client-okhttp:$ktorVersion"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:$serializationVersion"
api "com.soywiz.korlibs.klock:klock-jvm:$klockVersion"
implementation "org.slf4j:slf4j-api:$slf4jVersion"
}
}
jvm6Test {
dependencies {
implementation kotlin("test-junit")
implementation "org.junit.jupiter:junit-jupiter:$junit5Version"
runtimeOnly "org.junit.jupiter:junit-jupiter-engine:$junit5Version"
implementation "org.assertj:assertj-core:$assertJVersion"
implementation "org.mockito:mockito-core:$mockitoVersion"
implementation "org.apache.commons:commons-csv:1.8"
implementation "org.slf4j:slf4j-simple:$slf4jVersion"
}
}
androidMain {
dependsOn jvm6Main
dependencies {
implementation "io.ktor:ktor-client-android:$ktorVersion"
}
}
}
}
android {
compileSdkVersion androidCompileSdkVersion
defaultConfig {
minSdkVersion androidMinSdkVersion
targetSdkVersion androidTargetSdkVersion
versionName version
versionCode appVersionCode
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
packagingOptions {
pickFirst 'META-INF/ktor-http.kotlin_module'
pickFirst 'META-INF/kotlinx-io.kotlin_module'
pickFirst 'META-INF/atomicfu.kotlin_module'
pickFirst 'META-INF/ktor-utils.kotlin_module'
pickFirst 'META-INF/kotlinx-coroutines-io.kotlin_module'
pickFirst 'META-INF/ktor-client-core.kotlin_module'
pickFirst 'META-INF/DEPENDENCIES'
pickFirst 'META-INF/NOTICE'
pickFirst 'META-INF/LICENSE'
pickFirst 'META-INF/LICENSE.txt'
pickFirst 'META-INF/NOTICE.txt'
}
lintOptions {
abortOnError false
}
testImplementation "org.apache.commons:commons-csv:1.8"
}

View File

@ -1,5 +1,8 @@
package net.dankito.banking.fints
import com.soywiz.klock.DateTime
import com.soywiz.klock.DateTimeSpan
import com.soywiz.klock.DateTimeTz
import net.dankito.banking.fints.callback.FinTsClientCallback
import net.dankito.banking.fints.messages.MessageBuilder
import net.dankito.banking.fints.messages.MessageBuilderResult
@ -25,43 +28,37 @@ 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.utils.IThreadPool
import net.dankito.utils.ThreadPool
import net.dankito.utils.web.client.IWebClient
import net.dankito.utils.web.client.OkHttpWebClient
import net.dankito.utils.web.client.RequestParameters
import net.dankito.utils.web.client.WebClientResponse
import org.slf4j.LoggerFactory
import java.math.BigDecimal
import java.util.*
import java.util.concurrent.CopyOnWriteArrayList
import net.dankito.banking.fints.util.IThreadPool
import net.dankito.banking.fints.util.PureKotlinBase64Service
import net.dankito.banking.fints.util.log.LoggerFactory
import net.dankito.banking.fints.webclient.IWebClient
import net.dankito.banking.fints.webclient.KtorWebClient
import net.dankito.banking.fints.webclient.WebClientResponse
open class FinTsClient @JvmOverloads constructor(
open class FinTsClient(
protected val callback: FinTsClientCallback,
protected val base64Service: IBase64Service,
protected val webClient: IWebClient = OkHttpWebClient(),
protected val webClient: IWebClient = KtorWebClient(),
protected val base64Service: IBase64Service = PureKotlinBase64Service(),
protected val threadPool: IThreadPool,
protected val messageBuilder: MessageBuilder = MessageBuilder(),
protected val responseParser: ResponseParser = ResponseParser(),
protected val mt940Parser: IAccountTransactionsParser = Mt940AccountTransactionsParser(),
protected val threadPool: IThreadPool = ThreadPool(),
protected val product: ProductData = ProductData("15E53C26816138699C7B6A3E8", "1.0.0") // TODO: get version dynamically
) {
companion object {
const val NinetyDaysAgoMilliseconds = 90 * 24 * 60 * 60 * 1000L
val FindAccountTransactionsStartRegex = Regex("^HIKAZ:\\d:\\d:\\d\\+@\\d+@", RegexOption.MULTILINE)
val FindAccountTransactionsEndRegex = Regex("^-'", RegexOption.MULTILINE)
private val log = LoggerFactory.getLogger(FinTsClient::class.java)
private val log = LoggerFactory.getLogger(FinTsClient::class)
}
open var areWeThatGentleToCloseDialogs: Boolean = true
protected val messageLogField = CopyOnWriteArrayList<MessageLogEntry>()
protected val messageLogField = ArrayList<MessageLogEntry>() // TODO: make thread safe like with CopyOnWriteArrayList
// in either case remove sensitive data after response is parsed as otherwise some information like account holder name and accounts may is not set yet on CustomerData
open val messageLogWithoutSensitiveData: List<MessageLogEntry>
@ -215,7 +212,7 @@ open class FinTsClient @JvmOverloads constructor(
// also check if retrieving account transactions of last 90 days without tan is supported (and thereby may retrieve first account transactions)
val transactionsOfLast90DaysResponses = mutableListOf<GetTransactionsResponse>()
val balances = mutableMapOf<AccountData, BigDecimal>()
val balances = mutableMapOf<AccountData, Money>()
customer.accounts.forEach { account ->
if (account.supportsFeature(AccountFeature.RetrieveAccountTransactions)) {
val response = tryGetTransactionsOfLast90DaysWithoutTan(bank, customer, account, false)
@ -252,10 +249,10 @@ open class FinTsClient @JvmOverloads constructor(
protected open fun tryGetTransactionsOfLast90DaysWithoutTan(bank: BankData, customer: CustomerData, account: AccountData,
hasRetrievedTransactionsWithTanJustBefore: Boolean): GetTransactionsResponse {
val now = Date()
val ninetyDaysAgo = Date(now.time - NinetyDaysAgoMilliseconds - now.timezoneOffset * 60 * 1000) // map to UTC
val now = DateTimeTz.nowLocal()
val ninetyDaysAgo = now.minus(DateTimeSpan(days = 90))
val response = getTransactions(GetTransactionsParameter(account.supportsFeature(AccountFeature.RetrieveBalance), ninetyDaysAgo, abortIfTanIsRequired = true), bank, customer, account)
val response = getTransactions(GetTransactionsParameter(account.supportsFeature(AccountFeature.RetrieveBalance), ninetyDaysAgo.local.date, abortIfTanIsRequired = true), bank, customer, account)
account.triedToRetrieveTransactionsOfLast90DaysWithoutTan = true
@ -290,7 +287,7 @@ open class FinTsClient @JvmOverloads constructor(
}
var balance: BigDecimal? = null
var balance: Money? = null
if (parameter.alsoRetrieveBalance && account.supportsFeature(AccountFeature.RetrieveBalance)) {
val balanceResponse = getBalanceAfterDialogInit(account, dialogContext)
@ -301,7 +298,7 @@ open class FinTsClient @JvmOverloads constructor(
}
balanceResponse.getFirstSegmentById<BalanceSegment>(InstituteSegmentId.Balance)?.let {
balance = it.balance
balance = Money(it.balance, it.currency)
}
}
@ -576,9 +573,7 @@ open class FinTsClient @JvmOverloads constructor(
protected open fun getResponseForMessage(requestBody: String, finTs3ServerAddress: String): WebClientResponse {
val encodedRequestBody = base64Service.encode(requestBody)
return webClient.post(
RequestParameters(finTs3ServerAddress, encodedRequestBody, "application/octet-stream")
)
return webClient.post(finTs3ServerAddress, encodedRequestBody, "application/octet-stream")
}
protected open fun fireAndForgetMessage(message: MessageBuilderResult, dialogContext: DialogContext) {
@ -594,7 +589,7 @@ open class FinTsClient @JvmOverloads constructor(
protected open fun handleResponse(webResponse: WebClientResponse, dialogContext: DialogContext): Response {
val responseBody = webResponse.body
if (webResponse.isSuccessful && responseBody != null) {
if (webResponse.successful && responseBody != null) {
try {
val decodedResponse = decodeBase64Response(responseBody)
@ -603,14 +598,14 @@ open class FinTsClient @JvmOverloads constructor(
return responseParser.parse(decodedResponse)
} catch (e: Exception) {
log.error("Could not decode responseBody:\r\n'$responseBody'", e)
log.error(e) { "Could not decode responseBody:\r\n'$responseBody'" }
return Response(false, exception = e)
}
}
else {
val bank = dialogContext.bank
log.error("Request to $bank (${bank.finTs3ServerAddress}) failed", webResponse.error)
log.error { "Request to $bank (${bank.finTs3ServerAddress}) failed" } // TODO: add webResponse.error
}
return Response(false, exception = webResponse.error)
@ -633,14 +628,12 @@ open class FinTsClient @JvmOverloads constructor(
protected open fun addMessageLog(message: String, type: MessageLogEntryType, dialogContext: DialogContext) {
val timeStamp = Date()
val timeStamp = DateTime.now()
val messagePrefix = "${if (type == MessageLogEntryType.Sent) "Sending" else "Received"} message:\r\n" // currently no need to translate
val prettyPrintMessage = prettyPrintHbciMessage(message)
val prettyPrintMessageWithPrefix = "$messagePrefix$prettyPrintMessage"
if (log.isDebugEnabled) {
log.debug(prettyPrintMessageWithPrefix)
}
log.debug { prettyPrintMessageWithPrefix }
messageLogField.add(MessageLogEntry(prettyPrintMessageWithPrefix, timeStamp, dialogContext.customer))
}

View File

@ -10,26 +10,26 @@ import net.dankito.banking.fints.response.client.GetTransactionsResponse
import net.dankito.banking.fints.transactions.IAccountTransactionsParser
import net.dankito.banking.fints.transactions.Mt940AccountTransactionsParser
import net.dankito.banking.fints.util.IBase64Service
import net.dankito.utils.IThreadPool
import net.dankito.utils.ThreadPool
import net.dankito.utils.web.client.IWebClient
import net.dankito.utils.web.client.OkHttpWebClient
import net.dankito.banking.fints.util.IThreadPool
import net.dankito.banking.fints.util.PureKotlinBase64Service
import net.dankito.banking.fints.webclient.IWebClient
import net.dankito.banking.fints.webclient.KtorWebClient
open class FinTsClientForCustomer @JvmOverloads constructor(
open class FinTsClientForCustomer(
val bank: BankData,
val customer: CustomerData,
webClient: IWebClient = OkHttpWebClient(),
base64Service: IBase64Service,
threadPool: IThreadPool = ThreadPool(),
callback: FinTsClientCallback,
webClient: IWebClient = KtorWebClient(),
base64Service: IBase64Service = PureKotlinBase64Service(),
threadPool: IThreadPool,
messageBuilder: MessageBuilder = MessageBuilder(),
responseParser: ResponseParser = ResponseParser(),
mt940Parser: IAccountTransactionsParser = Mt940AccountTransactionsParser(),
product: ProductData = ProductData("15E53C26816138699C7B6A3E8", "1.0.0") // TODO: get version dynamically
) {
protected val client = FinTsClient(callback, base64Service, webClient, messageBuilder, responseParser, mt940Parser, threadPool, product)
protected val client = FinTsClient(callback, webClient, base64Service, threadPool, messageBuilder, responseParser, mt940Parser, product)
open val messageLogWithoutSensitiveData: List<MessageLogEntry>

View File

@ -1,5 +1,7 @@
package net.dankito.banking.fints.messages
import io.ktor.utils.io.charsets.Charsets
/**
* Der HBCI-Basiszeichensatz baut auf dem international normierten Zeichensatz ISO 8859 auf.

View File

@ -1,5 +1,6 @@
package net.dankito.banking.fints.messages
import com.soywiz.klock.DateTime
import net.dankito.banking.fints.messages.datenelemente.implementierte.Aufsetzpunkt
import net.dankito.banking.fints.messages.datenelemente.implementierte.KundensystemID
import net.dankito.banking.fints.messages.datenelemente.implementierte.Synchronisierungsmodus
@ -22,7 +23,7 @@ import net.dankito.banking.fints.response.segments.JobParameters
import net.dankito.banking.fints.response.segments.SepaAccountInfoParameters
import net.dankito.banking.fints.response.segments.TanResponse
import net.dankito.banking.fints.util.FinTsUtils
import net.dankito.utils.extensions.containsAny
import kotlin.math.absoluteValue
import kotlin.random.Random
@ -356,7 +357,7 @@ open class MessageBuilder(protected val generator: ISegmentNumberGenerator = Seg
}
protected open fun createControlReference(): String {
return Math.abs(Random(System.nanoTime()).nextInt()).toString()
return Random(DateTime.nowUnixLong()).nextInt().absoluteValue.toString()
}
@ -442,4 +443,16 @@ open class MessageBuilder(protected val generator: ISegmentNumberGenerator = Seg
}
}
// TODO: move to a library
fun <T> Collection<T>.containsAny(otherCollection: Collection<T>): Boolean {
for (otherItem in otherCollection) {
if (this.contains(otherItem)) {
return true
}
}
return false
}
}

View File

@ -1,9 +1,10 @@
package net.dankito.banking.fints.messages.datenelemente.abgeleiteteformate
import com.soywiz.klock.Date
import com.soywiz.klock.DateFormat
import com.soywiz.klock.parse
import net.dankito.banking.fints.messages.Existenzstatus
import net.dankito.banking.fints.messages.datenelemente.basisformate.NumerischesDatenelement
import java.text.SimpleDateFormat
import java.util.*
/**
@ -16,11 +17,20 @@ open class Datum(date: Int?, existenzstatus: Existenzstatus) : NumerischesDatene
companion object {
const val HbciDateFormatString = "yyyyMMdd"
val HbciDateFormat = SimpleDateFormat(HbciDateFormatString)
val HbciDateFormat = DateFormat(HbciDateFormatString)
fun format(date: Date): String {
return HbciDateFormat.format(date.dateTimeDayStart.localUnadjusted) // TODO: is this correct?
}
fun parse(dateString: String): Date {
return HbciDateFormat.parse(dateString).utc.date // TODO: really use UTC?
}
}
constructor(date: Date?, existenzstatus: Existenzstatus)
: this(date?.let { HbciDateFormat.format(it).toInt() }, existenzstatus)
: this(date?.let { format(it).toInt() }, existenzstatus)
}

View File

@ -1,9 +1,8 @@
package net.dankito.banking.fints.messages.datenelemente.abgeleiteteformate
import com.soywiz.klock.*
import net.dankito.banking.fints.messages.Existenzstatus
import net.dankito.banking.fints.messages.datenelemente.basisformate.ZiffernDatenelement
import java.text.SimpleDateFormat
import java.util.*
/**
@ -17,11 +16,20 @@ open class Uhrzeit(time: Int?, existenzstatus: Existenzstatus) : ZiffernDatenele
companion object {
const val HbciTimeFormatString = "HHmmss"
val HbciTimeFormat = SimpleDateFormat(HbciTimeFormatString)
val HbciTimeFormat = DateFormat(HbciTimeFormatString)
fun format(time: Time): String {
return HbciTimeFormat.format(DateTimeTz.Companion.fromUnixLocal(time.encoded.milliseconds)) // TODO: is this correct?
}
fun parse(dateString: String): Time {
return HbciTimeFormat.parse(dateString).utc.time // TODO: is this correct?
}
}
constructor(time: Date?, existenzstatus: Existenzstatus)
: this(time?.let { HbciTimeFormat.format(it).toInt() }, existenzstatus)
constructor(time: Time?, existenzstatus: Existenzstatus)
: this(time?.let { format(time).toInt() }, existenzstatus)
}

View File

@ -1,5 +1,7 @@
package net.dankito.banking.fints.messages.datenelemente.basisformate
import io.ktor.utils.io.charsets.encode
import io.ktor.utils.io.charsets.name
import net.dankito.banking.fints.messages.Existenzstatus
import net.dankito.banking.fints.messages.HbciCharset
import net.dankito.banking.fints.messages.Separators
@ -35,8 +37,10 @@ abstract class TextDatenelement(var value: String?, existenzstatus: Existenzstat
checkIfMandatoryValueIsSet()
try {
if (HbciCharset.DefaultCharset.newEncoder().canEncode(value) == false) {
throwInvalidCharacterException()
value?.let { // at this time value is != null otherwise checkIfMandatoryValueIsSet() would fail
if (HbciCharset.DefaultCharset.newEncoder().encode(it).canRead() == false) {
throwInvalidCharacterException()
}
}
} catch (e: Exception) {
throwInvalidCharacterException()
@ -46,14 +50,14 @@ abstract class TextDatenelement(var value: String?, existenzstatus: Existenzstat
protected open fun checkIfMandatoryValueIsSet() {
if (existenzstatus == Existenzstatus.Mandatory && value == null) {
throwValidationException("Wert ist auf dem Pflichtfeld ${javaClass.simpleName} not set")
throwValidationException("Wert ist auf dem Pflichtfeld ${this::class.simpleName} not set")
}
}
protected open fun throwInvalidCharacterException() {
throwValidationException(
"Wert '$value' enthält Zeichen die gemäß des Zeichensatzes " +
"${HbciCharset.DefaultCharset.displayName()} nicht erlaubt sind."
"${HbciCharset.DefaultCharset.name} nicht erlaubt sind."
)
}

View File

@ -3,14 +3,15 @@ package net.dankito.banking.fints.messages.datenelemente.implementierte.sepa
import net.dankito.banking.fints.messages.Existenzstatus
import net.dankito.banking.fints.messages.datenelemente.basisformate.BinaerDatenelement
import net.dankito.banking.fints.messages.segmente.implementierte.sepa.ISepaMessageCreator
import net.dankito.banking.fints.messages.segmente.implementierte.sepa.PaymentInformationMessages
open class SepaMessage(
filename: String,
messageTemplate: PaymentInformationMessages,
replacementStrings: Map<String, String>,
messageCreator: ISepaMessageCreator
)
: BinaerDatenelement(messageCreator.createXmlFile(filename, replacementStrings), Existenzstatus.Mandatory) {
: BinaerDatenelement(messageCreator.createXmlFile(messageTemplate, replacementStrings), Existenzstatus.Mandatory) {
}

Some files were not shown because too many files have changed in this diff Show More