# ConnectionManager.java

    /*
    ** Module   : ConnectionManager.java
    ** Abstract : Provides services for logical database connections and aliases
    **
    ** Copyright (c) 2005-2025, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
    ** 001 ECF 20051202   @23667 Created initial version. Provides services to
    **                           manage logical database connections and
    **                           aliases. Provides runtime support for the
    **                           following Progress statements/functions:
    **                              CONNECT/DISCONNECT
    **                              CONNECTED
    **                              CREATE/DELETE ALIAS
    **                              ALIAS
    **                              NUM-ALIASES
    **                              LDB-/PDB-/SDB-/DBNAME
    **                              NUM-DBS
    **                              DBTYPE/DBRESTRICTIONS
    ** 002 ECF 20060120   @24038 Integration with P2J server. Replaced
    **                           ThreadLocal with ContextLocal.
    ** 003 GES 20060517   @26198 Match new Finalizable interface requirement.
    ** 004 ECF 20060628   @27670 Fixed closeBufferScope. Only remove a buffer
    **                           from the set of those which reference a
    **                           physical database if its outermost scope is
    **                           closing.
    ** 005 ECF 20070907   @35292 Fixed constructor to avoid adding the temp
    **                           table dedicated database to the list of
    **                           connected databases. This information should
    **                           be transparent to business logic. Replaced
    **                           database name string with Database
    **                           information object. Required for non-local
    **                           database connections.
    ** 006 ECF 20071012   @35590 Intermediate set of changes to support remote
    **                           database connections more appropriately. This
    **                           feature set is not yet fully implemented, but
    **                           no existing functionality has been removed.
    ** 007 ECF 20071119   @35907 Completed implementation of remote database
    **                           connections. Works with the net package to
    **                           establish a virtual session with a remote
    **                           server, to enable remote database access.
    ** 008 ECF 20071127   @36051 Added certAlias parameter to makeConnection()
    **                           method. Passed through to SessionManager's
    **                           connectVirtual() method. Currently, parameter
    **                           is always null, indicating no requester-side
    **                           validation of remote server's certificate
    **                           will be performed by SecurityManager.
    ** 009 ECF 20080114   @36803 Fixed log output. Replaced incorrect format
    **                           string.
    ** 010 ECF 20080118   @36858 Added logging to connect logic and connected
    **                           check code.
    ** 011 ECF 20080213   @37069 Enable logical connections to locally managed
    **                           databases. If a connection is attempted to a
    **                           locally managed database, we short-circuit
    **                           the logic which creates a virtual session and
    **                           instead just manage the logical connection
    **                           information. Fixed reconnect logic for
    **                           pending disconnects.
    ** 012 SVL 20080806   @39313 Into disconnectImmediately() the session is
    **                           terminated only if it is not used by the
    **                           dirty share manager.
    ** 013 CA  20100223   @44686 When disconnecting from a remote database,
    **                           the session termination is done in a queue
    **                           (by using a global lock).
    ** 014 CA  20100329   @44784 Backed out H013 (@44686), as it was replaced
    **                           with a safer approach (the locking is done in
    **                           RouterSessionManager.terminateVirtual).
    ** 015 ECF 20120217          Added getPersistence(String) to allow the
    **                           Persistence object associated with the given
    **                           database to be used publicly.
    ** 016 OM  20121101          Implemented DBPARAM built-in function.
    ** 017 VMN 20130411          Added boolean parameter modal to recordOrShowError() method.
    ** 018 OM  20130507          Switch to int64 API function for obtaining desired java type.
    ** 019 ECF 20131010          Added metadata support.
    ** 020 CA  20131029          Added API to retrieve the aliases by their logical db name.
    ** 021 OM  20130924          Added support for userid/password authorization to database.
    **                           Switch to java.util.logging instead of Apache's logging API.
    **                           Enriched DBPARAM result with -U/-P parameters.
    ** 022 CA  20140513          Added a weight for the context-local var, to ensure predetermined
    **                           order during context reset.
    **                           Ensure the remote persistence is loaded, if connection was successful
    **                           (before authentication occurs).
    ** 023 OM  20140625          Replaced hardcoded rtrim sql function with dialect specific.
    ** 024 ECF 20141119          Added support for DBVERSION built-in function; fixed logical names
    **                           in ctor; fixed handling of empty string parameter passed to CONNECT
    **                           statement.
    ** 025 ECF 20141230          Completed support for DBVERSION built-in function. Added lookup of
    **                           LDB name by connected Database object. Create implicit DICTDB alias
    **                           for first connected database.
    ** 026 ECF 20150111          Added partial support for connection metadata.
    ** 027 ECF 20150212          Ensure the disconnection of a logical database does not deregister
    **                           the physical database prematurely, if other connections still exist
    **                           under other logical names.
    ** 028 ECF 20150221          Properly normalize LDB names (right trim and lowercase), instead of
    **                           only lowercasing.
    ** 029 CA  20150228          The meta CONNECT table needs to be cleaned up before anything else.
    ** 030 OM  20151130          Added proper number of parameters in rtrim. Fixed handling of query
    **                           result from authentication.
    ** 031 EVL 20160223          Javadoc fixes to make compatible with Oracle Java 8 for Solaris 10.
    ** 032 ECF 20160229          Added missing javadoc.
    ** 033 ECF 20160501          Replaced direct SQL use in authenticate with a more efficient
    **                           implementation.
    ** 034 ECF 20160609          SQL performance improvement.
    ** 035 OM  20160608          As rtrim was simplified, addRtrimmedExpression() returns nothing.
    ** 036 OM  20160628          Fixed case-sensitivity of some database-related functions.
    ** 037 ECF 20160829          Performance enhancement: use local BufferManager to check
    **                           transaction status instead of TransactionManager.
    ** 038 CA  20170427          Expose the FWD Session associated with a remote Database connection.
    ** 039 ECF 20171002          Added -1 (single-user mode) flag to DBPARAM output, and a log message
    **                           about its partial implementation.
    **     ECF 20171025          Implemented -ct (max connection retries) parameter.
    ** 040 CA  20171110          Fixed a bug in H039 related to -ct - if connection is succesful, then
    **                           break the loop.
    ** 041 GES 20171220          Changes to match new silent mode implementation.
    ** 042 ECF 20180116          Added no-op implementation of -N connect parameter. Fixed case where
    **                           logical and physical database names differ.
    **     OM  20180117          Fixed cast exception on SQL result. Small optimizations.
    ** 043 CA  20180208          metaCleaner is cleaned before the local var, so removeConnected()
    **                           must not attempt to clean it again when called from cleaning up the
    **                           local var.
    ** 044 OM  20180305          Fixed authentication. Added getDefaultDatabase() method.
    ** 045 ECF 20180408          Strip enclosing quotes from connect options, if present. DICTDB
    **                           alias fix.
    ** 046 CA  20180425          Fixed some NO-ERROR handling for errors throw by CONNECT.
    ** 047 CA  20180619          If a tx is still open, postpone the disconnect even if all buffers
    **                           are closed.
    ** 048 CA  20181128          Added support for GET-DB-CLIENT and SET-DB-CLIENT.
    ** 049 ECF 20181208          Stubbed dbTaskId method.
    ** 050 OM  20190111          Avoided circular indirect recursivity at initialization when _Connect
    **                           meta-table is active.
    ** 051 OM  20190117          Added implementation for dbTaskId(). Generified iterateToIndex().
    **                           Fixed dbParamImpl().
    ** 052 IAS 20190622          Changed getPersistence() method. It now returns null instead of
    **                           throwing NPE if getDatabase() returns null.
    **                           Added readUserTable() method
    ** 053 ECF 20190802          Fixed LDB name lookups for metadata databases.
    **     CA  20190902          Fixed problems when connecting to a FWD server running a database
    **                           schema which is not actively managed by the requester, too.
    ** 054 IAS 20191015          Rework SET-CLIENT/SET-DB-CLIENT
    ** 055 IAS 20200212          Rework CONNECT and DBPARAM
    ** 056 ECF 20200419          Removed Hibernate dependencies.
    ** 057 IAS 20200819          Added table CRUD ops statistics support
    ** 058 IAS 20210401          Fixed UserTableStatUpdater-related logic
    ** 059 IAS 20210520          Added support for session-based database auto-connect
    **     IAS 20210613          Get rid of the permanent connections notion.
    **     OM  20210825          Fixed conversion from type 12 to String.
    **     ECF 20210908          Refactored connect/disconnect metadata logic to remove synchronization in the
    **                           connection listeners which was leading to deadlocks.
    **     IAS 20211126          Special logic for auto-connect regarding registration
    **     CA  20220104          An attempt to fix a leak from pdbReferences - in RecordBuffer.finishedImpl,
    **                           if the buffer's scope is still 0 and is active, then call 'closeBufferScope'.
    **     ECF 20220301          Format cleanup.
    **     RFB 20220301          Allow for options to be case-insensitive. Ref. # 6088.
    **     IAS 20220311          Fix recursive call of the OptionsParser
    **     IAS 20220713          Do not lock database on SET-CLIENT.
    **     TJD 20220504          Upgrade do Java 11 minor changes
    **     CA  20220426          Added DBCODEPAGE builtin function stubs.
    **     IAS 20221128          Re-worked DB-RESTRICTIONS support.
    **     SVL 20230113          Improved performance by replacing some "for-each" loops with indexed "for" loops.
    **     HC  20230118          Eliminated some of the uses of String.toUpperCase and/or
    **                           String.toLowerCase for performance.
    **     CA  20230607          The leading underscore for _User metadata table fields depends on the minimal
    **                           conversion mode, so rely on this namespace attribute when resolving the field names.
    ** 060 CA  20230215          Improved other cases to reduce BDT usage from within FWD runtime.
    ** 061 GBB 20230512          Logging methods replaced by CentralLogger/ConversionStatus.
    ** 062 OM  20231215          Implemented DBCODEPAGE and DBCOLLATION 4GL functions.
    ** 063 GBB 20240110          -pf file resolvement to depend on configured file system (incl. client-side).
    ** 064 CA  20240319          Fixed ALIAS function to return the alias, and not the LDBNAME.
    ** 065 AL2 20240701          Added isBufferScopeLeaking to detect exceptional cases of buffer scope leaks.
    ** 066 RAA 20240424          Added map that stores the tenant ids based on the database names.
    **     RAA 20240426          Added tenantListeners list. The TenantChangeListeners are now notified when the
    **                           tenant changes.
    **     RAA 20240513          tenantListeners is now a map instead of a list.
    **                           Added notifyTenantListenersForReset methods.
    **     RAA 20240517          Fixed tenant listeners loop in setDbClient and in notifyTenantListenersForReset.
    **     RAA 20240627          Added deregisterTenantListener method.
    **     RAA 20240702          Added removeTenantId method. Added checks to prevent NPE for tenants.
    **     RAA 20240710          Made distinction between logical and physical database name.
    **     RAA 20240715          Clarified the type of database name occurrences.
    **     RAA 20240717          Removed notifyTenantListenersForReset methods.
    **     RAA 20240726          Fixed database retrieval in putTenantDatabaseConnected.
    **     RAA 20240807          Adjusted the tenant name retrieving process.
    **     RAA 20240808          Added tenant id to putConnected method.
    ** 067 OM  20240815          TenantManager.getDatabase() returns a Database object of same type as the
    **                           argument.
    ** 068 OM  20241001          Incremental update for tenant-related functionality.
    ** 069 OM  20241128          Multi-tenant runtime support: selected the proper persistence context, eventually
    **                           based on [sharedDb] parameter.
    **                           Implementation of [setMultiTenantAlternative] method.
    ** 070 OM  20241220          The list of active databases includes the default databases, too.
    ** 071 ICP 20250129          Used logical constants to leverage cached instances.
    ** 072 OM  20250303          Local optimizations.
    ** 073 GBB 20250403          Adding legacy log.
    ** 074 ES  20250407          Added getTimeOut method to get the value of timeout parameter.
    ** 075 AL2 20250522          Don't attempt to create a LOG-MANAGER when doing a session clean-up.
    */

    /*
    ** 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 may find a copy of the GNU Affero GPL version 3 at the following
    ** location: https://www.gnu.org/licenses/agpl-3.0.en.html
    **
    ** Additional terms under GNU Affero GPL version 3 section 7:
    **
    **   Under Section 7 of the GNU Affero GPL version 3, the following additional
    **   terms apply to the works covered under the License.  These additional terms
    **   are non-permissive additional terms allowed under Section 7 of the GNU
    **   Affero GPL version 3 and may not be removed by you.
    **
    **   0. Attribution Requirement.
    **
    **     You must preserve all legal notices or author attributions in the covered
    **     work or Appropriate Legal Notices displayed by works containing the covered
    **     work.  You may not remove from the covered work any author or developer
    **     credit already included within the covered work.
    **
    **   1. No License To Use Trademarks.
    **
    **     This license does not grant any license or rights to use the trademarks
    **     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
    **     of Golden Code Development Corporation. You are not authorized to use the
    **     name Golden Code, FWD, or the names of any author or contributor, for
    **     publicity purposes without written authorization.
    **
    **   2. No Misrepresentation of Affiliation.
    **
    **     You may not represent yourself as Golden Code Development Corporation or FWD.
    **
    **     You may not represent yourself for publicity purposes as associated with
    **     Golden Code Development Corporation, FWD, or any author or contributor to
    **     the covered work, without written authorization.
    **
    **   3. No Misrepresentation of Source or Origin.
    **
    **     You may not represent the covered work as solely your work.  All modified
    **     versions of the covered work must be marked in a reasonable way to make it
    **     clear that the modified work is not originating from Golden Code Development
    **     Corporation or FWD.  All modified versions must contain the notices of
    **     attribution required in this license.
    */

    package com.goldencode.p2j.persist;

    import java.math.*;
    import java.net.*;
    import java.nio.file.*;
    import java.util.*;
    import java.util.Map.*;
    import java.util.function.*;
    import java.util.logging.*;
    import java.util.stream.*;
    import com.goldencode.p2j.directory.*;
    import com.goldencode.p2j.net.*;
    import com.goldencode.p2j.net.Session;
    import com.goldencode.p2j.persist.dialect.*;
    import com.goldencode.p2j.persist.dirty.*;
    import com.goldencode.p2j.persist.meta.*;
    import com.goldencode.p2j.persist.orm.*;
    import com.goldencode.p2j.security.*;
    import com.goldencode.p2j.util.*;
    import com.goldencode.p2j.util.ErrorManager;
    import com.goldencode.p2j.util.logging.CentralLogger;
    import com.goldencode.p2j.util.ClientPrincipalResource.*;
    import com.goldencode.util.*;

    /**
     * Manages information about connected databases in the current context.
     * This includes the existence of connections and their status, and the
     * various aliases and logical names assigned to physical databases.  This
     * information is managed to support converted Progress logic, which relies
     * upon the state of this information to make logic decisions.  It also is
     * used to determine which physical database is associated with each record
     * buffer and its backing {@link Persistence} service object.
     * <p>
     * A connection in the sense of this class is not a database-level connection,
     * such as a JDBC connection.  Database-level connections are managed by the
     * <code>Persistence</code> class transparently.  A connection to this class
     * is a logical construct inherited from converted Progress code.  Connecting
     * and disconnecting from a database at this level does not create or destroy
     * a JDBC-level connection resource.  However, for transient databases at
     * least, it may affect the loading and unloading of database configurations
     * at the {@link DatabaseManager} level.  That is, the first time any client
     * context connects to a transient database (i.e., one that is not always
     * loaded by the server by default), that database's configuration is loaded
     * temporarily by the server.  The last time a client context disconnects
     * from a transient database, that database's configuration is unloaded.
     * <p>
     * Connections register a physical database name to a logical database name,
     * which can be different or the same as the physical name.  A database may
     * not be connected more than once without first disconnecting.  Once
     * disconnected, it may be connected again.  Only one physical database may
     * be registered to a given logical name at a time.  Methods are provided to
     * connect and disconnect, as well as to query information about connected
     * databases, such as their physical and logical names, how many are
     * connected, their types and restrictions.
     * <p>
     * Any number of aliases may be assigned to a given logical name.  Aliases
     * may only be created for connected databases, though they survive database
     * disconnect.  They may be explicitly deleted, however.
     * <p>
     * Whenever a new record buffer scope is opened, it is registered with its
     * context's connection manager.  References from buffers to their backing,
     * physical databases are tracked, to enable the correct processing of
     * database disconnect requests.  Disconnects can only occur immediately if
     * all procedures which reference the target database are complete.  This
     * translates to a state where no record buffers reference the database.
     * Furthermore, even if no buffers reference a database, it may not be
     * disconnected if the active code point currently is within a transaction.
     * At the end of the transaction, all disconnects which were deferred are
     * processed.
     * <p>
     * A single instance of this class is created for each client context.  The
     * constructor is not called directly by client code;  rather, access is
     * provided through the static {@link #get} method.
     * <p>
     * Currently, the Progress semantic of database-level auto connect lists is
     * not supported.
     */
    public final class ConnectionManager
    {
       /** Logger */
       private static final CentralLogger LOG = CentralLogger.get(ConnectionManager.class.getName());

       /** Context local instance of this class */
       private static final ContextLocal<ConnectionManager> local =
       new ContextLocal<ConnectionManager>()
       {
          @Override
          public WeightFactor getWeight()
          {
             return WeightFactor.LEVEL_5;
          }

          protected ConnectionManager initialValue()
          {
             return new ConnectionManager();
          }

          protected void cleanup(ConnectionManager connMgr)
          {
             connMgr.sessionClosed();
          }
       };

       /**
        * Collect all the DB connections so that they are cleaned up before anything else (as
        * these are dependent on persistence and other context-local data.
        */
       private static final ContextLocal<ConnectionDispatcher> metaCleaner =
       new ContextLocal<ConnectionDispatcher>()
       {
          @Override
          public WeightFactor getWeight()
          {
             return WeightFactor.FIRST;
          }

          @Override
          protected void cleanup(ConnectionDispatcher dispatcher)
          {
             dispatcher.cleanup();
          }
       };

       /** DICTDB alias name */
       private static final String DICTDB = "dictdb";

       /** Default DBVERSION text */
       private static final String DEFAULT_DBVERSION = "11";

       /** Default number of database connection retries */
       // TODO: "50" is documented as the "single-user default"; "multi-user default" is documented
       // as "-". Unclear what this means.
       private static final int DEFAULT_CONN_RETRIES = 50;

       /** Milliseconds to wait between connection retries */
       private static final long CONN_RETRY_DELAY = 1000L;

       /** Cache of database versions, indexed by PDB name */
       private static final Map<String, String> dbVersionCache = new HashMap<>();

       /** Next available connect ID for metadata updates */
       private static long nextConnectID = 0L;

       /** Set of locked database. */
       private final Set<String> locked = new CaseInsensitiveHashSet<>();

       /** Map of registered CLIENT-PRINCIPAL objects, for each database. */
       private final Map<String, ClientPrincipal> clients = new HashMap<>();

       /**
        * Map with the userids for the currently connected databases.
        * This map must be kept in sync with the <code>connected</code> map of the databases.
        */
       private final Map<String, String> userIds = new LinkedHashMap<>();

       /**
        * Stores the userid used at connection time whether the database is connected using
        * command-line (directory implementation) or programatically using <code>CONNECT</code>
        * with -U/-P parameters.
        * This list is not affected by later <code>SETUSERID</code> calls.
        */
       private final Map<String, String> connectTimeUserid = new LinkedHashMap<>();

       /** Map of logical database names to <code>ConnectInfo</code> objects */
       private final Map<String, ConnectInfo> connected = new CaseInsensitiveLinkedHashMap<>();

       /** Map of physical database helper objects to logical database names */
       private final Map<Database, String> ldbMap = new HashMap<>();

       /** Databases awaiting disconnect (first level) */
       private final Set<Database> pendingDisconn = new HashSet<>();

       /** Databases awaiting disconnect (second level) */
       private final Set<Database> pendingTxDisconn = new HashSet<>();

       /** Map of active aliases to logical database names */
       private final Map<String, String> aliases = new CaseInsensitiveLinkedHashMap<>();

       /** Map of buffer sets currently referencing databases */
       private final Map<Database, Set<RecordBuffer>> pdbReferences = new HashMap<>();

       /** Map of database info objects to network sessions */
       private final Map<Database, Session> sessions = new HashMap<>();

       /** Map of tenant names mapped by database logical names. */
       private final Map<String, String> tenantsByDatabase = new HashMap<>();

       /** Map with registered listeners for tenant changing processes based on the database logical name. */
       private final Map<String, Set<TenantChangeListener>> tenantListeners = new HashMap<>();

       /** Connect/disconnect event dispatcher */
       private final ConnectionDispatcher dispatcher;

       /**
        * Equivalent of -1 argument in CONNECT statement. At this moment it is not used
        * except for computing  DBPARAM function result.
        * TODO: Future implementation will address this flag.
        */
       private boolean singleClient = false;

       /**
        * This flags a registered (completely build) object. It is set after calling
        * {@link #register()}.
        */
       private int registerProgress = 0;

       /**
        * Default constructor.
        * <p>
        * Call {@link #register()} ASAP to actually connects to all permanent databases and register
        * with the transaction manager for master and global finish notifications.
        */
       private ConnectionManager()
       {
          // initialize connection- and user statistics-related metadata listeners, if any
          if (MetadataManager.inUse(MetadataManager._CONNECT) ||
              MetadataManager.inUse(MetadataManager._MYCONNECTION))
          {
             dispatcher = new ConnectionDispatcher();
             metaCleaner.set(dispatcher);
          }
          else
          {
             dispatcher = null;
          }
       }

       /**
        * Registers with the transaction manager for master and global finish notifications. Connects
        * to all permanent databases.
        * <p>
        * WARNING: Only call this method once!
        */
       private void register()
       {
          if (registerProgress > 2)
          {
             throw new RuntimeException("Already registered");
          }

          if (registerProgress == 1)
          {
             // registering in progress. This is the second call to this method. Return without
             // issuing any error to allow the previous all to build up the circular reference to the
             // BufferManager and this object
             return;
          }

          registerProgress = 1; // in progress

          // registration completed. Prevent any further registration
          registerProgress = 2;
       }

       /**
        * Retrieve the <code>ConnectionManager</code> instance for the local context.
        *
        * @return  Context local connection manager.
        */
       public static ConnectionManager get()
       {
          ConnectionManager cm = local.get();
          if (cm.registerProgress != 2)
          {
             cm.register();
             // now CM can be finally be used safely, without producing an infinite recursion
          }

          return cm;
       }

       /**
        * Retrieve the value of the LOCK_TIMEOUT startup parameter.
        *
        * @param   dbName
        *          Database name.
        *
        * @return  See above.
        */
       public static Long getTimeOut(String dbName)
       {
          ConnectionManager cm = local.get();
          ConnectInfo cntInfo = cm.connected.get(dbName);

          List<Map.Entry<ConnectOption, ConnectOptionValue>> opts = cntInfo != null ? cntInfo.opts : null;

          if (opts != null)
          {
             for (Map.Entry<ConnectOption, ConnectOptionValue> opt: opts)
             {
                if (opt.getKey() == ConnectOption.LOCK_TIMEOUT)
                {
                   return Long.valueOf(opt.getValue().intValue());
                }
             }
          }

          return 0L;
       }

       /**
        * Create a logical connection to a database using the specified
        * parameters.
        *
        * @param   args
        *          One or more connection parameters.  This may take one of several forms:
        *          <ul>
        *            <li>an ordered list of the following:
        *              <ol>
        *                <li>physical database name (required)
        *                <li>logical database name (optional)
        *                <li>P2J server host name/address (optional)
        *                <li>P2J server port number or service name (optional,
        *                    unless host name/address is supplied)
        *                <li>userid (optional)
        *                <li>password (optional, required if userid is specified)
        *              </ol>
        *            <li>a string with the above listed components, prefaced by
        *                the following identifiers, respectively:
        *              <ol>
        *                <li>-db
        *                <li>-ld
        *                <li>-H
        *                <li>-S
        *                <li>-U
        *                <li>-P
        *                <li>-ct
        *              </ol>
        *              For example:  <code>-db mydb -ld logical_name -H
        *              192.168.1.12 -S 3333</code>.  After <code>-db</code>,
        *              parameters can be in any order, so long as each is
        *              preceded by the specified option identifier.
        *            <li>any combination of string fragments, which, if
        *                concatenated together in the order supplied, would create
        *                a string matching the requirements of the previous item.
        *          </ul>
        *
        * @throws  ErrorConditionException
        *          if any parameter is null or represents the unknown value;
        *          if any parameter is neither a text value nor an integral
        *          value;
        *          if there is an error registering the database with the
        *          database manager.
        */
       public static void connect(Object... args)
       {
          connect(false, args);
       }

       /**
        * Create a logical connection to a database using the specified parameters.
        * @param isAutoConnect
        *        flag indication that is is an implicit auto-connect operation
        * @param   args
        *          One or more connection parameters.  This may take one of several forms:
        *          <ul>
        *            <li>an ordered list of the following:
        *              <ol>
        *                <li>physical database name (required)
        *                <li>logical database name (optional)
        *                <li>P2J server host name/address (optional)
        *                <li>P2J server port number or service name (optional,
        *                    unless host name/address is supplied)
        *                <li>userid (optional)
        *                <li>password (optional, required if userid is specified)
        *              </ol>
        *            <li>a string with the above listed components, prefaced by
        *                the following identifiers, respectively:
        *              <ol>
        *                <li>-db
        *                <li>-ld
        *                <li>-H
        *                <li>-S
        *                <li>-U
        *                <li>-P
        *                <li>-ct
        *              </ol>
        *              For example:  <code>-db mydb -ld logical_name -H
        *              192.168.1.12 -S 3333</code>.  After <code>-db</code>,
        *              parameters can be in any order, so long as each is
        *              preceded by the specified option identifier.
        *            <li>any combination of string fragments, which, if
        *                concatenated together in the order supplied, would create
        *                a string matching the requirements of the previous item.
        *          </ul>
        *
        * @throws  ErrorConditionException
        *          if any parameter is null or represents the unknown value;
        *          if any parameter is neither a text value nor an integral
        *          value;
        *          if there is an error registering the database with the
        *          database manager.
        */
       public static void connect(boolean isAutoConnect, Object... args)
       {
          try
          {
             if (LOG.isLoggable(Level.FINE))
             {
                String s = Arrays.stream(args)
                                 .map(a -> "[" + (a == null ? "<null>" : a + "]"))
                                 .collect(Collectors.joining(","));
                LOG.log(Level.FINE, "connect("+s +")", new Exception());

             }
             connect_(isAutoConnect, args);
          }
          catch (Throwable t)
          {
             LOG.log(Level.SEVERE, "CONNECT failed", t);
             throw t;
          }
       }

       /**
        * Create a logical connection to a database using the specified
        * parameters.
        *
        * @param isAutoConnect
        *        flag indication that is is an implicit auto-connect operation
        * @param   args
        *          One or more connection parameters.  This may take one of several forms:
        *
        * @throws  ErrorConditionException
        *          see above
        */
       private static void connect_(boolean isAutoConnect, Object... args)
       {
          // options will be parsed into individual components and stored in a list for further processing.
          List<Object> options = new ArrayList<>();

          try
          {
             for (Object arg : args)
             {
                String text = null;
                Integer num = null;

                if (arg instanceof character)
                {
                   character t = (character) arg;
                   assertKnownValue(t, "Connect option");
                   text = t.toStringMessage();
                }
                else if (arg instanceof String)
                {
                   text = (String) arg;
                }
                else if (arg instanceof int64)
                {
                   int64 i = (int64) arg;
                   assertKnownValue(i, "Connect option");
                   num = i.toJavaIntegerType();
                }
                else if (arg instanceof Integer)
                {
                   num = (Integer) arg;
                }
                else
                {
                   throw new IllegalArgumentException("Unexpected connect option:  " + arg);
                }

                if (text != null)
                {
                   if (text.trim().isEmpty())
                   {
                      continue;
                   }

                   // Parse this string, which may represent a single connect
                   // parameter, or multiple parameters delimited by whitespace.
                   String[] parts = text.split(" ");
                   for (String next : parts)
                   {
                      // Add each separate parameter into the options list.
                      options.add(next.trim());
                   }
                }
                else
                {
                   // Add this non-text parameter into the options list.
                   options.add(num);
                }
             }

             // Analyze all options and attempt connection.
             List<List<Map.Entry<ConnectOption, ConnectOptionValue>>> parsedOpts = parse(options);
             if (parsedOpts == null)
             {
                ErrorManager.recordOrThrowError(325, "Unable to execute CONNECT statement", false);
                return;
             }
             if (LOG.isLoggable(Level.FINE))
             {
                for (List<Map.Entry<ConnectOption, ConnectOptionValue>>  opts: parsedOpts)
                {
                   LOG.log(Level.FINE, dbparam(opts));
                }
             }
             for (List<Map.Entry<ConnectOption, ConnectOptionValue>>  opts: parsedOpts)
             {
                get().connectDbImpl(isAutoConnect, opts);
             }
          }
          catch (PersistenceException exc)
          {
             ErrorManager.recordOrThrowError(exc);
          }
       }

       /**
        * Parse CONNECT statement options.
        *
        * @param args statement arguments.
        *
        * @return  The parsed options (separately for all databases).
        *
        * @throws  PersistenceException
        *          if a fatal error was encountered
        */
       public static List<List<Map.Entry<ConnectOption, ConnectOptionValue>>> parse(List<Object> args)
       throws PersistenceException
       {
          return new OptionsParser(false).parse(args);
       }

       /**
        * Request a disconnect from a logical database.  This does not actually
        * perform the disconnect immediately, but registers <code>ldbName</code>
        * for disconnect at the appropriate time:  once no buffers reference the
        * database, and at the end of the current transaction (if any).
        *
        * @param   ldbName
        *          Logical name of the database to disconnect.
        *
        * @throws  ErrorConditionException
        *          if the specified database has not been opened.
        */
       public static void disconnect(character ldbName)
       {
          try
          {
             assertKnownValue(ldbName, "LDB name");
             disconnect(ldbName.toStringMessage());
          }
          catch (PersistenceException exc)
          {
             ErrorManager.recordOrThrowError(exc);
          }
       }

       /**
        * Request a disconnect from a logical database.  This does not actually
        * perform the disconnect immediately, but registers <code>ldbName</code>
        * for disconnect at the appropriate time:  once no buffers reference the
        * database, and at the end of the current transaction (if any).
        *
        * @param   ldbName
        *          Logical name of the database to disconnect.
        *
        * @throws  ErrorConditionException
        *          if the specified database has not been opened.
        */
       public static void disconnect(String ldbName)
       {
          try
          {
             get().disconnectImpl(ldbName);
          }
          catch (PersistenceException exc)
          {
             // not a fatal error
             ErrorManager.displayError(1009, "DISCONNECT:  Database " + ldbName + " is not open or unknown");
          }
       }

       /**
        * Tests whether a logical connection to a database exists in this context,
        * given a logical database name or alias.
        *
        * @param   name
        *          Logical database name or alias.
        *
        * @return  <code>true</code> if a logical connection exists;  else
        *          <code>false</code>.
        */
       public static logical connected(character name)
       {
          if (name.isUnknown())
          {
             return new logical();
          }

          return get().connectedImpl(name);
       }

       /**
        * Tests whether a logical connection to a database exists in this context,
        * given a logical database name or alias.
        *
        * @param   name
        *          Logical database name or alias.
        *
        * @return  <code>true</code> if a logical connection exists;  else
        *          <code>false</code>.
        */
       public static logical connected(String name)
       {
          return connected(new character(name));
       }

       /**
        * Get the number of aliases currently defined in this context.
        *
        * @return  Number of aliases in this context.
        */
       public static integer numAliases()
       {
          return new integer(get().aliases.size());
       }

       /**
        * Get the number of databases currently connected in this context.
        *
        * @return  Number of logical connections in this context.
        */
       public static int getConnectedDbCount()
       {
          return get().connected.size();
       }

       /**
        * Get the number of databases currently connected in this context.
        *
        * @return  Number of logical connections in this context.
        */
       public static integer numDBs()
       {
          return new integer(getConnectedDbCount());
       }

       /**
        * Implementation of the DBCODEPAGE function.
        *
        * @param   seqNo
        *          The sequence number of the connected database.
        *
        * @return  The CP of the specified database.
        */
       public static character dbCodepage(NumberType seqNo)
       {
          return seqNo.isUnknown() ? new character() : dbCodepage(seqNo.intValue());
       }

       /**
        * Implementation of the DBCODEPAGE function.
        *
        * @param   index
        *          The sequence number of the connected database.
        *
        * @return  The CP of the specified database.
        */
       public static character dbCodepage(int index)
       {
          ConnectionManager cm = get();
          if (index < 1 || index > cm.connected.size())
          {
             return new character();
          }

          return dbCodepage(iterateToIndex(cm.connected.keySet(), index));
       }

       /**
        * Implementation of the DBCODEPAGE function.
        *
        * @param   alias
        *          The logical name or alias of the connected database.
        *
        * @return  The CP of the specified database.
        */
       public static character dbCodepage(character alias)
       {
          return alias.isUnknown() ? new character() : dbCodepage(alias.toJavaType());
       }

       /**
        * Implementation of the DBCODEPAGE function.
        *
        * @param   alias
        *          The logical name or alias of the connected database.
        *
        * @return  The CP of the specified database.
        */
       public static character dbCodepage(String alias)
       {
          if (alias == null || alias.isEmpty())
          {
             return new character();
          }

          Persistence p = getPersistence(alias);
          return (p == null) ? new character() : new character(p.getCodepage());
       }

       /**
        * Implementation of the DBCOLLATION function.
        *
        * @param   seqNo
        *          The sequence number of the connected database.
        *
        * @return  The collation of the specified database.
        */
       public static character dbCollation(NumberType seqNo)
       {
          return seqNo.isUnknown() ? new character() : dbCollation(seqNo.intValue());
       }

       /**
        * Implementation of the DBCOLLATION function.
        *
        * @param   index
        *          The sequence number of the connected database.
        *
        * @return  The collation of the specified database.
        */
       public static character dbCollation(int index)
       {
          ConnectionManager cm = get();
          if (index < 1 || index > cm.connected.size())
          {
             return new character();
          }

          return dbCollation(iterateToIndex(cm.connected.keySet(), index));
       }

       /**
        * Implementation of the DBCOLLATION function.
        *
        * @param   alias
        *          The logical name or alias of the connected database.
        *
        * @return  The collation of the specified database.
        */
       public static character dbCollation(character alias)
       {
          return alias.isUnknown() ? new character() : dbCollation(alias.toJavaType());
       }

       /**
        * Implementation of the DBCOLLATION function.
        *
        * @param   alias
        *          The logical name or alias of the connected database.
        *
        * @return  The collation of the specified database.
        */
       public static character dbCollation(String alias)
       {
          if (alias == null || alias.isEmpty())
          {
             return new character();
          }

          Persistence p = getPersistence(alias);
          return (p == null) ? new character() : new character(p.getCollation());
       }

       /**
        * Get the name of the connected database in this context.  If more than
        * one, return the first.
        *
        * @return  First or only connected database, or unknown value if none.
        */
       public static character dbName()
       {
          return pdbName(1);
       }

       /**
        * Implementation of the <code>DBPARAM()</code> Progress function.
        * Returns, as a character string, a comma-separated list of the parameters
        * used to connect to the database.
        *
        * <p>The sequence number of a database the Progress session is connected
        * to. For example, DBPARAM(1) returns information on the first database
        * the Progress session is connected to, DBPARAM(2) returns information
        * on the second database the Progress session is connected to, etc.
        * If you specify a sequence number that does not correspond to a database
        * the Progress session is connected to, the DBPARAM function returns the
        * unknown value (?).
        *
        * @param   index
        *          One-based index of the target database among all databases
        *          currently connected in this context.
        *
        * @return  A comma-separated list of the parameters used to connect to
        *             the database
        */
       public static character dbParam(int index)
       {
          if (index == 0)
          {
             return new character();
          }

          return get().dbParamImpl(null, index);
       }

       /**
        * Implementation of the <code>DBPARAM()</code> Progress function.
        * Returns, as a character string, a comma-separated list of the parameters
        * used to connect to the database.
        *
        * <p>The sequence number of a database the Progress session is connected
        * to. For example, DBPARAM(1) returns information on the first database
        * the Progress session is connected to, DBPARAM(2) returns information
        * on the second database the Progress session is connected to, etc.
        * If you specify a sequence number that does not correspond to a database
        * the Progress session is connected to, the DBPARAM function returns the
        * unknown value (?).
        *
        * @param   index
        *          One-based index of the target database among all databases
        *          currently connected in this context.
        *
        * @return  A comma-separated list of the parameters used to connect to
        *             the database
        */
       public static character dbParam(NumberType index)
       {
          if (index == null || index.isUnknown())
          {
             return new character();
          }

          return get().dbParamImpl(null, index.intValue());
       }

       /**
        * Implementation of the <code>DBPARAM()</code> Progress function.
        * Returns, as a character string, a comma-separated list of the parameters
        * used to connect to the database.
        *
        * <p>This forms of the <code>DBPARAM()</code> function require a character
        * expression as a parameter. If the parameter is an alias or the logical
        * name of a connected database, then Progress returns the comma-separated
        * parameter list. Otherwise, it returns the unknown value (?).
        *
        * @param   nameOrAlias
        *          The logical-name or alias of a connected database
        *
        * @return  A comma-separated list of the parameters used to connect to
        *             the database
        */
       public static character dbParam(String nameOrAlias)
       {
          if (nameOrAlias == null || nameOrAlias.isEmpty())
          {
             return new character();
          }

          return get().dbParamImpl(nameOrAlias, 0);
       }

       /**
        * Implementation of the <code>DBPARAM()</code> Progress function.
        * Returns, as a character string, a comma-separated list of the parameters
        * used to connect to the database.
        *
        * <p>This forms of the <code>DBPARAM()</code> function require a character
        * expression as a parameter. If the parameter is an alias or the logical
        * name of a connected database, then Progress returns the comma-separated
        * parameter list. Otherwise, it returns the unknown value (?).
        *
        * @param   nameOrAlias
        *          The logical-name or alias of a connected database
        *
        * @return  A comma-separated list of the parameters used to connect to
        *             the database
        */
       public static character dbParam(character nameOrAlias)
       {
          if (nameOrAlias == null || nameOrAlias.isUnknown())
          {
             return new character();
          }

          character ret = get().dbParamImpl(nameOrAlias.toStringMessage(), 0);
          ret.setCaseSensitive(nameOrAlias.isCaseSensitive());
          return ret;
       }

       /**
        * Get a brief descriptor of the database type with the specified
        * connection index.
        *
        * @param   index
        *          One-based index of the target database among all databases
        *          currently connected in this context.
        *
        * @return  The string "PROGRESS" or unknown value if <code>index</code>
        *          represents the unknown value or is outside of the range of
        *          connected databases.
        */
       public static character dbType(NumberType index)
       {
          if (index.isUnknown())
          {
             return new character();
          }

          return dbType(index.intValue());
       }

       /**
        * Get a brief descriptor of the database type with the specified
        * connection index.
        *
        * @param   index
        *          One-based index of the target database among all databases
        *          currently connected in this context.
        *
        * @return  The string "PROGRESS" or unknown value if <code>index</code>
        *          represents the unknown value or is outside of the range of
        *          connected databases.
        */
       public static character dbType(int index)
       {
          if (index < 1 || index > get().connected.size())
          {
             return new character();
          }

          return new character("PROGRESS"); // TODO: shouldn't we use FWD or FORWARD as type for FWD backed databases?
       }

       /**
        * Get a brief descriptor of the database type with the specified logical
        * name or alias.
        *
        * @param   name
        *          Logical name or alias of the target database.
        *
        * @return  The string "PROGRESS" or unknown value if <code>name</code>
        *          is not a recognized logical name or alias.
        */
       public static character dbType(character name)
       {
          if (name.isUnknown())
          {
             return new character();
          }

          character ret = dbType(name.toStringMessage());
          ret.setCaseSensitive(name.isCaseSensitive());

          return ret;
       }

       /**
        * Get a brief descriptor of the database type with the specified logical
        * name or alias.
        *
        * @param   name
        *          Logical name or alias of the target database.
        *
        * @return  The string "PROGRESS" or unknown value if <code>name</code>
        *          is not a recognized logical name or alias.
        */
       public static character dbType(String name)
       {
          String ldb = get().getLDBName(name);
          if (ldb == null)
          {
             return new character();
          }

          return new character("PROGRESS");  // TODO: shouldn't we use FWD or FORWARD as type for FWD backed databases?
       }

       /**
        * Get the identifier of the current transaction for the given database.
        *
        * @param   name
        *          Logical name or alias of a connected database.
        *
        * @return  Current transaction identifier, or unknown value if {@code name} is not the name
        *          or an alias for a connected database, or if there is no active transaction.
        */
       public static integer dbTaskId(character name)
       {
          if (name == null || name.isUnknown())
          {
             return new integer();
          }

          return get().dbTaskIdImpl(name.toJavaType(), 0);
       }

       /**
        * Get the identifier of the current transaction for the given database.
        *
        * @param   name
        *          Logical name or alias of a connected database.
        *
        * @return  Current transaction identifier, or unknown value if {@code name} is not the name
        *          or an alias for a connected database, or if there is no active transaction.
        */
       public static integer dbTaskId(String name)
       {
          if (name == null)
          {
             return new integer();
          }

          return get().dbTaskIdImpl(name, 0);
       }

       /**
        * Get the identifier of the current transaction for the given database.
        *
        * @param   index
        *          One-based index of the target database among all databases currently connected in
        *          this context.
        *
        * @return  Current transaction identifier, or unknown value if {@code index} is not an index
        *          of a connected database, or if there is no active transaction.
        */
       public static integer dbTaskId(NumberType index)
       {
          if (index == null || index.isUnknown())
          {
             return new integer();
          }

          return get().dbTaskIdImpl(null, index.intValue());
       }

       /**
        * Get the identifier of the current transaction for the given database.
        *
        * @param   index
        *          One-based index of the target database among all databases currently connected in
        *          this context.
        *
        * @return  Current transaction identifier, or unknown value if {@code index} is not an index
        *          of a connected database, or if there is no active transaction.
        */
       public static integer dbTaskId(int index)
       {
          return get().dbTaskIdImpl(null, index);
       }

       /**
        * Get the version number of the target database.
        *
        * @param   index
        *          One-based index of the target database among all databases currently connected in
        *          this context.
        *
        * @return  The database's version, as extracted from runtime configuration, or unknown value
        *          if <code>index</code> represents the unknown value or is outside of the range of
        *          connected databases.
        */
       public static character dbVersion(NumberType index)
       {
          if (index.isUnknown())
          {
             return new character();
          }

          return dbVersion(index.intValue());
       }

       /**
        * Get the version number of the target database.
        *
        * @param   index
        *          One-based index of the target database among all databases currently connected in
        *          this context.
        *
        * @return  The database's version, as extracted from runtime configuration, or unknown value
        *          if <code>index</code> is outside of the range of connected databases.
        */
       public static character dbVersion(int index)
       {
          return get().dbVersionImpl(null, index);
       }

       /**
        * Get the version number of the target database.
        *
        * @param   name
        *          Logical name or alias of the target database.
        *
        * @return  The database's version, as extracted from runtime configuration, or unknown value
        *          if <code>name</code> represents the unknown value or is not a recognized logical
        *          name or alias.
        */
       public static character dbVersion(character name)
       {
          if (name.isUnknown())
          {
             return new character();
          }

          return dbVersion(name.toStringMessage());
       }

       /**
        * Get the version number of the target database.
        *
        * @param   name
        *          Logical name or alias of the target database.
        *
        * @return  The database's version, as extracted from runtime configuration, or unknown value
        *          if <code>name</code> is not a recognized logical name or alias.
        */
       public static character dbVersion(String name)
       {
          return get().dbVersionImpl(name, 0);
       }

       /**
        * Get a description of restrictions for the connected database at the
        * specified index.
        *
        * @param   index
        *          One-based index of the target database among all databases
        *          currently connected in this context.
        *
        * @return  The empty string, or unknown value if <code>index</code>
        *          represents the unknown value or is outside of the range of
        *          connected databases.
        */
       public static character dbRestrictions(NumberType index)
       {
          if (index.isUnknown())
          {
             return new character();
          }

          return dbRestrictions(index.intValue(), (String) null);
       }

       /**
        * Get a description of restrictions for the connected database at the
        * specified index.
        *
        * @param   index
        *          One-based index of the target database among all databases
        *          currently connected in this context.
        *
        * @return  The empty string, or unknown value if <code>index</code> is
        *          invalid.
        */
       public static character dbRestrictions(int index)
       {
          return get().dbRestrictionsImpl(null, index, null);
       }

       /**
        * Get a description of restrictions for the connected database at the
        * specified index.  Currently, <code>table</code> is ignored.
        *
        * @param   index
        *          One-based index of the target database among all databases
        *          currently connected in this context.
        * @param   table
        *          Name of a table within the target database.  Ignored.
        *
        * @return  The empty string, or unknown value if <code>index</code> is
        *          invalid.
        */
       public static character dbRestrictions(NumberType index, character table)
       {
          if (index.isUnknown())
          {
             return new character();
          }

          return get().dbRestrictionsImpl(null, index.intValue(), null);
       }

       /**
        * Get a description of restrictions for the connected database at the
        * specified index.  Currently, <code>table</code> is ignored.
        *
        * @param   index
        *          One-based index of the target database among all databases
        *          currently connected in this context.
        * @param   table
        *          Name of a table within the target database.  Ignored.
        *
        * @return  The empty string, or unknown value if <code>index</code> is
        *          invalid.
        */
       public static character dbRestrictions(NumberType index, String table)
       {
          if (index.isUnknown())
          {
             return new character();
          }

          return get().dbRestrictionsImpl(null, index.intValue(), null);
       }

       /**
        * Get a description of restrictions for the connected database at the
        * specified index.  Currently, <code>table</code> is ignored.
        *
        * @param   index
        *          One-based index of the target database among all databases
        *          currently connected in this context.
        * @param   table
        *          Name of a table within the target database.  Ignored.
        *
        * @return  The empty string, or unknown value if <code>index</code> is
        *          invalid.
        */
       public static character dbRestrictions(int index, character table)
       {
          return get().dbRestrictionsImpl(null, index, null);
       }

       /**
        * Get a description of restrictions for the connected database at the
        * specified index.  Currently, <code>table</code> is ignored.
        *
        * @param   index
        *          One-based index of the target database among all databases
        *          currently connected in this context.
        * @param   table
        *          Name of a table within the target database.  Ignored.
        *
        * @return  The empty string, or unknown value if <code>index</code> is
        *          invalid.
        */
       public static character dbRestrictions(int index, String table)
       {
          if (index < 1 || index > get().connected.size())
          {
             return (new character());
          }

          return get().dbRestrictionsImpl(null, index, null);
       }

       /**
        * Get a description of restrictions for the connected database with the
        * specified logical name or alias.
        *
        * @param   name
        *          Logical name or alias of the target database.
        *
        * @return  The empty string, or unknown value if <code>name</code>
        *          is not a recognized logical name or alias.
        */
       public static character dbRestrictions(character name)
       {
          return dbRestrictions(name, (character) null);
       }

       /**
        * Get a description of restrictions for the connected database with the
        * specified logical name or alias.
        *
        * @param   name
        *          Logical name or alias of the target database.
        *
        * @return  The empty string, or unknown value if <code>name</code>
        *          is not a recognized logical name or alias.
        */
       public static character dbRestrictions(String name)
       {
          return get().dbRestrictionsImpl(name, 0, null);
       }

       /**
        * Get a description of restrictions for the connected database with the
        * specified logical name or alias.  Currently, <code>table</code> is
        * ignored.
        *
        * @param   name
        *          Logical name or alias of the target database.
        * @param   table
        *          Name of a table within the target database.  Ignored.
        *
        * @return  The empty string, or unknown value if <code>name</code>
        *          is not a recognized logical name or alias.
        */
       public static character dbRestrictions(character name, character table)
       {
          if (name == null || name.isUnknown())
          {
             return new character();
          }

          character ret = get().dbRestrictionsImpl(name.toStringMessage(), 0, null);
          ret.setCaseSensitive(name.isCaseSensitive());
          return ret;
       }

       /**
        * Get a description of restrictions for the connected database with the
        * specified logical name or alias.  Currently, <code>table</code> is
        * ignored.
        *
        * @param   name
        *          Logical name or alias of the target database.
        * @param   table
        *          Name of a table within the target database.  Ignored.
        *
        * @return  The empty string, or unknown value if <code>name</code>
        *          is not a recognized logical name or alias.
        */
       public static character dbRestrictions(character name, String table)
       {
          return dbRestrictions(name, new character(table));
       }

       /**
        * Get a description of restrictions for the connected database with the
        * specified logical name or alias.  Currently, <code>table</code> is
        * ignored.
        *
        * @param   name
        *          Logical name or alias of the target database.
        * @param   table
        *          Name of a table within the target database.  Ignored.
        *
        * @return  The empty string, or unknown value if <code>name</code>
        *          is not a recognized logical name or alias.
        */
       public static character dbRestrictions(String name, character table)
       {
          return dbRestrictions(new character(name), table);
       }

       /**
        * Get a description of restrictions for the connected database with the
        * specified logical name or alias.  Currently, <code>table</code> is
        * ignored.
        *
        * @param   name
        *          Logical name or alias of the target database.
        * @param   table
        *          Name of a table within the target database.  Ignored.
        *
        * @return  The empty string, or unknown value if <code>name</code>
        *          is not a recognized logical name or alias.
        */
       public static character dbRestrictions(String name, String table)
       {
          if (name == null)
          {
             return new character();
          }

          return get().dbRestrictionsImpl(name, 0, null);
       }

       /**
        * Create an alias for a logical database name within the current context.
        * The alias may not conflict with an existing logical database name
        * (except for that of the database which it aliases).  The alias will be
        * created regardless of whether the target database currently is
        * connected;  however, an error will be raised (or silently registered)
        * in the event it is not, after the alias has been created.  The alias
        * persists until explicitly deleted with one of the variants of the
        * <code>deleteAlias</code> method, or until the current session ends.
        *
        * @param   alias
        *          Alias to be created.
        * @param   ldbName
        *          Logical name of the database to which the alias is assigned.
        *
        * @throws  ErrorConditionException
        *          if silent error mode is disabled and any of the following:
        *          <ul>
        *            <li><code>alias</code> represents the unknown value;
        *            <li><code>ldbName</code> represents the unknown value;
        *            <li>the target database is not connected;
        *            <li>the alias conflicts with an existing logical database
        *                name, other than <code>ldbName</code>.
        *          </ul>
        */
       public static void createAlias(character alias, character ldbName)
       {
          try
          {
             assertKnownValue(alias, "Alias");
             assertKnownValue(ldbName, "LDB name");
             createAlias(alias.toStringMessage(), ldbName.toStringMessage());
          }
          catch (PersistenceException exc)
          {
             ErrorManager.recordOrThrowError(exc);
          }
       }

       /**
        * Create an alias for a logical database name within the current context.
        * The alias may not conflict with an existing logical database name
        * (except for that of the database which it aliases).  The alias will be
        * created regardless of whether the target database currently is
        * connected;  however, an error will be raised (or silently registered)
        * in the event it is not, after the alias has been created.  The alias
        * persists until explicitly deleted with one of the variants of the
        * <code>deleteAlias</code> method, or until the current session ends.
        *
        * @param   alias
        *          Alias to be created.
        * @param   ldbName
        *          Logical name of the database to which the alias is assigned.
        *
        * @throws  ErrorConditionException
        *          if silent error mode is disabled and any of the following:
        *          <ul>
        *            <li><code>ldbName</code> represents the unknown value;
        *            <li>the target database is not connected;
        *            <li>the alias conflicts with an existing logical database
        *                name, other than <code>ldbName</code>.
        *          </ul>
        */
       public static void createAlias(String alias, character ldbName)
       {
          try
          {
             assertKnownValue(ldbName, "LDB name");
             createAlias(alias, ldbName.toStringMessage());
          }
          catch (PersistenceException exc)
          {
             ErrorManager.recordOrThrowError(exc);
          }
       }

       /**
        * Create an alias for a logical database name within the current context.
        * The alias may not conflict with an existing logical database name
        * (except for that of the database which it aliases).  The alias will be
        * created regardless of whether the target database currently is
        * connected;  however, an error will be raised (or silently registered)
        * in the event it is not, after the alias has been created.  The alias
        * persists until explicitly deleted with one of the variants of the
        * <code>deleteAlias</code> method, or until the current session ends.
        *
        * @param   alias
        *          Alias to be created.
        * @param   ldbName
        *          Logical name of the database to which the alias is assigned.
        *
        * @throws  ErrorConditionException
        *          if silent error mode is disabled and any of the following:
        *          <ul>
        *            <li><code>alias</code> represents the unknown value;
        *            <li>the target database is not connected;
        *            <li>the alias conflicts with an existing logical database
        *                name, other than <code>ldbName</code>.
        *          </ul>
        */
       public static void createAlias(character alias, String ldbName)
       {
          try
          {
             assertKnownValue(alias, "Alias");
             createAlias(alias.toStringMessage(), ldbName);
          }
          catch (PersistenceException exc)
          {
             ErrorManager.recordOrThrowError(exc);
          }
       }

       /**
        * Create an alias for a logical database name within the current context.
        * The alias may not conflict with an existing logical database name
        * (except for that of the database which it aliases).  The alias will be
        * created regardless of whether the target database currently is
        * connected;  however, an error will be raised (or silently registered)
        * in the event it is not, after the alias has been created.  The alias
        * persists until explicitly deleted with one of the variants of the
        * <code>deleteAlias</code> method, or until the current session ends.
        *
        * @param   alias
        *          Alias to be created.
        * @param   ldbName
        *          Logical name of the database to which the alias is assigned.
        *
        * @throws  ErrorConditionException
        *          if silent error mode is disabled and any of the following:
        *          <ul>
        *            <li>the target database is not connected;
        *            <li>the alias conflicts with an existing logical database
        *                name, other than <code>ldbName</code>.
        *          </ul>
        */
       public static void createAlias(String alias, String ldbName)
       {
          try
          {
             get().createAliasImpl(alias, ldbName);
          }
          catch (PersistenceException exc)
          {
             ErrorManager.recordOrThrowError(exc);
          }
       }

       /**
        * Delete the specified alias immediately.
        *
        * @param   alias
        *          Alias to be deleted.
        */
       public static void deleteAlias(character alias)
       {
          if (alias.isUnknown())
          {
             return;
          }

          deleteAlias(alias.toStringMessage());
       }

       /**
        * Delete the specified alias immediately.
        *
        * @param   alias
        *          Alias to be deleted.
        */
       public static void deleteAlias(String alias)
       {
          get().deleteAliasImpl(alias);
       }

       /**
        * Retrieve the alias at the specified index in the current context.
        *
        * @param   index
        *          One-based index of the alias (aliases are stored in the order
        *          in which they were created).
        *
        * @return  The target alias, or unknown value if <code>index</code> is
        *          invalid.
        */
       public static character alias(NumberType index)
       {
          if (index.isUnknown())
          {
             return (new character());
          }

          return alias(index.intValue());
       }

       /**
        * Retrieve the alias at the specified index in the current context.
        *
        * @param   index
        *          One-based index of the alias (aliases are stored in the order
        *          in which they were created).
        *
        * @return  The target alias, or unknown value if <code>index</code> is
        *          invalid.
        */
       public static character alias(int index)
       {
          return get().aliasImpl(index);
       }

       /**
        * Get the logical database name for the connected database at the given
        * index.
        *
        * @param   index
        *          One-based index of the target database among all databases
        *          currently connected in this context.
        *
        * @return  The logical name, or unknown value if <code>index</code> is
        *          invalid.
        */
       public static character ldbName(NumberType index)
       {
          if (index.isUnknown())
          {
             return (new character());
          }

          return ldbName(index.intValue());
       }

       /**
        * Get the logical database name for the connected database at the given
        * index.
        *
        * @param   index
        *          One-based index of the target database among all databases
        *          currently connected in this context.
        *
        * @return  The logical name, or unknown value if <code>index</code> is
        *          invalid.
        */
       public static character ldbName(int index)
       {
          return get().ldbNameImpl(null, index);
       }

       /**
        * Get the logical database name for the connected database with the given
        * logical name or alias.
        *
        * @param   name
        *          Logical name or alias of the target database.
        *
        * @return  The logical name, or unknown value if <code>name</code> is
        *          invalid.
        */
       public static character ldbName(character name)
       {
          if (name.isUnknown())
          {
             return (new character());
          }

          return ldbName(name.toStringMessage());
       }

       /**
        * Get the logical database name for the connected database with the given
        * logical name or alias.
        *
        * @param   name
        *          Logical name or alias of the target database.
        *
        * @return  The logical name, or unknown value if <code>name</code> is
        *          invalid.
        */
       public static character ldbName(String name)
       {
          return get().ldbNameImpl(name, 0);
       }

       /**
        * Get the logical database name for the record buffer associated with the
        * given DMO.
        *
        * @param   dmo
        *          Data model object returned by a previous call to {@link
        *          RecordBuffer#define}.
        *
        * @return  The logical database name for the DMO.
        */
       public static character ldbName(Object dmo)
       {
          RecordBuffer buffer = ((BufferReference) dmo).buffer();

          return (new character(buffer.getLogicalDatabase()));
       }

       /**
        * Get the physical database name for the connected database at the given
        * index.
        *
        * @param   index
        *          One-based index of the target database among all databases
        *          currently connected in this context.
        *
        * @return  The physical name, or unknown value if <code>index</code> is
        *          invalid.
        */
       public static character pdbName(NumberType index)
       {
          if (index.isUnknown())
          {
             return (new character());
          }

          return pdbName(index.intValue());
       }

       /**
        * Get the physical database name for the connected database at the given
        * index.
        *
        * @param   index
        *          One-based index of the target database among all databases
        *          currently connected in this context.
        *
        * @return  The physical name, or unknown value if <code>index</code> is
        *          invalid.
        */
       public static character pdbName(int index)
       {
          return get().pdbNameImpl(null, index);
       }

       /**
        * Get the physical database name for the connected database with the
        * given logical name or alias.
        *
        * @param   name
        *          Logical name or alias of the target database.
        *
        * @return  The physical name, or unknown value if <code>name</code> is
        *          invalid.
        */
       public static character pdbName(character name)
       {
          if (name.isUnknown())
          {
             return (new character());
          }

          return pdbName(name.toStringMessage());
       }

       /**
        * Get the physical database name for the connected database with the
        * given logical name or alias.
        *
        * @param   name
        *          Logical name or alias of the target database.
        *
        * @return  The physical name, or unknown value if <code>name</code> is
        *          invalid.
        */
       public static character pdbName(String name)
       {
          return get().pdbNameImpl(name, 0);
       }

       /**
        * Get the logical database name for the connected database at the given
        * index.
        * <p>
        * The current implementation delegates to {@link #ldbName(NumberType)}.
        *
        * @param   index
        *          One-based index of the target database among all databases
        *          currently connected in this context.
        *
        * @return  The logical name, or unknown value if <code>index</code> is
        *          invalid.
        */
       public static character sdbName(NumberType index)
       {
          if (index.isUnknown())
          {
             return (new character());
          }

          return sdbName(index.intValue());
       }

       /**
        * Get the logical database name for the connected database at the given
        * index.
        * <p>
        * The current implementation delegates to {@link #ldbName(int)}.
        *
        * @param   index
        *          One-based index of the target database among all databases
        *          currently connected in this context.
        *
        * @return  The logical name, or unknown value if <code>index</code> is
        *          invalid.
        */
       public static character sdbName(int index)
       {
          return ldbName(index);
       }

       /**
        * Get the logical database name for the connected database with the given
        * logical name or alias.
        * <p>
        * The current implementation delegates to {@link #ldbName(character)}.
        *
        * @param   name
        *          Logical name or alias of the target database.
        *
        * @return  The logical name, or unknown value if <code>name</code> is
        *          invalid.
        */
       public static character sdbName(character name)
       {
          if (name.isUnknown())
          {
             return (new character());
          }

          return sdbName(name.toStringMessage());
       }

       /**
        * Get the logical database name for the connected database with the given
        * logical name or alias.
        * <p>
        * The current implementation delegates to {@link #ldbName(String)}.
        *
        * @param   name
        *          Logical name or alias of the target database.
        *
        * @return  The logical name, or unknown value if <code>name</code> is
        *          invalid.
        */
       public static character sdbName(String name)
       {
          return ldbName(name);
       }

       /**
        * Retrieve the network session which manages the connection to the remote
        * server which is authoritative for the specified database.
        *
        * @param   database
        *          Database to which the target session is mapped.
        *
        * @return  Network session for the given database, or <code>null</code>
        *          if there is no current connection.
        */
       public static Session getSession(Database database)
       {
          return get().sessions.get(database);
       }

       /**
        * Retrieve the network session which manages the connection to the remote server which is
        * authoritative for the specified database name, which may be a logical name, physical name,
        * or alias.  It is assumed that the database currently is connected.
        *
        * @param   name
        *          Logical name, physical name, or alias for database.
        *
        * @return  Network session for the given database, or <code>null</code> if there is no current
        *          connection.
        */
       public static Session getSession(String name)
       {
          ConnectionManager mgr = get();

          Database db = mgr.getDatabase(name);

          return (db == null ? null : mgr.sessions.get(db));
       }

       /**
        * Get the {@link Persistence} instance associated with the given database
        * name, which may be a physical or logical name, or an alias.
        *
        * @param   name
        *          Physical or logical database name, or alias.
        *
        * @return  Persistence object associated with the given database, which
        *          may be locally or remotely connected or <code>null</code>
        *          if no database with such name is connected
        */
       public static Persistence getPersistence(String name)
       {
          Database database = get().getDatabase(name);

          return database != null ? PersistenceFactory.getInstance(database) : null;
       }

       /**
        * Returns a string representing the user ID used for authentication to a database.
        * The database is identified by its logical name.
        *
        * @param   ldbName
        *          The logical name of the database for which the userid is requested.
        *
        * @return  current userid that was authenticated to specified database
        */
       public static String getCurrentUserid(String ldbName)
       {
          ClientPrincipal cp = ConnectionManager.getDbClient(ldbName.toLowerCase());
          ConnectionManager cm = get();

          if (cp != null && cm.getTenantName(ldbName) != null)
          {
             return cp.getUserId().toJavaType() + "@" + cp.getDomainName().toJavaType();
          }

          // call the method with unwrapped-argument signature:
          return cm.getCurrentUseridImpl(ldbName);
       }

       /**
        * Return the default logical database. It is used only with {@code setuserid} and
        * {@code userid} with default (missing argument) for database parameter.
        *
        * @return  The logical database name if only one (permanent) database is connected.
        *          Otherwise it raise an error condition and abends.
        */
       public static String getDefaultDatabase()
       {
          ConnectionManager cm = get();

          if (cm.connected.size() == 1)
          {
             Iterator<String> it = cm.connected.keySet().iterator();
             return it.next(); // return 1st element
          }

          // TODO: this is a compile-time error. Apparently P4GL recompiles the procedures when it
          //       calls them and the set of connected databases has changed.
          String[] msgs =
          {
             "SETUSERID/USERID functions require one connected db or db argument",
             "<program> Could not understand line <number>"
          };
          ErrorManager.showErrorAndAbend(new int[] { 691, 196 }, msgs);
          return null;
       }

       /**
        * Authenticates the user for specified DB connection. Checks if the user login account match
        * the corresponding record of the _User table of the database.  If the specified database
        * is not connected, an error is issued.
        *
        * The user/password comparing operation is done as 4GL:
        * <ul>
        *    <li><strong>user</strong>: is case-insensitive, trailing spaces are ignored
        *             (in upper(rtrim()) mode)
        *    <li><strong>password</strong>: must be an exact match: after applying <code>ENCODE</code>
        *             to password string it is compared to existing record from database in case
        *             sensitive, taking into consideration trailing spaces
        * </ul>
        *
        * @param    userid
        *           The name of the user to set as UserID.
        * @param    password
        *           The users password.
        * @param    dbname
        *           Logical database name.
        *
        * @return   The <code>true</code> in case of valid user match <code>false</code> otherwise.
        */
       public static boolean authenticate(String userid, String password, String dbname)
       {
          ConnectionManager ctxCm = get();

          // validation: dbname MUST be the logical name or an alias of a connected database
          character ldbName = ctxCm.ldbNameImpl(dbname, 0);
          if (dbname == null || dbname.isEmpty() || ldbName.isUnknown())
          {
             // the 'dbname' is not a valid connected database logical name nor an alias
             ErrorManager.recordOrShowError(
                   1072,
                   "SETUSERID of database " + dbname + " requires that database to be connected",
                   false);
             // at runtime, we get a false value (not unknown, not error)
             return false;
          }

          String encPassword = SecurityOps.encode(password).toStringMessage();
          Persistence persistence = PersistenceFactory.getInstance(ctxCm.getDatabase(dbname));

          Dialect dialect = persistence.getDialect();
          String pccUserid = dialect.getProcessedCharacterColumnName(
                   MetadataSecurityOps.getMetaFieldUserid(ldbName.toStringMessage()), true);
          String pccPassword = MetadataSecurityOps.getMetaFieldPassword(ldbName.toStringMessage());
                   // SETUSERID() is strict when comparing passwords

          StringBuilder sb = new StringBuilder();
          sb.append("SELECT COUNT(*) FROM ");
          sb.append(MetadataSecurityOps.META_TABLE_USER);
          sb.append( " WHERE ");
          sb.append(pccUserid);
          sb.append(" = upper(");
          dialect.addRtrimmedExpression("?", sb);
          sb.append(") AND ");
          sb.append(pccPassword);
          sb.append(" = ?");
          String query = sb.toString();
          try
          {
             String[] paramList = new String[] { userid, encPassword };
             Object ufo = persistence.getSingleSQLResult(query, Persistence.META_CTX, paramList);

             if (ufo == null)
             {
                // this should not happen anyway
                return false;
             }

             Long count = getSQLResultAsLong(ufo);
             if (count == null || count != 1)
             {
                // no match (if zero), if 2 or more then the _user table is broken
                return false;
             }
          }
          catch (PersistenceException pe)
          {
             if (LOG.isLoggable(Level.WARNING))
             {
                LOG.log(Level.WARNING, "setUserId failed accessing database (SQL query)", pe);
             }
             return false;
          }

          // if we reached this point then the user/password combination is OK
          // (the userid may be in camel-case or have trailing spaces)
          return true;
       }

       /**
        * Check the access level for a database based on metadata of that database.
        * This will query if _User table and return <code>SecurityOps.FREE_ACCESS</code> if
        * the meta table is not populated and <code>SecurityOps.BLANK_ALLOWED</code> otherwise.
        *
        * @param   ldbname
        *          The logical database name to query.
        *
        * @return  The access level to specified database.
        */
       public static int getAuthLevel(String ldbname)
       {
          String query = "SELECT COUNT(*) FROM " + MetadataSecurityOps.META_TABLE_USER;
          try
          {
             Object result = getPersistence(ldbname).getSingleSQLResult(query, Persistence.META_CTX, null);
             if (result != null)
             {
                Long count = getSQLResultAsLong(result);
                return count == 0 ? SecurityOps.FREE_ACCESS : SecurityOps.BLANK_ALLOWED;
             }
          }
          catch (PersistenceException pe)
          {
             if (LOG.isLoggable(Level.WARNING))
             {
                LOG.log(Level.WARNING, "setUserId failed accessing database (SQL query)", pe);
             }
          }

          return SecurityOps.FREE_ACCESS;
       }

       /**
        * Mark the given database of a tenant as connected.
        * This method makes use of the {@code ConnectionManager.putConnected} method, and it will fail if
        * the database with the given name cannot be found.
        *
        * @param   client
        *          The client/tenant for which to mark the database.
        * @param   ldbName
        *          The logical name of the database.
        */
       public static void putTenantDatabaseConnected(ClientPrincipal client, String ldbName)
       {
          ConnectionManager cm = get();
          String tenantName = TenantManager.getTenantName(client.getDomainName().getValue(), ldbName);
          Database db = TenantManager.getDatabase(tenantName, ldbName, false, Database.Type.PRIMARY);
          if (db == null)
          {
             throw new IllegalArgumentException("Cannot determine the database of tenant " +
                                                tenantName                                 +
                                                " and logical database name "              +
                                                ldbName);
          }

          cm.putConnected(ldbName, db, null, tenantName);
       }

       /**
        * Associate the specified CLIENT-PRINCIPAL with the given database logical name.  If not specified,
        * use the default connected database.
        * <p>
        * A copy of the CLIENT-PRINCIPAL object is saved in the {@link #clients} map.
        *
        * @param    client
        *           The CLIENT-PRINCIPAL object.
        * @param    dbName
        *           A valid database name.
        * @param    callee
        *           where the method is called from
        *
        * @throws   IllegalArgumentException
        *           If the database name is not valid.
        */
       public static void setDbClient(ClientPrincipal client, String dbName, SealCallee callee)
       {
          ConnectionManager cm = get();
          String ldbName = cm.getLDBName(dbName);
          if (ldbName == null)
          {
             throw new IllegalArgumentException("Could not resolve the database name:" +
                                                (dbName == null ? "null" : dbName));
          }

          String userId = client.getUserId().getValue();
          String ldbNameLowerCase = ldbName.toLowerCase();
          boolean tenantEnabled = TenantConfig.isEnabled();
          String tenantName = null;
          if (tenantEnabled)
          {
             tenantName = TenantManager.getTenantName(client.getDomainName().getValue(), dbName);
          }

          cm.clients.put(ldbNameLowerCase, client.clone());
          cm.userIds.put(ldbNameLowerCase, userId);
          if (callee == SealCallee.SET_DB_CLIENT)
          {
             cm.locked.add(ldbName);
          }

          if (tenantEnabled)
          {
             Set<TenantChangeListener> listeners = cm.tenantListeners.get(ldbName);
             if (listeners == null)
             {
                return;
             }

             for (TenantChangeListener listener : new HashSet<>(listeners))
             {
                // Notify the listeners that the current tenant has changed
                listener.tenantChanged(tenantName, ldbName);
             }
          }
       }

       /**
        * Get the list of unlocked database connections.
        *
        * @return the list of unlocked database connections.
        */
       public static ArrayList<String> getUnlocked()
       {
          ConnectionManager cm = get();
          ArrayList<String> ret = new ArrayList<>();
          for (Entry<String, ConnectInfo> entry : cm.connected.entrySet())
          {
             String db = entry.getKey();
             if (!cm.locked.contains(db))
             {
                ret.add(db);
             }
          }
          return ret;
       }

       /**
        * Check if database connection is locked.
        *
        * @param    dbName
        *           A valid database name.
        *
        * @throws   IllegalArgumentException
        *           If the database name is not valid.
        *
        * @return <code>true</code> if the database connection is locked.
        */
       public static boolean isLocked(String dbName)
       {
          ConnectionManager cm = get();

          String ldbName = cm.getLDBName(dbName);
          if (ldbName == null)
          {
             throw new IllegalArgumentException("Could not resolve the database name:" +
                                                (dbName == null ? "null" : dbName));
          }
          return cm.locked.contains(ldbName);
       }

       /**
        * Unlock the database connection.
        *
        * @param    dbName
        *           A valid database name.
        *
        * @throws   IllegalArgumentException
        *           If the database name is not valid.
        */
       public static void unlock(String dbName)
       {
          ConnectionManager cm = get();

          String ldbName = cm.getLDBName(dbName);
          if (ldbName == null)
          {
             throw new IllegalArgumentException("Could not resolve the database name:" +
                                                (dbName == null ? "null" : dbName));
          }
          cm.locked.remove(ldbName);
       }

       /**
        * Get the CLIENT-PRINCIPAL associated with the given database name.
        *
        * @param    dbName
        *           A database name.
        *
        * @return   The CLIENT-PRINCIPAL object or <code>null</code> if none associated.
        */
       public static ClientPrincipal getDbClient(String dbName)
       {
          ConnectionManager cm = get();

          String ldbName = cm.getLDBName(dbName);
          if (ldbName == null)
          {
             return null;
          }

          return cm.clients.get(ldbName.toLowerCase());
       }

       /**
        * Set the currently authenticated userid for a database.
        * This method assumes that the userid has already been authenticated in this context using
        * <code>authenticate()</code> method.
        * If <code>byConnect</code> then the userid is stored and  will be reported by
        * <code>dbparam</code> function with -U tag.
        *
        * @param   dbName
        *          A valid database name.
        * @param   userid
        *          The name of the user to set as UserID.
        * @param    lock
        *           Flag  indicationg that the database should be locked
        * @param   byConnect
        *          <code>true</code>if the authentication was done from a connect (at startup time
        *          or later) or <code>false</code> if the authentication was done using the
        *          <code>setuserid</code> function.
        *
        * @throws   IllegalArgumentException
        *           If the database name is not valid.
        */
       public static void setAuthenticatedUserid(String dbName, String userid, boolean lock, boolean byConnect)
       {
          ConnectionManager cm = get();
          String ldbName = cm.getLDBName(dbName);
          if (ldbName == null)
          {
             throw new IllegalArgumentException("Could not resolve the database name:" +
                                                (dbName == null ? "null" : dbName));
          }
          cm.setCurrentUseridImpl(ldbName.toLowerCase(), userid, lock);
          if (byConnect)
          {
             cm.setConnectUserImpl(ldbName);
          }
       }

       /**
        * Read _User table from the database supplying String array: {userid, domain-name, password}
        * to a consumer.
        *
        * @param persistence
        *        {@link Persistence} instance for the database
        * @param consumer
        *        <code>Consumer&lt;String[]&gt;</code> instance
        */
       public static void readUserTable(Persistence persistence, Consumer<String[]> consumer)
       {
          ConnectionManager ctxCm = get();
          String ldbName = ctxCm.getLDBName(persistence.getDatabase(Persistence.META_CTX));

          Dialect dialect = persistence.getDialect();
          String pccUserid =
             dialect.getProcessedCharacterColumnName(MetadataSecurityOps.getMetaFieldUserid(ldbName), true);
          String pccPassword = MetadataSecurityOps.getMetaFieldPassword(ldbName);
          String pccDomain =
             dialect.getProcessedCharacterColumnName(MetadataSecurityOps.getMetaFieldDomain(ldbName), true);

          String sql = "SELECT " + pccUserid + " AS userid, " +
                                   pccDomain + " AS domain, " +
                                   pccPassword + " AS password " +
                       "FROM " + MetadataSecurityOps.META_TABLE_USER;
          readQueryResults(sql,
                           persistence,
                           rs -> {
                              Object[] data = rs.get();
                              consumer.accept(new String[]{(String) data[0], (String) data[1], (String) data[2]});
                           });
       }

       /**
        * Read query results.
        *
        * @param   sql
        *          SQL statement to be executed
        * @param   persistence
        *          {@link Persistence} instance for the database
        * @param   consumer
        *          Result set consumer
        */
       public static void readQueryResults(String sql,
                                           Persistence persistence,
                                           Consumer<ScrollableResults<?>> consumer)
       {
          try
          {
             ScrollableResults<?> rs = persistence.executeSQLQuery(sql, Persistence.META_CTX, null);
             while (rs.next())
             {
                consumer.accept(rs);
             }
             rs.close();
          }
          catch (PersistenceException e)
          {
             throw new RuntimeException(e);
          }
       }

       /**
        * Get the logical database name for the connected database at the given
        * index.
        *
        * @param   index
        *          One-based index of the target database among all databases
        *          currently connected in this context.
        *
        * @return  The logical name, or unknown value if <code>index</code> is
        *          invalid.
        */
       static String ldbNameNative(int index)
       {
          return get().ldbNameNativeImpl(null, index);
       }

       /**
        * Normalize the given filename by extracting the root name (removing the
        * file path and the filename extension).
        *
        * @param   name
        *          File name.
        *
        * @return  "Normalized" root file name.
        */
       static String normalizeFilename(String name)
       {
          // Extract root name from PDB name.
          int pos = name.lastIndexOf(".");
          if (pos >= 0)
          {
             name = name.substring(0, pos);
          }

          pos = name.lastIndexOf(FileSystems.getDefault().getSeparator());
          if (pos >= 0)
          {
             name = name.substring(pos + 1);
          }

          return name;
       }

       /**
        * Assert that <code>data</code> does not represent the unknown value.
        * If the assertion fails, an exception is thrown.
        *
        * @param   data
        *          The data value to be checked.
        * @param   description
        *          A brief description of <code>data</code>'s purpose;  this will
        *          be used in an error message if the assertion fails.
        *
        * @throws  PersistenceException
        *          if the assertion fails.
        */
       private static void assertKnownValue(BaseDataType data, String description)
       throws PersistenceException
       {
          if (data.isUnknown())
          {
             // TODO:  use proper error number/message.
             throw new PersistenceException(description + " cannot be unknown", -1);
          }
       }

       /**
        * Iterate over the specified collection, returning the element at index {@code index}.
        *
        * @param   coll
        *          Collection over which to iterate.
        * @param   index
        *          The <b>1-based</b> index of the target element in the collection.
        *
        * @return  The element at the specified index, or {@code null} if {@code index} was never
        *          reached.
        */
       private static <X> X iterateToIndex(Collection<X> coll, int index)
       {
          Iterator<X> iter = coll.iterator();

          for (int i = 1; iter.hasNext(); i++)
          {
             X next = iter.next();
             if (i == index)
             {
                return next;
             }
          }

          return null;
       }

       /**
        * Normalize an alias or logical database name by right trimming whitespace and lowercasing.
        *
        * @param   name
        *          Alias or LDB name to be normalized.
        *
        * @return  Normalized result.
        */
       private static character normalizeAliasOrLDB(character name)
       {
          return TextOps.toLowerCase(TextOps.rightTrim(name));
       }

       /**
        * Register a {@code TenantChangeListener} for actions that involve tenant id changes.
        *
        * @param   listener
        *          The listener to be notified when a tenant changes.
        * @param   ldbName
        *          The logical database name for which the listener should be registered.
        */
       public void registerTenantListener(TenantChangeListener listener, String ldbName)
       {
          if (ldbName == null)
          {
             LOG.log(Level.SEVERE, "Database should not be null when registering a tenant!");
          }

          Set<TenantChangeListener> tls = tenantListeners.computeIfAbsent(ldbName, k -> new HashSet<>());
          tls.add(listener);
       }

       /**
        * De-register a {@code TenantChangeListener}.
        *
        * @param   listener
        *          The listener to be de-registered.
        * @param   ldbName
        *          The logical database name for which the listener should be de-registered.
        */
       public void deregisterTenantListener(TenantChangeListener listener, String ldbName)
       {
          Set<TenantChangeListener> tls = tenantListeners.get(ldbName);
          if (tls == null)
          {
             return;
          }

          tls.remove(listener);
          removeTenant(ldbName);
       }

       /**
        * Get a tenant name based on a database logical name.
        *
        * @param   ldbName
        *          The database logical name.
        *
        * @return  The name of the tenant.
        */
       public String getTenantName(String ldbName)
       {
          return tenantsByDatabase.get(ldbName);
       }

       /**
        * Remove a tenant that is tied to a database.
        *
        * @param   ldbName
        *          The database logical name for which to remove the tenant.
        */
       public void removeTenant(String ldbName)
       {
          tenantsByDatabase.remove(ldbName);
       }

       /**
        * Get the logical database name for a connected physical database.
        *
        * @param   database
        *          Physical database helper object.
        *
        * @return  Logical database name, if database is connected, else <code>null</code>.
        */
       public String getLDBName(Database database)
       {
          if (database.isMeta())
          {
             // the fact that metatables are managed in a separate database is a FWD implementation
             // detail; do the lookup based on the associated, primary database
             database = database.toType(Database.Type.PRIMARY);
          }

          return ldbMap.get(database);
       }

       /**
        * This method is invoked after a master transaction commit/rollback;  any databases awaiting
        * the end of the current transaction to disconnect are now deregistered with the database
        * manager.
        */
       void transactionEnded()
       {
          disconnectImmediately(pendingTxDisconn, false);
       }

       /**
        * This method is invoked after a master transaction commit/rollback;  if the specified
        * database is waiting for the end of the current transaction to disconnect, it will be
        * deregistered with the database manager.
        *
        * @param    database
        *           The database for which to process the transaction end - as this will be executed
        *           for every database which was used in this transaction block.
        */
       void transactionEnded(Database database)
       {
          if (pendingTxDisconn.contains(database))
          {
             disconnectImmediately(database, false);

             pendingTxDisconn.remove(database);
          }
       }

       /**
        * Register that a reference from a record buffer to its database has
        * become active.  We will be notified when this reference is no longer
        * active, which is used to resolve pending disconnect requests.
        *
        * @param   buffer
        *          Record buffer whose scope has been opened.
        *
        * @see     #closeBufferScope
        * @see     #disconnect
        */
       void openBufferScope(RecordBuffer buffer)
       {
          // add buffer to set of those which reference a physical database
          Database pdb = buffer.getDatabase();
          Set<RecordBuffer> set = pdbReferences.computeIfAbsent(pdb, k -> new HashSet<>());
          set.add(buffer);
       }

       /**
        * Very specific method to check if a buffer is still dangling in the {@link #pdbReferences}
        * collection. This should be called only in extra-ordinary execution paths that may involve
        * native Java integrations outside the persist framework. This should be called in case of
        * fail-safes to avoid memory leaks.
        * <p>
        * DON'T use this method for trivial checks on whether the buffer has its scope opened or not.
        * This should be used only to detect unexpected leaks inside {@link #pdbReferences} that can't
        * be solved by the normal lifecycle of {@link TransactionManager} or {@link BufferManager}.
        * <p>
        * For example, invoking generic triggers that do not properly manage the buffer lifecycle
        * may result in memory leaks. Therefore, attempt to detect this case and improve the fault
        * tolerance on persistence layer side.
        *
        * @param   buffer
        *          The buffer that should be checked for leakage.
        *
        * @return  {@code true} if this buffer still resides in {@link #pdbReferences} due to a
        *          bad handling of the buffer from native Java integrations like generic triggers.
        */
       public boolean isBufferScopeLeaking(RecordBuffer buffer)
       {
          Database pdb = buffer.getDatabase();
          Set<RecordBuffer> set = pdbReferences.get(pdb);
          if (set == null)
          {
             return false;
          }
          return set.contains(buffer);
       }

       /**
        * Called by a record buffer to notify that its scope has been closed,
        * indicating that it no longer has an active reference to its database.
        * This may result in the current context disconnecting from the database,
        * if and only if:
        * <ol>
        *   <li>there is a (first level) disconnect request pending for the
        *       database;  AND
        *   <li>no record buffers currently have active references to it;  AND
        *   <li>no transaction currently is active.
        * </ol>
        * <p>
        * If all three conditions are met, the database is disconnected
        * immediately, and removed from the first level pending disconnect list.
        * If the first two conditions are met, but the third is not, the database
        * is moved from the first level to the second level disconnect list,
        * where it will await the end of the transaction for disconnect.
        *
        * @param   buffer
        *          Record buffer whose scope has been closed.
        *
        * @see     #openBufferScope
        * @see     #disconnect
        */
       void closeBufferScope(RecordBuffer buffer)
       {
          // nothing to do if this is not the outermost open scope for this buffer
          if (buffer.getOpenScopeCount() > 0)
          {
             return;
          }

          // remove buffer from set of those which reference a physical database
          Database pdb = buffer.getDatabase();
          Set<RecordBuffer> set = pdbReferences.get(pdb);
          if (set == null)
          {
             return;
          }
          set.remove(buffer);

          // if the set is now empty, we are not in a transaction, AND this database is pending
          // disconnect, deregister it with the database manager for this session
          if (set.isEmpty())
          {
             pdbReferences.remove(pdb);
             if (pendingDisconn.contains(pdb))
             {
                if (!buffer.getBufferManager().isTransaction())
                {
                   // Database can be deregistered immediately.
                   DatabaseManager.deregisterDatabase(pdb);
                }
                else
                {
                   // Database cannot be deregistered until transaction ends.
                   pendingTxDisconn.add(pdb);
                }

                // either way, remove it from the first level pending disconnect set
                pendingDisconn.remove(pdb);
             }
          }
       }

       /**
        * Get the logical database name for the given logical database name or
        * alias.  The database does not necessarily have to be connected, if
        * <code>name</code> represents an alias.
        *
        * @param   name
        *          Logical database name or alias.
        *
        * @return  Logical database name or <code>null</code> if <code>name</code>
        *          is not recognized.
        */
       public String getLDBName(String name)
       {
          // Look up name as an alias.
          String ldbName = aliases.get(name);
          if (ldbName != null)
          {
             // name is an alias.
             return ldbName;
          }
          else if (connected.containsKey(name))
          {
             // name is the LDB name.
             return name;
          }

          // name is unrecognized.
          return null;
       }

       /**
        * Get the physical database name for the given logical database name or
        * alias.  The database must be connected.  The resulting name may
        * optionally be normalized, which excludes any filename path and
        * extension.
        *
        * @param   name
        *          Logical database name or alias.
        * @param   normalize
        *          <code>true</code> to return the normalized name;  else
        *          <code>false</code> to return the unaltered (possibly fully
        *          qualified name), as provided by the caller of the {@link
        *          #connect} method.
        *
        * @return  Physical database name or <code>null</code> if
        *          <code>name</code> is not recognized.
        */
       String getPDBName(String name, boolean normalize)
       {
          String pdb = getDatabase(name).getName();
          if (normalize)
          {
             pdb = normalizeFilename(pdb);
          }

          return pdb;
       }

       /**
        * Get the database information object for the given database name, which
        * may be a logical name, physical name, or alias.  It is assumed that the
        * database currently is connected.
        *
        * @param   name
        *          Logical name, physical name, or alias for database.
        *
        * @return  Database information object associated with <code>name</code>,
        *          or <code>null</code> if no such database currently is
        *          connected.
        */
       Database getDatabase(String name)
       {
          // Get LDB name, in case name is an alias.
          String ldbName = getLDBName(name);

          ConnectInfo info = connected.get(ldbName);

          return info != null ? info.database : null;
       }

       /**
        * Get a list of active databases, which is meant to include the temp-table database and all
        * connected, persistent databases. The term &quot;active&quot; is used instead of
        * &quot;connected&quot; because the inclusion of the temp table database is an
        * implementation-specific requirement, which is not consistent with the 4GL's notion of
        * &quot;connected&quot; databases. Thus, this API is meant for internal use only and should
        * not be exposed as a legacy API.
        *
        * @return  List of all connected, persistent databases, plus the temp-table database.
        */
       ArrayList<Database> getActiveDatabases()
       {
          int size = connected.size();
          ArrayList<Database> list = new ArrayList<>(size + 1);
          list.add(DatabaseManager.TEMP_TABLE_DB);  // always connected
          if (size > 0)
          {
             for (ConnectInfo ci : connected.values())
             {
                list.add(ci.database);
                if (ci.database.isTenant())
                {
                   // for each tenant database, the default database is also connected (contains shared tables and
                   // some meta tables, like _users aand _sec_authentication*)
                   list.add(ci.database.getDefault());
                }
             }
          }

          return list;
       }

       /**
        * Get the currently connected aliases by their logical database name.
        *
        * @return   See above.
        */
       Map<String, String> getAliasesByLDB()
       {
          Map<String, String> res = new HashMap<>();

          for (String key : aliases.keySet())
          {
             res.put(aliases.get(key), key);
          }

          return res;
       }

       /**
        * Dynamically analyze (by class type) and convert a value returned by a SQL statement of an
        * unknown type (but assumed to be integer-like) to Long value. If the type cannot be detected,
        * {@code null} is returned.
        *
        * @param   ufo
        *          The value from a SQL result. It is assumed to be some kind of integer/long.
        *
        * @return  The long value of the unknown object or {@code null} if its type could not be
        *          detected.
        */
       private static Long getSQLResultAsLong(Object ufo)
       {
          if (ufo instanceof Integer)
          {
             return ((Integer) ufo).longValue();
          }
          else if (ufo instanceof Long)
          {
             return (Long) ufo;
          }
          else if (ufo instanceof BigInteger)
          {
             return ((BigInteger) ufo).longValue();
          }
          else
          {
             if (ufo != null && LOG.isLoggable(Level.WARNING))
             {
                LOG.log(Level.WARNING, "failed to extract long/int value SQL response");
             }
             return null;
          }
       }

       /**
        * Returns the current id used by the user to authenticated to a database
        * identified by its logical name.
        *
        * @param   ldbName
        *          The logical name of the database for which the userid is requested.
        *
        * @return  current userid
        */
       private String getCurrentUseridImpl(String ldbName)
       {
          String uid = userIds.get(ldbName);
          return uid == null ? "" : uid;
       }

       /**
        * Sets the currently authenticated {@code userid} for a database.
        * <p>
        * The only authority to set this is an instance of {@code MetadataSecurityOps} and only
        * after checking the user/password combination against the {@code _User} meta-table.
        * <p>
        * Notes:
        * <ul>
        *    <li>The {@code CONNECT} statement with {@code -U} and {@code -P} parameters will have to
        *        call {@code SecurityOps} to verify if the user/password combination if correct. <br>
        *       (At this moment the {@code ConnectionManager} does not support these parameters, yet).
        *    <li>The command-line parameters should be investigated.
        *  </ul>
        *
        * @param   ldbName
        *          The logical database name for which the {@code userid} is set/updated.
        * @param   newUserId
        *          The newly newUserId for this database context.
        * @param    lock
        *           Flag  indicationg that the database should be locked
        */
       private void setCurrentUseridImpl(String ldbName, String newUserId, boolean lock)
       {
          userIds.put(ldbName, newUserId);
          if (lock)
          {
             locked.add(ldbName);
          }
       }

       /**
        * Mark the current user for the database as the connection parameter.
        * The user currently authenticated will be reported by <code>dbparam</code> function with -U
        * tag.
        *
        * @param   ldbName
        *          The logical database name for which the userid is marked as connection parameter.
        */
       private void setConnectUserImpl(String ldbName)
       {
          String userId = userIds.get(ldbName);
          if (userId != null)
          {
             connectTimeUserid.put(ldbName, userId);
          }
       }

       /**
        * Perform end of session cleanup;  disconnects all connected databases
        * without regard to transaction state or buffer reference status.  This
        * ensures <code>DatabaseManager</code>'s transient database reference
        * counts are not corrupted when client sessions end.
        */
       private void sessionClosed()
       {
          // waistband
          Set<Database> all = new HashSet<>();
          for (ConnectInfo info : connected.values())
          {
             all.add(info.database);
          }

          // belt
          all.addAll(pendingTxDisconn);

          // suspenders
          all.addAll(pendingDisconn);

          // deregister all connected databases for this session
          disconnectImmediately(all, true);
       }

       /**
        * Disconnect all databases in the specified set by deregistering them with the
        * <code>DatabaseManager</code> and removing them from the set. Network sessions associated
        * with the databases in the set are terminated accordingly. If being called at the end of a
        * user's session, clean up internal data structures and update _connect metadata as well.
        *
        * @param   pending
        *          Set of databases to disconnect.
        * @param   sessionCleanup
        *          <code>true</code> if this action is being taken as part of cleanup for a session
        *          that is terminating; <code>false</code> if a normal disconnect.
        */
       private void disconnectImmediately(Set<Database> pending, boolean sessionCleanup)
       {
          LegacyLogManager logManager = LegacyLogOps.logMgr(!sessionCleanup);

          Iterator<Database> iter = pending.iterator();
          while (iter.hasNext())
          {
             Database database = iter.next();
             disconnectImmediately(database, sessionCleanup);

             if (logManager == null)
             {
                LOG.severe("LOG-MANAGER couldn't be created at session clean-up.");
             }
             else if (logManager.isLoggingEnabled())
             {
                LegacyLogOps.logMgr().writeMessage("Disconnecting from database " + database +
                                                      ", user number ?. (9543)",
                                                   LoggingEntryType.DB_Connects,
                                                   null,
                                                   LoggingSubsys.CONN,
                                                   LoggingLevel.BASIC);
             }

             iter.remove();
          }
       }

       /**
        * Disconnect the specified database by deregistering it with the <code>DatabaseManager</code>
        * and terminating the associated network session.
        *
        * @param   database
        *          Database to disconnect.
        * @param   sessionCleanup
        *          <code>true</code> if this action is being taken as part of cleanup for a session
        *          that is terminating; <code>false</code> if a normal disconnect.
        */
       private void disconnectImmediately(Database database, boolean sessionCleanup)
       {
          if (sessionCleanup)
          {
             String ldbName = ldbMap.get(database);
             if (ldbName != null)
             {
                removeConnected(ldbName, sessionCleanup);
             }
          }

          if (!database.isLocal())
          {
             DatabaseManager.deregisterDatabase(database);
          }
          else
          {
             DatabaseManager.deactivate(null, database, false);
          }

          Session session = sessions.remove(database);
          if (session != null)
          {
             // Close the session only if is not used by the dirty share manager
             // for this database.
             if (session.isRunning() && !DirtyShareFactory.isUsedByManager(database, session))
             {
                session.terminate();
             }
          }

          if (LOG.isLoggable(Level.FINE))
          {
             String ctx = Utils.describeContext();
             // database is not null
             String msg = String.format("{%s} Database '%s' disconnected", ctx, database);
             LOG.log(Level.FINE, msg);

             if (LOG.isLoggable(Level.FINEST))
             {
                msg = String.format("{%s} connected:  %s", ctx, connected);
                LOG.log(Level.FINEST, msg);
                msg = String.format("{%s} pendingDisconn:  %s", ctx, pendingDisconn);
                LOG.log(Level.FINEST, msg);
                msg = String.format("{%s} pendingTxDisconn:  %s", ctx, pendingTxDisconn);
                LOG.log(Level.FINEST, msg);
             }
          }
       }

       /**
        * Get the logical name for a database, given an alias (which may be the
        * logical name itself), or its index among all connected databases in
        * this context.
        *
        * @param   name
        *          Logical database name or alias.  May be <code>null</code>,
        *          but only if <code>index</code> is non-<code>null</code>.
        * @param   index
        *          Index of the database among connected databases.  May be
        *          <code>null</code>, but only if <code>name</code> is
        *          non-<code>null</code>.  This is a one-based index.
        *
        * @return  Logical database name, or unknown value if <code>name</code>
        *          is not recognized.
        */
       private character ldbNameImpl(String name, int index)
       {
          return new character(ldbNameNativeImpl(name, index));
       }

       /**
        * Get the logical name for a database, given an alias (which may be the
        * logical name itself), or its index among all connected databases in
        * this context.
        *
        * @param   name
        *          Logical database name or alias.  May be <code>null</code>,
        *          but only if <code>index</code> is non-<code>null</code>.
        * @param   index
        *          Index of the database among connected databases.  May be
        *          <code>null</code>, but only if <code>name</code> is
        *          non-<code>null</code>.  This is a one-based index.
        *
        * @return  Logical database name, or unknown value if <code>name</code>
        *          is not recognized.
        */
       private String ldbNameNativeImpl(String name, int index)
       {
          String ldbName = null;
          if (name == null)
          {
             ldbName = iterateToIndex(connected.keySet(), index);
          }
          else
          {
             ldbName = getLDBName(name);
          }

          if (ldbName != null)
          {
             return ldbName;
          }

          return null;
       }

       /**
        * Get the physical name for a database, given its logical name or alias,
        * or its index among all connected databases in this context.
        *
        * @param   name
        *          Logical database name or alias.  May be <code>null</code>,
        *          but only if <code>index</code> is non-<code>null</code>.
        * @param   index
        *          One-based index of the database among connected databases.
        *
        * @return  Physical database name, or unknown value if <code>name</code>
        *          is not recognized.
        */
       private character pdbNameImpl(String name, int index)
       {
          String key = null;
          if (name == null)
          {
             key = iterateToIndex(connected.keySet(), index);
          }
          else
          {
             key = name.toLowerCase();
          }

          String pdbName = getPDBName(key, false);
          if (pdbName != null)
          {
             return (new character(pdbName));
          }

          return (new character());
       }

       /**
        * Create a logical connection to a database using the specified
        * parameters.
        *
        * @param isAutoConnect
        *        flag indication that is is an implicit auto-connect operation
        * @param   opts
        *          A list of one or more connection parameters.
        * @throws  PersistenceException
        *          if any parameter is null;
        *          if any parameter is neither a text value nor an integral
        *          value;
        *          if there is an error registering the database with the
        *          database manager.
        */
       private void connectDbImpl(boolean isAutoConnect, List<Map.Entry<ConnectOption, ConnectOptionValue>> opts)
       throws PersistenceException
       {
          Map<ConnectOption, ConnectOptionValue> options = new EnumMap<>(ConnectOption.class);
          opts.forEach(e -> options.put(e.getKey(), e.getValue()));

          this.singleClient = options.containsKey(ConnectOption.SINGLE_USER);

          String pdb = !options.containsKey(ConnectOption.DATABASE) ? null :
                options.get(ConnectOption.DATABASE).textValue();
          String ldb = !options.containsKey(ConnectOption.LOGICAL_DBNAME) ? null :
                options.get(ConnectOption.LOGICAL_DBNAME).textValue();
          String host = !options.containsKey(ConnectOption.HOSTNAME) ? null :
                options.get(ConnectOption.HOSTNAME).textValue();
          String service = !options.containsKey(ConnectOption.SERVICE_NAME) ? null :
                options.get(ConnectOption.SERVICE_NAME).textValue();
          String user = !options.containsKey(ConnectOption.USERID) ? null :
             options.get(ConnectOption.USERID).textValue();
          String password = !options.containsKey(ConnectOption.PASSWORD) ? null :
             options.get(ConnectOption.PASSWORD).textValue();
          Integer port = null;
          if (service != null)
          {
             try
             {
                port = Integer.valueOf(service);
             }
             catch (NumberFormatException exc)
             {
                // Look up the port number associated with the given service name.
                port = Utils.getPortForService(null, service);
                if (port < 0)
                {
                   ErrorManager.recordOrShowError(5192,
                         String.format(
                               "Unable to find server %s with protocol TCP in file SERVICES" +
                               " or SERVICES file not found in expected location.",
                               service),
                         false, false, true);
                   return;
                }
             }
          }
          Integer maxRetries = !options.containsKey(ConnectOption.MAX_CONN_RETRIES) ?
                DEFAULT_CONN_RETRIES :
                options.get(ConnectOption.MAX_CONN_RETRIES).intValue();
          connect(isAutoConnect, pdb, ldb, host, service, user, password, port, maxRetries);
          ConnectInfo info = connected.get(ldb);
          if (info != null)
          {
             info.opts = opts;
          }
       }

       /**
        * Create a logical connection to a database using the specified
        * parameters.
        *
        * @param   args
        *          A list of one or more connection parameters.  This may take one
        *          of several forms:
        *          <ul>
        *            <li>an ordered list of the following:
        *              <ol>
        *                <li>physical database name (required)
        *                <li>logical database name (optional)
        *                <li>P2J server host name/address (optional)
        *                <li>P2J server port number or service name (optional, unless host
        *                      name/address is supplied)
        *                <li>userid (optional)
        *                <li>password (optional, required if userid is present)
        *              </ol>
        *            </li>
        *            <li>the following parameter pairs:
        *              <ol>
        *                <li>physical database name key/value pair...
        *                  <ul>
        *                    <li>"-db" (optional identifier for physical database
        *                        name option to follow)
        *                    <li>physical database name (required)
        *                  </ul>
        *                </li>
        *                <li>...followed by an unordered list of the following
        *                    options.  Note that the order of these parameter
        *                    key/value pairs is not important;  however, the
        *                    pairing of key string (e.g., "-ld"), with the
        *                    associated value must be preserved:
        *                  <ul>
        *                    <li>"-ld" (required only if next element is provided)
        *                    <li>logical database name (optional)
        *                    <li>"-H" (required only if next element is provided)
        *                    <li>P2J server host name/address (optional)
        *                    <li>"-S" (required only if next element is provided)
        *                    <li>P2J server port number or service name (optional,
        *                        unless host name/address is supplied)
        *                    <li>"-U" (required only if next element is provided)
        *                    <li>userid used to authenticate to database (optional)
        *                    <li>"-P" (required only if next element is provided)
        *                    <li>password used for authentication (optional)
        *                  </ul>
        *                </li>
        *              </ol>
        *            </li>
        *          </ul>
        *
        * @throws  PersistenceException
        *          if any parameter is null;
        *          if any parameter is neither a text value nor an integral value;
        *          if there is an error registering the database with the database manager.
        */
       private void connectImpl(List<Object> args)
       throws PersistenceException
       {
          String flag = null;
          String pdb = null;
          String ldb = null;
          String host = null;
          String service = null;
          String user = null;
          String password = null;
          Integer port = null;
          Integer maxRetries = DEFAULT_CONN_RETRIES;

          // Loop through the parameters and divine their meanings.
          for (Object arg : args)
          {
             String text = null;
             Integer num = null;

             if (arg instanceof String)
             {
                text = ParamFileReader.removeEnclosingQuotes((String) arg);
             }
             else if (arg instanceof Integer)
             {
                num = (Integer) arg;
             }
             else if (arg == null)
             {
                throw new PersistenceException("Invalid (null) connect argument");
             }

             if (text != null)
             {
                // Detect option keys.
                if (text.startsWith("-"))
                {
                   if (flag != null)
                   {
                      int[] nums     = new int[] {1403, 325};
                      String[] texts = new String[] {
                            "You have not supplied a parameter for argument " + flag,
                            "Unable to execute CONNECT statement" };
                      ErrorManager.showErrorAndAbend(nums, texts);
                   }

                   if (text.equals("-1"))
                   {
                      singleClient = true;
                      String msg = "Single-user mode (-1) is reported, but is not currently enforced";
                      UnimplementedFeature.partial(msg);
                   }
                   else
                   {
                      flag = text;
                   }

                   continue;
                }

                // Physical database name.
                if ("-db".equals(flag) || (flag == null && pdb == null))
                {
                   pdb = text;
                   flag = null;

                   continue;
                }

                // Logical database name.
                if ("-ld".equals(flag) || (flag == null && ldb == null))
                {
                   ldb = text;
                   flag = null;

                   continue;
                }

                // Host name/address.
                if ("-H".equals(flag) || (flag == null && host == null))
                {
                   host = text;
                   flag = null;

                   continue;
                }

                // Socket service/port number.
                if ("-S".equals(flag) || (flag == null && port == null))
                {
                   try
                   {
                      // Argument could be a string representation of a port
                      // number, so try to parse it as such.
                      service = text;
                      port = Integer.valueOf(text);
                   }
                   catch (NumberFormatException exc)
                   {
                      // Look up the port number associated with the given service name.
                      port = Utils.getPortForService(null, text);
                      if (port < 0)
                      {
                         throw new PersistenceException("Invalid service name:  " + text);
                      }
                   }
                   flag = null;

                   continue;
                }

                // network type
                if ("-N".equals(flag))
                {
                   // silently ignore
                   flag = null;

                   continue;
                }

                // number of connection retries allowed
                if ("-ct".equals(flag))
                {
                   try
                   {
                      maxRetries = Integer.valueOf(text);
                   }
                   catch (NumberFormatException exc)
                   {
                      throw new PersistenceException("Invalid retry count:  " + text);
                   }
                   flag = null;

                   continue;
                }

                // Authentication: userid
                if ("-U".equals(flag) || (flag == null && user == null))
                {
                   user = text;
                   flag = null;

                   continue;
                }

                // Authentication: password
                if ("-P".equals(flag) || (flag == null && password == null))
                {
                   password = text;
                   flag = null;

                   continue;
                }
             }
             else if ("-S".equals(flag) || (flag == null && port == null))
             {
                port = num;
                flag = null;

                continue;
             }
          }

          connect(pdb, ldb, host, service, user, password, port, maxRetries);
       }

       /**
        * Establish database connection.
        *
        * @param   isAutoConnect
        *          flag indication that is is an implicit auto-connect operation
        * @param   pdb
        *          database physical name.
        * @param   ldb
        *          database logical name.
        * @param   host
        *          remote host name.
        * @param   service
        *          remote service name.
        * @param   user
        *          used id.
        * @param   password
        *          user password.
        * @param   port
        *          remote port number.
        * @param   maxRetries
        *          max connection attempts.
        *
        * @throws  PersistenceException
        *          if any parameter is null;
        *          if any parameter is neither a text value nor an integral value;
        *          if there is an error registering the database with the database manager.
        */
       private void connect(boolean isAutoConnect,
                            String pdb,
                            String ldb,
                            String host,
                            String service,
                            String user,
                            String password,
                            Integer port,
                            Integer maxRetries)
       throws PersistenceException
       {
          if (host == null && port != null)
          {
             // TODO:  query host for current P2J server instance.
             throw new PersistenceException(
                "Database connect host must be specified for port '" + port + "'");
          }

          if (LOG.isLoggable(Level.FINE))
          {
             String msg = String.format(
                   "{%s} Connecting database [PDB = %s; LDB = %s; HOST = %s; SVC = %s; " +
                                            "PORT = %d; UID = %s; PWD = %s]",
                   Utils.describeContext(),
                   pdb,
                   ldb,
                   host,
                   service,
                   port,
                   user,
                   (password == null) ? "null" : "***");
             LOG.log(Level.FINE, msg);
          }

          Database database = null;

          if (host != null)
          {
             database = new Database(pdb, Database.Type.PRIMARY, host, port);
             InetSocketAddress address = database.getSocketAddress();
             if (SessionManager.get().isLocalNode(address))
             {
                database = new Database(pdb, Database.Type.PRIMARY);
             }
          }
          else
          {
             database = new Database(pdb, Database.Type.PRIMARY);
          }

          for (int i = 0; i <= maxRetries; i++)
          {
             // TODO:  passing null for certAlias disables requester-side server
             // validation, which is fine for the common case.  To enable this, we
             // would need a directory lookup to find the correct certificate alias
             // for the remote server in the directory, based upon the IP address
             // and port (or service name) of the remote server.

             // if ldb == null, a valid ldb will be returned
             ldb = makeConnection(database, ldb, null, isAutoConnect);

             // handle failed attempts
             if (ldb == null && !connectedImpl(new character(pdb)).booleanValue())
             {
                if (i < maxRetries)
                {
                   try
                   {
                      Thread.sleep(CONN_RETRY_DELAY);
                   }
                   catch (InterruptedException exc)
                   {
                      if (LOG.isLoggable(Level.WARNING))
                      {
                         String msg = Utils.describeContext() +
                                      " sleep between database connection retry attempts interrupted";
                         LOG.log(Level.WARNING, msg);
                      }
                   }
                }
                else
                {
                   // connection failed after maximum number of attempts
                   return;
                }
             }
             else
             {
                break;
             }
          }

          // ensure the remote persistence is loaded
          PersistenceFactory.getInstance(database);

          if (user == null && password == null)
          {
             // no user/password combination supplied, store the detected blank os username for userid
             setConnectUserImpl(getLDBName(pdb));
             return;
          }

          if (user == null || password == null)
          {
             int[] nums     = new int[] {1403, 325};
             String[] texts = new String[] {
                   "You have not supplied a parameter for argument " + (user == null ? "-U" : "-P"),
                   "Unable to execute CONNECT statement" };
             ErrorManager.showErrorAndAbend(nums, texts);
             return;
          }

          // after a successful connection check if user/password match
          if (SecurityOps.getAuthLevel(ldb) != SecurityOps.FREE_ACCESS)
          {
             if (SecurityOps.setUserId(user, password, ldb).booleanValue())
             {
                // pin the authenticated used so it will be returned by DBPARAM function
                setConnectUserImpl(ldb);
             }
             else
             {
                // authentication failed
                ErrorManager.recordOrThrowError(710,
                                                "Your Password and Userid " + user + " do not match",
                                                false,
                                                false);
             }
          }
          else
          {
             ErrorManager.recordOrThrowError(709,
                                             "-P and -U startup parameters require _User file records",
                                             false,
                                             false);
          }
       }

       /**
        * Create a logical connection to the database with the specified physical
        * name, creating the specified logical name for it.
        *
        * @param   database
        *          Database information object.
        * @param   ldbName
        *          Logical database name.  If <code>null</code>, root name of
        *          physical database name will be used.
        * @param   certAlias
        *          Alias of certificate to be read from trust store to enable
        *          certificate-based, server-to-server authentication.  This
        *          should match the server ID of the remote node.
        * @param   isAutoConnect
        *          flag indication that is is an implicit auto-connect operation
        *
        * @return  the logical database name on success (the ldbName input parameter may be
        *          <code>null</code>, but a new logical name will be computed and returned)
        *          and <code>null</code> otherwise.
        *
        * @throws  ErrorConditionException
        *          if the given database already is connected.
        * @throws  PersistenceException
        *          if there is an error registering the database with the
        *          database manager or establishing a network session with the
        *          remote server.
        */
       private String makeConnection(Database database, String ldbName,
                                     String certAlias, boolean isAutoConnect)
       throws PersistenceException
       {
          // Normalize names.
          String pdb = normalizeFilename(database.getName());
          if (ldbName == null)
          {
             // if ldbName is null, root name of physical database name will be used.
             ldbName = pdb.toLowerCase();
          }
          else
          {
             ldbName = normalizeAliasOrLDB(new character(ldbName)).toStringMessage();
          }

          // Is this database awaiting disconnection?
          boolean disconn = pendingDisconn.contains(database) || pendingTxDisconn.contains(database);

          if (connected.containsKey(ldbName))
          {
             String existing = normalizeFilename(connected.get(ldbName).database.getName());

             // Warn user if database is already connected (and is not pending a disconnect).
             if (existing.equals(pdb))
             {
                if (!disconn)
                {
                   if (isAutoConnect)
                   {
                      return ldbName;
                   }
                   ErrorManager.recordOrShowWarning(
                      1012,
                      "Warning -- database " + ldbName + " is already connected",
                      true,
                      false,
                      false,
                      true);

                   return null;
                }
             }
             // Error out if attempting to assign logical name to more than one physical database.
             else
             {
                int[] num = new int[] { 1012, 1018 };
                String[] msg = new String[]
                {
                   "Warning -- database " + ldbName + " is already connected",
                   "Database physical names " + pdb + "  and " + existing +
                   " do not match.  No connect."
                };
                ErrorManager.recordOrShowError(num, msg, true);

                return null;
             }
          }

          // If this database is pending a disconnect, the connect and pending
          // disconnect cancel each other out.  If not, register it and remember it is connected.
          if (!pendingDisconn.remove(database) && !pendingTxDisconn.remove(database))
          {
             boolean local = database.isLocal();

             try
             {
                Session session = null;
                if (!local)
                {
                   // Establish network session with remote server.
                   SessionManager sessMgr = SessionManager.get();
                   InetSocketAddress address = database.getSocketAddress();
                   session = sessMgr.connectVirtual(address, certAlias, null);
                }

                sessions.put(database, session);
             }
             catch (Exception exc)
             {
                throw new PersistenceException(exc);
             }

             // Register database with database manager if not local or have not been registered already.
             // If the database is local but not "load_at_startup" it must be activated.
             // If it was already registered false will be returned
             if (local)
             {
                DatabaseManager.activate(database.getName());
             }
             else
             {
                local |= DatabaseManager.registerDatabase(database);
                // we can safely assume that this is not a [_temp] database as temp-database cannot be
                // connected or disconnected
                local |= (database.isLocal() && DatabaseManager.registerDirtyDatabase(database));
             }

             if (local)
             {
                // Store mapping of logical name to database and vice-versa.
                putConnected(ldbName, database, null, null);

                if (LOG.isLoggable(Level.FINE))
                {
                   String msg = String.format("{%s} Connected database '%s' [%s]",
                                              Utils.describeContext(),
                                              ldbName,
                                              database);
                   LOG.log(Level.FINE, msg);

                   if (LOG.isLoggable(Level.FINEST))
                   {
                      msg = String.format("{%s} Current connections:  [%s]",
                                          Utils.describeContext(),
                                          connected);
                      LOG.log(Level.FINEST, msg);
                   }
                }
             }
             else if (LOG.isLoggable(Level.WARNING))
             {
                String msg = String.format("{%s} Unable to make database connection for '%s' [%s]",
                                           Utils.describeContext(),
                                           ldbName,
                                           database);
                LOG.log(Level.WARNING, msg);
             }
          }
          else
          {
             // Store mapping of logical name to database and vice-versa.
             putConnected(ldbName, database, SecurityOps.getDefaultUserid(), null);

             if (LOG.isLoggable(Level.FINE))
             {
                String msg = String.format("{%s} Cancelled disconnect of database '%s' [%s]",
                                           Utils.describeContext(),
                                           ldbName,
                                           database);
                LOG.log(Level.FINE, msg);

                if (LOG.isLoggable(Level.FINEST))
                {
                   msg = String.format("{%s} Current connections:  [%s]", Utils.describeContext(), connected);
                   LOG.log(Level.FINEST, msg);
                }
             }
          }

          // return the used logical name for the connected database
          return ldbName;
       }

       /**
        * Request a disconnect from a logical database.  This does not actually
        * perform the disconnect immediately, but registers <code>ldbName</code>
        * for disconnect at the appropriate time:  once no buffers reference the
        * database, and at the end of the current transaction (if any).
        *
        * @param   ldbName
        *          Logical name of the database to disconnect.
        *
        * @throws  PersistenceException
        *          if the specified database has not been opened.
        */
       private void disconnectImpl(String ldbName)
       throws PersistenceException
       {
          String ldb = getLDBName(ldbName);
          if (ldb == null)
          {
             throw new PersistenceException("DISCONNECT:  Database " + ldbName + " is not open or unknown", 1009);
          }

          // remove the database from the connected map whether or not the disconnect can occur immediately
          Database database = removeConnected(ldb, false);

          // check whether there are any other connections to this physical database under a different
          // logical name; if so, we can't deregister the database before those are disconnected
          for (ConnectInfo info : connected.values())
          {
             // although it may not be optimal to walk the values of a map in general, in practice the
             // number of connected databases typically is very low, so this should be fast
             if (database.equals(info.database))
             {
                // return without deregistering physical database (or scheduling it for deferred deregistration)
                return;
             }
          }

          // if any record buffers still reference the database, defer the disconnect until all
          // affected buffer scopes have closed
          if (pdbReferences.containsKey(database))
          {
             pendingDisconn.add(database);
          }
          else if (TransactionManager.isTransaction())
          {
             pendingTxDisconn.add(database);
          }
          else
          {
             disconnectImmediately(database, false);
          }
       }

       /**
        * Tests whether a logical connection to a database exists in this context,
        * given a logical database name or alias.
        *
        * @param   name
        *          Logical database name or alias.
        *
        * @return  <code>true</code> if a logical connection exists;  else
        *          <code>false</code>.
        */
       private logical connectedImpl(character name)
       {
          name = normalizeAliasOrLDB(name);

          // Get the logical DB name.
          String ldb = getLDBName(name.toStringMessage());

          ConnectInfo info = connected.get(ldb);
          Database database = info != null ? info.database : null;
          boolean isConn = (database != null                   &&
                            !pendingDisconn.contains(database) &&
                            !pendingTxDisconn.contains(database));

          if (LOG.isLoggable(Level.FINE))
          {
             String ctx = Utils.describeContext();

             String msg = String.format("{%s} Database '%s' (%s [%s]) connected status:  %s",
                                        ctx,
                                        name,
                                        ldb,
                                        (database != null ? database.toString() : null),
                                        isConn);
             LOG.log(Level.FINE, msg);

             if (LOG.isLoggable(Level.FINEST))
             {
                msg = String.format("{%s} connected:  %s", ctx, connected);
                LOG.log(Level.FINEST, msg);
                msg = String.format("{%s} pendingDisconn:  %s", ctx, pendingDisconn);
                LOG.log(Level.FINEST, msg);
                msg = String.format("{%s} pendingTxDisconn:  %s", ctx, pendingTxDisconn);
                LOG.log(Level.FINEST, msg);
             }
          }

          return logical.of(isConn);
       }

       /**
        * Create an alias for a logical database name. The alias may not conflict
        * with an existing logical database name (except for that of the database
        * which it aliases).  The alias will be created regardless of whether the
        * target database currently is connected;  however, an error will be
        * raised (or silently registered) in the event it is not, after the
        * alias has been created.  The alias persists until explicitly deleted
        * with one of the variants of the <code>deleteAlias</code> method, or
        * until the current session ends.
        *
        * @param   alias
        *          Alias to be created.
        * @param   ldbName
        *          Logical name of the database to which the alias is assigned.
        *
        * @throws  PersistenceException
        *          if the alias conflicts with an existing logical database name,
        *          other than <code>ldbName</code>.
        * @throws  ErrorConditionException
        *          if the target database is not connected, and we are not in
        *          silent error mode.
        */
       private void createAliasImpl(String alias, String ldbName)
       throws PersistenceException
       {
          // Alias cannot be an existing logical database name (except for the
          // one it aliases).
          if (connected.containsKey(alias) && !alias.equalsIgnoreCase(ldbName))
          {
             throw new PersistenceException(
                "Alias " + alias + " conflicts with a database name",
                389);
          }

          aliases.put(alias, ldbName);

          // Logical database name should already exist.  Otherwise, it is an
          // error.  However, if in silent error mode, the alias will still be
          // created (note that this is different than silent error mode for
          // most other statements).
          if (!connected.containsKey(ldbName))
          {
             ErrorManager.recordOrThrowError(
                1660,
                "Logical dbname " + ldbName + " must be connected in order to add alias " + alias);
          }
       }

       /**
        * Delete the specified alias immediately.
        *
        * @param   alias
        *          Alias to be deleted.
        */
       private void deleteAliasImpl(String alias)
       {
          aliases.remove(alias);
       }

       /**
        * Retrieve the alias at the specified index in the current context.
        *
        * @param   index
        *          One-based index of the alias (aliases are stored in the order
        *          in which they were created).
        *
        * @return  The target alias, or unknown value if <code>index</code> is
        *          invalid.
        */
       private character aliasImpl(int index)
       {
          String alias = iterateToIndex(aliases.keySet(), index);

          return alias == null ? new character() : new character(alias);
       }

       /**
        * Implementation of the <code>DBPARAM()</code> Progress function.
        * Returns, as a character string, a comma-separated list of the parameters
        * used to connect to the database.
        *
        * <p>The method does a lookup on the database's name, or id it is null, the
        * alias will extracted using the index.
        *
        * <p>Parameters returned by this method generally will not match those
        * returned by the original 4GL implementation, since we don't support any
        * others than those handled by <code>ConnectionManager.connect()</code>.
        *
        * @param   nameOrAlias
        *          Logical database name or alias.
        *
        * @param   index
        *          One-based index of the alias (aliases are stored in the order
        *          in which they were created).
        *
        * @return  A comma-separated list of the parameters used to connect to the database, or
        *          unknown value if {@code nameOrAlias} or {@code index} cannot be resolved to a
        *          database.
        */
       private character dbParamImpl(String nameOrAlias, int index)
       {
          if (nameOrAlias == null)
          {
             nameOrAlias = iterateToIndex(connected.keySet(), index);
             if (nameOrAlias == null)
             {
                return new character(); // returning unknown value
             }
          }

          // Get the logical DB name.
          String ldb = getLDBName(nameOrAlias);
          ConnectInfo info = connected.get(ldb);

          if (info == null)
          {
             return new character();
          }

          List<Map.Entry<ConnectOption, ConnectOptionValue>> opts = info.opts;
          if (opts != null)
          {
             return new character(dbparam(opts));
          }
          Database db = info.database;

          StringBuilder sb = new StringBuilder();
          sb.append("-db ").append(db.getName());
          sb.append(",-dt ").append(ConnectionManager.dbType(nameOrAlias));
          sb.append(",-ld ").append(nameOrAlias);
          if (!db.isLocal())
          {
             sb.append(",-H ").append(db.getSocketAddress().getHostName());
             sb.append(",-S ").append(db.getSocketAddress().getPort());
          }

          String userID = connectTimeUserid.get(ldb);
          if (userID != null && !userID.isEmpty())
          {
             sb.append(",-U ").append(userID).append(",-P");
          }

          if (singleClient)
          {
             sb.append(",-1");
          }

          // -B 200
          // -L 10000
          // any other connection params not supported (yet)

          return new character(sb.toString());
       }

       /**
        * Get a description of restrictions for the table in the connected database with a specified
        * alias or at the specified index.  Currently, <code>table</code> is ignored.
        *
        * @param   nameOrAlias
        *          Logical database name or alias.
        *
        * @param   index
        *          One-based index of the alias (aliases are stored in the order
        *          in which they were created).
        * @param   table
        *          table name (currently ignored)
        *
        * @return  a description of restrictions for the table in the connected database
        */
       private character dbRestrictionsImpl(String nameOrAlias, int index, String table)
       {
          if (nameOrAlias == null)
          {
             nameOrAlias = iterateToIndex(connected.keySet(), index);
             if (nameOrAlias == null)
             {
                return new character(); // returning unknown value
             }
          }

          // Get the logical DB name.
          String ldb = getLDBName(nameOrAlias);
          ConnectInfo info = connected.get(ldb);

          if (info == null)
          {
             return new character();
          }

          List<Map.Entry<ConnectOption, ConnectOptionValue>> opts = info.opts;
          if (opts != null)
          {
             boolean readOnly = opts.stream().map(Entry::getKey).anyMatch(co -> co == ConnectOption.READ_ONLY_DB);
             return readOnly ? new character("READ-ONLY") : new character("");
          }
          return new character();
       }

       /**
        * Convert CONNECT options to a DBPARAM string.
        *
        * @param   opts
        *          CONNECT options.
        *
        * @return  DBPARAM string.
        */
       private static String dbparam(List<Map.Entry<ConnectOption, ConnectOptionValue>> opts)
       {
          return opts.stream()
                     .map(e -> e.getKey().getOption() +
                              (e.getKey().isFlag() || e.getKey() == ConnectOption.PASSWORD
                                    ? ""
                                    : " " + e.getValue().value().orElse("")))
                     .collect(Collectors.joining(","));
       }

       /**
        * Get the version number of the target database.
        *
        * @param   name
        *          Logical name or alias of the target database. If <code>null</code>, then
        *          <code>index</code> is used instead.
        * @param   index
        *          One-based index of the target database among all databases currently connected in
        *          this context.
        *
        * @return  The database's version, as extracted from runtime configuration, or unknown value
        *          if <code>index</code> represents the unknown value or is outside of the range of
        *          connected databases.
        *
        * @throws  RuntimeException
        *          if there is an error binding to the directory service to retrieve runtime
        *          configuration information.
        */
       private character dbVersionImpl(String name, int index)
       {
          String ldb = null;

          if (name == null)
          {
             // if index is invalid, return unknown value
             if (index < 1 || index > connected.size())
             {
                return (new character());
             }

             ldb = iterateToIndex(connected.keySet(), index);
          }
          else
          {
             ldb = getLDBName(name);

             // if name is not recognized as a logical name or alias, return unknown value
             if (ldb == null)
             {
                return (new character());
             }
          }

          ConnectInfo info = connected.get(ldb);

          // if logical database is not connected, return unknown value
          if (info == null)
          {
             return (new character());
          }

          Database database = info.database;

          String key = database.getId();

          String version = null;

          // check cache first
          synchronized (dbVersionCache)
          {
             version = dbVersionCache.get(key);
          }

          if (version != null)
          {
             return (new character(version));
          }

          // check for dbversion value in the directory
          DirectoryService ds = DirectoryService.getInstance();
          if (!ds.bind())
          {
             throw new RuntimeException("Directory bind failed");
          }

          try
          {
             String nodeID = Utils.findDirectoryNodePath(ds, "database", "container", false);
             if (nodeID != null)
             {
                // check directory for database version for this database
                String path = String.format("%s/%s/p2j/dbversion", nodeID, database.getId());
                version = ds.getNodeString(path, "value");

                // if nothing found, use default (note: Progress appears to return only the major
                // version, no minor version info)
                if (version == null)
                {
                   version = DEFAULT_DBVERSION;
                }
             }
          }
          finally
          {
             ds.unbind();
          }

          // update the cache
          synchronized (dbVersionCache)
          {
             String v = dbVersionCache.get(key);
             if (v != null)
             {
                version = v;
             }
             else
             {
                dbVersionCache.put(key, version);
             }
          }

          return (new character(version));
       }

       /**
        * Update the various housekeeping data structures to reflect a successful connection event,
        * and update connection metadata if it is in use.
        *
        * @param   ldbName
        *          Logical name of connected database.
        * @param   database
        *          Object describing physical database.
        * @param   userId
        *          Current user's ID.
        * @param   tenantName
        *          The name of the tenant.
        */
       private void putConnected(String ldbName, Database database, String userId, String tenantName)
       {
          long connectID;
          synchronized (ConnectionManager.class)
          {
             connectID = ++nextConnectID;
          }

          ConnectInfo info = new ConnectInfo();
          info.connectID = new int64(connectID);
          info.database = database;

          // the first database to be connected gets the DICTDB alias
          if (connected.isEmpty())
          {
             aliases.put(DICTDB, ldbName);
          }

          ConnectInfo oldCI = connected.put(ldbName, info);
          ldbMap.put(database, ldbName);
          tenantsByDatabase.put(ldbName, tenantName);

          if (userId == null)
          {
             // userId will be null for a local database; this call has to occur after adding the LDB
             // name to the connected map
             userId = SecurityOps.getDefaultUserid(ldbName);
          }

          userIds.put(ldbName, userId);
    //      locked.add(ldbName);

          if (dispatcher != null)
          {
             // update connection metadata
             dispatcher.connected(info);
          }
       }

       /**
        * Update the various housekeeping data structures to reflect a disconnection event, and update
        * connection metadata if it is in use.
        *
        * @param   ldbName
        *          Logical name of disconnected database.
        * @param   sessionCleanup
        *          <code>true</code> if this action is being taken as part of cleanup for a session
        *          that is terminating; <code>false</code> if a normal disconnect.
        *
        * @return  database being removed
        */
       private Database removeConnected(String ldbName, boolean sessionCleanup)
       {
          ConnectInfo info = connected.remove(ldbName);

          // if database that was disconnected had DICTDB alias, reset the alias to the next oldest
          // connected database, if any
          String dictdb = aliases.get(DICTDB);
          if (dictdb != null && dictdb.equals(ldbName))
          {
             Iterator<String> iter = connected.keySet().iterator();
             if (iter.hasNext())
             {
                String nextLdbName = iter.next();
                aliases.put(DICTDB, nextLdbName);
             }
             else
             {
                aliases.remove(DICTDB);
             }
          }

          Database database = info.database;
          ldbMap.remove(database);
          userIds.remove(ldbName); // keep in sync
          clients.remove(ldbName); // keep in sync
          locked.remove(ldbName); // keep in sync
          connectTimeUserid.remove(ldbName); // keep in sync

          if (dispatcher != null && !sessionCleanup)
          {
             // update connection metadata
             dispatcher.disconnected(info);
          }

          return database;
       }

       /**
        * Get the version number of the target database.
        *
        * @param   name
        *          Logical name or alias of the target database. If <code>null</code>, then
        *          <code>index</code> is used instead.
        * @param   index
        *          One-based index of the target database among all databases currently connected in
        *          this context.
        *
        * @return  The database's transaction id, or {@code unknown} value if {@code name} is not a
        *          valid database name or {@code index} is outside of the range of connected
        *          databases.
        *
        * @throws  RuntimeException
        *          if there is an error binding to the directory service to retrieve runtime
        *          configuration information.
        */
       private integer dbTaskIdImpl(String name, int index)
       {
          String ldb = null;

          if (name == null)
          {
             if (index < 1 || index > connected.size())
             {
                // if index is invalid, return unknown value
                return new integer();
             }

             ldb = iterateToIndex(connected.keySet(), index);
          }
          else
          {
             ldb = getLDBName(name);
             if (ldb == null)
             {
                // if name is not recognized as a logical name or alias, return unknown value
                return new integer();
             }
          }

          ConnectInfo info = connected.get(ldb);
          if (info == null)
          {
             // if logical database is not connected, return unknown value
             return new integer();
          }

          return new integer(TransactionManager.getTransactionId(info.database.getId()));
       }

       /**
        * Information about a specific database connection (in 4GL terms, not JDBC terms).
        */
       private static class ConnectInfo
       {
          /** Unique connect ID for this connection */
          int64 connectID;

          /** Object describing the connected database */
          Database database;

          /** CONNECT statement options */
          List<Map.Entry<ConnectOption, ConnectOptionValue>> opts;

          /**
           * Get a string representation of this object, primarily for logging/debug purposes.
           *
           * @return  String describing this object.
           */
          public String toString()
          {
             return database.toString() + ":" + connectID;
          }
       }

       /**
        * This class dispatches connect and disconnect events to the listeners which manage related connection
        * and user statistics metadata. One instance is created per {@code ConnectionManager} instance.
        */
       private static class ConnectionDispatcher
       {
          /** Map of primary database to {@code ConnectionListener} objects */
          private final Map<Database, ConnectionListener> listeners = new HashMap<>();

          /** Set of connection info, maintained for context cleanup purposes */
          private final Set<ConnectInfo> connections = new HashSet<>();

          /**
           * Manage a connection event by dispatching it to the appropriate listener.
           *
           * @param   info
           *          Information about the connection to the associated, primary database.
           */
          void connected(ConnectInfo info)
          {
             Database database = info.database;
             if (database.isLocal())
             {
                connections.add(info);

                ConnectionListener listener = getListener(database);
                if (listener != null)
                {
                   listener.databaseConnected(info.connectID);
                }
             }
          }

          /**
           * Manage a disconnection event by dispatching it to the appropriate listener.
           *
           * @param   info
           *          Information about the disconnection from the associated, primary database.
           */
          void disconnected(ConnectInfo info)
          {
             if (connections.remove(info))
             {
                fireDisconnected(info);
             }
          }

          /**
           * This method is called during context cleanup to ensure that any connection listeners which might
           * still be active at this time are notified about the disconnection event.
           */
          void cleanup()
          {
             for (ConnectInfo info : connections)
             {
                fireDisconnected(info);
             }
             connections.clear();
          }

          /**
           * Fire a disconnection event to the appropriate listener.
           *
           * @param   info
           *          Information about the disconnection from the associated, primary database.
           */
          private void fireDisconnected(ConnectInfo info)
          {
             ConnectionListener listener = getListener(info.database);
             if (listener != null)
             {
                listener.databaseDisconnected(info.connectID);
             }
          }

          /**
           * Get the connection listener for the given, primary database.
           *
           * @param   database
           *          Primary database.
           *
           * @return  Connection listener associated with the given, primary database.
           */
          private ConnectionListener getListener(Database database)
          {
             return listeners.computeIfAbsent(database, ConnectTableUpdater::new);
          }
       }

       /**
        * CONNECT statement options.
        */
       private static enum ConnectOption
       {
          /* Single-user Mode*/
          SINGLE_USER("-1", logical.class),
          /* AdminServer Port*/
          ADMIN_PORT("-adminport", integer.class),
          /* Specifies the number of after-image buffers when running AIW*/
          AI_BUFFS("-aibufs", integer.class),
          /* Suspends database activity when an empty after-image (AI) file is unavailable*/
          AFTER_IMAGE_STALL("-aistall", logical.class),
          /* Specifies the number of blocks in the database buffers*/
          BUFFERS("-B", integer.class),
          /* Specifies the number of before-image buffers when running BIW*/
          AFTER_IMAGE_BUFFERS("-bibufs", integer.class),
          /* Sends a message to the log file when the recovery log threshold is reached.
           * Use with -bithold*/
          THRESHOLD_STALL("-bistall", logical.class),
          /* Specifies the maximum size of the recovery log files*/
          REC_LOG_THRESHOLD("-bithold", integer.class),
          /* Requests a number of private read-only buffers*/
          PRIVATE_RO_BUFFERS("-Bp", integer.class),
          /* Requests a maximum number of private buffers*/
          MAX_PRI_BUFFERS("-Bpmax", integer.class),
          /* Specifies the number of index place holders or cursors*/
          INDEX_CURSORS("-c", integer.class),
          /* Reads the database schema from a local file instead of the database*/
          SCHEMA_CAHE_FILE("-cache", Text.class),
          /* Identifies the Java classpath to use when starting an SQL server*/
          JAVA_CLASSPATH("-classpath", Text.class),
          /* Changes the maximum number of index levels*/
          CURSOR_SIZE("-cs", integer.class),
          /* Max connection retries */
          MAX_CONN_RETRIES("-ct", integer.class),
          /* The name of the DataService to connect through a NameServer to an ODBC, ORACLE, or SQL Server */
          DATA_SERVICE("-DataService", Text.class),
          /* Physical Database Name.*/
          DATABASE("-db", Text.class),
          /* Use Direct I/O to open all files in unbuffered mode*/
          DIRECT_IO("-directio", logical.class),
          /* DataServer parameters*/
          DATASERVER("-Dsrv", Text.class),
          /* Identifies the database type*/
          DB_TYPE("-dt", Text.class),
          /* Changes the number (soft limit) of entries in the schema field cache*/
          SCHEMA_FLD_CACHE_SIZE("-fc", integer.class),
          /* Specifies the number of seconds before OpenEdge reuses a before-image cluster*/
          BI_CLUSTER_SIZE("-G", integer.class),
          /* Specifies the number of milliseconds a transaction waits before committing*/
          GROUP_DELAY("-groupdelay", integer.class),
          /* Identifies a remote host*/
          HOSTNAME("-H", Text.class),
          /* Specifies the number of hash table entries for the buffer pool*/
          HASH("-hash", integer.class),
          /* No Crash Protection*/
          NO_INTEGRITY("-i", logical.class),
          /* Lock Table Entries*/
          NUMBER_LOCKS("-L", integer.class),
          /* Assigns the logical database name*/
          LOGICAL_DBNAME("-ld", Text.class),
          /* Lock Timeout in seconds*/
          LOCK_TIMEOUT("-lkwtmo", integer.class),
          /* Specifies the message buffer size for network client/server protocols*/
          MESSAGE_BUFFER_SIZE("-Mm", integer.class),
          /* Network type*/
          NETWORK_TYPE("-N", Text.class),
          /* Number of Users*/
          NUMBER_OF_USERS("-n", integer.class),
          /* Password*/
          PASSWORD("-P", Text.class),
          /* Parameter file*/
          PARAMETER_FILE("-pf", Text.class, false),
          /* Pin Shared Memory */
          PIN_SHARED_MEM("-pinshm", logical.class),
          /* Identifies the properties file an AdminServer uses when File starting a database server
           * or servergroup*/
          CONFIF_PROPS_FILE("-properties", Text.class),
          /* Open a database for read-only access*/
          READ_ONLY_DB("-RO", logical.class),
          /* Service name or port number*/
          SERVICE_NAME("-S", Text.class),
          /* Identifies a logical collection of server processes to start*/
          SERVER_GROUP("-servergroup", Text.class),
          /* Identifies a logical collection of server processes to start*/
          SPIN_LOCK_TRIES("-spin", integer.class),
          /* Identifies the directory or library containing the ABL triggers for a database*/
          TRIGGER_LOCATION("-trig", Text.class),
          /* Specifies the user ID*/
          USERID("-U", Text.class);

          /** option name */
          private final String option;

          /** option type */
          private final Class<?> type;

          /** option case-sensitivity */
          private final boolean caseSensitive;

          /**
           * Constructor
           *
           * @param   option
           *          option name.
           * @param   type
           *          option type.
           * @param   caseSensitive
           *          option case-sensitivity. This allows for options that are case-insensistive, whereas
           *          previously all were case-sensisitive..
           */
          private ConnectOption(String option, Class<?> type, boolean caseSensitive)
          {
             this.option = option;
             this.type = type;
             this.caseSensitive = caseSensitive;
          }

          /**
           * Constructor
           *
           * @param option
           *        option name.
           * @param type
           *        option type.
           */
          private ConnectOption(String option, Class<?> type)
          {
             // connectOption enum without the 3rd parameter defaults caseSensitive to true
             this(option, type, true);
          }

          /**
           * Check if option type is Text
           *
           * @return  <code>true</code> if option type is Text
           */
          public boolean isText()
          {
             return Text.class.equals(type);
          }

          /**
           * Check if option type is logical
           *
           * @return  <code>true</code> if option type is logical (no value)
           */
          public boolean isFlag()
          {
             return logical.class.equals(type);
          }

          /**
           * Get option name.
           *
           * @return  option name.
           */
          public String getOption()
          {
             return option;
          }

          /**
           * Get option type.
           *
           * @return  option type.
           */
          public Class<?> getType()
          {
             return type;
          }

          /**
           * Return CONNECT_OPTION by name.
           *
           * @param   s
           *          option name
           *
           * @return  CONNECT_OPTION or <code>null</code> if not found.
           */
          public static ConnectOption of(String s)
          {
             return Arrays.stream(values()).filter(o -> o.caseSensitive ? s.equals(o.option) : s.toLowerCase().equals(o.option)).findFirst().orElse(null);
          }
       }

       /**
        * Connection option value - base class
        */
       private static abstract class ConnectOptionValue
       {
          /**
           * Get type of the option.
           *
           * @return  type of the option
           */
          public abstract Class<?> type();

          /**
           * Get string value of the option if option has value.
           *
           * @return  string value of the option or Optional.empty() if option has no value
           */
          public abstract Optional<String> value();

          /**
           * Get integer value of the option if option has numeric value.
           *
           * @return  integer value of the option if option has numeric value
           */
          public int intValue()
          {
             throw new IllegalAccessError();
          }

          /**
           * Get boolean value of the option if option has boolean value.
           *
           * @return  boolean value of the option if option has boolean value
           */
          public boolean boolValue()
          {
             throw new IllegalAccessError();
          }

          /**
           * Get text value of the option if option has text value.
           *
           * @return  text value of the option if option has text value
           */
          public String textValue()
          {
             throw new IllegalAccessError();
          }

          /**
           * Create ConnectOptionValue instance for an option value.
           *
           * @param   value
           *          option value
           *
           * @return  ConnectOptionValue instance for an option value.
           */
          public static ConnectOptionValue option(Object value)
          {
             if (value instanceof String)
             {
                return new TextConnectOptionValue((String)value);
             }
             if (value instanceof Boolean)
             {
                return new BoolConnectOptionValue((Boolean)value);
             }
             if (value instanceof Integer)
             {
                return new IntConnectOptionValue((Integer)value);
             }
             throw new IllegalArgumentException();
          }
       }

       /**
        * Numeric connection option value.
        */
       public static class IntConnectOptionValue
       extends ConnectOptionValue
       {
          /** option value */
          private final int value;

          /**
           * Constructor.
           *
           * @param   value
           *          option value.
           */
          public IntConnectOptionValue(int value)
          {
             this.value = value;
          }

          /**
           * Get type of the option.
           *
           * @return  type of the option
           */
          @Override
          public Class<?> type()
          {
             return int.class;
          }

          /**
           * Get integer value of the option.
           *
           * @return  integer value of the option
           */
          @Override
          public int intValue()
          {
             return value;
          }

          /**
           * Get string value of the option wrapped in Optional.
           *
           * @return  string value of the option wrapped in Optional
           */
          @Override
          public Optional<String> value()
          {
             return Optional.of(String.valueOf(value));
          }

          /**
           * Returns a string representation of the object.
           *
           * @return  a string representation of the object.
           */
          @Override
          public String toString()
          {
             return "IntConnectOptionValue [value=" + value + "]";
          }
       }

       /**
        * Logical connection option value.
        */
       public static class BoolConnectOptionValue
       extends ConnectOptionValue
       {
          /** option value */
          private final boolean value;

          /**
           * Constructor.
           *
           * @param   value
           *          option value.
           */
          public BoolConnectOptionValue(boolean value)
          {
             this.value = value;
          }

          /**
           * Get type of the option.
           *
           * @return  type of the option
           */
          @Override
          public Class<?> type()
          {
             return boolean.class;
          }

          /**
           * Get boolean value of the option.
           *
           * @return  boolean value of the option
           */
          @Override
          public boolean boolValue()
          {
             return value;
          }

          /**
           * Get string value of the option wrapped in Optional.
           *
           * @return  Optional.empty() as logical options have no value.
           */
          @Override
          public Optional<String> value()
          {
             return Optional.empty();
          }

          /**
           * Returns a string representation of the object.
           *
           * @return  a string representation of the object.
           */
          @Override
          public String toString()
          {
             return "BoolConnectOptionValue [value=" + value + "]";
          }
       }

       /**
        * Text connection option value.
        */
       public static class TextConnectOptionValue
       extends ConnectOptionValue
       {
          /** option value */
          private final String value;

          /**
           * Constructor.
           *
           * @param   value
           *          option value.
           */
          public TextConnectOptionValue(String value)
          {
             this.value = value;
          }

          /**
           * Get type of the option.
           *
           * @return  type of the option
           */
          @Override
          public Class<?> type()
          {
             return String.class;
          }

          /**
           * Get text value of the option.
           *
           * @return  text value of the option
           */
          @Override
          public String textValue()
          {
             return value;
          }

          /**
           * Returns a string representation of the object.
           *
           * @return  a string representation of the object.
           */
          @Override
          public String toString()
          {
             return "TextConnectOptionValue [value=" + value + "]";
          }

          /**
           * Get string value of the option wrapped in Optional.
           *
           * @return  string value of the option wrapped in Optional
           */
          @Override
          public Optional<String> value()
          {
             return Optional.of(value);
          }
       }

       /**
        * CONNNECT statement options' parser
        */
       private static class OptionsParser
       {
          /** parsed options for all databases */
          private final List<List<Map.Entry<ConnectOption, ConnectOptionValue>>> dboptions = new ArrayList<>();

          /** processed configuration files */
          private final Set<String> pfs = new HashSet<>();

          /** parsed options for current database */
          private List<Map.Entry<ConnectOption, ConnectOptionValue>> options = new ArrayList<>();

          /** database name found flag */
          private boolean dbnameFound = false;

          /**
           * Constructor.
           *
           * @param   dbnameFound
           *          database name found flag
           */
          private OptionsParser(boolean dbnameFound)
          {
             this.dboptions.add(options);
             this.dbnameFound = dbnameFound;
          }

          /**
           * Parse options
           *
           * @param   inlineArgs
           *          inline options
           *
           * @return  List of all options in lists of entries
           *
           * @throws  PersistenceException
           *          if a recursive use of a configuration file was detected.
           */
          public List<List<Map.Entry<ConnectOption, ConnectOptionValue>>> parse(List<Object> inlineArgs)
          throws PersistenceException
          {
             Iterator<Object> iter = inlineArgs.iterator();
             ConnectOption opt = null;
             boolean wasBadOption = false;
             boolean noValueExpected = false;

             while (iter.hasNext())
             {
                String arg = ParamFileReader.removeEnclosingQuotes(iter.next().toString());
                if (arg.startsWith("-"))
                {
                   opt = ConnectOption.of(arg);
                   wasBadOption = false;
                   noValueExpected = false;

                   if (opt == null)
                   {
                      ErrorManager.recordOrShowError(
                         1402,
                         String.format("Warning: %s is not a database parameter and was ignored.", arg),
                         false,
                         false,
                         false);
                      wasBadOption = true;
                      continue;
                   }
                   if (opt.isFlag())
                   {
                      options.add(new AbstractMap.SimpleEntry<>(opt, ConnectOptionValue.option(true)));
                      noValueExpected = true;
                      continue;
                   }
                   if (!iter.hasNext())
                   {
                      ErrorManager.recordOrShowError(
                         1403,
                         String.format("You have not supplied a parameter for argument %s.", arg),
                         false,
                         false,
                         true);
                      return null;
                   }
                }
                else
                {
                   if (wasBadOption)
                   {
                      wasBadOption = false;
                      continue;
                   }
                   if (opt == null && !dbnameFound)
                   {
                      options.add(
                         new AbstractMap.SimpleEntry<>(ConnectOption.DATABASE, ConnectOptionValue.option(arg)));
                      dbnameFound = true;
                      continue;
                   }
                   if (opt == null || noValueExpected)
                   {
                      ErrorManager.recordOrShowError(
                         301,
                         String.format("Could not recognize argument: %s.", arg),
                         false,
                         true,
                         true);
                      return null;
                   }
                   if (opt == ConnectOption.PARAMETER_FILE)
                   {
                      parse(arg);
                      continue;
                   }
                   if (opt == ConnectOption.DATABASE)
                   {
                      if (dbnameFound)
                      {
                         options = new ArrayList<>();
                         dboptions.add(options);
                      }
                      dbnameFound = true;
                   }

                   Object optValue = opt.isText() ? arg : Integer.parseInt(arg);
                   options.add(new AbstractMap.SimpleEntry<>(opt, ConnectOptionValue.option(optValue)));
                }
             }
             return dboptions;
          }

          /**
           * Parse -pf file options
           *
           * @param   pfFile
           *          -pf file path
           *
           * @throws  PersistenceException
           *          if a recursive use of a configuration file was detected.
           */
          public void parse(String pfFile)
          throws PersistenceException
          {
             if (pfs.contains(pfFile))
             {
                LOG.warning("Recursive use of the configuration file " + pfFile);
                return;
             }
             pfs.add(pfFile);

             String pfContent = ParamFileReader.readPfFile(pfFile);
             if (pfContent == null)
             {
                ErrorManager.recordOrShowError(1247,
                                               String.format("Unable to open parameter file %s, errno 2.", pfFile),
                                               false);
                return;
             }

             String[] args = pfContent.split(" ");

             List<Object> pfFileArgs = new ArrayList<>();
             for (int i = 0; i < args.length; i++)
             {
                String txt = args[i];

                if (txt.length() > 0)
                {
                   pfFileArgs.add(txt);
                }
             }
             parse(pfFileArgs);
          }
       }
    }