Fixed JSON serialization (as cannot add Jackson annotations to model classes in common project)

This commit is contained in:
dankito 2020-08-06 01:30:15 +02:00
parent 60e4a82fe0
commit a3696a4716
10 changed files with 531 additions and 12 deletions

View File

@ -15,4 +15,12 @@ compileTestKotlin {
dependencies {
implementation project(':BankingUiCommon')
testImplementation "junit:junit:$junitVersion"
testImplementation "org.assertj:assertj-core:$assertJVersion"
testImplementation "org.mockito:mockito-core:$mockitoVersion"
testImplementation "org.slf4j:slf4j-simple:$slf4jVersion"
}

View File

@ -1,10 +1,13 @@
package net.dankito.banking.persistence
import net.dankito.banking.persistence.mapper.EntitiesMapper
import net.dankito.banking.persistence.model.CustomerEntity
import net.dankito.utils.multiplatform.File
import net.dankito.banking.ui.model.Customer
import net.dankito.banking.ui.model.AccountTransaction
import net.dankito.banking.ui.model.BankAccount
import net.dankito.banking.util.ISerializer
import net.dankito.utils.multiplatform.Month
import java.io.FileOutputStream
import java.net.URL
@ -14,6 +17,8 @@ open class BankingPersistenceJson(
protected val serializer: ISerializer
) : IBankingPersistence {
protected val mapper = EntitiesMapper()
init {
jsonFile.absoluteFile.parentFile.mkdirs()
@ -21,20 +26,30 @@ open class BankingPersistenceJson(
override fun saveOrUpdateAccount(customer: Customer, allCustomers: List<Customer>) {
serializer.serializeObject(allCustomers, jsonFile)
saveAllCustomers(allCustomers)
}
override fun deleteAccount(customer: Customer, allCustomers: List<Customer>) {
serializer.serializeObject(allCustomers, jsonFile)
saveAllCustomers(allCustomers)
}
override fun readPersistedAccounts(): List<Customer> {
return serializer.deserializeListOr(jsonFile, Customer::class, listOf())
val deserializedCustomers = serializer.deserializeListOr(jsonFile, CustomerEntity::class)
return mapper.mapCustomerEntities(deserializedCustomers)
}
override fun saveOrUpdateAccountTransactions(bankAccount: BankAccount, transactions: List<AccountTransaction>) {
// done when called saveOrUpdateAccount()
// TODO: or also call saveAllCustomers()?
}
protected open fun saveAllCustomers(allCustomers: List<Customer>) {
val mappedCustomers = mapper.mapCustomers(allCustomers)
serializer.serializeObject(mappedCustomers, jsonFile)
}

View File

@ -0,0 +1,132 @@
package net.dankito.banking.persistence.mapper
import net.dankito.banking.persistence.model.AccountTransactionEntity
import net.dankito.banking.persistence.model.BankAccountEntity
import net.dankito.banking.persistence.model.CustomerEntity
import net.dankito.banking.ui.model.AccountTransaction
import net.dankito.banking.ui.model.BankAccount
import net.dankito.banking.ui.model.Customer
open class EntitiesMapper {
open fun mapCustomers(customers: List<Customer>): List<CustomerEntity> {
return customers.map { mapCustomer(it) }
}
open fun mapCustomer(customer: Customer): CustomerEntity {
val mappedCustomer = CustomerEntity(
customer.bankCode, customer.customerId, customer.password, customer.finTsServerAddress,
customer.bankName, customer.bic, customer.customerName, customer.userId, customer.iconUrl,
listOf(), customer.supportedTanProcedures, customer.selectedTanProcedure, customer.tanMedia
)
mappedCustomer.id = customer.technicalId
mappedCustomer.accounts = mapBankAccounts(customer.accounts, mappedCustomer)
return mappedCustomer
}
open fun mapCustomerEntities(customers: List<CustomerEntity>): List<Customer> {
return customers.map { mapCustomer(it) }
}
open fun mapCustomer(customer: CustomerEntity): Customer {
val mappedCustomer = Customer(
customer.bankCode, customer.customerId, customer.password, customer.finTsServerAddress,
customer.bankName, customer.bic, customer.customerName, customer.userId, customer.iconUrl
)
mappedCustomer.technicalId = customer.id
mappedCustomer.accounts = mapBankAccounts(customer.accounts, mappedCustomer)
mappedCustomer.supportedTanProcedures = customer.supportedTanProcedures
mappedCustomer.selectedTanProcedure = customer.selectedTanProcedure
mappedCustomer.tanMedia = customer.tanMedia
return mappedCustomer
}
open fun mapBankAccounts(transactions: List<BankAccount>, customer: CustomerEntity): List<BankAccountEntity> {
return transactions.map { mapBankAccount(it, customer) }
}
open fun mapBankAccount(account: BankAccount, customer: CustomerEntity): BankAccountEntity {
val mappedAccount = BankAccountEntity(
customer, account.identifier, account.accountHolderName, account.iban, account.subAccountNumber,
account.customerId, account.balance, account.currency, account.type, account.productName,
account.accountLimit, account.lastRetrievedTransactionsTimestamp,
account.supportsRetrievingAccountTransactions, account.supportsRetrievingBalance,
account.supportsTransferringMoney, account.supportsInstantPaymentMoneyTransfer
)
mappedAccount.id = account.technicalId
mappedAccount.bookedTransactions = mapTransactions(account.bookedTransactions, mappedAccount)
return mappedAccount
}
open fun mapBankAccounts(transactions: List<BankAccountEntity>, customer: Customer): List<BankAccount> {
return transactions.map { mapBankAccount(it, customer) }
}
open fun mapBankAccount(account: BankAccountEntity, customer: Customer): BankAccount {
val mappedAccount = BankAccount(
customer, account.identifier, account.accountHolderName, account.iban, account.subAccountNumber,
account.customerId, account.balance, account.currency, account.type, account.productName,
account.accountLimit, account.lastRetrievedTransactionsTimestamp,
account.supportsRetrievingAccountTransactions, account.supportsRetrievingBalance,
account.supportsTransferringMoney, account.supportsInstantPaymentMoneyTransfer
)
mappedAccount.technicalId = account.id
mappedAccount.bookedTransactions = mapTransactions(account.bookedTransactions, mappedAccount)
return mappedAccount
}
open fun mapTransactions(transactions: List<AccountTransaction>, account: BankAccountEntity): List<AccountTransactionEntity> {
return transactions.map { mapTransaction(it, account) }
}
open fun mapTransaction(transaction: AccountTransaction, account: BankAccountEntity): AccountTransactionEntity {
return AccountTransactionEntity(account, transaction.amount, transaction.currency, transaction.unparsedUsage, transaction.bookingDate,
transaction.otherPartyName, transaction.otherPartyBankCode, transaction.otherPartyAccountId, transaction.bookingText,
transaction.valueDate, transaction.statementNumber, transaction.sequenceNumber, transaction.openingBalance, transaction.closingBalance,
transaction.endToEndReference, transaction.customerReference, transaction.mandateReference, transaction.creditorIdentifier, transaction.originatorsIdentificationCode,
transaction.compensationAmount, transaction.originalAmount, transaction.sepaUsage, transaction.deviantOriginator, transaction.deviantRecipient,
transaction.usageWithNoSpecialType, transaction.primaNotaNumber, transaction.textKeySupplement, transaction.currencyType, transaction.bookingKey,
transaction.referenceForTheAccountOwner, transaction.referenceOfTheAccountServicingInstitution, transaction.supplementaryDetails,
transaction.transactionReferenceNumber, transaction.relatedReferenceNumber, transaction.technicalId)
}
open fun mapTransactions(transactions: List<AccountTransactionEntity>, account: BankAccount): List<AccountTransaction> {
return transactions.map { mapTransaction(it, account) }
}
open fun mapTransaction(transaction: AccountTransactionEntity, account: BankAccount): AccountTransaction {
val mappedTransaction = AccountTransaction(account, transaction.amount, transaction.currency, transaction.unparsedUsage, transaction.bookingDate,
transaction.otherPartyName, transaction.otherPartyBankCode, transaction.otherPartyAccountId, transaction.bookingText,
transaction.valueDate, transaction.statementNumber, transaction.sequenceNumber, transaction.openingBalance, transaction.closingBalance,
transaction.endToEndReference, transaction.customerReference, transaction.mandateReference, transaction.creditorIdentifier, transaction.originatorsIdentificationCode,
transaction.compensationAmount, transaction.originalAmount, transaction.sepaUsage, transaction.deviantOriginator, transaction.deviantRecipient,
transaction.usageWithNoSpecialType, transaction.primaNotaNumber, transaction.textKeySupplement, transaction.currencyType, transaction.bookingKey,
transaction.referenceForTheAccountOwner, transaction.referenceOfTheAccountServicingInstitution, transaction.supplementaryDetails,
transaction.transactionReferenceNumber, transaction.relatedReferenceNumber)
mappedTransaction.technicalId = transaction.id
return mappedTransaction
}
}

View File

@ -0,0 +1,58 @@
package net.dankito.banking.persistence.model
import com.fasterxml.jackson.annotation.JsonIdentityInfo
import com.fasterxml.jackson.annotation.ObjectIdGenerators
import net.dankito.utils.multiplatform.BigDecimal
import net.dankito.utils.multiplatform.Date
import net.dankito.utils.multiplatform.UUID
@JsonIdentityInfo(property = "id", generator = ObjectIdGenerators.PropertyGenerator::class) // to avoid stack overflow due to circular references
// had to define all properties as 'var' 'cause MapStruct cannot handle vals
open class AccountTransactionEntity(
open var bankAccount: BankAccountEntity,
open var amount: BigDecimal,
open var currency: String,
open var unparsedUsage: String,
open var bookingDate: Date,
open var otherPartyName: String?,
open var otherPartyBankCode: String?,
open var otherPartyAccountId: String?,
open var bookingText: String?,
open var valueDate: Date,
open var statementNumber: Int,
open var sequenceNumber: Int?,
open var openingBalance: BigDecimal?,
open var closingBalance: BigDecimal?,
open var endToEndReference: String?,
open var customerReference: String?,
open var mandateReference: String?,
open var creditorIdentifier: String?,
open var originatorsIdentificationCode: String?,
open var compensationAmount: String?,
open var originalAmount: String?,
open var sepaUsage: String?,
open var deviantOriginator: String?,
open var deviantRecipient: String?,
open var usageWithNoSpecialType: String?,
open var primaNotaNumber: String?,
open var textKeySupplement: String?,
open var currencyType: String?,
open var bookingKey: String,
open var referenceForTheAccountOwner: String,
open var referenceOfTheAccountServicingInstitution: String?,
open var supplementaryDetails: String?,
open var transactionReferenceNumber: String,
open var relatedReferenceNumber: String?,
var id: String = UUID.random().toString()
) {
// for object deserializers
internal constructor() : this(BankAccountEntity(), BigDecimal.Zero, "", "", Date(), null, null, null, null, Date(),
-1, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, "", "", null,
null, "", null)
}

View File

@ -0,0 +1,38 @@
package net.dankito.banking.persistence.model
import com.fasterxml.jackson.annotation.JsonIdentityInfo
import com.fasterxml.jackson.annotation.ObjectIdGenerators
import net.dankito.banking.ui.model.BankAccountType
import net.dankito.utils.multiplatform.BigDecimal
import net.dankito.utils.multiplatform.Date
import net.dankito.utils.multiplatform.UUID
@JsonIdentityInfo(property = "id", generator = ObjectIdGenerators.PropertyGenerator::class) // to avoid stack overflow due to circular references
// had to define all properties as 'var' 'cause MapStruct cannot handle vals (and cannot use Pozo's mapstruct-kotlin as SerializableBankAccountBuilder would fail with @Context)
open class BankAccountEntity(
open var customer: CustomerEntity,
open var identifier: String,
open var accountHolderName: String,
open var iban: String?,
open var subAccountNumber: String?,
open var customerId: String,
open var balance: BigDecimal = BigDecimal.Zero,
open var currency: String = "EUR",
open var type: BankAccountType = BankAccountType.Girokonto,
open var productName: String? = null,
open var accountLimit: String? = null,
open var lastRetrievedTransactionsTimestamp: Date? = null,
open var supportsRetrievingAccountTransactions: Boolean = false,
open var supportsRetrievingBalance: Boolean = false,
open var supportsTransferringMoney: Boolean = false,
open var supportsInstantPaymentMoneyTransfer: Boolean = false,
open var bookedTransactions: List<AccountTransactionEntity> = listOf(),
open var unbookedTransactions: List<Any> = listOf(),
open var id: String = UUID.random().toString()
) {
internal constructor() : this(CustomerEntity(), "", "", null, null, "") // for object deserializers
}

View File

@ -0,0 +1,30 @@
package net.dankito.banking.persistence.model
import com.fasterxml.jackson.annotation.*
import net.dankito.banking.ui.model.tan.TanMedium
import net.dankito.banking.ui.model.tan.TanProcedure
import java.util.*
@JsonIdentityInfo(property = "id", generator = ObjectIdGenerators.PropertyGenerator::class) // to avoid stack overflow due to circular references
// had to define all properties as 'var' 'cause MapStruct cannot handle vals (and cannot use Pozo's mapstruct-kotlin as SerializableCustomerBuilder would fail with @Context)
open class CustomerEntity(
var bankCode: String,
var customerId: String,
var password: String,
var finTsServerAddress: String,
var bankName: String,
var bic: String,
var customerName: String,
var userId: String = customerId,
var iconUrl: String? = null,
var accounts: List<BankAccountEntity> = listOf(),
var supportedTanProcedures: List<TanProcedure> = listOf(),
var selectedTanProcedure: TanProcedure? = null,
var tanMedia: List<TanMedium> = listOf(),
var id: String = UUID.randomUUID().toString()
) {
internal constructor() : this("", "", "", "", "", "", "") // for object deserializers
}

View File

@ -0,0 +1,247 @@
package net.dankito.banking.persistence
import net.dankito.banking.persistence.mapper.CustomerConverter
import net.dankito.banking.persistence.mapper.CycleAvoidingMappingContext
import net.dankito.banking.persistence.model.AccountTransactionEntity
import net.dankito.banking.persistence.model.BankAccountEntity
import net.dankito.banking.persistence.model.CustomerEntity
import net.dankito.banking.ui.model.AccountTransaction
import net.dankito.banking.ui.model.BankAccount
import net.dankito.banking.ui.model.Customer
import net.dankito.banking.util.JacksonJsonSerializer
import net.dankito.utils.multiplatform.BigDecimal
import net.dankito.utils.multiplatform.Date
import net.dankito.utils.multiplatform.File
import org.assertj.core.api.Assertions.assertThat
import org.junit.Assert
import org.junit.Test
import org.mapstruct.factory.Mappers
import kotlin.random.Random
class BankingPersistenceJsonTest {
companion object {
const val BankCode = "12345678"
const val CustomerId = "0987654321"
const val Password = "12345"
const val FinTsServerAddress = "http://i-do-not-exist.fail/givemeyourmoney"
const val BankName = "Abzock GmbH"
const val Bic = "ABCDDEBB123"
const val CustomerName = "Hans Dampf"
const val UserId = CustomerId
const val IconUrl = "http://i-do-not-exist.fail/favicon.ico"
val NowMillis = System.currentTimeMillis()
val TwoYearsAgoMillis = NowMillis - (2 * 365 * 24 * 60 * 60 * 1000L)
val TestDataFolder = File("testData")
init {
TestDataFolder.mkdirs()
}
}
private val file = File(TestDataFolder, "test_accounts.json")
private val serializer = JacksonJsonSerializer()
private val underTest = BankingPersistenceJson(file, serializer)
@Test
fun saveOrUpdateAccount() {
// given
val customers = listOf(
createCustomer(2),
createCustomer(3)
)
// when
underTest.saveOrUpdateAccount(customers.first(), customers)
// then
val result = serializer.deserializeListOr(file, CustomerEntity::class)
assertCustomersEqual(result, customers)
}
@Test
fun saveOrUpdateAccountWithBankAccountsAndTransactions() {
// given
val customer = createCustomer(2)
// when
underTest.saveOrUpdateAccount(customer, listOf(customer))
// then
val result = serializer.deserializeListOr(file, CustomerEntity::class)
assertCustomersEqual(result, listOf(customer))
}
@Test
fun readPersistedAccounts() {
// given
val customers = listOf(
createCustomer(2),
createCustomer(3)
)
val serializableCustomers = Mappers.getMapper(CustomerConverter::class.java).mapToEntities(customers, CycleAvoidingMappingContext())
serializer.serializeObject(serializableCustomers, file)
// when
val result = underTest.readPersistedAccounts()
// then
assertCustomersEqual(serializableCustomers, result)
}
private fun createCustomer(countBankAccounts: Int = 0, customerId: String = CustomerId): Customer {
val result = Customer(BankCode, customerId, Password, FinTsServerAddress, BankName, Bic, CustomerName, UserId, IconUrl)
result.accounts = createBankAccounts(countBankAccounts, result)
return result
}
private fun createBankAccounts(count: Int, customer: Customer): List<BankAccount> {
val random = Random(System.nanoTime())
return IntRange(1, count).map { accountIndex ->
createBankAccount("Account_$accountIndex", customer, random.nextInt(2, 50))
}
}
private fun createBankAccount(productName: String, customer: Customer, countTransactions: Int = 0): BankAccount {
val result = BankAccount(customer, customer.customerId, "AccountHolder", "DE00" + customer.bankCode + customer.customerId, null,
customer.customerId, BigDecimal(84.25), productName = productName)
result.bookedTransactions = createAccountTransactions(countTransactions, result)
return result
}
private fun createAccountTransactions(countTransactions: Int, account: BankAccount): List<AccountTransaction> {
return IntRange(1, countTransactions).map { transactionIndex ->
createAccountTransaction(transactionIndex, account)
}
}
private fun createAccountTransaction(transactionIndex: Int, account: BankAccount): AccountTransaction {
return AccountTransaction(account, "OtherParty_$transactionIndex", "Usage_$transactionIndex", BigDecimal(transactionIndex.toDouble()), createDate(), null)
}
private fun createDate(): Date {
return Date(Random(System.nanoTime()).nextLong(TwoYearsAgoMillis, NowMillis))
}
private fun assertCustomersEqual(deserializedCustomers: List<CustomerEntity>, customers: List<Customer>) {
assertThat(deserializedCustomers.size).isEqualTo(customers.size)
deserializedCustomers.forEach { deserializedCustomer ->
val customer = customers.firstOrNull { it.technicalId == deserializedCustomer.id }
if (customer == null) {
Assert.fail("Could not find matching customer for deserialized customer $deserializedCustomer. customers = $customers")
}
else {
assertCustomersEqual(deserializedCustomer, customer)
}
}
}
private fun assertCustomersEqual(deserializedCustomer: CustomerEntity, customer: Customer) {
assertThat(deserializedCustomer.bankCode).isEqualTo(customer.bankCode)
assertThat(deserializedCustomer.customerId).isEqualTo(customer.customerId)
assertThat(deserializedCustomer.password).isEqualTo(customer.password)
assertThat(deserializedCustomer.finTsServerAddress).isEqualTo(customer.finTsServerAddress)
assertThat(deserializedCustomer.bankName).isEqualTo(customer.bankName)
assertThat(deserializedCustomer.bic).isEqualTo(customer.bic)
assertThat(deserializedCustomer.customerName).isEqualTo(customer.customerName)
assertThat(deserializedCustomer.userId).isEqualTo(customer.userId)
assertThat(deserializedCustomer.iconUrl).isEqualTo(customer.iconUrl)
assertBankAccountsEqual(deserializedCustomer.accounts, customer.accounts)
}
private fun assertBankAccountsEqual(deserializedAccounts: List<BankAccountEntity>, accounts: List<BankAccount>) {
assertThat(deserializedAccounts.size).isEqualTo(accounts.size)
deserializedAccounts.forEach { deserializedAccount ->
val account = accounts.firstOrNull { it.technicalId == deserializedAccount.id }
if (account == null) {
Assert.fail("Could not find matching account for deserialized account $deserializedAccount. accounts = $accounts")
}
else {
assertBankAccountsEqual(deserializedAccount, account)
}
}
}
private fun assertBankAccountsEqual(deserializedAccount: BankAccountEntity, account: BankAccount) {
// to check if MapStruct created reference correctly
assertThat(deserializedAccount.customer.id).isEqualTo(account.customer.technicalId)
assertThat(deserializedAccount.identifier).isEqualTo(account.identifier)
assertThat(deserializedAccount.iban).isEqualTo(account.iban)
assertThat(deserializedAccount.customerId).isEqualTo(account.customerId)
assertThat(deserializedAccount.balance).isEqualTo(account.balance)
assertThat(deserializedAccount.productName).isEqualTo(account.productName)
assertAccountTransactionsEqual(deserializedAccount.bookedTransactions, account.bookedTransactions)
}
private fun assertAccountTransactionsEqual(deserializedTransactions: List<AccountTransactionEntity>, transactions: List<AccountTransaction>) {
assertThat(deserializedTransactions.size).isEqualTo(transactions.size)
deserializedTransactions.forEach { deserializedTransaction ->
val transaction = transactions.firstOrNull { it.technicalId == deserializedTransaction.id }
if (transaction == null) {
Assert.fail("Could not find matching transaction for deserialized transaction $deserializedTransaction. transactions = $transactions")
}
else {
assertAccountTransactionsEqual(deserializedTransaction, transaction)
}
}
}
private fun assertAccountTransactionsEqual(deserializedTransaction: AccountTransactionEntity, transaction: AccountTransaction) {
// to check if MapStruct created reference correctly
assertThat(deserializedTransaction.bankAccount.id).isEqualTo(transaction.bankAccount.technicalId)
assertThat(deserializedTransaction.otherPartyName).isEqualTo(transaction.otherPartyName)
assertThat(deserializedTransaction.unparsedUsage).isEqualTo(transaction.unparsedUsage)
assertThat(deserializedTransaction.amount).isEqualTo(transaction.amount)
assertThat(deserializedTransaction.valueDate).isEqualTo(transaction.valueDate)
}
}

View File

@ -1,14 +1,11 @@
package net.dankito.banking.ui.model
//import com.fasterxml.jackson.annotation.JsonIdentityInfo
//import com.fasterxml.jackson.annotation.ObjectIdGenerators
import net.dankito.utils.multiplatform.BigDecimal
import net.dankito.utils.multiplatform.Date
import net.dankito.utils.multiplatform.DateFormatStyle
import net.dankito.utils.multiplatform.DateFormatter
//@JsonIdentityInfo(property = "technicalId", generator = ObjectIdGenerators.PropertyGenerator::class) // to avoid stack overflow due to circular references
open class AccountTransaction(
open val bankAccount: BankAccount,
open val amount: BigDecimal,

View File

@ -1,14 +1,11 @@
package net.dankito.banking.ui.model
//import com.fasterxml.jackson.annotation.JsonIdentityInfo
//import com.fasterxml.jackson.annotation.ObjectIdGenerators
import net.dankito.utils.multiplatform.BigDecimal
import net.dankito.utils.multiplatform.Date
import net.dankito.utils.multiplatform.UUID
import kotlin.jvm.JvmOverloads
//@JsonIdentityInfo(property = "technicalId", generator = ObjectIdGenerators.PropertyGenerator::class) // to avoid stack overflow due to circular references
open class BankAccount @JvmOverloads constructor(
open val customer: Customer,
open val identifier: String,

View File

@ -1,7 +1,5 @@
package net.dankito.banking.ui.model
//import com.fasterxml.jackson.annotation.JsonIdentityInfo
//import com.fasterxml.jackson.annotation.ObjectIdGenerators
import net.dankito.utils.multiplatform.BigDecimal
import net.dankito.utils.multiplatform.sum
import net.dankito.banking.ui.model.tan.TanMedium
@ -10,7 +8,6 @@ import net.dankito.banking.ui.model.tan.TanProcedure
import net.dankito.utils.multiplatform.UUID
//@JsonIdentityInfo(property = "technicalId", generator = ObjectIdGenerators.PropertyGenerator::class) // to avoid stack overflow due to circular references
open class Customer(
open var bankCode: String,
open var customerId: String,