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);
      }
   }
}