Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
org.jpos.q2.iso.QMUX Maven / Gradle / Ivy
/*
* jPOS Project [http://jpos.org]
* Copyright (C) 2000-2023 jPOS Software SRL
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
package org.jpos.q2.iso;
import org.HdrHistogram.AtomicHistogram;
import org.jdom2.Element;
import org.jpos.core.ConfigurationException;
import org.jpos.core.Environment;
import org.jpos.iso.*;
import org.jpos.q2.QBeanSupport;
import org.jpos.q2.QFactory;
import org.jpos.space.*;
import org.jpos.util.*;
import java.io.IOException;
import java.io.PrintStream;
import java.util.*;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
/**
* @author Alejandro Revilla
*/
@SuppressWarnings("unchecked")
public class QMUX
extends QBeanSupport
implements SpaceListener, MUX, QMUXMBean, Loggeable, MetricsProvider
{
static final String nomap = "0123456789";
static final String DEFAULT_KEY = "41, 11";
protected LocalSpace sp;
protected String in, out, unhandled;
protected String[] ready;
protected String[] key;
protected String ignorerc;
protected String[] mtiMapping;
private boolean headerIsKey;
private boolean returnRejects;
private LocalSpace isp; // internal space
private Map mtiKey = new HashMap<>();
private Metrics metrics = new Metrics(new AtomicHistogram(60000, 2));
List listeners;
private volatile int rx, tx, rxExpired, txExpired, rxPending, rxUnhandled, rxForwarded;
private volatile long lastTxn = 0L;
private boolean listenerRegistered;
public QMUX () {
super ();
listeners = new ArrayList<>();
}
public void initService () throws ConfigurationException {
Element e = getPersist ();
sp = grabSpace (e.getChild ("space"));
isp = cfg.getBoolean("reuse-space", false) ? sp : new TSpace();
in = Environment.get(e.getChildTextTrim ("in"));
out = Environment.get(e.getChildTextTrim ("out"));
if (in == null || out == null) {
throw new ConfigurationException ("Misconfigured QMUX. Please verify in/out queues");
}
ignorerc = Environment.get(e.getChildTextTrim ("ignore-rc"));
key = toStringArray(e.getChildTextTrim("key"), ", ", DEFAULT_KEY);
returnRejects = cfg.getBoolean("return-rejects", false);
for (Element keyElement : e.getChildren("key")) {
String mtiOverride = QFactory.getAttributeValue(keyElement, "mti");
if (mtiOverride != null && mtiOverride.length() >= 2) {
mtiKey.put (mtiOverride.substring(0,2), toStringArray(keyElement.getTextTrim(), ", ", null));
}
}
ready = toStringArray(Environment.get(e.getChildTextTrim ("ready")));
mtiMapping = toStringArray(Environment.get(e.getChildTextTrim ("mtimapping")));
if (mtiMapping == null || mtiMapping.length != 3)
mtiMapping = new String[] { nomap, nomap, "0022446689" };
addListeners ();
unhandled = Environment.get(e.getChildTextTrim ("unhandled"));
NameRegistrar.register ("mux."+getName (), this);
}
public void startService () {
if (!listenerRegistered) {
listenerRegistered = true;
// Handle messages that could be in the in queue at start time
synchronized (sp) {
Object[] pending = SpaceUtil.inpAll(sp, in);
sp.addListener (in, this);
for (Object o : pending)
sp.out(in, o);
}
}
}
public void stopService () {
listenerRegistered = false;
sp.removeListener (in, this);
}
public void destroyService () {
NameRegistrar.unregister ("mux."+getName ());
}
/**
* @return MUX with name using NameRegistrar
* @throws NameRegistrar.NotFoundException
* @see NameRegistrar
*/
public static MUX getMUX (String name)
throws NameRegistrar.NotFoundException
{
return (MUX) NameRegistrar.get ("mux."+name);
}
/**
* @param m message to send
* @param timeout amount of time in millis to wait for a response
* @return response or null
*/
public ISOMsg request (ISOMsg m, long timeout) throws ISOException {
String key = getKey (m);
String req = key + ".req";
synchronized (isp) {
if (isp.rdp (req) != null)
throw new ISOException ("Duplicate key '" + req + "' detected");
isp.out (req, m);
}
m.setDirection(0);
Chronometer c = new Chronometer();
if (timeout > 0)
sp.out (out, m, timeout);
else
sp.out (out, m);
ISOMsg resp;
try {
synchronized (this) { tx++; rxPending++; }
for (;;) {
resp = (ISOMsg) isp.in (key, timeout);
if (!shouldIgnore (resp))
break;
}
if (resp == null && isp.inp (req) == null) {
// possible race condition, retry for a few extra seconds
resp = (ISOMsg) isp.in (key, 10000);
}
synchronized (this) {
if (resp != null)
{
rx++;
lastTxn = System.currentTimeMillis();
}else {
rxExpired++;
if (m.getDirection() != ISOMsg.OUTGOING)
txExpired++;
}
}
} finally {
synchronized (this) { rxPending--; }
}
long elapsed = c.elapsed();
metrics.record("all", elapsed);
if (resp != null)
metrics.record("ok", elapsed);
return resp;
}
public void request (ISOMsg m, long timeout, ISOResponseListener rl, Object handBack)
throws ISOException
{
String key = getKey (m);
String req = key + ".req";
synchronized (isp) {
if (isp.rdp (req) != null)
throw new ISOException ("Duplicate key '" + req + "' detected.");
m.setDirection(0);
AsyncRequest ar = new AsyncRequest (rl, handBack);
synchronized (ar) {
if (timeout > 0)
ar.setFuture(getScheduledThreadPoolExecutor().schedule(ar, timeout, TimeUnit.MILLISECONDS));
}
isp.out (req, ar, timeout);
}
if (timeout > 0)
sp.out (out, m, timeout);
else
sp.out (out, m);
synchronized (this) { tx++; rxPending++; }
}
protected boolean isNotifyEligible(ISOMsg msg) {
if (returnRejects)
return true;
try {
return msg.isResponse();
} catch (RuntimeException | ISOException ex) {
// * ArrayIndexOutOfBoundsException - It may occur for messages where
// MTI is not standard 4 characters (eg. FSDISOMsg), then notification is expected.
// * ISOException: When there is no field 0, the error should be logged
return true;
}
}
@Override
public void notify (Object k, Object value) {
Object obj = sp.inp (k);
if (obj instanceof ISOMsg) {
ISOMsg m = (ISOMsg) obj;
try {
if (isNotifyEligible(m)) {
String key = getKey (m);
String req = key + ".req";
Object r = isp.inp (req);
if (r != null) {
if (r instanceof AsyncRequest) {
((AsyncRequest) r).responseReceived (m);
} else {
isp.out (key, m);
}
return;
}
}
} catch (ISOException e) {
LogEvent evt = getLog().createLogEvent("notify");
evt.addMessage(e);
evt.addMessage(obj);
Logger.log(evt);
}
processUnhandled (m);
}
}
public String getKey (ISOMsg m) throws ISOException {
if (out == null)
throw new NullPointerException ("Misconfigured QMUX. Please verify out queue is not null.");
StringBuilder sb = new StringBuilder (out);
sb.append ('.');
sb.append (mapMTI(m.getMTI()));
if (headerIsKey && m.getHeader()!=null) {
sb.append ('.');
sb.append(ISOUtil.hexString(m.getHeader()));
sb.append ('.');
}
boolean hasFields = false;
String[] k = mtiKey.getOrDefault(m.getMTI().substring(0,2), key);
for (String f : k) {
String v = m.getString(f);
if (v != null) {
if ("11".equals(f)) {
String vt = v.trim();
int l = m.getMTI().charAt(0) == '2' ? 12 : 6;
if (vt.length() < l)
v = ISOUtil.zeropad(vt, l);
}
if ("41".equals(f)) {
v = ISOUtil.zeropad(v.trim(), 16); // BIC ANSI to ISO hack
}
hasFields = true;
sb.append(v);
}
}
if (!hasFields)
throw new ISOException ("Key fields not found - not sending " + sb.toString());
return sb.toString();
}
@Override
public Metrics getMetrics() {
return metrics;
}
private String mapMTI (String mti) throws ISOException {
StringBuilder sb = new StringBuilder();
if (mti != null) {
if (mti.length() < 4)
mti = ISOUtil.zeropad(mti, 4); // #jPOS-55
if (mti.length() == 4) {
for (int i=0; i= 0 && c < 10)
sb.append (mtiMapping[i].charAt(c));
}
}
}
return sb.toString();
}
public synchronized void setInQueue (String in) {
this.in = in;
getPersist().getChild("in").setText (in);
setModified (true);
}
public String getInQueue () {
return in;
}
public synchronized void setOutQueue (String out) {
this.out = out;
getPersist().getChild("out").setText (out);
setModified (true);
}
public String getOutQueue () {
return out;
}
public Space getSpace() {
return sp;
}
public synchronized void setUnhandledQueue (String unhandled) {
this.unhandled = unhandled;
getPersist().getChild("unhandled").setText (unhandled);
setModified (true);
}
public String getUnhandledQueue () {
return unhandled;
}
@SuppressWarnings("unused")
public String[] getReadyIndicatorNames() {
return ready;
}
private void addListeners() throws ConfigurationException {
List rlisten = getPersist().getChildren("request-listener");
if (rlisten.isEmpty())
return;
QFactory factory = getFactory ();
for (Element l : rlisten) {
ISORequestListener listener = (ISORequestListener)
factory.newInstance (QFactory.getAttributeValue (l, "class"));
factory.setLogger (listener, l);
factory.setConfiguration (listener, l);
addISORequestListener (listener);
}
}
public void addISORequestListener(ISORequestListener l) {
listeners.add (l);
}
public boolean removeISORequestListener(ISORequestListener l) {
return listeners.remove(l);
}
public synchronized void resetCounters() {
rx = tx = rxExpired = txExpired = rxPending = rxUnhandled = rxForwarded = 0;
lastTxn = 0l;
}
public String getCountersAsString () {
StringBuffer sb = new StringBuffer();
append (sb, "tx=", tx);
append (sb, ", rx=", rx);
append (sb, ", tx_expired=", getTXExpired());
append (sb, ", tx_pending=", getTXPending());
append (sb, ", rx_expired=", getRXExpired());
append (sb, ", rx_pending=", getRXPending());
append (sb, ", rx_unhandled=", getRXUnhandled());
append (sb, ", rx_forwarded=", getRXForwarded());
sb.append (", connected=");
sb.append (Boolean.toString(isConnected()));
sb.append (", last=");
sb.append (lastTxn);
if (lastTxn > 0) {
sb.append (", idle=");
sb.append(System.currentTimeMillis() - lastTxn);
sb.append ("ms");
}
return sb.toString();
}
public int getTXCounter() {
return tx;
}
public int getRXCounter() {
return rx;
}
@Override
public int getTXExpired() {
return txExpired;
}
@Override
public int getTXPending() {
return sp.size(out);
}
@Override
public int getRXExpired() {
return rxExpired;
}
@Override
public int getRXPending() {
return rxPending;
}
@Override
public int getRXUnhandled() {
return rxUnhandled;
}
@Override
public int getRXForwarded() {
return rxForwarded;
}
public long getLastTxnTimestampInMillis() {
return lastTxn;
}
public long getIdleTimeInMillis() {
return lastTxn > 0L ? System.currentTimeMillis() - lastTxn : -1L;
}
protected void processUnhandled (ISOMsg m) {
ISOSource source = m.getSource();
source = source != null ? source : this;
Iterator iter = listeners.iterator();
if (iter.hasNext())
synchronized (this) { rxForwarded++; }
while (iter.hasNext())
if (((ISORequestListener)iter.next()).process (source, m))
return;
if (unhandled != null) {
synchronized (this) { rxUnhandled++; }
sp.out (unhandled, m, 120000);
}
}
private LocalSpace grabSpace (Element e)
throws ConfigurationException
{
String uri = e != null ? e.getText() : "";
Space sp = SpaceFactory.getSpace (uri);
if (sp instanceof LocalSpace) {
return (LocalSpace) sp;
}
throw new ConfigurationException ("Invalid space " + uri);
}
/**
* sends (or hands back) an ISOMsg
*
* @param m the Message to be sent
* @throws java.io.IOException
* @throws org.jpos.iso.ISOException
* @throws org.jpos.iso.ISOFilter.VetoException;
*/
public void send(ISOMsg m) throws IOException, ISOException {
if (!isConnected())
throw new ISOException ("MUX is not connected");
sp.out (out, m);
}
public boolean isConnected() {
if (running() && ready != null && ready.length > 0) {
for (String aReady : ready)
if (sp.rdp(aReady) != null)
return true;
return false;
}
return running();
}
public void dump (PrintStream p, String indent) {
p.println (indent + getCountersAsString());
metrics.dump (p, indent);
}
private String[] toStringArray(String s, String delimiter, String def) {
s = (s != null) ? Environment.get(s) : def;
String[] arr = null;
if (s != null && !s.isEmpty()) {
StringTokenizer st;
if (delimiter != null)
st = new StringTokenizer(s, delimiter);
else
st = new StringTokenizer(s);
List l = new ArrayList();
while (st.hasMoreTokens()) {
String t = st.nextToken();
if ("header".equalsIgnoreCase(t)) {
headerIsKey = true;
} else {
l.add (t);
}
}
arr = l.toArray(new String[l.size()]);
}
return arr;
}
private String[] toStringArray(String s) {
return toStringArray(s, null,null);
}
private boolean shouldIgnore (ISOMsg m) {
if (m != null && ignorerc != null
&& ignorerc.length() > 0 && m.hasField(39))
{
return ignorerc.contains(m.getString(39));
}
return false;
}
private void append (StringBuffer sb, String name, int value) {
sb.append (name);
sb.append (value);
}
public class AsyncRequest implements Runnable {
ISOResponseListener rl;
Object handBack;
ScheduledFuture future;
Chronometer chrono;
public AsyncRequest (ISOResponseListener rl, Object handBack) {
super();
this.rl = rl;
this.handBack = handBack;
this.chrono = new Chronometer();
}
public void setFuture(ScheduledFuture future) {
this.future = future;
}
public void responseReceived (ISOMsg response) {
if (future == null || future.cancel(false)) {
synchronized (QMUX.this) {
rx++;
rxPending--;
lastTxn = System.currentTimeMillis();
}
long elapsed = chrono.elapsed();
metrics.record("all", elapsed);
metrics.record("ok", elapsed);
rl.responseReceived(response, handBack);
}
}
public void run() {
synchronized(QMUX.this) {
rxPending--;
}
metrics.record("all", chrono.elapsed());
rl.expired(handBack);
}
}
}