TemporaryDatabaseManager.java
/*
** Module : TemporaryDatabaseManager.java
** Abstract : Handles the per-user temporary databases.
**
** Copyright (c) 2020-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
** 001 AIL 20200812 Created initial version.
** AIL 20200813 Added support for both private and shared temporary databases.
** 002 AIL 20210319 Disable undo log for primary temporary databse.
** ECF 20210927 Minor formatting.
** 003 AL2 20230112 Allow LOCK_MODE 0 for private temporary database.
** 004 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 005 DDF 20230620 Made FORCE_NO_UNDO_TEMP_TABLES non-final, renamed it.
** DDF 20230627 Made forceNoUndoTempTables accessible through a method.
** DDF 20230705 Used a standard handling for directory binding.
** 006 GBB 20230825 SecurityManager session methods calls updated.
** 007 DDF 20240325 ContextLocal of PrivateTempDbManager should have a lower weight level
** than Persistence, otherwise it can add it back to the token map
** during cleanup.
** 008 DDF 20240404 Removed weight level from context.
** 009 OM 20240909 Concurrent multitenancy functionality implementation.
** 010 DDF 20241002 When the temporary database is used in shared mode, prevent it from being closed,
** because it can be reused, added weight level (removed previously).
*/
/*
** 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.util.*;
import java.util.logging.*;
import com.goldencode.p2j.directory.DirectoryService;
import com.goldencode.p2j.persist.dialect.*;
import com.goldencode.p2j.persist.orm.*;
import com.goldencode.p2j.persist.remote.RemotePersistence;
import com.goldencode.p2j.security.*;
import com.goldencode.p2j.security.SecurityManager;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.logging.*;
/**
* Used for working with per-session temporary databases. The persistence framework
* should be independent from these; others should only know about the existence of the
* {@link DatabaseManager#TEMP_TABLE_DB}. The temporary database should be
* considered as per-session only in this manager in order to avoid leakage.
*/
public class TemporaryDatabaseManager
{
/** The dialect used for the temporary databases. */
private static Dialect dialect;
/** Flag indicating manager has been initialized */
private static boolean initialized = false;
/** Responsible for handling the operations on the temporary database*/
private static TempDbManager delegate;
/** Logger */
private static final CentralLogger log = CentralLogger.get(TemporaryDatabaseManager.class.getName());
/**
* Retrieve the URL used for the connection to {@link DatabaseManager#TEMP_TABLE_DB} temporary
* database.
*
* @return The settings used for the connection to the current per-session temporary database.
*/
public static Settings getMyTempDbSettings()
{
Settings settings = new Settings();
H2Helper.setCommonInMemoryProperties(getMyTempDbName(),
settings,
DatabaseManager.isForceNoUndoTempTables(),
getNoLock());
settings.put(Settings.FETCH_SIZE, "1024");
return settings;
}
/**
* Select a suitable delegate for handling the operations on the temporary database.
*/
static void initialize()
{
if (initialized)
{
throw new IllegalStateException("Persistence is already initialized");
}
DirectoryService ds = DirectoryService.getInstance();
if (ds != null)
{
if (!ds.bind())
{
throw new RuntimeException("Directory bind failed");
}
try
{
String privateTempDbs = Utils.findDirectoryNodePath(ds, "private-temp-dbs", "container", false);
if (privateTempDbs != null)
{
if (log.isLoggable(Level.INFO))
{
log.log(Level.INFO, "Using private (per-session) temporary databases.");
}
delegate = new PrivateTempDbManager();
}
else
{
if (log.isLoggable(Level.INFO))
{
log.log(Level.INFO, "Using public (shared) temporary database.");
}
delegate = new SharedTempDbManager();
}
}
finally
{
initialized = true;
ds.unbind();
}
}
}
/**
* Initialize the current temporary database.
*/
static void initializeMyTempDatabase()
{
if (delegate == null)
{
throw new IllegalStateException("Persistence was not initialized");
}
delegate.initializeMyTempDatabase();
}
/**
* Retrieve the dialect used for the current per-session temporary database.
* All such databases use the same dialect.
*
* @return The dialect used by the current per-session temporary database.
*/
static Dialect getMyTempDbDialect()
{
if (dialect == null)
{
Settings settings = new Settings();
settings.put(Settings.DIALECT, P2JH2Dialect.class.getName());
dialect = Dialect.getDialect(settings);
}
return dialect;
}
/**
* Remove the current temporary database.
*
* @return {@code true} if the associated {@link Database} can be removed as well.
* If {@code true} is returned, {@link PersistenceFactory#remove(Database)}
* will remove the temporary database from the cache.
*/
static boolean removeMyTempDatabase()
{
if (delegate == null)
{
throw new IllegalStateException("Persistence was not initialized");
}
return delegate.removeMyTempDatabase();
}
/**
* Check if the current database can be closed.
*
* @return {@code true} if the database can be closed. If {@code true} is returned,
* {@link PersistenceFactory#remove(Database)} will close the database
* as it is no longer used.
*/
static boolean isTempDatabaseCloseable()
{
if (delegate == null)
{
throw new IllegalStateException("Persistence was not initialized");
}
return delegate.isTempDatabaseCloseable();
}
/**
* Retrieve the name of the temporary database used by the current session.
*
* @return The name of the temporary database used by the current session.
*/
private static String getMyTempDbName()
{
if (delegate == null)
{
throw new IllegalStateException("Persistence was not initialized");
}
return delegate.getMyTempDbName();
}
/**
* Retrieve the lock mode specific to the delegate used. The shared database should
* work with internal locks, while the private database can avoid using locks.
*
* @return {@code true} if the delegate can avoid using internal locks.
*/
private static boolean getNoLock()
{
return delegate.getNoLock();
}
/**
* Used for defining a delegate responsible for managing the temporary databases.
*/
private interface TempDbManager
{
/**
* Initialize the current temporary database.
*/
void initializeMyTempDatabase();
/**
* Check if the current temporary database can avoid using locks.
*
* @return {@code true} if the delegate can avoid using locks.
*/
boolean getNoLock();
/**
* Retrieve the name of the temporary database used by the current session.
*
* @return The name of the temporary database used by the current session.
*/
String getMyTempDbName();
/**
* Remove the current temporary database.
*
* @return {@code true} if the associated {@link Database} can be removed as well.
* If {@code true} is returned, {@link PersistenceFactory#remove(Database)}
* will remove the temporary database from the cache.
*/
boolean removeMyTempDatabase();
/**
* Check if the current database can be closed.
*
* @return {@code true} if the database can be closed. If {@code true} is returned,
* {@link PersistenceFactory#remove(Database)} will close the database
* as it is no longer used.
*/
boolean isTempDatabaseCloseable();
}
/**
* Manager used for handling operations on multiple private temporary databases. This
* means that each session will be connected to a different database. Therefore, the connection
* string will differ depending on the session. Also, the initialization should be done for
* each physical database.
*/
private static class PrivateTempDbManager
implements TempDbManager
{
/** All of the per-user temporary databases initialized by now. */
private static final Set<String> perUserTempDatabases = new HashSet<String>();
/** Context local object to manage per-session temporary databases */
private static final ContextLocal<WorkArea> work = new ContextLocal<WorkArea>()
{
@Override
public WeightFactor getWeight() { return WeightFactor.LEVEL_2; }
protected WorkArea initialValue() { return new WorkArea(); }
};
/**
* Initialize the current per-session temporary database. Even though the
* {@link DatabaseManager#TEMP_TABLE_DB} is marked as initialized and eventually
* cached (see {@link PersistenceFactory#getInstance(Database)}), some per-session
* databases may be uninitialized. For this matter, make sure to initialize each
* per-session database separately.
*/
@Override
public void initializeMyTempDatabase()
{
synchronized (perUserTempDatabases)
{
String tempDbName = TemporaryDatabaseManager.getMyTempDbName();
if (!perUserTempDatabases.contains(tempDbName))
{
Database tempDb = DatabaseManager.TEMP_TABLE_DB;
Dialect dialect = getMyTempDbDialect();
if (dialect == null)
{
throw new IllegalArgumentException(
"Unable to determine database dialect for database " +
tempDb);
}
Persistence instance = (tempDb.isLocal()
? new Persistence(tempDb, dialect)
: new RemotePersistence(tempDb, dialect));
perUserTempDatabases.add(tempDbName);
}
}
}
/**
* Check if the current temporary database can avoid using locks.
*
* @return This is a private database, so there is only one user. Locking is not required.
*/
@Override
public boolean getNoLock()
{
return true;
}
/**
* Retrieve the name of the temporary database used by the current session.
* It is important to keep this under control in order to avoid leaking any of these
* per-user temporary databases names, which should be used exclusively in this manager.
*
* @return The name of the temporary database used by the current session.
*/
@Override
public String getMyTempDbName()
{
WorkArea wa = work.get();
return wa.getTempDBName();
}
/**
* Remove the current per-session temporary database.
*
* @return {@code true} if there are no more per-session temporary databases left.
* Return {@code true} will indicate the fact that no session needs the temporary
* database anymore, so the afferent {@code Database} can be removed from all caches.
*/
@Override
public boolean removeMyTempDatabase()
{
String tempDbName = getMyTempDbName();
synchronized (perUserTempDatabases)
{
perUserTempDatabases.remove(tempDbName);
return perUserTempDatabases.isEmpty();
}
}
/**
* Check if the current database can be closed. For temporary database that run
* in private mode, the return value is always {@code true} because the database
* is not reused.
*
* @return {@code true} if the database can be closed. If {@code true} is returned,
* {@link PersistenceFactory#remove(Database)} will close the database
* as it is no longer used.
*/
@Override
public boolean isTempDatabaseCloseable()
{
return true;
}
/**
* A class containing context-specific data.
*/
private static class WorkArea
implements Initializable
{
/** The security manager instance */
private static SecurityManager sm = SecurityManager.getInstance();
/** The session id of the current context */
private int sessionId;
/**
* Initialize the context.
*/
@Override
public void init()
{
sessionId = sm.sessionSm.getSessionId();
}
/**
* Compute the per-session temporary database name by concatenating the default temporary
* database schema name with the current session id.
*
* @return The name of the current per-session temporary database.
*/
String getTempDBName()
{
return DatabaseManager.TEMP_TABLE_SCHEMA + sessionId;
}
}
}
/**
* Manager used for handling operations on a single shared temporary database. This
* means that all sessions will connect to the same temporary database, so the connection
* URL is the same between sessions.
*/
private static class SharedTempDbManager
implements TempDbManager
{
/** Flag to indicate if the temporary database was initialized or not */
private boolean initialized = false;
/**
* Initialize the current temporary database only if it wasn't already initialized.
*/
@Override
public void initializeMyTempDatabase()
{
if (!initialized)
{
Database tempDb = DatabaseManager.TEMP_TABLE_DB;
Dialect dialect = getMyTempDbDialect();
if (dialect == null)
{
throw new IllegalArgumentException(
"Unable to determine database dialect for database " + tempDb);
}
Persistence instance = (tempDb.isLocal()
? new Persistence(tempDb, dialect)
: new RemotePersistence(tempDb, dialect));
initialized = true;
}
}
/**
* Check if the current temporary database can avoid using locks.
*
* @return This is a shared database, so locks are mandatory to ensure consistency.
*/
@Override
public boolean getNoLock()
{
return false;
}
/**
* Retrieve the name of the temporary database.
*
* @return The name of the temporary database.
*/
@Override
public String getMyTempDbName()
{
return DatabaseManager.TEMP_TABLE_SCHEMA;
}
/**
* Remove the temporary database.
*
* @return always {@code false} as there is only one physical temporary database
* which should not be removed because it is reused.
*/
@Override
public boolean removeMyTempDatabase()
{
return false;
}
/**
* Check if the current database can be closed. For the temporary database that runs
* in shared mode, the return value is always {@code false} because is is reused.
*
* @return {@code false} if the database can't be closed.
*/
@Override
public boolean isTempDatabaseCloseable()
{
return false;
}
}
}