ConnectionManager.java
/*
** Module : ConnectionManager.java
** Abstract : Provides services for logical database connections and aliases
**
** Copyright (c) 2005-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
** 001 ECF 20051202 @23667 Created initial version. Provides services to
** manage logical database connections and
** aliases. Provides runtime support for the
** following Progress statements/functions:
** CONNECT/DISCONNECT
** CONNECTED
** CREATE/DELETE ALIAS
** ALIAS
** NUM-ALIASES
** LDB-/PDB-/SDB-/DBNAME
** NUM-DBS
** DBTYPE/DBRESTRICTIONS
** 002 ECF 20060120 @24038 Integration with P2J server. Replaced
** ThreadLocal with ContextLocal.
** 003 GES 20060517 @26198 Match new Finalizable interface requirement.
** 004 ECF 20060628 @27670 Fixed closeBufferScope. Only remove a buffer
** from the set of those which reference a
** physical database if its outermost scope is
** closing.
** 005 ECF 20070907 @35292 Fixed constructor to avoid adding the temp
** table dedicated database to the list of
** connected databases. This information should
** be transparent to business logic. Replaced
** database name string with Database
** information object. Required for non-local
** database connections.
** 006 ECF 20071012 @35590 Intermediate set of changes to support remote
** database connections more appropriately. This
** feature set is not yet fully implemented, but
** no existing functionality has been removed.
** 007 ECF 20071119 @35907 Completed implementation of remote database
** connections. Works with the net package to
** establish a virtual session with a remote
** server, to enable remote database access.
** 008 ECF 20071127 @36051 Added certAlias parameter to makeConnection()
** method. Passed through to SessionManager's
** connectVirtual() method. Currently, parameter
** is always null, indicating no requester-side
** validation of remote server's certificate
** will be performed by SecurityManager.
** 009 ECF 20080114 @36803 Fixed log output. Replaced incorrect format
** string.
** 010 ECF 20080118 @36858 Added logging to connect logic and connected
** check code.
** 011 ECF 20080213 @37069 Enable logical connections to locally managed
** databases. If a connection is attempted to a
** locally managed database, we short-circuit
** the logic which creates a virtual session and
** instead just manage the logical connection
** information. Fixed reconnect logic for
** pending disconnects.
** 012 SVL 20080806 @39313 Into disconnectImmediately() the session is
** terminated only if it is not used by the
** dirty share manager.
** 013 CA 20100223 @44686 When disconnecting from a remote database,
** the session termination is done in a queue
** (by using a global lock).
** 014 CA 20100329 @44784 Backed out H013 (@44686), as it was replaced
** with a safer approach (the locking is done in
** RouterSessionManager.terminateVirtual).
** 015 ECF 20120217 Added getPersistence(String) to allow the
** Persistence object associated with the given
** database to be used publicly.
** 016 OM 20121101 Implemented DBPARAM built-in function.
** 017 VMN 20130411 Added boolean parameter modal to recordOrShowError() method.
** 018 OM 20130507 Switch to int64 API function for obtaining desired java type.
** 019 ECF 20131010 Added metadata support.
** 020 CA 20131029 Added API to retrieve the aliases by their logical db name.
** 021 OM 20130924 Added support for userid/password authorization to database.
** Switch to java.util.logging instead of Apache's logging API.
** Enriched DBPARAM result with -U/-P parameters.
** 022 CA 20140513 Added a weight for the context-local var, to ensure predetermined
** order during context reset.
** Ensure the remote persistence is loaded, if connection was successful
** (before authentication occurs).
** 023 OM 20140625 Replaced hardcoded rtrim sql function with dialect specific.
** 024 ECF 20141119 Added support for DBVERSION built-in function; fixed logical names
** in ctor; fixed handling of empty string parameter passed to CONNECT
** statement.
** 025 ECF 20141230 Completed support for DBVERSION built-in function. Added lookup of
** LDB name by connected Database object. Create implicit DICTDB alias
** for first connected database.
** 026 ECF 20150111 Added partial support for connection metadata.
** 027 ECF 20150212 Ensure the disconnection of a logical database does not deregister
** the physical database prematurely, if other connections still exist
** under other logical names.
** 028 ECF 20150221 Properly normalize LDB names (right trim and lowercase), instead of
** only lowercasing.
** 029 CA 20150228 The meta CONNECT table needs to be cleaned up before anything else.
** 030 OM 20151130 Added proper number of parameters in rtrim. Fixed handling of query
** result from authentication.
** 031 EVL 20160223 Javadoc fixes to make compatible with Oracle Java 8 for Solaris 10.
** 032 ECF 20160229 Added missing javadoc.
** 033 ECF 20160501 Replaced direct SQL use in authenticate with a more efficient
** implementation.
** 034 ECF 20160609 SQL performance improvement.
** 035 OM 20160608 As rtrim was simplified, addRtrimmedExpression() returns nothing.
** 036 OM 20160628 Fixed case-sensitivity of some database-related functions.
** 037 ECF 20160829 Performance enhancement: use local BufferManager to check
** transaction status instead of TransactionManager.
** 038 CA 20170427 Expose the FWD Session associated with a remote Database connection.
** 039 ECF 20171002 Added -1 (single-user mode) flag to DBPARAM output, and a log message
** about its partial implementation.
** ECF 20171025 Implemented -ct (max connection retries) parameter.
** 040 CA 20171110 Fixed a bug in H039 related to -ct - if connection is succesful, then
** break the loop.
** 041 GES 20171220 Changes to match new silent mode implementation.
** 042 ECF 20180116 Added no-op implementation of -N connect parameter. Fixed case where
** logical and physical database names differ.
** OM 20180117 Fixed cast exception on SQL result. Small optimizations.
** 043 CA 20180208 metaCleaner is cleaned before the local var, so removeConnected()
** must not attempt to clean it again when called from cleaning up the
** local var.
** 044 OM 20180305 Fixed authentication. Added getDefaultDatabase() method.
** 045 ECF 20180408 Strip enclosing quotes from connect options, if present. DICTDB
** alias fix.
** 046 CA 20180425 Fixed some NO-ERROR handling for errors throw by CONNECT.
** 047 CA 20180619 If a tx is still open, postpone the disconnect even if all buffers
** are closed.
** 048 CA 20181128 Added support for GET-DB-CLIENT and SET-DB-CLIENT.
** 049 ECF 20181208 Stubbed dbTaskId method.
** 050 OM 20190111 Avoided circular indirect recursivity at initialization when _Connect
** meta-table is active.
** 051 OM 20190117 Added implementation for dbTaskId(). Generified iterateToIndex().
** Fixed dbParamImpl().
** 052 IAS 20190622 Changed getPersistence() method. It now returns null instead of
** throwing NPE if getDatabase() returns null.
** Added readUserTable() method
** 053 ECF 20190802 Fixed LDB name lookups for metadata databases.
** CA 20190902 Fixed problems when connecting to a FWD server running a database
** schema which is not actively managed by the requester, too.
** 054 IAS 20191015 Rework SET-CLIENT/SET-DB-CLIENT
** 055 IAS 20200212 Rework CONNECT and DBPARAM
** 056 ECF 20200419 Removed Hibernate dependencies.
** 057 IAS 20200819 Added table CRUD ops statistics support
** 058 IAS 20210401 Fixed UserTableStatUpdater-related logic
** 059 IAS 20210520 Added support for session-based database auto-connect
** IAS 20210613 Get rid of the permanent connections notion.
** OM 20210825 Fixed conversion from type 12 to String.
** ECF 20210908 Refactored connect/disconnect metadata logic to remove synchronization in the
** connection listeners which was leading to deadlocks.
** IAS 20211126 Special logic for auto-connect regarding registration
** CA 20220104 An attempt to fix a leak from pdbReferences - in RecordBuffer.finishedImpl,
** if the buffer's scope is still 0 and is active, then call 'closeBufferScope'.
** ECF 20220301 Format cleanup.
** RFB 20220301 Allow for options to be case-insensitive. Ref. # 6088.
** IAS 20220311 Fix recursive call of the OptionsParser
** IAS 20220713 Do not lock database on SET-CLIENT.
** TJD 20220504 Upgrade do Java 11 minor changes
** CA 20220426 Added DBCODEPAGE builtin function stubs.
** IAS 20221128 Re-worked DB-RESTRICTIONS support.
** SVL 20230113 Improved performance by replacing some "for-each" loops with indexed "for" loops.
** HC 20230118 Eliminated some of the uses of String.toUpperCase and/or
** String.toLowerCase for performance.
** CA 20230607 The leading underscore for _User metadata table fields depends on the minimal
** conversion mode, so rely on this namespace attribute when resolving the field names.
** 060 CA 20230215 Improved other cases to reduce BDT usage from within FWD runtime.
** 061 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 062 OM 20231215 Implemented DBCODEPAGE and DBCOLLATION 4GL functions.
** 063 GBB 20240110 -pf file resolvement to depend on configured file system (incl. client-side).
** 064 CA 20240319 Fixed ALIAS function to return the alias, and not the LDBNAME.
** 065 AL2 20240701 Added isBufferScopeLeaking to detect exceptional cases of buffer scope leaks.
** 066 RAA 20240424 Added map that stores the tenant ids based on the database names.
** RAA 20240426 Added tenantListeners list. The TenantChangeListeners are now notified when the
** tenant changes.
** RAA 20240513 tenantListeners is now a map instead of a list.
** Added notifyTenantListenersForReset methods.
** RAA 20240517 Fixed tenant listeners loop in setDbClient and in notifyTenantListenersForReset.
** RAA 20240627 Added deregisterTenantListener method.
** RAA 20240702 Added removeTenantId method. Added checks to prevent NPE for tenants.
** RAA 20240710 Made distinction between logical and physical database name.
** RAA 20240715 Clarified the type of database name occurrences.
** RAA 20240717 Removed notifyTenantListenersForReset methods.
** RAA 20240726 Fixed database retrieval in putTenantDatabaseConnected.
** RAA 20240807 Adjusted the tenant name retrieving process.
** RAA 20240808 Added tenant id to putConnected method.
** 067 OM 20240815 TenantManager.getDatabase() returns a Database object of same type as the
** argument.
** 068 OM 20241001 Incremental update for tenant-related functionality.
** 069 OM 20241128 Multi-tenant runtime support: selected the proper persistence context, eventually
** based on [sharedDb] parameter.
** Implementation of [setMultiTenantAlternative] method.
** 070 OM 20241220 The list of active databases includes the default databases, too.
** 071 ICP 20250129 Used logical constants to leverage cached instances.
** 072 OM 20250303 Local optimizations.
** 073 GBB 20250403 Adding legacy log.
** 074 ES 20250407 Added getTimeOut method to get the value of timeout parameter.
** 075 AL2 20250522 Don't attempt to create a LOG-MANAGER when doing a session clean-up.
*/
/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
**
** Additional terms under GNU Affero GPL version 3 section 7:
**
** Under Section 7 of the GNU Affero GPL version 3, the following additional
** terms apply to the works covered under the License. These additional terms
** are non-permissive additional terms allowed under Section 7 of the GNU
** Affero GPL version 3 and may not be removed by you.
**
** 0. Attribution Requirement.
**
** You must preserve all legal notices or author attributions in the covered
** work or Appropriate Legal Notices displayed by works containing the covered
** work. You may not remove from the covered work any author or developer
** credit already included within the covered work.
**
** 1. No License To Use Trademarks.
**
** This license does not grant any license or rights to use the trademarks
** Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
** of Golden Code Development Corporation. You are not authorized to use the
** name Golden Code, FWD, or the names of any author or contributor, for
** publicity purposes without written authorization.
**
** 2. No Misrepresentation of Affiliation.
**
** You may not represent yourself as Golden Code Development Corporation or FWD.
**
** You may not represent yourself for publicity purposes as associated with
** Golden Code Development Corporation, FWD, or any author or contributor to
** the covered work, without written authorization.
**
** 3. No Misrepresentation of Source or Origin.
**
** You may not represent the covered work as solely your work. All modified
** versions of the covered work must be marked in a reasonable way to make it
** clear that the modified work is not originating from Golden Code Development
** Corporation or FWD. All modified versions must contain the notices of
** attribution required in this license.
*/
package com.goldencode.p2j.persist;
import java.math.*;
import java.net.*;
import java.nio.file.*;
import java.util.*;
import java.util.Map.*;
import java.util.function.*;
import java.util.logging.*;
import java.util.stream.*;
import com.goldencode.p2j.directory.*;
import com.goldencode.p2j.net.*;
import com.goldencode.p2j.net.Session;
import com.goldencode.p2j.persist.dialect.*;
import com.goldencode.p2j.persist.dirty.*;
import com.goldencode.p2j.persist.meta.*;
import com.goldencode.p2j.persist.orm.*;
import com.goldencode.p2j.security.*;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.ErrorManager;
import com.goldencode.p2j.util.logging.CentralLogger;
import com.goldencode.p2j.util.ClientPrincipalResource.*;
import com.goldencode.util.*;
/**
* Manages information about connected databases in the current context.
* This includes the existence of connections and their status, and the
* various aliases and logical names assigned to physical databases. This
* information is managed to support converted Progress logic, which relies
* upon the state of this information to make logic decisions. It also is
* used to determine which physical database is associated with each record
* buffer and its backing {@link Persistence} service object.
* <p>
* A connection in the sense of this class is not a database-level connection,
* such as a JDBC connection. Database-level connections are managed by the
* <code>Persistence</code> class transparently. A connection to this class
* is a logical construct inherited from converted Progress code. Connecting
* and disconnecting from a database at this level does not create or destroy
* a JDBC-level connection resource. However, for transient databases at
* least, it may affect the loading and unloading of database configurations
* at the {@link DatabaseManager} level. That is, the first time any client
* context connects to a transient database (i.e., one that is not always
* loaded by the server by default), that database's configuration is loaded
* temporarily by the server. The last time a client context disconnects
* from a transient database, that database's configuration is unloaded.
* <p>
* Connections register a physical database name to a logical database name,
* which can be different or the same as the physical name. A database may
* not be connected more than once without first disconnecting. Once
* disconnected, it may be connected again. Only one physical database may
* be registered to a given logical name at a time. Methods are provided to
* connect and disconnect, as well as to query information about connected
* databases, such as their physical and logical names, how many are
* connected, their types and restrictions.
* <p>
* Any number of aliases may be assigned to a given logical name. Aliases
* may only be created for connected databases, though they survive database
* disconnect. They may be explicitly deleted, however.
* <p>
* Whenever a new record buffer scope is opened, it is registered with its
* context's connection manager. References from buffers to their backing,
* physical databases are tracked, to enable the correct processing of
* database disconnect requests. Disconnects can only occur immediately if
* all procedures which reference the target database are complete. This
* translates to a state where no record buffers reference the database.
* Furthermore, even if no buffers reference a database, it may not be
* disconnected if the active code point currently is within a transaction.
* At the end of the transaction, all disconnects which were deferred are
* processed.
* <p>
* A single instance of this class is created for each client context. The
* constructor is not called directly by client code; rather, access is
* provided through the static {@link #get} method.
* <p>
* Currently, the Progress semantic of database-level auto connect lists is
* not supported.
*/
public final class ConnectionManager
{
/** Logger */
private static final CentralLogger LOG = CentralLogger.get(ConnectionManager.class.getName());
/** Context local instance of this class */
private static final ContextLocal<ConnectionManager> local =
new ContextLocal<ConnectionManager>()
{
@Override
public WeightFactor getWeight()
{
return WeightFactor.LEVEL_5;
}
protected ConnectionManager initialValue()
{
return new ConnectionManager();
}
protected void cleanup(ConnectionManager connMgr)
{
connMgr.sessionClosed();
}
};
/**
* Collect all the DB connections so that they are cleaned up before anything else (as
* these are dependent on persistence and other context-local data.
*/
private static final ContextLocal<ConnectionDispatcher> metaCleaner =
new ContextLocal<ConnectionDispatcher>()
{
@Override
public WeightFactor getWeight()
{
return WeightFactor.FIRST;
}
@Override
protected void cleanup(ConnectionDispatcher dispatcher)
{
dispatcher.cleanup();
}
};
/** DICTDB alias name */
private static final String DICTDB = "dictdb";
/** Default DBVERSION text */
private static final String DEFAULT_DBVERSION = "11";
/** Default number of database connection retries */
// TODO: "50" is documented as the "single-user default"; "multi-user default" is documented
// as "-". Unclear what this means.
private static final int DEFAULT_CONN_RETRIES = 50;
/** Milliseconds to wait between connection retries */
private static final long CONN_RETRY_DELAY = 1000L;
/** Cache of database versions, indexed by PDB name */
private static final Map<String, String> dbVersionCache = new HashMap<>();
/** Next available connect ID for metadata updates */
private static long nextConnectID = 0L;
/** Set of locked database. */
private final Set<String> locked = new CaseInsensitiveHashSet<>();
/** Map of registered CLIENT-PRINCIPAL objects, for each database. */
private final Map<String, ClientPrincipal> clients = new HashMap<>();
/**
* Map with the userids for the currently connected databases.
* This map must be kept in sync with the <code>connected</code> map of the databases.
*/
private final Map<String, String> userIds = new LinkedHashMap<>();
/**
* Stores the userid used at connection time whether the database is connected using
* command-line (directory implementation) or programatically using <code>CONNECT</code>
* with -U/-P parameters.
* This list is not affected by later <code>SETUSERID</code> calls.
*/
private final Map<String, String> connectTimeUserid = new LinkedHashMap<>();
/** Map of logical database names to <code>ConnectInfo</code> objects */
private final Map<String, ConnectInfo> connected = new CaseInsensitiveLinkedHashMap<>();
/** Map of physical database helper objects to logical database names */
private final Map<Database, String> ldbMap = new HashMap<>();
/** Databases awaiting disconnect (first level) */
private final Set<Database> pendingDisconn = new HashSet<>();
/** Databases awaiting disconnect (second level) */
private final Set<Database> pendingTxDisconn = new HashSet<>();
/** Map of active aliases to logical database names */
private final Map<String, String> aliases = new CaseInsensitiveLinkedHashMap<>();
/** Map of buffer sets currently referencing databases */
private final Map<Database, Set<RecordBuffer>> pdbReferences = new HashMap<>();
/** Map of database info objects to network sessions */
private final Map<Database, Session> sessions = new HashMap<>();
/** Map of tenant names mapped by database logical names. */
private final Map<String, String> tenantsByDatabase = new HashMap<>();
/** Map with registered listeners for tenant changing processes based on the database logical name. */
private final Map<String, Set<TenantChangeListener>> tenantListeners = new HashMap<>();
/** Connect/disconnect event dispatcher */
private final ConnectionDispatcher dispatcher;
/**
* Equivalent of -1 argument in CONNECT statement. At this moment it is not used
* except for computing DBPARAM function result.
* TODO: Future implementation will address this flag.
*/
private boolean singleClient = false;
/**
* This flags a registered (completely build) object. It is set after calling
* {@link #register()}.
*/
private int registerProgress = 0;
/**
* Default constructor.
* <p>
* Call {@link #register()} ASAP to actually connects to all permanent databases and register
* with the transaction manager for master and global finish notifications.
*/
private ConnectionManager()
{
// initialize connection- and user statistics-related metadata listeners, if any
if (MetadataManager.inUse(MetadataManager._CONNECT) ||
MetadataManager.inUse(MetadataManager._MYCONNECTION))
{
dispatcher = new ConnectionDispatcher();
metaCleaner.set(dispatcher);
}
else
{
dispatcher = null;
}
}
/**
* Registers with the transaction manager for master and global finish notifications. Connects
* to all permanent databases.
* <p>
* WARNING: Only call this method once!
*/
private void register()
{
if (registerProgress > 2)
{
throw new RuntimeException("Already registered");
}
if (registerProgress == 1)
{
// registering in progress. This is the second call to this method. Return without
// issuing any error to allow the previous all to build up the circular reference to the
// BufferManager and this object
return;
}
registerProgress = 1; // in progress
// registration completed. Prevent any further registration
registerProgress = 2;
}
/**
* Retrieve the <code>ConnectionManager</code> instance for the local context.
*
* @return Context local connection manager.
*/
public static ConnectionManager get()
{
ConnectionManager cm = local.get();
if (cm.registerProgress != 2)
{
cm.register();
// now CM can be finally be used safely, without producing an infinite recursion
}
return cm;
}
/**
* Retrieve the value of the LOCK_TIMEOUT startup parameter.
*
* @param dbName
* Database name.
*
* @return See above.
*/
public static Long getTimeOut(String dbName)
{
ConnectionManager cm = local.get();
ConnectInfo cntInfo = cm.connected.get(dbName);
List<Map.Entry<ConnectOption, ConnectOptionValue>> opts = cntInfo != null ? cntInfo.opts : null;
if (opts != null)
{
for (Map.Entry<ConnectOption, ConnectOptionValue> opt: opts)
{
if (opt.getKey() == ConnectOption.LOCK_TIMEOUT)
{
return Long.valueOf(opt.getValue().intValue());
}
}
}
return 0L;
}
/**
* Create a logical connection to a database using the specified
* parameters.
*
* @param args
* One or more connection parameters. This may take one of several forms:
* <ul>
* <li>an ordered list of the following:
* <ol>
* <li>physical database name (required)
* <li>logical database name (optional)
* <li>P2J server host name/address (optional)
* <li>P2J server port number or service name (optional,
* unless host name/address is supplied)
* <li>userid (optional)
* <li>password (optional, required if userid is specified)
* </ol>
* <li>a string with the above listed components, prefaced by
* the following identifiers, respectively:
* <ol>
* <li>-db
* <li>-ld
* <li>-H
* <li>-S
* <li>-U
* <li>-P
* <li>-ct
* </ol>
* For example: <code>-db mydb -ld logical_name -H
* 192.168.1.12 -S 3333</code>. After <code>-db</code>,
* parameters can be in any order, so long as each is
* preceded by the specified option identifier.
* <li>any combination of string fragments, which, if
* concatenated together in the order supplied, would create
* a string matching the requirements of the previous item.
* </ul>
*
* @throws ErrorConditionException
* if any parameter is null or represents the unknown value;
* if any parameter is neither a text value nor an integral
* value;
* if there is an error registering the database with the
* database manager.
*/
public static void connect(Object... args)
{
connect(false, args);
}
/**
* Create a logical connection to a database using the specified parameters.
* @param isAutoConnect
* flag indication that is is an implicit auto-connect operation
* @param args
* One or more connection parameters. This may take one of several forms:
* <ul>
* <li>an ordered list of the following:
* <ol>
* <li>physical database name (required)
* <li>logical database name (optional)
* <li>P2J server host name/address (optional)
* <li>P2J server port number or service name (optional,
* unless host name/address is supplied)
* <li>userid (optional)
* <li>password (optional, required if userid is specified)
* </ol>
* <li>a string with the above listed components, prefaced by
* the following identifiers, respectively:
* <ol>
* <li>-db
* <li>-ld
* <li>-H
* <li>-S
* <li>-U
* <li>-P
* <li>-ct
* </ol>
* For example: <code>-db mydb -ld logical_name -H
* 192.168.1.12 -S 3333</code>. After <code>-db</code>,
* parameters can be in any order, so long as each is
* preceded by the specified option identifier.
* <li>any combination of string fragments, which, if
* concatenated together in the order supplied, would create
* a string matching the requirements of the previous item.
* </ul>
*
* @throws ErrorConditionException
* if any parameter is null or represents the unknown value;
* if any parameter is neither a text value nor an integral
* value;
* if there is an error registering the database with the
* database manager.
*/
public static void connect(boolean isAutoConnect, Object... args)
{
try
{
if (LOG.isLoggable(Level.FINE))
{
String s = Arrays.stream(args)
.map(a -> "[" + (a == null ? "<null>" : a + "]"))
.collect(Collectors.joining(","));
LOG.log(Level.FINE, "connect("+s +")", new Exception());
}
connect_(isAutoConnect, args);
}
catch (Throwable t)
{
LOG.log(Level.SEVERE, "CONNECT failed", t);
throw t;
}
}
/**
* Create a logical connection to a database using the specified
* parameters.
*
* @param isAutoConnect
* flag indication that is is an implicit auto-connect operation
* @param args
* One or more connection parameters. This may take one of several forms:
*
* @throws ErrorConditionException
* see above
*/
private static void connect_(boolean isAutoConnect, Object... args)
{
// options will be parsed into individual components and stored in a list for further processing.
List<Object> options = new ArrayList<>();
try
{
for (Object arg : args)
{
String text = null;
Integer num = null;
if (arg instanceof character)
{
character t = (character) arg;
assertKnownValue(t, "Connect option");
text = t.toStringMessage();
}
else if (arg instanceof String)
{
text = (String) arg;
}
else if (arg instanceof int64)
{
int64 i = (int64) arg;
assertKnownValue(i, "Connect option");
num = i.toJavaIntegerType();
}
else if (arg instanceof Integer)
{
num = (Integer) arg;
}
else
{
throw new IllegalArgumentException("Unexpected connect option: " + arg);
}
if (text != null)
{
if (text.trim().isEmpty())
{
continue;
}
// Parse this string, which may represent a single connect
// parameter, or multiple parameters delimited by whitespace.
String[] parts = text.split(" ");
for (String next : parts)
{
// Add each separate parameter into the options list.
options.add(next.trim());
}
}
else
{
// Add this non-text parameter into the options list.
options.add(num);
}
}
// Analyze all options and attempt connection.
List<List<Map.Entry<ConnectOption, ConnectOptionValue>>> parsedOpts = parse(options);
if (parsedOpts == null)
{
ErrorManager.recordOrThrowError(325, "Unable to execute CONNECT statement", false);
return;
}
if (LOG.isLoggable(Level.FINE))
{
for (List<Map.Entry<ConnectOption, ConnectOptionValue>> opts: parsedOpts)
{
LOG.log(Level.FINE, dbparam(opts));
}
}
for (List<Map.Entry<ConnectOption, ConnectOptionValue>> opts: parsedOpts)
{
get().connectDbImpl(isAutoConnect, opts);
}
}
catch (PersistenceException exc)
{
ErrorManager.recordOrThrowError(exc);
}
}
/**
* Parse CONNECT statement options.
*
* @param args statement arguments.
*
* @return The parsed options (separately for all databases).
*
* @throws PersistenceException
* if a fatal error was encountered
*/
public static List<List<Map.Entry<ConnectOption, ConnectOptionValue>>> parse(List<Object> args)
throws PersistenceException
{
return new OptionsParser(false).parse(args);
}
/**
* Request a disconnect from a logical database. This does not actually
* perform the disconnect immediately, but registers <code>ldbName</code>
* for disconnect at the appropriate time: once no buffers reference the
* database, and at the end of the current transaction (if any).
*
* @param ldbName
* Logical name of the database to disconnect.
*
* @throws ErrorConditionException
* if the specified database has not been opened.
*/
public static void disconnect(character ldbName)
{
try
{
assertKnownValue(ldbName, "LDB name");
disconnect(ldbName.toStringMessage());
}
catch (PersistenceException exc)
{
ErrorManager.recordOrThrowError(exc);
}
}
/**
* Request a disconnect from a logical database. This does not actually
* perform the disconnect immediately, but registers <code>ldbName</code>
* for disconnect at the appropriate time: once no buffers reference the
* database, and at the end of the current transaction (if any).
*
* @param ldbName
* Logical name of the database to disconnect.
*
* @throws ErrorConditionException
* if the specified database has not been opened.
*/
public static void disconnect(String ldbName)
{
try
{
get().disconnectImpl(ldbName);
}
catch (PersistenceException exc)
{
// not a fatal error
ErrorManager.displayError(1009, "DISCONNECT: Database " + ldbName + " is not open or unknown");
}
}
/**
* Tests whether a logical connection to a database exists in this context,
* given a logical database name or alias.
*
* @param name
* Logical database name or alias.
*
* @return <code>true</code> if a logical connection exists; else
* <code>false</code>.
*/
public static logical connected(character name)
{
if (name.isUnknown())
{
return new logical();
}
return get().connectedImpl(name);
}
/**
* Tests whether a logical connection to a database exists in this context,
* given a logical database name or alias.
*
* @param name
* Logical database name or alias.
*
* @return <code>true</code> if a logical connection exists; else
* <code>false</code>.
*/
public static logical connected(String name)
{
return connected(new character(name));
}
/**
* Get the number of aliases currently defined in this context.
*
* @return Number of aliases in this context.
*/
public static integer numAliases()
{
return new integer(get().aliases.size());
}
/**
* Get the number of databases currently connected in this context.
*
* @return Number of logical connections in this context.
*/
public static int getConnectedDbCount()
{
return get().connected.size();
}
/**
* Get the number of databases currently connected in this context.
*
* @return Number of logical connections in this context.
*/
public static integer numDBs()
{
return new integer(getConnectedDbCount());
}
/**
* Implementation of the DBCODEPAGE function.
*
* @param seqNo
* The sequence number of the connected database.
*
* @return The CP of the specified database.
*/
public static character dbCodepage(NumberType seqNo)
{
return seqNo.isUnknown() ? new character() : dbCodepage(seqNo.intValue());
}
/**
* Implementation of the DBCODEPAGE function.
*
* @param index
* The sequence number of the connected database.
*
* @return The CP of the specified database.
*/
public static character dbCodepage(int index)
{
ConnectionManager cm = get();
if (index < 1 || index > cm.connected.size())
{
return new character();
}
return dbCodepage(iterateToIndex(cm.connected.keySet(), index));
}
/**
* Implementation of the DBCODEPAGE function.
*
* @param alias
* The logical name or alias of the connected database.
*
* @return The CP of the specified database.
*/
public static character dbCodepage(character alias)
{
return alias.isUnknown() ? new character() : dbCodepage(alias.toJavaType());
}
/**
* Implementation of the DBCODEPAGE function.
*
* @param alias
* The logical name or alias of the connected database.
*
* @return The CP of the specified database.
*/
public static character dbCodepage(String alias)
{
if (alias == null || alias.isEmpty())
{
return new character();
}
Persistence p = getPersistence(alias);
return (p == null) ? new character() : new character(p.getCodepage());
}
/**
* Implementation of the DBCOLLATION function.
*
* @param seqNo
* The sequence number of the connected database.
*
* @return The collation of the specified database.
*/
public static character dbCollation(NumberType seqNo)
{
return seqNo.isUnknown() ? new character() : dbCollation(seqNo.intValue());
}
/**
* Implementation of the DBCOLLATION function.
*
* @param index
* The sequence number of the connected database.
*
* @return The collation of the specified database.
*/
public static character dbCollation(int index)
{
ConnectionManager cm = get();
if (index < 1 || index > cm.connected.size())
{
return new character();
}
return dbCollation(iterateToIndex(cm.connected.keySet(), index));
}
/**
* Implementation of the DBCOLLATION function.
*
* @param alias
* The logical name or alias of the connected database.
*
* @return The collation of the specified database.
*/
public static character dbCollation(character alias)
{
return alias.isUnknown() ? new character() : dbCollation(alias.toJavaType());
}
/**
* Implementation of the DBCOLLATION function.
*
* @param alias
* The logical name or alias of the connected database.
*
* @return The collation of the specified database.
*/
public static character dbCollation(String alias)
{
if (alias == null || alias.isEmpty())
{
return new character();
}
Persistence p = getPersistence(alias);
return (p == null) ? new character() : new character(p.getCollation());
}
/**
* Get the name of the connected database in this context. If more than
* one, return the first.
*
* @return First or only connected database, or unknown value if none.
*/
public static character dbName()
{
return pdbName(1);
}
/**
* Implementation of the <code>DBPARAM()</code> Progress function.
* Returns, as a character string, a comma-separated list of the parameters
* used to connect to the database.
*
* <p>The sequence number of a database the Progress session is connected
* to. For example, DBPARAM(1) returns information on the first database
* the Progress session is connected to, DBPARAM(2) returns information
* on the second database the Progress session is connected to, etc.
* If you specify a sequence number that does not correspond to a database
* the Progress session is connected to, the DBPARAM function returns the
* unknown value (?).
*
* @param index
* One-based index of the target database among all databases
* currently connected in this context.
*
* @return A comma-separated list of the parameters used to connect to
* the database
*/
public static character dbParam(int index)
{
if (index == 0)
{
return new character();
}
return get().dbParamImpl(null, index);
}
/**
* Implementation of the <code>DBPARAM()</code> Progress function.
* Returns, as a character string, a comma-separated list of the parameters
* used to connect to the database.
*
* <p>The sequence number of a database the Progress session is connected
* to. For example, DBPARAM(1) returns information on the first database
* the Progress session is connected to, DBPARAM(2) returns information
* on the second database the Progress session is connected to, etc.
* If you specify a sequence number that does not correspond to a database
* the Progress session is connected to, the DBPARAM function returns the
* unknown value (?).
*
* @param index
* One-based index of the target database among all databases
* currently connected in this context.
*
* @return A comma-separated list of the parameters used to connect to
* the database
*/
public static character dbParam(NumberType index)
{
if (index == null || index.isUnknown())
{
return new character();
}
return get().dbParamImpl(null, index.intValue());
}
/**
* Implementation of the <code>DBPARAM()</code> Progress function.
* Returns, as a character string, a comma-separated list of the parameters
* used to connect to the database.
*
* <p>This forms of the <code>DBPARAM()</code> function require a character
* expression as a parameter. If the parameter is an alias or the logical
* name of a connected database, then Progress returns the comma-separated
* parameter list. Otherwise, it returns the unknown value (?).
*
* @param nameOrAlias
* The logical-name or alias of a connected database
*
* @return A comma-separated list of the parameters used to connect to
* the database
*/
public static character dbParam(String nameOrAlias)
{
if (nameOrAlias == null || nameOrAlias.isEmpty())
{
return new character();
}
return get().dbParamImpl(nameOrAlias, 0);
}
/**
* Implementation of the <code>DBPARAM()</code> Progress function.
* Returns, as a character string, a comma-separated list of the parameters
* used to connect to the database.
*
* <p>This forms of the <code>DBPARAM()</code> function require a character
* expression as a parameter. If the parameter is an alias or the logical
* name of a connected database, then Progress returns the comma-separated
* parameter list. Otherwise, it returns the unknown value (?).
*
* @param nameOrAlias
* The logical-name or alias of a connected database
*
* @return A comma-separated list of the parameters used to connect to
* the database
*/
public static character dbParam(character nameOrAlias)
{
if (nameOrAlias == null || nameOrAlias.isUnknown())
{
return new character();
}
character ret = get().dbParamImpl(nameOrAlias.toStringMessage(), 0);
ret.setCaseSensitive(nameOrAlias.isCaseSensitive());
return ret;
}
/**
* Get a brief descriptor of the database type with the specified
* connection index.
*
* @param index
* One-based index of the target database among all databases
* currently connected in this context.
*
* @return The string "PROGRESS" or unknown value if <code>index</code>
* represents the unknown value or is outside of the range of
* connected databases.
*/
public static character dbType(NumberType index)
{
if (index.isUnknown())
{
return new character();
}
return dbType(index.intValue());
}
/**
* Get a brief descriptor of the database type with the specified
* connection index.
*
* @param index
* One-based index of the target database among all databases
* currently connected in this context.
*
* @return The string "PROGRESS" or unknown value if <code>index</code>
* represents the unknown value or is outside of the range of
* connected databases.
*/
public static character dbType(int index)
{
if (index < 1 || index > get().connected.size())
{
return new character();
}
return new character("PROGRESS"); // TODO: shouldn't we use FWD or FORWARD as type for FWD backed databases?
}
/**
* Get a brief descriptor of the database type with the specified logical
* name or alias.
*
* @param name
* Logical name or alias of the target database.
*
* @return The string "PROGRESS" or unknown value if <code>name</code>
* is not a recognized logical name or alias.
*/
public static character dbType(character name)
{
if (name.isUnknown())
{
return new character();
}
character ret = dbType(name.toStringMessage());
ret.setCaseSensitive(name.isCaseSensitive());
return ret;
}
/**
* Get a brief descriptor of the database type with the specified logical
* name or alias.
*
* @param name
* Logical name or alias of the target database.
*
* @return The string "PROGRESS" or unknown value if <code>name</code>
* is not a recognized logical name or alias.
*/
public static character dbType(String name)
{
String ldb = get().getLDBName(name);
if (ldb == null)
{
return new character();
}
return new character("PROGRESS"); // TODO: shouldn't we use FWD or FORWARD as type for FWD backed databases?
}
/**
* Get the identifier of the current transaction for the given database.
*
* @param name
* Logical name or alias of a connected database.
*
* @return Current transaction identifier, or unknown value if {@code name} is not the name
* or an alias for a connected database, or if there is no active transaction.
*/
public static integer dbTaskId(character name)
{
if (name == null || name.isUnknown())
{
return new integer();
}
return get().dbTaskIdImpl(name.toJavaType(), 0);
}
/**
* Get the identifier of the current transaction for the given database.
*
* @param name
* Logical name or alias of a connected database.
*
* @return Current transaction identifier, or unknown value if {@code name} is not the name
* or an alias for a connected database, or if there is no active transaction.
*/
public static integer dbTaskId(String name)
{
if (name == null)
{
return new integer();
}
return get().dbTaskIdImpl(name, 0);
}
/**
* Get the identifier of the current transaction for the given database.
*
* @param index
* One-based index of the target database among all databases currently connected in
* this context.
*
* @return Current transaction identifier, or unknown value if {@code index} is not an index
* of a connected database, or if there is no active transaction.
*/
public static integer dbTaskId(NumberType index)
{
if (index == null || index.isUnknown())
{
return new integer();
}
return get().dbTaskIdImpl(null, index.intValue());
}
/**
* Get the identifier of the current transaction for the given database.
*
* @param index
* One-based index of the target database among all databases currently connected in
* this context.
*
* @return Current transaction identifier, or unknown value if {@code index} is not an index
* of a connected database, or if there is no active transaction.
*/
public static integer dbTaskId(int index)
{
return get().dbTaskIdImpl(null, index);
}
/**
* Get the version number of the target database.
*
* @param index
* One-based index of the target database among all databases currently connected in
* this context.
*
* @return The database's version, as extracted from runtime configuration, or unknown value
* if <code>index</code> represents the unknown value or is outside of the range of
* connected databases.
*/
public static character dbVersion(NumberType index)
{
if (index.isUnknown())
{
return new character();
}
return dbVersion(index.intValue());
}
/**
* Get the version number of the target database.
*
* @param index
* One-based index of the target database among all databases currently connected in
* this context.
*
* @return The database's version, as extracted from runtime configuration, or unknown value
* if <code>index</code> is outside of the range of connected databases.
*/
public static character dbVersion(int index)
{
return get().dbVersionImpl(null, index);
}
/**
* Get the version number of the target database.
*
* @param name
* Logical name or alias of the target database.
*
* @return The database's version, as extracted from runtime configuration, or unknown value
* if <code>name</code> represents the unknown value or is not a recognized logical
* name or alias.
*/
public static character dbVersion(character name)
{
if (name.isUnknown())
{
return new character();
}
return dbVersion(name.toStringMessage());
}
/**
* Get the version number of the target database.
*
* @param name
* Logical name or alias of the target database.
*
* @return The database's version, as extracted from runtime configuration, or unknown value
* if <code>name</code> is not a recognized logical name or alias.
*/
public static character dbVersion(String name)
{
return get().dbVersionImpl(name, 0);
}
/**
* Get a description of restrictions for the connected database at the
* specified index.
*
* @param index
* One-based index of the target database among all databases
* currently connected in this context.
*
* @return The empty string, or unknown value if <code>index</code>
* represents the unknown value or is outside of the range of
* connected databases.
*/
public static character dbRestrictions(NumberType index)
{
if (index.isUnknown())
{
return new character();
}
return dbRestrictions(index.intValue(), (String) null);
}
/**
* Get a description of restrictions for the connected database at the
* specified index.
*
* @param index
* One-based index of the target database among all databases
* currently connected in this context.
*
* @return The empty string, or unknown value if <code>index</code> is
* invalid.
*/
public static character dbRestrictions(int index)
{
return get().dbRestrictionsImpl(null, index, null);
}
/**
* Get a description of restrictions for the connected database at the
* specified index. Currently, <code>table</code> is ignored.
*
* @param index
* One-based index of the target database among all databases
* currently connected in this context.
* @param table
* Name of a table within the target database. Ignored.
*
* @return The empty string, or unknown value if <code>index</code> is
* invalid.
*/
public static character dbRestrictions(NumberType index, character table)
{
if (index.isUnknown())
{
return new character();
}
return get().dbRestrictionsImpl(null, index.intValue(), null);
}
/**
* Get a description of restrictions for the connected database at the
* specified index. Currently, <code>table</code> is ignored.
*
* @param index
* One-based index of the target database among all databases
* currently connected in this context.
* @param table
* Name of a table within the target database. Ignored.
*
* @return The empty string, or unknown value if <code>index</code> is
* invalid.
*/
public static character dbRestrictions(NumberType index, String table)
{
if (index.isUnknown())
{
return new character();
}
return get().dbRestrictionsImpl(null, index.intValue(), null);
}
/**
* Get a description of restrictions for the connected database at the
* specified index. Currently, <code>table</code> is ignored.
*
* @param index
* One-based index of the target database among all databases
* currently connected in this context.
* @param table
* Name of a table within the target database. Ignored.
*
* @return The empty string, or unknown value if <code>index</code> is
* invalid.
*/
public static character dbRestrictions(int index, character table)
{
return get().dbRestrictionsImpl(null, index, null);
}
/**
* Get a description of restrictions for the connected database at the
* specified index. Currently, <code>table</code> is ignored.
*
* @param index
* One-based index of the target database among all databases
* currently connected in this context.
* @param table
* Name of a table within the target database. Ignored.
*
* @return The empty string, or unknown value if <code>index</code> is
* invalid.
*/
public static character dbRestrictions(int index, String table)
{
if (index < 1 || index > get().connected.size())
{
return (new character());
}
return get().dbRestrictionsImpl(null, index, null);
}
/**
* Get a description of restrictions for the connected database with the
* specified logical name or alias.
*
* @param name
* Logical name or alias of the target database.
*
* @return The empty string, or unknown value if <code>name</code>
* is not a recognized logical name or alias.
*/
public static character dbRestrictions(character name)
{
return dbRestrictions(name, (character) null);
}
/**
* Get a description of restrictions for the connected database with the
* specified logical name or alias.
*
* @param name
* Logical name or alias of the target database.
*
* @return The empty string, or unknown value if <code>name</code>
* is not a recognized logical name or alias.
*/
public static character dbRestrictions(String name)
{
return get().dbRestrictionsImpl(name, 0, null);
}
/**
* Get a description of restrictions for the connected database with the
* specified logical name or alias. Currently, <code>table</code> is
* ignored.
*
* @param name
* Logical name or alias of the target database.
* @param table
* Name of a table within the target database. Ignored.
*
* @return The empty string, or unknown value if <code>name</code>
* is not a recognized logical name or alias.
*/
public static character dbRestrictions(character name, character table)
{
if (name == null || name.isUnknown())
{
return new character();
}
character ret = get().dbRestrictionsImpl(name.toStringMessage(), 0, null);
ret.setCaseSensitive(name.isCaseSensitive());
return ret;
}
/**
* Get a description of restrictions for the connected database with the
* specified logical name or alias. Currently, <code>table</code> is
* ignored.
*
* @param name
* Logical name or alias of the target database.
* @param table
* Name of a table within the target database. Ignored.
*
* @return The empty string, or unknown value if <code>name</code>
* is not a recognized logical name or alias.
*/
public static character dbRestrictions(character name, String table)
{
return dbRestrictions(name, new character(table));
}
/**
* Get a description of restrictions for the connected database with the
* specified logical name or alias. Currently, <code>table</code> is
* ignored.
*
* @param name
* Logical name or alias of the target database.
* @param table
* Name of a table within the target database. Ignored.
*
* @return The empty string, or unknown value if <code>name</code>
* is not a recognized logical name or alias.
*/
public static character dbRestrictions(String name, character table)
{
return dbRestrictions(new character(name), table);
}
/**
* Get a description of restrictions for the connected database with the
* specified logical name or alias. Currently, <code>table</code> is
* ignored.
*
* @param name
* Logical name or alias of the target database.
* @param table
* Name of a table within the target database. Ignored.
*
* @return The empty string, or unknown value if <code>name</code>
* is not a recognized logical name or alias.
*/
public static character dbRestrictions(String name, String table)
{
if (name == null)
{
return new character();
}
return get().dbRestrictionsImpl(name, 0, null);
}
/**
* Create an alias for a logical database name within the current context.
* The alias may not conflict with an existing logical database name
* (except for that of the database which it aliases). The alias will be
* created regardless of whether the target database currently is
* connected; however, an error will be raised (or silently registered)
* in the event it is not, after the alias has been created. The alias
* persists until explicitly deleted with one of the variants of the
* <code>deleteAlias</code> method, or until the current session ends.
*
* @param alias
* Alias to be created.
* @param ldbName
* Logical name of the database to which the alias is assigned.
*
* @throws ErrorConditionException
* if silent error mode is disabled and any of the following:
* <ul>
* <li><code>alias</code> represents the unknown value;
* <li><code>ldbName</code> represents the unknown value;
* <li>the target database is not connected;
* <li>the alias conflicts with an existing logical database
* name, other than <code>ldbName</code>.
* </ul>
*/
public static void createAlias(character alias, character ldbName)
{
try
{
assertKnownValue(alias, "Alias");
assertKnownValue(ldbName, "LDB name");
createAlias(alias.toStringMessage(), ldbName.toStringMessage());
}
catch (PersistenceException exc)
{
ErrorManager.recordOrThrowError(exc);
}
}
/**
* Create an alias for a logical database name within the current context.
* The alias may not conflict with an existing logical database name
* (except for that of the database which it aliases). The alias will be
* created regardless of whether the target database currently is
* connected; however, an error will be raised (or silently registered)
* in the event it is not, after the alias has been created. The alias
* persists until explicitly deleted with one of the variants of the
* <code>deleteAlias</code> method, or until the current session ends.
*
* @param alias
* Alias to be created.
* @param ldbName
* Logical name of the database to which the alias is assigned.
*
* @throws ErrorConditionException
* if silent error mode is disabled and any of the following:
* <ul>
* <li><code>ldbName</code> represents the unknown value;
* <li>the target database is not connected;
* <li>the alias conflicts with an existing logical database
* name, other than <code>ldbName</code>.
* </ul>
*/
public static void createAlias(String alias, character ldbName)
{
try
{
assertKnownValue(ldbName, "LDB name");
createAlias(alias, ldbName.toStringMessage());
}
catch (PersistenceException exc)
{
ErrorManager.recordOrThrowError(exc);
}
}
/**
* Create an alias for a logical database name within the current context.
* The alias may not conflict with an existing logical database name
* (except for that of the database which it aliases). The alias will be
* created regardless of whether the target database currently is
* connected; however, an error will be raised (or silently registered)
* in the event it is not, after the alias has been created. The alias
* persists until explicitly deleted with one of the variants of the
* <code>deleteAlias</code> method, or until the current session ends.
*
* @param alias
* Alias to be created.
* @param ldbName
* Logical name of the database to which the alias is assigned.
*
* @throws ErrorConditionException
* if silent error mode is disabled and any of the following:
* <ul>
* <li><code>alias</code> represents the unknown value;
* <li>the target database is not connected;
* <li>the alias conflicts with an existing logical database
* name, other than <code>ldbName</code>.
* </ul>
*/
public static void createAlias(character alias, String ldbName)
{
try
{
assertKnownValue(alias, "Alias");
createAlias(alias.toStringMessage(), ldbName);
}
catch (PersistenceException exc)
{
ErrorManager.recordOrThrowError(exc);
}
}
/**
* Create an alias for a logical database name within the current context.
* The alias may not conflict with an existing logical database name
* (except for that of the database which it aliases). The alias will be
* created regardless of whether the target database currently is
* connected; however, an error will be raised (or silently registered)
* in the event it is not, after the alias has been created. The alias
* persists until explicitly deleted with one of the variants of the
* <code>deleteAlias</code> method, or until the current session ends.
*
* @param alias
* Alias to be created.
* @param ldbName
* Logical name of the database to which the alias is assigned.
*
* @throws ErrorConditionException
* if silent error mode is disabled and any of the following:
* <ul>
* <li>the target database is not connected;
* <li>the alias conflicts with an existing logical database
* name, other than <code>ldbName</code>.
* </ul>
*/
public static void createAlias(String alias, String ldbName)
{
try
{
get().createAliasImpl(alias, ldbName);
}
catch (PersistenceException exc)
{
ErrorManager.recordOrThrowError(exc);
}
}
/**
* Delete the specified alias immediately.
*
* @param alias
* Alias to be deleted.
*/
public static void deleteAlias(character alias)
{
if (alias.isUnknown())
{
return;
}
deleteAlias(alias.toStringMessage());
}
/**
* Delete the specified alias immediately.
*
* @param alias
* Alias to be deleted.
*/
public static void deleteAlias(String alias)
{
get().deleteAliasImpl(alias);
}
/**
* Retrieve the alias at the specified index in the current context.
*
* @param index
* One-based index of the alias (aliases are stored in the order
* in which they were created).
*
* @return The target alias, or unknown value if <code>index</code> is
* invalid.
*/
public static character alias(NumberType index)
{
if (index.isUnknown())
{
return (new character());
}
return alias(index.intValue());
}
/**
* Retrieve the alias at the specified index in the current context.
*
* @param index
* One-based index of the alias (aliases are stored in the order
* in which they were created).
*
* @return The target alias, or unknown value if <code>index</code> is
* invalid.
*/
public static character alias(int index)
{
return get().aliasImpl(index);
}
/**
* Get the logical database name for the connected database at the given
* index.
*
* @param index
* One-based index of the target database among all databases
* currently connected in this context.
*
* @return The logical name, or unknown value if <code>index</code> is
* invalid.
*/
public static character ldbName(NumberType index)
{
if (index.isUnknown())
{
return (new character());
}
return ldbName(index.intValue());
}
/**
* Get the logical database name for the connected database at the given
* index.
*
* @param index
* One-based index of the target database among all databases
* currently connected in this context.
*
* @return The logical name, or unknown value if <code>index</code> is
* invalid.
*/
public static character ldbName(int index)
{
return get().ldbNameImpl(null, index);
}
/**
* Get the logical database name for the connected database with the given
* logical name or alias.
*
* @param name
* Logical name or alias of the target database.
*
* @return The logical name, or unknown value if <code>name</code> is
* invalid.
*/
public static character ldbName(character name)
{
if (name.isUnknown())
{
return (new character());
}
return ldbName(name.toStringMessage());
}
/**
* Get the logical database name for the connected database with the given
* logical name or alias.
*
* @param name
* Logical name or alias of the target database.
*
* @return The logical name, or unknown value if <code>name</code> is
* invalid.
*/
public static character ldbName(String name)
{
return get().ldbNameImpl(name, 0);
}
/**
* Get the logical database name for the record buffer associated with the
* given DMO.
*
* @param dmo
* Data model object returned by a previous call to {@link
* RecordBuffer#define}.
*
* @return The logical database name for the DMO.
*/
public static character ldbName(Object dmo)
{
RecordBuffer buffer = ((BufferReference) dmo).buffer();
return (new character(buffer.getLogicalDatabase()));
}
/**
* Get the physical database name for the connected database at the given
* index.
*
* @param index
* One-based index of the target database among all databases
* currently connected in this context.
*
* @return The physical name, or unknown value if <code>index</code> is
* invalid.
*/
public static character pdbName(NumberType index)
{
if (index.isUnknown())
{
return (new character());
}
return pdbName(index.intValue());
}
/**
* Get the physical database name for the connected database at the given
* index.
*
* @param index
* One-based index of the target database among all databases
* currently connected in this context.
*
* @return The physical name, or unknown value if <code>index</code> is
* invalid.
*/
public static character pdbName(int index)
{
return get().pdbNameImpl(null, index);
}
/**
* Get the physical database name for the connected database with the
* given logical name or alias.
*
* @param name
* Logical name or alias of the target database.
*
* @return The physical name, or unknown value if <code>name</code> is
* invalid.
*/
public static character pdbName(character name)
{
if (name.isUnknown())
{
return (new character());
}
return pdbName(name.toStringMessage());
}
/**
* Get the physical database name for the connected database with the
* given logical name or alias.
*
* @param name
* Logical name or alias of the target database.
*
* @return The physical name, or unknown value if <code>name</code> is
* invalid.
*/
public static character pdbName(String name)
{
return get().pdbNameImpl(name, 0);
}
/**
* Get the logical database name for the connected database at the given
* index.
* <p>
* The current implementation delegates to {@link #ldbName(NumberType)}.
*
* @param index
* One-based index of the target database among all databases
* currently connected in this context.
*
* @return The logical name, or unknown value if <code>index</code> is
* invalid.
*/
public static character sdbName(NumberType index)
{
if (index.isUnknown())
{
return (new character());
}
return sdbName(index.intValue());
}
/**
* Get the logical database name for the connected database at the given
* index.
* <p>
* The current implementation delegates to {@link #ldbName(int)}.
*
* @param index
* One-based index of the target database among all databases
* currently connected in this context.
*
* @return The logical name, or unknown value if <code>index</code> is
* invalid.
*/
public static character sdbName(int index)
{
return ldbName(index);
}
/**
* Get the logical database name for the connected database with the given
* logical name or alias.
* <p>
* The current implementation delegates to {@link #ldbName(character)}.
*
* @param name
* Logical name or alias of the target database.
*
* @return The logical name, or unknown value if <code>name</code> is
* invalid.
*/
public static character sdbName(character name)
{
if (name.isUnknown())
{
return (new character());
}
return sdbName(name.toStringMessage());
}
/**
* Get the logical database name for the connected database with the given
* logical name or alias.
* <p>
* The current implementation delegates to {@link #ldbName(String)}.
*
* @param name
* Logical name or alias of the target database.
*
* @return The logical name, or unknown value if <code>name</code> is
* invalid.
*/
public static character sdbName(String name)
{
return ldbName(name);
}
/**
* Retrieve the network session which manages the connection to the remote
* server which is authoritative for the specified database.
*
* @param database
* Database to which the target session is mapped.
*
* @return Network session for the given database, or <code>null</code>
* if there is no current connection.
*/
public static Session getSession(Database database)
{
return get().sessions.get(database);
}
/**
* Retrieve the network session which manages the connection to the remote server which is
* authoritative for the specified database name, which may be a logical name, physical name,
* or alias. It is assumed that the database currently is connected.
*
* @param name
* Logical name, physical name, or alias for database.
*
* @return Network session for the given database, or <code>null</code> if there is no current
* connection.
*/
public static Session getSession(String name)
{
ConnectionManager mgr = get();
Database db = mgr.getDatabase(name);
return (db == null ? null : mgr.sessions.get(db));
}
/**
* Get the {@link Persistence} instance associated with the given database
* name, which may be a physical or logical name, or an alias.
*
* @param name
* Physical or logical database name, or alias.
*
* @return Persistence object associated with the given database, which
* may be locally or remotely connected or <code>null</code>
* if no database with such name is connected
*/
public static Persistence getPersistence(String name)
{
Database database = get().getDatabase(name);
return database != null ? PersistenceFactory.getInstance(database) : null;
}
/**
* Returns a string representing the user ID used for authentication to a database.
* The database is identified by its logical name.
*
* @param ldbName
* The logical name of the database for which the userid is requested.
*
* @return current userid that was authenticated to specified database
*/
public static String getCurrentUserid(String ldbName)
{
ClientPrincipal cp = ConnectionManager.getDbClient(ldbName.toLowerCase());
ConnectionManager cm = get();
if (cp != null && cm.getTenantName(ldbName) != null)
{
return cp.getUserId().toJavaType() + "@" + cp.getDomainName().toJavaType();
}
// call the method with unwrapped-argument signature:
return cm.getCurrentUseridImpl(ldbName);
}
/**
* Return the default logical database. It is used only with {@code setuserid} and
* {@code userid} with default (missing argument) for database parameter.
*
* @return The logical database name if only one (permanent) database is connected.
* Otherwise it raise an error condition and abends.
*/
public static String getDefaultDatabase()
{
ConnectionManager cm = get();
if (cm.connected.size() == 1)
{
Iterator<String> it = cm.connected.keySet().iterator();
return it.next(); // return 1st element
}
// TODO: this is a compile-time error. Apparently P4GL recompiles the procedures when it
// calls them and the set of connected databases has changed.
String[] msgs =
{
"SETUSERID/USERID functions require one connected db or db argument",
"<program> Could not understand line <number>"
};
ErrorManager.showErrorAndAbend(new int[] { 691, 196 }, msgs);
return null;
}
/**
* Authenticates the user for specified DB connection. Checks if the user login account match
* the corresponding record of the _User table of the database. If the specified database
* is not connected, an error is issued.
*
* The user/password comparing operation is done as 4GL:
* <ul>
* <li><strong>user</strong>: is case-insensitive, trailing spaces are ignored
* (in upper(rtrim()) mode)
* <li><strong>password</strong>: must be an exact match: after applying <code>ENCODE</code>
* to password string it is compared to existing record from database in case
* sensitive, taking into consideration trailing spaces
* </ul>
*
* @param userid
* The name of the user to set as UserID.
* @param password
* The users password.
* @param dbname
* Logical database name.
*
* @return The <code>true</code> in case of valid user match <code>false</code> otherwise.
*/
public static boolean authenticate(String userid, String password, String dbname)
{
ConnectionManager ctxCm = get();
// validation: dbname MUST be the logical name or an alias of a connected database
character ldbName = ctxCm.ldbNameImpl(dbname, 0);
if (dbname == null || dbname.isEmpty() || ldbName.isUnknown())
{
// the 'dbname' is not a valid connected database logical name nor an alias
ErrorManager.recordOrShowError(
1072,
"SETUSERID of database " + dbname + " requires that database to be connected",
false);
// at runtime, we get a false value (not unknown, not error)
return false;
}
String encPassword = SecurityOps.encode(password).toStringMessage();
Persistence persistence = PersistenceFactory.getInstance(ctxCm.getDatabase(dbname));
Dialect dialect = persistence.getDialect();
String pccUserid = dialect.getProcessedCharacterColumnName(
MetadataSecurityOps.getMetaFieldUserid(ldbName.toStringMessage()), true);
String pccPassword = MetadataSecurityOps.getMetaFieldPassword(ldbName.toStringMessage());
// SETUSERID() is strict when comparing passwords
StringBuilder sb = new StringBuilder();
sb.append("SELECT COUNT(*) FROM ");
sb.append(MetadataSecurityOps.META_TABLE_USER);
sb.append( " WHERE ");
sb.append(pccUserid);
sb.append(" = upper(");
dialect.addRtrimmedExpression("?", sb);
sb.append(") AND ");
sb.append(pccPassword);
sb.append(" = ?");
String query = sb.toString();
try
{
String[] paramList = new String[] { userid, encPassword };
Object ufo = persistence.getSingleSQLResult(query, Persistence.META_CTX, paramList);
if (ufo == null)
{
// this should not happen anyway
return false;
}
Long count = getSQLResultAsLong(ufo);
if (count == null || count != 1)
{
// no match (if zero), if 2 or more then the _user table is broken
return false;
}
}
catch (PersistenceException pe)
{
if (LOG.isLoggable(Level.WARNING))
{
LOG.log(Level.WARNING, "setUserId failed accessing database (SQL query)", pe);
}
return false;
}
// if we reached this point then the user/password combination is OK
// (the userid may be in camel-case or have trailing spaces)
return true;
}
/**
* Check the access level for a database based on metadata of that database.
* This will query if _User table and return <code>SecurityOps.FREE_ACCESS</code> if
* the meta table is not populated and <code>SecurityOps.BLANK_ALLOWED</code> otherwise.
*
* @param ldbname
* The logical database name to query.
*
* @return The access level to specified database.
*/
public static int getAuthLevel(String ldbname)
{
String query = "SELECT COUNT(*) FROM " + MetadataSecurityOps.META_TABLE_USER;
try
{
Object result = getPersistence(ldbname).getSingleSQLResult(query, Persistence.META_CTX, null);
if (result != null)
{
Long count = getSQLResultAsLong(result);
return count == 0 ? SecurityOps.FREE_ACCESS : SecurityOps.BLANK_ALLOWED;
}
}
catch (PersistenceException pe)
{
if (LOG.isLoggable(Level.WARNING))
{
LOG.log(Level.WARNING, "setUserId failed accessing database (SQL query)", pe);
}
}
return SecurityOps.FREE_ACCESS;
}
/**
* Mark the given database of a tenant as connected.
* This method makes use of the {@code ConnectionManager.putConnected} method, and it will fail if
* the database with the given name cannot be found.
*
* @param client
* The client/tenant for which to mark the database.
* @param ldbName
* The logical name of the database.
*/
public static void putTenantDatabaseConnected(ClientPrincipal client, String ldbName)
{
ConnectionManager cm = get();
String tenantName = TenantManager.getTenantName(client.getDomainName().getValue(), ldbName);
Database db = TenantManager.getDatabase(tenantName, ldbName, false, Database.Type.PRIMARY);
if (db == null)
{
throw new IllegalArgumentException("Cannot determine the database of tenant " +
tenantName +
" and logical database name " +
ldbName);
}
cm.putConnected(ldbName, db, null, tenantName);
}
/**
* Associate the specified CLIENT-PRINCIPAL with the given database logical name. If not specified,
* use the default connected database.
* <p>
* A copy of the CLIENT-PRINCIPAL object is saved in the {@link #clients} map.
*
* @param client
* The CLIENT-PRINCIPAL object.
* @param dbName
* A valid database name.
* @param callee
* where the method is called from
*
* @throws IllegalArgumentException
* If the database name is not valid.
*/
public static void setDbClient(ClientPrincipal client, String dbName, SealCallee callee)
{
ConnectionManager cm = get();
String ldbName = cm.getLDBName(dbName);
if (ldbName == null)
{
throw new IllegalArgumentException("Could not resolve the database name:" +
(dbName == null ? "null" : dbName));
}
String userId = client.getUserId().getValue();
String ldbNameLowerCase = ldbName.toLowerCase();
boolean tenantEnabled = TenantConfig.isEnabled();
String tenantName = null;
if (tenantEnabled)
{
tenantName = TenantManager.getTenantName(client.getDomainName().getValue(), dbName);
}
cm.clients.put(ldbNameLowerCase, client.clone());
cm.userIds.put(ldbNameLowerCase, userId);
if (callee == SealCallee.SET_DB_CLIENT)
{
cm.locked.add(ldbName);
}
if (tenantEnabled)
{
Set<TenantChangeListener> listeners = cm.tenantListeners.get(ldbName);
if (listeners == null)
{
return;
}
for (TenantChangeListener listener : new HashSet<>(listeners))
{
// Notify the listeners that the current tenant has changed
listener.tenantChanged(tenantName, ldbName);
}
}
}
/**
* Get the list of unlocked database connections.
*
* @return the list of unlocked database connections.
*/
public static ArrayList<String> getUnlocked()
{
ConnectionManager cm = get();
ArrayList<String> ret = new ArrayList<>();
for (Entry<String, ConnectInfo> entry : cm.connected.entrySet())
{
String db = entry.getKey();
if (!cm.locked.contains(db))
{
ret.add(db);
}
}
return ret;
}
/**
* Check if database connection is locked.
*
* @param dbName
* A valid database name.
*
* @throws IllegalArgumentException
* If the database name is not valid.
*
* @return <code>true</code> if the database connection is locked.
*/
public static boolean isLocked(String dbName)
{
ConnectionManager cm = get();
String ldbName = cm.getLDBName(dbName);
if (ldbName == null)
{
throw new IllegalArgumentException("Could not resolve the database name:" +
(dbName == null ? "null" : dbName));
}
return cm.locked.contains(ldbName);
}
/**
* Unlock the database connection.
*
* @param dbName
* A valid database name.
*
* @throws IllegalArgumentException
* If the database name is not valid.
*/
public static void unlock(String dbName)
{
ConnectionManager cm = get();
String ldbName = cm.getLDBName(dbName);
if (ldbName == null)
{
throw new IllegalArgumentException("Could not resolve the database name:" +
(dbName == null ? "null" : dbName));
}
cm.locked.remove(ldbName);
}
/**
* Get the CLIENT-PRINCIPAL associated with the given database name.
*
* @param dbName
* A database name.
*
* @return The CLIENT-PRINCIPAL object or <code>null</code> if none associated.
*/
public static ClientPrincipal getDbClient(String dbName)
{
ConnectionManager cm = get();
String ldbName = cm.getLDBName(dbName);
if (ldbName == null)
{
return null;
}
return cm.clients.get(ldbName.toLowerCase());
}
/**
* Set the currently authenticated userid for a database.
* This method assumes that the userid has already been authenticated in this context using
* <code>authenticate()</code> method.
* If <code>byConnect</code> then the userid is stored and will be reported by
* <code>dbparam</code> function with -U tag.
*
* @param dbName
* A valid database name.
* @param userid
* The name of the user to set as UserID.
* @param lock
* Flag indicationg that the database should be locked
* @param byConnect
* <code>true</code>if the authentication was done from a connect (at startup time
* or later) or <code>false</code> if the authentication was done using the
* <code>setuserid</code> function.
*
* @throws IllegalArgumentException
* If the database name is not valid.
*/
public static void setAuthenticatedUserid(String dbName, String userid, boolean lock, boolean byConnect)
{
ConnectionManager cm = get();
String ldbName = cm.getLDBName(dbName);
if (ldbName == null)
{
throw new IllegalArgumentException("Could not resolve the database name:" +
(dbName == null ? "null" : dbName));
}
cm.setCurrentUseridImpl(ldbName.toLowerCase(), userid, lock);
if (byConnect)
{
cm.setConnectUserImpl(ldbName);
}
}
/**
* Read _User table from the database supplying String array: {userid, domain-name, password}
* to a consumer.
*
* @param persistence
* {@link Persistence} instance for the database
* @param consumer
* <code>Consumer<String[]></code> instance
*/
public static void readUserTable(Persistence persistence, Consumer<String[]> consumer)
{
ConnectionManager ctxCm = get();
String ldbName = ctxCm.getLDBName(persistence.getDatabase(Persistence.META_CTX));
Dialect dialect = persistence.getDialect();
String pccUserid =
dialect.getProcessedCharacterColumnName(MetadataSecurityOps.getMetaFieldUserid(ldbName), true);
String pccPassword = MetadataSecurityOps.getMetaFieldPassword(ldbName);
String pccDomain =
dialect.getProcessedCharacterColumnName(MetadataSecurityOps.getMetaFieldDomain(ldbName), true);
String sql = "SELECT " + pccUserid + " AS userid, " +
pccDomain + " AS domain, " +
pccPassword + " AS password " +
"FROM " + MetadataSecurityOps.META_TABLE_USER;
readQueryResults(sql,
persistence,
rs -> {
Object[] data = rs.get();
consumer.accept(new String[]{(String) data[0], (String) data[1], (String) data[2]});
});
}
/**
* Read query results.
*
* @param sql
* SQL statement to be executed
* @param persistence
* {@link Persistence} instance for the database
* @param consumer
* Result set consumer
*/
public static void readQueryResults(String sql,
Persistence persistence,
Consumer<ScrollableResults<?>> consumer)
{
try
{
ScrollableResults<?> rs = persistence.executeSQLQuery(sql, Persistence.META_CTX, null);
while (rs.next())
{
consumer.accept(rs);
}
rs.close();
}
catch (PersistenceException e)
{
throw new RuntimeException(e);
}
}
/**
* Get the logical database name for the connected database at the given
* index.
*
* @param index
* One-based index of the target database among all databases
* currently connected in this context.
*
* @return The logical name, or unknown value if <code>index</code> is
* invalid.
*/
static String ldbNameNative(int index)
{
return get().ldbNameNativeImpl(null, index);
}
/**
* Normalize the given filename by extracting the root name (removing the
* file path and the filename extension).
*
* @param name
* File name.
*
* @return "Normalized" root file name.
*/
static String normalizeFilename(String name)
{
// Extract root name from PDB name.
int pos = name.lastIndexOf(".");
if (pos >= 0)
{
name = name.substring(0, pos);
}
pos = name.lastIndexOf(FileSystems.getDefault().getSeparator());
if (pos >= 0)
{
name = name.substring(pos + 1);
}
return name;
}
/**
* Assert that <code>data</code> does not represent the unknown value.
* If the assertion fails, an exception is thrown.
*
* @param data
* The data value to be checked.
* @param description
* A brief description of <code>data</code>'s purpose; this will
* be used in an error message if the assertion fails.
*
* @throws PersistenceException
* if the assertion fails.
*/
private static void assertKnownValue(BaseDataType data, String description)
throws PersistenceException
{
if (data.isUnknown())
{
// TODO: use proper error number/message.
throw new PersistenceException(description + " cannot be unknown", -1);
}
}
/**
* Iterate over the specified collection, returning the element at index {@code index}.
*
* @param coll
* Collection over which to iterate.
* @param index
* The <b>1-based</b> index of the target element in the collection.
*
* @return The element at the specified index, or {@code null} if {@code index} was never
* reached.
*/
private static <X> X iterateToIndex(Collection<X> coll, int index)
{
Iterator<X> iter = coll.iterator();
for (int i = 1; iter.hasNext(); i++)
{
X next = iter.next();
if (i == index)
{
return next;
}
}
return null;
}
/**
* Normalize an alias or logical database name by right trimming whitespace and lowercasing.
*
* @param name
* Alias or LDB name to be normalized.
*
* @return Normalized result.
*/
private static character normalizeAliasOrLDB(character name)
{
return TextOps.toLowerCase(TextOps.rightTrim(name));
}
/**
* Register a {@code TenantChangeListener} for actions that involve tenant id changes.
*
* @param listener
* The listener to be notified when a tenant changes.
* @param ldbName
* The logical database name for which the listener should be registered.
*/
public void registerTenantListener(TenantChangeListener listener, String ldbName)
{
if (ldbName == null)
{
LOG.log(Level.SEVERE, "Database should not be null when registering a tenant!");
}
Set<TenantChangeListener> tls = tenantListeners.computeIfAbsent(ldbName, k -> new HashSet<>());
tls.add(listener);
}
/**
* De-register a {@code TenantChangeListener}.
*
* @param listener
* The listener to be de-registered.
* @param ldbName
* The logical database name for which the listener should be de-registered.
*/
public void deregisterTenantListener(TenantChangeListener listener, String ldbName)
{
Set<TenantChangeListener> tls = tenantListeners.get(ldbName);
if (tls == null)
{
return;
}
tls.remove(listener);
removeTenant(ldbName);
}
/**
* Get a tenant name based on a database logical name.
*
* @param ldbName
* The database logical name.
*
* @return The name of the tenant.
*/
public String getTenantName(String ldbName)
{
return tenantsByDatabase.get(ldbName);
}
/**
* Remove a tenant that is tied to a database.
*
* @param ldbName
* The database logical name for which to remove the tenant.
*/
public void removeTenant(String ldbName)
{
tenantsByDatabase.remove(ldbName);
}
/**
* Get the logical database name for a connected physical database.
*
* @param database
* Physical database helper object.
*
* @return Logical database name, if database is connected, else <code>null</code>.
*/
public String getLDBName(Database database)
{
if (database.isMeta())
{
// the fact that metatables are managed in a separate database is a FWD implementation
// detail; do the lookup based on the associated, primary database
database = database.toType(Database.Type.PRIMARY);
}
return ldbMap.get(database);
}
/**
* This method is invoked after a master transaction commit/rollback; any databases awaiting
* the end of the current transaction to disconnect are now deregistered with the database
* manager.
*/
void transactionEnded()
{
disconnectImmediately(pendingTxDisconn, false);
}
/**
* This method is invoked after a master transaction commit/rollback; if the specified
* database is waiting for the end of the current transaction to disconnect, it will be
* deregistered with the database manager.
*
* @param database
* The database for which to process the transaction end - as this will be executed
* for every database which was used in this transaction block.
*/
void transactionEnded(Database database)
{
if (pendingTxDisconn.contains(database))
{
disconnectImmediately(database, false);
pendingTxDisconn.remove(database);
}
}
/**
* Register that a reference from a record buffer to its database has
* become active. We will be notified when this reference is no longer
* active, which is used to resolve pending disconnect requests.
*
* @param buffer
* Record buffer whose scope has been opened.
*
* @see #closeBufferScope
* @see #disconnect
*/
void openBufferScope(RecordBuffer buffer)
{
// add buffer to set of those which reference a physical database
Database pdb = buffer.getDatabase();
Set<RecordBuffer> set = pdbReferences.computeIfAbsent(pdb, k -> new HashSet<>());
set.add(buffer);
}
/**
* Very specific method to check if a buffer is still dangling in the {@link #pdbReferences}
* collection. This should be called only in extra-ordinary execution paths that may involve
* native Java integrations outside the persist framework. This should be called in case of
* fail-safes to avoid memory leaks.
* <p>
* DON'T use this method for trivial checks on whether the buffer has its scope opened or not.
* This should be used only to detect unexpected leaks inside {@link #pdbReferences} that can't
* be solved by the normal lifecycle of {@link TransactionManager} or {@link BufferManager}.
* <p>
* For example, invoking generic triggers that do not properly manage the buffer lifecycle
* may result in memory leaks. Therefore, attempt to detect this case and improve the fault
* tolerance on persistence layer side.
*
* @param buffer
* The buffer that should be checked for leakage.
*
* @return {@code true} if this buffer still resides in {@link #pdbReferences} due to a
* bad handling of the buffer from native Java integrations like generic triggers.
*/
public boolean isBufferScopeLeaking(RecordBuffer buffer)
{
Database pdb = buffer.getDatabase();
Set<RecordBuffer> set = pdbReferences.get(pdb);
if (set == null)
{
return false;
}
return set.contains(buffer);
}
/**
* Called by a record buffer to notify that its scope has been closed,
* indicating that it no longer has an active reference to its database.
* This may result in the current context disconnecting from the database,
* if and only if:
* <ol>
* <li>there is a (first level) disconnect request pending for the
* database; AND
* <li>no record buffers currently have active references to it; AND
* <li>no transaction currently is active.
* </ol>
* <p>
* If all three conditions are met, the database is disconnected
* immediately, and removed from the first level pending disconnect list.
* If the first two conditions are met, but the third is not, the database
* is moved from the first level to the second level disconnect list,
* where it will await the end of the transaction for disconnect.
*
* @param buffer
* Record buffer whose scope has been closed.
*
* @see #openBufferScope
* @see #disconnect
*/
void closeBufferScope(RecordBuffer buffer)
{
// nothing to do if this is not the outermost open scope for this buffer
if (buffer.getOpenScopeCount() > 0)
{
return;
}
// remove buffer from set of those which reference a physical database
Database pdb = buffer.getDatabase();
Set<RecordBuffer> set = pdbReferences.get(pdb);
if (set == null)
{
return;
}
set.remove(buffer);
// if the set is now empty, we are not in a transaction, AND this database is pending
// disconnect, deregister it with the database manager for this session
if (set.isEmpty())
{
pdbReferences.remove(pdb);
if (pendingDisconn.contains(pdb))
{
if (!buffer.getBufferManager().isTransaction())
{
// Database can be deregistered immediately.
DatabaseManager.deregisterDatabase(pdb);
}
else
{
// Database cannot be deregistered until transaction ends.
pendingTxDisconn.add(pdb);
}
// either way, remove it from the first level pending disconnect set
pendingDisconn.remove(pdb);
}
}
}
/**
* Get the logical database name for the given logical database name or
* alias. The database does not necessarily have to be connected, if
* <code>name</code> represents an alias.
*
* @param name
* Logical database name or alias.
*
* @return Logical database name or <code>null</code> if <code>name</code>
* is not recognized.
*/
public String getLDBName(String name)
{
// Look up name as an alias.
String ldbName = aliases.get(name);
if (ldbName != null)
{
// name is an alias.
return ldbName;
}
else if (connected.containsKey(name))
{
// name is the LDB name.
return name;
}
// name is unrecognized.
return null;
}
/**
* Get the physical database name for the given logical database name or
* alias. The database must be connected. The resulting name may
* optionally be normalized, which excludes any filename path and
* extension.
*
* @param name
* Logical database name or alias.
* @param normalize
* <code>true</code> to return the normalized name; else
* <code>false</code> to return the unaltered (possibly fully
* qualified name), as provided by the caller of the {@link
* #connect} method.
*
* @return Physical database name or <code>null</code> if
* <code>name</code> is not recognized.
*/
String getPDBName(String name, boolean normalize)
{
String pdb = getDatabase(name).getName();
if (normalize)
{
pdb = normalizeFilename(pdb);
}
return pdb;
}
/**
* Get the database information object for the given database name, which
* may be a logical name, physical name, or alias. It is assumed that the
* database currently is connected.
*
* @param name
* Logical name, physical name, or alias for database.
*
* @return Database information object associated with <code>name</code>,
* or <code>null</code> if no such database currently is
* connected.
*/
Database getDatabase(String name)
{
// Get LDB name, in case name is an alias.
String ldbName = getLDBName(name);
ConnectInfo info = connected.get(ldbName);
return info != null ? info.database : null;
}
/**
* Get a list of active databases, which is meant to include the temp-table database and all
* connected, persistent databases. The term "active" is used instead of
* "connected" because the inclusion of the temp table database is an
* implementation-specific requirement, which is not consistent with the 4GL's notion of
* "connected" databases. Thus, this API is meant for internal use only and should
* not be exposed as a legacy API.
*
* @return List of all connected, persistent databases, plus the temp-table database.
*/
ArrayList<Database> getActiveDatabases()
{
int size = connected.size();
ArrayList<Database> list = new ArrayList<>(size + 1);
list.add(DatabaseManager.TEMP_TABLE_DB); // always connected
if (size > 0)
{
for (ConnectInfo ci : connected.values())
{
list.add(ci.database);
if (ci.database.isTenant())
{
// for each tenant database, the default database is also connected (contains shared tables and
// some meta tables, like _users aand _sec_authentication*)
list.add(ci.database.getDefault());
}
}
}
return list;
}
/**
* Get the currently connected aliases by their logical database name.
*
* @return See above.
*/
Map<String, String> getAliasesByLDB()
{
Map<String, String> res = new HashMap<>();
for (String key : aliases.keySet())
{
res.put(aliases.get(key), key);
}
return res;
}
/**
* Dynamically analyze (by class type) and convert a value returned by a SQL statement of an
* unknown type (but assumed to be integer-like) to Long value. If the type cannot be detected,
* {@code null} is returned.
*
* @param ufo
* The value from a SQL result. It is assumed to be some kind of integer/long.
*
* @return The long value of the unknown object or {@code null} if its type could not be
* detected.
*/
private static Long getSQLResultAsLong(Object ufo)
{
if (ufo instanceof Integer)
{
return ((Integer) ufo).longValue();
}
else if (ufo instanceof Long)
{
return (Long) ufo;
}
else if (ufo instanceof BigInteger)
{
return ((BigInteger) ufo).longValue();
}
else
{
if (ufo != null && LOG.isLoggable(Level.WARNING))
{
LOG.log(Level.WARNING, "failed to extract long/int value SQL response");
}
return null;
}
}
/**
* Returns the current id used by the user to authenticated to a database
* identified by its logical name.
*
* @param ldbName
* The logical name of the database for which the userid is requested.
*
* @return current userid
*/
private String getCurrentUseridImpl(String ldbName)
{
String uid = userIds.get(ldbName);
return uid == null ? "" : uid;
}
/**
* Sets the currently authenticated {@code userid} for a database.
* <p>
* The only authority to set this is an instance of {@code MetadataSecurityOps} and only
* after checking the user/password combination against the {@code _User} meta-table.
* <p>
* Notes:
* <ul>
* <li>The {@code CONNECT} statement with {@code -U} and {@code -P} parameters will have to
* call {@code SecurityOps} to verify if the user/password combination if correct. <br>
* (At this moment the {@code ConnectionManager} does not support these parameters, yet).
* <li>The command-line parameters should be investigated.
* </ul>
*
* @param ldbName
* The logical database name for which the {@code userid} is set/updated.
* @param newUserId
* The newly newUserId for this database context.
* @param lock
* Flag indicationg that the database should be locked
*/
private void setCurrentUseridImpl(String ldbName, String newUserId, boolean lock)
{
userIds.put(ldbName, newUserId);
if (lock)
{
locked.add(ldbName);
}
}
/**
* Mark the current user for the database as the connection parameter.
* The user currently authenticated will be reported by <code>dbparam</code> function with -U
* tag.
*
* @param ldbName
* The logical database name for which the userid is marked as connection parameter.
*/
private void setConnectUserImpl(String ldbName)
{
String userId = userIds.get(ldbName);
if (userId != null)
{
connectTimeUserid.put(ldbName, userId);
}
}
/**
* Perform end of session cleanup; disconnects all connected databases
* without regard to transaction state or buffer reference status. This
* ensures <code>DatabaseManager</code>'s transient database reference
* counts are not corrupted when client sessions end.
*/
private void sessionClosed()
{
// waistband
Set<Database> all = new HashSet<>();
for (ConnectInfo info : connected.values())
{
all.add(info.database);
}
// belt
all.addAll(pendingTxDisconn);
// suspenders
all.addAll(pendingDisconn);
// deregister all connected databases for this session
disconnectImmediately(all, true);
}
/**
* Disconnect all databases in the specified set by deregistering them with the
* <code>DatabaseManager</code> and removing them from the set. Network sessions associated
* with the databases in the set are terminated accordingly. If being called at the end of a
* user's session, clean up internal data structures and update _connect metadata as well.
*
* @param pending
* Set of databases to disconnect.
* @param sessionCleanup
* <code>true</code> if this action is being taken as part of cleanup for a session
* that is terminating; <code>false</code> if a normal disconnect.
*/
private void disconnectImmediately(Set<Database> pending, boolean sessionCleanup)
{
LegacyLogManager logManager = LegacyLogOps.logMgr(!sessionCleanup);
Iterator<Database> iter = pending.iterator();
while (iter.hasNext())
{
Database database = iter.next();
disconnectImmediately(database, sessionCleanup);
if (logManager == null)
{
LOG.severe("LOG-MANAGER couldn't be created at session clean-up.");
}
else if (logManager.isLoggingEnabled())
{
LegacyLogOps.logMgr().writeMessage("Disconnecting from database " + database +
", user number ?. (9543)",
LoggingEntryType.DB_Connects,
null,
LoggingSubsys.CONN,
LoggingLevel.BASIC);
}
iter.remove();
}
}
/**
* Disconnect the specified database by deregistering it with the <code>DatabaseManager</code>
* and terminating the associated network session.
*
* @param database
* Database to disconnect.
* @param sessionCleanup
* <code>true</code> if this action is being taken as part of cleanup for a session
* that is terminating; <code>false</code> if a normal disconnect.
*/
private void disconnectImmediately(Database database, boolean sessionCleanup)
{
if (sessionCleanup)
{
String ldbName = ldbMap.get(database);
if (ldbName != null)
{
removeConnected(ldbName, sessionCleanup);
}
}
if (!database.isLocal())
{
DatabaseManager.deregisterDatabase(database);
}
else
{
DatabaseManager.deactivate(null, database, false);
}
Session session = sessions.remove(database);
if (session != null)
{
// Close the session only if is not used by the dirty share manager
// for this database.
if (session.isRunning() && !DirtyShareFactory.isUsedByManager(database, session))
{
session.terminate();
}
}
if (LOG.isLoggable(Level.FINE))
{
String ctx = Utils.describeContext();
// database is not null
String msg = String.format("{%s} Database '%s' disconnected", ctx, database);
LOG.log(Level.FINE, msg);
if (LOG.isLoggable(Level.FINEST))
{
msg = String.format("{%s} connected: %s", ctx, connected);
LOG.log(Level.FINEST, msg);
msg = String.format("{%s} pendingDisconn: %s", ctx, pendingDisconn);
LOG.log(Level.FINEST, msg);
msg = String.format("{%s} pendingTxDisconn: %s", ctx, pendingTxDisconn);
LOG.log(Level.FINEST, msg);
}
}
}
/**
* Get the logical name for a database, given an alias (which may be the
* logical name itself), or its index among all connected databases in
* this context.
*
* @param name
* Logical database name or alias. May be <code>null</code>,
* but only if <code>index</code> is non-<code>null</code>.
* @param index
* Index of the database among connected databases. May be
* <code>null</code>, but only if <code>name</code> is
* non-<code>null</code>. This is a one-based index.
*
* @return Logical database name, or unknown value if <code>name</code>
* is not recognized.
*/
private character ldbNameImpl(String name, int index)
{
return new character(ldbNameNativeImpl(name, index));
}
/**
* Get the logical name for a database, given an alias (which may be the
* logical name itself), or its index among all connected databases in
* this context.
*
* @param name
* Logical database name or alias. May be <code>null</code>,
* but only if <code>index</code> is non-<code>null</code>.
* @param index
* Index of the database among connected databases. May be
* <code>null</code>, but only if <code>name</code> is
* non-<code>null</code>. This is a one-based index.
*
* @return Logical database name, or unknown value if <code>name</code>
* is not recognized.
*/
private String ldbNameNativeImpl(String name, int index)
{
String ldbName = null;
if (name == null)
{
ldbName = iterateToIndex(connected.keySet(), index);
}
else
{
ldbName = getLDBName(name);
}
if (ldbName != null)
{
return ldbName;
}
return null;
}
/**
* Get the physical name for a database, given its logical name or alias,
* or its index among all connected databases in this context.
*
* @param name
* Logical database name or alias. May be <code>null</code>,
* but only if <code>index</code> is non-<code>null</code>.
* @param index
* One-based index of the database among connected databases.
*
* @return Physical database name, or unknown value if <code>name</code>
* is not recognized.
*/
private character pdbNameImpl(String name, int index)
{
String key = null;
if (name == null)
{
key = iterateToIndex(connected.keySet(), index);
}
else
{
key = name.toLowerCase();
}
String pdbName = getPDBName(key, false);
if (pdbName != null)
{
return (new character(pdbName));
}
return (new character());
}
/**
* Create a logical connection to a database using the specified
* parameters.
*
* @param isAutoConnect
* flag indication that is is an implicit auto-connect operation
* @param opts
* A list of one or more connection parameters.
* @throws PersistenceException
* if any parameter is null;
* if any parameter is neither a text value nor an integral
* value;
* if there is an error registering the database with the
* database manager.
*/
private void connectDbImpl(boolean isAutoConnect, List<Map.Entry<ConnectOption, ConnectOptionValue>> opts)
throws PersistenceException
{
Map<ConnectOption, ConnectOptionValue> options = new EnumMap<>(ConnectOption.class);
opts.forEach(e -> options.put(e.getKey(), e.getValue()));
this.singleClient = options.containsKey(ConnectOption.SINGLE_USER);
String pdb = !options.containsKey(ConnectOption.DATABASE) ? null :
options.get(ConnectOption.DATABASE).textValue();
String ldb = !options.containsKey(ConnectOption.LOGICAL_DBNAME) ? null :
options.get(ConnectOption.LOGICAL_DBNAME).textValue();
String host = !options.containsKey(ConnectOption.HOSTNAME) ? null :
options.get(ConnectOption.HOSTNAME).textValue();
String service = !options.containsKey(ConnectOption.SERVICE_NAME) ? null :
options.get(ConnectOption.SERVICE_NAME).textValue();
String user = !options.containsKey(ConnectOption.USERID) ? null :
options.get(ConnectOption.USERID).textValue();
String password = !options.containsKey(ConnectOption.PASSWORD) ? null :
options.get(ConnectOption.PASSWORD).textValue();
Integer port = null;
if (service != null)
{
try
{
port = Integer.valueOf(service);
}
catch (NumberFormatException exc)
{
// Look up the port number associated with the given service name.
port = Utils.getPortForService(null, service);
if (port < 0)
{
ErrorManager.recordOrShowError(5192,
String.format(
"Unable to find server %s with protocol TCP in file SERVICES" +
" or SERVICES file not found in expected location.",
service),
false, false, true);
return;
}
}
}
Integer maxRetries = !options.containsKey(ConnectOption.MAX_CONN_RETRIES) ?
DEFAULT_CONN_RETRIES :
options.get(ConnectOption.MAX_CONN_RETRIES).intValue();
connect(isAutoConnect, pdb, ldb, host, service, user, password, port, maxRetries);
ConnectInfo info = connected.get(ldb);
if (info != null)
{
info.opts = opts;
}
}
/**
* Create a logical connection to a database using the specified
* parameters.
*
* @param args
* A list of one or more connection parameters. This may take one
* of several forms:
* <ul>
* <li>an ordered list of the following:
* <ol>
* <li>physical database name (required)
* <li>logical database name (optional)
* <li>P2J server host name/address (optional)
* <li>P2J server port number or service name (optional, unless host
* name/address is supplied)
* <li>userid (optional)
* <li>password (optional, required if userid is present)
* </ol>
* </li>
* <li>the following parameter pairs:
* <ol>
* <li>physical database name key/value pair...
* <ul>
* <li>"-db" (optional identifier for physical database
* name option to follow)
* <li>physical database name (required)
* </ul>
* </li>
* <li>...followed by an unordered list of the following
* options. Note that the order of these parameter
* key/value pairs is not important; however, the
* pairing of key string (e.g., "-ld"), with the
* associated value must be preserved:
* <ul>
* <li>"-ld" (required only if next element is provided)
* <li>logical database name (optional)
* <li>"-H" (required only if next element is provided)
* <li>P2J server host name/address (optional)
* <li>"-S" (required only if next element is provided)
* <li>P2J server port number or service name (optional,
* unless host name/address is supplied)
* <li>"-U" (required only if next element is provided)
* <li>userid used to authenticate to database (optional)
* <li>"-P" (required only if next element is provided)
* <li>password used for authentication (optional)
* </ul>
* </li>
* </ol>
* </li>
* </ul>
*
* @throws PersistenceException
* if any parameter is null;
* if any parameter is neither a text value nor an integral value;
* if there is an error registering the database with the database manager.
*/
private void connectImpl(List<Object> args)
throws PersistenceException
{
String flag = null;
String pdb = null;
String ldb = null;
String host = null;
String service = null;
String user = null;
String password = null;
Integer port = null;
Integer maxRetries = DEFAULT_CONN_RETRIES;
// Loop through the parameters and divine their meanings.
for (Object arg : args)
{
String text = null;
Integer num = null;
if (arg instanceof String)
{
text = ParamFileReader.removeEnclosingQuotes((String) arg);
}
else if (arg instanceof Integer)
{
num = (Integer) arg;
}
else if (arg == null)
{
throw new PersistenceException("Invalid (null) connect argument");
}
if (text != null)
{
// Detect option keys.
if (text.startsWith("-"))
{
if (flag != null)
{
int[] nums = new int[] {1403, 325};
String[] texts = new String[] {
"You have not supplied a parameter for argument " + flag,
"Unable to execute CONNECT statement" };
ErrorManager.showErrorAndAbend(nums, texts);
}
if (text.equals("-1"))
{
singleClient = true;
String msg = "Single-user mode (-1) is reported, but is not currently enforced";
UnimplementedFeature.partial(msg);
}
else
{
flag = text;
}
continue;
}
// Physical database name.
if ("-db".equals(flag) || (flag == null && pdb == null))
{
pdb = text;
flag = null;
continue;
}
// Logical database name.
if ("-ld".equals(flag) || (flag == null && ldb == null))
{
ldb = text;
flag = null;
continue;
}
// Host name/address.
if ("-H".equals(flag) || (flag == null && host == null))
{
host = text;
flag = null;
continue;
}
// Socket service/port number.
if ("-S".equals(flag) || (flag == null && port == null))
{
try
{
// Argument could be a string representation of a port
// number, so try to parse it as such.
service = text;
port = Integer.valueOf(text);
}
catch (NumberFormatException exc)
{
// Look up the port number associated with the given service name.
port = Utils.getPortForService(null, text);
if (port < 0)
{
throw new PersistenceException("Invalid service name: " + text);
}
}
flag = null;
continue;
}
// network type
if ("-N".equals(flag))
{
// silently ignore
flag = null;
continue;
}
// number of connection retries allowed
if ("-ct".equals(flag))
{
try
{
maxRetries = Integer.valueOf(text);
}
catch (NumberFormatException exc)
{
throw new PersistenceException("Invalid retry count: " + text);
}
flag = null;
continue;
}
// Authentication: userid
if ("-U".equals(flag) || (flag == null && user == null))
{
user = text;
flag = null;
continue;
}
// Authentication: password
if ("-P".equals(flag) || (flag == null && password == null))
{
password = text;
flag = null;
continue;
}
}
else if ("-S".equals(flag) || (flag == null && port == null))
{
port = num;
flag = null;
continue;
}
}
connect(pdb, ldb, host, service, user, password, port, maxRetries);
}
/**
* Establish database connection.
*
* @param isAutoConnect
* flag indication that is is an implicit auto-connect operation
* @param pdb
* database physical name.
* @param ldb
* database logical name.
* @param host
* remote host name.
* @param service
* remote service name.
* @param user
* used id.
* @param password
* user password.
* @param port
* remote port number.
* @param maxRetries
* max connection attempts.
*
* @throws PersistenceException
* if any parameter is null;
* if any parameter is neither a text value nor an integral value;
* if there is an error registering the database with the database manager.
*/
private void connect(boolean isAutoConnect,
String pdb,
String ldb,
String host,
String service,
String user,
String password,
Integer port,
Integer maxRetries)
throws PersistenceException
{
if (host == null && port != null)
{
// TODO: query host for current P2J server instance.
throw new PersistenceException(
"Database connect host must be specified for port '" + port + "'");
}
if (LOG.isLoggable(Level.FINE))
{
String msg = String.format(
"{%s} Connecting database [PDB = %s; LDB = %s; HOST = %s; SVC = %s; " +
"PORT = %d; UID = %s; PWD = %s]",
Utils.describeContext(),
pdb,
ldb,
host,
service,
port,
user,
(password == null) ? "null" : "***");
LOG.log(Level.FINE, msg);
}
Database database = null;
if (host != null)
{
database = new Database(pdb, Database.Type.PRIMARY, host, port);
InetSocketAddress address = database.getSocketAddress();
if (SessionManager.get().isLocalNode(address))
{
database = new Database(pdb, Database.Type.PRIMARY);
}
}
else
{
database = new Database(pdb, Database.Type.PRIMARY);
}
for (int i = 0; i <= maxRetries; i++)
{
// TODO: passing null for certAlias disables requester-side server
// validation, which is fine for the common case. To enable this, we
// would need a directory lookup to find the correct certificate alias
// for the remote server in the directory, based upon the IP address
// and port (or service name) of the remote server.
// if ldb == null, a valid ldb will be returned
ldb = makeConnection(database, ldb, null, isAutoConnect);
// handle failed attempts
if (ldb == null && !connectedImpl(new character(pdb)).booleanValue())
{
if (i < maxRetries)
{
try
{
Thread.sleep(CONN_RETRY_DELAY);
}
catch (InterruptedException exc)
{
if (LOG.isLoggable(Level.WARNING))
{
String msg = Utils.describeContext() +
" sleep between database connection retry attempts interrupted";
LOG.log(Level.WARNING, msg);
}
}
}
else
{
// connection failed after maximum number of attempts
return;
}
}
else
{
break;
}
}
// ensure the remote persistence is loaded
PersistenceFactory.getInstance(database);
if (user == null && password == null)
{
// no user/password combination supplied, store the detected blank os username for userid
setConnectUserImpl(getLDBName(pdb));
return;
}
if (user == null || password == null)
{
int[] nums = new int[] {1403, 325};
String[] texts = new String[] {
"You have not supplied a parameter for argument " + (user == null ? "-U" : "-P"),
"Unable to execute CONNECT statement" };
ErrorManager.showErrorAndAbend(nums, texts);
return;
}
// after a successful connection check if user/password match
if (SecurityOps.getAuthLevel(ldb) != SecurityOps.FREE_ACCESS)
{
if (SecurityOps.setUserId(user, password, ldb).booleanValue())
{
// pin the authenticated used so it will be returned by DBPARAM function
setConnectUserImpl(ldb);
}
else
{
// authentication failed
ErrorManager.recordOrThrowError(710,
"Your Password and Userid " + user + " do not match",
false,
false);
}
}
else
{
ErrorManager.recordOrThrowError(709,
"-P and -U startup parameters require _User file records",
false,
false);
}
}
/**
* Create a logical connection to the database with the specified physical
* name, creating the specified logical name for it.
*
* @param database
* Database information object.
* @param ldbName
* Logical database name. If <code>null</code>, root name of
* physical database name will be used.
* @param certAlias
* Alias of certificate to be read from trust store to enable
* certificate-based, server-to-server authentication. This
* should match the server ID of the remote node.
* @param isAutoConnect
* flag indication that is is an implicit auto-connect operation
*
* @return the logical database name on success (the ldbName input parameter may be
* <code>null</code>, but a new logical name will be computed and returned)
* and <code>null</code> otherwise.
*
* @throws ErrorConditionException
* if the given database already is connected.
* @throws PersistenceException
* if there is an error registering the database with the
* database manager or establishing a network session with the
* remote server.
*/
private String makeConnection(Database database, String ldbName,
String certAlias, boolean isAutoConnect)
throws PersistenceException
{
// Normalize names.
String pdb = normalizeFilename(database.getName());
if (ldbName == null)
{
// if ldbName is null, root name of physical database name will be used.
ldbName = pdb.toLowerCase();
}
else
{
ldbName = normalizeAliasOrLDB(new character(ldbName)).toStringMessage();
}
// Is this database awaiting disconnection?
boolean disconn = pendingDisconn.contains(database) || pendingTxDisconn.contains(database);
if (connected.containsKey(ldbName))
{
String existing = normalizeFilename(connected.get(ldbName).database.getName());
// Warn user if database is already connected (and is not pending a disconnect).
if (existing.equals(pdb))
{
if (!disconn)
{
if (isAutoConnect)
{
return ldbName;
}
ErrorManager.recordOrShowWarning(
1012,
"Warning -- database " + ldbName + " is already connected",
true,
false,
false,
true);
return null;
}
}
// Error out if attempting to assign logical name to more than one physical database.
else
{
int[] num = new int[] { 1012, 1018 };
String[] msg = new String[]
{
"Warning -- database " + ldbName + " is already connected",
"Database physical names " + pdb + " and " + existing +
" do not match. No connect."
};
ErrorManager.recordOrShowError(num, msg, true);
return null;
}
}
// If this database is pending a disconnect, the connect and pending
// disconnect cancel each other out. If not, register it and remember it is connected.
if (!pendingDisconn.remove(database) && !pendingTxDisconn.remove(database))
{
boolean local = database.isLocal();
try
{
Session session = null;
if (!local)
{
// Establish network session with remote server.
SessionManager sessMgr = SessionManager.get();
InetSocketAddress address = database.getSocketAddress();
session = sessMgr.connectVirtual(address, certAlias, null);
}
sessions.put(database, session);
}
catch (Exception exc)
{
throw new PersistenceException(exc);
}
// Register database with database manager if not local or have not been registered already.
// If the database is local but not "load_at_startup" it must be activated.
// If it was already registered false will be returned
if (local)
{
DatabaseManager.activate(database.getName());
}
else
{
local |= DatabaseManager.registerDatabase(database);
// we can safely assume that this is not a [_temp] database as temp-database cannot be
// connected or disconnected
local |= (database.isLocal() && DatabaseManager.registerDirtyDatabase(database));
}
if (local)
{
// Store mapping of logical name to database and vice-versa.
putConnected(ldbName, database, null, null);
if (LOG.isLoggable(Level.FINE))
{
String msg = String.format("{%s} Connected database '%s' [%s]",
Utils.describeContext(),
ldbName,
database);
LOG.log(Level.FINE, msg);
if (LOG.isLoggable(Level.FINEST))
{
msg = String.format("{%s} Current connections: [%s]",
Utils.describeContext(),
connected);
LOG.log(Level.FINEST, msg);
}
}
}
else if (LOG.isLoggable(Level.WARNING))
{
String msg = String.format("{%s} Unable to make database connection for '%s' [%s]",
Utils.describeContext(),
ldbName,
database);
LOG.log(Level.WARNING, msg);
}
}
else
{
// Store mapping of logical name to database and vice-versa.
putConnected(ldbName, database, SecurityOps.getDefaultUserid(), null);
if (LOG.isLoggable(Level.FINE))
{
String msg = String.format("{%s} Cancelled disconnect of database '%s' [%s]",
Utils.describeContext(),
ldbName,
database);
LOG.log(Level.FINE, msg);
if (LOG.isLoggable(Level.FINEST))
{
msg = String.format("{%s} Current connections: [%s]", Utils.describeContext(), connected);
LOG.log(Level.FINEST, msg);
}
}
}
// return the used logical name for the connected database
return ldbName;
}
/**
* Request a disconnect from a logical database. This does not actually
* perform the disconnect immediately, but registers <code>ldbName</code>
* for disconnect at the appropriate time: once no buffers reference the
* database, and at the end of the current transaction (if any).
*
* @param ldbName
* Logical name of the database to disconnect.
*
* @throws PersistenceException
* if the specified database has not been opened.
*/
private void disconnectImpl(String ldbName)
throws PersistenceException
{
String ldb = getLDBName(ldbName);
if (ldb == null)
{
throw new PersistenceException("DISCONNECT: Database " + ldbName + " is not open or unknown", 1009);
}
// remove the database from the connected map whether or not the disconnect can occur immediately
Database database = removeConnected(ldb, false);
// check whether there are any other connections to this physical database under a different
// logical name; if so, we can't deregister the database before those are disconnected
for (ConnectInfo info : connected.values())
{
// although it may not be optimal to walk the values of a map in general, in practice the
// number of connected databases typically is very low, so this should be fast
if (database.equals(info.database))
{
// return without deregistering physical database (or scheduling it for deferred deregistration)
return;
}
}
// if any record buffers still reference the database, defer the disconnect until all
// affected buffer scopes have closed
if (pdbReferences.containsKey(database))
{
pendingDisconn.add(database);
}
else if (TransactionManager.isTransaction())
{
pendingTxDisconn.add(database);
}
else
{
disconnectImmediately(database, false);
}
}
/**
* Tests whether a logical connection to a database exists in this context,
* given a logical database name or alias.
*
* @param name
* Logical database name or alias.
*
* @return <code>true</code> if a logical connection exists; else
* <code>false</code>.
*/
private logical connectedImpl(character name)
{
name = normalizeAliasOrLDB(name);
// Get the logical DB name.
String ldb = getLDBName(name.toStringMessage());
ConnectInfo info = connected.get(ldb);
Database database = info != null ? info.database : null;
boolean isConn = (database != null &&
!pendingDisconn.contains(database) &&
!pendingTxDisconn.contains(database));
if (LOG.isLoggable(Level.FINE))
{
String ctx = Utils.describeContext();
String msg = String.format("{%s} Database '%s' (%s [%s]) connected status: %s",
ctx,
name,
ldb,
(database != null ? database.toString() : null),
isConn);
LOG.log(Level.FINE, msg);
if (LOG.isLoggable(Level.FINEST))
{
msg = String.format("{%s} connected: %s", ctx, connected);
LOG.log(Level.FINEST, msg);
msg = String.format("{%s} pendingDisconn: %s", ctx, pendingDisconn);
LOG.log(Level.FINEST, msg);
msg = String.format("{%s} pendingTxDisconn: %s", ctx, pendingTxDisconn);
LOG.log(Level.FINEST, msg);
}
}
return logical.of(isConn);
}
/**
* Create an alias for a logical database name. The alias may not conflict
* with an existing logical database name (except for that of the database
* which it aliases). The alias will be created regardless of whether the
* target database currently is connected; however, an error will be
* raised (or silently registered) in the event it is not, after the
* alias has been created. The alias persists until explicitly deleted
* with one of the variants of the <code>deleteAlias</code> method, or
* until the current session ends.
*
* @param alias
* Alias to be created.
* @param ldbName
* Logical name of the database to which the alias is assigned.
*
* @throws PersistenceException
* if the alias conflicts with an existing logical database name,
* other than <code>ldbName</code>.
* @throws ErrorConditionException
* if the target database is not connected, and we are not in
* silent error mode.
*/
private void createAliasImpl(String alias, String ldbName)
throws PersistenceException
{
// Alias cannot be an existing logical database name (except for the
// one it aliases).
if (connected.containsKey(alias) && !alias.equalsIgnoreCase(ldbName))
{
throw new PersistenceException(
"Alias " + alias + " conflicts with a database name",
389);
}
aliases.put(alias, ldbName);
// Logical database name should already exist. Otherwise, it is an
// error. However, if in silent error mode, the alias will still be
// created (note that this is different than silent error mode for
// most other statements).
if (!connected.containsKey(ldbName))
{
ErrorManager.recordOrThrowError(
1660,
"Logical dbname " + ldbName + " must be connected in order to add alias " + alias);
}
}
/**
* Delete the specified alias immediately.
*
* @param alias
* Alias to be deleted.
*/
private void deleteAliasImpl(String alias)
{
aliases.remove(alias);
}
/**
* Retrieve the alias at the specified index in the current context.
*
* @param index
* One-based index of the alias (aliases are stored in the order
* in which they were created).
*
* @return The target alias, or unknown value if <code>index</code> is
* invalid.
*/
private character aliasImpl(int index)
{
String alias = iterateToIndex(aliases.keySet(), index);
return alias == null ? new character() : new character(alias);
}
/**
* Implementation of the <code>DBPARAM()</code> Progress function.
* Returns, as a character string, a comma-separated list of the parameters
* used to connect to the database.
*
* <p>The method does a lookup on the database's name, or id it is null, the
* alias will extracted using the index.
*
* <p>Parameters returned by this method generally will not match those
* returned by the original 4GL implementation, since we don't support any
* others than those handled by <code>ConnectionManager.connect()</code>.
*
* @param nameOrAlias
* Logical database name or alias.
*
* @param index
* One-based index of the alias (aliases are stored in the order
* in which they were created).
*
* @return A comma-separated list of the parameters used to connect to the database, or
* unknown value if {@code nameOrAlias} or {@code index} cannot be resolved to a
* database.
*/
private character dbParamImpl(String nameOrAlias, int index)
{
if (nameOrAlias == null)
{
nameOrAlias = iterateToIndex(connected.keySet(), index);
if (nameOrAlias == null)
{
return new character(); // returning unknown value
}
}
// Get the logical DB name.
String ldb = getLDBName(nameOrAlias);
ConnectInfo info = connected.get(ldb);
if (info == null)
{
return new character();
}
List<Map.Entry<ConnectOption, ConnectOptionValue>> opts = info.opts;
if (opts != null)
{
return new character(dbparam(opts));
}
Database db = info.database;
StringBuilder sb = new StringBuilder();
sb.append("-db ").append(db.getName());
sb.append(",-dt ").append(ConnectionManager.dbType(nameOrAlias));
sb.append(",-ld ").append(nameOrAlias);
if (!db.isLocal())
{
sb.append(",-H ").append(db.getSocketAddress().getHostName());
sb.append(",-S ").append(db.getSocketAddress().getPort());
}
String userID = connectTimeUserid.get(ldb);
if (userID != null && !userID.isEmpty())
{
sb.append(",-U ").append(userID).append(",-P");
}
if (singleClient)
{
sb.append(",-1");
}
// -B 200
// -L 10000
// any other connection params not supported (yet)
return new character(sb.toString());
}
/**
* Get a description of restrictions for the table in the connected database with a specified
* alias or at the specified index. Currently, <code>table</code> is ignored.
*
* @param nameOrAlias
* Logical database name or alias.
*
* @param index
* One-based index of the alias (aliases are stored in the order
* in which they were created).
* @param table
* table name (currently ignored)
*
* @return a description of restrictions for the table in the connected database
*/
private character dbRestrictionsImpl(String nameOrAlias, int index, String table)
{
if (nameOrAlias == null)
{
nameOrAlias = iterateToIndex(connected.keySet(), index);
if (nameOrAlias == null)
{
return new character(); // returning unknown value
}
}
// Get the logical DB name.
String ldb = getLDBName(nameOrAlias);
ConnectInfo info = connected.get(ldb);
if (info == null)
{
return new character();
}
List<Map.Entry<ConnectOption, ConnectOptionValue>> opts = info.opts;
if (opts != null)
{
boolean readOnly = opts.stream().map(Entry::getKey).anyMatch(co -> co == ConnectOption.READ_ONLY_DB);
return readOnly ? new character("READ-ONLY") : new character("");
}
return new character();
}
/**
* Convert CONNECT options to a DBPARAM string.
*
* @param opts
* CONNECT options.
*
* @return DBPARAM string.
*/
private static String dbparam(List<Map.Entry<ConnectOption, ConnectOptionValue>> opts)
{
return opts.stream()
.map(e -> e.getKey().getOption() +
(e.getKey().isFlag() || e.getKey() == ConnectOption.PASSWORD
? ""
: " " + e.getValue().value().orElse("")))
.collect(Collectors.joining(","));
}
/**
* Get the version number of the target database.
*
* @param name
* Logical name or alias of the target database. If <code>null</code>, then
* <code>index</code> is used instead.
* @param index
* One-based index of the target database among all databases currently connected in
* this context.
*
* @return The database's version, as extracted from runtime configuration, or unknown value
* if <code>index</code> represents the unknown value or is outside of the range of
* connected databases.
*
* @throws RuntimeException
* if there is an error binding to the directory service to retrieve runtime
* configuration information.
*/
private character dbVersionImpl(String name, int index)
{
String ldb = null;
if (name == null)
{
// if index is invalid, return unknown value
if (index < 1 || index > connected.size())
{
return (new character());
}
ldb = iterateToIndex(connected.keySet(), index);
}
else
{
ldb = getLDBName(name);
// if name is not recognized as a logical name or alias, return unknown value
if (ldb == null)
{
return (new character());
}
}
ConnectInfo info = connected.get(ldb);
// if logical database is not connected, return unknown value
if (info == null)
{
return (new character());
}
Database database = info.database;
String key = database.getId();
String version = null;
// check cache first
synchronized (dbVersionCache)
{
version = dbVersionCache.get(key);
}
if (version != null)
{
return (new character(version));
}
// check for dbversion value in the directory
DirectoryService ds = DirectoryService.getInstance();
if (!ds.bind())
{
throw new RuntimeException("Directory bind failed");
}
try
{
String nodeID = Utils.findDirectoryNodePath(ds, "database", "container", false);
if (nodeID != null)
{
// check directory for database version for this database
String path = String.format("%s/%s/p2j/dbversion", nodeID, database.getId());
version = ds.getNodeString(path, "value");
// if nothing found, use default (note: Progress appears to return only the major
// version, no minor version info)
if (version == null)
{
version = DEFAULT_DBVERSION;
}
}
}
finally
{
ds.unbind();
}
// update the cache
synchronized (dbVersionCache)
{
String v = dbVersionCache.get(key);
if (v != null)
{
version = v;
}
else
{
dbVersionCache.put(key, version);
}
}
return (new character(version));
}
/**
* Update the various housekeeping data structures to reflect a successful connection event,
* and update connection metadata if it is in use.
*
* @param ldbName
* Logical name of connected database.
* @param database
* Object describing physical database.
* @param userId
* Current user's ID.
* @param tenantName
* The name of the tenant.
*/
private void putConnected(String ldbName, Database database, String userId, String tenantName)
{
long connectID;
synchronized (ConnectionManager.class)
{
connectID = ++nextConnectID;
}
ConnectInfo info = new ConnectInfo();
info.connectID = new int64(connectID);
info.database = database;
// the first database to be connected gets the DICTDB alias
if (connected.isEmpty())
{
aliases.put(DICTDB, ldbName);
}
ConnectInfo oldCI = connected.put(ldbName, info);
ldbMap.put(database, ldbName);
tenantsByDatabase.put(ldbName, tenantName);
if (userId == null)
{
// userId will be null for a local database; this call has to occur after adding the LDB
// name to the connected map
userId = SecurityOps.getDefaultUserid(ldbName);
}
userIds.put(ldbName, userId);
// locked.add(ldbName);
if (dispatcher != null)
{
// update connection metadata
dispatcher.connected(info);
}
}
/**
* Update the various housekeeping data structures to reflect a disconnection event, and update
* connection metadata if it is in use.
*
* @param ldbName
* Logical name of disconnected database.
* @param sessionCleanup
* <code>true</code> if this action is being taken as part of cleanup for a session
* that is terminating; <code>false</code> if a normal disconnect.
*
* @return database being removed
*/
private Database removeConnected(String ldbName, boolean sessionCleanup)
{
ConnectInfo info = connected.remove(ldbName);
// if database that was disconnected had DICTDB alias, reset the alias to the next oldest
// connected database, if any
String dictdb = aliases.get(DICTDB);
if (dictdb != null && dictdb.equals(ldbName))
{
Iterator<String> iter = connected.keySet().iterator();
if (iter.hasNext())
{
String nextLdbName = iter.next();
aliases.put(DICTDB, nextLdbName);
}
else
{
aliases.remove(DICTDB);
}
}
Database database = info.database;
ldbMap.remove(database);
userIds.remove(ldbName); // keep in sync
clients.remove(ldbName); // keep in sync
locked.remove(ldbName); // keep in sync
connectTimeUserid.remove(ldbName); // keep in sync
if (dispatcher != null && !sessionCleanup)
{
// update connection metadata
dispatcher.disconnected(info);
}
return database;
}
/**
* Get the version number of the target database.
*
* @param name
* Logical name or alias of the target database. If <code>null</code>, then
* <code>index</code> is used instead.
* @param index
* One-based index of the target database among all databases currently connected in
* this context.
*
* @return The database's transaction id, or {@code unknown} value if {@code name} is not a
* valid database name or {@code index} is outside of the range of connected
* databases.
*
* @throws RuntimeException
* if there is an error binding to the directory service to retrieve runtime
* configuration information.
*/
private integer dbTaskIdImpl(String name, int index)
{
String ldb = null;
if (name == null)
{
if (index < 1 || index > connected.size())
{
// if index is invalid, return unknown value
return new integer();
}
ldb = iterateToIndex(connected.keySet(), index);
}
else
{
ldb = getLDBName(name);
if (ldb == null)
{
// if name is not recognized as a logical name or alias, return unknown value
return new integer();
}
}
ConnectInfo info = connected.get(ldb);
if (info == null)
{
// if logical database is not connected, return unknown value
return new integer();
}
return new integer(TransactionManager.getTransactionId(info.database.getId()));
}
/**
* Information about a specific database connection (in 4GL terms, not JDBC terms).
*/
private static class ConnectInfo
{
/** Unique connect ID for this connection */
int64 connectID;
/** Object describing the connected database */
Database database;
/** CONNECT statement options */
List<Map.Entry<ConnectOption, ConnectOptionValue>> opts;
/**
* Get a string representation of this object, primarily for logging/debug purposes.
*
* @return String describing this object.
*/
public String toString()
{
return database.toString() + ":" + connectID;
}
}
/**
* This class dispatches connect and disconnect events to the listeners which manage related connection
* and user statistics metadata. One instance is created per {@code ConnectionManager} instance.
*/
private static class ConnectionDispatcher
{
/** Map of primary database to {@code ConnectionListener} objects */
private final Map<Database, ConnectionListener> listeners = new HashMap<>();
/** Set of connection info, maintained for context cleanup purposes */
private final Set<ConnectInfo> connections = new HashSet<>();
/**
* Manage a connection event by dispatching it to the appropriate listener.
*
* @param info
* Information about the connection to the associated, primary database.
*/
void connected(ConnectInfo info)
{
Database database = info.database;
if (database.isLocal())
{
connections.add(info);
ConnectionListener listener = getListener(database);
if (listener != null)
{
listener.databaseConnected(info.connectID);
}
}
}
/**
* Manage a disconnection event by dispatching it to the appropriate listener.
*
* @param info
* Information about the disconnection from the associated, primary database.
*/
void disconnected(ConnectInfo info)
{
if (connections.remove(info))
{
fireDisconnected(info);
}
}
/**
* This method is called during context cleanup to ensure that any connection listeners which might
* still be active at this time are notified about the disconnection event.
*/
void cleanup()
{
for (ConnectInfo info : connections)
{
fireDisconnected(info);
}
connections.clear();
}
/**
* Fire a disconnection event to the appropriate listener.
*
* @param info
* Information about the disconnection from the associated, primary database.
*/
private void fireDisconnected(ConnectInfo info)
{
ConnectionListener listener = getListener(info.database);
if (listener != null)
{
listener.databaseDisconnected(info.connectID);
}
}
/**
* Get the connection listener for the given, primary database.
*
* @param database
* Primary database.
*
* @return Connection listener associated with the given, primary database.
*/
private ConnectionListener getListener(Database database)
{
return listeners.computeIfAbsent(database, ConnectTableUpdater::new);
}
}
/**
* CONNECT statement options.
*/
private static enum ConnectOption
{
/* Single-user Mode*/
SINGLE_USER("-1", logical.class),
/* AdminServer Port*/
ADMIN_PORT("-adminport", integer.class),
/* Specifies the number of after-image buffers when running AIW*/
AI_BUFFS("-aibufs", integer.class),
/* Suspends database activity when an empty after-image (AI) file is unavailable*/
AFTER_IMAGE_STALL("-aistall", logical.class),
/* Specifies the number of blocks in the database buffers*/
BUFFERS("-B", integer.class),
/* Specifies the number of before-image buffers when running BIW*/
AFTER_IMAGE_BUFFERS("-bibufs", integer.class),
/* Sends a message to the log file when the recovery log threshold is reached.
* Use with -bithold*/
THRESHOLD_STALL("-bistall", logical.class),
/* Specifies the maximum size of the recovery log files*/
REC_LOG_THRESHOLD("-bithold", integer.class),
/* Requests a number of private read-only buffers*/
PRIVATE_RO_BUFFERS("-Bp", integer.class),
/* Requests a maximum number of private buffers*/
MAX_PRI_BUFFERS("-Bpmax", integer.class),
/* Specifies the number of index place holders or cursors*/
INDEX_CURSORS("-c", integer.class),
/* Reads the database schema from a local file instead of the database*/
SCHEMA_CAHE_FILE("-cache", Text.class),
/* Identifies the Java classpath to use when starting an SQL server*/
JAVA_CLASSPATH("-classpath", Text.class),
/* Changes the maximum number of index levels*/
CURSOR_SIZE("-cs", integer.class),
/* Max connection retries */
MAX_CONN_RETRIES("-ct", integer.class),
/* The name of the DataService to connect through a NameServer to an ODBC, ORACLE, or SQL Server */
DATA_SERVICE("-DataService", Text.class),
/* Physical Database Name.*/
DATABASE("-db", Text.class),
/* Use Direct I/O to open all files in unbuffered mode*/
DIRECT_IO("-directio", logical.class),
/* DataServer parameters*/
DATASERVER("-Dsrv", Text.class),
/* Identifies the database type*/
DB_TYPE("-dt", Text.class),
/* Changes the number (soft limit) of entries in the schema field cache*/
SCHEMA_FLD_CACHE_SIZE("-fc", integer.class),
/* Specifies the number of seconds before OpenEdge reuses a before-image cluster*/
BI_CLUSTER_SIZE("-G", integer.class),
/* Specifies the number of milliseconds a transaction waits before committing*/
GROUP_DELAY("-groupdelay", integer.class),
/* Identifies a remote host*/
HOSTNAME("-H", Text.class),
/* Specifies the number of hash table entries for the buffer pool*/
HASH("-hash", integer.class),
/* No Crash Protection*/
NO_INTEGRITY("-i", logical.class),
/* Lock Table Entries*/
NUMBER_LOCKS("-L", integer.class),
/* Assigns the logical database name*/
LOGICAL_DBNAME("-ld", Text.class),
/* Lock Timeout in seconds*/
LOCK_TIMEOUT("-lkwtmo", integer.class),
/* Specifies the message buffer size for network client/server protocols*/
MESSAGE_BUFFER_SIZE("-Mm", integer.class),
/* Network type*/
NETWORK_TYPE("-N", Text.class),
/* Number of Users*/
NUMBER_OF_USERS("-n", integer.class),
/* Password*/
PASSWORD("-P", Text.class),
/* Parameter file*/
PARAMETER_FILE("-pf", Text.class, false),
/* Pin Shared Memory */
PIN_SHARED_MEM("-pinshm", logical.class),
/* Identifies the properties file an AdminServer uses when File starting a database server
* or servergroup*/
CONFIF_PROPS_FILE("-properties", Text.class),
/* Open a database for read-only access*/
READ_ONLY_DB("-RO", logical.class),
/* Service name or port number*/
SERVICE_NAME("-S", Text.class),
/* Identifies a logical collection of server processes to start*/
SERVER_GROUP("-servergroup", Text.class),
/* Identifies a logical collection of server processes to start*/
SPIN_LOCK_TRIES("-spin", integer.class),
/* Identifies the directory or library containing the ABL triggers for a database*/
TRIGGER_LOCATION("-trig", Text.class),
/* Specifies the user ID*/
USERID("-U", Text.class);
/** option name */
private final String option;
/** option type */
private final Class<?> type;
/** option case-sensitivity */
private final boolean caseSensitive;
/**
* Constructor
*
* @param option
* option name.
* @param type
* option type.
* @param caseSensitive
* option case-sensitivity. This allows for options that are case-insensistive, whereas
* previously all were case-sensisitive..
*/
private ConnectOption(String option, Class<?> type, boolean caseSensitive)
{
this.option = option;
this.type = type;
this.caseSensitive = caseSensitive;
}
/**
* Constructor
*
* @param option
* option name.
* @param type
* option type.
*/
private ConnectOption(String option, Class<?> type)
{
// connectOption enum without the 3rd parameter defaults caseSensitive to true
this(option, type, true);
}
/**
* Check if option type is Text
*
* @return <code>true</code> if option type is Text
*/
public boolean isText()
{
return Text.class.equals(type);
}
/**
* Check if option type is logical
*
* @return <code>true</code> if option type is logical (no value)
*/
public boolean isFlag()
{
return logical.class.equals(type);
}
/**
* Get option name.
*
* @return option name.
*/
public String getOption()
{
return option;
}
/**
* Get option type.
*
* @return option type.
*/
public Class<?> getType()
{
return type;
}
/**
* Return CONNECT_OPTION by name.
*
* @param s
* option name
*
* @return CONNECT_OPTION or <code>null</code> if not found.
*/
public static ConnectOption of(String s)
{
return Arrays.stream(values()).filter(o -> o.caseSensitive ? s.equals(o.option) : s.toLowerCase().equals(o.option)).findFirst().orElse(null);
}
}
/**
* Connection option value - base class
*/
private static abstract class ConnectOptionValue
{
/**
* Get type of the option.
*
* @return type of the option
*/
public abstract Class<?> type();
/**
* Get string value of the option if option has value.
*
* @return string value of the option or Optional.empty() if option has no value
*/
public abstract Optional<String> value();
/**
* Get integer value of the option if option has numeric value.
*
* @return integer value of the option if option has numeric value
*/
public int intValue()
{
throw new IllegalAccessError();
}
/**
* Get boolean value of the option if option has boolean value.
*
* @return boolean value of the option if option has boolean value
*/
public boolean boolValue()
{
throw new IllegalAccessError();
}
/**
* Get text value of the option if option has text value.
*
* @return text value of the option if option has text value
*/
public String textValue()
{
throw new IllegalAccessError();
}
/**
* Create ConnectOptionValue instance for an option value.
*
* @param value
* option value
*
* @return ConnectOptionValue instance for an option value.
*/
public static ConnectOptionValue option(Object value)
{
if (value instanceof String)
{
return new TextConnectOptionValue((String)value);
}
if (value instanceof Boolean)
{
return new BoolConnectOptionValue((Boolean)value);
}
if (value instanceof Integer)
{
return new IntConnectOptionValue((Integer)value);
}
throw new IllegalArgumentException();
}
}
/**
* Numeric connection option value.
*/
public static class IntConnectOptionValue
extends ConnectOptionValue
{
/** option value */
private final int value;
/**
* Constructor.
*
* @param value
* option value.
*/
public IntConnectOptionValue(int value)
{
this.value = value;
}
/**
* Get type of the option.
*
* @return type of the option
*/
@Override
public Class<?> type()
{
return int.class;
}
/**
* Get integer value of the option.
*
* @return integer value of the option
*/
@Override
public int intValue()
{
return value;
}
/**
* Get string value of the option wrapped in Optional.
*
* @return string value of the option wrapped in Optional
*/
@Override
public Optional<String> value()
{
return Optional.of(String.valueOf(value));
}
/**
* Returns a string representation of the object.
*
* @return a string representation of the object.
*/
@Override
public String toString()
{
return "IntConnectOptionValue [value=" + value + "]";
}
}
/**
* Logical connection option value.
*/
public static class BoolConnectOptionValue
extends ConnectOptionValue
{
/** option value */
private final boolean value;
/**
* Constructor.
*
* @param value
* option value.
*/
public BoolConnectOptionValue(boolean value)
{
this.value = value;
}
/**
* Get type of the option.
*
* @return type of the option
*/
@Override
public Class<?> type()
{
return boolean.class;
}
/**
* Get boolean value of the option.
*
* @return boolean value of the option
*/
@Override
public boolean boolValue()
{
return value;
}
/**
* Get string value of the option wrapped in Optional.
*
* @return Optional.empty() as logical options have no value.
*/
@Override
public Optional<String> value()
{
return Optional.empty();
}
/**
* Returns a string representation of the object.
*
* @return a string representation of the object.
*/
@Override
public String toString()
{
return "BoolConnectOptionValue [value=" + value + "]";
}
}
/**
* Text connection option value.
*/
public static class TextConnectOptionValue
extends ConnectOptionValue
{
/** option value */
private final String value;
/**
* Constructor.
*
* @param value
* option value.
*/
public TextConnectOptionValue(String value)
{
this.value = value;
}
/**
* Get type of the option.
*
* @return type of the option
*/
@Override
public Class<?> type()
{
return String.class;
}
/**
* Get text value of the option.
*
* @return text value of the option
*/
@Override
public String textValue()
{
return value;
}
/**
* Returns a string representation of the object.
*
* @return a string representation of the object.
*/
@Override
public String toString()
{
return "TextConnectOptionValue [value=" + value + "]";
}
/**
* Get string value of the option wrapped in Optional.
*
* @return string value of the option wrapped in Optional
*/
@Override
public Optional<String> value()
{
return Optional.of(value);
}
}
/**
* CONNNECT statement options' parser
*/
private static class OptionsParser
{
/** parsed options for all databases */
private final List<List<Map.Entry<ConnectOption, ConnectOptionValue>>> dboptions = new ArrayList<>();
/** processed configuration files */
private final Set<String> pfs = new HashSet<>();
/** parsed options for current database */
private List<Map.Entry<ConnectOption, ConnectOptionValue>> options = new ArrayList<>();
/** database name found flag */
private boolean dbnameFound = false;
/**
* Constructor.
*
* @param dbnameFound
* database name found flag
*/
private OptionsParser(boolean dbnameFound)
{
this.dboptions.add(options);
this.dbnameFound = dbnameFound;
}
/**
* Parse options
*
* @param inlineArgs
* inline options
*
* @return List of all options in lists of entries
*
* @throws PersistenceException
* if a recursive use of a configuration file was detected.
*/
public List<List<Map.Entry<ConnectOption, ConnectOptionValue>>> parse(List<Object> inlineArgs)
throws PersistenceException
{
Iterator<Object> iter = inlineArgs.iterator();
ConnectOption opt = null;
boolean wasBadOption = false;
boolean noValueExpected = false;
while (iter.hasNext())
{
String arg = ParamFileReader.removeEnclosingQuotes(iter.next().toString());
if (arg.startsWith("-"))
{
opt = ConnectOption.of(arg);
wasBadOption = false;
noValueExpected = false;
if (opt == null)
{
ErrorManager.recordOrShowError(
1402,
String.format("Warning: %s is not a database parameter and was ignored.", arg),
false,
false,
false);
wasBadOption = true;
continue;
}
if (opt.isFlag())
{
options.add(new AbstractMap.SimpleEntry<>(opt, ConnectOptionValue.option(true)));
noValueExpected = true;
continue;
}
if (!iter.hasNext())
{
ErrorManager.recordOrShowError(
1403,
String.format("You have not supplied a parameter for argument %s.", arg),
false,
false,
true);
return null;
}
}
else
{
if (wasBadOption)
{
wasBadOption = false;
continue;
}
if (opt == null && !dbnameFound)
{
options.add(
new AbstractMap.SimpleEntry<>(ConnectOption.DATABASE, ConnectOptionValue.option(arg)));
dbnameFound = true;
continue;
}
if (opt == null || noValueExpected)
{
ErrorManager.recordOrShowError(
301,
String.format("Could not recognize argument: %s.", arg),
false,
true,
true);
return null;
}
if (opt == ConnectOption.PARAMETER_FILE)
{
parse(arg);
continue;
}
if (opt == ConnectOption.DATABASE)
{
if (dbnameFound)
{
options = new ArrayList<>();
dboptions.add(options);
}
dbnameFound = true;
}
Object optValue = opt.isText() ? arg : Integer.parseInt(arg);
options.add(new AbstractMap.SimpleEntry<>(opt, ConnectOptionValue.option(optValue)));
}
}
return dboptions;
}
/**
* Parse -pf file options
*
* @param pfFile
* -pf file path
*
* @throws PersistenceException
* if a recursive use of a configuration file was detected.
*/
public void parse(String pfFile)
throws PersistenceException
{
if (pfs.contains(pfFile))
{
LOG.warning("Recursive use of the configuration file " + pfFile);
return;
}
pfs.add(pfFile);
String pfContent = ParamFileReader.readPfFile(pfFile);
if (pfContent == null)
{
ErrorManager.recordOrShowError(1247,
String.format("Unable to open parameter file %s, errno 2.", pfFile),
false);
return;
}
String[] args = pfContent.split(" ");
List<Object> pfFileArgs = new ArrayList<>();
for (int i = 0; i < args.length; i++)
{
String txt = args[i];
if (txt.length() > 0)
{
pfFileArgs.add(txt);
}
}
parse(pfFileArgs);
}
}
}