# DirtyShareMultiplexerImpl.java

    /*
    ** Module   : DirtyShareMultiplexerImpl.java
    ** Abstract : Handles server-to-server requests to manage the information
    **            about uncommitted modifications to database records.
    **
    ** Copyright (c) 2004-2024, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- --JPRM-- --------------------------------Description-------------------------------------
    ** 001 SVL 20080605   @38559 Created initial version. Handles server-to-
    **                           server requests to manage the information
    **                           about uncommitted modifications.
    ** 002 ECF 20080612   @38727 Replaced isEntityDirty() with
    **                           getEntityDirtyState(). New method returns
    **                           state information (is entity dirty with
    **                           changes and/or deletes), rather than a simple
    **                           boolean.
    ** 003 CA  20080716   @39116 Renamed method lockUniqueIndexes to
    **                           lockAllIndexes - this will lock all the
    **                           indexes for a certain entity. Modified the
    **                           delete(...) and rollbackInsert(...) methods
    **                           so the index locking can be disabled.
    ** 004 ECF 20080815   @39547 Added support for global change events. These
    **                           are events that represent index-changing
    **                           inserts, updates, or deletes, which are
    **                           processed by sessions other than the session
    **                           in which the event originated.
    ** 005 ECF 20090513   @42170 Modified variant of update() method. Added lock
    **                           parameter to make index locking optional.
    ** 006 ECF 20090602   @42575 Added support for Progress isolation leak quirk.
    **                           Added updateSnapshots() method.
    ** 007 ECF 20090609   @42646 Modified cleanupChanges(). The list of entities
    **                           which have had their uncommitted changes leaked
    **                           is now passed in, allowing proper cleanup.
    ** 008 ECF 20090720   @43311 Changed delete() method signature. Now it throws
    **                           PersistenceException.
    ** 009 ECF 20131028          Import change.
    ** 010 ECF 20140825          Track unvalidated inserts.
    ** 011 OM  20160307          Added support for EXTENTs in update() methods. Updated signature of
    **                           getDirtyInfo().
    ** 012 ECF 20200901          New ORM implementation.
    ** 013 SVL 20230516          registerForGlobalEvents can return null now.
    ** 014 AL2 20230822          Removed readOnly parameter.
    ** 015 TJD 20240123          Java 17 compatibility updates
    ** 016 TJD 20230509          Extend interface to allow dirty DMO leaks and to update shared snapshots
    ** 017 AL2 20240903          Added integration with DirtyData.
    */
    /*
    ** 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.remote;

    import java.util.*;
    import com.goldencode.p2j.persist.*;
    import com.goldencode.p2j.persist.Record;
    import com.goldencode.p2j.persist.dirty.*;
    import com.goldencode.p2j.persist.event.*;
    import com.goldencode.p2j.persist.lock.*;
    import com.goldencode.p2j.persist.orm.*;
    import com.goldencode.p2j.net.RemoteObject;

    /**
     * A singleton instance of this class services server-to-server requests to
     * manage the information about uncommitted modifications to database records.
     * This instance is registered as a remote object and serves dirty share
     * management requests for one or more databases which the server
     * authoritatively manages.
     * <p>
     * Service requests may originate from more than one external server, so this
     * implementation is threadsafe.
     *
     * @author SVL
     */
    public final class DirtyShareMultiplexerImpl
    extends DatabaseMultiplexer<DirtyShareManager>
    implements DirtyShareMultiplexer
    {
       /** Singleton instance of this class */
       private static DirtyShareMultiplexer instance = null;

       /**
        * Initialize the dirty share multiplexer for this server instance. This
        * method is to be called during server bootstrap, but not before the local
        * database manager has been initialized.
        * <p>
        * This method enforces the singleton pattern.
        *
        * @throws  IllegalStateException
        *          if this method is invoked more than once.
        */
       public static synchronized void initialize()
       {
          if (instance != null)
          {
             throw new IllegalStateException(
                "Dirty share multiplexer already initialized for this process");
          }

          instance = new DirtyShareMultiplexerImpl();
          RemoteObject.registerNetworkServer(DirtyShareMultiplexer.class, instance);
       }

       /**
        * Default constructor which initializes the resources needed for this
        * multiplexer to service dirty share manager requests.
        */
       private DirtyShareMultiplexerImpl()
       {
          super();
       }

       /**
        * Register with a global event clearinghouse an interest in insert,
        * delete, and update events which affect the given DMO entity.
        *
        * @param   database
        *          Value that uniquely identifies the target database on the
        *          remote server.
        * @param   entity
        *          DMO entity for which the caller is interested in tracking
        *          change.
        *
        * @return  A unique identifier for the registrant, assigned by the event
        *          clearinghouse, used later to collect events and deregister or
        *          <code>null</code> if global events are disabled.
        *
        * @see     #getGlobalEvents(int, long, long)
        * @see     #deregisterForGlobalEvents(int, long)
        */
       public Long registerForGlobalEvents(int database, String entity)
       {
          return lookupWorker(database).registerForGlobalEvents(entity);
       }

       /**
        * Deregister with a global event clearinghouse an interest in insert,
        * delete, and update events which affect a particular DMO type.
        *
        * @param   database
        *          Value that uniquely identifies the target database on the
        *          remote server.
        * @param   registerID
        *          A unique identifier for the registrant being deregistered.
        *
        * @see     #getGlobalEvents(int, long, long)
        * @see     #registerForGlobalEvents(int, String)
        */
       public void deregisterForGlobalEvents(int database, long registerID)
       {
          lookupWorker(database).deregisterForGlobalEvents(registerID);
       }

       /**
        * Retrieve any new events that have been collected by a global event
        * clearinghouse, representing inserts, deletes, and updates made to
        * record types for which the caller previously has registered interest.
        *
        * @param   database
        *          Value that uniquely identifies the target database on the
        *          remote server.
        * @param   registerID
        *          A unique identifier for an object which has registered interest
        *          in DMO changing events.
        * @param   lastID
        *          A unique identifier representing the last event retrieved by
        *          the caller.  If less than 0, it is assumed no events have yet
        *          been retrieved since the caller registered interest in these
        *          events.
        *
        * @return  Array of applicable, new events, or <code>null</code> if no
        *          such events have been collected.
        *
        * @throws  EventRegistrationException
        *          if <code>registerID</code> is invalid or has expired.
        */
       public GlobalChangeEvent[] getGlobalEvents(int database, long registerID, long lastID)
       throws EventRegistrationException
       {
          return lookupWorker(database).getGlobalEvents(registerID, lastID);
       }

       /**
        * Lock or unlock all indexes associated with the given DMO entity.
        *
        * @param   database
        *          Value that uniquely identifies the target database on the
        *          remote server.
        * @param   entity
        *          Name of an entity, which is the name of the DMO implementation
        *          class associated with a table being tracked for uncommitted
        *          changes.
        * @param   lockType
        *          Type of lock to acquire, or <code>NONE</code> to release
        *          existing locks.  <code>NO_WAIT</code> variants will be treated
        *          as regular, blocking variants.
        *
        * @throws  PersistenceException
        *          if there is an error querying JDBC metadata while retrieving
        *          index information.
        */
       public void lockAllIndexes(int database, String entity, int lockType)
       throws PersistenceException
       {
          LockType lt = LockType.fromInt(lockType);
          lookupWorker(database).lockAllIndexes(entity, lt);
       }

       /**
        * Begin tracking a newly inserted record in the dirty database.
        * <p>
        * This record is removed when the current transaction is committed or rolled back.
        *
        * @param   database
        *          Value that uniquely identifies the target database on the remote server.
        * @param   entity
        *          Name of an entity, which is the name of the DMO implementation class associated with a table
        *          being tracked for uncommitted changes.
        * @param   copy
        *          Record to be tracked.
        * @param   indexState
        *          The index update state of the record at the time of insertion.
        * @param   lock
        *          {@code true} to acquire write locks to insert the record; {@code false} to assume write locks
        *          are acquired by calling code.
        * @param   dirtyData
        *          The data that was changed and caused this update. This is explicitly sent
        *          because sometimes we can't infer the dirty props from the DMO anymore.
        *
        * @throws  PersistenceException
        *          if there is any database error.
        */
       @Override
       public void insert(int database, String entity, Record copy, IndexState indexState, boolean lock, DirtyData dirtyData)
       throws PersistenceException
       {
          lookupWorker(database).insert(entity, copy, indexState, lock, dirtyData);
       }

       /**
        * Rollback the insert of a record previously introduced with the {@link
        * #insert(int, String, Record, IndexState, boolean, DirtyData)} method.
        *
        * @param   database
        *          Value that uniquely identifies the target database on the
        *          remote server.
        * @param   ident
        *          Identifier for the target record, which encapsulates its entity
        *          name and primary key.
        * @param   lock
        *          <code>true</code> to acquire write locks;
        *          <code>false</code> to assume write locks are acquired by
        *          calling code.
        *
        * @throws  PersistenceException
        *          if there is any database error.
        */
       public void rollbackInsert(int database, RecordIdentifier<String> ident, boolean lock)
       throws PersistenceException
       {
          lookupWorker(database).rollbackInsert(ident, lock);
       }

       /**
        * Indicate whether the record specified by the given entity and primary
        * key is being tracked for changes within an uncommitted transaction.
        * This includes records which have been added to the dirty database due to
        * an insert or an update, not due to a delete.
        *
        * @param   database
        *          Value that uniquely identifies the target database on the
        *          remote server.
        * @param   entity
        *          Name of an entity, which is the name of the DMO implementation
        *          class associated with a table being tracked for uncommitted
        *          changes.
        * @param   id
        *          Primary key of the record to be checked.
        *
        * @return  <code>true</code> to indicate the target record is being
        *          tracked for changes, else <code>false</code>.
        */
       public boolean isTracked(int database, String entity, Long id)
       {
          return lookupWorker(database).isTracked(entity, id);
       }

       /**
        * Begin tracking a DMO for changes, based on the updates indicated by the
        * {@code properties}, {@code extIndexes} and {@code values} arguments.
        * <p>
        * Make a copy of the given record, apply the specified updates to that
        * copy, and store the copy in the dirty database.
        * <p>
        * This record is removed when the adding context's current transaction is
        * committed or rolled back.
        *
        * @param   database
        *          Value that uniquely identifies the target database on the remote server.
        * @param   entity
        *          Name of an entity, which is the name of the DMO implementation class associated with a table
        *          being tracked for uncommitted changes.
        * @param   dmo
        *          Record to be tracked.
        * @param   lock
        *          {@code true} to acquire write locks to update the record;
        *          {@code false} to assume write locks are acquired by calling code.
        * @param   dirtyData
        *          The data that was changed and caused this update. This is explicitly sent
        *          because sometimes we can't infer the dirty props from the DMO anymore.
        *
        * @throws  PersistenceException
        *          if there is any database error.
        */
       public void update(int database,
                          String entity,
                          Record dmo,
                          boolean lock,
                          DirtyData dirtyData)
       throws PersistenceException
       {
          lookupWorker(database).update(entity, dmo, lock, dirtyData);
       }

       /**
        * Retrieve a DMO which is being tracked in the dirty database, and apply the specified updates
        * to it, as provided by the {@code properties}, {@code extIndexes} and {@code values}
        * arguments. It is assumed the DMO already exists in the database, from a previous call to
        * {@link #update(int, String, Record, boolean, DirtyData)}.
        *
        * @param   database
        *          Value that uniquely identifies the target database on the remote server.
        * @param   entity
        *          Name of an entity, which is the name of the DMO implementation
        *          class associated with a table being tracked for uncommitted changes.
        * @param   id
        *          Primary key id of the record to be retrieved and updated.
        * @param   lock
        *          <code>true</code> to acquire write locks to update the record;
        *          <code>false</code> to assume write locks are acquired by calling code.
        * @param   dirtyData
        *          The data that was changed and caused this update. This is explicitly sent
        *          because sometimes we can't infer the dirty props from the DMO anymore.
        *
        * @throws  PersistenceException
        *          if there is any database error.
        */
       public void update(int database,
                          String entity,
                          Long id,
                          boolean lock,
                          DirtyData dirtyData)
       throws PersistenceException
       {
          lookupWorker(database).update(entity, id, false, lock, dirtyData);
       }

       /**
        * Indicate whether any uncommitted modification is being tracked for the
        * database table associated with the specified entity.  A modification is
        * the insertion, deletion, or update of a record.  Insertions and updates
        * considered the same for purposes of this method.
        *
        * @param   database
        *          Value that uniquely identifies the target database on the
        *          remote server.
        * @param   entity
        *          Name of an entity, which is the name of the DMO implementation
        *          class associated with a table being tracked for uncommitted
        *          changes.
        *
        * @return  Bitfield indicating dirty status of entity, using constants
        *          from {@link DirtyInfo}.  Zero return indicates entity is not
        *          dirty.  Only <code>CHANGE</code> and <code>DELETE</code> are
        *          detected, not <code>INSERT</code>.
        */
       public int getEntityDirtyState(int database, String entity)
       {
          return lookupWorker(database).getEntityDirtyState(entity);
       }

       /**
        * Indicate whether the record specified by the given entity and primary
        * key has been deleted within an uncommitted transaction in <i>any</i>
        * context.
        *
        * @param   database
        *          Value that uniquely identifies the target database on the
        *          remote server.
        * @param   entity
        *          Name of an entity, which is the name of the DMO implementation
        *          class associated with a table being tracked for uncommitted
        *          changes.
        * @param   id
        *          Primary key of the record to be checked.
        *
        * @return  <code>true</code> if the specified record has been deleted,
        *          else <code>false</code>.
        */
       public boolean isDeleted(int database, String entity, Long id)
       {
          return lookupWorker(database).isDeleted(entity, id);
       }

       /**
        * Mark a record as having been deleted in an uncommitted transaction, or
        * unmark a record which previously was so marked.
        * <p>
        * A record is unmarked when the adding context's current transaction is
        * committed or rolled back.
        *
        * @param   database
        *          Value that uniquely identifies the target database on the
        *          remote server.
        * @param   entity
        *          Name of an entity, which is the name of the DMO implementation
        *          class associated with a table being tracked for uncommitted
        *          changes.
        * @param   id
        *          Primary key of deleted record.
        * @param   rollback
        *          If <code>false</code>, the target record is marked as deleted;
        *          if <code>true</code>, it is unmarked.
        * @param   lock
        *          <code>true</code> to acquire write locks to delete the record;
        *          <code>false</code> to assume write locks are acquired by
        *          calling code.
        *
        * @throws  PersistenceException
        *          if there was an error deleting the record from the database.
        */
       public void delete(int database, String entity, Long id, boolean rollback, boolean lock)
       throws PersistenceException
       {
          lookupWorker(database).delete(entity, id, rollback, lock);
       }

       /**
        * Retrieve from the dirty database the record specified by the given
        * entity and primary key, if it exists.
        *
        * @param   database
        *          Value that uniquely identifies the target database on the
        *          remote server.
        * @param   entity
        *          Name of an entity, which is the name of the DMO implementation
        *          class associated with a table being tracked for uncommitted
        *          changes.
        * @param   id
        *          Primary key of the record to be retrieved.
        *
        * @return  Target record if it exists in the dirty database, else
        *          <code>null</code>.
        *
        * @throws  PersistenceException
        *          if there is any database error.
        */
       public Record getDirtyDMO(int database, String entity, Long id)
       throws PersistenceException
       {
          return lookupWorker(database).getDirtyDMO(entity, id);
       }

       /**
        * Execute an HQL query and return the results as a list. If the query returns multiple results
        * per row, each element in the list will be an array of {@code Object}s.
        * <p>
        * No locking is attempted.
        *
        * @param   database
        *          Value that uniquely identifies the target database on the remote server.
        * @param   hql
        *          HQL query statement passed to Hibernate.  This method makes no
        *          assumptions as to the types of object(s) returned.
        * @param   params
        *          Substitution values for the query.  If none, this should be an empty array.
        * @param   maxResults
        *          The maximum number of elements to be returned in the list.
        *          If this value is non-positive, no upper limit is applied.
        * @param   startOffset
        *          The 0-based offset of the first record to retrieve.  If this
        *          value is non-positive, an offset of 0 is used by default.
        *
        * @return  A list of results, or {@code null} if no result was found.
        *
        * @throws  PersistenceException
        *          if there was an error executing the query.
        */
       @Override
       public <T> List<T> list(int database,
                               String hql,
                               Object[] params,
                               int maxResults,
                               int startOffset)
       throws PersistenceException
       {
          return lookupWorker(database).list(hql, params, maxResults, startOffset);
       }

       /**
        * Given certain search criteria and possibly the primary key of a potential DMO match in the
        * primary database, report any significant information currently being tracked for uncommitted
        * transactions, which might override data (or the lack thereof) found in the primary database.
        * <p>
        * If a {@link DirtyInfo} object is returned by this method, it will contain overriding
        * information, such as a dirty DMO which matched the search criteria, whether that record was
        * newly inserted, or the fact that the candidate record found in the primary search has been
        * deleted or modified in an uncommitted transaction.  A {@code null} return indicates no
        * overriding information was available.
        *
        * @param   database
        *          Value that uniquely identifies the target database on the remote server.
        * @param   entity
        *          Name of an entity, which is the name of the DMO implementation class associated
        *          with a table being tracked for uncommitted changes.
        * @param   candidateID
        *          Primary key ID of DMO, if any, found in the primary database, which matches the
        *          current query criteria.
        * @param   index
        *          UID of index which governs the ordering of data in the current query.
        * @param   statements
        *          One or more HQL statements to be executed against the dirty database to find a
        *          record which matches the current query criteria.  The list is in order of most
        *          specific to least specific search criteria.
        * @param   params
        *          Query substitution parameters which match placeholders in the most specific HQL
        *          statement.
        * @param   rowidLookup
        *          The predicate of the query does a lookup for ROWID/RECID of the table. In case of
        *          transient records they are accessible without the need of having a full matched
        *          index.
        *
        * @return  An object which summarizes the findings, or {@code null} if no uncommitted change
        *          information was available for the given arguments.
        *
        * @throws  PersistenceException
        *          if there was any database error.
        */
       @Override
       public DirtyInfo getDirtyInfo(int database,
                                     String entity,
                                     Long candidateID,
                                     int index,
                                     List<String> statements,
                                     Object[] params,
                                     boolean rowidLookup)
       throws PersistenceException
       {
          return lookupWorker(database).getDirtyInfo(entity, candidateID, index, statements, params, rowidLookup);
       }

       /**
        * Stop tracking uncommitted changes to the the subset of records
        * represented by <code>subChanges</code>, and remove from the master set
        * of uncommitted, inserted records the subset of records represented by
        * <code>subInserts</code>.
        *
        * @param   database
        *          Value that uniquely identifies the target database on the
        *          remote server.
        * @param   subChanges
        *          Sets of primary keys representing those records to be removed
        *          from uncommitted change tracking, mapped by entity name.
        * @param   subInserts
        *          A set of identifiers of those records to be removed from the
        *          master set of uncommitted inserts.
        *
        * @throws  PersistenceException
        *          if there is any database error.
        */
       public void cleanupChanges(int database,
                                  Map<String, Set<Long>> subChanges,
                                  Set<RecordIdentifier<String>> subInserts)
       throws PersistenceException
       {
          lookupWorker(database).cleanupChanges(subChanges, subInserts);
       }

       /**
        * Remove from the master set of uncommitted, deleted records the subset of
        * records represented by <code>subDeletes</code>.
        *
        * @param   database
        *          Value that uniquely identifies the target database on the
        *          remote server.
        * @param   subDeletes
        *          Sets of primary keys representing those records to be removed
        *          from uncommitted delete tracking, mapped by entity name.
        */
       public void cleanupDeletes(int database, Map<String, Set<Long>> subDeletes)
       {
          lookupWorker(database).cleanupDeletes(subDeletes);
       }

       /**
        * Get the dirty share manager which is associated with the given database.
        *
        * @param   database
        *          Physical database with which the returned worker is associated.
        *
        * @return  Worker object to which service requests associated with the
        *          given database must be delegated.
        */
       protected DirtyShareManager getWorker(Database database)
       {
          return DirtyShareFactory.getManagerInstance(database);
       }

       /**
        * Update DMO snapshot to be leaked to other sessions
        *
        * @param   database
        *          Value that uniquely identifies the target database on the
        *          remote server.
        * @param   entity
        *          Name of an entity, which is the name of the DMO implementation
        *          class associated with the table being created.
        * @param   dmos
        *          DMOs to be leaked, not NULL
        * @throws  PersistenceException
        *          if there is any database error.
        */
       @Override
       public void updateSnaphots(int database, String entity, Set<Record> dmos)
       throws PersistenceException
       {
          lookupWorker(database).updateSnapshots(entity, dmos);
       }
    }