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