# ChangeSet.java

    /*
    ** Module   : ChangeSet.java
    ** Abstract : Tracks a record's changes during a transaction to enable in-memory rollback of data and state
    **
    ** Copyright (c) 2020-2025, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
    ** 001 ECF 20200602 Created initial version. Tracks a record's changes during a transaction and rolls back
    **                  data and state at full and sub-transaction levels.
    **     ECF 20211230 Added missing javadoc.
    **     CA  20220308 In rollback(), allow any nested sub-tx changes to be applied.
    ** 002 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
    ** 003 CA  20240422 For a NEW temp record which was not flushed and is being deleted or rolled back in the
    **                  transaction, the allocated primary key row (which is an empty template) to reserve this
    **                  primary key, needs to be explicitly removed.
    ** 004 AL2 20240517 Don't remove CACHED flag if the DMO is still loaded.
    ** 005 TJD 20230303 Allow resetting changeset state and changeset changes roll-up
    **     TJD 20230705 Make sure resetted changesets fit into structures
    **     TJD 20231019 fix resetChangeset function name, remove unused variable
    **     TJD 20240926 Revert AL2 20240517 change, as its causing regression
    ** 006 LS  20241118 Changed rollUpChanges() to also propagate the changes.
    ** 007 AL2 20250113 Reintroduce record in the dirty-share context if delete was rollback-ed.
    ** 008 LS  20250219 Reintroduced AL2 20240517 change.
    */

    /*
    ** 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.orm;

    import java.util.*;
    import java.util.logging.*;
    import com.goldencode.p2j.persist.*;
    import com.goldencode.p2j.persist.dirty.DirtyShareContext;
    import com.goldencode.p2j.persist.dirty.DirtyShareFactory;
    import com.goldencode.p2j.util.logging.*;

    /**
     * This object manages the rollback of in-memory DMO data and state to match legacy semantics. Data and state
     * change events are logged as they occur, to allow the rollback of these actions to occur at the same full
     * and sub-transaction block levels. It is necessary to manage these changes in memory, rather than relying
     * on the state of the database to restore the state of a DMO, because not all legacy state is captured in
     * the database. Also, the legacy rules of when changes to a record are flushed to the database may mean
     * that certain state changes are not represented in the database until a more deeply nested sub-transaction
     * scope than when those changes are realized in the FWD server.
     * <p>
     * A {@link BaseRecord} instance references zero or one {@code ChangeSet} instances. An instance is only
     * associated with a {@code BaseRecord} during an active, application-level transaction. During that
     * transaction, and any sub-transactions it contains, changes to the record are recorded in the change set.
     * These include both state events, such as the record being created, deleted, validated, and flushed to
     * the database, as well as the updates of individual DMO properties. Each change is tracked by the
     * transaction nesting level in which it occurred, where 0 represents the full transaction, 1 represents the
     * first level of nested sub-transaction, and so on.
     * <p>
     * Records which have had data or state changes are tracked by the {@link SavepointManager}. When a rollback
     * event occurs, the savepoint manager rolls back the associated database savepoint, and notifies each
     * tracked DMO to roll back its changes to the appropriate transaction nesting level.
     * <p>
     * The implementation of this class is biased toward high performance update tracking, at the cost of some
     * memory. Rollback processing, being the exceptional case rather than the common case, requires more work
     * to be performed, but it should also be quite fast in most cases.
     */
    class ChangeSet
    implements DmoState
    {
       /** Logger */
       private static final CentralLogger log = CentralLogger.get(ChangeSet.class.getName());

       /** Types of change events which are logged in change set for rollback processing */
       static interface Event
       {
          public static final int NONE     = 0x00;
          public static final int CREATE   = 0x01;
          public static final int INSERT   = 0x02;
          public static final int UPDATE   = 0x04;
          public static final int DELETE   = 0x08;
          public static final int VALIDATE = 0x10;
       }

       /** Default initial number of subtransaction levels recorded by the change set */
       private static final int INITIAL_SIZE = 16;

       /** Number of subtransaction levels to increment when growing changes array */
       private static final int INCREMENT = INITIAL_SIZE / 2;

       /** Session under which this change set was created */
       private final Session session;

       /** Transaction ID associated with this change set */
       private final int txId;

       /** Baseline snapshot of data before first change is made */
       private final Object[] baselineData;

       /** Baseline state before first change is made */
       private final int baselineState;

       /** Two dimensional array representing data changes across nested sub-transactions */
       private Object[][] changes = null;

       /** Bitsets corresponding with the data changes array, indicating which data elements have changed */
       private BitSet[] marked = null;

       /** Record states corresponding with the data changes array */
       private int[] states = null;

       /** Least deeply nested (outermost) sub-transaction level at which a change is made */
       int outer = -1;

       /** Most deeply nested (innermost) sub-transaction level at which a change is made */
       int inner = -1;

       /**
        * Constructor.
        *
        * @param   session
        *          Session under which this change set was created.
        * @param   txId
        *          ID for the transaction associated with this change set.
        * @param   data
        *          DMO's data at the moment of the first state or data change. A baseline snapshot of this
        *          data is made to enable rollback later.
        * @param   state
        *          Baseline state before first change is made.
        * @param   capacity
        *          Anticipated maximum number of transaction nesting levels to record. It will be increased
        *          dynamically later, if this capacity is exceeded.
        */
       ChangeSet(Session session, int txId, Object[] data, int state, int capacity)
       {
          this.session = session;
          this.txId = txId;
          int len = data.length;
          this.baselineData = new Object[len];
          System.arraycopy(data, 0, this.baselineData, 0, len);
          this.baselineState = state;

          ensureCapacity(capacity);
       }

       /**
        * Verify that this change set was created for the given session and transaction.
        *
        * @param   session
        *          Database session.
        * @param   txId
        *          Transaction ID.
        *
        * @return  {@code true} if the given session and transaction ID match that of this change set, else
        *          {@code false}.
        */
       boolean verify(Session session, int txId)
       {
          return this.session == session && this.txId == txId;
       }

       /**
        * Log a record state change.
        *
        * @param   state
        *          Bitfield of states to be logged.
        * @param   txLevel
        *          Transaction nesting level at which event occurred.
        */
       void logStateChange(int state, int txLevel)
       {
          prepare(txLevel);
          states[txLevel] = state;
       }

       /**
        * Log a data change.
        *
        * @param   txLevel
        *          The transaction nesting level at which the change occurred.
        * @param   offset
        *          Index of the changed data element in the record's data array.
        * @param   datum
        *          The new value of the data element. The original revision of the datum will be recorded in
        *          the baseline snapshot, and intermediate revisions may be recorded in earlier transaction
        *          nesting levels in the change set.
        */
       void logDataChange(int txLevel, int offset, Object datum)
       {
          prepare(txLevel);

          changes[txLevel][offset] = datum;
          marked[txLevel].set(offset);
       }

       /**
        * Perform a full or sub-transaction level rollback of the given record to its state as it was immediately
        * before the block of the specified transaction nesting level was entered.
        *
        * @param   txLevel
        *          The nesting level of sub-transactions of the block being rolled back, where 0 represents
        *          the full transaction block, and a positive number represents that number of nested
        *          sub-transactions. The record will be rolled back to its state as it was immediately before
        *          this block was entered.
        * @param   dmo
        *          Record to be rolled back.
        */
       void rollback(int txLevel, BaseRecord dmo)
       throws PersistenceException
       {
          if (changes == null || txLevel > inner)
          {
             // TODO: what is appropriate here?
             return;
          }

          // live data to be updated by rollback
          Object[] liveData = dmo.data;

          // gather the positions of all the data elements that were changed at this block and in more
          // deeply nested blocks and clear those data structures after collecting this information
          BitSet touched = new BitSet(baselineData.length);
          for (int i = txLevel; i <= inner; i++)
          {
             // the bitset representing changed data elements (if any) at this transaction nesting level
             BitSet next = marked[i];

             // add the bits representing the changed values in this rolled back transaction level to the
             // aggregate bitset of elements which need to be rolled back
             touched.or(next);

             // release the object references in the rolled back change set level for garbage collection
             for (int j = next.nextSetBit(0); j >= 0; j = next.nextSetBit(j + 1))
             {
                // null out the corresponding data element in this transaction nesting level
                changes[i][j] = null;

                // clear the corresponding bit for this transaction nesting level
                next.clear(j);
             }
          }

          // starting at the rolled back block's parent (if any), work our way outward, overlaying data
          // elements with the most recent changes we find
          for (int i = txLevel - 1; i >= outer && !touched.isEmpty(); i--)
          {
             // bitset representing elements changed at this transaction nesting level
             BitSet next = marked[i];
             if (next.isEmpty())
             {
                continue;
             }

             // get the intersection of bits which represent data elements needing rollback and those (if
             // any) changed at this transaction nesting level
             BitSet copy = (BitSet) next.clone();
             copy.and(touched);
             if (copy.isEmpty())
             {
                continue;
             }

             // roll back to the most recent version of the data element found at this transaction nesting
             // level
             for (int j = copy.nextSetBit(0); j >= 0; j = copy.nextSetBit(j + 1))
             {
                // overlay the live data
                liveData[j] = changes[i][j];

                // remember that we've rolled back this data element
                touched.clear(j);
             }
          }

          // if any touched elements remain to be rolled back, get the values from the baseline snapshot
          if (!touched.isEmpty())
          {
             for (int j = touched.nextSetBit(0); j >= 0; j = touched.nextSetBit(j + 1))
             {
                liveData[j] = baselineData[j];
             }
          }

          // roll back state
          try
          {
             RecordState liveState = dmo.state;

             if (dmo.checkState(NEW) && !dmo.checkState(DELETED) && dmo instanceof TempRecord)
             {
                // remove the slot from MultiplexedScanIndex
                DirectAccessHelper.clearPrimaryKey(session, (TempRecord) dmo);
             }

             int startingState = liveState.state;

             // reset live state to baseline state
             liveState.state = baselineState;

             if ((baselineState & CACHED) != CACHED && (startingState & CACHED) == CACHED)
             {
                if (dmo.isInUse())
                {
                   // the DMO is in use so we can't evict it; make sure it is still marked as cached
                   liveState.state |= CACHED;
                }
                else
                {
                   // rolled back DMO must be removed from session cache
                   session.evict(dmo);
                }
             }
             else if ((baselineState & CACHED) == CACHED && (startingState & CACHED) != CACHED)
             {
                // rolled back DMO must be added back to the session cache
                session.associate(dmo);
             }

             if ((baselineState & DELETED) != DELETED && (startingState & DELETED) == DELETED)
             {
                // revive record in dirty-share as well
                Database database = this.session.getDatabase();
                DirtyShareContext dirtyContext = DirtyShareFactory.getContextInstance(database);
                if (dirtyContext != null)
                {
                   dirtyContext.delete(dmo.getClass().getName(), dmo.primaryKey(), true, true);
                }
             }
          }
          catch (StaleRecordException exc)
          {
             // not a fatal error, we just were not able to re-associate a stale record with the session cache
             if (log.isLoggable(Level.FINE))
             {
                log.log(Level.FINE, exc.getMessage(), exc);
             }
          }
          catch (DirectAccessException e)
          {
             throw new PersistenceException(e);
          }
          finally
          {
             // clear flags for rolled back state
             for (int i = inner; i >= txLevel; i--)
             {
                // clear stored state
                states[i] = DEFAULT;
             }
          }
       }

       /**
        * Prepare for a log entry into the change set by updating the outermost and innermost transaction
        * nesting levels at which changes occur, and by ensuring the internal data structures have enough
        * capacity to record information for the given transaction nesting level.
        *
        * @param   txLevel
        *          Current transaction nesting level.
        */
       private void prepare(int txLevel)
       {
          outer = outer >= 0 ? Math.min(outer, txLevel) : txLevel;
          inner = inner >= 0 ? Math.max(inner, txLevel) : txLevel;

          ensureCapacity(txLevel + 1);
       }

       /**
        * Ensure that this object's internal data structures are sufficiently sized to handle the given
        * capacity. If the required capacity exceeds the current capacity, capacity is dynamically
        * increased.
        *
        * @param   capacity
        *          Required maximum number of nested transaction levels to be tracked.
        */
       private void ensureCapacity(int capacity)
       {
          int len = this.changes != null ? this.changes.length : INITIAL_SIZE;

          if (capacity <= len && this.changes != null)
          {
             // the current data structures are sufficient to handle the required capacity
             return;
          }

          // increment target size until it meets or exceeds the required capacity
          int size = len;
          while (size < capacity)
          {
             size += INCREMENT;
          }

          int width = baselineData.length;
          Object[][] changes = new Object[size][width];
          BitSet[] active = new BitSet[size];
          int[] states = new int[size];

          if (this.changes != null)
          {
             System.arraycopy(this.changes, 0, changes, 0, len);
             System.arraycopy(this.marked, 0, active, 0, len);
             System.arraycopy(this.states, 0, states, 0, len);
             for (int i = len; i < active.length; i++)
             {
                active[i] = new BitSet(width);
             }
          }
          else
          {
             for (int i = 0; i < active.length; i++)
             {
                active[i] = new BitSet(width);
             }
          }

          this.changes = changes;
          this.marked = active;
          this.states = states;
       }

       /**
        * Perform a sub-transaction level ChangeSet state reset of a given record to its state as it was
        * immediately before the block of the specified transaction nesting level was entered.
        *
        * @param   txLevel
        *          The nesting level of sub-transactions of the block being reset, where 0 represents
        *          the full transaction block, and a positive number represents that number of nested
        *          sub-transactions.
        */
       void resetTxLevel(int txLevel)
       {
          prepare(txLevel);
          int width = baselineData.length;
          states[txLevel] = DEFAULT;
          marked[txLevel] = new BitSet(width);
       }

       /**
        * Propagate information about changed to upper transaction level. This is needed to perform rollback
        * correctly.
        *
        * @param   txLevel
        *          The nesting level of sub-transactions of the block being rolled up back, where 0 represents
        *          the full transaction block, and a positive number represents that number of nested
        *          sub-transactions. The record's changed fields information will be rolled back to
        *          upper transaction level/block.
        */
       public void rollUpChanges(int txLevel)
       {
          prepare(txLevel);
          // propagate change marks down and changes
          marked[txLevel].or(marked[txLevel+1]);

          BitSet next = marked[txLevel + 1];
          for (int j = next.nextSetBit(0); j >= 0; j = next.nextSetBit(j + 1))
          {
             changes[txLevel][j] = changes[txLevel + 1][j];
             changes[txLevel + 1][j] = null;
          }
       }
    }