# DatabaseManager.java

    /*
    ** Module   : DatabaseManager.java
    ** Abstract : Registers, configures, and manages databases and exposes APIs for persistence runtime use.
    **
    ** Copyright (c) 2004-2025, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
    ** 001 ECF 20050901   @22473 Created initial version. Extracted and
    **                           enhanced database configuration/management
    **                           functionality from Persistence class.
    ** 002 ECF 20050915   @23252 Store Hibernate configurations during
    **                           initialization. Provide lookups of ORM
    **                           metadata at runtime.
    ** 003 ECF 20051111   @23378 Added notNullProperties method. Allows query
    **                           of all non-nullable properties for a specific
    **                           DMO class. Also replaced many checked
    **                           exceptions with unchecked exceptions.
    ** 004 ECF 20051204   @23668 Added permanentDatabases method. Returns an
    **                           iterator over auto-connect, permanent
    **                           databases.
    ** 005 ECF 20060112   @23942 Partial integration with P2J server. Some
    **                           directory service access points implemented.
    ** 006 ECF 20060117   @23958 Remaining directory service access points
    **                           implemented. Hibernate properties are now
    **                           read from the directory.
    ** 007 ECF 20060121   @24040 Integrated with P2J server. Replaced
    **                           ThreadLocal with ContextLocal.
    ** 008 ECF 20060130   @24353 Added temp table support. Introduced new
    **                           methods and inner classes to support runtime
    **                           management of temp tables.
    ** 009 ECF 20060309   @24993 Fixed NPE in notNullProperties method. Added
    **                           a safety check to test the iterator for
    **                           content before accessing it.
    ** 010 ECF 20060317   @25231 Added HQL where clause preprocessing support.
    **                           Configuration data about lists of composite
    **                           properties is stored for runtime lookup.
    ** 011 ECF 20060329   @25257 Changed directory entry name 'permanent' to
    **                           'load_at_startup'. The former name was not
    **                           intuitive in that the _temp (temporary table
    **                           database) is actually a permanent database
    **                           (i.e., loaded at server startup).
    ** 012 ECF 20060504   @26006 Added runtime support for the creation and
    **                           dropping of secondary temp tables. These are
    **                           tables created to support the extent fields
    **                           of a primary temp table.
    ** 013 ECF 20060620   @27504 Added facility to store and report database
    **                           dialects. Dialects are stored during startup
    **                           and are queried using the getDialect method.
    ** 014 ECF 20060718   @28090 Made class public. Also exposed a number of
    **                           generally useful methods as public.
    ** 015 ECF 20060831   @29084 Enable schema lookup by PDB or by DMO class.
    ** 016 NVS 20070126   @32007 "database" node of the directory is searched
    **                           as a server level standard search item.
    **                           HIBERNATE properties are read in natural
    **                           data types and converted to strings.
    ** 017 ECF 20070212   @32159 Fixed lookups by DMO implementation class.
    **                           Need to scan up chain of superclasses in case
    **                           the caller of these lookup methods obtained
    **                           the DMO class by invoking getClass() on a DMO
    **                           instance. This is necessary because such an
    **                           instance may actually be a proxied subclass
    **                           of the actual DMO implementation class.
    ** 018 ECF 20070412   @32977 intern() table name strings. This will use
    **                           slightly more permanent generation heap, but
    **                           will avoid the creation of many duplicate
    **                           instances, since these are used heavily for
    **                           record buffers and record identifiers.
    ** 019 ECF 20070416   @33020 Integrated user interrupt handling.
    ** 020 ECF 20070508   @33455 Added getColumnToPropertyMap(). Maps column
    **                           names to their corresponding Java property
    **                           names from information read from Hibernate
    **                           mapping documents.
    ** 021 ECF 20070517   @33675 Modified TempTableHelper's index generation.
    **                           Added a unique index on a temp table's
    **                           primary key.
    ** 022 ECF 20070629   @34903 Minor optimization. Replaced StringBuffer
    **                           with StringBuilder.
    ** 023 ECF 20070906   @34995 Optimized temp table indices. Ensure leading
    **                           component of every index is multiplex ID. If
    **                           no other indices exist, create an index on
    **                           multiplex ID only.
    ** 024 ECF 20070911   @35284 Replaced database name string with Database
    **                           information object. Required for non-local
    **                           database connections.
    ** 025 ECF 20071002   @35330 Exposed certain members as public. Expanded
    **                           use of generics.
    ** 026 ECF 20071018   @35601 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.
    ** 027 ECF 20071120   @35908 Completed work to support remote database
    **                           connections. Corrected transient database
    **                           registration/deregistration. Implemented code
    **                           to fetch and filter DatabaseConfig properties
    **                           from a remote server. Updated initialize() to
    **                           initialize LockManagerMultiplexer and
    **                           DirtyReadMultiplexer network service
    **                           providers.
    ** 028 ECF 20071128   @36120 Modified initialization. Temp table database
    **                           is not included in set of locally managed
    **                           databases.
    ** 029 ECF 20080111   @36797 Modified initialization. Exposed remote API
    **                           for IdentityManagerMultiplexer to allow
    **                           remote servers to request this server to
    **                           generate unique primary keys for a database
    **                           managed by the local server.
    ** 030 ECF 20080118   @36859 Added logging to registerDatabase().
    ** 031 ECF 20080310   @37474 Extracted TempTableHelper into a separate,
    **                           top level class. Moved TableMapping as a
    **                           secondary class into TempTableHelper.java.
    ** 032 ECF 20080331   @37730 Code cleanup. Renamed certain methods and
    **                           added more generics.
    ** 033 SVL 20080421   @38096 If foreign keys are disabled, check whether
    **                           all columns are mapped is disabled too.
    ** 034 CA  20080425   @38112 Modified initialization - a dirty database
    **                           will be created for each non-temp database.
    **                           On registration, this dirty database will
    **                           have all its foreign-key relations dropped,
    **                           except the relations regarding secondary
    **                           tables (which keep the "extent" data). This
    **                           database is an in-memory, H2 database.
    ** 035 ECF 20080529   @38484 Fixed initialization. Secondary tables for
    **                           extent fields must be mapped for permanent
    **                           tables as well as for temporary tables.
    ** 036 ECF 20080605   @38602 Initialize DirtyShareMultiplexerImpl.
    **                           Replaces DirtyReadMultiplexerImpl.
    ** 037 ECF 20080606   @38640 Added getSchemaByInterface() and supporting
    **                           code.
    ** 038 ECF 20080610   @38679 Changed getColumnToPropertyMap() signature.
    **                           Optionally include primary key 'id' mapping
    **                           in resulting map.
    ** 039 ECF 20080611   @38693 Improved support for computed columns.
    **                           Information for computed columns needed by
    **                           dirty databases is now collected during
    **                           server initialization, so it is available to
    **                           the HQLPreprocessor when generating HQL
    **                           optimized to the dirty databases' dialect.
    **                           This required making JDBC connections to
    **                           primary databases available during server
    **                           initialization, before the Hibernate
    **                           infrastructure is fully ready.
    ** 040 ECF 20080624   @38917 Optimized early connection management. These
    **                           connections are now recycled within a (very)
    **                           crude connection pool. No error handling was
    **                           added, since a JDBC error during server
    **                           initialization is generally a fatal error.
    ** 041 SVL 20080725   @39176 UnpooledConnectionProvider is specified as
    **                           the connection provider for dirty databases.
    ** 042 SVL 20080806   @39312 DirtyShareContext and DirtyShareManager
    **                           unregistered into deregisterDatabase().
    ** 043 CA  20080815   @39453 Support API change in DBUtils.
    ** 044 GES 20090421   @41828 Matched utility class package change.
    ** 045 ECF 20090617   @42765 Minor startup optimization. Avoid unnecessary
    **                           work in mapComputedCharacterColumns().
    ** 046 ECF 20090801   @43497 Added support for record lock administration. New
    **                           getAllRecordLockInfo() method returns a snapshot
    **                           of record lock information for all managed
    **                           databases.
    ** 047 SVL 20091126   @44443 Added getDatabaseTables() and
    **                           getEntityNameByTableName() functions.
    ** 048 CA  20101029          Added getBackingTableName(dmoClass), which reads
    **                           the DMO's backing table name directly from its
    **                           corresponding .hbm.xml file. Also,set any dialect
    **                           specific VM properties before the database driver
    **                           is loaded.
    ** 049 CA  20101202          The remote connection URL is determined using the
    **                           dialect.
    ** 050 SVL 20130331          Upgraded to Hibernate 4.
    ** 051 VMN 20130711          Fixed registerDatabase, which must store Hibernate configs before
    **                           computed columns are mapped.
    ** 052 SVL 20130716          Added support for dynamic tables.
    ** 053 SVL 20130822          Added support for secondary dynamic tables.
    ** 054 CA  20131015          Added support for registration/deregistration of legacy table/field
    **                           names.
    ** 055 ECF 20130806          Added metadata support; moved logging to J2SE logging framework.
    ** 056 ECF 20131028          Ensure metadata databases are pre-loaded, even if not auto-connect
    **                           (TODO: this may not be optimal for databases which are never or
    **                           seldom connected, since this will consume memory for no good reason).
    **                           Added EntityResolver parameter to getBackingTableName variant which
    **                           reads hbm files, to prevent error finding DTD when launching server
    **                           offline. Added lock metadata support.
    ** 057 OM  20131029          Added support for dirty database registration for non
    **                           load-at-startup databases.
    **                           Fixed cleanup of localTransients context-local variable.
    ** 058 SVL 20131204          Do not remove schema mapping from TableMapper if there are other
    **                           databases sharing the same schema.
    ** 059 ECF 20140116          Change to LockTableUpdater constructor signature.
    ** 060 SVL 20140205          Added dynamicComposites.
    ** 061 OM  20140417          Granted mapComputedCharacterColumns() package access.
    ** 062 CA  20140531          Added a weight for the context-local vars, to ensure predetermined
    **                           order during context reset.
    ** 063 ECF 20140611          Rolled back 061, refactored mapComputedCharacterColumns, extracting
    **                           a worker method which also is called from registerDynamicTable.
    **                           Combined multiple, context-local variables into inner class Context,
    **                           stored under a single variable. Added context-local mapping for
    **                           computed character column info, for dynamically generated tables.
    ** 064 ECF 20141204          Only populate a metaschema database once per schema, instead of once
    **                           per physical database; allow qualified PDB names as directory IDs;
    **                           reduce log noise when reconnecting a transient database. Don't
    **                           initialize metaschema databases if MetadataManager did not
    **                           initialize.
    ** 065 ECF 20141220          Allow early connection use during transient database registration to
    **                           avoid premature dependency on Persistence instance.
    ** 066 OM  20150114          Fixed collection of processed meta databases in initialization.
    ** 067 ECF 20150123          Call ConnectionManager during initialization to initialize connection
    **                           metadata support.
    ** 068 ECF 20150414          Log warning instead of throwing IllegalArgumentException if _temp
    **                           database is configured in directory but there are no static DMOs
    **                           defined for the application (in dmo_index.xml).
    ** 069 ECF 20150504          Optimized normalizeDMOClass to avoid hitting context local variable
    **                           if possible.
    ** 070 ECF 20150801          Added support for database statistics.
    ** 071 OM  20151130          Removed deprecated import.
    ** 072 EVL 20160223          Javadoc fixes to make compatible with Oracle Java 8 for Solaris 10.
    ** 073 ECF 20160225          Change required by H2 upgrade to 1.4.191.
    ** 074 ECF 20160305          Enable multi-threaded use of early connection pool during server
    **                           initialization to reduce server startup time.
    ** 075 IAS 20160621          Added support for read-only tables, use SAX parser for Hibernate
    **                           mapping files' read
    ** 076 ECF 20160615          Implemented custom dirty DMO checking strategy.
    ** 077 ECF 20160827          Do not intern table name returned by getBackingTableName. The code
    **                           paths which call this method do not store the name long term, so it
    **                           is not worth taking the performance hit of the intern call.
    ** 078 ECF 20160830          Fixed collection of read-only entities. Refactored startup to always
    **                           initialize _temp database services, using default settings if this
    **                           database is not configured explicitly in the directory. Only specific
    **                           override settings should be specified in the directory now.
    ** 079 HC  20170120          Added UDF version reporting when a database is being initialized.
    ** 080 OM  20160712          Called setupIdentityManager() for meta database.
    ** 081 HC  20170425          Explicitly disabled bean validation for Hibernate in case a
    **                           JSR 303 validation appears on the classpath (GWT for example).
    ** 082 ECF 20171030          Implemented transaction metadata support.
    ** 083 ECF 20180331          Allow metadata database to be bootstrapped without primary database.
    ** 084 ECF 20190620          EmptyIterator API change.
    ** 085 ECF 20190827          Ensure permanentConnections are iterated in the order added.
    **     CA  20190902          Fixed problems when connecting to a FWD server running a database
    **                           schema which is not actively managed by the requester, too.
    ** 086 CA  20200503          Added TENANT-NAME() and IS-DB-MULTITENANT() runtime stub.
    ** 087 IAS 20200503          Added getDatabaseDMOs() method.
    ** 088 ECF 20200419          Removed Hibernate dependencies.
    ** 089 ECF 20200916          Moved getSchemaByInterface to DmoMeta.
    ** 090 AIL 20200918          Removed temp-table initialization at server startup.
    **     OM  20200924          P2JIndexComponent carries multiple information to avoid map lookups for them.
    **     OM  20201001          Improved DMO manipulation performance by caching slow Property annotation access.
    **     CA  20201003          Use an identity HashSet where possible.
    **     OM  20201012          Force use locally cached meta information instead of map lookup.
    **     AIL 20210319          Disable undo-log for primary temporary database.
    **     IAS 20210520          Added support for session-based database auto-connect
    **     ECF 20210603          Default load_at_startup to true, other cleanup.
    **     IAS 20210613          Get rid of the permanent connections notion.
    **     ECF 20210617          Fixed dirty database regression; cleaned up initialization.
    **     ECF 20210628          Fixed meta database regression.
    **     ECF 20210630          Fixed meta database regression, again.
    **     OM  20210908          Defined TEMP_TABLE_SCHEMA as SchemaDictionary.TEMP_TABLE_DB (same value).
    **     ECF 20210909          Refactored metadata database initialization. Use same connection pool settings
    **                           for metadata database as for primary database.
    **     ECF 20210926          Only report UDF version once it is safe to do so.
    **     ECF 20210927          Support initialPoolSize for c3p0 configuration.
    **     IAS 29211126          Special logic for auto-connect regarding registration
    **     CA  20211228          IS-DB-MULTITENANT() builtin function must return false, even if is not
    **                           implemented - unknown is not an acceptable returned value for this function.
    **                           Replaced the UnimplementedFeature.missing from tenant-related APIs with a TODO
    **                           comment.
    **     IAS 20220324          Re-worked LockTableUpdater.
    **     IAS 20220408          Additional filtering for Reflections.
    **     IAS 20220512          Fixed getLockTableUpdater for meta tables.
    **     OM  20220516          The dirty databases were not properly prepared for use (no UDF and collation).
    **                           Dropped incorrect warning about double registration of _TEMP database.
    **     IAS 20220601          Use native UDFs by default for PostgreSQL.
    **     IAS 20220608          Fixed native UDFs configuration.
    **     TJD 20220622          Applied CA's patch to make remote database connections work correctly
    **     ECF 20220825          Support SAVE CACHE statement as schemaChanged method. Acts as a notification
    **                           that a mutable schema has been changed and the runtime state needs to be sync'd.
    **     ECF 20221001          Refactored SAVE CACHE support into separate SchemaCheck class.
    **     TW  20221024          Implement support for driver_properties.
    **     ECF 20230125          Implement mutable configuration and run schema check at database registration for
    **                           mutable databases.
    **     CA  20220503          Added stubbed runtime for CREATE DATABASE and SAVE CACHE language statements.
    **     CA  20220531          SQL UDFs are now default, if use_java_udfs is not configured in directory.xml.
    **     ECF 20220825          Support SAVE CACHE statement as schemaChanged method. Acts as a notification
    **                           that a mutable schema has been changed and the runtime state needs to be sync'd.
    **     ECF 20221001          Refactored SAVE CACHE support into separate SchemaCheck class.
    **     TW  20221024          Implement support for driver_properties.
    **     AL2 20230112          Comply with the new signature of H2 connection settings; don't disable locking.
    **     ECF 20230125          Implement mutable configuration and run schema check at database registration for
    **                           mutable databases.
    ** 091 ECF 20230301          Changed isMutable access to package private.
    ** 092 GBB 20230512          Logging methods replaced by CentralLogger/ConversionStatus.
    ** 093 DDF 20230620          Replaced static initialization of values from the directory configuration with
    **                           a method called at server bootstrap.
    **     DDF 20230627          Made forceNoUndoTempTables private and made it accessible through a method.
    ** 094 GBB 20230825          SecurityManager session methods calls updated.
    ** 095 RFB 20231013          Fixed a NPE where the dialect hasn't been added to the mapping yet.
    ** 096 OM  20240311          API changes in MetadataManager.
    **     OM  20240313          When a database is registered, its dialect must be created before checking it
    **                           for structural changes.
    **     OM  20240314          Added the dynamic permanent tables to DMO scan.
    ** 097 OM  20240402          Dropped dynamicDMOIfaces field and related methods: they had no effect.
    ** 098 TJD 20240220          Removed compiler warnings related with libraries updates
    ** 099 RAA 20240423          Added multi-tenancy support.
    **     RAA 20240513          Better URL validation and error handling in createSettings.
    **     RAA 20240517          Changed the way the dialect is extracted from the database URL.
    **     RAA 20240604          Fixed parameters for extracting the dialect from URL.
    **     RAA 20240613          Added tenantDbConfigs map for storing the database configurations per tenant.
    **     RAA 20240627          Re-factored multi-tenancy to allow multiple database instances per tenant.
    **     RAA 20240702          Replaced DBCredentials occurrences with TenantDatabaseDescriptor.
    **     RAA 20240710          Changed how the multi-tenancy landlord database is initialized.
    **     RAA 20240715          Clarified the type of database name occurrences.
    **     RAA 20240717          Removed multi-tenancy initialization from this class.
    **     RAA 20240723          Added removeTenantDbFromLockTable method.
    **     RAA 20240726          Updated parameters of the getDatabaseCredentials() calls.
    **     RAA 20240730          Call activateMetaDb when registering the tenant meta databases.
    **     RAA 20240807          Removed TenantDatabaseDescriptorKey occurrences. When creating the database
    **                           configuration for a tenant database, pass the schema name of the main database.
    ** 100 AD  20240313          Modified getDatabaseDMOs(Database) to ignore existing DMO implementation files.
    ** 101 ICP 20240703          Fixed getDatabaseDMOs method to ensure that tableDMOs is populated correctly.
    ** 102 OM  20240815          TenantManager.getDatabase() returns a Database object of a specific type.
    **     OM  20240816          Fixed configuration for META tenant databases.
    **     OM  20240909          Basic implementation of tenant-related functions.
    ** 103 OM  20241001          Subsequent implementation of tenant management functions.
    ** 104 OM  20241128          Multi-tenant runtime support: selected the proper persistence context, eventually
    **                           based on [sharedDb] parameter.
    ** 105 OM  20250110          Added sequence support for multi-tenant databases. Removed deprecated APIs.
    ** 106 OM  20251016          Added support for additional dialects for landlord database.
    ** 107 LS  20250211          Added support for session hook through directory configuration.
    ** 108 ICP 20241114          Added getAllDatabases method to return all databases the server interacts with.
    ** 109 LS  20250325          Configured the tenant database to inherit the c3p0 settings from the default db.
    ** 110 OM  20250310          Moved readOnly set to DmoMetadataManager.
    ** 111 GBB 20250403          Adding legacy log.
    ** 112 LS  20250403          Set breakAfterAcquireFailure flag in the c3p0 configuration settings.
    ** 113 OM  20250331          Added c3p0 connection parameters for tenants.
    */

    /*
    ** 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.io.*;
    import java.lang.reflect.InvocationTargetException;
    import java.sql.*;
    import java.util.*;
    import java.util.Date;
    import java.util.concurrent.*;
    import java.util.concurrent.atomic.*;
    import java.util.concurrent.locks.*;
    import java.util.logging.*;
    import com.goldencode.p2j.schema.*;
    import com.goldencode.p2j.admin.*;
    import com.goldencode.p2j.directory.*;
    import com.goldencode.p2j.net.*;
    import com.goldencode.p2j.persist.Database.*;
    import com.goldencode.p2j.persist.dialect.*;
    import com.goldencode.p2j.persist.dirty.*;
    import com.goldencode.p2j.persist.lock.*;
    import com.goldencode.p2j.persist.lock.LockManager;
    import com.goldencode.p2j.persist.meta.*;
    import com.goldencode.p2j.persist.orm.*;
    import com.goldencode.p2j.persist.orm.Session;
    import com.goldencode.p2j.persist.orm.TenantManager.*;
    import com.goldencode.p2j.persist.remote.*;
    import com.goldencode.p2j.security.*;
    import com.goldencode.p2j.security.SecurityManager;
    import com.goldencode.p2j.util.*;
    import com.goldencode.p2j.util.ErrorManager;
    import com.goldencode.p2j.util.Utils;
    import com.goldencode.p2j.util.logging.*;
    import org.reflections.*;
    import org.reflections.scanners.*;
    import org.reflections.util.*;

    /**
     * Manages configuration for all databases accessed by the current server,
     * and provides access to Hibernate session factories to create new sessions.
     * This class is a helper for the {@link Persistence} class, but since it
     * contains generally useful methods, it has been made public.
     * <p>
     * During server bootstrap, this class reads configuration data from the
     * directory service.  This information includes the names of databases which
     * must be configured and be permanently accessible, as well as those which
     * are considered "transient".  A transient database is one which can be
     * connected to and disconnected from (in the Progress sense) on demand, from
     * application code.
     * <p>
     * When a database is initialized for use via Hibernate, whether permanent
     * or transient, additional configuration data, including Hibernate's
     * configuration properties, is read from the directory service.  This is
     * assembled into a <code>Properties</code> object and passed to Hibernate's
     * configuration subsystem.
     * <p>
     * Each database is represented by a Hibernate <code>SessionFactory</code>
     * object, which is the object which the <code>Persistence</code> class uses
     * to create database <code>Session</code>s lazily.  In addition to the
     * configuration options read from the directory, each session factory
     * requires the list of ORM mapping classes which will be used to represent
     * database tables in any session established with that database.
     * <p>
     * <code>SessionFactory</code> objects for transient databases are created on
     * demand, which can be an expensive process.  A session factory for a
     * transient database is required when at least one client session connects
     * dynamically to that database.  A reference count is maintained across
     * client sessions for each transient database, such that a session factory
     * need not be created for one client context, if another is already using it.
     * Session factory objects for transient databases thus are not discarded
     * while any context is connected to the associated database.
     * <p>
     * This class can both register remote databases for local use, as well as
     * serve requests from remote servers to provide access to local databases.
     * Whether a database is remote or local is transparent to users of this
     * class.  Both are registered using the {@link #registerDatabase(Database)}
     * method.
     * <p>
     * Implementation Note:  several methods of this class synchronizes on two objects:
     * <ul>
     *   <li><code>configLock</code>
     *   <li><code>sessionFactories</code>
     * </ul>
     * <p>
     * <code>configLock</code> is the less granular of these. It is important
     * that any code path which requires access to both locks simultaneously
     * always acquire <code>configLock</code> first, then
     * <code>sessionFactories</code>, to avoid deadlock.
     */
    public final class DatabaseManager
    {
       /** Logger. */
       private static final CentralLogger log = CentralLogger.get(DatabaseManager.class.getName());

       /** session auto-connection registry key */
       private static final String CFG_AUTO_CONNECT = "database-connections";

       /** Reserved name of temp table schema */
       public static final String TEMP_TABLE_SCHEMA = SchemaDictionary.TEMP_TABLE_DB;

       /** Temp table database information object */
       public static final Database TEMP_TABLE_DB =
          new Database(TEMP_TABLE_SCHEMA, Database.Type.PRIMARY, true);

       /** Reserved name of primary key identifier column/property */
       public static final String PRIMARY_KEY = Session.PK;

       /** Port for mixed mode, remote, SSL connections to embedded databases */
       public static final int MIXED_MODE_SSL_PORT = 7691;

       /** Initial connection pool size key name */
       public static final String CP_INIT_POOL_SIZE = "c3p0.initialPoolSize";

       /** Minimum connection pool size key name */
       public static final String CP_MIN_POOL_SIZE = "c3p0.minPoolSize";

       /** Maximum connection pool size key name */
       public static final String CP_MAX_POOL_SIZE = "c3p0.maxPoolSize";

       /** Connection pool acquire increment key name */
       public static final String CP_ACQUIRE_INCREMENT = "c3p0.acquireIncrement";

       /** Connection pool max statements per connection key name */
       public static final String CP_MAX_STATEMENTS_PER_CONNECTION = "c3p0.maxStatementsPerConnection";

       /** Connection pool max idle time key name */
       public static final String CP_MAX_IDLE_TIME = "c3p0.maxIdleTime";

       /** Connection pool break after acquire failure key name */
       public static final String CP_BREAK_AFTER_ACQUIRE_FAILURE = "c3p0.breakAfterAcquireFailure";

       /** Map of local DatabaseConfig objects, keyed by database info objects */
       private static final Map<Database, DatabaseConfig> dbConfigs = new ConcurrentHashMap<>();

       /** Map that stores the database configurations per tenant. */
       private static final Map<String, Map<String, DatabaseConfig>> tenantDbConfigs = new ConcurrentHashMap<>();

       /** Map of schema names, keyed by database */
       private static final Map<Database, String> schemaByDatabase = new ConcurrentHashMap<>();

       // TODO: temporary solution for backward compatibility. Should be removed
       /** Local databases configured as 'load_at_statup' */
       private static final ArrayList<String> loadedOnStartupDBs = new ArrayList<>();

       /** Map of session auto-connected databases to the guarded sets of connected sessions. */
       private static final Map<Database, AutoConnected> dbSessions = new ConcurrentHashMap<>();

       /** Auto-connections' cleanup executor */
       private static final ScheduledExecutorService scheduler =
          Executors.newScheduledThreadPool(1, new ThreadFactory()
          {
             @Override
             public Thread newThread(Runnable r)
             {
                Thread t = new Thread(r, "Auto-connections' cleanup thread");
                t.setDaemon(true);
                return t;
             }
          });

       /** Registered dirty databases */
       private static final Set<Database> dirtyDBs = ConcurrentHashMap.newKeySet();

       /** Map of databases to database {@code Dialect} objects */
       private static final Map<Database, Dialect> dialects = new ConcurrentHashMap<>();

       /** Map of transient databases to reference counts of registrations */
       private static final Map<Database, Integer> transientDatabases = new HashMap<>();

       /** Set of all databases managed by the local P2J server instance */
       private static final Set<Database> managed = new LinkedHashSet<>();

       /** Flag indicating temp-tables must be NO-UNDO application-wide */
       private static boolean forceNoUndoTempTables = false;

       /** Flag indicating that the metadata manager was initialized */
       private static boolean useMeta = false;

       /** Context local data */
       private static final ContextLocal<Context> context = new ContextLocal<Context>()
       {
          @Override
          public WeightFactor getWeight()
          {
             return WeightFactor.LEVEL_6;
          }

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

          @Override
          protected void cleanup(Context value)
          {
             value.cleanup();
          }
       };

       /** Object upon which configuration routines synchronize */
       private static final Object configLock = new Object();

       /** Map of LockTableUpdater instances, keyed by database */
       private static Map<Database, LockTableUpdater> lockTableUpdaters = null;

       /** Flag indicating persistence has been initialized */
       private static volatile boolean initialized = false;

       /**
        * This class cannot be instantiated;  all methods are static.
        */
       private DatabaseManager()
       {
       }

       /**
        * Implementation of the CREATE DATABASE language statement.  Just a stub at this time.
        *
        * @param    name
        *           The database name.
        * @param    replace
        *           Flag indicating the REPLACE option is used.
        */
       public static void createDatabase(Text name, boolean replace)
       {
          UnimplementedFeature.missing("CREATE DATABASE statement is not implemented.");
       }

       /**
        * Implementation of the CREATE DATABASE language statement.  Just a stub at this time.
        *
        * @param    name
        *           The new database name.
        * @param    source
        *           The source database name.
        * @param    newInstance
        *           Flag indicating the NEW-INSTANCE option is used.
        * @param    replace
        *           Flag indicating the REPLACE option is used.
        */
       public static void createDatabase(Text name, Text source, boolean newInstance, boolean replace)
       {
          UnimplementedFeature.missing("CREATE DATABASE statement is not implemented.");
       }

       /** TODO: temporary solution for backward compatibility. Should be removed */
       /**
        * Get list of local databases loaded on startup.
        *
        * @return  the list of local databases loaded on startup.
        */
       public static ArrayList<String> loadedOnStartup()
       {
          return loadedOnStartupDBs;
       }

       /**
        * Report the database dialect in use for the given database.
        *
        * @param   database
        *          Database information object.
        *
        * @return  Dialect in use for <code>database</code>.
        */
       public static Dialect getDialect(Database database)
       {
          if (database.isMeta() || database.isDirty())
          {
             return new P2JH2Dialect();
          }

          if (database.isTemporary())
          {
             return TemporaryDatabaseManager.getMyTempDbDialect();
          }

          return dialects.get(database);
       }

       /**
        * Lookup the schema name associated with the given physical database
        * name.
        *
        * @param   database
        *          Database information object.  May be <code>null</code> if
        *          <code>dmoClass</code> represents a temporary table.
        *
        * @return  Schema name for the given database.
        */
       public static String getSchema(Database database)
       {
          return schemaByDatabase.get(database);
       }

       /**
        * Retrieve the database table name for a given DMO implementation class.
        *
        * @param   dmoClass
        *          DMO implementation class to which the table is mapped.
        *
        * @return  Name of the database table.
        */
       public static String getBackingTableName(Class<? extends Record> dmoClass)
       {
          return Session.getMetadata(dmoClass).tables[0];
       }

       /**
        * Get a list of all configured databases managed by this P2J server instance.
        *
        * @return  List of database information objects managed by this server instance.
        */
       public static List<Database> getManagedDatabases()
       {
          synchronized (configLock)
          {
             return new ArrayList<>(managed);
          }
       }

       /**
        * Get a list of all databases P2J server interacts with.
        *
        * @return  List of all databases.
        */
       public static List<Database> getAllDatabases()
       {
          synchronized (configLock)
          {
             return new ArrayList<>(dbConfigs.keySet());
          }
       }

       /**
        * Gather snapshots of all record locks for all, non-temporary, permanently
        * connected databases which are authoritatively managed by this P2J server
        * instance.  Note that this data is for informational, reporting purposes
        * only.  It is guaranteed to be consistent (per database) at the moment it
        * is collected, but it is possible (likely) for this information to become
        * stale quickly.
        *
        * @return  Record lock information for all non-temporary databases managed
        *          by this server.
        */
       public static RecordLockInfo[] getAllRecordLockInfo()
       {
          List<RecordLockInfo> lockInfo = new ArrayList<>();

          for (Database database : getManagedDatabases())
          {
             Persistence persistence = PersistenceFactory.getInstance(database);
             LockManager<?> manager = persistence.getLockManager(!database.isTenant());
             LockAdministrator admin = manager.getLockAdministrator();
             if (admin != null)
             {
                lockInfo.addAll(admin.collectRecordLockInfo());
             }
          }

          return lockInfo.toArray(new RecordLockInfo[lockInfo.size()]);
       }

       /**
        * Get the entity name which corresponds the given table of the given database.
        *
        * @param   database
        *          Database which contains the target table.
        * @param   table
        *          Target table
        *
        * @return  Entity name which corresponds the given table.
        */
       public static String getEntityNameByTableName(Database database, String table)
       {
          return null;
       }

       /**
        * Get the tables of the given database.
        *
        * @param   database
        *          Target database.
        *
        * @return  the tables of the given database.
        */
       public static List<String> getDatabaseTables(Database database)
       {
          String pkg = DmoMetadataManager.getDmoBasePackage() + "." + database.getName();
          String pkgd = pkg + ".";
          // Reflections API uses incorrect regex for filtering package name,
          // (ends with '.' instead of '\\.')
          // so addional filter is required
          Reflections reflections = new Reflections(
                   new ConfigurationBuilder().
                   setUrls(ClasspathHelper.forPackage(pkg)).
                   setScanners(Scanners.SubTypes).
                   filterInputsBy(new FilterBuilder().add(s -> s.startsWith(pkgd)))
          );
          Set<Class<? extends DataModelObject>> dmos = reflections.getSubTypesOf(DataModelObject.class);
          List<String> tables = new ArrayList<>(dmos.size() / 2); // including .Buf interfaces
          dmos.stream().filter(dmo -> !Buffer.class.isAssignableFrom(dmo)).
                        map(DmoMetadataManager::registerDmo).
                        map(DmoMeta::getSqlTableName).
                        forEach(tables::add);
          return tables;
       }

       /**
        * Get the table DMO names of the given database.
        * For a given database, searches through the package corresponding to that databases name and
        * that contains DMO interface classes. Out of all the classes found in that package,
        * all implementations of those interfaces and all Buffer interfaces need to be excluded.
        *
        * @param   database
        *          Target database.
        *
        * @return  List of DMO table names/classes names, prefixed with the package thay are located in.
        */
       public static List<String> getDatabaseDMOs(Database database)
       {
          Set<String> dynamicDmos = null;
          try
          {
             if (isMutable(database))
             {
                dynamicDmos = DmoMetadataManager.getDmoInterfaces(
                      DynamicTablesHelper.DMO_BASE_PACKAGE + "." + database.getName());
             }
          }
          catch (PersistenceException e)
          {
             // silent default to false
          }

          Set<Class<? extends DataModelObject>> dmos = new HashSet<>();
          // Reflections API uses incorrect regex for filtering package name, (ends with '.' instead of '\\.')
          // so additional filter is required
          String pkg = DmoMetadataManager.getDmoBasePackage() + "." + database.getName();
          String pkgd = pkg + ".";
          Reflections reflections = new Reflections(
                new ConfigurationBuilder().setUrls(ClasspathHelper.forPackage(pkg))
                                          .setScanners(Scanners.SubTypes)
                                          .filterInputsBy(new FilterBuilder().add(s -> s.startsWith(pkgd) &&
                                                                                  !s.endsWith(DBUtils.IMPL_SUFFIX + ".class"))));
          dmos.addAll(reflections.getSubTypesOf(DataModelObject.class));
          // including .Buf interfaces
          List<String> tableDMOs = new ArrayList<>(dmos.size() / 2 +
                                                   (dynamicDmos == null ? 0
                                                                        : dynamicDmos.size()));
          dmos.stream()
              .filter(dmo -> !Buffer.class.isAssignableFrom(dmo))
              .map(Class::getName)
              .forEach(tableDMOs::add);

          if (dynamicDmos != null)
          {
             tableDMOs.addAll(dynamicDmos);
          }
          return tableDMOs;
       }

       /**
        * Get a mapping of column names to property names for the given DMO in the given database.
        *
        * @param   database
        *          Target database.
        * @param   dmoClass
        *          Target DMO implementation class.
        * @param   columnNames
        *          Set of column names to be mapped to properties. If {@code null}, all of the table's
        *          columns will be mapped to DMO property names.
        * @param   includePK
        *          {@code true} to include the reserved mapping of primary "recid" to reserved
        *          property name "recid";  {@code false} to omit this mapping.
        *
        * @return  A map whose keys are the given set of column names, and whose values are the
        *          corresponding DMO property names.
        */
       public static Map<String, String> getColumnToPropertyMap(Database database,
                                                                Class<? extends Record> dmoClass,
                                                                Set<String> columnNames,
                                                                boolean includePK)
       {
          DmoMeta dmoInfo = DmoMetadataManager.getDmoInfo(dmoClass);
          if (dmoInfo == null)
          {
             return null;
          }

          Map<String, String> map = new HashMap<>();
          Iterator<Property> it = dmoInfo.getFields(false);
          it.forEachRemaining(p -> {
             if (columnNames == null || columnNames.contains(p.name))
             {
                map.put(p.column, p.name);
             }
          });

          if (includePK)
          {
             map.put(PRIMARY_KEY, PRIMARY_KEY);
          }

          return map;
       }

       /**
        * Return the database object for the given logical database name, if the database is mutable and currently
        * connected. Otherwise, raise an exception.
        *
        * @param   ldbName
        *          Logical database name (or alias).
        *
        * @return  Database object.
        *
        * @throws  PersistenceException
        *          if the database does not exist, is not connected, or its schema is not mutable by the
        *          application.
        */
       public static Database getMutableDatabase(String ldbName)
       throws PersistenceException
       {
          Database database = ConnectionManager.get().getDatabase(ldbName);

          if (database == null)
          {
             throw new PersistenceException("Database " + ldbName + " not found.");
          }

          if (!isMutable(database))
          {
             throw new PersistenceException("Database " + ldbName + " is immutable. No changes are expected.");
          }

          return database;
       }

       /**
        * Implementation of the IS-DB-MULTITENANT() builtin function.
        *
        * @return  {@code true} if the single connected database is multitenant. If there are more or no database
        *          connected, an error condition is raised.
        */
       public static logical isDbMultiTenant()
       {
          return isDbMultiTenant((String) null);
       }

       /**
        * Implementation of the IS-DB-MULTI-TENANT() builtin function.
        *
        * @param   db
        *          The db name.
        *
        * @return  {@code true} if the {@code db} database is multitenant.
        */
       public static logical isDbMultiTenant(String db)
       {
          if (!ConnectionManager.connected(db).booleanValue())
          {
             ErrorManager.recordOrThrowError(15976, db, "IS-DB-MULTI-TENANT");
             // Database '<dbname>' must be connected for tenant function <name>. (15976)
             return logical.FALSE;
          }

          return new logical(TenantConfig.isEnabled());
       }

       /**
        * Implementation of the IS-DB-MULTI-TENANT() builtin function.
        *
        * @param   db
        *          The db name.
        *
        * @return  {@code true} if the {@code db} database is multitenant.
        */
       public static logical isDbMultiTenant(character db)
       {
          return (db == null || db.isUnknown()) ? logical.UNKNOWN : isDbMultiTenant(db.getValue());
       }

       /**
        * Implementation of the TENANT-NAME() builtin function.
        *
        * @return  The tenant name currently authenticated for the single connected database. If there are more or
        *          no database connected, an error condition is raised.
        */
       public static character getTenantName()
       {
          return getTenantName((String) null);
       }

       /**
        * Implementation of the TENANT-NAME() builtin function.
        *
        * @param   db
        *          The db name.
        *
        * @return  The tenant name currently authenticated for {@code db} database. If there are more or
        *          no database connected, an error condition is raised.
        */
       public static character getTenantName(String db)
       {
          if (db == null)
          {
             if (ConnectionManager.getConnectedDbCount() != 1)
             {
                ErrorManager.recordOrThrowError(15977, "TENANT-ID", "");
                // Must supply a connected database name for tenant function <name>. (15977)
                return new character();
             }
             db = ConnectionManager.ldbName(1).toJavaType();
          }

          if (!ConnectionManager.connected(db).booleanValue())
          {
             ErrorManager.recordOrThrowError(15976, db, "TENANT-NAME");
             // Database '<dbname>' must be connected for tenant function <name>. (15976)
             return new character();
          }

          // if an alias was used, switch to logical name
          db = ConnectionManager.get().getLDBName(db);

          if (!TenantConfig.isEnabled())
          {
             return new character("");
          }

          return new character(
                PersistenceFactory.getInstance(db).getContext(Persistence.PRIVATE_CTX).getTenantName());
       }

       /**
        * Implementation of the TENANT-ID() builtin function.
        *
        * @return  The id of the tenant currently authenticated for {@code db} database. If there are more or no
        *          database connected, an error condition is raised.
        */
       public static int64 getTenantId()
       {
          return getTenantId((String) null);
       }

       /**
        * Implementation of the TENANT-ID() builtin function.
        *
        * @param   db
        *          The db name.
        *
        * @return  The id of the tenant currently authenticated for {@code db} database. If there are more or no
        *          database connected, an error condition is raised.
        */
       public static int64 getTenantId(String db)
       {
          if (db == null)
          {
             if (ConnectionManager.getConnectedDbCount() != 1)
             {
                ErrorManager.recordOrThrowError(15977, "TENANT-ID", "");
                // Must supply a connected database name for tenant function <name>. (15977)
                return new int64();
             }
             db = ConnectionManager.ldbName(1).toJavaType();
          }

          if (!ConnectionManager.connected(db).booleanValue())
          {
             ErrorManager.recordOrThrowError(15976, db, "TENANT-ID");
             // Database '<dbname>' must be connected for tenant function <name>. (15976)
             return new int64();
          }

          // if an alias was used, switch to logical name
          db = ConnectionManager.get().getLDBName(db);

          return new int64(PersistenceFactory.getInstance(db).getContext(Persistence.PRIVATE_CTX).getTenantId());
       }

       /**
        * Implementation of the TENANT-ID() builtin function.
        *
        * @param   db
        *          The db name.
        *
        * @return  The id of the tenant currently authenticated for {@code db} database. If there are more or no
        *          database connected, an error condition is raised.
        */
       public static int64 getTenantId(character db)
       {
          return (db == null || db.isUnknown()) ? new int64() : getTenantId(db.getValue());
       }

       /**
        * Implementation of the TENANT-NAME() builtin function.
        *
        * @param   db
        *          The db name.
        *
        * @return  The tenant name currently authenticated for {@code db} database. If there are more or
        *          no database connected, an error condition is raised.
        */
       public static character getTenantName(character db)
       {
          return (db == null || db.isUnknown()) ? new character() : getTenantName(db.getValue());
       }

       /**
        * Get the database configuration.
        *
        * @param   database
        *          The database whose configuration is requested.
        *
        * @return  the database configuration.
        *
        * @throws  PersistenceException
        *          if an error occurs accessing configuration data.
        */
       public static DatabaseConfig getConfiguration(Database database)
       throws PersistenceException
       {
          return database.isLocal() ? getLocalConfiguration(database) : getRemoteConfiguration(database);
       }

       /**
        * Auto-connect databases configured to be auto-connected on session start
        */
       public static void autoConnect()
       {
          Directory dir = DirectoryManager.getInstance();

          /* TODO: temporary solution for backward compatibility. Should be removed */
          List<String> connectStrings = new ArrayList<>(DatabaseManager.loadedOnStartup());
          connectStrings.addAll(dir.getStrings(Directory.ID_RELATIVE, CFG_AUTO_CONNECT));
          Set<String> connected = new HashSet<>();
          LegacyLogManager logManager = LegacyLogOps.logMgr();

          for (String connectString: connectStrings)
          {
             if (connected.contains(connectString))
             {
                continue;
             }
             log.info("Connecting to [" + connectString + "]");
             ConnectionManager.connect(true,  new Object[] {connectString});
             connected.add(connectString);

             if (logManager.isLoggingEnabled())
             {
                logManager.writeMessage("Connected to database " + connectString + ", user number ?. (9543)",
                                        LoggingEntryType.DB_Connects,
                                        null,
                                        LoggingSubsys.CONN,
                                        LoggingLevel.BASIC);
             }
          }
       }

       /**
        * Deactivate session auto-connected database.
        *
        * <ol>
        * <li>Detach database from the session.</li>
        * <li>If there are no other session connected to the database and the the database is
        *     configured for the de-activation schedule the de-activation after a configured time.
        * </ol>
        *
        * @param   sessionId
        *          User session Id, if <code>null</code> get from the SecurityManager.
        * @param   db
        *          Database to be deactivated.
        * @param   cleanup
        *          Flag indicating that session cleanup is in progress.
        */
       public static void deactivate(Integer sessionId, Database db, boolean cleanup)
       {
          if (sessionId == null)
          {
             sessionId = SecurityManager.getInstance().sessionSm.getSessionId();
          }
          AutoConnected connected = dbSessions.get(db);
          if (connected == null)
          {
             return;
          }

          connected.readLock().lock();
          try
          {
             if (connected.remove(sessionId))
             {
                if (!cleanup)
                {
                   context.get().autoConnected.remove(db);
                }
                if (connected.notActiveSince() != null)
                {
                   DatabaseConfig config = dbConfigs.get(db);
                   if (config != null && config.getDeactivateIfNotUsedSec() >= 0)
                   {
                      if (log.isLoggable(Level.FINE))
                      {
                         log.fine(String.format("Database %s will be deactivated in %s seconds",
                                                db.toString(),
                                                config.getDeactivateIfNotUsedSec()));
                      }
                      scheduler.schedule(
                         () -> deactivate(db, config.getDeactivateIfNotUsedSec()),
                         config.getDeactivateIfNotUsedSec(),
                         TimeUnit.SECONDS);
                   }
                }
             }
          }
          finally
          {
             connected.readLock().unlock();
          }
       }

       /**
        * Returns the value of the flag that indicates if temp-tables must be NO-UNDO.
        *
        * @return  {@code true} if temp-tables must be NO-UNDO,
        *          {@code false} otherwise.
        */
       public static boolean isForceNoUndoTempTables()
       {
          return forceNoUndoTempTables;
       }

       /**
        * Create a database configuration based on the given tenant and database.
        *
        * @param   tenant
        *          The tenant.
        * @param   db
        *          The database for which the configuration is created.
        * @param   credentials
        *          The credentials (URL, username, password) of the given database.
        *
        * @return  The created/retrieved database configuration.
        */
       public static DatabaseConfig createSettings(Tenant tenant,
                                                   Database db,
                                                   TenantDatabaseDescriptor credentials)
       {
          String tenantName = tenant.getTenantName();
          String pdbName = db.getName();
          Map<String, DatabaseConfig> tenantConfigs = tenantDbConfigs.get(tenantName);
          if (tenantConfigs != null)
          {
             DatabaseConfig config = tenantConfigs.get(pdbName);
             if (config != null)
             {
                return config;
             }
          }

          String url = credentials.getUrl();
          Dialect dialect = Dialect.getDialectForURL(url);
          if (dialect == null)
          {
             log.log(Level.WARNING, "Database URL is not valid.");
             return null;
          }

          Database mainDb = TenantManager.getDatabase(TenantManager.DEFAULT_TENANT_NAME,
                                                      credentials.getLdbName(),
                                                      false,
                                                      Type.PRIMARY);

          Settings settings = new Settings(db);
          try
          {
             settings.put("connection.driver_class", DriverManager.getDriver(url).getClass().getName());
             settings.put("connection.url", url);
             settings.put("connection.username", credentials.getUsername());
             settings.put("connection.password", credentials.getPassword());
             settings.put("dialect", dialect.getClass().getName());

             try
             {
                // inherit the c3p0 settings from mainDb
                if (mainDb != null)
                {
                   DatabaseConfig mainDbCfg = getConfiguration(mainDb);
                   Settings mainDbSettings = mainDbCfg.getOrmSettings();

                   settings.put(CP_MIN_POOL_SIZE, mainDbSettings.getInteger(CP_MIN_POOL_SIZE, 4));
                   settings.put(CP_MAX_POOL_SIZE, mainDbSettings.getInteger(CP_MAX_POOL_SIZE, 20));
                   settings.put(CP_ACQUIRE_INCREMENT, mainDbSettings.getInteger(CP_ACQUIRE_INCREMENT, 2));
                   settings.put(CP_MAX_IDLE_TIME, mainDbSettings.getInteger(CP_MAX_IDLE_TIME, 900));
                   settings.put(CP_MAX_STATEMENTS_PER_CONNECTION,
                                mainDbSettings.getInteger(CP_MAX_STATEMENTS_PER_CONNECTION, 100));
                   settings.put(CP_BREAK_AFTER_ACQUIRE_FAILURE,
                                mainDbSettings.getBoolean(CP_BREAK_AFTER_ACQUIRE_FAILURE, true));
                }
                // overwrite any values configured in [credentials] parameter:
                if (credentials.getC3p0MinPool() != null)
                {
                   settings.put(CP_MIN_POOL_SIZE, credentials.getC3p0MinPool());
                }
                if (credentials.getC3p0MaxPool() != null)
                {
                   settings.put(CP_MAX_POOL_SIZE, credentials.getC3p0MaxPool());
                }
                if (credentials.getC3p0AcqInc() != null)
                {
                   settings.put(CP_ACQUIRE_INCREMENT, credentials.getC3p0AcqInc());
                }
                if (credentials.getC3p0MaxIdle() != null)
                {
                   settings.put(CP_MAX_IDLE_TIME, credentials.getC3p0MaxIdle());
                }
                if (credentials.getC3p0MaxStmts() != null)
                {
                   settings.put(CP_MAX_STATEMENTS_PER_CONNECTION, credentials.getC3p0MaxStmts());
                }
             }
             catch (PersistenceException e)
             {
                log.log(Level.WARNING,
                        "Could not get the configuration for the default database " +
                        mainDb.getName() + " due to " + e.getMessage());
             }
          }
          catch (SQLException e)
          {
             log.log(Level.WARNING,
                     "Could not initialize the settings for database " +
                     db.getName() + " due to " + e.getMessage());
          }

          String schema = mainDb == null ? null : schemaByDatabase.get(mainDb);
          DatabaseConfig dbConfig = new DatabaseConfig(settings, schema);
          tenantDbConfigs.putIfAbsent(tenantName, new HashMap<>());
          tenantDbConfigs.get(tenantName).put(pdbName, dbConfig);
          return dbConfig;
       }

       /**
        * Remove all the database configurations of a certain tenant.
        *
        * @param   tenantName
        *          The tenant for which to remove the configurations.
        */
       public static void removeTenantDbConfigs(String tenantName)
       {
          tenantDbConfigs.remove(tenantName);
       }

       /**
        * Remove the stored database configuration of the given tenant. This operation can have two roots.
        * Either the database instance has been unassigned from the tenant and a database configuration is
        * no longer required, or the tenant has been updated, which means that this stored configuration is
        * out-dated.
        *
        * @param   tenantName
        *          The tenant id for which to remove the database configuration.
        * @param   pdbName
        *          The physical name of the database for which to remove the configuration.
        */
       public static void removeTenantDbConfig(String tenantName, String pdbName)
       {
          Map<String, DatabaseConfig> configs = tenantDbConfigs.get(tenantName);
          if (configs == null)
          {
             return;
          }

          configs.remove(pdbName);
       }

       /**
        * Remove the given tenant database from the map of {@code LockTableUpdater} instances.
        *
        * @param   db
        *          The database for which to remove the {@code LockTableUpdater}.
        */
       public static void removeTenantDbFromLockTable(Database db)
       {
          lockTableUpdaters.remove(db);
       }

       /**
        * Create, register and activate a given database for a given tenant.
        *
        * @param   tenant
        *          The tenant for which to perform these operations.
        * @param   pdbName
        *          The physical name of the database for which to perform these operations.
        *
        * @return  {@code null} if the operation was successful, an error message otherwise.
        */
       public static String activateTenantDatabase(Tenant tenant, String pdbName)
       throws PersistenceException
       {
          TenantDatabaseDescriptor dbDescriptor = tenant.getDatabaseCredentials(pdbName, true);
          Database db = new Database(dbDescriptor.getLdbName(), Database.Type.PRIMARY, true)
                .createTenant(dbDescriptor.getPdbName());

          DatabaseConfig cfg = createSettings(tenant, db, dbDescriptor);
          if (cfg == null)
          {
             return "Failed to create database configuration for physical database '" + pdbName + "'.";
          }
          else if (cfg.getSchema() == null)
          {
             return "Unknown schema for physical database '" + pdbName + "'.";
          }

          Integer sessionId = SecurityManager.getInstance().sessionSm.getSessionId();
          Database metaDb = initDb(cfg, db);
          activateAndRegister(sessionId, db, () -> activateDb(cfg, db));
          if (metaDb != null)
          {
             activateAndRegister(sessionId, metaDb, () -> activateMetaDb(db, metaDb));
          }

          dbDescriptor.setDatabase(db);
          return null; // this means OK
       }

       /**
        * Create, register and activate the databases for the given tenant.
        *
        * @param   tenant
        *          The tenant to work with.
        *
        * @return  {@code true} if all database configurations have been created successfully,
        *          {@code false} if at least one configuration failed.
        */
       public static boolean activateTenantDatabases(Tenant tenant)
       throws PersistenceException
       {
          // create the database instances and register them
          boolean success = true;
          Integer sessionId = SecurityManager.getInstance().sessionSm.getSessionId();
          for (Map.Entry<String, TenantDatabaseDescriptor> entry : tenant.getDatabaseCredentials().entrySet())
          {
             String pdbName = entry.getKey();
             TenantDatabaseDescriptor tenantDescriptor = entry.getValue();
             Database defaultDb = new Database(tenantDescriptor.getLdbName(), Type.PRIMARY, true);
             Database tenantDb = defaultDb.createTenant(pdbName);
             DatabaseConfig cfg = createSettings(tenant, tenantDb, tenantDescriptor);
             if (cfg == null)
             {
                // Creating the configuration fails if the given information (e.g. database URL) is not valid
                success = false;
                continue;
             }

             Database metaDb = initDb(cfg, tenantDb);
             activateAndRegister(sessionId, tenantDb, () -> activateDb(cfg, tenantDb));
             if (metaDb != null)
             {
                activateAndRegister(sessionId, metaDb, () -> activateMetaDb(tenantDb, metaDb));
             }

             tenantDescriptor.setDatabase(tenantDb);
          }

          return success;
       }

       /**
        * Helper method to determine whether the specified class or interface
        * represents the DMO front-end for temp table records.
        *
        * @param   dmoClass
        *          A DMO interface or implementation class.
        *
        * @return  <code>true</code> if <code>dmoClass</code> is assignable from
        *          the {@link Temporary} marker interface;  else
        *          <code>false</code>.
        */
       static boolean isTempTable(Class<?> dmoClass)
       {
          return Temporary.class.isAssignableFrom(dmoClass);
       }

       /**
        * Check if the specified database is a permanent database. This is <code>true</code> only if
        * the database is neither a dirty database nor the temporary database.
        *
        * TODO: what about meta?
        *
        * @param    database
        *           The database to be checked.
        *
        * @return   <code>true</code> if this database object is configured for a permanent DB.
        */
       static boolean isPermanentDB(Database database)
       {
          return !database.isDirty() && !database.isTemporary();
       }

       /**
        * Initialize the persistence framework. This entails configuring and
        * storing a Hibernate session factory for each database to be supported.
        * This may be done immediately (for databases flagged as "auto-connect"),
        * or may be deferred for databases which are dynamically connected and
        * disconnected at runtime. This information will be retrieved from the
        * P2J directory.
        * <p>
        * Remote database service provider exports are registered at this time as
        * well.  This includes services to fetch database configurations for
        * remote requesters, record locking services, and the tracking of dirty
        * records within uncommitted transactions.
        * <p>
        * Databases and the DMO classes associated with them are determined by
        * reading the DMO index configuration file. The location of this file is
        * read from the P2J directory. Hibernate configuration properties for
        * each session factory are read from the directory as well.
        * <p>
        * The logic flow is:
        * <ol>
        *   <li>read from the directory the location of the DMO index document;
        *   <li>parse the DMO index document and compile a list of fully
        *       qualified class names for each schema node in the document;
        *       store these in a map, keyed by schema name.
        *   <li>read from the directory the list of physical database names of
        *       those databases supported by the current server;
        *   <li>for each database:
        *   <ul>
        *     <li>get the name of its schema, which should match one of the
        *         names mapped to a DMOIndex instance;
        *     <li>determine whether it must be auto-connected.
        *   </ul>
        *   <li>if a database is to be auto-connected:
        *   <ul>
        *     <li>create a Hibernate <code>Configuration</code>;
        *     <li>retrieve all Hibernate properties for that database from the
        *         directory and add them to the configuration;
        *     <li>add to the configuration each DMO class which corresponds to
        *         that database's schema name, as read from the DMO index;
        *     <li>build a Hibernate <code>SessionFactory</code> from the
        *         configuration and store it in a map, keyed by the database
        *         name (not the schema name, which may be different)
        *     <li>add the database name to the set of permanent databases which
        *         have been configured.
        *   </ul>
        *   <li>if a database is <i>not</i> to be auto-connected:
        *   <ul>
        *     <li>defer session factory configuration until such time as a
        *         dynamic configuration request is processed (i.e., do nothing
        *         at this time).
        *   </ul>
        * </ol>
        *
        * @throws  IllegalStateException
        *          if invoked more than once.
        * @throws  PersistenceException
        *          if any error is encountered initializing the persistence
        *          framework.
        */
       static void initialize()
       throws PersistenceException
       {
          synchronized (configLock)
          {
             try
             {
                if (initialized)
                {
                   throw new IllegalStateException("Persistence is already initialized");
                }

                DirectoryService ds = DirectoryService.getInstance();
                if (!ds.bind())
                {
                   throw new RuntimeException("Directory bind failed");
                }

                try
                {
                   String dbID = Utils.findDirectoryNodePath(ds, "database", "container", false);

                   // read the names of all locally managed databases for which this server is authoritative
                   String[] dbNodes = (dbID == null ? null : ds.enumerateNodes(dbID));

                   if (log.isLoggable(Level.INFO))
                   {
                      log.log(Level.INFO, "Using H2 database version " + H2Helper.getH2Version());
                   }

                   useMeta = MetadataManager.initialize();

                   if (useMeta)
                   {
                      // determine whether we need to create LockTableUpdaters
                      try
                      {
                         // if the MetaLock interface is found, we will need to register listeners
                         // with our lock managers
                         String metaIface = String.format("%s._meta.MetaLock",
                                                          DmoMetadataManager.getDmoBasePackage());
                         Class.forName(metaIface);
                         lockTableUpdaters = new HashMap<>();
                      }
                      catch (ClassNotFoundException exc)
                      {
                         // this is OK, just means the current application does not need to update
                         // the _lock metadata table with lock change information
                      }
                   }

                   // initialize persistent databases managed by this server
                   if (dbNodes != null)
                   {
                      for (String nodeID : dbNodes)
                      {
                         String pdb = nodeID.replace('.', File.separatorChar);
                         if (TEMP_TABLE_SCHEMA.equals(pdb))
                         {
                            // it is possible for there to be a _temp database entry in configuration, but
                            // the _temp database is always initialized below, so we skip it here
                            continue;
                         }

                         initDb(pdb, ConnectionManager.normalizeFilename(pdb).toLowerCase());
                      }
                   }

                   // the temp-table database is initialized unconditionally
                   initTempDb();
                }
                finally
                {
                   // Done with directory.
                   ds.unbind();
                }
             }
             finally
             {
                initialized = true;
             }
          }

          // Initialize remote database service provider exports.

          // Database configuration fetcher.
          DatabaseConfigFetcher dcf = new DatabaseConfigFetcherImpl();
          RemoteObject.registerNetworkServer(DatabaseConfigFetcher.class, dcf);

          // Lock manager multiplexer.
          LockManagerMultiplexerImpl.initialize();

          // Dirty share multiplexer.
          DirtyShareMultiplexerImpl.initialize();

          // Identity manager multiplexer.
          IdentityManagerMultiplexerImpl.initialize();

          // Initialize the rest of the values from directory configuration
          DatabaseManager.bootstrap();
       }

       /**
        * Method called at server bootstrap that initializes values from the directory
        * configuration. Until this method is called, default values are used.
        */
       private static void bootstrap()
       {
          DirectoryService ds = DirectoryService.getInstance();
          if (ds != null)
          {
             if (!ds.bind())
             {
                throw new RuntimeException("Directory bind failed");
             }

             try
             {
                // read optional, temp-table NO-UNDO override value from directory; disabled by
                // default (that is, by default temp-tables can be undoable)
                String path = "persistence/force-no-undo-temp-tables";
                forceNoUndoTempTables = Utils.getDirectoryNodeBoolean(ds, path, false, false);
             }
             finally
             {
                ds.unbind();
             }
          }
       }

       /**
        * Deactivate session auto-connected database.
        *
        * @param   db
        *          Database to be deactivated.
        * @param   deactivateIfNotUsedSec
        *          Idle period threshold for deactivation in sec.
        */
       private static void deactivate(Database db, long deactivateIfNotUsedSec)
       {
          AutoConnected connected = dbSessions.get(db);
          if (connected == null)
          {
             return;
          }
          connected.writeLock().lock();
          try
          {
             if (!connected.isActivated()) // already deactivated
             {
                return;
             }
             Date notActiveSince = connected.notActiveSince();
             if (notActiveSince == null) // was re-activated
             {
                return;
             }
             long notActiveSec = TimeUnit.MILLISECONDS.toSeconds(
                      System.currentTimeMillis() - notActiveSince.getTime());
             if (notActiveSec < deactivateIfNotUsedSec)
             {
                scheduler.schedule(
                         () -> deactivate(db, deactivateIfNotUsedSec),
                         deactivateIfNotUsedSec - notActiveSec,
                         TimeUnit.SECONDS);
                return;
             }

             if (log.isLoggable(Level.FINE))
             {
                log.fine("Deactivating " + db.getName());
             }
             if (!useMeta)
             {
                return;
             }

             Database metaDB = new Database(db.getName(), Database.Type.META, true);
             try (Session session = new Session(metaDB))
             {
                Connection conn = session.getConnection();
                PreparedStatement pstmt = conn.prepareStatement("DROP ALL OBJECTS");
                pstmt.executeUpdate();
                pstmt.close();
                connected.deactivated();
                AutoConnected mconnected = dbSessions.get(metaDB);
                if (mconnected != null)
                {
                   mconnected.deactivated();
                }
             }
             catch (Exception e)
             {
                log.log(Level.WARNING, "Failed to cleanup " + metaDB, e);
             }
             MetadataManager.reset(db, metaDB);
          }
          finally
          {
             connected.writeLock().unlock();
          }

       }

       /**
        * Initialize local database.
        *
        * @param   pdb
        *          The physical name of the database.
        * @param   ldb
        *          The logical name of the database.
        *
        * @throws  PersistenceException
        *          on initialization failure.
        */
       private static void initDb(String pdb, String ldb)
       throws PersistenceException
       {
          Database primaryDB = new Database(pdb, Database.Type.PRIMARY, true);
          DatabaseConfig cfg = getLocalConfiguration(primaryDB);
          String schema = cfg.getSchema();

          // Remember the database to schema name mapping.
          schemaByDatabase.put(primaryDB, schema);

          if (useMeta)
          {
             // create and prepare the physical meta database
             Database metaDB = new Database(pdb, Database.Type.META, true);
             schemaByDatabase.put(metaDB, MetadataManager.META_SCHEMA);
             registerDb(metaDB);

             if (lockTableUpdaters != null)
             {
                LockTableUpdater ltu = new LockTableUpdater(primaryDB, metaDB, cfg);
                lockTableUpdaters.put(primaryDB, ltu);
             }
          }

          registerDb(primaryDB);
          if (TenantConfig.isEnabled())
          {
             Settings dbSettings = cfg.getOrmSettings();
             TenantManager.addDatabaseToDefaultTenant(
                   new TenantDatabaseDescriptor(pdb, ldb,
                         dbSettings.getString(Settings.URL, null),
                         dbSettings.getString(Settings.USER, null),
                         dbSettings.getString(Settings.PASSWORD, null),
                         dbSettings.getInteger(DatabaseManager.CP_MAX_STATEMENTS_PER_CONNECTION, null),
                         dbSettings.getInteger(DatabaseManager.CP_MIN_POOL_SIZE, null),
                         dbSettings.getInteger(DatabaseManager.CP_MAX_POOL_SIZE, null),
                         dbSettings.getInteger(DatabaseManager.CP_ACQUIRE_INCREMENT, null),
                         dbSettings.getInteger(DatabaseManager.CP_MAX_IDLE_TIME, null),
                         primaryDB));
          }
       }

       /**
        * Initialize local database based on its configuration.
        *
        * @param   cfg
        *          The database configuration.
        * @param   primaryDB
        *          The database to be initialized.
        *
        * @return  The meta database instance that was created.
        */
       private static Database initDb(DatabaseConfig cfg, Database primaryDB)
       throws PersistenceException
       {
          Database metaDB = null;
          String schema = cfg.getSchema();

          // Remember the database to schema name mapping.
          schemaByDatabase.put(primaryDB, schema);

          if (useMeta)
          {
             // create and prepare the physical meta database
             metaDB = primaryDB.toType(Type.META);
             schemaByDatabase.put(metaDB, MetadataManager.META_SCHEMA);
             registerDb(metaDB, getLocalConfiguration(metaDB));

             if (lockTableUpdaters != null)
             {
                lockTableUpdaters.put(primaryDB, new LockTableUpdater(primaryDB, metaDB, cfg));
             }
          }
          registerDb(primaryDB, cfg);
          return metaDB;
       }

       /**
        * Activate meta database.
        *
        * @param   primaryDB
        *          Primary database.
        * @param   metaDB
        *          Meta database.
        *
        * @throws  PersistenceException
        *          on initialization failure.
        */
       private static void activateMetaDb(Database primaryDB, Database metaDB)
       throws PersistenceException
       {
          if (!useMeta)
          {
             return;
          }
          initMetaDb(primaryDB, metaDB);

          // initialize the meta persistence's lock table updater to be that of the primary database
          if (lockTableUpdaters != null)
          {
             Persistence mPrim = PersistenceFactory.getInstance(metaDB);
             mPrim.shareLockTableUpdater(primaryDB);
          }
       }

       /**
        * Activate database - register the database with the persistence framework. For the
        * primary database register its dirty database as well.
        *
        * @param   cfg
        *          Database configuration.
        * @param   database
        *          Database to be activated.
        *
        * @throws  PersistenceException
        *          on initialization failure.
        */
       private static void activateDb(DatabaseConfig cfg, Database database)
       throws PersistenceException
       {
          registerDatabase(cfg, database, false, false);

          // preload Persistence object for database
          PersistenceFactory.getInstance(database, cfg);

          if (database.isPrimary())
          {
             registerDirtyDatabase(cfg, database, false);
          }
       }

       /**
        * Activate local primary database. Register the database and its meta database
        * with the persistence framework
        *
        * @param   pdb
        *          Database name.
        *
        * @throws  PersistenceException
        *          on initialization failure.
        */
       public static void activate(String pdb)
       throws PersistenceException
       {
          Database primaryDB = new Database(pdb, Database.Type.PRIMARY, true);
          if (!managed.contains(primaryDB))
          {
             throw new PersistenceException("Unknown local database: [" + pdb + "]");
          }
          Integer sessionId = SecurityManager.getInstance().sessionSm.getSessionId();
          DatabaseConfig cfg = getLocalConfiguration(primaryDB);

          activateAndRegister(sessionId, primaryDB, () -> activateDb(cfg, primaryDB));

          if (useMeta)
          {
             Database metaDB = new Database(pdb, Database.Type.META, true);
             activateAndRegister(sessionId, metaDB, () -> activateMetaDb(primaryDB, metaDB));
          }
       }

       /**
        * Activate and register the database for the user session.
        *
        * @param   sessionId
        *          The user session Id.
        * @param   db
        *          Database to be activated and registered.
        * @param   activator
        *          Database {@link Activator}
        *
        * @throws  PersistenceException
        *          on activation failure.
        */
       private static void activateAndRegister(Integer sessionId, Database db, Activator activator)
       throws PersistenceException
       {
          AutoConnected connected = dbSessions.computeIfAbsent(db, AutoConnected::new);
          connected.writeLock().lock();
          try
          {
             if (!connected.isActivated())
             {
                activator.activate();
             }
             connected.add(sessionId);
          }
          finally
          {
             connected.writeLock().unlock();
          }
          context.get().autoConnected.add(db);
       }

       /**
        * Initialize meta database. The {@code _File}, {@code _Field}, {@code _Index}, {@code _Index_Field} tables
        * are populated with information extracted from DMO annotations of the primary database.
        *
        * @param   primaryDB
        *          Primary database.
        * @param   metaDB
        *          Meta database.
        *
        * @throws  PersistenceException
        *          on initialization failure.
        */
       private static void initMetaDb(Database primaryDB, Database metaDB)
       throws PersistenceException
       {
          if (metaDB.isTenant())
          {
             H2Helper.prepareDatabase(metaDB);
          }

          MetadataManager.prepareDatabase(metaDB);
          MetadataManager.setupIdentityManager(metaDB, dialects.get(metaDB));

          // initialize infrastructure for _trans metadata
          TransactionManager.initializeMeta(primaryDB);

          if (!metaDB.isTenant()) // the tenants share the same meta database with their default database
          {
             // initialize it later, after its [persistence.tenant] is updated to new database
             MetadataManager.populateDatabase(metaDB);
          }
       }

       /**
        * Register the database.
        *
        * @param   database
        *          The database to be registered.
        */
       private static void registerDb(Database database)
       throws PersistenceException
       {
          registerDb(database, getLocalConfiguration(database));
       }

       /**
        * Register database.
        *
        * @param   database
        *          Database to be registered.
        * @param   cfg
        *          The configuration of the given database.
        *
        * @throws  PersistenceException
        *          on initialization failure.
        */
       private static void registerDb(Database database, DatabaseConfig cfg)
       throws PersistenceException
       {
          boolean added = managed.add(database);
          if (!added)
          {
             return; // if the database was already processed, skip the rest of the method
          }

          // remember dialect for this database
          Settings settings = cfg.getOrmSettings();
          Dialect dialect = Dialect.getDialect(settings);
          dialects.put(database, dialect);

          // TODO: does this belong elsewhere?
          if (isMutable(database))
          {
             try
             {
                SchemaCheck.run(database, true);
             }
             catch (Exception exc)
             {
                if (log.isLoggable(Level.SEVERE))
                {
                   log.log(Level.SEVERE, exc.getMessage(), exc);
                }
             }
          }
       }

       /**
        * Initialize _temp database. This should ONLY be called from {@link #initialize()}.
        *
        * @throws  PersistenceException
        *          on initialization failure.
        */
       private static void initTempDb()
       throws PersistenceException
       {
          Database tempDB = new Database(TEMP_TABLE_SCHEMA, Database.Type.PRIMARY, true);
          DatabaseConfig cfg = getLocalConfiguration(tempDB);

          // remember the database to schema name mapping
          schemaByDatabase.put(tempDB, TEMP_TABLE_SCHEMA);

          // register database
          registerDatabase(cfg, tempDB, true, false);

          // preload Persistence object for database
          PersistenceFactory.getInstance(tempDB);
       }

       /**
        * Indicate whether the database manager is currently initializing.
        *
        * @return  {@code true} if database manager is initializing, else {@code false} if
        *          initialization is complete.
        */
       public static boolean isInitializing()
       {
          return !initialized;
       }

       /**
        * Determine whether a database's schema is programmatically mutable, which is set by configuration.
        *
        * @param   database
        *          Database identifier.
        *
        * @return  {@code true} if the schema is mutable, else {@code false}.
        */
       static boolean isMutable(Database database)
       throws PersistenceException
       {
          DatabaseConfig cfg = getDatabaseConfig(database);

          return cfg.isMutable();
       }

       /**
        * Register an individual database with the persistence framework. This configures the database to allow
        * the framework to access it.
        *
        * @param   database
        *          Database to be configured.
        *
        * @return  {@code true} if the database was newly registered; {@code false} if the database already was
        *          registered, and only the reference count was incremented, or if this registration attempt was
        *          redundant in the local context.
        *
        * @throws  PersistenceException
        *          if {@code database} is not recognized, based upon configuration information in the directory.
        */
       static boolean registerDatabase(Database database)
       throws PersistenceException
       {
          DatabaseConfig cfg = getDatabaseConfig(database);

          return registerDatabase(cfg, database, false, true);
       }

       /**
        * Given a normal database, this method will register its associated dirty database with the
        * persistence framework. This configures the database to allow the framework to access it.
        * The created dirty database will have the same name as the original database.
        *
        * @param   database
        *          Database to be configured.
        *
        * @return  {@code true} if the database was newly registered; {@code false} if the database already was
        *          registered, and only the reference count was incremented, or if this registration attempt was
        *          redundant in the local context.
        *
        * @throws  PersistenceException
        *          if {@code database} is not recognized, based upon configuration information in the directory.
        */
       static boolean registerDirtyDatabase(Database database)
       throws PersistenceException
       {
          DatabaseConfig cfg = (database.isLocal()
                                ? getLocalConfiguration(database)
                                : getRemoteConfiguration(database));

          return registerDirtyDatabase(cfg, database, false);
       }

       /**
        * Return the lock table updater associated with the given database, if any.
        *
        * @param   database
        *          A primary database for which the lock table updater is needed.
        *
        * @return  Lock table updater, or <code>null</code> if lock metadata is not used by the
        *          current application.
        *
        * @throws  IllegalArgumentException
        *          if lock metadata is used by the current application, but no lock table updater is
        *          found for the given database.
        */
       public static LockTableUpdater getLockTableUpdater(Database database)
       {
          if (lockTableUpdaters == null)
          {
             return null;
          }
          if (database.isMeta())
          {
             database = database.toType(Type.PRIMARY);
          }
          LockTableUpdater ltu = lockTableUpdaters.get(database);
          if (ltu == null)
          {
             throw new IllegalArgumentException("No lock table updater found for database " + database);
          }

          return ltu;
       }

       /**
        * Get the local or remote configuration for the given database.
        *
        * @param   database
        *          The database instance.
        *
        * @return  See above.
        */
       static DatabaseConfig getDatabaseConfig(Database database)
       throws PersistenceException
       {
          return database.isLocal() ? getLocalConfiguration(database) : getRemoteConfiguration(database);
       }

       /**
        * Get all data necessary to configure a database managed by the current
        * P2J server instance.  This information is read from the directory the
        * first time it is needed and then cached for future accesses.
        *
        * @param   database
        *          A locally managed database.
        *
        * @return  A configuration object which provides all information needed to
        *          connect to a physical database.
        *
        * @throws  PersistenceException
        *          if an error occurs accessing configuration data.
        */
       private static DatabaseConfig getLocalConfiguration(Database database)
       throws PersistenceException
       {
          DatabaseConfig cfg = dbConfigs.get(database);
          if (cfg == null)
          {
             cfg = readDatabaseConfig(database, null);
             dbConfigs.put(database, cfg);
          }

          return cfg;
       }

       /**
        * Get all data necessary to configure a database managed by a remote P2J server instance.
        *
        * @param   database
        *          A remotely managed database.
        *
        * @return  A configuration object which provides all information needed to connect to a
        *          physical database.
        *
        * @throws  PersistenceException
        *          if the specified database is not managed by the remote server.
        */
       private static DatabaseConfig getRemoteConfiguration(Database database)
       throws PersistenceException
       {
          // Obtain database configuration fetcher proxy for local use.
          com.goldencode.p2j.net.Session session = ConnectionManager.getSession(database);
          DatabaseConfigFetcher dcf = (DatabaseConfigFetcher)
             RemoteObject.obtainNetworkInstance(DatabaseConfigFetcher.class, session);
          DatabaseConfig config = dcf.fetch(database);

          if (config == null)
          {
             throw new PersistenceException(
                "Remote database configuration for '" + database + "' was not found");
          }

          // Validate and possibly modify connection URL.
          Settings remoteSettings = config.getOrmSettings();
          checkRemoteConnectURL(database, remoteSettings);
          return config;
       }

       /**
        * Validate and possibly modify the remote connection URL. Validation consists of confirming
        * the existence of the appropriate property in the {@code settings} object and confirming it
        * begins with the necessary {@code jdbc:} prefix.
        * <p>
        * The URL setting is modified in the event it specifies a localhost/loopback address. Such an
        * address specification is relative to the remote server and is most likely invalid for the
        * requesting server. The host in such a case is replaced the host name specified in {@code
        * database}, and the modified URL is set back into <code>settings</code> under the appropriate
        * key.
        * <p>
        * NOTE: this approach assumes the local and remote servers share a DNS which will resolve the
        * given connection URL to the same physical address.
        *
        * @param   database
        *          Object containing address information for the remotely managed database.
        * @param   settings
        *          ORM configuration as reported by the remote server. This is a local copy of the
        *          remote settings; it is modified by this method.
        *
        * @throws  PersistenceException
        *          if the connection URL specified in {@code settings} is missing or invalid.
        */
       private static void checkRemoteConnectURL(Database database, Settings settings)
       throws PersistenceException
       {
          String url = settings.getString(Settings.URL, null);
          if (url == null)
          {
             throw new PersistenceException(
                "Remote database configuration does not specify connection URL");
          }

          if (!url.startsWith("jdbc:"))
          {
             throw new PersistenceException("Invalid database connection URL [" + url + "]");
          }

          Dialect dialect = Dialect.getDialect(settings);
          url = dialect.buildRemoteURL(url, database);

          // update URL in properties
          settings.put(Settings.URL, url);
       }

       /**
        * Read from the directory all data necessary to configure a database
        * managed by the current P2J server instance.
        *
        * @param   database
        *          A locally managed database.
        * @param   config
        *          The database configuration, if one already exists.
        *
        * @return  A configuration object which provides all information needed to
        *          connect to a physical database.
        *
        * @throws  PersistenceException
        *          if an error occurs accessing configuration data.
        */
       private static DatabaseConfig readDatabaseConfig(Database database, DatabaseConfig config)
       throws PersistenceException
       {
          Settings settings = null;
          String schema = null;
          long deactivateIfNotUsedSec = -1;
          boolean meta = database.isMeta();
          Boolean useJavaUDFsConfigured = null;
          Boolean mutable = null;

          if (meta || database.isDirty())
          {
             settings = createEmbeddedSettings(database);
             schema = meta ? MetadataManager.META_SCHEMA
                           : database.isTenant() ? database.getDefault().getName() : database.getName();
             if (meta)
             {
                // copy the connection pool size settings from the primary configuration, which will already
                // have been loaded
                DatabaseConfig primaryCfg = config;
                if (primaryCfg == null)
                {
                   primaryCfg = getConfiguration(database.toType(Type.PRIMARY));
                }

                if (primaryCfg != null)
                {
                   Settings primarySettings = primaryCfg.getOrmSettings();
                   settings.put(CP_INIT_POOL_SIZE, primarySettings.getInteger(CP_INIT_POOL_SIZE, 3));
                   settings.put(CP_MIN_POOL_SIZE, primarySettings.getInteger(CP_MIN_POOL_SIZE, 3));
                   settings.put(CP_MAX_POOL_SIZE, primarySettings.getInteger(CP_MAX_POOL_SIZE, 30));
                   settings.put(CP_ACQUIRE_INCREMENT, primarySettings.getInteger(CP_ACQUIRE_INCREMENT, 3));
                   settings.put(CP_BREAK_AFTER_ACQUIRE_FAILURE,
                         primarySettings.getBoolean(CP_BREAK_AFTER_ACQUIRE_FAILURE, true));
                }
             }
          }
          else
          {
             DirectoryService ds = DirectoryService.getInstance();
             if (!ds.bind())
             {
                throw new PersistenceException("Directory bind failed");
             }

             // get schema name used by this database, which may not be same as physical database name
             String nodeID = Utils.findDirectoryNodePath(ds, "database", "container", false);
             if (nodeID != null)
             {
                String dbId = database.isTenant() ? database.getDefault().getId() : database.getId();
                schema = ds.getNodeString(nodeID + "/" + dbId + "/p2j/schema", "value");

                // TODO: temporary solution for backward compatibility. Should be removed.
                if (!database.isTemporary() &&
                    Boolean.TRUE.equals(
                       ds.getNodeBoolean(nodeID + "/" + dbId + "/p2j/load_at_startup", "value")))
                {
                   if (!loadedOnStartupDBs.contains(dbId))
                   {
                      loadedOnStartupDBs.add(dbId);
                   }
                }

                mutable = ds.getNodeBoolean(nodeID + "/" + dbId + "/p2j/mutable", "value");

                Integer deactivationPeriod = ds.getNodeInteger(
                      nodeID + "/" + dbId + "/p2j/deactivate_if_not_used_sec", "value");
                if (deactivationPeriod != null)
                {
                   deactivateIfNotUsedSec = deactivationPeriod;
                }

                useJavaUDFsConfigured = ds.getNodeBoolean(nodeID + "/" + dbId + "/p2j/use_java_udfs", "value");
             }

             ds.unbind();

             // If schema name is not explicitly mapped in the directory, assume
             // it is the same as the physical database name.
             if (schema == null)
             {
                schema = database.getName();
             }

             settings = loadSettings(database.isTenant() ? database.getDefault() : database);
          }

          Dialect dialect = Dialect.getDialect(settings);
          boolean useJavaUDFs = dialect == null || !dialect.isNativeUDFsSupported();
          if (useJavaUDFsConfigured != null)
          {
             if (!useJavaUDFsConfigured && useJavaUDFs)
             {
                log.warning(String.format(
                            "Using native UDFs is configured for the %s database, " +
                            "but this is not supported for the %s dialect, ignored",
                            database.getName(),
                            dialect == null ? "[unknown]" : dialect.getClass().getSimpleName()));
             }
             else
             {
                useJavaUDFs = useJavaUDFsConfigured;
             }
          }
          if (mutable == null)
          {
             mutable = false;
          }

          // instantiate session hook
          String hookClassName = settings.getString("session_hook", null);
          SessionHook hook = null;
          if (hookClassName != null)
          {
             try
             {
                Class<?> clazz = Class.forName(hookClassName);
                Object o = clazz.getDeclaredConstructor().newInstance();
                hook = (SessionHook) o;
             }
             catch (ClassNotFoundException |
                    NoSuchMethodException |
                    IllegalAccessException |
                    InvocationTargetException |
                    InstantiationException e)
             {
                throw new RuntimeException(e);
             }
          }

          return new DatabaseConfig(settings, schema, deactivateIfNotUsedSec, useJavaUDFs, mutable, hook);
       }

       /**
        * Deregister the specified, transient database, if and only if all other
        * contexts using the database have deregistered it. After deregistration,
        * sessions can no longer be opened for <code>database</code>, without
        * first building a new session factory (an expensive process).
        *
        * @param   database
        *          Database to be deregistered.
        *
        * @return  <code>true</code> if <code>database</code> was deregistered;
        *          else <code>false</code>. Note that the latter does not
        *          represent an error condition, but rather indicates that the
        *          database could not be deregistered because other contexts
        *          still referenced it, or because it is a permanent database.
        *
        * @throws  IllegalArgumentException
        *          if <code>database</code> is not currently registered.
        */
       static boolean deregisterDatabase(Database database)
       {
          synchronized (configLock)
          {
             if (!dbSessions.containsKey(database) &&!transientDatabases.containsKey(database))
             {
                throw new IllegalArgumentException("Database '" + database + "' not registered");
             }
          }

          boolean result = false;

          DirtyShareFactory.unregisterContextInstance(database);

          // If no one is using the database any longer, clean up most resources associated with it.
          if (updateTransientRefCount(database, false) == 0)
          {
             // clean up local context before closing session factory
             PersistenceFactory.getInstance(database).cleanup();
             // Unregister dirty share manager for this database (and close the session held by it).
             DirtyShareFactory.unregisterManagerForDatabase(database);
          }

          return result;
       }

       /**
        * Indicate whether this P2J server instance is authoritative for the given
        * database.  Remote P2J servers must connect to the authoritative server
        * instance for a given database, in order to access that database and its
        * supporting resources, such as a lock manager, dirty read manager, etc.
        *
        * @param   database
        *          Database information object.
        *
        * @return  <code>true</code> if this server instance is authoritative for
        *          the specified database.
        */
       static boolean isAuthoritative(Database database)
       {
          synchronized (configLock)
          {
             return managed.contains(database);
          }
       }

       /**
        * For a given DMO property which may represent an indexed text column, indicate whether that
        * column is set to ignore case.
        *
        * @param   dmoClass
        *          DMO implementation class.
        * @param   property
        *          DMO property name.
        *
        * @return  {@code true} to ignore case, {@code false} to preserve case sensitivity,
        *          {@code null} if the property does not represent an indexed text column.
        */
       static Boolean getIgnoreCase(Class<? extends Record> dmoClass, String property)
       {
          DmoMeta dmoMeta = DmoMetadataManager.getDmoInfo(dmoClass);
          return dmoMeta.isIndexedIgnoreCase(property);
       }

       /**
        * Given a normal database, this method will register its associated dirty database; the
        * created dirty database will have the same name as the original database.
        * <p>
        * The dirty database is hard-coded to be an in-memory H2 database. See
        * {@link #createEmbeddedSettings(Database)} method for the Hibernate settings used.
        *
        * @param   dbConfig
        *          Configuration properties for the given database.
        * @param   database
        *          Database to be configured.
        * @param   bootstrap
        *          <code>true</code> if this method is invoked during bootstrap
        *          initialization at server start-up;  <code>false</code> if
        *          invoked otherwise. If <code>false</code>, it is assumed that
        *          <code>database</code> represents a transient database.
        *
        * @return  <code>true</code> if the dirty database was newly registered;
        *          <code>false</code> if the database was already registered, and
        *          only the reference count was incremented (transient only), or
        *          if this registration attempt was redundant in the local
        *          context.
        *
        * @throws  PersistenceException
        *          If there is an error configuring the Hibernate framework.
        * @throws  IllegalStateException
        *          if <code>database</code> is permanent and already has been
        *          registered.
        * @throws  IllegalArgumentException
        *          if <code>database</code> is not recognized, based upon
        *          configuration information in the directory.
        */
       private static boolean registerDirtyDatabase(DatabaseConfig dbConfig, Database database, boolean bootstrap)
       throws PersistenceException
       {
          String schema = dbConfig.getSchema();
          Database dirtyDB = database.toType(Type.DIRTY);
          Settings dirtySettings = createEmbeddedSettings(dirtyDB);
          DatabaseConfig dirtyConfig = new DatabaseConfig(dirtySettings, schema);

          dialects.put(dirtyDB, new P2JH2Dialect());
          schemaByDatabase.put(dirtyDB, schema);

          boolean newReg = registerDatabase(dirtyConfig, dirtyDB, bootstrap, !bootstrap);
          if (!dirtyDBs.contains(dirtyDB))
          {
             H2Helper.prepareDatabase(dirtyDB);
             dirtyDBs.add(dirtyDB);
          }

          return newReg;
       }

       /**
        * Register an individual database with the persistence framework. This
        * configures the database to allow the framework to access it. This
        * method ensures that a permanent database is never registered more
        * than once. Registration involves the following:
        * <ul>
        *   <li>create a Hibernate <code>Configuration</code>;
        *   <li>retrieve all Hibernate properties for that database from the
        *       directory and add them to the configuration;
        *   <li>add to the configuration each DMO class which corresponds to
        *       that database's schema name, as read from the DMO index;
        *   <li>build a Hibernate <code>SessionFactory</code> from the
        *       configuration and store it in a map, keyed by the database
        *       name (not the schema name, which may be different)
        *   <li>if the database is transient (i.e., not configured immediately
        *       during bootstrap), update its configuration reference count;
        *       otherwise, add the database name to the set of permanent
        *       databases which have been configured.
        * </ul>
        * <p>
        * This method may be invoked during bootstrap initialization, to register
        * a permanent database, or after initialization, to dynamically register
        * a transient database.
        *
        * @param   dbConfig
        *          Configuration properties for the given database.
        * @param   database
        *          Database to be configured.
        * @param   bootstrap
        *          {@code true} if this method is invoked during bootstrap
        *          initialization at server start-up;  {@code false} if
        *          invoked otherwise. If {@code false}, it is assumed that
        *          {@code database} represents a transient database.
        * @param   updateRefCount
        *          {@code true} if the transient reference count should be updated;
        *          else {@code false}.
        *
        * @return  <code>true</code> if the (permanent or transient) database was
        *          newly registered;
        *          <code>false</code> if the database was already registered, and
        *          only the reference count was incremented (transient only), or
        *          if this registration attempt was redundant in the local
        *          context.
        *
        * @throws  PersistenceException
        *          If there is an error configuring the Hibernate framework.
        * @throws  IllegalStateException
        *          if <code>database</code> is permanent and already has been
        *          registered.
        */
       private static boolean registerDatabase(DatabaseConfig dbConfig,
                                               Database database,
                                               boolean bootstrap,
                                               boolean updateRefCount)
       throws PersistenceException
       {
          String schema = dbConfig.getSchema().intern();
          boolean isTemp = TEMP_TABLE_SCHEMA.equals(schema);
          boolean ret = false;

          synchronized (configLock)
          {
             // Determine global reference count for this database.
             Integer refCountObj = transientDatabases.get(database);
             int refCount = (refCountObj != null) ? refCountObj : 0;

             if (schemaByDatabase.get(database) != null)
             {
                if (bootstrap && !isTemp)
                {
                   log.log(Level.SEVERE,
                           "Auto-connect database " + database + " was already registered.");
                }

                if (updateRefCount)
                {
                   refCount = -1;
                }
             }

             // if transient database already has been configured, we don't need to reconfigure it,
             // but we do need to update the global reference count
             if (bootstrap || refCount == 0)
             {
                ret = true;
                SequenceManager.prepare(database, schema);

                boolean logTime = bootstrap && database.isPrimary() && log.isLoggable(Level.INFO);
                long startTime = logTime ? System.currentTimeMillis() : 0L;

                try
                {
                   // TODO: disabling this for now, as this information is now retrieved from DMO annotations;
                   // however, keeping this code around to eventually be used to compare index metadata in the
                   // database with that in DMO annotations and to report discrepancies
                   /*
                   if (database.isPrimary() && !isTemp)
                   {
                      // faster pre-load of all index data for this database
                      IndexMetadataCollector.queryAllIndexData(database, schema);
                   }
                   */

                   if (database.isMeta() /*|| (database.isPrimary() && !isTemp)*/)
                   {
                      MetadataManager.setupIdentityManager(database, dialects.get(database));
                   }

                   if (!database.isLocal())
                   {
                      //Remember dialect for this database.
                      Settings settings = dbConfig.getOrmSettings();
                      Dialect dialect = Dialect.getDialect(settings);
                      dialects.put(database, dialect);
                      schemaByDatabase.put(database, schema);
                   }

                }
                catch (PersistenceException exc)
                {
                   DBUtils.handleException(database, exc);

                   throw new PersistenceException(
                      "Error configuring persistence framework for database: " + database,
                      exc);
                }
                finally
                {
                   if (logTime)
                   {
                      long elapsed = System.currentTimeMillis() - startTime;
                      log.info("Database " + database + " initialized in " + elapsed + " ms.");
                   }
                }
             }

             // Increment reference count for a transient database, or add
             // database to configured, permanent databases if database is not transient.
             if (database.isPrimary())
             {
                if (updateRefCount)
                {
                   int newCount = updateTransientRefCount(database, true);

                   return (newCount > refCount);
                }
             }

             return ret;
          }
       }

       /**
        * Update the reference count of registrations of the specified database
        * across all contexts. The reference count for each database is global
        * across the system. Its value determines when a database configuration
        * may be safely dropped by the server.
        * <p>
        * The reference count is either incremented by 1, decremented by 1, or
        * left unchanged, based upon the following rules:
        * <ul>
        *   <li>if <code>increment</code> is <code>true</code>:
        *   <ul>
        *     <li>if <code>database</code> is already registered in the local
        *         context, no change is made;
        *     <li>otherwise, the count is incremented by 1.
        *   </ul>
        *   <li>if <code>increment</code> is <code>false</code>:
        *   <ul>
        *     <li>if <code>database</code> is <i>not</i> registered in the local
        *         context, no change is made;
        *     <li>otherwise, the count is decremented by 1.
        *   </ul>
        * </ul>
        * <p>
        * This logic ensures that attempts to register and deregister the same
        * database, if not properly bracketed, will not corrupt the global
        * reference count.
        *
        * @param   database
        *          A transient database.
        * @param   increment
        *          <code>true</code> to increment reference count by 1;
        *          <code>false</code> to decrement reference count by 1;
        *          See above for the implications of this.
        *
        * @return  A non-negative number indicating the reference count for
        *          <code>database</code> <i>after</i> the update; or -1 if
        *          <code>database</code> was not recognized.
        */
       private static int updateTransientRefCount(Database database, boolean increment)
       {
          // Amount to increment or decrement the reference count.
          int delta = (increment ? 1 : -1);

          // The context local set of registered databases must be managed, to
          // make sure that multiple register/deregister requests made within
          // the same context do not skew the global reference count.
          Context ctx = context.get();
          if (ctx.localTransients == null)
          {
             ctx.localTransients = new HashSet<>();
          }

          boolean locallyRegistered = ctx.localTransients.contains(database);
          if (increment)
          {
             // We should not increment the reference count if this database is
             // already registered in the local context.
             if (locallyRegistered)
             {
                delta = 0;
             }
             else
             {
                ctx.localTransients.add(database);
             }
          }
          else
          {
             // We should not decrement the reference count if this database is
             // not registered in the local context.
             if (!locallyRegistered)
             {
                delta = 0;
             }
             else
             {
                ctx.localTransients.remove(database);
                if (ctx.localTransients.isEmpty())
                {
                   ctx.localTransients = null;
                }
             }
          }

          return updateTransientRefCountWorker(database, delta);
       }

       /**
        * Update the reference count of registrations of the specified database. This method must
        * be called from a point where the <code>delta</code> have been correctly computed,
        * respectively <code>updateTransientRefCount()</code> where the current context is inspected
        * to see if incrementation is needed, or from context clean when all databases from context
        * should be de-ref counted (and <code>delta</code> is -1).
        *
        * @param   database
        *          A transient database.
        * @param   delta
        *          Specifies the direction of change (1 = increment, -1 decrement, 0 = no change);
        *          See updateTransientRefCount() for details.
        *
        * @return  A non-negative number indicating the reference count for
        *          <code>database</code> <i>after</i> the update; or -1 if
        *          <code>database</code> was not recognized.
        */
       private static int updateTransientRefCountWorker(Database database, int delta)
       {
          int result = -1;
          synchronized (configLock)
          {
             // Update the global reference count for this transient database.
             Integer refCount = transientDatabases.get(database);
             result = (refCount != null) ? Math.max(refCount + delta, 0) : delta;
             transientDatabases.put(database, result);
          }

          return result;
       }

       /**
        * Load configuration from the P2J directory service and return it as a {@code Settings}
        * object. The contents of this object must match the properties expected by the ORM
        * framework.
        *
        * @param   database
        *          Database for which properties are to be loaded. This differentiates the directory
        *          key by database name.
        *
        * @return  ORM settings for the given database.
        *
        * @throws  PersistenceException
        *          if no configuration properties for the specified database were found, or if the
        *          directory service is unavailable.
        */
       private static Settings loadSettings(Database database)
       throws PersistenceException
       {
          DirectoryService ds = DirectoryService.getInstance();
          if (!ds.bind())
          {
             throw new PersistenceException("Directory bind failed");
          }

          try
          {
             String nodeId = Utils.findDirectoryNodePath(ds, "database", "container", false);
             String baseID = (nodeId == null) ? null : (nodeId + "/" + database.getId() + "/orm");

             Settings settings = database.isTemporary() ? createDefaultTempSettings() : new Settings(database);

             // TODO:  handle fetching props from remote server if database is remote and directory lookup fails
             readOrmSettings(ds, baseID, "", settings);

             // set breakAfterAcquire flag for c3p0
             settings.put(CP_BREAK_AFTER_ACQUIRE_FAILURE, true);

             return settings;
          }
          finally
          {
             ds.unbind();
          }
       }

       /**
        * This method returns the Hibernate properties which should be used when connecting to a
        * secondary (e.g., dirty or metadata) database associated with a normal database. A random
        * username and password are generated. Hibernate second level caching is disabled.
        *
        * @param   database
        *          The database information object for the embedded database.
        *
        * @return  The created Hibernate properties.
        */
       private static Settings createEmbeddedSettings(Database database)
       {
          String dbName = database.getName();
          if (database.isMeta())
          {
             dbName = MetadataManager.META_SCHEMA + "_" + dbName;
          }

          Settings settings = new Settings(database);
          H2Helper.setCommonInMemoryProperties(dbName, settings, false, false);

          return settings;
       }

       /**
        * This method returns the ORM settings which should be used when connecting to the primary
        * temp-table database. A random username and password are generated.
        *
        * @return  The created ORM settings.
        */
       private static Settings createDefaultTempSettings()
       {
          Settings settings = new Settings(TEMP_TABLE_DB);
          H2Helper.setCommonInMemoryProperties(TEMP_TABLE_DB.getName(),
                                               settings,
                                               DatabaseManager.forceNoUndoTempTables,
                                               false);
          settings.put(Settings.FETCH_SIZE, "1024");

          return settings;
       }

       /**
        * Read ORM configuration properties from the directory and populate a properties file which
        * will be passed to the ORM subsystem to configure database access.
        * <p>
        * Properties are organized hierarchically in the directory. This method recursively walks the
        * tree and adds a new property entry when it reaches any directory entry terminus. Keys are
        * qualified with the given prefix, if any.
        * <p>
        * This method does not know or care about the names of individual properties; it simply
        * performs a depth-first enumeration of a directory subtree and composes the property names
        * from the directory entry names it encounters. Error checking of property names and values
        * is left to the ORM configuration subsystem.
        *
        * @param   ds
        *          Directory service instance.
        * @param   id
        *          Directory entry key for the node currently being visited.
        * @param   prefix
        *          Prefix to prepend to key (may be empty string, but not {@code null}.
        * @param   settings
        *          Object to which settings are added.
        */
       private static void readOrmSettings(DirectoryService ds,
                                           String id,
                                           String prefix,
                                           Settings settings)
       {
          String nodeClass = ds.getNodeClass(id);

          if (nodeClass == null)
          {
             if (log.isLoggable(Level.CONFIG))
             {
                log.config("No override configuration for directory id '" + id + "'");
             }

             return;
          }

          prefix = prefix.trim();

          if (nodeClass.endsWith("/container"))
          {
             // we are at a node containing nested nodes; enumerate and call ourselves recursively to
             // walk down to the next level
             String[] names = ds.enumerateNodes(id);

             if (names == null || names.length == 0)
             {
                throw new RuntimeException("Invalid directory entry:  " + id);
             }

             for (String name : names)
             {
                String qualifier = prefix.length() > 0 ? prefix + '.' : prefix;
                readOrmSettings(ds, id + "/" + name, qualifier, settings);
             }
          }
          else
          {
             String name = id.substring(id.indexOf("/orm/") + 1 + 4).replaceAll("/", ".");

             // if the node name starts with a dot use it literally (after removing the dot)
             if (name.contains(".."))
             {
                name = name.substring(name.indexOf("..") + 2);
             }

             // prepend with qualifier
             name = prefix + name;

             if (nodeClass.endsWith("/string"))
             {
                // we are at a string terminus; record the property
                String value = ds.getNodeString(id, "value");
                settings.put(name, value);
             }
             else if (nodeClass.endsWith("/strings"))
             {
                // we are at a strings terminus; record the properties
                if (!name.contentEquals("connection.driver_properties"))
                {
                   throw new RuntimeException("Directory strings entry for connection.driver_properties has an invalid name: " + name);
                }
                String[] values = ds.getNodeStrings(id, "values");
                for (int i = 0; i < values.length; i++)
                {
                   String[] kv = values[i].split(":", 2);
                   if (kv.length != 2)
                   {
                      throw new RuntimeException("Directory connection.driver_properties entry is missing a ':' delimiter: " + values[i]);
                   }
                   settings.put(name + "." + kv[0], kv[1]);
                }
             }
             else if (nodeClass.endsWith("/integer"))
             {
                // we are at an integer terminus; record the property
                Integer value = ds.getNodeInteger(id, "value");
                settings.put(name, value);
             }
             else if (nodeClass.endsWith("/boolean"))
             {
                // we are at a boolean terminus; record the property
                Boolean value = ds.getNodeBoolean(id, "value");
                settings.put(name, value);
             }
             else
             {
                throw new RuntimeException("Directory entry is incorrect type:  " + id);
             }
          }
       }

       /**
        * Interface for a remote object whose purpose is to fetch a local {@link DatabaseConfig}
        * object and deliver it to the remote caller. It is the interface used as a proxy for a
        * remote object.
        */
       public static interface DatabaseConfigFetcher
       {
          /**
           * Fetch and return a local database configuration, given the database identifier.
           *
           * @param   database
           *          Database information object.
           *
           * @return  Database configuration for the given database.
           */
          public DatabaseConfig fetch(Database database);
       }

       /**
        * Concrete implementation of the <code>DatabaseConfigFetcher</code> interface. Instances of
        * this class run as remote objects, serving requests for database configuration information
        * from other servers.
        */
       public static class DatabaseConfigFetcherImpl
       implements DatabaseConfigFetcher
       {
          /** Default constructor. */
          private DatabaseConfigFetcherImpl()
          {
          }

          /**
           * Fetch and return a local database configuration, given the database info object.
           *
           * @param   database
           *          Database information object.
           *
           * @return  Database configuration for the given database.
           */
          public DatabaseConfig fetch(Database database)
          {
             // since this is running on the remote server (not the requesting server), we have to
             // treat the database object as though it represents a local database;  we do this by
             // creating a new Database object without the host address information
             Database localDB = new Database(database.getName(), database.getType());

             return dbConfigs.get(localDB);
          }
       }

       /**
        * A class containing context-specific data.
        */
       private static class Context
       {
          /** Transient databases registered in the current context */
          Set<Database> localTransients = null;

          /** Auto-connected databases. */
          Set<Database> autoConnected = ConcurrentHashMap.newKeySet();

          /**
           * Deregister all transient databases registered for use in the current context, when that
           * context is closing. This method is invoked automatically when the context is being
           * cleaned up.
           */
          void cleanup()
          {
             if (localTransients != null)
             {
                for (Database database : localTransients)
                {
                   // if a database is contained by the value set, it was used in the context that
                   // is cleaned up so the counter must be decremented.
                   updateTransientRefCountWorker(database, -1);
                }
             }

             Integer sessionId = SecurityManager.getInstance().sessionSm.getSessionId();
             for (Database db: autoConnected)
             {
                deactivate(sessionId, db, true);
             }
          }
       }

       /**
        * Database activator.
        */
       @FunctionalInterface
       private static interface Activator
       {
          /**
           * Activate database.
           *
           * @throws  PersistenceException
           *          if any error is encountered initializing the persistence framework.
           */
          public void activate() throws PersistenceException;
       }

       /**
        * The guarded set of auto-connected sessions for the local database
        */
       private static class AutoConnected
       {
          /** Database */
          private final Database db;

          /** Data guard */
          private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

          /** Connected sessions' ids */
          private final Set<Integer> connected = ConcurrentHashMap.newKeySet();

          /** The timestamp of the most recent database deactivation */
          private final AtomicReference<Date> notActiveSince = new AtomicReference<>();

          /** Flag indicating that the database has been deactivated */
          private final AtomicBoolean activated = new AtomicBoolean(false);

          /**
           * Constructor
           *
           * @param   db
           *          Database.
           */
          public AutoConnected(Database db)
          {
             this.db = db;
          }

          /**
           * Get the read lock.
           *
           * @return  The read lock.
           */
          public Lock readLock()
          {
             return lock.readLock();
          }

          /**
           * Get the write lock.
           *
           * @return  The write lock.
           */
          public Lock writeLock()
          {
             return lock.writeLock();
          }

          /**
           * Check if the database is activated.
           *
           * @return  <code>true</code> if the database is activated.
           */
          public boolean isActivated()
          {
             return activated.get();
          }

          /**
           * Mark the database as activated
           */
          public void activated()
          {
             boolean active = activated.getAndSet(true);
             boolean deactivationInProgress = notActiveSince.getAndSet(null) != null;
             if (db.isPrimary() && active && deactivationInProgress)
             {
                log.info(String.format("Deactivation of %s is cancelled", db));
             }
          }

          /**
           * Mark the database as deactivated
           */
          public void deactivated()
          {
             activated.set(false);
          }

          /**
           * Add the user session which uses the database.
           *
           * @param   sessionId
           *          The session id which uses the database.
           *
           * @return  <code>true</code> if the session was not registered before.
           */
          public boolean add(Integer sessionId)
          {
             activated();
             return connected.add(sessionId);
          }

          /**
           * Remove the user session which has used the database.
           *
           * @param   sessionId
           *          The session id which has used the database.
           *
           * @return  <code>true</code> if the session was registered.
           */
          public boolean remove(Integer sessionId)
          {
             try
             {
                return connected.remove(sessionId);
             }
             finally
             {
                if (connected.isEmpty())
                {
                   notActiveSince.set(new Date());
                }
             }
          }

          /**
           * Get the timestamp of the most recent database deactivation.
           *
           * @return  The timestamp of the most recent database deactivation.
           */
          public Date notActiveSince()
          {
             return notActiveSince.get();
          }
       }
    }