MIDDLEWARE SERVICE TRANSLATOR
JSON TO ISO8583
USING SPRING BOOT JPOS
First, what is Jpos? Jpos is a framework for creating ISO8583-based applications.
Then what is iso8583? Iso8583 is an internasional standart for financial transaction card originated messaging.
Here, I want to share my experience in creating a middleware service to translate from rest (json) to iso8583 message.
What to prepare before starting
- check_circle Girlfriend (not found)
- check_circle Most beautifull IDE (IntelIj Idea)
- check_circle Java 17
- check_circle Maven
- check_circle Postman
- check_circle Internet
- check_circle Coffee (optional)
Initiate Project
Go to Spring Initialzr Spring Initializr then create new project.

with the following dependencies pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.jpos.ee</groupId>
<artifactId>jposee-qrest</artifactId>
<version>2.2.7</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-codec-http</artifactId>
<version>4.1.111.Final</version>
</dependency>
<dependency>
<groupId>org.jpos</groupId>
<artifactId>jpos</artifactId>
<version>2.1.7</version>
</dependency>
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.31</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
<dependency>
<groupId>com.github.fge</groupId>
<artifactId>jackson-coreutils</artifactId>
<version>1.8</version>
</dependency>
<dependency>
<groupId>com.github.java-json-tools</groupId>
<artifactId>json-schema-validator</artifactId>
<version>2.2.14</version>
</dependency>
<dependency>
<groupId>org.osgi</groupId>
<artifactId>org.osgi.core</artifactId>
<version>6.0.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>Endpoint
We will create 2 endpoints as an example.
1. Method: GET
URL: /v1/actuator/health
Response:
{
"status": "string"
}2. Method: POST
URL: /v1/check
Request:
{
"primaryAccountNumber": "string",
"trxAmount": "string",
"localDateTime": "string",
"systemTraceAuditNumber": int,
"adminFee": "string",
"acquiringInstitutionId": "string",
"retrievalReferenceNumber": "string",
"terminalId": "string",
"identificationCode": "string",
"terminalOwner": "string",
"terminalCity": "string",
"terminalState": "string",
"terminalCountry": "string",
"currencyCode": "string",
"issuerCode": "string",
"acquirerCode": "string",
"beneficiaryCode": "string",
"phoneNo": "string",
"fromAccountNumber": "string",
"accountType": int,
"accountNumber": "string",
"transactionId": "string"
}Response:
{
"status": "string",
"message": "string",
"responseCode": "string",
"data": {
"accountNumber": "string"
}
}Let's code!
Open the project then create folder cfg, deploy dan log.

Create data element xml, iso87ascii.xml inside folder cfg. Download file here: iso87ascii.xml
Then we will create configuration request and response for the endpoint.
Create folder general at folder cfg. Add file base-isomsg.xml.ftl
<isomsg direction="outgoing">
<field id="0" value="0800"/>
<field id="7" value="0801041229"/>
<field id="11" value="000001"/>
<field id="12" value="041229"/>
<field id="13" value="0801"/>
<field id="70" value="301"/>
</isomsg>Create file request.schema.json at folder general.
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Fundtransfer Request Base Schema",
"type": "object",
"allOf": [{ "$ref": "file:cfg/check/common-request.schema.json" }],
"properties": {
},
"required": [
]
}Create file response.json.ftl
<#import "common-utils" as commonUtils />
{
"status": "${rc.status}",
"message": "${rc.description}",
"responseCode": "${(isoresp.bit39)!}"
}Next create folder health inside cfg, create file response.json.ftl.
<#import "common-utils" as commonUtils />
{
"status": "UP"
}
Before continue to next step, we need to create util for truncate string.
Create file common-utils.ftl inside folder cfg.
<#function substr str startIdx endIdx defaultValue>
<#local startZeroBased = startIdx-1>
<#if str?has_content && str?length gte startZeroBased>
<#return str[startZeroBased..*endIdx]?trim>
</#if>
<#return defaultValue>
</#function>
<#function truncate str startIdx len defaultValue>
<#if str?has_content>
<#local expectedStrLength = startIdx+len-1>
<#if str?length gte expectedStrLength>
<#return str?substring(startIdx-1, expectedStrLength)?trim>
<#elseif str?length gte startIdx>
<#return str?substring(startIdx-1, str?length)?trim>
</#if>
</#if>
<#return defaultValue>
</#function>
<#function truncateUntrim str startIdx len defaultValue>
<#if str?has_content>
<#local expectedStrLength = startIdx+len-1>
<#if str?length gte expectedStrLength>
<#return str?substring(startIdx-1, expectedStrLength)>
<#elseif str?length gte startIdx>
<#return str?substring(startIdx-1, str?length)>
</#if>
</#if>
<#return defaultValue>
</#function>
<#function truncateRest str startIdx defaultValue>
<#if str?has_content && (str?length) gte startIdx>
<#return str?substring(startIdx-1, str?length)?trim>
</#if>
<#return defaultValue>
</#function>
<#function addDecimal amt padLen>
<#local len = padLen-2>
<#return (amt?left_pad(len, '0')+'00')!>
</#function>
<#function removeDecimal amt>
<#return (amt?number?remove_ending("00"))!>
</#function>
<#function reFormatDate dateStr fromPattern toPattern>
<#return (dateStr?date(fromPattern)?string(toPattern))!>
</#function>
<#function reFormatDateTime dateTimeStr toPattern>
<#return (dateStr?datetime("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")?string(toPattern))!>
</#function>
<#function addZero amt len>
<#return (amt?left_pad(len, '0'))!>
</#function>
<#function addSpace amt len>
<#return (amt?right_pad(len))!>
</#function>
<#function removeLeadingZeros val>
<#return (val?matches("^0+(?!$)")) >
</#function>Next create folder check inside cfg. Create file common-request.schema.json
{
"id": "https://ist.id/schemas#common",
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Common JSON-Request Schema",
"type": "object",
"properties": {
"systemTraceAuditNumber": {
"type": "integer",
"minimum": 1,
"maximum": 999999
},
"terminalId": {
"type": "string",
"minLength": 1,
"maxLength": 16
},
"identificationCode": {
"type": "string"
},
"terminalOwner": {
"type": "string"
},
"terminalCity": {
"type": "string"
},
"terminalState": {
"type": "string"
},
"terminalCountry": {
"type": "string"
},
"retrievalReferenceNumber": {
"type": "string",
"minLength": 1,
"maxLength": 12
},
"localDateTime": {
"type": "string",
"format": "date-time"
}
},
"required": [
"systemTraceAuditNumber",
"terminalId",
"identificationCode",
"terminalOwner",
"terminalCity",
"terminalState",
"terminalCountry",
"retrievalReferenceNumber",
"localDateTime"
]
}Create file base-isomsg.xml.ftl
<isomsg direction="outgoing">
<#macro commonBit>
<field id="0" value="0200" />
<field id="2" value="${jsonreq.primaryAccountNumber?left_pad(19, '0')}" />
<field id="4" value="${(jsonreq.trxAmount!'0')?left_pad(10, '0')+'00'}" />
<field id="7" value="${jsonreq.localDateTime?datetime("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")?string('MMddHHmmss')}" />
<field id="11" value="${jsonreq.systemTraceAuditNumber!}" />
<field id="12" value="${jsonreq.localDateTime?datetime("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")?string('HHmmss')}" />
<field id="13" value="${jsonreq.localDateTime?datetime("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")?string('MMdd')}" />
<field id="14" value="${jsonreq.localDateTime?datetime("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")?string('MMdd')}" />
<field id="15" value="${jsonreq.localDateTime?datetime("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")?string('MMdd')}" />
<field id="30" value="${(jsonreq.adminFee!'0')?left_pad(7, '0')+'00'}" />
<field id="32" value="${jsonreq.acquiringInstitutionId!}" />
<field id="37" value="${jsonreq.retrievalReferenceNumber!}" />
<field id="41" value="${jsonreq.terminalId!}" />
<field id="42" value="${jsonreq.identificationCode!}" />
<isomsg id="43">
<field id="1" value="${jsonreq.terminalOwner?right_pad(22)}" />
<field id="2" value="${jsonreq.terminalCity?right_pad(13)}" />
<field id="3" value="${jsonreq.terminalState?right_pad(3)}" />
<field id="4" value="${jsonreq.terminalCountry?right_pad(2)}" />
</isomsg>
<field id="49" value="${jsonreq.currencyCode!}" />
<isomsg id="62">
<field id="1" value="${jsonreq.issuerCode?right_pad(3)}" />
<field id="2" value="${jsonreq.acquirerCode?right_pad(3)}" />
<field id="3" value="${jsonreq.beneficiaryCode?right_pad(3)}" />
</isomsg>
<field id="56" value="${jsonreq.phoneNo}" />
<field id="102" value="${jsonreq.fromAccountNumber?left_pad(19, '0')}" />
</#macro>
</isomsg>Create file check.xml.ftl
<#import "base" as baseMsg />
<isomsg>
<@baseMsg.commonBit />
<isomsg id="3">
<field id="1" value="30" />
<field id="2" value="${jsonreq.accountType}" />
<field id="3" value="00" />
</isomsg>
<isomsg id="48">
<field id="1" value="${jsonreq.accountNumber?right_pad(20)}" />
<field id="2" value="${jsonreq.transactionId?right_pad(10)}" />
</isomsg>
</isomsg>Create file check-request.schema.json
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "/check",
"title": "Get test Request Base Schema",
"type": "object",
"allOf": [{ "$ref": "file:cfg/check/common-request.schema.json" }],
"properties": {
"accountNumber": {
"type": "string"
},
"transactionId": {
"type": "string"
},
"accountType": {
"type": "integer",
"enum": [10, 20]
}
},
"required": [
"accountNumber",
"transactionId",
"accountType"
]
}Create file check-response.json.ftl
<#import "common-utils" as commonUtils />
{
"status": "${rc.status}",
"message": "${rc.description}",
"responseCode": "${(isoresp.bit39)!}",
"data": {
"accountNumber": "${(commonUtils.truncate((isoresp.bit48)!, 1, 3, '')?trim)!}"
}
}Then add configuration for logging, create file 00_logger.xml inside folder deploy.
<?xml version="1.0" encoding="UTF-8"?>
<logger name="Q2" class="org.jpos.q2.qbean.LoggerAdaptor">
<property name="redirect" value="stdout, stderr" />
<log-listener class="org.jpos.util.SimpleLogListener" />
<log-listener class="org.jpos.util.BufferedLogListener">
<property name="max-size" value="100" />
<property name="name" value="logger.Q2.buffered" />
</log-listener>
<log-listener class="org.jpos.util.DailyLogListener">
<property name="window" value="86400" />
<property name="prefix" value="log/server" />
<property name="suffix" value=".log"/>
<property name="date-format" value="-yyyy-MM-dd-HH"/>
<property name="compression-format" value="gzip"/>
</log-listener>
</logger>Add config message converter rest to iso, create file 02_msg_config.xml at folder deploy.
<qbean name="msgconfig" logger="Q2" realm="msg-config" class="id.co.pandz.rest_jposs.qBean.QMessageConfigurator">
<libraries name="base">
<library name="base" template="cfg/check/base-isomsg.xml.ftl" />
<library name="common-utils" template="cfg/common-utils.ftl" />
</libraries>
<messages name="general">
<message code="check"
request-schema="cfg/check/check-request.schema.json"
iso-request="cfg/check/check.xml.ftl"
response-json="cfg/check/check-response.json.ftl" />
</messages>
<messages name="health">
<message code="healthcheck"
iso-request="cfg/general/base-isomsg.xml.ftl"
response-json="cfg/health/response.json.ftl" />
</messages>
</qbean>Add config channel ISO server which we will connect, create file 03_channel.xml
<channel-adaptor name='iso-channel' class="org.jpos.q2.iso.ChannelAdaptor" logger="Q2">
<channel class="org.jpos.iso.channel.ASCIIChannel" packager="org.jpos.iso.packager.GenericPackager" logger="Q2">
<property name="packager-config" value="cfg/iso87ascii.xml" />
<property name="host" value="127.0.0.1" />
<property name="port" value="10001" />
<property name="connect-timeout" value="5000" />
</channel>
<in>iso-client-send</in>
<out>iso-client-receive</out>
<reconnect-delay>10000</reconnect-delay>
</channel-adaptor>Add config mux to hit ISO server, create file 04_mux.xml
<mux name="iso.server" class="org.jpos.q2.iso.QMUX" logger="Q2">
<in>iso-client-receive</in>
<out>iso-client-send</out>
<unhandled>iso-client-unhandled</unhandled>
<ready>iso-channel.ready</ready>
</mux>Add config translation response code, create file 07_rc_mapping.xml
<qbean name="rcmapper" logger="Q2" realm="rc-mapping" class="id.co.pandz.rest_jposs.qBean.QRcMapper">
<response-code source-rc="00" target-rc="00" status="success" description="Transaction Success" />
<response-code source-rc="04" target-rc="04" status="failed" description="Salah PIN ATM" />
<response-code source-rc="09" target-rc="09" status="failed" description="Kartu Tidak Ditemukan" />
<response-code source-rc="10" target-rc="10" status="failed" description="General Error from HSM" />
<response-code source-rc="12" target-rc="12" status="failed" description="Invalid Account" />
</qbean>Add folder constant at /src/main/java/id/co/pandz/rest_jposs. Then create class GeneralConstant
public class GeneralConstant {
public static final String LOG_LEVEL = "LOG_LEVEL";
public static final String ENDPOINT = "ENDPOINT";
public static final String JSON_NODE = "JSON_NODE";
public static final String ISO_REQUEST = "ISO_REQUEST";
public static final String ISO_RESPONSE = "ISO_RESPONSE";
public static final String DETAIL_RC = "DETAIL_RC";
}Create Enum LogLevel
public enum LogLevel {
DEBUG(1),
INFO(2),
WARN(3),
ERROR(4);
public final Integer level;
LogLevel(int level) {
this.level = level;
}
}Create Enum Status
public enum Status {
SUCCESS,
PENDING,
FAILED
}Add folder dto at /src/main/java/id/co/pandz/rest_jposs. Then create class ResponseCodeDetail
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ResponseCodeDetail {
private String sourceRc;
private String targetRc;
private Status status;
private String description;
}Create class ResponseDto
@Data
@Builder
@NoArgsConstructor
public class ResponseDto<T> {
private String status;
private String message;
private String responseCode;
private T data;
public ResponseDto(String status, String message) {
this(status, message, null, null);
}
public ResponseDto(String status, String message, T data) {
this(status, message, null, data);
}
public ResponseDto(String status, String message, String rc) {
this(status, message, rc, null);
}
public ResponseDto(String status, String message, String rc, T data) {
this.status = status;
this.message = message;
this.data = data;
this.responseCode = rc;
}
}Add folder util at /src/main/java/id/co/pandz/rest_jposs. Then create class JsonUtil
public class JsonUtil {
private static ObjectMapper objectMapper;
static {
objectMapper = new ObjectMapper();
}
public static Map<String, Object> convertValueToMap(JsonNode jn) {
return objectMapper.convertValue(jn, new TypeReference<Map<String, Object>>(){});
}
}Create class ISOLogger
public class ISOLogger {
public static void logDebug(Context ctx, String message) {
log(ctx, message, null, LogLevel.DEBUG);
}
public static void logDebug(Context ctx, String message, String tag) {
log(ctx, message, tag, LogLevel.DEBUG);
}
public static void logInfo(Context ctx, String message) {
log(ctx, message, null, LogLevel.INFO);
}
public static void logInfo(Context ctx, String message, String tag) {
log(ctx, message, tag, LogLevel.INFO);
}
public static void logWarn(Context ctx, String message) {
log(ctx, message, null, LogLevel.WARN);
}
public static void logWarn(Context ctx, String message, String tag) {
log(ctx, message, tag, LogLevel.WARN);
}
public static void logError(Context ctx, String message) {
logError(ctx, message, null);
}
public static void logError(Context ctx, String message, Exception ex) {
log(ctx, message, null, LogLevel.ERROR);
if(ex != null)
ctx.log(ex);
}
public static void logError(Context ctx, String message, String tag, Exception ex) {
log(ctx, message, tag, LogLevel.ERROR);
if(ex != null)
ctx.log(ex);
}
private static void log(Context ctx, String message, String tag, LogLevel printLogLevel) {
String configuredLogLevelStr = (String) ctx.get(GeneralConstant.LOG_LEVEL, LogLevel.INFO.toString());
LogLevel configuredLogLevel = LogLevel.valueOf(configuredLogLevelStr);
if(configuredLogLevel.level <= printLogLevel.level) {
LogEvent logEvent = ctx.getLogEvent();
if(StringUtils.isNotBlank(tag))
logEvent.addMessage(tag, LocalDateTime.now() + " ["+printLogLevel+"] "+message);
else
logEvent.addMessage(LocalDateTime.now() + " ["+printLogLevel+"] "+message);
}
}
}Create class MessageUtil
public class MessageUtil {
private static ISOPackager xmlPackager;
static {
try {
xmlPackager = new XMLPackager();
} catch (ISOException e) {
e.printStackTrace();
}
}
private static Response buildFailedResponse(String message, String rc) {
return new Response(HttpResponseStatus.OK,
new ResponseDto<>>("failed", message, rc));
}
private static Response buildPendingResponse(String message) {
return new Response(HttpResponseStatus.OK,
new ResponseDto<>("pending", message));
}
private static <T> Response buildSuccessResponse(String message, T data) {
return new Response(HttpResponseStatus.OK, new ResponseDto<T>("success", message, "00", data));
}
public static void setFailedResponse(Context ctx, String message, String rc) {
ctx.put(org.jpos.qrest.Constants.RESPONSE, MessageUtil.buildFailedResponse(message, rc));
}
public static void setPendingResponse(Context ctx, String message) {
ctx.put(org.jpos.qrest.Constants.RESPONSE, MessageUtil.buildPendingResponse(message));
}
public static <T> void setSuccessResponse(Context ctx, String message, T data) {
ctx.put(org.jpos.qrest.Constants.RESPONSE, MessageUtil.buildSuccessResponse(message, data));
}
public static <T> Response buildResponse(ResponseCodeDetail rcDetail) {
ResponseDto<T> response = new ResponseDto<T>();
response.setStatus(rcDetail.getStatus().name().toLowerCase());
response.setMessage(rcDetail.getDescription());
response.setResponseCode(rcDetail.getTargetRc());
return new Response(HttpResponseStatus.OK, new ResponseDto<T>());
}
public static String toTrxAmount(Long amt, int zeroPadLen) {
Long amtWithDecimal = amt * 100;
return ISOUtil.zeropad(amtWithDecimal, zeroPadLen);
}
/**
*
* @param trxAmount
* @return
*/
public static Long fromTrxAmount(String trxAmount) {
String amtWithoutDecimal = StringUtils.substring(trxAmount, 0, trxAmount.length()-2);
return Long.valueOf(amtWithoutDecimal);
}
public static Object getContextValue(String value, Context context) throws Exception
{
Object formatedValue = StringUtils.defaultString(value);
try {
if (StringUtils.startsWith(value, "JSON_REQ{")) {
formatedValue = getJsonValue(value, context);
}
else if (StringUtils.startsWith(value, "RC_MAP{")) {
String params = StringUtils.substringBetween(value, "RC_MAP{", "}");
ResponseCodeDetail detailRc = context.get(GeneralConstant.DETAIL_RC);
if (detailRc != null) {
if ("status".equals(params)) {
formatedValue = detailRc.getStatus().toString().toLowerCase();
} else if ("description".equals(params)) {
formatedValue = getContextValue(detailRc.getDescription(), context);
}
} else {
formatedValue = "";
}
} else if (StringUtils.startsWith(value, "PATH_PARAM{")) {
Map<String, String> pathParams = context.get(Constants.PATHPARAMS);
String paramName = StringUtils.substringBetween(value, "PATH_PARAM{", "}");
formatedValue = pathParams.get(paramName);
}
else if(StringUtils.startsWith(value, "URL_SEGMENT{")) {
String endpoint = context.get(GeneralConstant.ENDPOINT);
String paramName = StringUtils.substringBetween(value, "URL_SEGMENT{", "}");
URI uri = new URI(endpoint);
String[] segments = uri.getPath().split("/");
if ("first".equals(paramName)) {
formatedValue = segments[0];
} else if ("last".equals(paramName)) {
formatedValue = segments[segments.length-1];
} else {
if (StringUtils.isNumeric(paramName)) {
Integer urlSegment = Integer.valueOf(paramName);
formatedValue = segments[urlSegment];
}
else {
formatedValue = null;
}
}
}
} finally {
ISOLogger.logDebug(context, "GET BIT VALUE FOR [" + value + "]\tRESULT [" + formatedValue + "]");
}
return formatedValue;
}
public static String getIsoValue(String value, Context context) {
ISOMsg isoResp = context.get(GeneralConstant.ISO_RESPONSE);
if(isoResp != null) {
String fieldNo = StringUtils.substringBetween(value, "ISO_RESP{", "}");
if(StringUtils.isNotBlank(fieldNo))
return isoResp.getString(fieldNo);
}
return value;
}
private static Object getJsonValue(String value, Context context) {
JsonNode jsonNode = context.get(GeneralConstant.JSON_NODE);
String fieldName = StringUtils.substringBetween(value, "JSON_REQ{", "}").trim();
JsonNode jsonValue = (fieldName.startsWith("/")) ? jsonNode.at(fieldName) : jsonNode.findValue(fieldName);
if(jsonValue != null && !(jsonValue instanceof NullNode)) {
if(jsonValue instanceof NumericNode)
return jsonValue.asLong();
else if(jsonValue instanceof IntNode)
return jsonValue.asInt();
else
return jsonValue.asText();
}
return "";
}
public static ISOMsg xml2ISOMsg(byte[] contentBytes) throws ISOException {
ISOMsg templateReqMsg = new ISOMsg();
templateReqMsg.setPackager(xmlPackager);
templateReqMsg.unpack(contentBytes);
return templateReqMsg;
}
}Add folder template at /src/main/java/id/co/pandz/rest_jposs. Then create class MessageTemplateLoader
public class MessageTemplateLoader implements TemplateLoader {
private Map<String, File> templateMap = new HashMap<String, File>();
public void putTemplate(String name, File file) {
this.templateMap.put(name, file);
}
@Override
public Object findTemplateSource(String name) throws IOException {
return this.templateMap.get(name);
}
@Override
public long getLastModified(Object templateSource) {
return (AccessController.doPrivileged(new PrivilegedAction<Long>() {
@Override
public Long run() {
return Long.valueOf(((File) templateSource).lastModified());
}
})).longValue();
}
@Override
public Reader getReader(Object templateSource, String encoding) throws IOException {
try {
return AccessController.doPrivileged(new PrivilegedExceptionAction<Reader>() {
@Override
public Reader run() throws IOException {
if (!(templateSource instanceof File)) {
throw new IllegalArgumentException(
"templateSource wasn't a File, but a: " +
templateSource.getClass().getName());
}
return new InputStreamReader(new FileInputStream((File) templateSource), encoding);
}
});
} catch (PrivilegedActionException e) {
throw (IOException) e.getException();
}
}
@Override
public void closeTemplateSource(Object templateSource) throws IOException {
// Do Nothing
}
}Add folder qBean at /src/main/java/id/co/pandz/rest_jposs. Then create interface QMessageConfiguratorMBean
public interface QMessageConfiguratorMBean extends QBeanSupportMBean {
}Create implementation class QMessageConfigurator
public class QMessageConfigurator extends QBeanSupport implements QMessageConfiguratorMBean {
public static final String PREFIX = "msgcfg.";
public static final String ISO_REQUEST = "iso-request";
public static final String RESPONSE_JSON = "response-json";
private freemarker.template.Configuration fcfg;
private MessageTemplateLoader messageTemplateLoader;
private Map<String, JsonSchema> requestSchemaMap = new HashMap<String, JsonSchema>();
public JsonSchema getRequestSchema(String type, String code) {
return requestSchemaMap.get(type+code);
}
public ISOMsg processISORequest(String type, String code, Context ctx) throws Exception {
String renderedMsgStr = process(StringUtils.defaultString(type)+code+ISO_REQUEST, ctx);
return MessageUtil.xml2ISOMsg(renderedMsgStr.getBytes());
}
public String processJSONResponse(String type, String code, Context ctx) throws Exception {
return process(StringUtils.defaultString(type)+code+RESPONSE_JSON, ctx);
}
protected String process(String name, Context ctx) throws Exception {
ISOLogger.logInfo(ctx, "FIND TEMPLATE: " + name);
Template template = fcfg.getTemplate(name);
StringWriter sw = new StringWriter();
try {
template.process(getRenderContext(ctx), sw);
return sw.toString();
} catch (TemplateException e) {
throw e;
}
}
public Map<String,Object> getRenderContext (Context ctx) {
Map<String,Object> rctx = ctx.get(RENDER_CONTEXT);
if (rctx == null) {
rctx = new HashMap<>();
ctx.put (RENDER_CONTEXT, rctx);
}
return rctx;
}
@Override
public void setName(String name) {
NameRegistrar.register(PREFIX+name, this);
}
public static QMessageConfigurator getMessageConfigurator(String name) throws NameRegistrar.NotFoundException {
return NameRegistrar.get(PREFIX+name);
}
@Override
protected void startService() throws Exception {
super.startService();
this.messageTemplateLoader = new MessageTemplateLoader();
this.fcfg = new Configuration(Configuration.VERSION_2_3_31);
this.fcfg.setNumberFormat("computer");
LogEvent logEvent = log.createInfo();
logEvent.addMessage("[INFO] ", "Start Message Configuration");
List<Element> librariesElmList = getPersist().getChildren("libraries");
for (Element librariesElm : librariesElmList) {
String librariesName = librariesElm.getAttributeValue("name");
List<Element> libElmList = librariesElm.getChildren("library");
for (Element msgElm : libElmList) {
String nameVal = msgElm.getAttributeValue("name");
String templateVal = msgElm.getAttributeValue("template");
logEvent.addMessage("[INFO] ", "Name value: " + nameVal);
logEvent.addMessage("[INFO] ", "Template value: " + templateVal);
logEvent.addMessage(librariesName, "PROCESSING LIBRARY ["+nameVal+"] TEMPLATE ["+templateVal+"]");
File libraryFile = new File(templateVal);
if (!libraryFile.exists())
throw new Exception("LIBRARY FILE NOT FOUND ["+templateVal+"]");
this.messageTemplateLoader.putTemplate(nameVal, libraryFile);
}
}
List<Element> messagesElmList = getPersist().getChildren("messages");
for (Element messagesElm : messagesElmList) {
String messagesName = messagesElm.getAttributeValue("name");
List<Element> msgElmList = messagesElm.getChildren("message");
for (Element msgElm : msgElmList) {
String typeVal = msgElm.getAttributeValue("type", "");
String codeVal = msgElm.getAttributeValue("code");
String requestSchemaVal = msgElm.getAttributeValue("request-schema");
String isoRequestVal = msgElm.getAttributeValue("iso-request");
String responseJsonVal = msgElm.getAttributeValue("response-json");
logEvent.addMessage(messagesName, "PROCESSING TEMPLATE ["+typeVal+"."+codeVal+"] "
+ "REQUEST-SCHEMA ["+requestSchemaVal+"] "
+ "ISO-REQUEST ["+isoRequestVal+"] "
+ "RESPONSE-JSON ["+responseJsonVal+"]");
if(StringUtils.isAnyBlank(codeVal, isoRequestVal, responseJsonVal)) {
throw new Exception("INVALID CONFIGURATION FOR "+messagesName +" ATTRIBUTE [code, iso-request, response-json] IS MANDATORY");
}
if (StringUtils.isNotBlank(requestSchemaVal)) {
File requestSchemaFile = new File(requestSchemaVal);
if (requestSchemaFile.exists()) {
JsonNode nodeSchema = JsonLoader.fromFile(requestSchemaFile);
JsonSchema jsonSchema = JsonSchemaFactory.byDefault().getJsonSchema(nodeSchema);
requestSchemaMap.put(typeVal+codeVal, jsonSchema);
}
}
File isoRequestFile = new File(isoRequestVal);
if (!isoRequestFile.exists())
throw new Exception("FILE NOT FOUND ["+isoRequestVal+"]");
File responseJsonFile = new File(responseJsonVal);
if (!responseJsonFile.exists())
throw new Exception("FILE NOT FOUND ["+responseJsonVal+"]");
this.messageTemplateLoader.putTemplate(typeVal+codeVal+ISO_REQUEST, isoRequestFile);
this.messageTemplateLoader.putTemplate(typeVal+codeVal+RESPONSE_JSON, responseJsonFile);
}
}
fcfg.setTemplateLoader(this.messageTemplateLoader);
String name = getPersist().getAttributeValue("name");
if(StringUtils.isNotBlank(name))
setName(name);
Logger.log(logEvent);
}
@Override
protected void stopService() throws Exception {
super.stopService();
String name = getPersist().getAttributeValue("name");
NameRegistrar.unregister(PREFIX+name);
}
}Create interface QRcMapperMBean
public interface QRcMapperMBean extends QBeanSupportMBean {
ResponseCodeDetail getDetailRC(String sourceRc, Context ctx);
boolean containsKey(String sourceRc);
}Create implementation class QRcMapper
public class QRcMapper extends QBeanSupport implements QRcMapperMBean {
private Map<String, ResponseCodeDetail> rcMapping = new HashMap<String, ResponseCodeDetail>();
@Override
protected void startService() throws Exception {
super.startService();
List<Element> responseCodeElmList = getPersist().getChildren("response-code");
for (Element responseCodeElm : responseCodeElmList) {
String sourceRc = responseCodeElm.getAttributeValue("source-rc");
String targetRc = responseCodeElm.getAttributeValue("target-rc");
String status = responseCodeElm.getAttributeValue("status");
String description = responseCodeElm.getAttributeValue("description");
rcMapping.put(sourceRc, new ResponseCodeDetail(sourceRc, targetRc, Status.valueOf(status.toUpperCase()), description));
}
String name = getPersist().getAttributeValue("name");
if(StringUtils.isNotBlank(name))
setName(name);
}
@Override
public void setName(String name) {
NameRegistrar.register("rcmapper." + name, this);
}
@Override
protected void stopService() throws Exception {
super.stopService();
String name = getPersist().getAttributeValue("name");
NameRegistrar.unregister("rcmapper." + name);
}
public static QRcMapper getRcMapper(String name) {
return (QRcMapper) NameRegistrar.get("rcmapper." + name, 3000l);
}
@Override
public boolean containsKey(String sourceRc) {
return rcMapping.containsKey(sourceRc);
}
@Override
public ResponseCodeDetail getDetailRC(String sourceRc, Context ctx) {
if(containsKey(sourceRc))
return rcMapping.get(sourceRc);
ISOLogger.logWarn(ctx, "RC MAPPING FOR [" + sourceRc + "] NOT FOUND");
return ResponseCodeDetail.builder()
.description("RC MAPPING FOR [" + sourceRc + "] NOT FOUND")
.status(Status.FAILED)
.targetRc(sourceRc)
.build();
}
}Add folder participant at /src/main/java/id/co/pandz/rest_jposs. Then create class ISOMsgBuilder
public class ISOMsgBuilder implements TransactionParticipant, Configurable {
private QMessageConfigurator messageConfigurator;
private String code;
private String type;
@Override
public int prepare(long id, Serializable context) {
Context ctx = (Context) context;
try {
ISOLogger.logInfo(ctx, "CONSTRUCT ISOMSG START");
String keyValue = (String) MessageUtil.getContextValue(this.code, ctx);
ISOMsg isoMsg = messageConfigurator.processISORequest(this.type, keyValue, ctx);
Map<String, Object> renderContext = messageConfigurator.getRenderContext(ctx);
for (int i = 0; i <= isoMsg.getMaxField(); i++) {
if (!isoMsg.hasField(i))
continue;
Object value = isoMsg.getValue(i);
// update sub ISOMsg to flat String
if (value instanceof ISOMsg) {
ISOMsg subMsg = (ISOMsg) value;
int maxFieldSubMsg = subMsg.getMaxField();
StringBuilder sb = new StringBuilder();
for (int j = 1; j <= maxFieldSubMsg; j++) {
sb.append(subMsg.getString(j));
}
String valueStr = sb.toString();
isoMsg.set(i, valueStr);
renderContext.put("isoreq.bit"+i, valueStr);
}
else {
renderContext.put("isoreq.bit"+i, value);
}
}
ctx.put(GeneralConstant.ISO_REQUEST, isoMsg);
return PREPARED;
} catch (Exception e) {
ISOLogger.logError(ctx, "ERROR CONSTRUCT ISOMSG", e);
MessageUtil.setFailedResponse(ctx, e.getMessage(), null);
} finally {
ISOLogger.logInfo(ctx, "CONSTRUCT ISOMSG DONE");
}
return ABORTED;
}
@Override
public void setConfiguration(Configuration cfg) throws ConfigurationException {
try {
this.code = cfg.get("code");
this.type = cfg.get("type");
this.messageConfigurator = QMessageConfigurator.getMessageConfigurator(cfg.get("messageConfigurator"));
} catch (NameRegistrar.NotFoundException e) {
throw new ConfigurationException(e);
}
}
}Create class JsonRequestValidator
public class JsonRequestValidator implements TransactionParticipant, Configurable {
private QMessageConfigurator messageConfigurator;
private String code;
private String type;
@Override
public int prepare(long id, Serializable context) {
Context ctx = (Context) context;
try {
String jsonRequest = ctx.getString(JSON_REQUEST.name());
JsonNode node = JsonLoader.fromString(jsonRequest);
String keyValue = (String) MessageUtil.getContextValue(this.code, ctx);
ISOLogger.logInfo(ctx, "GET SCHEMA TYPE: " + this.type);
ISOLogger.logInfo(ctx, "GET SCHEMA VALUE: " + keyValue);
JsonSchema schema = messageConfigurator.getRequestSchema(this.type, keyValue);
if(schema != null) {
// ProcessingReport report = schema.validate(node);
// if (!report.isSuccess()) {
// ctx.getResult().fail(ResultCode.BAD_REQUEST, Caller.info(), report.toString());
// ISOLogger.logError(ctx, report.toString());
// return ABORTED;
// }
}
storeJsonRequestToRenderContext(node, ctx);
ctx.put(GeneralConstant.JSON_NODE, node);
return PREPARED;
} catch (Exception e) {
ISOLogger.logError(ctx, Caller.info(), e);
ctx.getResult().fail(ResultCode.BAD_REQUEST, Caller.info(), e.toString());
}
return ABORTED;
}
private void storeJsonRequestToRenderContext(JsonNode node, Context ctx) {
Map<String, Object> renderContext = messageConfigurator.getRenderContext(ctx);
Map<String, Object> jsonReqContext = JsonUtil.convertValueToMap(node);
renderContext.put("jsonreq", jsonReqContext);
renderContext.put("endpoint", ctx.get(GeneralConstant.ENDPOINT));
renderContext.put("pathparam", ctx.get(PATHPARAMS));
renderContext.put("queryparam", ctx.get(QUERYPARAMS));
}
@Override
public void setConfiguration(Configuration cfg) throws ConfigurationException {
try {
this.code = cfg.get("code");
this.type = cfg.get("type");
this.messageConfigurator = QMessageConfigurator.getMessageConfigurator(cfg.get("messageConfigurator"));
} catch (NameRegistrar.NotFoundException e) {
throw new ConfigurationException(e);
}
}
}Create class JsonResponseBuilder
public class JsonResponseBuilder extends Log implements TransactionParticipant, Configurable {
private QMessageConfigurator messageConfigurator;
private String code;
private String type;
private String rcMapper;
@Override
public int prepare(long id, Serializable context) {
Context ctx = (Context) context;
try {
ISOLogger.logInfo(ctx, "JSON RESPONSE BUILDER START");
String keyValue = (String) MessageUtil.getContextValue(this.code, ctx);
String jsonRespString = messageConfigurator.processJSONResponse(type, keyValue, ctx);
ISOLogger.logDebug(ctx, jsonRespString);
ctx.put(Constants.RESPONSE, new Response(HttpResponseStatus.OK, jsonRespString));
} catch (Exception e) {
ISOLogger.logError(ctx, "JSON RESPONSE BUILDER ERROR", e);
ISOMsg isoRespMsg = ctx.get(GeneralConstant.ISO_RESPONSE);
if (isoRespMsg != null) {
// Already got response from esb but there is exception when build json response
QRcMapper qrcMapper = QRcMapper.getRcMapper(rcMapper);
ResponseCodeDetail detailRC = qrcMapper.getDetailRC(isoRespMsg.getString(39), ctx);
ctx.put(org.jpos.qrest.Constants.RESPONSE, MessageUtil.buildResponse(detailRC));
}
} finally {
ISOLogger.logInfo(ctx, "JSON RESPONSE BUILDER DONE");
}
return PREPARED;
}
@Override
public void setConfiguration(Configuration cfg) throws ConfigurationException {
try {
this.code = cfg.get("code");
this.type = cfg.get("type");
this.messageConfigurator = QMessageConfigurator.getMessageConfigurator(cfg.get("messageConfigurator"));
this.rcMapper = cfg.get("rcMapper", "rcmapper");
} catch (NameRegistrar.NotFoundException e) {
throw new ConfigurationException(e);
}
}
}Create class LogPrinterParticipant
public class LogPrinterParticipant implements AbortParticipant {
@Override
public int prepare(long id, Serializable o) {
return PREPARED | READONLY;
}
@Override
public void commit(long id, Serializable context) {
Context ctx = (Context) context;
Logger.log(ctx.getLogEvent());
}
@Override
public void abort(long id, Serializable context) {
Context ctx = (Context) context;
Logger.log(ctx.getLogEvent());
}
}Create class SendIsoRequest
public class SendIsoRequest extends Log implements TransactionParticipant, Configurable {
private MUX mux;
private Long timeout;
private String rcMapper;
@Override
public int prepare(long id, Serializable context) {
Context ctx = (Context) context;
QRcMapper qrcMapper = QRcMapper.getRcMapper(rcMapper);
Map<String, String> rcMap = new HashMap<String, String>();
Map<String,Object> renderContext = ctx.get(RENDER_CONTEXT);
try {
ISOLogger.logInfo(ctx, "SEND ISO REQUEST START");
ISOMsg reqMsg = ctx.get(GeneralConstant.ISO_REQUEST);
ISOLogger.logInfo(ctx, "SEND ISO REQUEST SENT");
ISOMsg respMsg = mux.request(reqMsg, timeout);
ISOLogger.logInfo(ctx, "SEND ISO RESPONSE RECEIVED");
if (respMsg != null) {
ctx.put(GeneralConstant.ISO_RESPONSE, respMsg);
Map<String, String> isorespMap = getIsoRespContext(renderContext);
int maxField = respMsg.getMaxField();
for (int i = 1; i <= maxField; i++) {
if(!respMsg.hasField(i))
continue;
String bitValue = respMsg.getString(i);
isorespMap.put("bit" + i, bitValue);
}
ResponseCodeDetail detailRC = qrcMapper.getDetailRC(respMsg.getString(39), ctx);
rcMap.put("status", detailRC.getStatus().name().toLowerCase());
rcMap.put("description", MessageUtil.getIsoValue(detailRC.getDescription(), ctx));
renderContext.put("rc", rcMap);
renderContext.put("isoresp", isorespMap);
} else {
rcMap.put("status", "pending");
rcMap.put("description", "Timeout");
}
} catch (Exception e) {
rcMap.put("status", "failed");
rcMap.put("description", "Internal Error");
ISOLogger.logError(ctx, "SEND ISO REQUEST ERROR", e);
} finally {
ISOLogger.logInfo(ctx, "SEND ISO REQUEST DONE");
}
renderContext.put("rc", rcMap);
return PREPARED;
}
@Override
public void setConfiguration(Configuration cfg) throws ConfigurationException {
try {
String muxName = cfg.get("muxName", "esb");
Long timeout = cfg.getLong("timeout", 60000);
this.rcMapper = cfg.get("rcMapper", "rcmapper");
this.mux = QMUX.getMUX(muxName);
this.timeout = timeout;
} catch (NameRegistrar.NotFoundException e) {
throw new ConfigurationException(e);
}
}
@SuppressWarnings("unchecked")
private Map<String, String> getIsoRespContext(Map<String, Object> renderContext) {
return (Map<String, String>) renderContext.getOrDefault("isoresp", new HashMap<String, String>());
}
}Oke next we need to create class that listen incoming request. Create class RequestListener at /src/main/java/id/co/pandz/rest_jposs.
public class RequestListener implements ISORequestListener, Configurable {
private String queueName;
private Space<String, Context> sp;
private Long timeout;
@Override
public boolean process(ISOSource isoSrc, ISOMsg isoMsg) {
Context ctx = new Context();
ctx.put(ContextConstants.REQUEST.toString(), isoMsg);
ctx.put(ContextConstants.SOURCE.toString(), isoSrc);
sp.out(queueName, ctx, timeout);
return false;
}
@Override
@SuppressWarnings("unchecked")
public void setConfiguration(Configuration cfg)
{
queueName = cfg.get("queue");
timeout = cfg.getLong("timeout");
sp = SpaceFactory.getSpace(cfg.get("space"));
}
}Create class RestServer
public class RestServer extends org.jpos.qrest.RestServer {
private String logLevel;
@Override
public void queue(FullHttpRequest request, Context ctx) {
ctx.put(GeneralConstant.LOG_LEVEL, logLevel.toUpperCase());
super.queue(request, ctx);
}
@Override
public void setConfiguration(Configuration cfg) throws ConfigurationException {
super.setConfiguration(cfg);
try {
logLevel = cfg.get("logLevel", "info");
} catch (Exception e) {
getLog().error(e);
}
}
}Create class Router which will identify requests coming into the service.
public class Router implements GroupSelector, XmlConfigurable {
private Map<String, List<Route<String>>> routes = new HashMap<>();
@Override
public int prepare(long id, Serializable context) {
return PREPARED | NO_JOIN | READONLY;
}
@SuppressWarnings("rawtypes")
@Override
public String select(long id, Serializable context) {
Context ctx = (Context) context;
FullHttpRequest request = (FullHttpRequest) ctx.get(REQUEST);
ISOLogger.logInfo(ctx, "Method: " + request.method().name());
ISOLogger.logDebug(ctx, request.content().toString(CharsetUtil.UTF_8));
List<Route<String>> routesByMethod = routes.get(request.method().name());
QueryStringDecoder decoder = new QueryStringDecoder(request.uri());
if (!decoder.parameters().isEmpty())
ctx.put(QUERYPARAMS, decoder.parameters());
if (routesByMethod != null) {
Optional<Route<String>> route = routesByMethod.stream().filter(r -> r.matches(decoder.uri())).findFirst();
String path = URI.create(decoder.uri()).getPath();
ctx.put(GeneralConstant.ENDPOINT, path);
if (route.isPresent()) {
Map m = route.get().parameters(path);
if (m != null)
ctx.put(PATHPARAMS, m);
ISOLogger.logInfo(ctx, "Routes: " + path);
ctx.getLogEvent().setTag(path.substring(1).replace('/', '_'));
return route.get().apply(route.get(), path);
}
}
return null;
}
@Override
public void setConfiguration(Element e) throws ConfigurationException {
try {
for (Element r : e.getChildren("route")) {
routes.computeIfAbsent(
r.getAttributeValue("method"),
k -> new ArrayList<>()).add(
new Route<>(
r.getAttributeValue("path"),
r.getAttributeValue("method"),
(t, s) -> r.getAttributeValue("name"))
);
}
} catch (Throwable t) {
throw new ConfigurationException(t);
}
}
}Create class Q2 server. Download file here: Q2.java
Back to folder deploy, add more config xml. Config request listener, create file 05_isoserver.xml
<server class="org.jpos.q2.iso.QServer" logger="Q2" name="server-simulator">
<attr name="port" type="java.lang.Integer">20801</attr>
<attr name="maxSessions" type="java.lang.Integer">500</attr>
<channel class="org.jpos.iso.channel.ASCIIChannel" packager="org.jpos.iso.packager.GenericPackager"
logger="Q2" name="server-channel">
<property name="packager-config" value="cfg/iso87ascii.xml" />
</channel>
<request-listener class="id.co.pandz.rest_jposs.RequestListener" logger="Q2">
<property name="space" value="TransactionSpace"/>
<property name="queue" value="TransactionQueue"/>
<property name="timeout" value="30000"/>
</request-listener>
</server>Create file 05_rest_server.xml
<qrest name="restserver" class='id.co.pandz.rest_jposs.RestServer' logger='Q2'>
<property name='port' value='8080' />
<property name='queue' value='ISO.TXNMGR' />
<property name="TLS" value="false" />
<property name="server-auth" value="false" />
<property name="client-auth" value="false" />
<property name="logLevel" value="debug" />
<route path="/v1/actuator/health" method="GET" queue="ISO.TXNMGR" />
<route path="/v1/check" method="POST" queue="ISO.TXNMGR" />
</qrest>Add config grouping participant process, create file 06_rest_txnmanager.xml
<txnmgr name="iso.default" class="org.jpos.transaction.TransactionManager" logger="Q2">
<property name="queue" value="ISO.TXNMGR" />
<property name="sessions" value="10" />
<property name="max-sessions" value="512" />
<property name="debug" value="false" />
<participant class="id.co.pandz.rest_jposs.Router">
<route path="/v1/check" method="POST" name="Check"/>
<route path="/v1/actuator/health" method="GET" name="HealthCheck"/>
</participant>
<group name="Check">
<participant class="org.jpos.qrest.ExtractJSONRequest" />
<participant class="id.co.pandz.rest_jposs.participant.JsonRequestValidator">
<property name="code" value="check" />
<property name="messageConfigurator" value="msgconfig" />
</participant>
<participant class="id.co.pandz.rest_jposs.participant.ISOMsgBuilder">
<property name="code" value="URL_SEGMENT{last}" />
<property name="messageConfigurator" value="msgconfig" />
</participant>
<participant class="id.co.pandz.rest_jposs.participant.SendIsoRequest">
<property name="muxName" value="iso.server"></property>
<property name="timeout" value="60000"></property>
<property name="rcMapper" value="rcmapper"></property>
</participant>
<participant class="id.co.pandz.rest_jposs.participant.JsonResponseBuilder">
<property name="code" value="URL_SEGMENT{last}" />
<property name="messageConfigurator" value="msgconfig" />
<property name="rcMapper" value="rcmapper"></property>
</participant>
</group>
<group name="HealthCheck">
<participant class="id.co.pandz.rest_jposs.participant.JsonResponseBuilder">
<property name="code" value="healthcheck" />
<property name="messageConfigurator" value="msgconfig" />
<property name="rcMapper" value="rcmapper"></property>
</participant>
</group>
<participant class="org.jpos.qrest.SendResponse" logger="Q2">
<property name="content-type" value="application/json"></property>
</participant>
<
</txnmgr>Create file 20_tnxmanager.xml
<txnmgr name="transactionManager" logger="Q2" class="org.jpos.transaction.TransactionManager">
<property name="space" value="TransactionSpace" />
<property name="queue" value="TransactionQueue" />
<property name="sessions" value="1" />
<property name="max-sessions" value="10" />
<property name="debug" value="false" />
<participant class="org.jpos.transaction.participant.SendResponse" logger="Q2" />
</txnmgr>Finally, run Q2 server in our main class.
@SpringBootApplication
public class RestJpossApplication {
public static void main(String[] args) {
Q2 appServer = new Q2(new String[] {"-r"});
appServer.start();
System.out.println("ISO8583 server started...");
}
}Clone ISO server
Before we test it, we need server ISO, refer from this: Create ISO server using Q2 Jpos
Run service
Oke next, run ISO server and rest middleware service.


Test
Hit it with postman


Enyoiii yeee.
Any question? Don't ask me. Remember Aldi Taher's word? Everyone in this world is confused.
You can find complete source code here: https://github.com/pandz3rd/middleware-translator-json-to-iso-jpos