From 16d665634367004d131361ed9812ad981be836ca Mon Sep 17 00:00:00 2001 From: dankito Date: Sat, 25 Apr 2020 02:45:37 +0200 Subject: [PATCH] Implemented displaying remittees from all account transactions so that user can choose between them and get bank transfer done faster --- .../persistence/IBankingPersistence.kt | 5 + .../banking/search/IRemitteeSearcher.kt | 8 + .../net/dankito/banking/search/Remittee.kt | 18 ++ .../net/dankito/banking/ui/model/Account.kt | 7 +- .../banking/ui/model/AccountTransaction.kt | 6 +- .../dankito/banking/ui/model/BankAccount.kt | 8 +- .../banking/ui/presenter/BankingPresenter.kt | 11 +- fints4javaAndroidApp/build.gradle | 1 + .../fints4java/android/di/BankingModule.kt | 14 +- .../android/ui/adapter/RemitteeListAdapter.kt | 28 +++ .../ui/adapter/presenter/RemitteePresenter.kt | 36 +++ .../adapter/viewholder/RemitteeViewHolder.kt | 15 ++ .../android/ui/dialogs/AddAccountDialog.kt | 14 +- .../android/ui/dialogs/TransferMoneyDialog.kt | 36 ++- .../util/StandardAutocompleteCallback.kt | 20 ++ .../main/res/layout/list_item_remittee.xml | 29 +++ .../src/main/res/values/dimens.xml | 4 + .../LuceneBankingPersistence/build.gradle | 28 +++ .../net/dankito/banking/LuceneConfig.kt | 39 +++ .../persistence/LuceneBankingPersistence.kt | 65 +++++ .../banking/search/LuceneRemitteeSearcher.kt | 42 ++++ .../search/LuceneRemitteeSearcherTest.kt | 235 ++++++++++++++++++ .../json/BankingPersistenceJson/build.gradle | 2 - .../persistence/BankingPersistenceJson.kt | 7 + settings.gradle | 4 +- 25 files changed, 660 insertions(+), 22 deletions(-) create mode 100644 BankingUiCommon/src/main/java/net/dankito/banking/search/IRemitteeSearcher.kt create mode 100644 BankingUiCommon/src/main/java/net/dankito/banking/search/Remittee.kt create mode 100644 fints4javaAndroidApp/src/main/java/net/dankito/banking/fints4java/android/ui/adapter/RemitteeListAdapter.kt create mode 100644 fints4javaAndroidApp/src/main/java/net/dankito/banking/fints4java/android/ui/adapter/presenter/RemitteePresenter.kt create mode 100644 fints4javaAndroidApp/src/main/java/net/dankito/banking/fints4java/android/ui/adapter/viewholder/RemitteeViewHolder.kt create mode 100644 fints4javaAndroidApp/src/main/java/net/dankito/banking/fints4java/android/util/StandardAutocompleteCallback.kt create mode 100644 fints4javaAndroidApp/src/main/res/layout/list_item_remittee.xml create mode 100644 persistence/LuceneBankingPersistence/build.gradle create mode 100644 persistence/LuceneBankingPersistence/src/main/kotlin/net/dankito/banking/LuceneConfig.kt create mode 100644 persistence/LuceneBankingPersistence/src/main/kotlin/net/dankito/banking/persistence/LuceneBankingPersistence.kt create mode 100644 persistence/LuceneBankingPersistence/src/main/kotlin/net/dankito/banking/search/LuceneRemitteeSearcher.kt create mode 100644 persistence/LuceneBankingPersistence/src/test/kotlin/net/dankito/banking/search/LuceneRemitteeSearcherTest.kt diff --git a/BankingUiCommon/src/main/java/net/dankito/banking/persistence/IBankingPersistence.kt b/BankingUiCommon/src/main/java/net/dankito/banking/persistence/IBankingPersistence.kt index 826aabf3..95642c3c 100644 --- a/BankingUiCommon/src/main/java/net/dankito/banking/persistence/IBankingPersistence.kt +++ b/BankingUiCommon/src/main/java/net/dankito/banking/persistence/IBankingPersistence.kt @@ -1,6 +1,8 @@ package net.dankito.banking.persistence import net.dankito.banking.ui.model.Account +import net.dankito.banking.ui.model.AccountTransaction +import net.dankito.banking.ui.model.BankAccount interface IBankingPersistence { @@ -11,4 +13,7 @@ interface IBankingPersistence { fun readPersistedAccounts(): List + + fun saveOrUpdateAccountTransactions(bankAccount: BankAccount, transactions: List) + } \ No newline at end of file diff --git a/BankingUiCommon/src/main/java/net/dankito/banking/search/IRemitteeSearcher.kt b/BankingUiCommon/src/main/java/net/dankito/banking/search/IRemitteeSearcher.kt new file mode 100644 index 00000000..0c050bd5 --- /dev/null +++ b/BankingUiCommon/src/main/java/net/dankito/banking/search/IRemitteeSearcher.kt @@ -0,0 +1,8 @@ +package net.dankito.banking.search + + +interface IRemitteeSearcher { + + fun findRemittees(query: String): List + +} \ No newline at end of file diff --git a/BankingUiCommon/src/main/java/net/dankito/banking/search/Remittee.kt b/BankingUiCommon/src/main/java/net/dankito/banking/search/Remittee.kt new file mode 100644 index 00000000..5588421e --- /dev/null +++ b/BankingUiCommon/src/main/java/net/dankito/banking/search/Remittee.kt @@ -0,0 +1,18 @@ +package net.dankito.banking.search + + +data class Remittee( + val name: String, + val iban: String, + val bic: String +) { + + + internal constructor() : this("", "", "") // for object deserializers + + + override fun toString(): String { + return name + } + +} \ No newline at end of file diff --git a/BankingUiCommon/src/main/java/net/dankito/banking/ui/model/Account.kt b/BankingUiCommon/src/main/java/net/dankito/banking/ui/model/Account.kt index 99815622..65dd11a4 100644 --- a/BankingUiCommon/src/main/java/net/dankito/banking/ui/model/Account.kt +++ b/BankingUiCommon/src/main/java/net/dankito/banking/ui/model/Account.kt @@ -6,9 +6,10 @@ import net.dankito.banking.ui.model.tan.TanMedium import net.dankito.banking.ui.model.tan.TanMediumStatus import net.dankito.banking.ui.model.tan.TanProcedure import java.math.BigDecimal +import java.util.* -@JsonIdentityInfo(generator= ObjectIdGenerators.UUIDGenerator::class) // to avoid stack overflow due to circular references // TODO: remove again, add custom domain object +@JsonIdentityInfo(property = "id", generator = ObjectIdGenerators.PropertyGenerator::class) // to avoid stack overflow due to circular references open class Account( val bank: Bank, val customerId: String, @@ -22,6 +23,10 @@ open class Account( internal constructor() : this(Bank(), "", "", "") // for object deserializers + var id: String = UUID.randomUUID().toString() + protected set + + var supportedTanProcedures: List = listOf() var selectedTanProcedure: TanProcedure? = null diff --git a/BankingUiCommon/src/main/java/net/dankito/banking/ui/model/AccountTransaction.kt b/BankingUiCommon/src/main/java/net/dankito/banking/ui/model/AccountTransaction.kt index 8ba1e5fe..d533beb3 100644 --- a/BankingUiCommon/src/main/java/net/dankito/banking/ui/model/AccountTransaction.kt +++ b/BankingUiCommon/src/main/java/net/dankito/banking/ui/model/AccountTransaction.kt @@ -7,7 +7,7 @@ import java.text.DateFormat import java.util.* -@JsonIdentityInfo(generator= ObjectIdGenerators.UUIDGenerator::class) // to avoid stack overflow due to circular references // TODO: remove again, add custom domain object +@JsonIdentityInfo(property = "id", generator = ObjectIdGenerators.PropertyGenerator::class) // to avoid stack overflow due to circular references open class AccountTransaction( val amount: BigDecimal, val bookingDate: Date, @@ -25,6 +25,10 @@ open class AccountTransaction( internal constructor() : this(BigDecimal.ZERO, Date(),"", null, null, null, null, BigDecimal.ZERO, "", BankAccount()) + var id: String = UUID.randomUUID().toString() + protected set + + val showOtherPartyName: Boolean get() = otherPartyName.isNullOrBlank() == false /* && type != "ENTGELTABSCHLUSS" && type != "AUSZAHLUNG" */ // TODO diff --git a/BankingUiCommon/src/main/java/net/dankito/banking/ui/model/BankAccount.kt b/BankingUiCommon/src/main/java/net/dankito/banking/ui/model/BankAccount.kt index 258170af..a6331796 100644 --- a/BankingUiCommon/src/main/java/net/dankito/banking/ui/model/BankAccount.kt +++ b/BankingUiCommon/src/main/java/net/dankito/banking/ui/model/BankAccount.kt @@ -3,9 +3,10 @@ package net.dankito.banking.ui.model import com.fasterxml.jackson.annotation.JsonIdentityInfo import com.fasterxml.jackson.annotation.ObjectIdGenerators import java.math.BigDecimal +import java.util.* -@JsonIdentityInfo(generator= ObjectIdGenerators.UUIDGenerator::class) // to avoid stack overflow due to circular references // TODO: remove again, add custom domain object +@JsonIdentityInfo(property = "id", generator = ObjectIdGenerators.PropertyGenerator::class) // to avoid stack overflow due to circular references open class BankAccount @JvmOverloads constructor( val account: Account, val identifier: String, @@ -21,10 +22,13 @@ open class BankAccount @JvmOverloads constructor( bookedAccountTransactions: List = listOf() ) { - internal constructor() : this(Account(), "", "", null, null) // for object deserializers + var id: String = UUID.randomUUID().toString() + protected set + + val displayName: String get() { var displayName = identifier diff --git a/BankingUiCommon/src/main/java/net/dankito/banking/ui/presenter/BankingPresenter.kt b/BankingUiCommon/src/main/java/net/dankito/banking/ui/presenter/BankingPresenter.kt index 96d2b09f..d3ebeb53 100644 --- a/BankingUiCommon/src/main/java/net/dankito/banking/ui/presenter/BankingPresenter.kt +++ b/BankingUiCommon/src/main/java/net/dankito/banking/ui/presenter/BankingPresenter.kt @@ -242,13 +242,22 @@ open class BankingPresenter( entry.key.balance = entry.value } - persistAccount(bankAccount.account) + persistAccount(bankAccount.account) // only needed because of balance + persistAccountTransactions(response.bookedTransactions, response.unbookedTransactions) } protected open fun persistAccount(account: Account) { persister.saveOrUpdateAccount(account, accounts) } + protected open fun persistAccountTransactions(bookedTransactions: Map>, unbookedTransactions: Map>) { + bookedTransactions.forEach { + persister.saveOrUpdateAccountTransactions(it.key, it.value) + } + + // TODO: someday also persist unbooked transactions + } + open fun transferMoneyAsync(bankAccount: BankAccount, data: TransferMoneyData, callback: (BankingClientResponse) -> Unit) { getClientForAccount(bankAccount.account)?.let { client -> diff --git a/fints4javaAndroidApp/build.gradle b/fints4javaAndroidApp/build.gradle index a9e33265..7e64c37d 100644 --- a/fints4javaAndroidApp/build.gradle +++ b/fints4javaAndroidApp/build.gradle @@ -57,6 +57,7 @@ dependencies { implementation project(':fints4javaBankingClient') implementation project(':BankingPersistenceJson') + implementation project(':LuceneBankingPersistence') implementation "com.github.clans:fab:$clansFloatingActionButtonVersion" diff --git a/fints4javaAndroidApp/src/main/java/net/dankito/banking/fints4java/android/di/BankingModule.kt b/fints4javaAndroidApp/src/main/java/net/dankito/banking/fints4java/android/di/BankingModule.kt index e9babd98..2c25ba1d 100644 --- a/fints4javaAndroidApp/src/main/java/net/dankito/banking/fints4java/android/di/BankingModule.kt +++ b/fints4javaAndroidApp/src/main/java/net/dankito/banking/fints4java/android/di/BankingModule.kt @@ -7,8 +7,10 @@ import dagger.Provides import net.dankito.banking.fints4java.android.RouterAndroid import net.dankito.banking.fints4java.android.util.Base64ServiceAndroid import net.dankito.banking.fints4javaBankingClientCreator -import net.dankito.banking.persistence.BankingPersistenceJson import net.dankito.banking.persistence.IBankingPersistence +import net.dankito.banking.persistence.LuceneBankingPersistence +import net.dankito.banking.search.IRemitteeSearcher +import net.dankito.banking.search.LuceneRemitteeSearcher import net.dankito.banking.ui.IBankingClientCreator import net.dankito.banking.ui.IRouter import net.dankito.banking.ui.presenter.BankingPresenter @@ -92,8 +94,14 @@ class BankingModule(internal val mainActivity: AppCompatActivity) { @Provides @Singleton - fun provideBankingPersistence(@Named(DatabaseFolderKey) databaseFolder: File, serializer: ISerializer) : IBankingPersistence { - return BankingPersistenceJson(File(databaseFolder, "accounts.json"), serializer) + fun provideBankingPersistence(@Named(IndexFolderKey) indexFolder: File, @Named(DatabaseFolderKey) databaseFolder: File, serializer: ISerializer) : IBankingPersistence { + return LuceneBankingPersistence(databaseFolder, indexFolder, serializer) + } + + @Provides + @Singleton + fun provideRemitteeSearcher(@Named(IndexFolderKey) indexFolder: File) : IRemitteeSearcher { + return LuceneRemitteeSearcher(indexFolder) } @Provides diff --git a/fints4javaAndroidApp/src/main/java/net/dankito/banking/fints4java/android/ui/adapter/RemitteeListAdapter.kt b/fints4javaAndroidApp/src/main/java/net/dankito/banking/fints4java/android/ui/adapter/RemitteeListAdapter.kt new file mode 100644 index 00000000..67077386 --- /dev/null +++ b/fints4javaAndroidApp/src/main/java/net/dankito/banking/fints4java/android/ui/adapter/RemitteeListAdapter.kt @@ -0,0 +1,28 @@ +package net.dankito.banking.fints4java.android.ui.adapter + +import android.view.View +import net.dankito.banking.fints4java.android.R +import net.dankito.banking.fints4java.android.ui.adapter.viewholder.RemitteeViewHolder +import net.dankito.banking.search.Remittee +import net.dankito.utils.android.ui.adapter.ListRecyclerAdapter + + +open class RemitteeListAdapter(protected val itemClicked: ((Remittee) -> Unit)? = null) : ListRecyclerAdapter() { + + override fun getListItemLayoutId() = R.layout.list_item_remittee + + override fun createViewHolder(itemView: View): RemitteeViewHolder { + return RemitteeViewHolder(itemView) + } + + override fun bindItemToView(viewHolder: RemitteeViewHolder, item: Remittee) { + viewHolder.txtvwRemitteeName.text = item.name + + viewHolder.txtvwRemitteeBankCode.text = item.iban + + viewHolder.itemView.setOnClickListener { + itemClicked?.invoke(item) + } + } + +} \ No newline at end of file diff --git a/fints4javaAndroidApp/src/main/java/net/dankito/banking/fints4java/android/ui/adapter/presenter/RemitteePresenter.kt b/fints4javaAndroidApp/src/main/java/net/dankito/banking/fints4java/android/ui/adapter/presenter/RemitteePresenter.kt new file mode 100644 index 00000000..a574711d --- /dev/null +++ b/fints4javaAndroidApp/src/main/java/net/dankito/banking/fints4java/android/ui/adapter/presenter/RemitteePresenter.kt @@ -0,0 +1,36 @@ +package net.dankito.banking.fints4java.android.ui.adapter.presenter + +import android.content.Context +import androidx.recyclerview.widget.RecyclerView +import com.otaliastudios.autocomplete.RecyclerViewPresenter +import kotlinx.coroutines.* +import net.dankito.banking.fints4java.android.ui.adapter.RemitteeListAdapter +import net.dankito.banking.search.IRemitteeSearcher +import net.dankito.banking.search.Remittee +import net.dankito.utils.Stopwatch + + +open class RemitteePresenter(protected val remitteeSearcher: IRemitteeSearcher, context: Context) : RecyclerViewPresenter(context) { + + protected val adapter = RemitteeListAdapter { dispatchClick(it) } + + protected var lastSearchRemitteeJob: Job? = null + + + override fun instantiateAdapter(): RecyclerView.Adapter<*> { + return adapter + } + + override fun onQuery(query: CharSequence?) { + lastSearchRemitteeJob?.cancel() + + lastSearchRemitteeJob = GlobalScope.launch(Dispatchers.IO) { + val potentialRemittees = Stopwatch.logDuration("findRemittees()") { remitteeSearcher.findRemittees(query?.toString() ?: "") } + + withContext(Dispatchers.Main) { + adapter.items = potentialRemittees + } + } + } + +} \ No newline at end of file diff --git a/fints4javaAndroidApp/src/main/java/net/dankito/banking/fints4java/android/ui/adapter/viewholder/RemitteeViewHolder.kt b/fints4javaAndroidApp/src/main/java/net/dankito/banking/fints4java/android/ui/adapter/viewholder/RemitteeViewHolder.kt new file mode 100644 index 00000000..3b9af45e --- /dev/null +++ b/fints4javaAndroidApp/src/main/java/net/dankito/banking/fints4java/android/ui/adapter/viewholder/RemitteeViewHolder.kt @@ -0,0 +1,15 @@ +package net.dankito.banking.fints4java.android.ui.adapter.viewholder + +import android.view.View +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import kotlinx.android.synthetic.main.list_item_remittee.view.* + + +open class RemitteeViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + + val txtvwRemitteeName: TextView = itemView.txtvwRemitteeName + + val txtvwRemitteeBankCode: TextView = itemView.txtvwRemitteeBankCode + +} \ No newline at end of file diff --git a/fints4javaAndroidApp/src/main/java/net/dankito/banking/fints4java/android/ui/dialogs/AddAccountDialog.kt b/fints4javaAndroidApp/src/main/java/net/dankito/banking/fints4java/android/ui/dialogs/AddAccountDialog.kt index 83e6296a..1ad53100 100644 --- a/fints4javaAndroidApp/src/main/java/net/dankito/banking/fints4java/android/ui/dialogs/AddAccountDialog.kt +++ b/fints4javaAndroidApp/src/main/java/net/dankito/banking/fints4java/android/ui/dialogs/AddAccountDialog.kt @@ -15,12 +15,12 @@ import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.DialogFragment import com.otaliastudios.autocomplete.Autocomplete -import com.otaliastudios.autocomplete.AutocompleteCallback import kotlinx.android.synthetic.main.dialog_add_account.* import kotlinx.android.synthetic.main.dialog_add_account.view.* import net.dankito.banking.fints4java.android.R import net.dankito.banking.fints4java.android.di.BankingComponent import net.dankito.banking.fints4java.android.ui.adapter.presenter.BankInfoPresenter +import net.dankito.banking.fints4java.android.util.StandardAutocompleteCallback import net.dankito.banking.ui.model.responses.AddAccountResponse import net.dankito.banking.ui.presenter.BankingPresenter import net.dankito.fints.model.BankInfo @@ -76,15 +76,9 @@ open class AddAccountDialog : DialogFragment() { } private fun initBankListAutocompletion(rootView: View) { - val autocompleteCallback = object : AutocompleteCallback { - - override fun onPopupItemClicked(editable: Editable, item: BankInfo): Boolean { - bankSelected(item) - return true - } - - override fun onPopupVisibilityChanged(shown: Boolean) {} - + val autocompleteCallback = StandardAutocompleteCallback { _, item -> + bankSelected(item) + true } Autocomplete.on(rootView.edtxtBankCode) diff --git a/fints4javaAndroidApp/src/main/java/net/dankito/banking/fints4java/android/ui/dialogs/TransferMoneyDialog.kt b/fints4javaAndroidApp/src/main/java/net/dankito/banking/fints4java/android/ui/dialogs/TransferMoneyDialog.kt index b6984ee9..1fe02508 100644 --- a/fints4javaAndroidApp/src/main/java/net/dankito/banking/fints4java/android/ui/dialogs/TransferMoneyDialog.kt +++ b/fints4javaAndroidApp/src/main/java/net/dankito/banking/fints4java/android/ui/dialogs/TransferMoneyDialog.kt @@ -1,21 +1,29 @@ package net.dankito.banking.fints4java.android.ui.dialogs +import android.graphics.Color +import android.graphics.drawable.ColorDrawable import android.os.Bundle import android.text.TextWatcher import android.text.method.DigitsKeyListener import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.EditText import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.DialogFragment +import com.otaliastudios.autocomplete.Autocomplete import kotlinx.android.synthetic.main.dialog_transfer_money.* import kotlinx.android.synthetic.main.dialog_transfer_money.view.* import net.dankito.banking.fints4java.android.R import net.dankito.banking.fints4java.android.di.BankingComponent import net.dankito.banking.fints4java.android.ui.adapter.BankAccountsAdapter +import net.dankito.banking.fints4java.android.ui.adapter.presenter.RemitteePresenter import net.dankito.banking.fints4java.android.ui.listener.ListItemSelectedListener +import net.dankito.banking.fints4java.android.util.StandardAutocompleteCallback import net.dankito.banking.fints4java.android.util.StandardTextWatcher +import net.dankito.banking.search.IRemitteeSearcher +import net.dankito.banking.search.Remittee import net.dankito.banking.ui.model.BankAccount import net.dankito.banking.ui.model.parameters.TransferMoneyData import net.dankito.banking.ui.model.responses.BankingClientResponse @@ -50,6 +58,9 @@ open class TransferMoneyDialog : DialogFragment() { @Inject protected lateinit var presenter: BankingPresenter + @Inject + protected lateinit var remitteeSearcher: IRemitteeSearcher + init { BankingComponent.component.inject(this) @@ -96,7 +107,8 @@ open class TransferMoneyDialog : DialogFragment() { preselectedBankAccount?.let { rootView.spnBankAccounts.setSelection(adapter.getItems().indexOf(it)) } } - // TODO: add autocompletion by searching for name in account entries + initRemitteeAutocompletion(rootView.edtxtRemitteeName) + rootView.edtxtRemitteeName.addTextChangedListener(checkRequiredDataWatcher { checkIfEnteredRemitteeNameIsValid() }) @@ -124,6 +136,21 @@ open class TransferMoneyDialog : DialogFragment() { rootView.btnTransferMoney.setOnClickListener { transferMoney() } } + private fun initRemitteeAutocompletion(edtxtRemitteeName: EditText) { + val autocompleteCallback = StandardAutocompleteCallback { _, item -> + remitteeSelected(item) + true + } + + Autocomplete.on(edtxtRemitteeName) + .with(6f) + .with(ColorDrawable(Color.WHITE)) + .with(autocompleteCallback) + .with(RemitteePresenter(remitteeSearcher, edtxtRemitteeName.context)) + .build() + } + + override fun onStart() { super.onStart() @@ -159,6 +186,13 @@ open class TransferMoneyDialog : DialogFragment() { } } + + protected open fun remitteeSelected(item: Remittee) { + edtxtRemitteeName.setText(item.name) + edtxtRemitteeBic.setText(item.bic) + edtxtRemitteeIban.setText(item.iban) + } + protected open fun transferMoney() { getEnteredAmount()?.let { amount -> // should only come at this stage when a valid amount has been entered val data = TransferMoneyData( diff --git a/fints4javaAndroidApp/src/main/java/net/dankito/banking/fints4java/android/util/StandardAutocompleteCallback.kt b/fints4javaAndroidApp/src/main/java/net/dankito/banking/fints4java/android/util/StandardAutocompleteCallback.kt new file mode 100644 index 00000000..3e6a15f3 --- /dev/null +++ b/fints4javaAndroidApp/src/main/java/net/dankito/banking/fints4java/android/util/StandardAutocompleteCallback.kt @@ -0,0 +1,20 @@ +package net.dankito.banking.fints4java.android.util + +import android.text.Editable +import com.otaliastudios.autocomplete.AutocompleteCallback + + +open class StandardAutocompleteCallback( + protected val onPopupVisibilityChanged: ((shown: Boolean) -> Unit)? = null, + protected val onPopupItemClicked: ((editable: Editable?, item: T) -> Boolean)? = null +) : AutocompleteCallback { + + override fun onPopupItemClicked(editable: Editable?, item: T): Boolean { + return onPopupItemClicked?.invoke(editable, item) ?: false + } + + override fun onPopupVisibilityChanged(shown: Boolean) { + onPopupVisibilityChanged?.invoke(shown) + } + +} \ No newline at end of file diff --git a/fints4javaAndroidApp/src/main/res/layout/list_item_remittee.xml b/fints4javaAndroidApp/src/main/res/layout/list_item_remittee.xml new file mode 100644 index 00000000..bf790571 --- /dev/null +++ b/fints4javaAndroidApp/src/main/res/layout/list_item_remittee.xml @@ -0,0 +1,29 @@ + + + + + + + + \ No newline at end of file diff --git a/fints4javaAndroidApp/src/main/res/values/dimens.xml b/fints4javaAndroidApp/src/main/res/values/dimens.xml index 02d172ae..8a816527 100644 --- a/fints4javaAndroidApp/src/main/res/values/dimens.xml +++ b/fints4javaAndroidApp/src/main/res/values/dimens.xml @@ -50,6 +50,10 @@ 4dp 13sp + 60dp + 6dp + 6dp + 40dp 40dp diff --git a/persistence/LuceneBankingPersistence/build.gradle b/persistence/LuceneBankingPersistence/build.gradle new file mode 100644 index 00000000..e118b0b7 --- /dev/null +++ b/persistence/LuceneBankingPersistence/build.gradle @@ -0,0 +1,28 @@ +apply plugin: 'java-library' +apply plugin: 'kotlin' + + +sourceCompatibility = "1.7" +targetCompatibility = "1.7" + +compileKotlin { + kotlinOptions.jvmTarget = "1.6" +} +compileTestKotlin { + kotlinOptions.jvmTarget = "1.6" +} + + +dependencies { + implementation project(":BankingUiCommon") + + implementation project(":BankingPersistenceJson") + + implementation "net.dankito.search:lucene-4-utils:$luceneUtilsVersion" + + + testImplementation "junit:junit:$junitVersion" + testImplementation "org.assertj:assertj-core:$assertJVersion" + + testImplementation "org.mockito:mockito-core:$mockitoVersion" +} \ No newline at end of file diff --git a/persistence/LuceneBankingPersistence/src/main/kotlin/net/dankito/banking/LuceneConfig.kt b/persistence/LuceneBankingPersistence/src/main/kotlin/net/dankito/banking/LuceneConfig.kt new file mode 100644 index 00000000..921213fc --- /dev/null +++ b/persistence/LuceneBankingPersistence/src/main/kotlin/net/dankito/banking/LuceneConfig.kt @@ -0,0 +1,39 @@ +package net.dankito.banking + +import java.io.File + + +class LuceneConfig { + + companion object { + const val BankAccountIdFieldName = "bank_account_id" + + const val IdFieldName = "id" + + const val OtherPartyNameFieldName = "other_party_name" + + const val OtherPartyBankCodeFieldName = "other_party_bank_code" + + const val OtherPartyAccountIdFieldName = "other_party_account_id" + + const val BookingDateFieldName = "booking_date" + const val BookingDateSortFieldName = "booking_date_sort" + + const val UsageFieldName = "usage" + + const val BookingTextFieldName = "booking_text" + + const val AmountFieldName = "amount" + + const val CurrencyFieldName = "currency" + + const val BalanceFieldName = "balance" + + + fun getAccountTransactionsIndexFolder(indexFolder: File): File { + return File(indexFolder, "account_transactions") + } + + } + +} \ No newline at end of file diff --git a/persistence/LuceneBankingPersistence/src/main/kotlin/net/dankito/banking/persistence/LuceneBankingPersistence.kt b/persistence/LuceneBankingPersistence/src/main/kotlin/net/dankito/banking/persistence/LuceneBankingPersistence.kt new file mode 100644 index 00000000..f514150a --- /dev/null +++ b/persistence/LuceneBankingPersistence/src/main/kotlin/net/dankito/banking/persistence/LuceneBankingPersistence.kt @@ -0,0 +1,65 @@ +package net.dankito.banking.persistence + +import net.dankito.banking.LuceneConfig +import net.dankito.banking.LuceneConfig.Companion.AmountFieldName +import net.dankito.banking.LuceneConfig.Companion.BalanceFieldName +import net.dankito.banking.LuceneConfig.Companion.BankAccountIdFieldName +import net.dankito.banking.LuceneConfig.Companion.BookingDateFieldName +import net.dankito.banking.LuceneConfig.Companion.BookingDateSortFieldName +import net.dankito.banking.LuceneConfig.Companion.BookingTextFieldName +import net.dankito.banking.LuceneConfig.Companion.CurrencyFieldName +import net.dankito.banking.LuceneConfig.Companion.IdFieldName +import net.dankito.banking.LuceneConfig.Companion.OtherPartyAccountIdFieldName +import net.dankito.banking.LuceneConfig.Companion.OtherPartyBankCodeFieldName +import net.dankito.banking.LuceneConfig.Companion.OtherPartyNameFieldName +import net.dankito.banking.LuceneConfig.Companion.UsageFieldName +import net.dankito.banking.ui.model.AccountTransaction +import net.dankito.banking.ui.model.BankAccount +import net.dankito.utils.lucene.index.DocumentsWriter +import net.dankito.utils.lucene.index.FieldBuilder +import net.dankito.utils.serialization.ISerializer +import net.dankito.utils.serialization.JacksonJsonSerializer +import java.io.Closeable +import java.io.File + + +open class LuceneBankingPersistence( + databaseFolder: File, + indexFolder: File, + serializer: ISerializer = JacksonJsonSerializer() +) : BankingPersistenceJson(File(databaseFolder, "accounts.json"), serializer), IBankingPersistence, Closeable { + + + protected val fields = FieldBuilder() + + protected val writer = DocumentsWriter(LuceneConfig.getAccountTransactionsIndexFolder(indexFolder)) + + + override fun close() { + writer.close() + } + + + override fun saveOrUpdateAccountTransactions(bankAccount: BankAccount, transactions: List) { + transactions.forEach { transaction -> + writer.updateDocumentForNonNullFields(IdFieldName, transaction.id, + fields.keywordField(BankAccountIdFieldName, bankAccount.id), + fields.nullableFullTextSearchField(OtherPartyNameFieldName, transaction.otherPartyName, true), + fields.fullTextSearchField(UsageFieldName, transaction.usage, true), + fields.nullableFullTextSearchField(BookingTextFieldName, transaction.bookingText, true), + + fields.nullableStoredField(OtherPartyBankCodeFieldName, transaction.otherPartyBankCode), + fields.nullableStoredField(OtherPartyAccountIdFieldName, transaction.otherPartyAccountId), + fields.storedField(BookingDateFieldName, transaction.bookingDate), + fields.storedField(AmountFieldName, transaction.amount), + fields.storedField(CurrencyFieldName, transaction.currency), + fields.nullableStoredField(BalanceFieldName, transaction.balance), + + fields.sortField(BookingDateSortFieldName, transaction.bookingDate) + ) + } + + writer.flushChangesToDisk() + } + +} \ No newline at end of file diff --git a/persistence/LuceneBankingPersistence/src/main/kotlin/net/dankito/banking/search/LuceneRemitteeSearcher.kt b/persistence/LuceneBankingPersistence/src/main/kotlin/net/dankito/banking/search/LuceneRemitteeSearcher.kt new file mode 100644 index 00000000..b5677d5f --- /dev/null +++ b/persistence/LuceneBankingPersistence/src/main/kotlin/net/dankito/banking/search/LuceneRemitteeSearcher.kt @@ -0,0 +1,42 @@ +package net.dankito.banking.search + +import net.dankito.banking.LuceneConfig +import net.dankito.banking.LuceneConfig.Companion.OtherPartyAccountIdFieldName +import net.dankito.banking.LuceneConfig.Companion.OtherPartyBankCodeFieldName +import net.dankito.banking.LuceneConfig.Companion.OtherPartyNameFieldName +import net.dankito.utils.lucene.mapper.PropertyDescription +import net.dankito.utils.lucene.mapper.PropertyType +import net.dankito.utils.lucene.search.QueryBuilder +import net.dankito.utils.lucene.search.Searcher +import java.io.File + + +open class LuceneRemitteeSearcher(indexFolder: File) : IRemitteeSearcher { + + companion object { + + private val properties = listOf( + PropertyDescription(PropertyType.NullableString, OtherPartyNameFieldName, Remittee::name), + PropertyDescription(PropertyType.NullableString, OtherPartyBankCodeFieldName, Remittee::bic), + PropertyDescription(PropertyType.NullableString, OtherPartyAccountIdFieldName, Remittee::iban) + ) + + } + + + protected val queries = QueryBuilder() + + protected val searcher = Searcher(LuceneConfig.getAccountTransactionsIndexFolder(indexFolder)) + + + override fun findRemittees(query: String): List { + val luceneQuery = queries.createQueriesForSingleTerms(query.toLowerCase()) { singleTerm -> + listOf( + queries.fulltextQuery(OtherPartyNameFieldName, singleTerm) + ) + } + + return searcher.searchAndMap(luceneQuery, Remittee::class.java, properties).toSet().toList() + } + +} \ No newline at end of file diff --git a/persistence/LuceneBankingPersistence/src/test/kotlin/net/dankito/banking/search/LuceneRemitteeSearcherTest.kt b/persistence/LuceneBankingPersistence/src/test/kotlin/net/dankito/banking/search/LuceneRemitteeSearcherTest.kt new file mode 100644 index 00000000..95500c4f --- /dev/null +++ b/persistence/LuceneBankingPersistence/src/test/kotlin/net/dankito/banking/search/LuceneRemitteeSearcherTest.kt @@ -0,0 +1,235 @@ +package net.dankito.banking.search + +import net.dankito.banking.persistence.LuceneBankingPersistence +import net.dankito.banking.ui.model.Account +import net.dankito.banking.ui.model.AccountTransaction +import net.dankito.banking.ui.model.BankAccount +import net.dankito.utils.io.FileUtils +import org.assertj.core.api.Assertions.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.mockito.Mockito.mock +import java.io.File +import java.math.BigDecimal +import java.text.SimpleDateFormat +import java.util.* +import java.util.concurrent.ThreadLocalRandom + + +class LuceneRemitteeSearcherTest { + + companion object { + + private val dataFolder = File("testData") + + private val databaseFolder = File(dataFolder, "db") + + private val indexFolder = File(dataFolder, "index") + + + private val BookingDate = "27.03.2020" + private val OtherPartyName = "Mahatma Gandhi" + private val OtherPartyBankCode = "12345678" + private val OtherPartyAccountId = "0987654321" + private val Amount = BigDecimal.valueOf(123.45) + + + private val bankAccountMock = BankAccount(mock(Account::class.java), "", "", null, null) + + + private val dateFormat = SimpleDateFormat("dd.MM.yyyy") + + } + + + private val fileUtils = FileUtils() + + private val bankingPersistence = LuceneBankingPersistence(databaseFolder, indexFolder) + + private val underTest = LuceneRemitteeSearcher(indexFolder) + + + @Before + fun setUp() { + clearDataFolder() + } + + @After + fun tearDown() { + bankingPersistence.close() + + clearDataFolder() + } + + private fun clearDataFolder() { + fileUtils.deleteFolderRecursively(dataFolder) + } + + + @Test + fun findRemittees_ByFullName() { + + // given + val query = OtherPartyName + + val before = underTest.findRemittees(query) + assertThat(before).isEmpty() + + bankingPersistence.saveOrUpdateAccountTransactions(bankAccountMock, listOf( + createTransaction(bankAccountMock, BookingDate, Amount, OtherPartyName, OtherPartyBankCode, OtherPartyAccountId), + createTransaction(), + createTransaction() + )) + + + // when + val result = underTest.findRemittees(query) + + + // then + assertThat(result).hasSize(1) + assertThat(result.first().name).isEqualTo(OtherPartyName) + assertThat(result.first().bic).isEqualTo(OtherPartyBankCode) + assertThat(result.first().iban).isEqualTo(OtherPartyAccountId) + } + + @Test + fun findRemittees_ByPartialName() { + + // given + val query = "gand" + + val before = underTest.findRemittees(query) + assertThat(before).isEmpty() + + bankingPersistence.saveOrUpdateAccountTransactions(bankAccountMock, listOf( + createTransaction(bankAccountMock, BookingDate, Amount, OtherPartyName, OtherPartyBankCode, OtherPartyAccountId), + createTransaction(), + createTransaction() + )) + + + // when + val result = underTest.findRemittees(query) + + + // then + assertThat(result).hasSize(1) + assertThat(result.first().name).isEqualTo(OtherPartyName) + assertThat(result.first().bic).isEqualTo(OtherPartyBankCode) + assertThat(result.first().iban).isEqualTo(OtherPartyAccountId) + } + + @Test + fun findRemittees_SimilarNames() { + + // given + val query = "gand" + val secondOtherPartyName = "Gandalf" + + val before = underTest.findRemittees(query) + assertThat(before).isEmpty() + + bankingPersistence.saveOrUpdateAccountTransactions(bankAccountMock, listOf( + createTransaction(bankAccountMock, BookingDate, Amount, OtherPartyName, OtherPartyBankCode, OtherPartyAccountId), + createTransaction(otherPartyName = secondOtherPartyName), + createTransaction() + )) + + + // when + val result = underTest.findRemittees(query) + + + // then + assertThat(result).hasSize(2) + assertThat(result.map { it.name }).containsExactlyInAnyOrder(OtherPartyName, secondOtherPartyName) + } + + @Test + fun findRemittees_DuplicateEntries() { + + // given + val query = OtherPartyName + + val before = underTest.findRemittees(query) + assertThat(before).isEmpty() + + bankingPersistence.saveOrUpdateAccountTransactions(bankAccountMock, listOf( + createTransaction(bankAccountMock, BookingDate, Amount, OtherPartyName, OtherPartyBankCode, OtherPartyAccountId), + createTransaction(bankAccountMock, "01.02.2020", Amount, OtherPartyName, OtherPartyBankCode, OtherPartyAccountId), + createTransaction(bankAccountMock, "03.04.2020", Amount, OtherPartyName, OtherPartyBankCode, OtherPartyAccountId), + createTransaction(), + createTransaction() + )) + + + // when + val result = underTest.findRemittees(query) + + + // then + assertThat(result).hasSize(1) + assertThat(result.first().name).isEqualTo(OtherPartyName) + assertThat(result.first().bic).isEqualTo(OtherPartyBankCode) + assertThat(result.first().iban).isEqualTo(OtherPartyAccountId) + } + + @Test + fun findRemittees_OtherName() { + + // given + val query = "Mandela" + + val before = underTest.findRemittees(query) + assertThat(before).isEmpty() + + bankingPersistence.saveOrUpdateAccountTransactions(bankAccountMock, listOf( + createTransaction(bankAccountMock, BookingDate, Amount, OtherPartyName, OtherPartyBankCode, OtherPartyAccountId), + createTransaction(), + createTransaction() + )) + + + // when + val result = underTest.findRemittees(query) + + + // then + assertThat(result).isEmpty() + } + + + private fun createTransaction(bankAccount: BankAccount = bankAccountMock, bookingDate: String, amount: BigDecimal = randomBigDecimal(), + otherPartyName: String = randomString(), otherPartyBankCode: String = randomString(), + otherPartyAccountId: String = randomString(), usage: String = randomString()): AccountTransaction { + + return createTransaction(bankAccount, dateFormat.parse(bookingDate), amount, otherPartyName, + otherPartyBankCode, otherPartyAccountId, usage) + } + + private fun createTransaction(bankAccount: BankAccount = bankAccountMock, bookingDate: Date = randomDate(), amount: BigDecimal = randomBigDecimal(), + otherPartyName: String = randomString(), otherPartyBankCode: String = randomString(), + otherPartyAccountId: String = randomString(), usage: String = randomString()): AccountTransaction { + + return AccountTransaction(amount, bookingDate, usage, otherPartyName, otherPartyBankCode, otherPartyAccountId, null, null, "EUR", bankAccount) + } + + private fun randomString(): String { + return UUID.randomUUID().toString() + } + + private fun randomDate(): Date { + val pseudoRandomLong = ThreadLocalRandom.current().nextLong(0, Date().time) + + return Date(pseudoRandomLong) + } + + private fun randomBigDecimal(): BigDecimal { + val pseudoRandomDouble = ThreadLocalRandom.current().nextDouble(-5-000.0, 12_000.0) + + return BigDecimal.valueOf(pseudoRandomDouble) + } + +} \ No newline at end of file diff --git a/persistence/json/BankingPersistenceJson/build.gradle b/persistence/json/BankingPersistenceJson/build.gradle index 4e23da0e..702f2fb8 100644 --- a/persistence/json/BankingPersistenceJson/build.gradle +++ b/persistence/json/BankingPersistenceJson/build.gradle @@ -1,8 +1,6 @@ apply plugin: 'java-library' apply plugin: 'kotlin' -apply plugin: 'kotlin-kapt' - sourceCompatibility = "1.7" targetCompatibility = "1.7" diff --git a/persistence/json/BankingPersistenceJson/src/main/kotlin/net/dankito/banking/persistence/BankingPersistenceJson.kt b/persistence/json/BankingPersistenceJson/src/main/kotlin/net/dankito/banking/persistence/BankingPersistenceJson.kt index b33574f0..57a3e4b7 100644 --- a/persistence/json/BankingPersistenceJson/src/main/kotlin/net/dankito/banking/persistence/BankingPersistenceJson.kt +++ b/persistence/json/BankingPersistenceJson/src/main/kotlin/net/dankito/banking/persistence/BankingPersistenceJson.kt @@ -1,6 +1,8 @@ package net.dankito.banking.persistence import net.dankito.banking.ui.model.Account +import net.dankito.banking.ui.model.AccountTransaction +import net.dankito.banking.ui.model.BankAccount import net.dankito.utils.serialization.ISerializer import net.dankito.utils.serialization.JacksonJsonSerializer import java.io.File @@ -29,4 +31,9 @@ open class BankingPersistenceJson( return serializer.deserializeListOr(jsonFile, Account::class.java, listOf()) } + + override fun saveOrUpdateAccountTransactions(bankAccount: BankAccount, transactions: List) { + // done when called saveOrUpdateAccount() + } + } \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 5689d88b..f8b7fe98 100644 --- a/settings.gradle +++ b/settings.gradle @@ -10,6 +10,7 @@ include ':fints4javaBankingClient' include ':hbci4jBankingClient' include ':BankingPersistenceJson' +include ':LuceneBankingPersistence' include ':fints4javaAndroidApp' @@ -17,4 +18,5 @@ include ':BankingJavaFxControls' include ':BankingJavaFxApp' -project(':BankingPersistenceJson').projectDir = "$rootDir/persistence/json/BankingPersistenceJson/" as File \ No newline at end of file +project(':BankingPersistenceJson').projectDir = "$rootDir/persistence/json/BankingPersistenceJson/" as File +project(':LuceneBankingPersistence').projectDir = "$rootDir/persistence/LuceneBankingPersistence/" as File