From 9ac2e50587e8d756f36630bc8181034cba000056 Mon Sep 17 00:00:00 2001 From: Konstantin Pavlov Date: Tue, 8 Feb 2022 23:13:50 +0200 Subject: [PATCH] #134 Fix message origin of heartbeat message (#146) * :sparkles: #134 Fix message origin of heartbeat message - Require MessageOrigin to construct J8583MessageFactory - IdleEventHandler sends message with default MessageOrigin * :gem: dependency definitions --- README.md | 16 ++++++----- build.gradle.kts | 26 ++++++++++-------- .../jreactive8583/iso/J8583MessageFactory.kt | 20 ++++++++++++-- .../kpavlov/jreactive8583/iso/MessageClass.kt | 16 +++++------ .../jreactive8583/iso/MessageFactory.kt | 9 +++++++ .../netty/pipeline/IdleEventHandler.kt | 27 ++++++++++--------- .../example/client/Iso8583ClientConfig.java | 8 +++--- .../example/server/Iso8583ServerConfig.java | 3 ++- .../pipeline/ParseExceptionHandlerTest.java | 3 ++- 9 files changed, 83 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index afcd461..eaef047 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ Now you may use ISO8583 client or server in your code. The minimal client workflow includes: ~~~java -var messageFactory = new J8583MessageFactory<>();// [1] +var messageFactory = new J8583MessageFactory<>(ISO8583Version.V1987, MessageOrigin.OTHER);// [1] Iso8583Client client = new Iso8583Client<>(messageFactory);// [2] client.addMessageListener(new IsoMessageListener() { // [3] @@ -88,12 +88,15 @@ if (client.isConnected()) { // [7] client.shutdown();// [11] ~~~ -1. First you need to create a `MessageFactory` -2. Then you create a [`Iso8583Client`][Iso8583Client] providing `MessageFactory` and, optionally, `SocketAddress` +1. First you need to create a `MessageFactory`. You MUST specify a role of your client for + originated messages, e.g. `ACQUIRER`, `ISSUER` or `OTHER`. +2. Then you create a [`Iso8583Client`][Iso8583Client] providing `MessageFactory` and, + optionally, `SocketAddress` 3. Add one or more custom [`IsoMessageListener`][IsoMessageListener]s to handle `IsoMessage`s. 4. Configure the client. You may omit this step if you're fine with default configuration. 5. Initialize a client. Now it is ready to connect. -6. Establish a connection. By default, if connection will is lost, it reconnects automatically. You may disable this behaviour or change _reconnectInterval_. +6. Establish a connection. By default, if connection will is lost, it reconnects automatically. You + may disable this behaviour or change _reconnectInterval_. 7. Verify that connection is established 8. Send `IsoMessage` asynchronously 9. Send `IsoMessage` synchronously @@ -105,7 +108,7 @@ client.shutdown();// [11] Typical server workflow includes: ~~~java -var messageFactory = new J8583MessageFactory<>(ConfigParser.createDefault(), ISO8583Version.V1987);// [1] +var messageFactory = new J8583MessageFactory<>(ConfigParser.createDefault(), ISO8583Version.V1987, MessageOrigin.ACQUIRER);// [1] Iso8583Server server = new Iso8583Server<>(port, messageFactory);// [2] server.addMessageListener(new IsoMessageListener() { // [3] @@ -123,7 +126,8 @@ if (server.isStarted()) { // [7] server.shutdown();// [8] ~~~ -1. First you need to create a `MessageFactory` +1. First you need to create a `MessageFactory`. You MUST specify a role of your server for + originated messages, e.g. `ACQUIRER`, `ISSUER` or `OTHER`. 2. Then you create a [`Iso8583Server`][Iso8583Server] providing `MessageFactory` and port to bind to 3. Add one or more custom [`IsoMessageListener`][IsoMessageListener]s to handle `IsoMessage`s. 4. Configure the server. You may omit this step if you're fine with default configuration. diff --git a/build.gradle.kts b/build.gradle.kts index 53ce7e8..d25c670 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -18,23 +18,27 @@ repositories { } dependencies { + val slf4jVersion = "1.7.35" + val junitJupiterVersion = "5.8.2" + implementation(platform("org.jetbrains.kotlin:kotlin-bom")) - implementation(kotlin("stdlib-jdk8")) - implementation("net.sf.j8583:j8583:1.17.0") - implementation("io.netty:netty-handler:4.1.73.Final") - implementation("org.slf4j:slf4j-api:1.7.35") - implementation("com.google.code.findbugs:jsr305:3.0.2") - testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.8.2") - testImplementation("org.junit.jupiter:junit-jupiter-params:5.8.2") + api(kotlin("stdlib-jdk8")) + api("net.sf.j8583:j8583:1.17.0") + api("io.netty:netty-handler:4.1.73.Final") + api("org.slf4j:slf4j-api:$slf4jVersion") + api("com.google.code.findbugs:jsr305:3.0.2") + testImplementation(kotlin("test-junit5")) + testImplementation("org.junit.jupiter:junit-jupiter-params:$junitJupiterVersion") testImplementation("org.mockito:mockito-junit-jupiter:4.3.1") testImplementation("org.apache.commons:commons-lang3:3.12.0") testImplementation("org.assertj:assertj-core:3.22.0") - testImplementation("org.springframework:spring-context:5.3.15") - testImplementation("org.springframework:spring-test:5.3.15") - testImplementation("org.slf4j:slf4j-simple:1.7.35") + testImplementation(platform("org.springframework:spring-framework-bom:5.3.15")) + testImplementation("org.springframework:spring-context") + testImplementation("org.springframework:spring-test") + testImplementation("org.slf4j:slf4j-simple:$slf4jVersion") testImplementation("net.jcip:jcip-annotations:1.0") testImplementation("org.awaitility:awaitility:4.1.1") - testImplementation(kotlin("test-junit5")) + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:$junitJupiterVersion") } group = "com.github.kpavlov.jreactive8583" diff --git a/src/main/kotlin/com/github/kpavlov/jreactive8583/iso/J8583MessageFactory.kt b/src/main/kotlin/com/github/kpavlov/jreactive8583/iso/J8583MessageFactory.kt index 0cbb441..89e4971 100644 --- a/src/main/kotlin/com/github/kpavlov/jreactive8583/iso/J8583MessageFactory.kt +++ b/src/main/kotlin/com/github/kpavlov/jreactive8583/iso/J8583MessageFactory.kt @@ -7,12 +7,20 @@ import java.io.UnsupportedEncodingException import java.text.ParseException import javax.annotation.Nonnull +/** + * @param role Role of the communicating party. + * @see MessageOrigin + */ public open class J8583MessageFactory @JvmOverloads constructor( private val messageFactory: com.solab.iso8583.MessageFactory = defaultMessageFactory(), - private val isoVersion: ISO8583Version = ISO8583Version.V1987 + private val isoVersion: ISO8583Version = ISO8583Version.V1987, + private val role: MessageOrigin ) : MessageFactory { - public constructor(isoVersion: ISO8583Version) : this(defaultMessageFactory(), isoVersion) + public constructor( + isoVersion: ISO8583Version, + role: MessageOrigin + ) : this(defaultMessageFactory(), isoVersion, role) override fun newMessage(type: Int): T { return messageFactory.newMessage(type) @@ -26,6 +34,13 @@ public open class J8583MessageFactory @JvmOverloads constructor( return newMessage(mtiValue(isoVersion, messageClass, messageFunction, messageOrigin)) } + override fun newMessage( + @Nonnull messageClass: MessageClass, + @Nonnull messageFunction: MessageFunction + ): T { + return newMessage(mtiValue(isoVersion, messageClass, messageFunction, this.role)) + } + override fun createResponse(requestMessage: T): T { return messageFactory.createResponse(requestMessage) } @@ -49,5 +64,6 @@ public open class J8583MessageFactory @JvmOverloads constructor( } } +@Suppress("UNCHECKED_CAST") private fun defaultMessageFactory() = ConfigParser.createDefault() as com.solab.iso8583.MessageFactory diff --git a/src/main/kotlin/com/github/kpavlov/jreactive8583/iso/MessageClass.kt b/src/main/kotlin/com/github/kpavlov/jreactive8583/iso/MessageClass.kt index 93070c7..c743306 100644 --- a/src/main/kotlin/com/github/kpavlov/jreactive8583/iso/MessageClass.kt +++ b/src/main/kotlin/com/github/kpavlov/jreactive8583/iso/MessageClass.kt @@ -10,7 +10,7 @@ package com.github.kpavlov.jreactive8583.iso @Suppress("unused") public enum class MessageClass(internal val value: Int) { /** - * x1xx Authorization message + * `x1xx` - Authorization message * * * Determine if funds are available, get an approval but do not post @@ -20,7 +20,7 @@ public enum class MessageClass(internal val value: Int) { AUTHORIZATION(0x0100), /** - * x2xx Financial messages + * `x2xx` - Financial messages * * * Determine if funds are available, get an approval and post directly @@ -29,7 +29,7 @@ public enum class MessageClass(internal val value: Int) { FINANCIAL(0x0200), /** - * x3xx File actions message + * `x3xx` - File actions message * * * Used for hot-card, TMS and other exchanges @@ -37,7 +37,7 @@ public enum class MessageClass(internal val value: Int) { FILE_ACTIONS(0x0300), /** - * x4xx Reversal and chargeback messages + * `x4xx` Reversal and chargeback messages * * * - Reversal (x4x0 or x4x1): Reverses the action of a previous authorization. @@ -46,14 +46,14 @@ public enum class MessageClass(internal val value: Int) { REVERSAL_CHARGEBACK(0x0400), /** - * x5xx Reconciliation message + * `x5xx` - Reconciliation message + * * Transmits settlement information message. */ RECONCILIATION(0x0500), /** - * x6xx Administrative message - * + * `x6xx` - Administrative message * * Transmits administrative advice. Often used for failure messages * (e.g., message reject or failure to apply). @@ -61,7 +61,7 @@ public enum class MessageClass(internal val value: Int) { ADMINISTRATIVE(0x0600), /** - * x7xx Fee collection messages + * `x7xx` - Fee collection messages */ FEE_COLLECTION(0x0700), diff --git a/src/main/kotlin/com/github/kpavlov/jreactive8583/iso/MessageFactory.kt b/src/main/kotlin/com/github/kpavlov/jreactive8583/iso/MessageFactory.kt index 898bd17..f3df0e4 100644 --- a/src/main/kotlin/com/github/kpavlov/jreactive8583/iso/MessageFactory.kt +++ b/src/main/kotlin/com/github/kpavlov/jreactive8583/iso/MessageFactory.kt @@ -6,6 +6,7 @@ import java.io.UnsupportedEncodingException import java.text.ParseException public interface MessageFactory { + public fun newMessage(type: Int): T public fun newMessage( @@ -14,6 +15,14 @@ public interface MessageFactory { messageOrigin: MessageOrigin ): T + /** + * Creates a new message with a default message origin (i.e. role) + */ + public fun newMessage( + messageClass: MessageClass, + messageFunction: MessageFunction + ): T + public fun createResponse(requestMessage: T): T public fun createResponse(request: T, copyAllFields: Boolean): T diff --git a/src/main/kotlin/com/github/kpavlov/jreactive8583/netty/pipeline/IdleEventHandler.kt b/src/main/kotlin/com/github/kpavlov/jreactive8583/netty/pipeline/IdleEventHandler.kt index 4a6fcc8..edbaf51 100644 --- a/src/main/kotlin/com/github/kpavlov/jreactive8583/netty/pipeline/IdleEventHandler.kt +++ b/src/main/kotlin/com/github/kpavlov/jreactive8583/netty/pipeline/IdleEventHandler.kt @@ -3,32 +3,33 @@ package com.github.kpavlov.jreactive8583.netty.pipeline import com.github.kpavlov.jreactive8583.iso.MessageClass import com.github.kpavlov.jreactive8583.iso.MessageFactory import com.github.kpavlov.jreactive8583.iso.MessageFunction -import com.github.kpavlov.jreactive8583.iso.MessageOrigin -import com.solab.iso8583.IsoMessage import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelInboundHandlerAdapter import io.netty.handler.timeout.IdleState import io.netty.handler.timeout.IdleStateEvent -internal class IdleEventHandler( - private val isoMessageFactory: MessageFactory +/** + * IdleEventHandler sends heartbeats (administrative messages) when channel becomes idle, + * i.e. `IdleStateEvent` is received. + */ +internal class IdleEventHandler( + private val isoMessageFactory: MessageFactory ) : ChannelInboundHandlerAdapter() { override fun userEventTriggered(ctx: ChannelHandlerContext, evt: Any) { - if (evt is IdleStateEvent) { - if (evt.state() == IdleState.READER_IDLE || evt.state() == IdleState.ALL_IDLE) { - val echoMessage = createEchoMessage() - ctx.write(echoMessage) - ctx.flush() - } + if (evt is IdleStateEvent && + (evt.state() == IdleState.READER_IDLE || evt.state() == IdleState.ALL_IDLE) + ) { + val heartbeatMessage = createHeartbeatMessage() + ctx.write(heartbeatMessage) + ctx.flush() } } - private fun createEchoMessage(): IsoMessage { + private fun createHeartbeatMessage(): T { return isoMessageFactory.newMessage( MessageClass.NETWORK_MANAGEMENT, - MessageFunction.REQUEST, - MessageOrigin.ACQUIRER + MessageFunction.REQUEST ) } } diff --git a/src/test/java/com/github/kpavlov/jreactive8583/example/client/Iso8583ClientConfig.java b/src/test/java/com/github/kpavlov/jreactive8583/example/client/Iso8583ClientConfig.java index 879995c..4f7c857 100644 --- a/src/test/java/com/github/kpavlov/jreactive8583/example/client/Iso8583ClientConfig.java +++ b/src/test/java/com/github/kpavlov/jreactive8583/example/client/Iso8583ClientConfig.java @@ -5,6 +5,7 @@ import com.github.kpavlov.jreactive8583.iso.ISO8583Version; import com.github.kpavlov.jreactive8583.iso.J8583MessageFactory; import com.github.kpavlov.jreactive8583.iso.MessageFactory; +import com.github.kpavlov.jreactive8583.iso.MessageOrigin; import com.solab.iso8583.IsoMessage; import com.solab.iso8583.impl.SimpleTraceGenerator; import com.solab.iso8583.parse.ConfigParser; @@ -52,8 +53,9 @@ private MessageFactory clientMessageFactory() throws IOException { messageFactory.setCharacterEncoding(StandardCharsets.US_ASCII.name()); messageFactory.setUseBinaryMessages(false); messageFactory.setAssignDate(true); - messageFactory.setTraceNumberGenerator(new SimpleTraceGenerator((int) (System - .currentTimeMillis() % 1000000))); - return new J8583MessageFactory<>(messageFactory, ISO8583Version.V1987); + messageFactory.setTraceNumberGenerator( + new SimpleTraceGenerator((int) (System.currentTimeMillis() % 1000000)) + ); + return new J8583MessageFactory<>(messageFactory, ISO8583Version.V1987, MessageOrigin.OTHER); } } diff --git a/src/test/java/com/github/kpavlov/jreactive8583/example/server/Iso8583ServerConfig.java b/src/test/java/com/github/kpavlov/jreactive8583/example/server/Iso8583ServerConfig.java index d396295..31a81a3 100644 --- a/src/test/java/com/github/kpavlov/jreactive8583/example/server/Iso8583ServerConfig.java +++ b/src/test/java/com/github/kpavlov/jreactive8583/example/server/Iso8583ServerConfig.java @@ -3,6 +3,7 @@ import com.github.kpavlov.jreactive8583.iso.ISO8583Version; import com.github.kpavlov.jreactive8583.iso.J8583MessageFactory; import com.github.kpavlov.jreactive8583.iso.MessageFactory; +import com.github.kpavlov.jreactive8583.iso.MessageOrigin; import com.github.kpavlov.jreactive8583.server.Iso8583Server; import com.github.kpavlov.jreactive8583.server.ServerConfiguration; import com.solab.iso8583.IsoMessage; @@ -40,7 +41,7 @@ private MessageFactory serverMessageFactory() throws IOException { messageFactory.setCharacterEncoding(StandardCharsets.US_ASCII.name()); messageFactory.setUseBinaryMessages(false); messageFactory.setAssignDate(true); - return new J8583MessageFactory<>(messageFactory, ISO8583Version.V1987); + return new J8583MessageFactory<>(messageFactory, ISO8583Version.V1987, MessageOrigin.ACQUIRER); } diff --git a/src/test/java/com/github/kpavlov/jreactive8583/netty/pipeline/ParseExceptionHandlerTest.java b/src/test/java/com/github/kpavlov/jreactive8583/netty/pipeline/ParseExceptionHandlerTest.java index 026add6..644c8e4 100644 --- a/src/test/java/com/github/kpavlov/jreactive8583/netty/pipeline/ParseExceptionHandlerTest.java +++ b/src/test/java/com/github/kpavlov/jreactive8583/netty/pipeline/ParseExceptionHandlerTest.java @@ -3,6 +3,7 @@ import com.github.kpavlov.jreactive8583.iso.ISO8583Version; import com.github.kpavlov.jreactive8583.iso.J8583MessageFactory; import com.github.kpavlov.jreactive8583.iso.MessageFactory; +import com.github.kpavlov.jreactive8583.iso.MessageOrigin; import com.solab.iso8583.IsoMessage; import com.solab.iso8583.IsoType; import com.solab.iso8583.IsoValue; @@ -34,7 +35,7 @@ public class ParseExceptionHandlerTest { @BeforeAll public static void beforeClass() { - messageFactory = new J8583MessageFactory<>(ISO8583Version.V1993); + messageFactory = new J8583MessageFactory<>(ISO8583Version.V1993, MessageOrigin.ACQUIRER); } @BeforeEach