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