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