libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

commit 9eec3fc932a62c9040fa406b357e946c1634353c
parent ae2a31b369db7678a4f01ffef67af323c35d1e7a
Author: MS <ms@taler.net>
Date:   Mon, 30 Oct 2023 15:48:39 +0100

reducing util dependencies and unused code

Diffstat:
Mnexus/build.gradle | 2--
Mutil/build.gradle | 21+++++----------------
Dutil/src/main/kotlin/CamtJsonMapping.kt | 332-------------------------------------------------------------------------------
Mutil/src/main/kotlin/DB.kt | 191+------------------------------------------------------------------------------
Dutil/src/main/kotlin/zip.kt | 71-----------------------------------------------------------------------
Mutil/src/test/kotlin/DomainSocketTest.kt | 11-----------
6 files changed, 7 insertions(+), 621 deletions(-)

diff --git a/nexus/build.gradle b/nexus/build.gradle @@ -83,8 +83,6 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1" // Unit testing - // testImplementation 'junit:junit:4.13.2' - // From https://docs.gradle.org/current/userguide/java_testing.html#sec:java_testing_basics: testImplementation 'org.junit.jupiter:junit-jupiter:5.7.1' testImplementation 'org.jetbrains.kotlin:kotlin-test:1.5.21' testImplementation 'org.jetbrains.kotlin:kotlin-test-junit:1.5.21' diff --git a/util/build.gradle b/util/build.gradle @@ -31,32 +31,21 @@ sourceSets { def netty_version = '4.1.68.Final' dependencies { - implementation 'io.ktor:ktor-server-netty:1.6.1' implementation 'ch.qos.logback:logback-classic:1.4.5' - + implementation 'io.ktor:ktor-server-netty:1.6.1' // XML Stuff implementation "javax.xml.bind:jaxb-api:2.3.1" implementation "org.glassfish.jaxb:jaxb-runtime:2.3.1" implementation 'org.apache.santuario:xmlsec:2.2.2' - + // Crypto implementation group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.69' - - // Compression - implementation group: 'org.apache.commons', name: 'commons-compress', version: '1.21' - - // https://mvnrepository.com/artifact/org.jetbrains.kotlin/kotlin-reflect - implementation group: 'org.jetbrains.kotlin', name: 'kotlin-reflect', version: '1.5.21' - - // Database helper - implementation group: 'org.postgresql', name: 'postgresql', version: '42.5.4' - implementation "org.jetbrains.exposed:exposed-core:$exposed_version" - implementation "org.jetbrains.exposed:exposed-dao:$exposed_version" + // Unix domain socket to serve HTTP implementation "io.netty:netty-all:$netty_version" implementation "io.netty:netty-transport-native-epoll:$netty_version" implementation "io.ktor:ktor-server-test-host:$ktor_version" - implementation "io.ktor:ktor-serialization-jackson:$ktor_version" + // Database helper + implementation group: 'org.postgresql', name: 'postgresql', version: '42.5.4' - testImplementation "io.ktor:ktor-server-content-negotiation:$ktor_version" testImplementation group: 'junit', name: 'junit', version: '4.13.2' testImplementation 'org.jetbrains.kotlin:kotlin-test-junit:1.5.21' testImplementation 'org.jetbrains.kotlin:kotlin-test:1.5.21' diff --git a/util/src/main/kotlin/CamtJsonMapping.kt b/util/src/main/kotlin/CamtJsonMapping.kt @@ -1,331 +0,0 @@ -import com.fasterxml.jackson.annotation.JsonIgnore -import com.fasterxml.jackson.annotation.JsonInclude -import com.fasterxml.jackson.core.JsonGenerator -import com.fasterxml.jackson.core.JsonParser -import com.fasterxml.jackson.databind.DeserializationContext -import com.fasterxml.jackson.databind.SerializerProvider -import com.fasterxml.jackson.databind.annotation.JsonDeserialize -import com.fasterxml.jackson.databind.annotation.JsonSerialize -import com.fasterxml.jackson.databind.deser.std.StdDeserializer -import com.fasterxml.jackson.databind.ser.std.StdSerializer - -enum class CreditDebitIndicator { - DBIT, - CRDT -} - -enum class EntryStatus { - BOOK, // Booked - PDNG, // Pending - INFO, // Informational -} - -class CurrencyAmountDeserializer(jc: Class<*> = CurrencyAmount::class.java) : StdDeserializer<CurrencyAmount>(jc) { - override fun deserialize(p: JsonParser?, ctxt: DeserializationContext?): CurrencyAmount { - if (p == null) { - throw UnsupportedOperationException(); - } - val s = p.valueAsString - val components = s.split(":") - // FIXME: error handling! - return CurrencyAmount(components[0], components[1]) - } -} - -class CurrencyAmountSerializer(jc: Class<CurrencyAmount> = CurrencyAmount::class.java) : StdSerializer<CurrencyAmount>(jc) { - override fun serialize(value: CurrencyAmount?, gen: JsonGenerator?, provider: SerializerProvider?) { - if (gen == null) { - throw UnsupportedOperationException() - } - if (value == null) { - gen.writeNull() - } else { - gen.writeString("${value.currency}:${value.value}") - } - } -} - -// FIXME: this type duplicates AmountWithCurrency. -@JsonDeserialize(using = CurrencyAmountDeserializer::class) -@JsonSerialize(using = CurrencyAmountSerializer::class) -data class CurrencyAmount( - val currency: String, - val value: String -) - -@JsonInclude(JsonInclude.Include.NON_NULL) -data class CashAccount( - val name: String?, - val currency: String?, - val iban: String?, - val otherId: GenericId? -) - -@JsonInclude(JsonInclude.Include.NON_NULL) -data class GenericId( - val id: String, - val schemeName: String?, - val proprietarySchemeName: String?, - val issuer: String? -) - -@JsonInclude(JsonInclude.Include.NON_NULL) -data class PrivateIdentification( - val birthDate: String?, - val provinceOfBirth: String?, - val cityOfBirth: String?, - val countryOfBirth: String? -) - -@JsonInclude(JsonInclude.Include.NON_NULL) -data class OrganizationIdentification( - val bic: String?, - val lei: String? -) - -/** - * Identification of a party, which can be a private party - * or an organization. - * - * Mapping of ISO 20022 PartyIdentification135. - */ -@JsonInclude(JsonInclude.Include.NON_NULL) -data class PartyIdentification( - val name: String?, - val countryOfResidence: String?, - val privateId: PrivateIdentification?, - val organizationId: OrganizationIdentification?, - val postalAddress: PostalAddress?, - - /** - * Identification that applies to both private parties and organizations. - */ - val otherId: GenericId? -) - -@JsonInclude(JsonInclude.Include.NON_NULL) -data class PostalAddress( - val addressCode: String?, - val addressProprietaryId: String?, - val addressProprietarySchemeName: String?, - val addressProprietaryIssuer: String?, - val department: String?, - val subDepartment: String?, - val streetName: String?, - val buildingNumber: String?, - val buildingName: String?, - val floor: String?, - val postBox: String?, - val room: String?, - val postCode: String?, - val townName: String?, - val townLocationName: String?, - val districtName: String?, - val countrySubDivision: String?, - val country: String?, - val addressLines: List<String> -) - -@JsonInclude(JsonInclude.Include.NON_NULL) -data class AgentIdentification( - val name: String?, - - val bic: String?, - - /** - * Legal entity identification. - */ - val lei: String?, - - val clearingSystemMemberId: String?, - - val clearingSystemCode: String?, - - val proprietaryClearingSystemCode: String?, - - val postalAddress: PostalAddress?, - - val otherId: GenericId? -) - -@JsonInclude(JsonInclude.Include.NON_NULL) -data class CurrencyExchange( - val sourceCurrency: String, - val targetCurrency: String, - val unitCurrency: String?, - val exchangeRate: String, - val contractId: String?, - val quotationDate: String? -) - -@JsonInclude(JsonInclude.Include.NON_NULL) -data class Batch( - val messageId: String?, - val paymentInformationId: String?, - val batchTransactions: List<BatchTransaction> -) - -@JsonInclude(JsonInclude.Include.NON_NULL) -data class TransactionDetails( - val debtor: PartyIdentification?, - val debtorAccount: CashAccount?, - val debtorAgent: AgentIdentification?, - val creditor: PartyIdentification?, - val creditorAccount: CashAccount?, - val creditorAgent: AgentIdentification?, - val ultimateCreditor: PartyIdentification?, - val ultimateDebtor: PartyIdentification?, - - val endToEndId: String? = null, - val paymentInformationId: String? = null, - val messageId: String? = null, - val accountServicerRef: String? = null, - - val purpose: String?, - val proprietaryPurpose: String?, - - /** - * Currency exchange information for the transaction's amount. - */ - val currencyExchange: CurrencyExchange?, - - /** - * Amount as given in the payment initiation. - * Can be same or different currency as account currency. - */ - val instructedAmount: CurrencyAmount?, - - /** - * Raw amount used for currency exchange, before extra charges. - * Can be same or different currency as account currency. - */ - val counterValueAmount: CurrencyAmount?, - - /** - * Money that was moved between banks. - * - * For CH, we use the "TxAmt". - * For EPC, this amount is either blank or taken - * from the "IBC" proprietary amount. - */ - val interBankSettlementAmount: CurrencyAmount?, - - // PoFi shown entries lacking it. - val unstructuredRemittanceInformation: String?, - val returnInfo: ReturnInfo? -) - -@JsonInclude(JsonInclude.Include.NON_NULL) -data class ReturnInfo( - val originalBankTransactionCode: String?, - val originator: PartyIdentification?, - val reason: String?, - val proprietaryReason: String?, - val additionalInfo: String? -) - -data class BatchTransaction( - val amount: CurrencyAmount, // Fuels Taler withdrawal amount. - val creditDebitIndicator: CreditDebitIndicator, - val details: TransactionDetails -) - -@JsonInclude(JsonInclude.Include.NON_NULL) -data class CamtBankAccountEntry( - val amount: CurrencyAmount, - /** - * Is this entry debiting or crediting the account - * it is reported for? - */ - val creditDebitIndicator: CreditDebitIndicator, - - /** - * Booked, pending, etc. - */ - val status: EntryStatus, - - /** - * Code that describes the type of bank transaction - * in more detail - */ - val bankTransactionCode: String, - - val valueDate: String?, - - val bookingDate: String?, - - val accountServicerRef: String?, - - val entryRef: String?, - - /** - * Currency exchange information for the entry's amount. - * Only present if currency exchange happened at the entry level. - */ - val currencyExchange: CurrencyExchange?, - - /** - * Value before/after currency exchange before charges have been applied. - * Only present if currency exchange happened at the entry level. - */ - val counterValueAmount: CurrencyAmount?, - - /** - * Instructed amount. - * Only present if currency exchange happens at the entry level. - */ - val instructedAmount: CurrencyAmount?, - - // list of sub-transactions participating in this money movement. - val batches: List<Batch>? -) { - // Checks that the given list contains only one element and returns it. - private fun <T>checkAndGetSingleton(maybeTxs: List<T>?): T? { - if (maybeTxs == null || maybeTxs.size > 1) { - logger.error("Only a singleton transaction is allowed inside ${this.javaClass}.") - return null - } - return maybeTxs[0] - } - private fun getSingletonTxDtls(): TransactionDetails? { - /** - * Types breakdown until the meaningful payment information is reached. - * - * CamtBankAccountEntry contains: - * - Batch 0 - * - Batch 1 - * - Batch N - * - * Batch X contains: - * - BatchTransaction 0 - * - BatchTransaction 1 - * - BatchTransaction N - * - * BatchTransaction X contains: - * - TransactionDetails - * - * TransactionDetails contains the involved parties - * and the payment subject but MAY NOT contain the amount. - * In this model, the amount is held in the BatchTransaction - * type, that is also -- so far -- required to be a singleton - * inside Batch. - */ - val batch: Batch = checkAndGetSingleton(this.batches) ?: return null - val batchTransactions = batch.batchTransactions - val tx: BatchTransaction = checkAndGetSingleton(batchTransactions) ?: return null - val details: TransactionDetails = tx.details - return details - } - /** - * This function returns the subject of the unique transaction - * accounted in this object. If the transaction is not unique, - * it throws an exception. NOTE: the caller has the responsibility - * of not passing an empty report; those usually should be discarded - * and never participate in the application logic. - */ - @JsonIgnore - fun getSingletonSubject(): String? { - val maybeSubject = getSingletonTxDtls()?.unstructuredRemittanceInformation ?: return null - return maybeSubject - } -} -\ No newline at end of file diff --git a/util/src/main/kotlin/DB.kt b/util/src/main/kotlin/DB.kt @@ -23,11 +23,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import net.taler.wallet.crypto.Base32Crockford -import org.jetbrains.exposed.sql.Database -import org.jetbrains.exposed.sql.Transaction -import org.jetbrains.exposed.sql.name -import org.jetbrains.exposed.sql.transactions.TransactionManager -import org.jetbrains.exposed.sql.transactions.transaction import org.postgresql.ds.PGSimpleDataSource import org.postgresql.jdbc.PgConnection import org.slf4j.Logger @@ -39,13 +34,6 @@ import java.sql.ResultSet fun getCurrentUser(): String = System.getProperty("user.name") -fun isPostgres(): Boolean { - val db = TransactionManager.defaultDatabase ?: throw Exception( - "Could not find the default database, can't check if that's Postgres." - ) - return db.vendor == "postgresql" - -} // Check GANA (https://docs.gnunet.org/gana/index.html) for numbers allowance. /** @@ -92,181 +80,6 @@ fun buildChannelName( return ret } -fun Transaction.postgresNotify( - channel: String, - payload: String? = null -) { - logger.debug("Sending NOTIFY on channel '$channel' with payload '$payload'") - if (payload != null) { - val argEnc = Base32Crockford.encode(payload.toByteArray()) - if (payload.toByteArray().size > 8000) - throw Exception( - "DB notification on channel $channel used >8000 bytes payload '$payload'" - ) - this.exec("NOTIFY $channel, '$argEnc'") - return - } - this.exec("NOTIFY $channel") -} - -/** - * postgresListen() and postgresGetNotifications() appear to have - * to use the same connection, in order for the notifications to - * arrive. Therefore, calling LISTEN inside one "transaction {}" - * and postgresGetNotifications() outside of it did NOT work because - * Exposed _closes_ the connection as soon as the transaction block - * completes. OTOH, calling postgresGetNotifications() _inside_ the - * same transaction block as LISTEN's would lead to keep the database - * locked for the timeout duration. - * - * For this reason, opening and keeping one connection open for the - * lifetime of this object and only executing postgresListen() and - * postgresGetNotifications() _on that connection_ makes the event - * delivery more reliable. - */ -class PostgresListenHandle(val channelName: String) { - private val db = TransactionManager.defaultDatabase ?: throw Exception( - "Could not find the default database, won't get Postgres notifications." - ) - private val conn = db.connector().connection as PgConnection - - // Gets set to the NOTIFY's payload, in case one exists. - var receivedPayload: String? = null - - // Signals whether the connection should be kept open, - // after one (and possibly not expected) event arrives. - // This gives more flexibility to the caller. - var keepConnection: Boolean = false - - fun postgresListen() { - val stmt = conn.createStatement() - stmt.execute("LISTEN $channelName") - stmt.close() - logger.debug("LISTENing on channel: $channelName") - } - - fun postgresUnlisten() { - val stmt = conn.createStatement() - stmt.execute("UNLISTEN $channelName") - stmt.close() - logger.debug("UNLISTENing on channel: $channelName") - conn.close() - } - - private fun likelyCloseConnection() { - if (this.keepConnection) - return - this.conn.close() - } - - fun postgresGetNotifications(timeoutMs: Long): Boolean { - if (timeoutMs == 0L) - logger.info( - "Database notification checker has timeout == 0," + - " that waits FOREVER until a notification arrives." - ) - logger.debug( - "Waiting Postgres notifications on channel " + - "'$channelName' for $timeoutMs millis." - ) - val maybeNotifications = this.conn.getNotifications(timeoutMs.toInt()) - if (maybeNotifications == null || maybeNotifications.isEmpty()) { - logger.debug("DB notifications not found on channel $channelName.") - this.likelyCloseConnection() - return false - } - for (n in maybeNotifications) { - if (n.name.lowercase() != channelName.lowercase()) { - conn.close() // always close on error, without the optional check. - throw Exception("Channel $channelName got notified from ${n.name}!") - } - } - logger.debug("Found DB notifications on channel $channelName") - // Only ever used for singleton notifications. - assert(maybeNotifications.size == 1) - if (maybeNotifications[0].parameter.isNotEmpty()) - this.receivedPayload = maybeNotifications[0].parameter - this.likelyCloseConnection() - return true - } - - // Wrapper around the core method "postgresGetNotifications()" that - // sets up the coroutine environment to wait and release the execution. - suspend fun waitOnIODispatchers(timeoutMs: Long): Boolean = - coroutineScope { - async(Dispatchers.IO) { - postgresGetNotifications(timeoutMs) - }.await() - } - - /** - * Waits at most 'timeoutMs' on 'this.channelName' for - * the one particular payload that's passed in the 'payload' - * argument. FIXME: will be used along the fiat side of cash-outs. - */ - suspend fun waitOnIoDispatchersForPayload( - timeoutMs: Long, - expectedPayload: String - ): Boolean { - var leftTime = timeoutMs - val expectedPayloadEnc = Base32Crockford.encode(expectedPayload.toByteArray()) - /** - * This setting allows the loop to reuse the open connection, - * otherwise the internal loop would close it if one unexpected - * payload wakes it up. - */ - this.keepConnection = true - while (leftTime > 0) { - val loopStart = System.currentTimeMillis() - // Ask for notifications. - val maybeNotification = waitOnIODispatchers(leftTime) - // One arrived, check the payload. - if (maybeNotification) { - if (this.receivedPayload != null && this.receivedPayload == expectedPayloadEnc) { - conn.close() - return true - } - } - val loopEnd = System.currentTimeMillis() - // Account the spent time. - leftTime -= loopEnd - loopStart - } - conn.close() - return false - } -} - -fun getDatabaseName(): String { - var maybe_db_name: String? = null - transaction { - this.exec("SELECT current_database() AS database_name;") { oneLineRes -> - if (oneLineRes.next()) - maybe_db_name = oneLineRes.getString("database_name") - } - } - return maybe_db_name ?: throw Exception("Could not find current DB name") -} - -/** - * Abstracts over the Exposed details to connect - * to a database and ONLY use the passed schema - * WHEN PostgreSQL is the DBMS. - */ -fun connectWithSchema(jdbcConn: String, schemaName: String? = null) { - Database.connect( - jdbcConn, - setupConnection = { conn -> - if (isPostgres() && schemaName != null) - conn.schema = schemaName - } - ) - try { - transaction { this.db.name } - } catch (e: Throwable) { - logger.error("Test query failed: ${e.message}") - throw Exception("Failed connection to: $jdbcConn") - } -} /** * This function converts postgresql:// URIs to JDBC URIs. @@ -328,7 +141,6 @@ fun getJdbcConnectionFromPg(pgConn: String): String { return "jdbc:$pgConn" } - data class DatabaseConfig( val dbConnStr: String, val sqlDir: String @@ -426,4 +238,4 @@ fun resetDatabaseTables(cfg: DatabaseConfig, sqlFilePrefix: String) { val sqlDrop = File("${cfg.sqlDir}/$sqlFilePrefix-drop.sql").readText() conn.execSQLUpdate(sqlDrop) // TODO can fail ? } -} +} +\ No newline at end of file diff --git a/util/src/main/kotlin/zip.kt b/util/src/main/kotlin/zip.kt @@ -1,70 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2020 Taler Systems S.A. - * - * LibEuFin is free software; you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation; either version 3, or - * (at your option) any later version. - * - * LibEuFin is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General - * Public License for more details. - * - * You should have received a copy of the GNU Affero General Public - * License along with LibEuFin; see the file COPYING. If not, see - * <http://www.gnu.org/licenses/> - */ - -package tech.libeufin.util - -import java.io.ByteArrayInputStream -import java.io.ByteArrayOutputStream -import org.apache.commons.compress.archivers.ArchiveStreamFactory -import org.apache.commons.compress.archivers.zip.ZipArchiveEntry -import org.apache.commons.compress.archivers.zip.ZipFile -import org.apache.commons.compress.utils.IOUtils -import org.apache.commons.compress.utils.SeekableInMemoryByteChannel - -fun List<ByteArray>.zip(): ByteArray { - val baos = ByteArrayOutputStream() - val asf = ArchiveStreamFactory().createArchiveOutputStream( - ArchiveStreamFactory.ZIP, - baos - ) - for (fileIndex in this.indices) { - val zae = ZipArchiveEntry("File $fileIndex") - asf.putArchiveEntry(zae) - val bais = ByteArrayInputStream(this[fileIndex]) - IOUtils.copy(bais, asf) - bais.close() - asf.closeArchiveEntry() - } - asf.finish() - baos.close() - return baos.toByteArray() -} - -fun ByteArray.prettyPrintUnzip(): String { - val mem = SeekableInMemoryByteChannel(this) - val zipFile = ZipFile(mem) - val s = java.lang.StringBuilder() - zipFile.getEntriesInPhysicalOrder().iterator().forEach { entry -> - s.append("<=== File ${entry.name} ===>\n") - s.append(zipFile.getInputStream(entry).readAllBytes().toString(Charsets.UTF_8)) - s.append("\n") - } - return s.toString() -} - -fun ByteArray.unzipWithLambda(process: (Pair<String, String>) -> Unit) { - val mem = SeekableInMemoryByteChannel(this) - val zipFile = ZipFile(mem) - zipFile.getEntriesInPhysicalOrder().iterator().forEach { - process( - Pair(it.name, zipFile.getInputStream(it).readAllBytes().toString(Charsets.UTF_8)) - ) - } - zipFile.close() -} -\ No newline at end of file diff --git a/util/src/test/kotlin/DomainSocketTest.kt b/util/src/test/kotlin/DomainSocketTest.kt @@ -1,24 +1,13 @@ -import com.fasterxml.jackson.core.util.DefaultIndenter -import com.fasterxml.jackson.core.util.DefaultPrettyPrinter -import com.fasterxml.jackson.databind.DeserializationFeature -import com.fasterxml.jackson.databind.SerializationFeature -import com.fasterxml.jackson.module.kotlin.KotlinModule import io.ktor.server.application.* -import io.ktor.http.* import io.ktor.server.response.* import io.ktor.server.routing.* import org.junit.Test -import io.ktor.serialization.jackson.* -import io.ktor.server.request.* -import org.junit.Assert import org.junit.Ignore -import io.ktor.server.plugins.contentnegotiation.* class DomainSocketTest { @Test @Ignore fun bind() { startServer("/tmp/java.sock") { - install(ContentNegotiation) { jackson() } routing { get("/") { this.call.respond(object {})