001/*
002 * jPOS Project [http://jpos.org]
003 * Copyright (C) 2000-2026 jPOS Software SRL
004 *
005 * This program is free software: you can redistribute it and/or modify
006 * it under the terms of the GNU Affero General Public License as
007 * published by the Free Software Foundation, either version 3 of the
008 * License, or (at your option) any later version.
009 *
010 * This program is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
013 * GNU Affero General Public License for more details.
014 *
015 * You should have received a copy of the GNU Affero General Public License
016 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
017 */
018
019package org.jpos.iso;
020
021import java.io.EOFException;
022import java.io.IOException;
023import java.io.InterruptedIOException;
024import java.io.PrintStream;
025import java.lang.ref.WeakReference;
026import java.net.InetAddress;
027import java.net.InetSocketAddress;
028import java.net.ServerSocket;
029import java.net.Socket;
030import java.net.BindException;
031import java.net.SocketException;
032import java.net.UnknownHostException;
033import java.time.Duration;
034import java.util.*;
035import java.util.concurrent.ExecutorService;
036import java.util.concurrent.Semaphore;
037import java.util.concurrent.TimeUnit;
038import java.util.concurrent.atomic.AtomicInteger;
039import java.util.concurrent.locks.LockSupport;
040
041import org.jpos.core.Configurable;
042import org.jpos.core.Configuration;
043import org.jpos.core.ConfigurationException;
044import org.jpos.jfr.ChannelEvent;
045import org.jpos.log.AuditLogEvent;
046import org.jpos.log.evt.*;
047import org.jpos.q2.QFactory;
048import org.jpos.util.*;
049
050/**
051 * Accept ServerChannel sessions and forwards them to ISORequestListeners
052 * @author Alejandro P. Revilla
053 * @author Bharavi Gade
054 * @version $Revision$ $Date$
055 */
056@SuppressWarnings("unchecked")
057public class ISOServer extends Observable
058    implements LogSource, Runnable, Observer, ISOServerMBean, Configurable,
059    Loggeable, ISOServerSocketFactory
060{
061    private enum PermLogPolicy {
062        ALLOW_NOLOG, DENY_LOG, ALLOW_LOG, DENY_LOGWARNING
063    }
064
065    int port;
066    private InetAddress bindAddr;
067
068    private Map<String,Boolean> specificIPPerms= new HashMap<>();   // TRUE means allow; FALSE means deny
069    private List<String> wildcardAllow;
070    private List<String> wildcardDeny;
071    private PermLogPolicy ipPermLogPolicy= PermLogPolicy.ALLOW_NOLOG;
072
073    protected ISOChannel clientSideChannel;
074    ISOPackager clientPackager;
075    protected Collection clientOutgoingFilters, clientIncomingFilters;
076    protected List<ISORequestListener> listeners;
077    public static final int DEFAULT_MAX_SESSIONS = 100;
078    public static final String LAST = ":last";
079    String name;
080    protected long lastTxn = 0l;
081    protected Logger logger;
082    protected String realm;
083    protected String realmChannel;
084    protected ISOServerSocketFactory socketFactory = null;
085
086    private AtomicInteger connectionCount = new AtomicInteger();
087
088    private int backlog;
089    protected Configuration cfg;
090    private volatile boolean shutdown = false;
091    private ServerSocket serverSocket;
092    private Map<String,WeakReference<ISOChannel>> channels;
093    protected boolean ignoreISOExceptions;
094    protected List<ISOServerEventListener> serverListeners = null;
095    private ExecutorService executor;
096    private Semaphore permits;
097    private int permitsCount = DEFAULT_MAX_SESSIONS;
098    private static final long SMALL_RELAX = 250;
099    private static final long LONG_RELAX = 5000;
100    private static final long SHUTDOWN_WAIT = 15000;
101    private final UUID uuid = UUID.randomUUID();
102
103   /**
104    * @param port port to listen
105    * @param clientSide client side ISOChannel, used as a "clonable template" to accept new connections
106    */
107    public ISOServer(int port, ServerChannel clientSide, int maxSessions) {
108        super();
109        this.port = port;
110        this.clientSideChannel = clientSide;
111        this.clientPackager = clientSide.getPackager();
112        if (clientSide instanceof FilteredChannel fc) {
113            this.clientOutgoingFilters = fc.getOutgoingFilters();
114            this.clientIncomingFilters = fc.getIncomingFilters();
115        }
116        listeners = new ArrayList<>();
117        name = "";
118        channels = Collections.synchronizedMap(new HashMap<>());
119        serverListeners = Collections.synchronizedList(new ArrayList<>());
120
121        if (maxSessions > 0)
122            permitsCount = maxSessions;
123        permits = new Semaphore(permitsCount);
124    }
125
126    @Override
127    public void setConfiguration (Configuration cfg) throws ConfigurationException {
128        this.cfg = cfg;
129        configureConnectionPerms();
130        backlog = cfg.getInt ("backlog", 5);
131        ignoreISOExceptions = cfg.getBoolean("ignore-iso-exceptions");
132        String ip = cfg.get ("bind-address", null);
133        if (ip != null) {
134            try {
135                bindAddr = InetAddress.getByName (ip);
136            } catch (UnknownHostException e) {
137                throw new ConfigurationException ("Invalid bind-address " + ip, e);
138            }
139        }
140        if (socketFactory == null) {
141            socketFactory = this;
142        }
143        if (socketFactory != this && socketFactory instanceof Configurable) {
144            ((Configurable)socketFactory).setConfiguration (cfg);
145        }
146        executor = QFactory.executorService(cfg.getBoolean("virtual-threads", false));
147    }
148
149    // Helper method to setConfiguration. Handles "allow" and "deny" params
150    private void configureConnectionPerms() throws ConfigurationException
151    {
152        boolean hasAllows= false, hasDenies= false;
153
154        String[] allows= cfg.getAll ("allow");
155        if (allows != null && allows.length > 0) {
156            hasAllows= true;
157
158            for (String allowIP : allows) {
159                allowIP= allowIP.trim();
160
161                if (allowIP.indexOf('*') == -1) {                   // specific IP with no wildcards
162                    specificIPPerms.put(allowIP, true);
163                } else {                                            // there's a wildcard
164                    wildcardAllow= (wildcardAllow == null) ? new ArrayList<>() : wildcardAllow;
165                    String[] parts= allowIP.split("[*]");
166                    wildcardAllow.add(parts[0]);                    // keep only the first part
167                }
168            }
169        }
170
171        String[] denies= cfg.getAll ("deny");
172        if (denies != null && denies.length > 0) {
173            hasDenies= true;
174
175            for (String denyIP : denies) {
176                boolean conflict= false;                            // used for a little sanity check
177
178                denyIP= denyIP.trim();
179                if (denyIP.indexOf('*') == -1) {                    // specific IP with no wildcards
180                    Boolean oldVal= specificIPPerms.put(denyIP, false);
181                    conflict= (oldVal == Boolean.TRUE);
182                } else {                                            // there's a wildcard
183                    wildcardDeny= (wildcardDeny == null) ? new ArrayList<>() : wildcardDeny;
184                    String[] parts= denyIP.split("[*]");
185                    if (wildcardAllow != null && wildcardAllow.contains(parts[0]))
186                        conflict= true;
187                    else
188                        wildcardDeny.add(parts[0]);                 // keep only the first part
189                }
190
191                if (conflict) {
192                    throw new ConfigurationException(
193                            "Conflicting IP permission in '"+getName()+"' configuration: 'deny' "
194                                    +denyIP+" while having an identical previous 'allow'.");
195                }
196            }
197        }
198
199        // sum up permission policy and logging type
200        ipPermLogPolicy= (!hasAllows && !hasDenies) ? PermLogPolicy.ALLOW_NOLOG :           // default when no permissions specified
201                         ( hasAllows && !hasDenies) ? PermLogPolicy.DENY_LOG :
202                         (!hasAllows && hasDenies)  ? PermLogPolicy.ALLOW_LOG :
203                                                      PermLogPolicy.DENY_LOGWARNING;        // mixed allows & denies, if nothing matches we'll DENY and log a warning
204    }
205
206   /**
207    * add an ISORequestListener
208    * @param l request listener to be added
209    * @see ISORequestListener
210    */
211    public void addISORequestListener(ISORequestListener l) {
212        listeners.add (l);
213    }
214   /**
215    * remove an ISORequestListener
216    * @param l a request listener to be removed
217    * @see ISORequestListener
218    */
219    public void removeISORequestListener(ISORequestListener l) {
220        listeners.remove (l);
221    }
222
223    /**
224     * Shutdown this server
225     */
226    public void shutdown () {
227        shutdown = true;
228        executor.submit(() -> {
229            Thread.currentThread().setName("ISOServer-shutdown");
230            shutdownServer();
231            if (!cfg.getBoolean("keep-channels")) {
232                shutdownChannels();
233            }
234        });
235        executor.shutdown();
236        try {
237            if (!executor.awaitTermination(SHUTDOWN_WAIT, TimeUnit.MILLISECONDS)) {
238                executor.shutdownNow();
239            }
240        } catch (InterruptedException e) {
241            Thread.currentThread().interrupt();
242        }
243    }
244    private void shutdownServer () {
245        try {
246            if (serverSocket != null) {
247                serverSocket.close ();
248                fireEvent(new ISOServerShutdownEvent(this));
249            }
250        } catch (IOException e) {
251            fireEvent(new ISOServerShutdownEvent(this));
252            Logger.log (new LogEvent (this, "shutdown", e));
253        }
254    }
255    private void shutdownChannels () {
256        Iterator iter = channels.entrySet().iterator();
257        while (iter.hasNext()) {
258            Map.Entry entry = (Map.Entry) iter.next();
259            WeakReference ref = (WeakReference) entry.getValue();
260            ISOChannel c = (ISOChannel) ref.get ();
261            if (c != null) {
262                try {
263                    c.disconnect ();
264                    fireEvent(new ISOServerClientDisconnectEvent(this, c));
265                } catch (IOException e) {
266                    Logger.log (new LogEvent (this, "shutdown", e));
267                }
268            }
269        }
270    }
271    private void purgeChannels() {
272        channels.entrySet().removeIf(entry -> {
273            ISOChannel channel = entry.getValue().get();
274            return channel == null || !channel.isConnected();
275        });
276    }
277
278    @Override
279    public ServerSocket createServerSocket(int port) throws IOException {
280        ServerSocket ss = new ServerSocket();
281        try {
282            ss.setReuseAddress(true);
283            ss.bind(new InetSocketAddress(bindAddr, port), backlog);
284        } catch(SecurityException e) {
285            ss.close();
286            fireEvent(new ISOServerShutdownEvent(this));
287            throw e;
288        } catch(IOException e) {
289            ss.close();
290            fireEvent(new ISOServerShutdownEvent(this));
291            throw e;
292        }
293        return ss;
294    }
295
296    //-----------------------------------------------------------------------------
297    // -- Helper Session inner class. It's a Runnable, running in its own
298    // -- thread and handling a connection to this ISOServer
299    // --
300    protected Session createSession (ServerChannel channel) {
301        return new Session (channel);
302    }
303
304    protected class Session implements Runnable, LogSource {
305        ServerChannel channel;
306        String realm;
307        protected Session(ServerChannel channel) {
308            this.channel = channel;
309            realm = ISOServer.this.getRealm() + ".session";
310        }
311        @Override
312        public void run() {
313            setChanged ();
314            notifyObservers ();
315            UUID sessionUUID = uuid;
316            String sessionInfo = "";
317            if (channel instanceof BaseChannel baseChannel) {
318                Socket socket = baseChannel.getSocket ();
319                sessionInfo = socket.toString();
320                sessionUUID = getSocketUUID(socket);
321                LogEvent ev = new LogEvent()
322                  .withSource(this)
323                  .withTraceId(sessionUUID)
324                  .add(new SessionStart(getActiveConnections(), permitsCount, sessionInfo)
325                );
326                if (!checkPermission (socket, ev))
327                    return;
328                realm = realm + "/" + socket.getInetAddress().getHostAddress() + ":" + socket.getPort();
329            }
330            try {
331                WeakReference<ISOChannel> wr = new WeakReference<> (channel);
332                channels.put (channel.getName(), wr);
333                channels.put (LAST, wr); // we are most likely the last one
334                while (true) try {
335                    ISOMsg m = channel.receive();
336                    lastTxn = System.currentTimeMillis();
337                    for (ISORequestListener listener : listeners) {
338                        if (listener.process(channel, m)) {
339                            break;
340                        }
341                    }
342                } catch (ISOFilter.VetoException e) {
343                    Logger.log(new LogEvent(this, "VetoException", e.getMessage()));
344                } catch (ISOException e) {
345                    if (ignoreISOExceptions) {
346                        Logger.log(new LogEvent(this, "ISOException", e.getMessage()));
347                    } else {
348                        throw e;
349                    }
350                }
351            } catch (EOFException e) {
352                 // Logger.log (new LogEvent (this, "session-warning", "<eof/>"));
353            } catch (SocketException e) {
354                 if (!shutdown)
355                     Logger.log (new LogEvent (this, "session-warning", e));
356            } catch (InterruptedIOException e) {
357                // nothing to log
358            } catch (Throwable e) {
359                Logger.log (new LogEvent (this, "session-error", e));
360            }
361
362            try {
363                channel.disconnect();
364                fireEvent(new ISOServerClientDisconnectEvent(ISOServer.this, channel));
365            } catch (IOException ex) {
366                Logger.log (new LogEvent (this, "session-error", ex));
367                fireEvent(new ISOServerClientDisconnectEvent(ISOServer.this, channel));
368            }
369            Logger.log(new LogEvent()
370              .withSource(this)
371              .withTraceId(sessionUUID)
372              .add(new SessionEnd(getActiveConnections(), permitsCount, sessionInfo)
373              )
374            );
375        }
376        @Override
377        public void setLogger (Logger logger, String realm) {
378        }
379        @Override
380        public String getRealm () {
381            return realm;
382        }
383        @Override
384        public Logger getLogger() {
385            return ISOServer.this.getLogger();
386        }
387
388        private boolean checkPermission (Socket socket, LogEvent ev) {
389            try {
390                checkPermission0 (socket, ev);
391                return true;
392            } catch (ISOException e) {
393                try {
394                    int delay = 1000 + new Random().nextInt(4000);
395                    ev.addMessage(e.getMessage());
396                    ev.addMessage("delay=" + delay);
397                    ISOUtil.sleep(delay);
398                    socket.close();
399                    fireEvent(new ISOServerShutdownEvent(ISOServer.this));
400                } catch (Throwable t) {
401                    ev.addMessage (t);
402                }
403            } finally {
404                Logger.log (ev);
405            }
406            return false;
407        }
408
409        private void checkPermission0 (Socket socket, LogEvent evt) throws ISOException {
410            // if there are no allow/deny params, just return without doing any checks
411            // (i.e.: "silent allow policy", keeping backward compatibility)
412            if (specificIPPerms.isEmpty() && wildcardAllow == null && wildcardDeny == null)
413                return;
414
415            String ip= socket.getInetAddress().getHostAddress ();           // The remote IP
416
417            // first, check allows or denies for specific/whole IPs (no wildcards)
418            boolean specificAllow = specificIPPerms.get(ip);
419            if (specificAllow == Boolean.TRUE) {                            // specific IP allow
420                evt.addMessage("access granted, ip=" + ip);
421                return;
422            } else if (specificAllow == Boolean.FALSE) {                    // specific IP deny
423                throw new ISOException("access denied, ip=" + ip);
424            } else {                                                        // no specific match under the specificIPPerms Map
425                // We check the wildcard lists, deny first
426                if (wildcardDeny != null) {
427                    for (String wdeny : wildcardDeny) {
428                        if (ip.startsWith(wdeny)) {
429                            throw new ISOException ("access denied, ip=" + ip);
430                        }
431                    }
432                }
433                if (wildcardAllow != null) {
434                    for (String wallow : wildcardAllow) {
435                        if (ip.startsWith(wallow)) {
436                            evt.addMessage("access granted, ip=" + ip);
437                            return;
438                        }
439                    }
440                }
441
442                // Reaching this point means that nothing matched our specific or wildcard rules, so we fall
443                // back on the default permission policies and log type
444                switch (ipPermLogPolicy) {
445                    case DENY_LOG:        // only allows were specified, default policy is to deny non-matches and log the issue
446                        throw new ISOException ("access denied, ip=" + ip);
447                        // break;
448
449                    case ALLOW_LOG:       // only denies were specified, default policy is to allow non-matches and log the issue
450                        evt.addMessage("access granted, ip=" + ip);
451                        break;
452
453                    case DENY_LOGWARNING: // mix of allows and denies were specified, but the IP matched no rules!
454                                          // so we adopt a deny policy but give a special warning
455                        throw new ISOException ("access denied, ip=" + ip + " (WARNING: the IP did not match any rules!)");
456                        // break;
457
458                    case ALLOW_NOLOG:   // this is the default case when no allow/deny are specified
459                                        // the method will abort early on the first "if", so this is here just for completion
460                        break;
461                }
462
463            }
464            // we should never reach this point!! :-)
465        }
466    } // inner class Session
467
468    //-------------------------------------------------------------------------------
469    //-- This is the main run for this ISOServer's Thread
470    @Override
471    public void run() {
472        if (socketFactory == null) {
473            socketFactory = this;
474        }
475        int round = 0;
476        serverLoop : while  (!shutdown) {
477            round++;
478            try {
479                if (permits.availablePermits() <= 0) {
480                    LockSupport.parkNanos(Duration.ofMillis(SMALL_RELAX).toNanos());
481                    if (round % 240 == 0 && cfg.getBoolean("permits-exhaustion-warning", true)) {
482                        log(new Warning("permits exhausted " + serverSocket.toString()));
483                    }
484                    continue;
485                }
486                serverSocket = socketFactory.createServerSocket(port);
487                log (new Listen(port, bindAddr, permits.availablePermits(), backlog));
488                while (!shutdown) {
489                    try {
490                        if (permits.availablePermits() <= 0) {
491                            ChannelEvent jfr = new ChannelEvent.AcceptException(
492                              "Available permits too low (%d)".formatted(permits.availablePermits())
493                            );
494                            jfr.begin();
495                            try {
496                                serverSocket.close();
497                                fireEvent(new ISOServerShutdownEvent(this));
498                            } catch (IOException e){
499                                log (new ThrowableAuditLogEvent(e));
500                            } finally {
501                                jfr.commit();
502                            }
503                            continue serverLoop;
504                        }
505                        final ServerChannel channel = (ServerChannel) clientSideChannel.clone();
506                        channel.accept (serverSocket);
507                        if (connectionCount.getAndIncrement() % 100 == 0) {
508                            purgeChannels ();
509                        }
510                        executor.submit (() -> {
511                            try {
512                                permits.acquireUninterruptibly();
513                                createSession(channel).run();
514                            } finally {
515                                permits.release();
516                            }
517                          });
518                        setChanged ();
519                        notifyObservers (this);
520                        fireEvent(new ISOServerAcceptEvent(this, channel));
521                        if (channel instanceof Observable) {
522                            ((Observable)channel).addObserver (this);
523                        }
524                    } catch (SocketException e) {
525                        if (!shutdown) {
526                            log (new ThrowableAuditLogEvent(e));
527                            relax();
528                            continue serverLoop;
529                        }
530                    } catch (IOException e) {
531                        log (new ThrowableAuditLogEvent(e));
532                        relax();
533                    }
534                } // while !shutdown
535            } catch (BindException e) {
536                warn(new Listen(port, bindAddr,
537                                permits.availablePermits(), backlog,
538                                "(round "+round+") "+e));
539                relax();
540            } catch (Throwable e) {
541                log (new ThrowableAuditLogEvent(e));
542                relax();
543            }
544        }
545    } // ISOServer's run()
546    //-------------------------------------------------------------------------------
547
548    private void relax() {
549        LockSupport.parkNanos(Duration.ofMillis(LONG_RELAX).toNanos());
550    }
551
552    /**
553     * associates this ISOServer with a name using NameRegistrar
554     * @param name name to register
555     * @see NameRegistrar
556     */
557    public void setName (String name) {
558        this.name = name;
559        NameRegistrar.register ("server."+name, this);
560    }
561    /**
562     * @return ISOServer instance with given name.
563     * @throws NameRegistrar.NotFoundException;
564     * @see NameRegistrar
565     */
566    public static ISOServer getServer (String name)
567        throws NameRegistrar.NotFoundException
568    {
569        return NameRegistrar.get ("server."+name);
570    }
571    /**
572     * @return this ISOServer's name ("" if no name was set)
573     */
574    public String getName() {
575        return this.name;
576    }
577    @Override
578    public void setLogger (Logger logger, String realm) {
579        this.logger = logger;
580        this.realm  = realm;
581        this.realmChannel = realm + ".channel";
582    }
583    @Override
584    public String getRealm () {
585        return realm;
586    }
587    @Override
588    public Logger getLogger() {
589        return logger;
590    }
591    @Override
592    public void update(Observable o, Object arg) {
593        setChanged ();
594        notifyObservers (arg);
595    }
596   /**
597    * Gets the ISOClientSocketFactory (may be null)
598    * @see     ISOClientSocketFactory
599    * @since 1.3.3
600    */
601    public ISOServerSocketFactory getSocketFactory() {
602        return socketFactory;
603    }
604   /**
605    * Sets the specified Socket Factory to create sockets
606    * @param         socketFactory the ISOClientSocketFactory
607    * @see           ISOClientSocketFactory
608    * @since 1.3.3
609    */
610    public void setSocketFactory(ISOServerSocketFactory socketFactory) {
611        this.socketFactory = socketFactory;
612    }
613    @Override
614    public int getPort () {
615        return port;
616    }
617    @Override
618    public void resetCounters () {
619        connectionCount.set(0);
620        lastTxn = 0l;
621    }
622
623    /**
624     * @return number of connections accepted by this server
625     */
626    @Override
627    public int getConnectionCount () {
628        return connectionCount.get();
629    }
630
631    /**
632     * @return most recently connected ISOChannel or null
633     */
634    public ISOChannel getLastConnectedISOChannel () {
635        return getISOChannel (LAST);
636    }
637
638    /**
639     * @return ISOChannel under the given name
640     */
641    public ISOChannel getISOChannel (String name) {
642        WeakReference ref = channels.get (name);
643        if (ref != null) {
644            return (ISOChannel) ref.get ();
645        }
646        return null;
647    }
648
649
650    @Override
651    public String getISOChannelNames () {
652        StringBuilder sb = new StringBuilder ();
653        Iterator iter = channels.entrySet().iterator();
654        for (int i=0; iter.hasNext(); i++) {
655            Map.Entry entry = (Map.Entry) iter.next();
656            WeakReference ref = (WeakReference) entry.getValue();
657            ISOChannel c = (ISOChannel) ref.get ();
658            if (c != null && !LAST.equals (entry.getKey()) && c.isConnected()) {
659                if (i > 0 && !sb.isEmpty()) {
660                    sb.append (' ');
661                }
662                sb.append (entry.getKey());
663            }
664        }
665        return sb.toString();
666    }
667    public String getCountersAsString () {
668        StringBuilder sb = new StringBuilder ();
669        int cnt[] = getCounters();
670        sb.append ("connected=");
671        sb.append (cnt[2]);
672        sb.append (", rx=");
673        sb.append (cnt[0]);
674        sb.append (", tx=");
675        sb.append (cnt[1]);
676        sb.append (", last=");
677        sb.append (lastTxn);
678        if (lastTxn > 0) {
679            sb.append (", idle=");
680            sb.append(System.currentTimeMillis() - lastTxn);
681            sb.append ("ms");
682        }
683        return sb.toString();
684    }
685
686    public int[] getCounters()
687    {
688        Iterator iter = channels.entrySet().iterator();
689        int[] cnt = new int[3];
690        cnt[2] = 0;
691        for (int i=0; iter.hasNext(); i++) {
692            Map.Entry entry = (Map.Entry) iter.next();
693            WeakReference ref = (WeakReference) entry.getValue();
694            ISOChannel c = (ISOChannel) ref.get ();
695            if (c != null && !LAST.equals (entry.getKey()) && c.isConnected()) {
696                cnt[2]++;
697                if (c instanceof BaseChannel) {
698                    int[] cc = ((BaseChannel)c).getCounters();
699                    cnt[0] += cc[ISOChannel.RX];
700                    cnt[1] += cc[ISOChannel.TX];
701                }
702            }
703        }
704        return cnt;
705    }
706
707    @Override
708    public int getTXCounter() {
709        int cnt[] = getCounters();
710        return cnt[1];
711    }
712    @Override
713    public int getRXCounter() {
714        int cnt[] = getCounters();
715        return cnt[0];
716    }
717    public int getConnections () {
718        return connectionCount.get();
719    }
720    @Override
721    public long getLastTxnTimestampInMillis() {
722        return lastTxn;
723    }
724    @Override
725    public long getIdleTimeInMillis() {
726        return lastTxn > 0L ? System.currentTimeMillis() - lastTxn : -1L;
727    }
728
729
730    @Override
731    public String getCountersAsString (String isoChannelName) {
732        ISOChannel channel = getISOChannel(isoChannelName);
733        StringBuffer sb = new StringBuffer();
734        if (channel instanceof BaseChannel) {
735            int[] counters = ((BaseChannel)channel).getCounters();
736            append (sb, "rx=", counters[ISOChannel.RX]);
737            append (sb, ", tx=", counters[ISOChannel.TX]);
738            append (sb, ", connects=", counters[ISOChannel.CONNECT]);
739        }
740        return sb.toString();
741    }
742    @Override
743    public void dump (PrintStream p, String indent) {
744        p.println (indent + getCountersAsString());
745        Iterator iter = channels.entrySet().iterator();
746        String inner = indent + "  ";
747        for (int i=0; iter.hasNext(); i++) {
748            Map.Entry entry = (Map.Entry) iter.next();
749            WeakReference ref = (WeakReference) entry.getValue();
750            ISOChannel c = (ISOChannel) ref.get ();
751            if (c != null && !LAST.equals (entry.getKey()) && c.isConnected() && c instanceof BaseChannel) {
752                StringBuilder sb = new StringBuilder ();
753                int[] cc = ((BaseChannel)c).getCounters();
754                sb.append (inner);
755                sb.append (entry.getKey());
756                sb.append (": rx=");
757                sb.append (cc[ISOChannel.RX]);
758                sb.append (", tx=");
759                sb.append (cc[ISOChannel.TX]);
760                sb.append (", last=");
761                sb.append (lastTxn);
762                p.println (sb);
763            }
764        }
765    }
766    private void append (StringBuffer sb, String name, int value) {
767        sb.append (name);
768        sb.append (value);
769    }
770
771    public void addServerEventListener(ISOServerEventListener listener) {
772        serverListeners.add(listener);
773    }
774    public void removeServerEventListener(ISOServerEventListener listener) {
775        serverListeners.remove(listener);
776    }
777
778    public void fireEvent(EventObject event) {
779        for (ISOServerEventListener l : serverListeners) {
780            try {
781                l.handleISOServerEvent(event);
782            }
783            catch (Throwable ignore) {
784                /*
785                 * Don't want an exception from a handler to exit the loop or
786                 * let it bubble up.
787                 * If it bubbles up it can cause side effects like getting caught
788                 * in the throwable catch leading to server trying to listen on
789                 * the same port.
790                 * We don't want a side effect in jpos caused by custom user
791                 * handler code.
792                 */
793            }
794
795        }
796    }
797
798    private void warn (AuditLogEvent log) {
799        Logger.log(new LogEvent(Log.WARN)
800          .withSource(this)
801          .withTraceId(uuid)
802          .add(log)
803        );
804    }
805
806    private void log (AuditLogEvent log) {
807        Logger.log(new LogEvent()
808          .withSource(this)
809          .withTraceId(uuid)
810          .add(log)
811        );
812    }
813
814    private void log (String level, String message) {
815        LogEvent evt = new LogEvent (this, level).withTraceId(uuid);
816        evt.addMessage (message);
817        Logger.log (evt);
818    }
819
820    public int getActiveConnections () {
821        return permitsCount - permits.availablePermits();
822    }
823
824    private UUID getSocketUUID(Socket socket) {
825        return socket != null ?
826          new UUID(uuid.getMostSignificantBits(), uuid.getLeastSignificantBits() ^ socket.hashCode()) :
827          uuid;
828    }
829}