Tutorial AUG 08, 2024 • 12 MIN READ

MIDDLEWARE SERVICE TRANSLATOR
JSON TO ISO8583
USING SPRING BOOT JPOS

Abstract technical background

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.

Initial spring project

with the following dependencies pom.xml

POM.XML JAVA
<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:

HEALTH.JSON JAVA
{
  "status": "string"
}

2. Method: POST

URL: /v1/check
Request:

CHECKREQ.JSON JAVA
{
  "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:

CHECKRES.JSON JAVA
{
  "status": "string",
  "message": "string",
  "responseCode": "string",
  "data": {
    "accountNumber": "string"
  }
}

Let's code!

Open the project then create folder cfg, deploy dan log.

Project structure

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

base-isomsg.xml.ftl XML
<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.

request.schema.json JSON
{
  "$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

response.json.ftl JAVA
<#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.

response.json.ftl JAVA
<#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.

common-utils.ftl JAVA
<#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

common-request.schema.json JAVA
{
  "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

base-isomsg.xml.ftl JAVA
<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

check.xml.ftl JAVA
<#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

check-request.schema.json JAVA
{
  "$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

check-response.json.ftl JAVA
<#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.

00_logger.xml JAVA
<?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.

02_msg_config.xml JAVA
<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

03_channel.xml JAVA
<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

04_mux.xml JAVA
<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

07_rc_mapping.xml JAVA
<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

GeneralConstant.JAVA JAVA
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

LogLevel.JAVA JAVA
public enum LogLevel {
  DEBUG(1),
  INFO(2),
  WARN(3),
  ERROR(4);

  public final Integer level;
  LogLevel(int level) {
      this.level = level;
  }
}

Create Enum Status

Status.JAVA JAVA
public enum Status {
  SUCCESS,
  PENDING,
  FAILED
}

Add folder dto at /src/main/java/id/co/pandz/rest_jposs. Then create class ResponseCodeDetail

ResponseCodeDetail.JAVA JAVA
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ResponseCodeDetail {
    private String sourceRc;
    private String targetRc;
    private Status status;
    private String description;
}

Create class ResponseDto

ResponseDto.JAVA JAVA
@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

ResponseDto.JAVA JAVA
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

ISOLogger.JAVA JAVA
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

MessageUtil.JAVA JAVA
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

MessageTemplateLoader.JAVA JAVA
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

QMessageConfiguratorMBean.JAVA JAVA
public interface QMessageConfiguratorMBean extends QBeanSupportMBean {
}

Create implementation class QMessageConfigurator

QMessageConfigurator.JAVA JAVA
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

QRcMapperMBean.JAVA JAVA
public interface QRcMapperMBean extends QBeanSupportMBean {
  ResponseCodeDetail getDetailRC(String sourceRc, Context ctx);
  boolean containsKey(String sourceRc);
}

Create implementation class QRcMapper

QRcMapper.JAVA JAVA
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

ISOMsgBuilder.JAVA JAVA
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

JsonRequestValidator.JAVA JAVA
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

JsonResponseBuilder.JAVA JAVA
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

LogPrinterParticipant.JAVA JAVA
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

SendIsoRequest.JAVA JAVA
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.

RequestListener.JAVA JAVA
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

RestServer.JAVA JAVA
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.

Router.JAVA JAVA
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

05_isoserver.XML JAVA
<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

05_rest_server.XML JAVA
<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

06_rest_txnmanager.XML JAVA
<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

20_tnxmanager.XML JAVA
<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.

RestJpossApplication.XML JAVA
@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.

Start iso service
Start rest service

Test

Hit it with postman

Postman health
Postman check

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

READ BEYOND THE VOID

Technical blueprint background
Tutorial

Integrate Jenkins to Gitlab Webhook

Install Jenkins and connect to Gitlab...

Read Entry
Abstract digital network
Engineering Topic

Difference Between JPA and Hibernate

What difference between JPA and Hibernate...

Read Entry
Circuit board macro
Backend Notes

ForkJoinPool vs ThreadPoolExecutor

What difference between ForkJoinPool and ThreadPoolExecutor...

Read Entry