gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[libeufin] branch master updated: more ISO parsing/schema work


From: gnunet
Subject: [libeufin] branch master updated: more ISO parsing/schema work
Date: Thu, 02 Jul 2020 11:49:59 +0200

This is an automated email from the git hooks/post-receive script.

dold pushed a commit to branch master
in repository libeufin.

The following commit(s) were added to refs/heads/master by this push:
     new 340506a  more ISO parsing/schema work
340506a is described below

commit 340506abc60e1c3e30767b1a14e54bd6cf620f71
Author: Florian Dold <florian.dold@gmail.com>
AuthorDate: Thu Jul 2 15:19:51 2020 +0530

    more ISO parsing/schema work
---
 .idea/dictionaries/dold.xml                        |   4 +
 .idea/misc.xml                                     |   5 +
 .../main/kotlin/tech/libeufin/nexus/Iso20022.kt    | 140 ++++++++++-----------
 nexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt |   8 +-
 .../tech/libeufin/nexus/bankaccount/BankAccount.kt |  18 ++-
 .../main/kotlin/tech/libeufin/nexus/server/JSON.kt |  28 ++++-
 .../tech/libeufin/nexus/server/NexusServer.kt      |   3 +-
 nexus/src/test/kotlin/Iso20022Test.kt              |  21 +++-
 .../src/main/kotlin/tech/libeufin/sandbox/DB.kt    |   2 +-
 9 files changed, 128 insertions(+), 101 deletions(-)

diff --git a/.idea/dictionaries/dold.xml b/.idea/dictionaries/dold.xml
index d87c61c..762a1ed 100644
--- a/.idea/dictionaries/dold.xml
+++ b/.idea/dictionaries/dold.xml
@@ -2,11 +2,15 @@
   <dictionary name="dold">
     <words>
       <w>affero</w>
+      <w>camt</w>
       <w>combinators</w>
+      <w>crdt</w>
       <w>cronspec</w>
+      <w>dbit</w>
       <w>ebics</w>
       <w>libeufin</w>
       <w>payto</w>
+      <w>pdng</w>
       <w>sqlite</w>
     </words>
   </dictionary>
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 4bc4fc6..24ee198 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,5 +1,10 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <project version="4">
+  <component name="EntryPointsManager">
+    <list size="1">
+      <item index="0" class="java.lang.String" 
itemvalue="com.fasterxml.jackson.annotation.JsonValue" />
+    </list>
+  </component>
   <component name="ExternalStorageConfigurationManager" enabled="true" />
   <component name="ProjectRootManager" version="2" languageLevel="JDK_11" 
default="true" project-jdk-name="11" project-jdk-type="JavaSDK" />
 </project>
\ No newline at end of file
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt
index c7e4394..7f04de9 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt
@@ -27,6 +27,7 @@ import com.fasterxml.jackson.annotation.JsonSubTypes
 import com.fasterxml.jackson.annotation.JsonTypeInfo
 import com.fasterxml.jackson.annotation.JsonValue
 import org.w3c.dom.Document
+import tech.libeufin.nexus.server.CurrencyAmount
 import tech.libeufin.util.*
 import java.time.Instant
 import java.time.ZoneId
@@ -54,24 +55,30 @@ enum class TransactionStatus {
     INFO,
 }
 
-enum class CashManagementResponseType(@get:JsonValue val jsonname: String) {
+enum class CashManagementResponseType(@get:JsonValue val jsonName: String) {
     Report("report"), Statement("statement"), Notification("notification")
 }
 
-/**
- * Schemes to identify a transaction within an account.
- * An identifier from such a scheme will be used to reconcile transactions
- * from multiple sources.
- * (LibEuFin-specific, not defined by ISO 20022)
- */
-enum class TransactionIdentifierSchemes {
+data class CamtReport(
+    val account: AccountIdentification,
+    val entries: List<CamtBankAccountEntry>
+)
+
+data class CamtParseResult(
+    val reports: List<CamtReport>,
+    val messageId: String,
     /**
-     * Reconcile based on the account servicer reference.
+     * Message type in form of the ISO 20022 message name.
      */
-    AcctSvcrRef
-}
+    val messageType: CashManagementResponseType,
+    val creationDateTime: String
+)
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+data class TransactionInfo(
+    val batchPaymentInformationId: String?,
+    val batchMessageId: String?,
 
-data class TransactionDetails(
     /**
      * Related parties as JSON.
      */
@@ -100,22 +107,15 @@ data class AccountIdentificationGeneric(
     val proprietary: String?
 ) : AccountIdentification()
 
-data class BankTransaction(
-    val account: AccountIdentification,
-    /**
-     * Identifier for the transaction that should be unique within an account.
-     * Prefix by the identifier scheme name followed by a colon.
-     */
-    val transactionIdentifier: String,
-    /**
-     * Scheme used for the account identifier.
-     */
-    val currency: String,
-    val amount: String,
+
+data class CamtBankAccountEntry(
+    val entryAmount: CurrencyAmount,
+
     /**
      * Booked, pending, etc.
      */
     val status: TransactionStatus,
+
     /**
      * Is this transaction debiting or crediting the account
      * it is reported for?
@@ -127,13 +127,13 @@ data class BankTransaction(
      */
     val bankTransactionCode: BankTransactionCode,
     /**
-     * Is this a batch booking?
+     * Transaction details, if this entry contains a single transaction.
      */
-    val isBatch: Boolean,
-    val details: List<TransactionDetails>,
+    val transactionInfos: List<TransactionInfo>,
     val valueDate: DateOrDateTime?,
     val bookingDate: DateOrDateTime?,
-    val accountServicerReference: String?
+    val accountServicerRef: String?,
+    val entryRef: String?
 )
 
 @JsonTypeInfo(
@@ -175,15 +175,11 @@ class DateTime(
 
 @JsonInclude(JsonInclude.Include.NON_NULL)
 data class BankTransactionCode(
-    /**
-     * Standardized bank transaction code, as "$domain/$family/$subfamily"
-     */
-    val iso: String?,
-
-    /**
-     * Proprietary code, as "$issuer/$code".
-     */
-    val proprietary: String?
+    val domain: String?,
+    val family: String?,
+    val subfamily: String?,
+    val proprietaryCode: String?,
+    val proprietaryIssuer: String?
 )
 
 data class AmountAndCurrencyExchangeDetails(
@@ -441,10 +437,12 @@ private fun 
XmlElementDestructor.extractAmountAndCurrencyExchangeDetails(): Amou
     )
 }
 
-private fun XmlElementDestructor.extractTransactionDetails(): 
List<TransactionDetails> {
+private fun XmlElementDestructor.extractTransactionInfos(): 
List<TransactionInfo> {
     return requireUniqueChildNamed("NtryDtls") {
         mapEachChildNamed("TxDtls") {
-            TransactionDetails(
+            TransactionInfo(
+                batchMessageId = null,
+                batchPaymentInformationId = null,
                 relatedParties = extractPartiesAndAgents(),
                 amountDetails = maybeUniqueChildNamed("AmtDtls") {
                     AmountDetails(
@@ -467,9 +465,9 @@ private fun 
XmlElementDestructor.extractTransactionDetails(): List<TransactionDe
     }
 }
 
-private fun XmlElementDestructor.extractInnerTransactions(): 
List<BankTransaction> {
+private fun XmlElementDestructor.extractInnerTransactions(): CamtReport {
     val account = requireUniqueChildNamed("Acct") { extractAccount() }
-    return mapEachChildNamed("Ntry") {
+    val entries = mapEachChildNamed("Ntry") {
         val amount = requireUniqueChildNamed("Amt") { it.textContent }
         val currency = requireUniqueChildNamed("Amt") { it.getAttribute("Ccy") 
}
         val status = requireUniqueChildNamed("Sts") { it.textContent }.let {
@@ -480,54 +478,44 @@ private fun 
XmlElementDestructor.extractInnerTransactions(): List<BankTransactio
         }
         val btc = requireUniqueChildNamed("BkTxCd") {
             BankTransactionCode(
-                proprietary = maybeUniqueChildNamed("Prtry") {
-                    val cd = requireUniqueChildNamed("Cd") { it.textContent }
-                    val issr = requireUniqueChildNamed("Issr") { 
it.textContent }
-                    "$issr/$cd"
+                domain = maybeUniqueChildNamed("Domn") { 
maybeUniqueChildNamed("Cd") { it.textContent} },
+                family = maybeUniqueChildNamed("Domn") {
+                    maybeUniqueChildNamed("Fmly") {
+                        maybeUniqueChildNamed("Cd") { it.textContent }
+                    }
                 },
-                iso = maybeUniqueChildNamed("Domn") {
-                    val cd = requireUniqueChildNamed("Cd") { it.textContent }
-                    val r = requireUniqueChildNamed("Fmly") {
-                        object {
-                            val fmlyCd = requireUniqueChildNamed("Cd") { 
it.textContent }
-                            val subFmlyCd = 
requireUniqueChildNamed("SubFmlyCd") { it.textContent }
-                        }
+                subfamily = maybeUniqueChildNamed("Domn") {
+                    maybeUniqueChildNamed("Fmly") {
+                        maybeUniqueChildNamed("SubFmlyCd") { it.textContent }
                     }
-                    "$cd/${r.fmlyCd}/${r.subFmlyCd}"
+                },
+                proprietaryCode = maybeUniqueChildNamed("Prtry") {
+                    maybeUniqueChildNamed("Cd") { it.textContent }
+                },
+                proprietaryIssuer = maybeUniqueChildNamed("Prtry") {
+                    maybeUniqueChildNamed("Issr") { it.textContent }
                 }
             )
         }
         val acctSvcrRef = maybeUniqueChildNamed("AcctSvcrRef") { 
it.textContent }
+        val entryRef = maybeUniqueChildNamed("NtryRef") { it.textContent }
         // For now, only support account servicer reference as id
-        val txId = "AcctSvcrRef:" + (acctSvcrRef ?: throw 
Exception("currently, AcctSvcrRef is mandatory in LibEuFin"))
-        val details = extractTransactionDetails()
-        BankTransaction(
-            account = account,
-            amount = amount,
-            currency = currency,
+        val transactionInfos = extractTransactionInfos()
+        CamtBankAccountEntry(
+            entryAmount = CurrencyAmount(currency, amount),
             status = status,
             creditDebitIndicator = creditDebitIndicator,
             bankTransactionCode = btc,
-            details = details,
-            isBatch = details.size > 1,
+            transactionInfos = transactionInfos,
             bookingDate = maybeUniqueChildNamed("BookgDt") { 
extractDateOrDateTime() },
             valueDate = maybeUniqueChildNamed("ValDt") { 
extractDateOrDateTime() },
-            accountServicerReference = acctSvcrRef,
-            transactionIdentifier = txId
+            accountServicerRef = acctSvcrRef,
+            entryRef = entryRef
         )
     }
+    return CamtReport(account, entries);
 }
 
-data class CamtParseResult(
-    val transactions: List<BankTransaction>,
-    val messageId: String,
-    /**
-     * Message type in form of the ISO 20022 message name.
-     */
-    val messageType: CashManagementResponseType,
-    val creationDateTime: String
-)
-
 /**
  * Extract a list of transactions from an ISO20022 camt.052 / camt.053 message.
  */
@@ -535,7 +523,7 @@ fun parseCamtMessage(doc: Document): CamtParseResult {
     return destructXml(doc) {
         requireRootElement("Document") {
             // Either bank to customer statement or report
-            val transactions = requireOnlyChild {
+            val reports = requireOnlyChild {
                 when (it.localName) {
                     "BkToCstmrAcctRpt" -> {
                         mapEachChildNamed("Rpt") {
@@ -551,7 +539,7 @@ fun parseCamtMessage(doc: Document): CamtParseResult {
                         throw CamtParsingError("expected statement or report")
                     }
                 }
-            }.flatten()
+            }
             val messageId = requireOnlyChild {
                 requireUniqueChildNamed("GrpHdr") {
                     requireUniqueChildNamed("MsgId") { it.textContent }
@@ -571,7 +559,7 @@ fun parseCamtMessage(doc: Document): CamtParseResult {
                     }
                 }
             }
-            CamtParseResult(transactions, messageId, messageType, 
creationDateTime)
+            CamtParseResult(reports, messageId, messageType, creationDateTime)
         }
     }
 }
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt
index 6cb3adf..33e7e43 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt
@@ -348,7 +348,7 @@ private suspend fun talerAddIncoming(call: ApplicationCall, 
httpClient: HttpClie
 }
 
 
-private fun ingestIncoming(payment: NexusBankTransactionEntity, txDtls: 
TransactionDetails) {
+private fun ingestIncoming(payment: NexusBankTransactionEntity, txDtls: 
TransactionInfo) {
     val subject = txDtls.unstructuredRemittanceInformation
     val debtorName = txDtls.relatedParties.debtor?.name
     if (debtorName == null) {
@@ -414,13 +414,13 @@ fun ingestTalerTransactions() {
                     (NexusBankTransactionsTable.id.greater(lastId))
         }.orderBy(Pair(NexusBankTransactionsTable.id, SortOrder.ASC)).forEach {
             // Incoming payment.
-            val tx = jacksonObjectMapper().readValue(it.transactionJson, 
BankTransaction::class.java)
-            if (tx.isBatch) {
+            val tx = jacksonObjectMapper().readValue(it.transactionJson, 
CamtBankAccountEntry::class.java)
+            if (tx.transactionInfos.size != 1) {
                 // We don't support batch transactions at the moment!
                 logger.warn("batch transactions not supported")
             } else {
                 when (tx.creditDebitIndicator) {
-                    CreditDebitIndicator.CRDT -> ingestIncoming(it, txDtls = 
tx.details[0])
+                    CreditDebitIndicator.CRDT -> ingestIncoming(it, txDtls = 
tx.transactionInfos[0])
                 }
             }
             lastId = it.id.value
diff --git 
a/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt
index b665dbd..1f97cf9 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt
@@ -21,21 +21,17 @@ package tech.libeufin.nexus.bankaccount
 
 import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
 import io.ktor.application.ApplicationCall
-import io.ktor.application.call
 import io.ktor.client.HttpClient
 import io.ktor.http.HttpStatusCode
 import org.jetbrains.exposed.sql.*
 import org.jetbrains.exposed.sql.transactions.transaction
 import org.w3c.dom.Document
 import tech.libeufin.nexus.*
-import tech.libeufin.nexus.OfferedBankAccountsTable.iban
-import tech.libeufin.nexus.OfferedBankAccountsTable.imported
 import tech.libeufin.nexus.ebics.fetchEbicsBySpec
 import tech.libeufin.nexus.ebics.submitEbicsPaymentInitiation
 import tech.libeufin.nexus.server.FetchSpecJson
 import tech.libeufin.nexus.server.Pain001Data
 import tech.libeufin.nexus.server.requireBankConnection
-import tech.libeufin.nexus.server.requireBankConnectionInternal
 import tech.libeufin.util.XMLUtil
 import java.time.Instant
 import java.time.ZonedDateTime
@@ -139,10 +135,10 @@ fun processCamtMessage(
             }
         }
 
-        val transactions = res.transactions
-        logger.info("found ${transactions.size} transactions")
-        txloop@ for (tx in transactions) {
-            val acctSvcrRef = tx.accountServicerReference
+        val entries = res.reports.map { it.entries }.flatten()
+        logger.info("found ${entries.size} transactions")
+        txloop@ for (tx in entries) {
+            val acctSvcrRef = tx.accountServicerRef
             if (acctSvcrRef == null) {
                 // FIXME(dold): Report this!
                 logger.error("missing account servicer reference in 
transaction")
@@ -158,15 +154,15 @@ fun processCamtMessage(
             val rawEntity = NexusBankTransactionEntity.new {
                 bankAccount = acct
                 accountTransactionId = "AcctSvcrRef:$acctSvcrRef"
-                amount = tx.amount
-                currency = tx.currency
+                amount = tx.entryAmount.amount
+                currency = tx.entryAmount.currency
                 transactionJson = 
jacksonObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(tx)
                 creditDebitIndicator = tx.creditDebitIndicator.name
                 status = tx.status
             }
             rawEntity.flush()
             if (tx.creditDebitIndicator == CreditDebitIndicator.DBIT) {
-                val t0 = tx.details.getOrNull(0)
+                val t0 = tx.transactionInfos.getOrNull(0)
                 val msgId = t0?.references?.messageIdentification
                 val pmtInfId = t0?.references?.paymentInformationIdentification
                 if (t0 != null && msgId != null && pmtInfId != null) {
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/server/JSON.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/server/JSON.kt
index 02cf1b8..8bae0fb 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/server/JSON.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/server/JSON.kt
@@ -24,7 +24,9 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo
 import com.fasterxml.jackson.annotation.JsonTypeName
 import com.fasterxml.jackson.annotation.JsonValue
 import com.fasterxml.jackson.databind.JsonNode
-import tech.libeufin.nexus.BankTransaction
+import tech.libeufin.nexus.CamtBankAccountEntry
+import tech.libeufin.nexus.CreditDebitIndicator
+import tech.libeufin.nexus.TransactionStatus
 import tech.libeufin.util.*
 import java.time.Instant
 import java.time.ZoneId
@@ -224,7 +226,7 @@ data class PaymentStatus(
 )
 
 data class Transactions(
-    val transactions: MutableList<BankTransaction> = mutableListOf()
+    val transactions: MutableList<CamtBankAccountEntry> = mutableListOf()
 )
 
 data class BankProtocolsResponse(
@@ -338,4 +340,24 @@ data class CreateAccountTaskRequest(
 data class ImportBankAccount(
     val offeredAccountId: String,
     val nexusBankAccountId: String
-)
\ No newline at end of file
+)
+
+data class CurrencyAmount(
+    val currency: String,
+    val amount: String
+)
+
+
+/**
+ * Account entry item as returned by the /bank-accounts/{acctId}/transactions 
API.
+ */
+data class AccountEntryItemJson(
+    val nexusEntryId: String,
+    val nexusStatusSequenceId: Int,
+
+    val entryId: String?,
+    val accountServicerRef: String?,
+    val creditDebitIndicator: CreditDebitIndicator,
+    val entryAmount: CurrencyAmount,
+    val status: TransactionStatus
+)
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt
index 22d130f..fadea58 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt
@@ -48,7 +48,6 @@ import io.ktor.server.netty.Netty
 import io.ktor.utils.io.ByteReadChannel
 import io.ktor.utils.io.jvm.javaio.toByteReadChannel
 import io.ktor.utils.io.jvm.javaio.toInputStream
-import org.jetbrains.exposed.dao.id.EntityID
 import org.jetbrains.exposed.sql.and
 import org.jetbrains.exposed.sql.select
 import org.jetbrains.exposed.sql.transactions.transaction
@@ -574,7 +573,7 @@ fun serverMain(dbName: String, host: String) {
                 transaction {
                     authenticateRequest(call.request).id.value
                     NexusBankTransactionEntity.all().map {
-                        val tx = 
jacksonObjectMapper().readValue(it.transactionJson, BankTransaction::class.java)
+                        val tx = 
jacksonObjectMapper().readValue(it.transactionJson, 
CamtBankAccountEntry::class.java)
                         ret.transactions.add(tx)
                     }
                 }
diff --git a/nexus/src/test/kotlin/Iso20022Test.kt 
b/nexus/src/test/kotlin/Iso20022Test.kt
index 792bb8a..4a91f17 100644
--- a/nexus/src/test/kotlin/Iso20022Test.kt
+++ b/nexus/src/test/kotlin/Iso20022Test.kt
@@ -22,11 +22,24 @@ class Iso20022Test {
         assertEquals(r.messageId, "27632364572")
         assertEquals(r.creationDateTime, "2016-05-11T19:30:47.0+01:00")
         assertEquals(r.messageType, CashManagementResponseType.Statement)
-        for (tx in r.transactions) {
-            // Make sure that roundtripping works
-            val txStr = 
jacksonObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(tx)
+        assertEquals(r.reports.size, 1)
+        assertEquals(r.reports[0].entries[0].entryAmount.amount, "100.00")
+        assertEquals(r.reports[0].entries[0].entryAmount.currency, "EUR")
+        assertEquals(r.reports[0].entries[0].status, TransactionStatus.BOOK)
+        assertEquals(r.reports[0].entries[0].entryRef, null)
+        assertEquals(r.reports[0].entries[0].accountServicerRef, 
"Bankreferenz")
+        assertEquals(r.reports[0].entries[0].bankTransactionCode.domain, 
"PMNT")
+        assertEquals(r.reports[0].entries[0].bankTransactionCode.family, 
"RCDT")
+        assertEquals(r.reports[0].entries[0].bankTransactionCode.subfamily, 
"ESCT")
+        
assertEquals(r.reports[0].entries[0].bankTransactionCode.proprietaryCode, "166")
+        
assertEquals(r.reports[0].entries[0].bankTransactionCode.proprietaryIssuer, 
"DK")
+        assertEquals(r.reports[0].entries[0].transactionInfos.size, 1)
+
+        // Make sure that round-tripping of entry CamtBankAccountEntry JSON 
works
+        for (entry in r.reports.flatMap { it.entries }) {
+            val txStr = 
jacksonObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(entry)
             println(txStr)
-            val tx2 = jacksonObjectMapper().readValue(txStr, 
BankTransaction::class.java)
+            val tx2 = jacksonObjectMapper().readValue(txStr, 
CamtBankAccountEntry::class.java)
             val tx2Str = 
jacksonObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(tx2)
             assertEquals(jacksonObjectMapper().readTree(txStr), 
jacksonObjectMapper().readTree(tx2Str))
         }
diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt 
b/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt
index cc31bbb..b2f5369 100644
--- a/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt
+++ b/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt
@@ -315,4 +315,4 @@ fun dbCreateTables(dbName: String) {
             BankAccountsTable
         )
     }
-}
\ No newline at end of file
+}

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]