# UniqueTracker.java

    /*
    ** Module   : UniqueTracker.java
    ** Abstract : Tracks records for testing whether the unique index constraints are broken.
    **
    ** Copyright (c) 2020-2024, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
    ** 001 OM  20200110 Created initial version.
    **     ECF 20200211 Changes to integrate with DMO validation and flushing.
    **     CA  20200622 Small performance improvement - use IdentityHashMap if the key is java.lang.Class.
    ** 002 OM  20201002 Use DmoMeta cached information instead of map lookups.
    ** 003 CA  20220210 Removed Commitable.rollbackPending and its notifications, as there is no actual
    **                  implementation of this method.
    **     OM  20221005 Do not save deleted transient records for rollback. Added UniqueIndex.toString() method.
    **     TJD 20221122 Reset rollback flag on commit
    ** 004 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
    ** 005 CA  20240507 Unique index validation must validate only dirty indexes, even for NEW records, if we are
    **                  not in flush mode.
    ** 006 TJD 20240123 Java 17 compatibility updates.
    ** 007 CA  20240924 Do not push scopes to 'changes' and 'deletes' until the scope is used.
    */

    /*
    ** 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.BitSet;
    import java.util.List;
    import java.util.concurrent.locks.*;
    import com.goldencode.p2j.persist.*;
    import com.goldencode.p2j.persist.Record;
    import com.goldencode.p2j.security.*;
    import com.goldencode.p2j.util.*;
    import com.goldencode.p2j.util.logging.*;
    import org.apache.commons.lang3.tuple.*;

    /**
     * Implements a tracker for records in unique indices in uncommitted transactions. This enables
     * a proactive, synchronized check to ensure the insertion or update of a record will not cause
     * a unique constraint violation, with respect to any uncommitted changes across FWD sessions.
     * Otherwise, such a failure would not occur until the latter of two transactions containing a
     * conflicting insert/update were committed. This would be too late in terms of legacy
     * compatibility, since the 4GL reports this failure at the moment of the index update, not at
     * the eventual transaction commit.
     * <p>
     * The overall structure of the {@code UniqueTracker} architecture is one map instance per unique
     * index, per table/DMO type. The maps are keyed by {@code Key} which emulate the P4GL
     * constraints from the point of view of the unique indexes. The values mapped are the {@code id}s
     * of the records.
     * <p>
     * The exposed API allows to:
     * <ul>
     *    <li>add new {@code Record} to tracking;</li>
     *    <li>test whether a specified {@code Record} is tracked;</li>
     *    <li>remove a specified {@code Record} (or using its {@code id}) from tracking;</li>
     * </ul>
     *
     * The tracker also keeps track of the transactions and all elements associated with a transaction
     * are processed when the transaction is committed (or rolled back).
     */
    public class UniqueTracker
    {
       /** Logger */
       private static final CentralLogger LOG = CentralLogger.get(UniqueTracker.class);

       /** Cache of DMO implementation classes to instances of this class */
       private static final Map<Class<? extends BaseRecord>, UniqueTracker> instances =
          new IdentityHashMap<>();

       /** Context local state for this session's uncommitted transaction data */
       private static final ContextLocal<Context> context = new ContextLocal<Context>()
       {
          @Override protected Context initialValue() { return new Context(); }
          @Override protected void cleanup(Context context) { context.cleanup(true); }
       };

       /**
        * The list of {@code UniqueIndex} for a {@code DataModelObject}. When an {@code UniqueTracker}
        * is created the DMO class is analyzed and the list of unique indexes is extracted. For each
        * index there an {@code UniqueIndex} is created and added to this array.
        */
       private final UniqueIndex[] uniqueIndexes;

       /**
        * Default constructor, used only by no-op subclass.
        *
        * @see  #instantiate(DmoMeta)
        */
       protected UniqueTracker()
       {
          this.uniqueIndexes = null;
       }

       /**
        * Creates a new tracker for a designated {@code BaseRecord}. The {@code dmoClass} is used to
        * extract the unique indexes and create an optimized internal data structure.
        *
        * @param   dmoInfo
        *          The DMO meta information for DMO implementation class.
        */
       private UniqueTracker(DmoMeta dmoInfo)
       {
          RecordMeta rm = dmoInfo.recordMeta;
          BitSet[] uniqueBits = rm.getUniqueIndices();
          List<UniqueIndex> indexList = new ArrayList<>(5);
          for (BitSet b : uniqueBits)
          {
             indexList.add(new UniqueIndex(b));
          }

          uniqueIndexes = indexList.toArray(new UniqueIndex[indexList.size()]);
       }

       /**
        * Get the context-local helper object which is the local session's access to the {@code
        * UniqueTracker} API.
        *
        * @return  Context-local helper object.
        */
       public static Context getContext()
       {
          return context.get();
       }

       /**
        * Get a {@code UniqueTracker} for a specified {@code DataModelObject}. If this is the first
        * call for a specified class, a new instance is created.
        *
        * @param   dmoInfo
        *          The DMO meta information for DMO implementation class.
        *
        * @return  The {@code  UniqueTracker} for the specified {@code DataModelObject}.
        */
       public static UniqueTracker getInstance(DmoMeta dmoInfo)
       {
          Class<? extends Record> dmoClass = dmoInfo.getImplementationClass();
          UniqueTracker ut = instances.get(dmoClass);
          if (ut == null)
          {
             ut = instantiate(dmoInfo);
             instances.put(dmoClass, ut);
          }
          return ut;
       }

       /**
        * Instantiate the appropriate form of this class for the given DMO implementation class.
        * This returns a no-op version for use with temp-table DMOs, and fully functioning version
        * for persistent table DMOs. Temp-tables operate only in a single context. Therefore,
        * validating against the database is sufficient to detect any unique constraint violation.
        *
        * @param   dmoInfo
        *          The DMO meta information for DMO implementation class.
        *
        * @return  A new instance of this class, possibly with some methods neutered.
        */
       private static UniqueTracker instantiate(DmoMeta dmoInfo)
       {
          if (dmoInfo.isTempTable())
          {
             // temp-tables use a no-op implementation of this class
             return new UniqueTracker()
             {
                @Override
                protected final Token lockAndChange(BaseRecord dmo, Context context, String bufferName, boolean flush)
                { return null; }
                @Override
                protected final Token lockAndDelete(BaseRecord dmo, Context context) { return null; }
                @Override
                protected final void rollbackChange(Token token) { }
                @Override
                protected final void unlock(Token token) { }
             };
          }

          return new UniqueTracker(dmoInfo);
       }

       /**
        * Check the given record's state and update the minimal, necessary unique index(es), based
        * on any dirty properties in the record, or update all unique indexes in the case that
        * {@code dmo} has been newly created and was not previously persisted.
        * <p>
        * If the data within {@code dmo} violates an existing unique constraint, an error is
        * raised. Any changes made up to that point are rolled back automatically.
        *
        * @param   dmo
        *          Record whose state is driving an update to this tracker.
        * @param   context
        *          Uncommitted data managed for the current context.
        * @param   bufferName
        *          The name of the buffer. It is used when printing the error message, if any.
        * @param   flush
        *          Whether to flush the record; {@code false} to validate only.
        *
        * @return  A token which contains information about the update operation. This token must
        *          be passed to either {@link #unlock(Token)} or to {@link #rollbackChange(Token)}
        *          in order to commit or roll back the update, respectively, and to ensure any
        *          locks acquired by the update are released.
        *
        * @throws  ValidationException
        *          if the data in {@code dmo} represent a unique constraint violation within any
        *          session's uncommitted transactions.
        */
       protected Token lockAndChange(BaseRecord dmo, Context context, String bufferName, boolean flush)
       throws ValidationException
       {
          if (context.pendingChangesScopes == 0 && context.changes.scopes() <= 0)
          {
             LOG.severe("", new IllegalStateException("Empty scopes in UniqueTracker"));
             return null;
          }

          // figure out which indexes need to be locked for update; for a newly created record, a new
          // entry must be created for each unique index; otherwise, just dirty indices are updated

          boolean full = flush && dmo.checkState(DmoState.NEW);

          // affected pairs of Integers to Keys; the left element represents the position of
          // the affected UniqueIndex in uniqueIndexes array, the right is the old value to be reset
          // on rollback, or null if this operation represents a new insert
          List<Pair<Integer, UniqueIndex.Key>> affected = new LinkedList<>();

          Long id = dmo.primaryKey();

          try
          {
             BitSet dirty = dmo.getDirtyProps();

             int len = uniqueIndexes.length;
             for (int i = 0; i < len; i++)
             {
                UniqueIndex ui = uniqueIndexes[i];

                // only update UniqueIndex if we are indexing a new record or if one of its fields
                // has been touched
                if (full || dirty.intersects(ui.fieldMap))
                {
                   // obtain lock; may block here
                   ui.lock();

                   // if an entry for this record exists in this UniqueIndex, remove it before
                   // trying to add new Key
                   UniqueIndex.Key oldVal = ui.entries.remove(id);
                   if (oldVal != null)
                   {
                      ui.records.remove(oldVal, id);
                   }

                   // remember changes in case rollback is needed; left element of pair is position
                   // of affected UniqueIndex instance in uniqueIndexes array; right element is old
                   // value of Key (null in full case)
                   affected.add(Pair.of(i, oldVal));

                   // attempt to store new Key, based on record's data
                   UniqueIndex.Key newVal = ui.createKey(dmo);
                   if (ui.records.containsKey(newVal))
                   {
                      reportUniqueConstraintViolation(dmo, newVal, bufferName);
                   }

                   // track unique key in shared data structures
                   ui.records.put(newVal, id);
                   ui.entries.put(id, newVal);

                   // track unique key in context-local data structure
                   while (context.pendingChangesScopes > 0)
                   {
                      context.changes.pushScope();
                      context.pendingChangesScopes--;
                   }
                   context.changes.add(false, newVal);
                }
             }
          }
          catch (ValidationException exc)
          {
             rollbackChange(id, affected);

             throw exc;
          }

          return new Token(id, affected);
       }

       /**
        * The given DMO has been deleted in the current transaction. If a unique index already is
        * being tracked for this DMO, it indicates that the DMO previously was inserted or updated
        * in the same transaction. In this case, remove its earlier unique index entry, but
        * remember it in case we need to roll back this delete later. In the event there is no
        * unique index already being tracked, it indicates that the record deleted was not
        * previously inserted or updated during the current transaction. In this case, we store
        * {@code null} for the key in the {@code deletes} we are tracking. Rollback code must
        * tolerate this {@code null}.
        * <p>
        * This operation performs no validation, as no unique constraint can be violated by a
        * delete.
        *
        * @param   dmo
        *          DMO which was deleted.
        * @param   context
        *          Uncommitted data managed for the current context.
        *
        * @return  A token which contains information about the update operation. This token must
        *          be passed to either {@link #unlock(Token)} or to {@link #rollbackChange(Token)}
        *          in order to commit or roll back the update, respectively, and to ensure any
        *          locks acquired by the update are released.
        */
       protected Token lockAndDelete(BaseRecord dmo, Context context)
       {
          // affected pairs of Integers and Keys; the left element represents the position of the
          // affected UniqueIndex in uniqueIndexes array, the right is the old value to be reset
          // on rollback, or null if this operation represents the delete of a record not previously
          // inserted or updated in this transaction
          List<Pair<Integer, UniqueIndex.Key>> affected = new LinkedList<>();

          Long id = dmo.primaryKey();

          int len = uniqueIndexes.length;
          for (int i = 0; i < len; i++)
          {
             UniqueIndex ui = uniqueIndexes[i];

             // obtain lock; may block here
             ui.lock();

             // if an entry for this record exists in this UniqueIndex, remove it
             UniqueIndex.Key oldVal = ui.entries.remove(id);
             if (oldVal != null)
             {
                ui.records.remove(oldVal, id);
             }

             // remember changes in case rollback is needed; left element of pair is position of affected
             // UniqueIndex instance in uniqueIndexes array; right element is old value of Key (null in the
             // case of the delete of a record not previously inserted or updated in this transaction)
             affected.add(Pair.of(i, oldVal));

             if (!dmo.checkState(DmoState.NEW))
             {
                // track pair of record ID and old unique key in context-local data structure; null key
                // has same meaning as above
                // NOTE: do this only for non transient record. The transient records were not yet saved so there
                //       is no point in restoring them.
                while (context.pendingDeletesScopes > 0)
                {
                   context.deletes.pushScope();
                   context.pendingDeletesScopes--;
                }
                context.deletes.add(false, Pair.of(id, oldVal));
             }
          }

          return new Token(id, affected);
       }

       /**
        * Complete the operation begun by {@link #lockAndChange} or by
        * {@link #lockAndDelete(BaseRecord, Context)}, using the token returned by the respective method.
        * A commit is no more than the absence of a rollback and an unlocking of any {@code UniqueIndex}
        * instances locked by the update or delete.
        * <p>
        * This method should be invoked when no unique constraint violation has occurred during a record
        * validation event.
        *
        * @param   token
        *          A token returned by {@link #lockAndChange} or by {@link
        *          #lockAndDelete(BaseRecord, Context)}, used to unlock any {@code UniqueIndex} instances
        *          instances locked by those methods.
        */
       protected void unlock(Token token)
       {
          if (token == null || token.affected.isEmpty())
          {
             // no resources are locked
             return;
          }

          for (Pair<Integer, UniqueIndex.Key> pair : token.affected)
          {
             int i = pair.getLeft();
             uniqueIndexes[i].unlock();
          }
       }

       /**
        * Roll back an update operation on zero or more unique keys in this tracker, presumably in response
        * to a unique constraint violation either generated by this tracker or which occurred during a database
        * insert or update.
        * <p>
        * This method should be invoked when a unique constraint violation has occurred during a record
        * validation event.
        *
        * @param   token
        *          A token returned by {@link #lockAndChange}, used to rollback
        *          any partial or full updates made and to unlock any {@code UniqueIndex} instances locked by
        *          the update.
        */
       protected void rollbackChange(Token token)
       {
          rollbackChange(token.id, token.affected);
       }

       /**
        * Roll back an update operation on zero or more unique keys in this tracker, presumably in
        * response to a unique constraint violation either generated by this tracker or which
        * occurred during a database insert or update.
        *
        * @param   id
        *          Primary key of the record for which the update operation was performed.
        * @param   affected
        *          A list of pairs of {@code UniqueIndex} positions in the {@code uniqueIndexes}
        *          array and corresponding {@code Key} instances to be restored (or {@code
        *          null} if the operation represented a new record insert).
        */
       private void rollbackChange(Long id, List<Pair<Integer, UniqueIndex.Key>> affected)
       {
          try
          {
             Iterator<Pair<Integer, UniqueIndex.Key>> iter = affected.iterator();
             while (iter.hasNext())
             {
                Pair<Integer, UniqueIndex.Key> pair = iter.next();

                // access UniqueIndex instance
                int i = pair.getLeft();
                UniqueIndex ui = uniqueIndexes[i];

                // remove new mappings, if any
                UniqueIndex.Key newVal = ui.entries.remove(id);
                if (newVal != null)
                {
                   ui.records.remove(newVal);
                }

                // restore old mappings, if any
                UniqueIndex.Key oldVal = pair.getRight();
                if (oldVal != null)
                {
                   ui.entries.put(id, oldVal);
                   ui.records.put(oldVal, id);
                }

                // unlock the UniqueIndex
                ui.unlock();

                // remove the pair, to prevent attempting rollback/unlock more than once
                iter.remove();
             }
          }
          finally
          {
             // unlock all affected resources, in case we exited the try block abnormally; no-op
             // if already unlocked above
             unlock(affected);
          }
       }

       /**
        * Unlock the {@code UniqueIndex} instances specified in the given list of pairs.
        *
        * @param   affected
        *          A list of zero or more pairs of {@code UniqueIndex} positions in the {@code
        *          uniqueIndexes} array and {@code Key} instances. The {@code UniqueIndex}
        *          instances at the given positions are to be unlocked.
        */
       private void unlock(List<Pair<Integer, UniqueIndex.Key>> affected)
       {
          for (Pair<Integer, UniqueIndex.Key> pair : affected)
          {
             int i = pair.getLeft();
             uniqueIndexes[i].unlock();
          }
       }

       /**
        * Report a unique constraint violation using the expected, legacy error message.
        *
        * @param   dmo
        *          Record which violated constraint.
        * @param   key
        *          Unique index key which contains the non-unique data.
        * @param   bufferName
        *          The name of the buffer. It is used when printing the error message, if any.
        *
        * @throws  ValidationException
        *          always; describes the unique constraint violation.
        */
       private void reportUniqueConstraintViolation(BaseRecord dmo, UniqueIndex.Key key, String bufferName)
       throws ValidationException
       {
          BitSet uIndex = new BitSet();
          for (int i : key.fieldPos)
          {
             uIndex.set(i);
          }

          String msg = Validation.getFailUniqueIndexMessage(uIndex, bufferName, dmo);
          throw new ValidationException(msg, 132);
       }

       /**
        * Context-local API through which {@code UniqueTracker} functionality should be accessed.
        */
       public static class Context
       implements Finalizable,
                  Commitable
       {
          /** Scoped list of uncommitted insert/update changes local to this context */
          private final ScopedList<UniqueIndex.Key> changes = new ScopedList<>();

          /** The number of pending scopes to be added to {@link #changes}. */
          private int pendingChangesScopes = 0;

          /** Scoped list of uncommitted deletes local to this context */
          private final ScopedList<Pair<Long, UniqueIndex.Key>> deletes = new ScopedList<>();

          /** The number of pending scopes to be added to {@link #deletes}. */
          private int pendingDeletesScopes = 0;

          /** Whether to reverse apply changes/deletes for the innermost scope at end of life */
          private boolean rollback = false;

          /** Whether the innermost scope represents a full (vs. sub) transaction */
          private boolean fullTx = false;

          public void register(TransactionManager.TransactionHelper txHelper, int blockDepth)
          {
             txHelper.registerFinalizable(this, false);
             txHelper.registerCommit(this);

    //         log(">>> entry");
             beginTxScope();
    //         log("<<< entry");
          }

          /**
           * Check the given record's state and update the minimal, necessary unique index(es), based
           * on any dirty properties in the record, or update all unique indexes in the case that
           * {@code dmo} has been newly created and was not previously persisted.
           * <p>
           * If the data within {@code dmo} violates an existing unique constraint, an error is
           * raised. Any changes made up to that point are rolled back automatically.
           *
           * @param   tracker
           *          The unique tracker associated with the affected table.
           * @param   dmo
           *          Record whose state is driving an update to this tracker.
           * @param   bufferName
           *          The name of the buffer. It is used when printing the error message, if any.
           * @param   flush
           *          Whether to flush the record; {@code false} to validate only.
           *
           * @return  A token which contains information about the update operation. This token must
           *          be passed to either {@link #unlock(Token)} or to {@link #rollbackChange(Token)}
           *          in order to commit or roll back the update, respectively, and to ensure any
           *          locks acquired by the update are released.
           *
           * @throws  ValidationException
           *          if the data in {@code dmo} represent a unique constraint violation within any
           *          session's uncommitted transactions.
           */
          public Token lockAndChange(UniqueTracker tracker, BaseRecord dmo, String bufferName, boolean flush)
          throws ValidationException
          {
             return tracker.lockAndChange(dmo, this, bufferName, flush);
          }

          /**
           * The given DMO has been deleted in the current transaction. If a unique index already is
           * being tracked for this DMO, it indicates that the DMO previously was inserted or updated
           * in the same transaction. In this case, remove its earlier unique index entry, but
           * remember it in case we need to roll back this delete later. In the event there is no
           * unique index already being tracked, it indicates that the record deleted was not
           * previously inserted or updated during the current transaction. In this case, we store
           * {@code null} for the key in the {@code deletes} we are tracking. Rollback code must
           * tolerate this {@code null}.
           * <p>
           * This operation performs no validation, as no unique constraint can be violated by a
           * delete.
           *
           * @param   tracker
           *          The unique tracker associated with the affected table.
           * @param   dmo
           *          DMO which was deleted.
           *
           * @return  A token which contains information about the update operation. This token must
           *          be passed to either {@link #unlock(Token)} or to {@link #rollbackChange(Token)}
           *          in order to commit or roll back the update, respectively, and to ensure any
           *          locks acquired by the update are released.
           */
          public Token lockAndDelete(UniqueTracker tracker, BaseRecord dmo)
          {
             return tracker.lockAndDelete(dmo, this);
          }

          /**
           * Roll back an update operation on zero or more unique keys in this tracker, presumably
           * in response to a unique constraint violation either generated by this tracker or which
           * occurred during a database insert or update.
           * <p>
           * This method should be invoked when a unique constraint violation has occurred during a
           * record validation event.
           *
           * @param   tracker
           *          The unique tracker associated with the affected table.
           * @param   token
           *          A token returned by {@link #lockAndChange}, used
           *          to roll back any partial or full updates made and to unlock any {@code UniqueIndex}
           *          instances locked by the update.
           */
          public void rollbackChange(UniqueTracker tracker, Token token)
          {
             tracker.rollbackChange(token);
          }

          /**
           * Complete the operation begun by {@link #lockAndChange}
           * or by {@link #lockAndDelete(UniqueTracker, BaseRecord)}, using the token returned by the
           * respective method. A commit is no more than the absence of a rollback and an unlocking
           * of any {@code UniqueIndex} instances locked by the update or delete.
           * <p>
           * This method should be invoked when no unique constraint violation has occurred during a
           * record validation event.
           *
           * @param   token
           *          A token returned by {@link #lockAndChange} or by
           *          {@link #lockAndDelete(UniqueTracker, BaseRecord)}, used to unlock any {@code
           *          UniqueIndex} instances locked by those methods.
           */
          public void unlock(UniqueTracker tracker, Token token)
          {
             tracker.unlock(token);
          }

          /**
           * Callback when a looping block is iterated, before business logic in the block is
           * executed. End the current scope and begin a new one.
           */
          @Override
          public void iterate()
          {
    //         log(">>> iterate");
             endTxScope();
             beginTxScope();
    //         log("<<< iterate");
          }

          /**
           * Callback when a block is retried, before business logic in the block is executed.
           * End the current scope and begin a new one.
           */
          @Override
          public void retry()
          {
    //         log(">>> retry");
             endTxScope();
             beginTxScope();
    //         log("<<< retry");
          }

          /**
           * Callback when a block is finished. End the current scope.
           */
          @Override
          public void finished()
          {
    //         log(">>> finished");
             endTxScope();
    //         log("<<< finished");
          }

          /**
           * No-op block callback.
           */
          @Override
          public void deleted()
          {
          }

          /**
           * Callback when a full or sub-transaction block is committed.
           *
           * @param   fullTx
           *          {@code true} if the block is a full transaction block; {@code false} if it is
           *          a sub-transaction block.
           */
          @Override
          public void commit(boolean fullTx)
          {
    //         log("commit");
             this.fullTx = fullTx;
             this.rollback = false;
          }

          /**
           * Callback when a full or sub-transaction block is rolled back.
           *
           * @param   fullTx
           *          {@code true} if the block is a full transaction block; {@code false} if it is
           *          a sub-transaction block.
           */
          @Override
          public void rollback(boolean fullTx)
          {
    //         log("rollback");
             this.fullTx = fullTx;
             this.rollback = true;
          }

          /**
           * No-op callback.
           */
          @Override
          public void validate(boolean transaction, boolean aggressiveFlush)
          {
          }

          /*
          // temporary for debugging
          private final Set<String> entries = new LinkedHashSet<>();
          private final Set<String> finishes = new LinkedHashSet<>();

          private void log(String msg)
          {
             Throwable t = new Throwable();
             StackTraceElement[] ste = t.getStackTrace();

             String elem = "UNINITIALIZED";

             if (ste != null)
             {
                for (int i = 0; i < ste.length; i++)
                {
                   StackTraceElement e = ste[i];
                   String className = e.getClassName();
                   if (className.startsWith("com.goldencode.hotel."))
                   {
                      elem = e.toString();

                      if (msg.startsWith(">>> entry"))
                      {
                         entries.add(elem);
                      }
                      else if (msg.startsWith(">>> finished"))
                      {
                         finishes.add(elem);
                      }

                      msg += (" " + elem);

                      break;
                   }
                }
             }
             int count = changes.scopes();

             if (count == 0 && msg.startsWith(">>>"))
             {
                entries.clear();
                finishes.clear();
                LOG.finer();
                LOG.finer("---------------------------- BEGIN ----------------------------");
             }

             LOG.finer("#" + count + " --> " + msg);

             if (count == 0 && msg.startsWith("<<<"))
             {
                LOG.finer("----------------------------- END -----------------------------");
                LOG.finer();

                Set<String> unmatched = new LinkedHashSet<>(finishes);
                unmatched.removeAll(entries);
                if (!unmatched.isEmpty())
                {
                   LOG.finer("finished() calls unmatched by entry() calls:");
                   for (String e : unmatched)
                   {
                      LOG.finer(e);
                   }
                   LOG.finer();
                }
             }
          }
          */

          /**
           * Begin a transaction scope (full or sub-). This event causes new scopes to be pushed
           * onto the scoped lists of changes and deletes.
           */
          private void beginTxScope()
          {
             // changes.pushScope();
             pendingChangesScopes++;
             // deletes.pushScope();
             pendingDeletesScopes++;
          }

          /**
           * End a transaction scope (full or sub-), resulting in a commit or rollback of any changes
           * or deletes in the context's current, uncommitted database transaction, with respect to
           * the {@code UniqueTracker} instances active for this context.
           * <p>
           * A commit simply rolls up any changes/deletes for the current sub-transaction scope to
           * the enclosing scope, if any. A rollback reverse applies the changes/deletes for the current
           * current (sub-)transaction scope. Either way, the current scope is popped.
           */
          private void endTxScope()
          {
             if (pendingChangesScopes == 0 && changes.scopes() <= 0)
             {
                LOG.severe("", new IllegalStateException("Imbalanced Scopes in UniqueTracker"));
                return;
             }

             try
             {
                if (fullTx)
                {
                   cleanup(false);
                }
                else if (rollback)
                {
                   // roll back insert/update changes by removing each of them from their index
                   if (pendingChangesScopes == 0)
                   {
                      for (UniqueIndex.Key key : changes.sublistAtScope(0))
                      {
                         key.removeFromIndex();
                      }
                   }

                   if (pendingDeletesScopes == 0)
                   {
                      // roll back deletes by adding any deleted unique index keys  back to their index;
                      // if the key is null, it indicates the delete being rolled back was of a record
                      // not inserted or updated in this transaction, so do not try to re-insert in this
                      // case
                      for (Pair<Long, UniqueIndex.Key> pair : deletes.sublistAtScope(0))
                      {
                         Long id = pair.getLeft();
                         UniqueIndex.Key key = pair.getRight();
                         if (key != null)
                         {
                            // if key was not null, it indicates the delete that we are rolling back
                            // was of a record we were tracking, so it had been inserted and/or updated
                            // in the current transaction previous to the delete.
                            key.addToIndex(id);

                            // if the key was null, the delete being rolled back was of a record not
                            // otherwise touched by the current transaction; therefore, the rollback of
                            // the delete already is reflected in this session's version of the
                            // database, so we do not need to track it separately as uncommitted data
                         }
                      }
                   }
                }
                else
                {
                   // on sub-transaction commit, reversible changes need to roll up to parent scope
                   if (pendingChangesScopes == 0)
                   {
                      changes.rollUp();
                   }

                   if (pendingDeletesScopes == 0)
                   {
                      deletes.rollUp();
                   }
                }
             }
             finally
             {
                if (pendingChangesScopes == 0)
                {
                   changes.popScope();
                }
                else
                {
                   pendingChangesScopes--;
                }

                if (pendingDeletesScopes == 0)
                {
                   deletes.popScope();
                }
                else
                {
                   pendingDeletesScopes--;
                }
             }
          }

          /**
           * Clean up as a block is finishing its processing. Optionally cleanup as the current
           * context is exiting.
           *
           * @param   exit
           *          {@code true} if the context is exiting, not just the current block. This
           *          represents an emergency cleanup.
           */
          private void cleanup(boolean exit)
          {
             // TODO: make lock/unlock more efficient
             for (UniqueIndex.Key key : changes)
             {
                key.removeFromIndex();
             }

             // TODO: is this sufficient cleanup on context end? Don't we need to rollback this
             // context's impact on shared resources?
             if (exit)
             {
                pendingChangesScopes = 0;
                while (changes.scopes() > 0)
                {
                   changes.popScope();
                }

                pendingDeletesScopes = 0;
                while (deletes.scopes() > 0)
                {
                   deletes.popScope();
                }
             }
          }
       }

       /**
        * An object which holds information about the set of {@code UniqueIndex} objects locked and
        * updated by an operation on a particular {@code UniqueTracker} instance. A token is created
        * by {@link UniqueTracker#lockAndChange} or {@link
        * UniqueTracker#lockAndDelete(BaseRecord, Context)}, and must subsequently be passed to
        * {@link UniqueTracker#unlock(Token)} or {@link UniqueTracker#rollbackChange(Token)}.
        * The latter method uses the token to roll back the changes made in an update or delete
        * operation. Both methods use the token to unlock any locked {@code UniqueIndex} instances.
        */
       public static class Token
       {
          /** Primary key of record indexed by a unit of work */
          private final Long id;

          /** Pairs of {@code UniqueIndex} offsets and old {@code Key} values */
          private final List<Pair<Integer, UniqueIndex.Key>> affected;

          /**
           * Constructor.
           *
           * @param   id
           *          Primary key of the record involved in the operation described by this token.
           * @param   affected
           *          Pairs of {@code UniqueIndex} offsets in their array within the tracker, and
           *          {@code Key} instances which were affected by the operation.
           */
          private Token(long id, List<Pair<Integer, UniqueIndex.Key>> affected)
          {
             this.id = id;
             this.affected = affected;
          }
       }

       /**
        * The data related to a specified unique index. This holds all the unique records added for
        * an index of a specific {@code DMO}.
        */
       private static class UniqueIndex
       {
          /** Mutex lock */
          private final ReentrantLock lock = new ReentrantLock();

          /**
           * The set of fields of the {@code BaseRecord} which comprise the index. The order is
           * irrelevant and this array uses the 'natural' order in which the fields were declared at
           * the definition of the table.
           */
          private final int[] fieldPos;

          /** Bitset whereby set bits describe the components of the index by position in the data */
          private final BitSet fieldMap;

          /**
           * The set of unique records, keyed by {@code Key}, mapping to the original
           * {@code id} of the record.
           */
          private final Map<Key, Long> records = new HashMap<>();

          /** Unique index keys mapped by primary keys */
          private final Map<Long, Key> entries = new HashMap<>();

          /**
           * Creates a new structure that contains data related to an unique index of a table.
           *
           * @param   fieldMap
           *          The bitmap of the index, that is, the bits corresponding to field component of
           *          the index.
           */
          public UniqueIndex(BitSet fieldMap)
          {
             this.fieldMap = fieldMap;

             // convert from BitSet to an array of the offsets of the component fields. The size of
             // the array is the length of index component. We can now access the k field directly
             // using record.data[fieldPos[k]].
             this.fieldPos = fieldMap.stream().toArray();
          }

          /**
           * Lock this resource exclusively.
           */
          void lock()
          {
             lock.lock();
          }

          /**
           * Release the lock on this resource.
           */
          void unlock()
          {
             lock.unlock();
          }

          /**
           * Create a {@code Key} for the given record within the current index. This is just
           * a wrapper for the {@code Key} constructor with access to its implicit {@code
           * UniqueIndex.this} reference.
           *
           * @param   dmo
           *          Database record.
           *
           * @return  {@code Key} instance.
           */
          Key createKey(BaseRecord dmo)
          {
             return new Key(this, dmo.data);
          }

          /**
           * The key used for tracking a record within an unique index.
           */
          private class Key
          {
             /** The snapshot of the record at the moment of indexing. */
             private final Object[] snapshot;

             /**
              * The fields that the unique index is compounded of. Having the offsets in an int array
              * is faster than computing them bitwise from a {@code BitSet}.
              */
             private final int[] fieldPos;

             /** The pre-computed {@code hash} code */
             private final int hash;

             /**
              * Flags whether the record has {@code null} values for fields components of this unique
              * index. In this case the record is unique.
              */
             private boolean hasNulls;

             /**
              * Creates a new, immutable {@code Key} object. For performance reasons, the
              * {@code hash} code will be computed only once and the {@code hasNulls} will also be
              * evaluated.
              *
              * @param   uniqueIndex
              *          The components of the index, by their position in {@code data} array.
              * @param   data
              *          The values of the fields in the record (a reference will be saved, not
              *          copied).
              */
             public Key(UniqueIndex uniqueIndex, Object[] data)
             {
                snapshot = data;
                fieldPos = uniqueIndex.fieldPos;
                hash = computeHash();
             }

             /**
              * Test for equality with a specified object. The test is done using the 4GL uniqueness
              * semantics: two records are unique from the point of view of an unique index if the
              * compound fields (their index in {@code data} array is specified in the {@code
              * fieldPos}) must be different except when one of them is {@code null}, in which case
              * the records are unique.
              *
              * @param   o
              *          The object to be tested against.
              *
              * @return  {@code true} if and only if the two keys are equals from the point of view of
              *          the 4GL uniqueness semantics.
              */
             @Override
             public boolean equals(Object o)
             {
                // basic tests:

                // identity:
                if (this == o)
                {
                   return true;
                }

                // null comparison
                if (o == null)
                {
                   return false;
                }

                // just in case this was skipped: check hashes:
                Key that = (Key) o;
                if (this.hash != that.hash)
                {
                   return false;
                }

                // in P4GL 'unique' semantics, a record with an unknown (null) field as index component
                // will make the record unique; this intentionally departs from the implementations of
                // hashCode and equals being consistent, to ensure separate Key instances are
                // stored if all else is the same, except that one or both contain null
                if (this.hasNulls || that.hasNulls)
                {
                   return false;
                }

                // check for equality of fields in index:
                int len = fieldPos.length;
                for (int i = 0; i < len; i++)
                {
                   int position = fieldPos[i];
                   if (!Objects.equals(this.snapshot[position], that.snapshot[position]))
    //               if (!this.snapshot[position].equals(that.snapshot[position]))
                   {
                      return false;
                   }
                }

                return true;
             }

             /**
              * Composes and return a short description of this object which includes the values of the indexed
              * fields.
              *
              * @return  a short description of this object.
              */
             @Override
             public String toString()
             {
                StringBuilder sb = new StringBuilder("Kuid{");
                for (int i = 0; i < fieldPos.length; i++)
                {
                   if (i != 0)
                   {
                      sb.append(":");
                   }
                   sb.append(snapshot[fieldPos[i]]);
                }
                sb.append('}');
                return sb.toString();
             }

             /**
              * Obtain the hash code for this unique key.
              *
              * @return  the hash code for this unique key.
              */
             @Override
             public int hashCode()
             {
                return hash;
             }

             /**
              * Add this key to its enclosing {@code UniqueIndex}. The index is locked before the
              * operation and released after.
              *
              * @param   id
              *          Primary key to associate with this key.
              */
             void addToIndex(Long id)
             {
                lock();

                try
                {
                   records.put(this, id);
                   entries.put(id, this);
                }
                finally
                {
                   unlock();
                }
             }

             /**
              * Remove this key from its enclosing {@code UniqueIndex}. The index is locked before
              * the operation and released after.
              */
             void removeFromIndex()
             {
                lock();

                try
                {
                   Long id = records.remove(this);
                   if (id != null)
                   {
                      entries.remove(id);
                   }
                }
                finally
                {
                   unlock();
                }
             }

             /**
              * Computes the hash code for this unique key. Only the compound fields of the current index
              * from the record are taken into consideration, the other are ignored.
              *
              * @return  the hash code for this unique key.
              */
             private int computeHash()
             {
                int res = 0;
                int len = fieldPos.length;
                for (int i = 0; i < len; i++)
                {
                   int position = fieldPos[i];
                   Object o = snapshot[position];
                   if (o == null)
                   {
                      hasNulls = true;
                   }
                   res = res * 37 + (o == null ? 97 : o.hashCode());
                }

                return res;
             }
          }
       }
    }