Implemented BankListCreator to parse German banks file from Deutsche Kreditwirtschaft
This commit is contained in:
parent
304b3ba9d6
commit
a07b6b115e
|
@ -0,0 +1,27 @@
|
|||
apply plugin: 'java-library'
|
||||
apply plugin: 'kotlin'
|
||||
|
||||
|
||||
sourceCompatibility = 1.8
|
||||
|
||||
compileKotlin {
|
||||
kotlinOptions.jvmTarget = "1.8"
|
||||
}
|
||||
compileTestKotlin {
|
||||
kotlinOptions.jvmTarget = "1.8"
|
||||
}
|
||||
|
||||
|
||||
|
||||
dependencies {
|
||||
compile project(':fints4javaLib')
|
||||
|
||||
implementation 'org.docx4j:docx4j-JAXB-ReferenceImpl:8.1.3'
|
||||
|
||||
|
||||
testCompile "junit:junit:$junitVersion"
|
||||
testCompile "org.assertj:assertj-core:$assertJVersion"
|
||||
|
||||
testCompile "ch.qos.logback:logback-core:$logbackVersion"
|
||||
testCompile "ch.qos.logback:logback-classic:$logbackVersion"
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package net.dankito.banking.banklistcreator
|
||||
|
||||
import net.dankito.banking.banklistcreator.parser.DeutscheKreditwirtschaftBankListParser
|
||||
import net.dankito.utils.serialization.JacksonJsonSerializer
|
||||
import java.io.File
|
||||
|
||||
|
||||
open class BankListCreator @JvmOverloads constructor(
|
||||
protected val parser: DeutscheKreditwirtschaftBankListParser = DeutscheKreditwirtschaftBankListParser()
|
||||
) {
|
||||
|
||||
fun createBankListFromDeutscheKreditwirtschaftXlsxFile(bankFileOutputFile: File,
|
||||
deutscheKreditwirtschaftXlsxFile: File) {
|
||||
|
||||
val banks = parser.parse(deutscheKreditwirtschaftXlsxFile)
|
||||
|
||||
JacksonJsonSerializer().serializeObject(banks, bankFileOutputFile)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,276 @@
|
|||
package net.dankito.banking.banklistcreator.parser
|
||||
|
||||
import net.dankito.banking.banklistcreator.parser.model.BankCodeListEntry
|
||||
import net.dankito.banking.banklistcreator.parser.model.ServerAddressesListEntry
|
||||
import net.dankito.fints.model.BankInfo
|
||||
import org.docx4j.openpackaging.packages.SpreadsheetMLPackage
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.xlsx4j.org.apache.poi.ss.usermodel.DataFormatter
|
||||
import org.xlsx4j.sml.Cell
|
||||
import org.xlsx4j.sml.Row
|
||||
import org.xlsx4j.sml.SheetData
|
||||
import java.io.File
|
||||
|
||||
|
||||
/**
|
||||
* Parses the list of German banks from Deutsche Kreditwirtschaft you can retrieve by registering here:
|
||||
* https://www.hbci-zka.de/register/hersteller.htm
|
||||
*/
|
||||
open class DeutscheKreditwirtschaftBankListParser {
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(DeutscheKreditwirtschaftBankListParser::class.java)
|
||||
}
|
||||
|
||||
|
||||
fun parse(bankListFile: File): List<BankInfo> {
|
||||
val xlsxPkg = SpreadsheetMLPackage.load(bankListFile)
|
||||
|
||||
val workbookPart = xlsxPkg.getWorkbookPart()
|
||||
val sheets = workbookPart.contents.sheets.sheet
|
||||
val formatter = DataFormatter()
|
||||
|
||||
var serverAddressesList = listOf<ServerAddressesListEntry>()
|
||||
var bankCodesList = listOf<BankCodeListEntry>()
|
||||
|
||||
for (index in 0 until sheets.size) {
|
||||
log.info("\nParsing sheet ${sheets[index].name}:\n")
|
||||
val sheet = workbookPart.getWorksheet(index)
|
||||
val workSheetData = sheet.contents.sheetData
|
||||
|
||||
if (isListWithFinTsServerAddresses(workSheetData, formatter)) {
|
||||
serverAddressesList = parseListWithFinTsServerAddresses(workSheetData, formatter)
|
||||
}
|
||||
else if (isBankCodeList(workSheetData, formatter)) {
|
||||
bankCodesList = parseBankCodeList(workSheetData, formatter)
|
||||
}
|
||||
}
|
||||
|
||||
return mapBankCodeAndServerAddressesList(bankCodesList, serverAddressesList)
|
||||
}
|
||||
|
||||
private fun mapBankCodeAndServerAddressesList(banks: List<BankCodeListEntry>,
|
||||
serverAddresses: List<ServerAddressesListEntry>): List<BankInfo> {
|
||||
|
||||
val serverAddressesByBankCode = mutableMapOf<String, MutableList<ServerAddressesListEntry>>()
|
||||
serverAddresses.forEach { serverAddress ->
|
||||
if (serverAddressesByBankCode.containsKey(serverAddress.bankCode) == false) {
|
||||
serverAddressesByBankCode.put(serverAddress.bankCode, mutableListOf(serverAddress))
|
||||
}
|
||||
else {
|
||||
serverAddressesByBankCode[serverAddress.bankCode]!!.add(serverAddress)
|
||||
}
|
||||
}
|
||||
|
||||
return banks.map { mapToBankInfo(it, serverAddressesByBankCode as Map<String, List<ServerAddressesListEntry>>) }
|
||||
}
|
||||
|
||||
private fun mapToBankInfo(bank: BankCodeListEntry,
|
||||
serverAddressesByBankCode: Map<String, List<ServerAddressesListEntry>>): BankInfo {
|
||||
|
||||
val serverAddress = findServerAddress(bank, serverAddressesByBankCode)
|
||||
|
||||
return BankInfo(
|
||||
bank.bankName,
|
||||
bank.bankCode,
|
||||
bank.bic,
|
||||
bank.postalCode,
|
||||
bank.city,
|
||||
bank.checksumMethod,
|
||||
serverAddress?.pinTanAddress,
|
||||
serverAddress?.pinTanVersion,
|
||||
bank.oldBankCode
|
||||
)
|
||||
}
|
||||
|
||||
private fun findServerAddress(bankCode: BankCodeListEntry,
|
||||
serverAddressesByBankCode: Map<String, List<ServerAddressesListEntry>>
|
||||
): ServerAddressesListEntry? {
|
||||
|
||||
serverAddressesByBankCode[bankCode.bankCode]?.let { serverAddresses ->
|
||||
serverAddresses.firstOrNull { it.city == bankCode.city }?.let {
|
||||
return it
|
||||
}
|
||||
|
||||
return serverAddresses[0]
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
private fun isListWithFinTsServerAddresses(workSheetData: SheetData, formatter: DataFormatter): Boolean {
|
||||
return hasHeaders(workSheetData, formatter, listOf("BLZ", "PIN/TAN-Zugang URL"))
|
||||
}
|
||||
|
||||
private fun parseListWithFinTsServerAddresses(workSheetData: SheetData, formatter: DataFormatter):
|
||||
List<ServerAddressesListEntry> {
|
||||
|
||||
val entries = mutableListOf<ServerAddressesListEntry>()
|
||||
|
||||
val headerRow = workSheetData.row[0]
|
||||
val headerNames = headerRow.c.map { getCellText(it, formatter) }
|
||||
|
||||
val bankNameColumnIndex = headerNames.indexOf("Institut")
|
||||
val bankCodeColumnIndex = headerNames.indexOf("BLZ")
|
||||
val bicColumnIndex = headerNames.indexOf("BIC")
|
||||
val cityColumnIndex = headerNames.indexOf("Ort")
|
||||
val pinTanAddressColumnIndex = headerNames.indexOf("PIN/TAN-Zugang URL")
|
||||
val pinTanVersionColumnIndex = headerNames.indexOf("Version")
|
||||
|
||||
for (row in workSheetData.row.subList(1, workSheetData.row.size)) {
|
||||
parseToServerAddressesListEntry(row, formatter, bankNameColumnIndex, bankCodeColumnIndex, bicColumnIndex,
|
||||
cityColumnIndex, pinTanAddressColumnIndex, pinTanVersionColumnIndex)?.let { entry ->
|
||||
entries.add(entry)
|
||||
}
|
||||
}
|
||||
|
||||
return entries
|
||||
}
|
||||
|
||||
private fun parseToServerAddressesListEntry(row: Row, formatter: DataFormatter, bankNameColumnIndex: Int,
|
||||
bankCodeColumnIndex: Int, bicColumnIndex: Int, cityColumnIndex: Int,
|
||||
pinTanAddressColumnIndex: Int, pinTanVersionColumnIndex: Int):
|
||||
ServerAddressesListEntry? {
|
||||
|
||||
try {
|
||||
val bankCode = getCellText(row, bankCodeColumnIndex, formatter)
|
||||
|
||||
if (bankCode.isNotEmpty()) { // filter out empty rows
|
||||
|
||||
return ServerAddressesListEntry(
|
||||
getCellText(row, bankNameColumnIndex, formatter),
|
||||
bankCode,
|
||||
getCellText(row, bicColumnIndex, formatter),
|
||||
getCellText(row, cityColumnIndex, formatter),
|
||||
getCellText(row, pinTanAddressColumnIndex, formatter),
|
||||
getCellText(row, pinTanVersionColumnIndex, formatter)
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
log.error("Could not parse row ${getRowAsString(row, formatter)} to BankCodeListEntry", e)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
private fun isBankCodeList(workSheetData: SheetData, formatter: DataFormatter): Boolean {
|
||||
return hasHeaders(workSheetData, formatter, listOf("Bankleitzahl", "Merkmal"))
|
||||
}
|
||||
|
||||
private fun parseBankCodeList(workSheetData: SheetData, formatter: DataFormatter): List<BankCodeListEntry> {
|
||||
val entries = mutableListOf<BankCodeListEntry>()
|
||||
|
||||
val headerRow = workSheetData.row[0]
|
||||
val headerNames = headerRow.c.map { getCellText(it, formatter) }
|
||||
|
||||
val bankNameColumnIndex = headerNames.indexOf("Bezeichnung")
|
||||
val bankCodeColumnIndex = headerNames.indexOf("Bankleitzahl")
|
||||
val bicColumnIndex = headerNames.indexOf("BIC")
|
||||
val postalCodeColumnIndex = headerNames.indexOf("PLZ")
|
||||
val cityColumnIndex = headerNames.indexOf("Ort")
|
||||
val checksumMethodColumnIndex = headerNames.indexOf("Prüfziffer-berechnungs-methode")
|
||||
val bankCodeDeletedColumnIndex = headerNames.indexOf("Bankleitzahl-löschung")
|
||||
val newBankCodeColumnIndex = headerNames.indexOf("Nachfolge-Bankleitzahl")
|
||||
|
||||
var lastParsedEntry: BankCodeListEntry? = null
|
||||
|
||||
for (row in workSheetData.row.subList(1, workSheetData.row.size)) {
|
||||
parseToBankCodeListEntry(row, formatter, bankNameColumnIndex, bankCodeColumnIndex, bicColumnIndex,
|
||||
postalCodeColumnIndex, cityColumnIndex, checksumMethodColumnIndex, bankCodeDeletedColumnIndex,
|
||||
newBankCodeColumnIndex)?.let { entry ->
|
||||
// if the following banks have the same BIC, the BIC is only given for the first bank -> get BIC from previous bank
|
||||
if (entry.bic.isEmpty() &&
|
||||
(entry.bankCode == lastParsedEntry?.bankCode || entry.bankCode == lastParsedEntry?.oldBankCode)) {
|
||||
entry.bic = lastParsedEntry?.bic!!
|
||||
}
|
||||
|
||||
entries.add(entry)
|
||||
|
||||
lastParsedEntry = entry
|
||||
}
|
||||
}
|
||||
|
||||
updateDeletedBanks(entries)
|
||||
|
||||
return entries
|
||||
}
|
||||
|
||||
private fun parseToBankCodeListEntry(row: Row, formatter: DataFormatter, bankNameColumnIndex: Int,
|
||||
bankCodeColumnIndex: Int, bicColumnIndex: Int, postalCodeColumnIndex: Int,
|
||||
cityColumnIndex: Int, checksumMethodColumnIndex: Int,
|
||||
bankCodeDeletedColumnIndex: Int, newBankCodeColumnIndex: Int): BankCodeListEntry? {
|
||||
|
||||
try {
|
||||
val bankCode = getCellText(row, bankCodeColumnIndex, formatter)
|
||||
|
||||
if (bankCode.isNotEmpty()) { // filter out empty rows
|
||||
var newBankCode: String? = null
|
||||
val isBankCodeDeleted = getCellText(row, bankCodeDeletedColumnIndex, formatter) == "1"
|
||||
val newBankCodeCellText = getCellText(row, newBankCodeColumnIndex, formatter)
|
||||
if (isBankCodeDeleted && newBankCodeCellText.isNotEmpty() && newBankCodeCellText != "00000000") {
|
||||
newBankCode = newBankCodeCellText
|
||||
}
|
||||
|
||||
return BankCodeListEntry(
|
||||
getCellText(row, bankNameColumnIndex, formatter),
|
||||
newBankCode ?: bankCode,
|
||||
getCellText(row, bicColumnIndex, formatter),
|
||||
getCellText(row, postalCodeColumnIndex, formatter),
|
||||
getCellText(row, cityColumnIndex, formatter),
|
||||
getCellText(row, checksumMethodColumnIndex, formatter),
|
||||
if (newBankCode != null) bankCode else newBankCode
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
log.error("Could not parse row ${getRowAsString(row, formatter)} to BankCodeListEntry", e)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Deleted banks may not have a BIC. This method fixes this
|
||||
*/
|
||||
private fun updateDeletedBanks(banks: MutableList<BankCodeListEntry>) {
|
||||
val banksByCode = banks.associateBy { it.bankCode }
|
||||
|
||||
val deletedBanks = banks.filter { it.isBankCodeDeleted }
|
||||
|
||||
for (deletedBank in deletedBanks) {
|
||||
banksByCode[deletedBank.bankCode]?.let { newBank ->
|
||||
deletedBank.bic = newBank.bic
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun hasHeaders(workSheetData: SheetData, formatter: DataFormatter, headerNames: List<String>): Boolean {
|
||||
if (workSheetData.row.isNotEmpty()) {
|
||||
val headerRow = workSheetData.row[0]
|
||||
|
||||
val rowHeaderNames = headerRow.c.map { getCellText(it, formatter) }
|
||||
|
||||
return rowHeaderNames.containsAll(headerNames)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private fun getCellText(row: Row, columnIndex: Int, formatter: DataFormatter): String {
|
||||
return getCellText(row.c[columnIndex], formatter)
|
||||
}
|
||||
|
||||
private fun getCellText(cell: Cell, formatter: DataFormatter): String {
|
||||
if (cell.f != null) { // cell with formular
|
||||
return cell.v
|
||||
}
|
||||
return formatter.formatCellValue(cell)
|
||||
}
|
||||
|
||||
private fun getRowAsString(row: Row, formatter: DataFormatter): String {
|
||||
return row.c.joinToString("\t|", "|\t", "\t|") { getCellText(it, formatter) }
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package net.dankito.banking.banklistcreator.parser.model
|
||||
|
||||
|
||||
open class BankCodeListEntry(
|
||||
val bankName: String,
|
||||
val bankCode: String,
|
||||
var bic: String, // TODO: make val again
|
||||
val postalCode: String,
|
||||
val city: String,
|
||||
val checksumMethod: String,
|
||||
val oldBankCode: String?
|
||||
) {
|
||||
|
||||
val isBankCodeDeleted: Boolean
|
||||
get() = oldBankCode != null
|
||||
|
||||
|
||||
override fun toString(): String {
|
||||
return "$bankCode $bankName ($bic, $city)"
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package net.dankito.banking.banklistcreator.parser.model
|
||||
|
||||
|
||||
open class ServerAddressesListEntry(
|
||||
val bankName: String,
|
||||
val bankCode: String,
|
||||
val bic: String,
|
||||
val city: String,
|
||||
val pinTanAddress: String?,
|
||||
val pinTanVersion: String?
|
||||
) {
|
||||
|
||||
override fun toString(): String {
|
||||
return "$bankCode $bankName ($city, $pinTanAddress)"
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package net.dankito.banking.banklistcreator.parser
|
||||
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
import java.io.File
|
||||
|
||||
|
||||
@Ignore
|
||||
class DeutscheKreditwirtschaftBankListParserTest {
|
||||
|
||||
private val underTest = DeutscheKreditwirtschaftBankListParser()
|
||||
|
||||
|
||||
@Test
|
||||
fun parse() {
|
||||
|
||||
// when
|
||||
// TODO: set path to bank list file from Deutsche Kreditwirtschaft here
|
||||
val result = underTest.parse(File(""))
|
||||
|
||||
// then
|
||||
assertThat(result).hasSize(16282)
|
||||
|
||||
result.forEach { bankInfo ->
|
||||
assertThat(bankInfo.name).isNotEmpty()
|
||||
assertThat(bankInfo.bankCode).isNotEmpty()
|
||||
// assertThat(bankInfo.bic).isNotEmpty() // TODO: is there a way to find BICs for all banks?
|
||||
assertThat(bankInfo.postalCode).isNotEmpty()
|
||||
assertThat(bankInfo.city).isNotEmpty()
|
||||
assertThat(bankInfo.checksumMethod).isNotEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
<configuration>
|
||||
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<!-- encoders are assigned the type
|
||||
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
|
||||
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
|
||||
<level>DEBUG</level>
|
||||
</filter>
|
||||
</appender>
|
||||
|
||||
<!-- Insert the current time formatted as "yyyyMMdd'T'HHmmss" under
|
||||
the key "bySecond" into the logger context. This value will be
|
||||
available to all subsequent configuration elements. -->
|
||||
<timestamp key="bySecond" datePattern="yyyyMMdd'T'HHmmss"/>
|
||||
|
||||
<!-- Raise log level here if you don't want to see noisy log output -->
|
||||
<root level="INFO">
|
||||
<appender-ref ref="STDOUT"/>
|
||||
</root>
|
||||
</configuration>
|
|
@ -15,7 +15,9 @@ compileTestKotlin {
|
|||
|
||||
|
||||
dependencies {
|
||||
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
|
||||
api "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
|
||||
|
||||
api "net.dankito.utils:java-utils:$javaUtilsVersion"
|
||||
|
||||
compile "net.dankito.utils:java-utils:$javaUtilsVersion"
|
||||
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
package net.dankito.fints.model
|
||||
|
||||
|
||||
open class BankInfo(
|
||||
val name: String,
|
||||
val bankCode: String,
|
||||
val bic: String,
|
||||
val postalCode: String,
|
||||
val city: String,
|
||||
val checksumMethod: String,
|
||||
val pinTanAddress: String?,
|
||||
val pinTanVersion: String?,
|
||||
val oldBankCode: String?
|
||||
) {
|
||||
|
||||
|
||||
protected constructor() : this("", "", "", "", "", "", null, null, null) // for object deserializers
|
||||
|
||||
val supportsPinTan: Boolean
|
||||
get() = pinTanAddress.isNullOrEmpty() == false
|
||||
|
||||
val supportsFinTs3_0: Boolean
|
||||
get() = pinTanVersion == "FinTS V3.0"
|
||||
|
||||
val isBankCodeDeleted: Boolean
|
||||
get() = oldBankCode != null // TODO: this is not in all cases true, there are banks with new bank code which haven't been deleted
|
||||
|
||||
|
||||
override fun toString(): String {
|
||||
return "$bankCode $name $city"
|
||||
}
|
||||
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
rootProject.name = 'fints4java'
|
||||
|
||||
include ':fints4javaLib'
|
||||
include ':fints4javaLib'
|
||||
|
||||
include ':BankListCreator'
|
Loading…
Reference in New Issue