# BaseRecord.java

    /*
    ** Module   : BaseRecord.java
    ** Abstract : The abstract base class for all business data model object classes.
    **
    ** Copyright (c) 2019-2023, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
    ** 001 ECF 20191001 Created initial version with basic data and accessors.
    **     OM  20200110 Added support various data types and setter/getter signatures.
    ** 002 CA  20200922 Added getDatum(offset).
    **     ECF 20201201 Cleaned up file and added some missing javadoc.
    **     AIL 20201210 Added bulk set datum for specific buffer-copy operations.
    **     ECF 20201210 Added direct access to data array for RecordBuffer.
    **         20210126 Fixed getUnvalidatedIndices to account optionally for untouched, default property values.
    **     AIL 20210129 Added support for bulkDataChanged. Improved bulk copy (doing lock and report only once).
    **     OM  20201027 copy() method will update the status flags to allow the record to be correctly saved.
    **     OM  20210404 CLOB fields are created and returned with their CP set.
    **     ECF 20210423 Fixed mismatch between updates to dirtyProps and activePreviousValue, which resulted in
    **                  incorrect before/after diffs being reported. Renamed setAllDatum to setAllData.
    **     ECF 20210519 Added "in-use" tracking to report whether this record is referenced by any buffers or is
    **                  being tracked for UNDO purposes.
    **     ECF 20210805 Properly enable nested calls to set and restore the activeBuffer and related state.
    **     ECF 20210924 Refactored debug string representation.
    **     CA  20210929 Cache the PK getter method.
    **     OM  20211020 Added _DATASOURCE_ROWID property.
    **     CA  20220420 Extracted 'initialize' method which sets the default field values and sets the initial
    **                  state for indices, nulls and validation bitsets.  This is required for Record arguments
    **                  used in a Java REST service, which must not be associated with a session.
    **     ECF 20221005 Added support for dynamic initial values.
    **     TJD 20220504 Upgrade do Java 11 minor changes
    **     CA  20221109 Implemented runtime for EXCEPT/FIELDS options - only NO-LOCK records are loaded as
    **                  'partial/incomplete'.
    **     TJD 20230103 The defaults MUST be initialized BEFORE the updateState call
    **     CA  20220613 'getCodePage' must ignore an empty codepage set at the field.
    **     CA  20230110 getDirtyIndices and getUnvalidatedIndices now return null instead of an empty BitSet, when
    **                  no indices are found.
    **     CA  20221109 Implemented runtime for EXCEPT/FIELDS options - only NO-LOCK records are loaded as
    **                  'partial/incomplete'.
    ** 003 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
    ** 004 EVL 20230605 Fix from OM for crash with updating state of the rolled back record.
    ** 005 SR  20230731 Created getSize() method to return the size of a record in bytes.
    ** 006 AL2 20240409 Reworked references to store both the number of real references and transient references.
    ** 007 SP  20240805 Added new restoreActiveState method.
    ** 008 TJD 20230303 Allow performing a sub-transaction level changeset reset and roll-up
    **     TJD 20230306 Reset changeset changes on roll-up
    **     TJD 20231019 fix resetChangeset function name
    ** 009 AS  20241010 Reworked activeOffset into activeOffsets(BitSet) to support
    **                  validation of a subset of indices.
    **     AS  20241010 Reworked activePropPreviousValues as Object[] to hold multiple values.
    ** 010 AL2 20241105 Added isReferenced API.
    ** 011 ES  20241203 Added new hasIndices method.
    ** 012 AS  20250220 Refactored getActiveUpdateDiff to return the diff for only one property.
    ** 013 AS  20250227 Store the result of applyInitialValues in nullProps, rather than pass it as parameter.
    */

    /*
    ** 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 com.goldencode.p2j.persist.*;
    import com.goldencode.p2j.rest.serializers.*;
    import com.goldencode.p2j.schema.*;
    import com.goldencode.p2j.util.*;
    import com.goldencode.p2j.util.logging.*;

    import java.lang.reflect.*;
    import java.sql.*;
    import java.util.*;
    import java.util.concurrent.atomic.*;

    /**
     * Base record class which stores data and current state for a single record, and which provides services to
     * application-specific, DMO implementation subclasses.
     * <p>
     * Access to the internal state of this class is not thread-safe. Thus, instances are <strong>not</strong>
     * intended to be accessed from more than one thread.
     */
    public abstract class BaseRecord
    implements DmoState,
               Persistable
    {
       /** Logger */
       private static final CentralLogger LOG = CentralLogger.get(BaseRecord.class);

       /** The cached getter method for the PK property. */
       public static final Method PK_GETTER;

       static
       {
          try
          {
             PK_GETTER = BaseRecord.class.getMethod("primaryKey");
          }
          catch (NoSuchMethodException |
                 SecurityException e)
          {
             throw new RuntimeException("Could not resolve PK getter!", e);
          }
       }

       /** Surrogate primary key of the record */
       protected Long id = null;

       /** The data represented by this record, in it's low-level (database friendly) form */
       protected final Object[] data;

       /** For an incomplete record, this holds the fields which were read or assigned in this record. */
       protected BitSet readFields = null;

       /** Current state of the record */
       protected final RecordState state = new RecordState();

       /**
        * Count of references to this DMO; while positive, it must remain in the current session cache.
        * The 4 high bytes represent the number of buffers the record is transiently loaded in.
        * The 4 low bytes represent the number of active buffers the record is loaded in.
        */
       protected long references = 0;

       /** Map of properties touched, each bit corresponds with a property in the data array */
       private final BitSet dirtyProps;

       /** Map of properties currently requiring validation */
       private final BitSet unvalidated;

       /** Map of properties currently set to null, indicating unknown value */
       private final BitSet nullProps;

       /** Tracker to manage which indices have been updated by property updates */
       private final IndexState indexState;

       /** Record identifier (lock variant, which uses table name) for this record (created lazily) */
       private RecordIdentifier<String> recid = null;

       /** The currently active buffer when state is changing */
       private RecordBuffer activeBuffer = null;

       /** The offsets of the properties currently being modified. */
       protected BitSet activeOffsets = null;

       /** The most recent values of the properties currently being modified, for rollback purposes */
       private Object[] activePropPreviousValues = null;

       /** Object which tracks change to enable rollback of the record's data and state */
       private ChangeSet changeSet = null;

       /** Current version number, for stale checks */
       private int version = DmoVersioning.UNVERSIONED;

       /** Shared version data structure, for version updates and stale checks */
       private transient AtomicIntegerArray sharedVersion = null;

       /**
        * Default constructor which can only be invoked by subclasses. This constructor does not initialize
        * most state-related data structures. If these are needed for legacy record behavior, the {@link
        * #initialize(Session, boolean)} method should be invoked after construction.
        */
       protected BaseRecord()
       {
          RecordMeta recMeta = _recordMeta();

          int size = recMeta.getPropertyMeta(false).length;

          // initialize data array
          data = new Object[size];

          // initialize property-level state bit sets
          dirtyProps = new BitSet(size);
          unvalidated = new BitSet(size);
          nullProps = new BitSet(size);

          // track all indices which require full update (and validation) to enable record visibility
          int ulen = recMeta.uniqueIndices.length;
          int nlen = recMeta.nonuniqueIndices.length;
          indexState = new IndexState(ulen, nlen, recMeta.getPrimaryIndexId());
          activeOffsets = new BitSet();
          activePropPreviousValues = new Object[size];
       }

       /**
        * Copy the source record to the destination record.
        * <p>
        * The records are checked first to decide whether they are compatible (I.e. the number of properties
        * matches and also the type for each field, including the optional extent attribute). If not, the method
        * returns {@code false} and not copy operation is executed.
        * <p>
        * If the records pass validation, all properties are copied from source ({@code from}) to destination
        * {@code to}). At the same time, the status flags are updated so that the destination record can be
        * correctly saved.
        *
        * @param   to
        *          The destination record.
        * @param   from
        *          The source record.
        * @param   b4reserved
        *          If {@code true} the before-table specific attributes are copied, too. Otherwise only the normal
        *          properties are processed.
        *
        * @return  {@code true} if the copy was performed (record's tables match).
        */
       public static boolean copy(BaseRecord to, BaseRecord from, boolean b4reserved)
       {
          PropertyMeta[] fromProps = from._recordMeta().getPropertyMeta(false);
          PropertyMeta[] toProps = to._recordMeta().getPropertyMeta(false);

          if (fromProps.length != toProps.length)
          {
             return false;
          }

          for (int i = 0; i < fromProps.length; i++)
          {
             PropertyMeta fromProp = fromProps[i];
             PropertyMeta toProp = toProps[i];

             if (fromProp.getType() != toProp.getType() || fromProp.getExtent() != toProp.getExtent())
             {
                return false;
             }
          }

          for (int i = 0; i < fromProps.length; i++)
          {
             if (!b4reserved)
             {
                switch (fromProps[i].getId())
                {
                   case ReservedProperty.ID_ERROR_FLAG:
                   case ReservedProperty.ID_ERROR_STRING:
                   case ReservedProperty.ID_ROW_STATE:
                   case ReservedProperty.ID_ORIGIN_ROWID:
                   case ReservedProperty.ID_DATASOURCE_ROWID:
                   case ReservedProperty.ID_PEER_ROWID:
                      // skip these attributes/properties
                      continue;
                }
             }

             // TODO: LOB data is mutable, needs to be duplicated explicitly
             boolean change = to.data[i] != from.data[i];
             if (change)
             {
                to.data[i] = from.data[i];

                // update the null property bitset and mark the property as changed to be able to save it
                to.nullProps.set(i, to.data[i] == null);
                to.dirtyProps.set(i);
             }
          }

          // mark it as changed
          to.updateState(CHANGED, true);

          return true;
       }

       /**
        * Populate the record's data with its default, initial values and update the record and
        * property states accordingly. Mark the record as newly created.
        * <p>
        * This method should be invoked only once, immediately after the object is constructed. It
        * is not invoked directly from the constructor, because the only use case which requires the
        * initialization performed here is a new data record being created on behalf of application
        * logic.
        * <p>
        * This is done separately from construction because there are use cases where an instance of
        * this class is needed, but we do not need the overhead of initializing the data with default
        * values, as they will be overwritten anyway. For instance, if the object is read from the
        * database or a copy/snapshot is being made, the data array will be populated with the values
        * retrieved from the database or copied from the other record, respectively.
        * <p>
        * It is not invoked optionally from the constructor, because a default constructor called
        * from subclasses is preferable, as the subclasses are often constructed by reflection.
        * <p>
        * This operation updates the not-null bitset, based on the values of non-nullable columns.
        *
        * @param   session
        *          Database session, which is needed if this record must have its changes tracked for
        *          UNDO/rollback purposes; otherwise, may be {@code null}.
        * @param   setDefaults
        *          If {@code true}, set all default property values; if {@code false}, leave data array
        *          filled with null values.
        */
       public void initialize(Session session, boolean setDefaults)
       {
          // the defaults MUST be initialized BEFORE the updateState call
          initialize(setDefaults);

          // mark the record as newly created; this should be the only place this flag is set;
          // a newly created record is considered invalid until it passes validation, even though
          // its initial data may in fact be valid
          updateState(session, NEW, true);
       }

       /**
        * Initialize this record instance, by setting all fields to their default values and also initializing
        * the {@link #indexState}, {@link #unvalidated} and {@link #nullProps} states.
        * <p>
        * This method should be invoked only once, immediately after the object is constructed. It is not invoked
        * directly from the constructor, because the only use case which requires the initialization performed
        * here is a new data record being created on behalf of application logic.
        * <p>
        * This will be used by, {@link RecordSerializer JSON deserialization} when creating a new record for an
        * instance associated with the web service request (either request or response).
        *
        * @param    setDefaults
        *           If {@code true}, set all default property values; if {@code false}, leave data array
        *           filled with null values.
        */
       public void initialize(boolean setDefaults)
       {
          RecordMeta recMeta = _recordMeta();

          // for a newly created record, all index state bits need to be set; they will be cleared one at a
          // time, as each index is updated
          indexState.setAll(recMeta.uniqueIndices.length, recMeta.nonuniqueIndices.length);

          if (setDefaults)
          {
             nullProps.clear();
             nullProps.or(recMeta.applyInitialValues(data));
             unvalidated.or(recMeta.getValidatableProperties());
          }
       }

       /**
        * Indicate whether this DMO is referenced by any buffer.
        *
        * @return  {@code true} if in use by the above definition, else {@code false}.
        */
       public boolean isReferenced()
       {
          return references > 0;
       }

       /**
        * Indicate whether this DMO is "in use"; that is, whether buffers reference it or it is being tracked
        * for UNDO processing.
        *
        * @return  {@code true} if in use by the above definition, else {@code false}.
        */
       public boolean isInUse()
       {
          return isReferenced() || checkState(TRACKED);
       }

       /**
        * Get the update state of this record's indices.
        *
        * @return  Index state.
        */
       public IndexState getIndexState()
       {
          return indexState;
       }

       /**
        * Get the database identifier for this persistable object. This generally equates to the
        * backing table's primary key. However, it will be {@code null} for DMO instances which have
        * not yet been persisted to the database.
        *
        * @return  Unique, database identifier associated with this object.
        */
       @Override
       public final Long primaryKey()
       {
          return id;
       }

       /**
        * Set the database identifier for this persistable object (i.e., surrogate primary key).
        *
        * @param   id
        *          Unique, database identifier to be associated with this object.
        */
       @Override
       public final void primaryKey(Long id)
       {
          this.id = id;
       }

       /**
        * Copy this record's data and record state, including its primary key. The copy MUST NOT find its way
        * into the {@link Session} cache.
        *
        * @return  Copy of this record.
        */
       public BaseRecord deepCopy()
       {
          BaseRecord copy = null;
          try
          {
             copy = getClass().getDeclaredConstructor().newInstance();
          }
          catch (ReflectiveOperationException exc)
          {
             throw new RuntimeException(exc);
          }

          // copies data and primary key
          copy.copy(this);

          // OR works here because copy's bitsets are initialized to all bits cleared
          copy.dirtyProps.or(this.dirtyProps);
          copy.nullProps.or(this.nullProps);

          // record state
          copy.state.state = this.state.state;

          // record identifier is immutable; ok to reference same object from copy
          copy.recid = this.recid;

          return copy;
       }

       /**
        * Makes this a copy of a source {@code BaseRecord} object. For permanent databases, the
        * internal {@code data} and the {@code id} of the {@code BaseRecord} are copied.
        * <p>
        * The only state copied is that of which properties are dirty (all of them after the copy).
        * Thus, this method is for specialized, internal use only, such as creating a new record in
        * order to copy data from one temp-table buffer to another.
        *
        * @param   from
        *          The source to copy data from.
        */
       public void copy(BaseRecord from)
       {
          // TODO: BLOB data is mutable, needs to be duplicated explicitly
          int len = this.data.length;
          this.id = from.id;
          System.arraycopy(from.data, 0, this.data, 0, len);
          dirtyProps.set(0, len); // TODO: nullProps?
       }

       /**
        * Checks whether a set of record state flags are set for this record.
        *
        * @param   flags
        *          One or more {@code RecordState} flags, bitwise OR'd together.
        *
        * @return  {@code true} if ALL flags are set.
        *
        * @see     RecordState
        */
       public boolean checkState(int flags)
       {
          return (state.state & flags) == flags;
       }

       /**
        * Indicate whether the record has passed validation. A newly created and initialized record
        * or a record which has had any of its properties changed is considered invalid, until it
        * passes validation.
        *
        * NOTE: in fact, this is the state the record after the last validation, it might have been
        *       changes meanwhile so the state might be staled, check the NEED_VALIDATION bit to see
        *       if this is the case and call validation routines
        *
        * @return  {@code true} if this record has passed validation and has not since had any of its
        *          properties changed, else {@code false}.
        */
       public boolean isValid()
       {
          return !checkState(INVALID);
       }

       /**
        * Updates the set of state flags for this record. Depending on {@code set} parameter, the
        * flags can be set (when {@code true}) or removed (when {@code false}). If one flag is already
        * set and {@code set == true} that flag remains unchanged. The same applies for un-setting an
        * inactive flag.
        *
        * @param   session
        *          Database session (used for UNDO purposes).
        * @param   flags
        *          A set of OR-ed {@code RecordState} flags to be updated.
        * @param   set
        *          If {@code true} the flags will be set, otherwise they will be unset.
        *
        * @see     RecordState
        */
       public void updateState(Session session, int flags, boolean set)
       {
          // if this update will actually change the current state, track the change for possible rollback;
          // only track state changes which do not affect the fundamental nature of the record
          if (session != null                             &&
              (state.state & flags) != (set ? flags : 0)  &&
              ((flags & (NOUNDO | COPY)) == 0)            &&
              (set || flags != CACHED))
          {
             try
             {
                int txLevel = prepareChangeSet(session);
                if (txLevel >= 0)
                {
                   changeSet.logStateChange(state.state, txLevel);
                }
             }
             catch (PersistenceException exc)
             {
                // TODO: handle
                LOG.severe("", exc);
             }
          }

          updateState(flags, set);
       }

       /**
        * Get the unique identifier for this record, comprised of its table name and primary key.
        *
        * @return  Record identifier.
        */
       public RecordIdentifier<String> getRecordLockIdentifier()
       {
          if (recid == null)
          {
             recid = new RecordIdentifier<>(getTable(), id);
          }

          return recid;
       }

       /**
        * Report whether the table associated with this record has any (legacy) index.
        *
        * @return  {@code true} if there is at least one legacy index defined in the table of this record.
        */
       public boolean isIndexed()
       {
          return !_recordMeta().allIndexedProps.isEmpty();
       }

       /**
        * Set the record buffer which is currently active for this record. This method is called to set the
        * active buffer temporarily associated with this record before some work is done to update this record's
        * data. This may be an update to a single datum or a bulk update.
        * <p>
        * A call to this method must be bracketed with a call to {@code run} the {@code Runnable} returned by
        * this method. This allows nested invocations and ensures that the active buffer and related state are
        * reset properly after the update work is done.
        *
        * @param   buffer
        *          The buffer which is considered the &quot;active&quot; buffer for a subsequent unit of update
        *          work.
        *
        * @return  A {@code Runnable} which MUST be executed in a {@code finally} block when the update work
        *          for which the active buffer was set is finished.
        */
       public Runnable setActiveBuffer(RecordBuffer buffer)
       {
          // remember the previously active buffer, offset, and datum value for restoration of state
          RecordBuffer previousBuffer = activeBuffer;
          BitSet previousOffsets = (BitSet) activeOffsets.clone();
          Object[] propPreviousValue = activePropPreviousValues.clone();
          boolean propPreviousValueEmpty = true;
          for (int i = 0; i < propPreviousValue.length; i++)
          {
             if (propPreviousValue[i] != null)
             {
                propPreviousValueEmpty = false;
             }
          }

          // set active buffer
          activeBuffer = buffer;

          if (previousBuffer == null && previousOffsets.cardinality() == 0 && propPreviousValueEmpty)
          {
             return this::restoreActiveState;
          }

          return () ->
          {
             // reset active buffer to previous state (null for outermost call; no-op for nested call)
             activeBuffer = previousBuffer;

             // reset remaining state to previous values
             activePropPreviousValues = propPreviousValue.clone();
             activeOffsets = previousOffsets;
          };
       }

       /**
        * Restore to default active buffer, offset, and datum value.
        */
       public void restoreActiveState()
       {
          activeBuffer = null;
          activeOffsets.clear();
          activePropPreviousValues = new Object[_recordMeta().getPropertyMeta(false).length];
       }

       /**
        * Indicate whether any property in the record currently is dirty (i.e., touched, not necessarily changed).
        *
        * @return  {@code true} if any property was touched, else {@code false}.
        */
       public boolean isDirty()
       {
          return !dirtyProps.isEmpty();
       }

       /**
        * Indicate whether all index components for any index on this record are dirty (i.e., have been touched,
        * not necessarily changed..
        *
        * @return  {@code true} if any index is fully dirty.
        */
       public boolean isAnyIndexFullyDirty()
       {
          RecordMeta rm = _recordMeta();

          return isAnyIndexFullyDirty(rm.uniqueIndices) || isAnyIndexFullyDirty(rm.nonuniqueIndices);
       }

       /**
        * Indicate whether tables have any indices.
        *
        * @return  {@code true} if the table have any index.
        */
       public boolean hasIndices()
       {
          RecordMeta rm = _recordMeta();

          return rm.uniqueIndices.length > 0 || rm.nonuniqueIndices.length > 0;
       }

       // Undo the current/active update
       public void undoActive()
       {
          // TODO: complete this implementation: what about state? we probably need to preserve the
          //       previous state and roll this back as well
          for (int i = activeOffsets.nextSetBit(0); i != -1; i = activeOffsets.nextSetBit(i + 1))
          {
             data[i] = activePropPreviousValues[i];
          }
       }

       /**
        * Return a string representation of this object, primarily for debug purposes.
        *
        * @return  String representation of this object.
        */
       @Override
       public String toString()
       {
          return toString(true);
       }

       /**
        * Return a string representation of this object, primarily for debug purposes, optionally including the
        * record's data in the output.
        *
        * @param   includeData
        *          {@code true} to include the record's data. If {@code false}, all other state still will be
        *          included.
        *
        * @return  String representation of this object.
        */
       public String toString(boolean includeData)
       {
          String sep = System.lineSeparator();

          StringBuilder buf = new StringBuilder(_recordMeta().tables[0]);
          buf.append(':').append(primaryKey()).append(sep)
             .append(state).append(sep)
             .append("references: ").append(references).append(sep)
             .append("dirty: ").append(dirtyProps).append(sep)
             .append("unvalidated: ").append(unvalidated);

          if (includeData)
          {
             int len = data.length;
             buf.append(sep).append("data: {");
             for (int i = 0; i < len; i++)
             {
                if (i > 0)
                {
                   buf.append(", ");
                }

                buf.append(data[i]);
             }

             buf.append('}');
          }

          return buf.toString();
       }

       /**
        * This accessor allows direct access to private {@code data} array <strong>only to</strong>
        * instances of {@code PropertyMapper}. This is needed because the these instances need direct
        * and fast access the {@code data} and avoid normal status management of a record when the
        * properties are accessed using the {@code Record}'s setters and getters. Instances of
        * {@code PropertyMapper} are created only when the import is run so that management is not
        * required nor needed.
        *
        * @param   pm
        *          The {@code PropertyMapper} which requests access.
        *
        * @return  The reference to internal {@code data} array if the passed parameter is a
        *          {@code PropertyMapper}
        *
        * @throws  NullPointerException
        *          If the passed in parameter is {@code null}.
        */
       public Object[] getData(PropertyMapper pm)
       {
          Objects.requireNonNull(pm);

          return data;
       }

       /**
        * This accessor allows direct access to private {@code data} array <strong>only to</strong>
        * instances of {@code RecordBuffer}.
        *
        * @param   rb
        *          The {@code RecordBuffer} which requests access.
        *
        * @return  The reference to internal {@code data} array if the passed parameter is a
        *          {@code RecordBuffer}
        *
        * @throws  NullPointerException
        *          If the passed in parameter is {@code null}.
        */
       public Object[] getData(RecordBuffer rb)
       {
          Objects.requireNonNull(rb);

          return data;
       }

       /**
        * Indicate whether any of this record's validatable properties are dirty and have not been validated.
        *
        * @return  {@code true} if record needs validation, else {@code false}.
        */
       public boolean needsValidation()
       {
          return !unvalidated.isEmpty();
       }

       /**
        * Get a bit set representing dirty unique or non-unique indices, subject to the conditions described by
        * the parameters. A set bit in the returned set indicates a dirty index. The position of each set bit
        * corresponds with the 0-based position of the associated index in the array of unique or non-unique
        * indices for the record.
        *
        * @param   dirtyOffsets
        *          Optional offsets of dirty properties which must be a component of the indices returned.
        * @param   unique
        *          {@code true} to return dirty unique indices; {@code false} to return dirty non-unique indices.
        * @param   remaining
        *          If {@code true}, only <em>unvalidated</em> indices are returned. Otherwise, validation is not
        *          considered.
        *
        * @return  A bit set, as described above.  <code>null</code> is returned when no indices are found.
        */
       public BitSet getDirtyIndices(BitSet dirtyOffsets, boolean unique, boolean remaining)
       {
          RecordMeta recMeta = _recordMeta();
          BitSet filter = new BitSet();
          BitSet[] indices = unique ? recMeta.uniqueIndices : recMeta.nonuniqueIndices;

          if (remaining)
          {
             filter.or(indexState.getBitSet(unique));
          }
          else
          {
             filter.set(0, indices.length);
          }

          return getDirtyIndices(indices, dirtyOffsets, filter, (state.state & NEW) == NEW);
       }

       /**
        * Get an Object array representing the old and new values of the property currently being changed
        * with the properties' offsets as keys and a two element arrays as values.
        * The first element is the previous value of the property; the second is the new value. If the property
        * has not been touched or the old and new values are the same, {@code null} is returned.
        * <p>
        * <strong>Note:</strong> this method will only return a non-null result while a property update is
        * active; that is, while a DMO property is being updated via the {@code RecordBuffer} invocation handler.
        * It is only at that moment that the previous value for the property being changed is guaranteed to
        * contain meaningful data.
        *
        * @param   ormIndex
        *          The index of the property.
        * @param   extentIndex
        *          The offset index of the extent.
        *
        * @return  See above.
        */
       public Object[] getActiveUpdateDiff(int ormIndex, int extentIndex)
       {
          int actualIndex = ormIndex + extentIndex;
          if (dirtyProps.get(actualIndex) && !Objects.equals(activePropPreviousValues[actualIndex],
                                                            data[actualIndex]))
          {
             Object[] res = new Object[2];
             res[0] = activePropPreviousValues[actualIndex];
             res[1] = data[actualIndex];
             return res;
          }
          return null;
       }

       /**
        * Return the set of bits which are marked dirty. For performance reasons, the object is not copied before
        * being returned. The caller MUST NOT alter its state!
        *
        * @return  Bit set of dirty properties (those that have been touched, not necessarily changed).
        */
       public BitSet getDirtyProps()
       {
          return dirtyProps;
       }

       /**
        * Get the {@link #readFields}.
        *
        * @return   The {@link #readFields}.
        */
       public BitSet _getReadFields()
       {
          return readFields;
       }

       /**
        * Set a single datum into the data array, and update the record and property state accordingly.
        *
        * @param   offset
        *          Zero-based offset of the datum in the data array.
        * @param   datum
        *          Value to be set into the data array.
        */
       protected void setDatum(int offset, Object datum)
       {
          if (readFields != null)
          {
             readFields.set(offset);
          }

          if (activeBuffer != null)
          {
             activeOffsets.set(offset);
          }

          // update the bitset of dirty (touched) properties
          dirtyProps.set(offset);

          // temporarily save the old value (even if it is the same as the new value)
          activePropPreviousValues[offset] = data[offset];

          // for BigDecimal, equals checks the scale - we need compareTo, to check if the values are really the
          // same, regardless of the scale value.
          if (!(Objects.equals(data[offset], datum) ||
                (datum instanceof Comparable &&
                 data[offset] != null        &&
                 ((Comparable) datum).compareTo(data[offset]) == 0)))
          {
             // ensure we have an exclusive lock on the record
             if (!lockForUpdate())
             {
                // do not pass go, do not collect $200
                return;
             }

             try
             {
                dataChanged(offset, datum);
             }
             catch (PersistenceException exc)
             {
                throw new RuntimeException(exc);
             }

             // update the value in the data array
             data[offset] = datum;
          }
       }

       /**
        * Set all data in the data array. This works only if the source has the same structure as the
        * destination (this). A prevalidation should be done - this will only check integrity, type and extents.
        * The order of the matching will be from left to right and it will avoid reserved properties.
        *
        * @param   source
        *          A record from which the data will be copied to this data array.
        *
        * @return  {@code true} if a valid copying could be done; so the properties match in type and extent and
        *          the number of properties match (except the reserved ones).
        */
       protected boolean setAllData(BaseRecord source)
       {
          PropertyMeta[] fromProps = source._recordMeta().getPropertyMeta(false);
          PropertyMeta[] toProps = this._recordMeta().getPropertyMeta(false);
          Map<Integer, Integer> mapping = new HashMap<>();
          int i, j;

          // do the mapping and check if this copy makes sense (we have a correct mapping)
          for (i = 0, j = 0; i < fromProps.length && j < toProps.length;)
          {
             PropertyMeta fromProp = fromProps[i];
             PropertyMeta toProp = toProps[j];
             if (fromProp.isReserved())
             {
                i++;
                continue;
             }
             if (toProp.isReserved())
             {
                j++;
                continue;
             }

             if (fromProp.getType()   != toProp.getType() ||
                 fromProp.getExtent() != toProp.getExtent())
             {
                return false;
             }

             mapping.put(j, i);
             j++;
             i++;
          }

          // the mapping is broken; the source can't be copied here so this method was used incorrectly
          if (!(i == fromProps.length && j == toProps.length))
          {
             return false;
          }

          // do an initial filtering of the offsets which should be updated
          List<Integer> updatedOffsets = null;
          List<Object> updatedDatums = null;
          for (Map.Entry<Integer, Integer> entry : mapping.entrySet())
          {
             Integer toOffset = entry.getKey();
             Integer fromOffset = entry.getValue();
             Object datum = source.getDatum(fromOffset);

             // update the bitset of dirty properties
             dirtyProps.set(toOffset);
             activeOffsets.set(toOffset);

             // for BigDecimal, equals checks the scale - we need compareTo, to check if the values are really the
             // same, regardless of the scale value.
             if (!(Objects.equals(data[toOffset], datum) ||
                  (datum instanceof Comparable &&
                   data[toOffset] != null &&
                   ((Comparable) datum).compareTo(data[toOffset]) == 0)))
             {
                if (updatedOffsets == null)
                {
                   updatedOffsets = new ArrayList<>();
                   updatedDatums = new ArrayList<>();
                }

                updatedOffsets.add(toOffset);
                updatedDatums.add(datum);
             }
          }

          // we shouldn't do any copy as no offset will be updated
          if (updatedOffsets == null || updatedOffsets.size() == 0)
          {
             return true;
          }

          // we will do a copy, so upgrade lock at first
          if (!lockForUpdate())
          {
             return false;
          }

          try
          {
             // notify data change in bulk mode
             // NOTE: this does not set activeOffset as we try to update all offsets at once!
             bulkDataChanged(updatedOffsets, updatedDatums);
          }
          catch (PersistenceException exc)
          {
             throw new RuntimeException(exc);
          }

          // finally, do the data set
          for (int idx = 0; idx < updatedOffsets.size(); idx++)
          {
             int offset = updatedOffsets.get(idx);
             Object datum = updatedDatums.get(idx);
             data[offset] = datum;
          }

          return true;
       }

       /**
        * Get the field's value on the specified offset.
        *
        * @param    offset
        *           Zero-based offset of the datum in the data array.
        *
        * @return   The field's value.
        */
       protected Object getDatum(int offset)
       {
          return data[offset];
       }

       /**
        * Get the static record metadata for the concrete subclass.
        *
        * @return  Static record metadata for the subclass.
        */
       protected abstract RecordMeta _recordMeta();

       /**
        * Obtain the codepage of the {@code clob} object at position {@code offset}.
        * <p>
        * Note: in order for this method to work, the {@code activeBuffer} must be set in advance. After the
        *       method was invoked, it is recommended to clear its value.
        *
        * @param   offset
        *          The position for {@code clob} field.
        *
        * @return  The 4GL name of codepage of the {@code clob} field at position {@code offset}. If a codepage is
        *          not specified or if the field is not a {@code clob}, {@code null} is returned.
        */
       protected String getCodePage(int offset)
       {
          if (activeBuffer == null)
          {
             return null;
          }

          TempTable parentTable = activeBuffer.getParentTable();
          Property prop = activeBuffer.getDmoInfo().getProperty(offset);
          String cp = null;
          if (parentTable != null)
          {
             cp = parentTable.getCodePage(prop.name);
          }
          else
          {
             // must be a permanent table, use TableMapper
             TableMapper.LegacyFieldInfo lfi = TableMapper.getLegacyFieldInfo(activeBuffer, prop.name);
             if (lfi != null)
             {
                cp = lfi.getCodePage();
             }
          }

          return cp == null || cp.isEmpty() ? null : cp;

    //      Property property = activeBuffer.getDmoInfo().getProperty(offset);
    //      if (property == null || property.codePage.isEmpty())
    //      {
    //         return null;
    //      }
    //
    //      return property.codePage;
       }

       /**
        * Check if for an incomplete record, the specified field was read or assigned.
        *
        * @param    index
        *           The field index.
        */
       protected void checkIncomplete(int index)
       {
          if (checkState(DmoState.INCOMPLETE) && !readFields.get(index))
          {
             RecordMeta meta = _recordMeta();
             String tname = meta.legacyName;
             String fname = meta.getPropertyMeta(false)[index].getLegacyName();
             String msg = "Field " + fname + " from " + tname + " record (recid " + primaryKey() +
                           ") was missing from FIELDS phrase";

             ErrorManager.recordOrThrowError(8826, msg, false);
          }
       }

       /**
        * Mark this record as incomplete.
        *
        * @param    session
        *           Database session (used for UNDO purposes).
        */
       void markIncomplete(Session session)
       {
          updateState(session, DmoState.INCOMPLETE, true);
          readFields = new BitSet(data.length);
       }

       /**
        * An index was updated.
        *
        * @param   idxUid
        *          Index identifier.
        */
       void indexUpdated(int idxUid)
       {
          indexState.clear(idxUid);
       }

       /**
        * Compare the bitsets in the given array with the bitset of dirty properties for this record
        * and indicate whether all index components for any index in the array are dirty. The
        * positions of the bits correspond with the positions of the properties in the record's data
        * array.
        *
        * @param   indices
        *          Array of bitsets, where each set bit represents a component of an index.
        *
        * @return  {@code true} if all of the components of any index represented in the array are
        *          dirty, else {@code false}.
        */
       boolean isAnyIndexFullyDirty(BitSet[] indices)
       {
          int len = indices.length;
          for (int i = 0; i < len; i++)
          {
             BitSet orig = indices[i];
             BitSet copy = (BitSet) orig.clone();
             copy.and(dirtyProps);
             if (copy.equals(orig))
             {
                return true;
             }
          }

          return false;
       }

       /**
        * Mark all dirty properties as having been validated.
        */
       void markValidated()
       {
          unvalidated.andNot(dirtyProps);
       }

       /**
        * Mark properties as having been validated.
        *
        * @param   bitset
        *          A bitset of offsets representing the properties which has been validated.
        */
       void markValidated(BitSet bitset)
       {
          unvalidated.andNot(bitset);
       }

       /**
        * Get a bit set representing the unique indices, if any, which are either fully or partially updated
        * and which have not already been validated. If the record is newly created and has not yet been
        * flushed to the database, a fully updated index is required; otherwise, a partially updated index is
        * sufficient.
        *
        * @param   dirtyOffsets
        *          Optional offsets of  dirty properties which must be a component of the indices returned.
        *          An unvalidated indices bitset is computed for each dirty offset and is being added to the
        *          resulting bitset.
        * @param   allUnvalidated
        *          If {@code true}, then any unique index containing any property not previously validated is
        *          included in the returned bit set. If {@code false}, then only indices containing properties
        *          that have been explicitly made dirty are included. Setting this to {@code true} effectively
        *          considers all properties with untouched, default values in the check.
        *
        * @return  A bit set as described above.  <code>null</code> is returned if no index is found.
        */
       BitSet getUnvalidatedIndices(BitSet dirtyOffsets, boolean allUnvalidated)
       {
          if (dirtyProps.cardinality() == 0 && unvalidated.cardinality() == 0 && !allUnvalidated)
          {
             return new BitSet();
          }
          BitSet unvalidatedIndices = new BitSet();
          int dirtyOffset = dirtyOffsets.nextSetBit(0);
          boolean dirtyPropsEmpty = dirtyProps.isEmpty();
          do {
             if (!allUnvalidated && (dirtyPropsEmpty || (dirtyOffset >= 0 && !unvalidated.get(dirtyOffset))))
             {
                dirtyOffset = dirtyOffsets.nextSetBit(dirtyOffset + 1);
                continue;
             }
             BitSet result = null;
             boolean isNew = (state.state & NEW) == NEW;
             BitSet[] unique = _recordMeta().uniqueIndices;
             int len = unique.length;

             for (int i = 0; i < len; i++) {
                BitSet nextUnique = unique[i];

                if (allUnvalidated) {
                   // if the unique index contains any unvalidated property (including untouched default values),
                   // include it in the set of indices which require validation
                   if (nextUnique.intersects(unvalidated)) {
                      if (result == null) {
                         result = new BitSet();
                      }
                      result.set(i);
                   }

                   continue;
                }

                // skip this one if:
                // * an offset is provided, but it is not a component of the index; or
                // * no offset is provided, but none of the unvalidated properties are a component of the index
                if ((dirtyOffset >= 0 && !nextUnique.get(dirtyOffset)) ||
                        (dirtyOffset < 0 && !nextUnique.intersects(unvalidated))) {
                   continue;
                }

                if (isNew) {
                   // every component of the index must have been touched
                   BitSet copy = (BitSet) nextUnique.clone();
                   copy.and(dirtyProps);
                   if (copy.equals(nextUnique)) {
                      if (result == null) {
                         result = new BitSet();
                      }
                      result.set(i);
                   }
                } else {
                   // any component of the index must have been touched; the tests above already have required
                   // this, so just set the bit in the result
                   if (result == null) {
                      result = new BitSet();
                   }
                   result.set(i);
                }
             }
             if (result != null) {
                unvalidatedIndices.or(result);
             }
             dirtyOffset = dirtyOffsets.nextSetBit(dirtyOffset + 1);
          }
          while (dirtyOffset != -1);
          return unvalidatedIndices;
       }

       /**
        * Get the offsets of the properties currently being updated, if any. When a DMO's setter method
        * is invoked, the DMO goes into an &apos;active&apos; state, during which the offsets of the
        * properties being updated are stored temporarily as the active offsets. When the update is
        * complete, no property is considered active.
        * <p>
        * The active state may be reset by this method. This should only be done from a {@link
        * Validation} object.
        *
        * @return  A BitSet representing the offsets of properties being updated.
        */
       BitSet getActiveOffsets()
       {
          return (BitSet) activeOffsets.clone();
       }

       /**
        * Reset record and property state after database is synchronized with the record's data
        * (i.e., the most recent set of changes have been persisted).
        * <p>
        * This method does not turn off the {@code INVALID} flag.
        */
       void clearDirtyState(Session session)
       throws PersistenceException
       {
          dirtyProps.clear();
          unvalidated.clear();
          indexState.clearAll();
          updateState(session, CHANGED, false);
       }

       /**
        * Indicate whether the property at the given offset in the DMO's data array is dirty (i.e., has been
        * touched, but not necessarily changed).
        *
        * @param   offset
        *          Zero-based offset of the target property.
        *
        * @return  {@code true} if the property is dirty, else {@code false}.
        */
       boolean isPropertyDirty(int offset)
       {
          return dirtyProps.get(offset);
       }

       /**
        * Get a bit set indicating the positions of data elements which currently are set to {@code null}.
        *
        * @return  Null properties bit set.
        */
       BitSet getNullProps()
       {
          return nullProps;
       }

       /**
        * Read a property directly from a {@code ResultSet}.
        *
        * @param   propOffset
        *          The index of the property to be read.
        * @param   rs
        *          The result set to read from.
        * @param   rsOffset
        *          The index in {@code ResultSet} to read from. If the property is a multiple column
        *          then more than one column will ve read from {@code ResultSet}.
        *
        * @return  The number of columns actually read from the {@code ResultSet}.
        */
       final int readProperty(int propOffset, ResultSet rs, int rsOffset)
       throws PersistenceException
       {
          return readProperty(propOffset, rs, rsOffset, false);
       }

       /**
        * Read a property directly from a {@code ResultSet}.
        *
        * @param   propOffset
        *          The index of the property to be read.
        * @param   rs
        *          The result set to read from.
        * @param   rsOffset
        *          The index in {@code ResultSet} to read from. If the property is a multiple column
        *          then more than one column will ve read from {@code ResultSet}.
        * @param   skip
        *          Flag indicating if this property will not be read from the result-set.
        *
        * @return  The number of columns actually read from the {@code ResultSet}.
        */
       final int readProperty(int propOffset, ResultSet rs, int rsOffset, boolean skip)
       throws PersistenceException
       {
          PropertyMeta pm = _recordMeta().getPropertyMeta(false)[propOffset];
          PropertyReader propertyReader = pm.getDataHandler();
          if (skip)
          {
             return propertyReader.propertySize();
          }

          try
          {
             if (readFields != null)
             {
                readFields.set(propOffset);
             }

             return propertyReader.readProperty(rs, rsOffset, data, propOffset);
          }
          catch (java.sql.SQLException sqle)
          {
             throw new PersistenceException(sqle);
          }
       }

       /**
        * Set this record's version to the canonical, shared version number for this record.
        *
        * @param   sharedVersion
        *          DMO version information.
        */
       void version(AtomicIntegerArray sharedVersion)
       {
          this.sharedVersion = sharedVersion;
          this.version = sharedVersion.get(DmoVersioning.VERSION);
       }

       /**
        * Indicate whether this record's data is stale, based on changes committed by other user contexts.
        * <p>
        * Always returns {@code false} for temp-table records.
        *
        * @return  {@code true} if this record's version does not match the canonical, shared version of the
        *          record known to the FWD runtime; {@code false} if the versions match or if versioning is
        *          disabled for this record.
        */
       boolean isStale()
       {
          if (sharedVersion == null)
          {
             return false;
          }

          return version != sharedVersion.get(DmoVersioning.VERSION);
       }

       /**
        * The current application transaction has been committed. We do not need to maintain a change set any
        * longer. This is only invoked if this record was modified during the transaction, so increment this
        * DMO's version and the shared version.
        */
       void commit()
       {
          // discard tracked changes for UNDO
          changeSet = null;

          // update the canonical, shared version of this record
          if (sharedVersion != null)
          {
             // note: we rely on the fact that SavepointManager (which invokes this method from its
             // Block.commit() method) registers for master transaction commit AFTER the caller of
             // Session.commit(), so the transaction should have been committed before this method is invoked
             do
             {
                version = sharedVersion.incrementAndGet(DmoVersioning.VERSION);
             } while (version == DmoVersioning.UNVERSIONED);
          }
       }

       /**
        * Perform a full or sub-transaction level rollback of this 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.
        */
       void rollback(int txLevel)
       throws PersistenceException
       {
          if (changeSet == null)
          {
             // this should not happen; we should only get this call if this record had at least one change
             // during an application transaction
             return;
          }

          changeSet.rollback(txLevel, this);

          // discard the change set after rollback of the full transaction
          if (txLevel == 0)
          {
             changeSet = null;
          }
       }

       /**
        * Perform a sub-transaction level changeset reset of this record
        * @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.
        */
       void resetChangeSet(int txLevel)
       {
          if (changeSet == null)
          {
             // this should not happen; we should only get this call if this record had at least one change
             // during an application transaction
             return;
          }

          changeSet.resetTxLevel(txLevel);

       }

       /**
        * Perform a sub-transaction level changeset roll-up of this record
        * @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.
        */
       public void rollUpChanges(int txLevel)
       {
          if (changeSet == null)
          {
             // this should not happen; we should only get this call if this record had at least one change
             // during an application transaction
             return;
          }
          changeSet.rollUpChanges(txLevel);
          resetChangeSet(txLevel+1);
       }

       /**
        * Get a bit set representing dirty unique or non-unique indices, subject to the conditions described by
        * the parameters. A set bit in the returned set indicates a dirty index. The position of each set bit
        * corresponds with the 0-based position of the associated index in the array of unique or non-unique
        * indices for the record.
        *
        * @param   indices
        *          The array of bit sets representing the indices to be checked. This must be either the set of
        *          unique indices or non-unique indices associated with the record. Only one or the other can be
        *          checked by a call to this method.
        * @param   dirtyOffsets
        *          Optional offsets of  dirty properties which must be a component of the indices returned.
        * @param   filter
        *          An inclusive filter to apply to the results. If non-empty, each bit in the filter's bit set
        *          represents the position of an index to be checked in the {@code indices} parameter. If empty,
        *          the returned bit set will be empty.
        * @param   full
        *          If {@code true}, ALL index components must be dirty for an index to be considered dirty.
        *          If {@code false}, one or more index components must be dirty for an index to be considered
        *          dirty.
        *
        * @return  A bit set, as described above.  <code>null</code> is returned when no indices are found.
        */
       private BitSet getDirtyIndices(BitSet[] indices, BitSet dirtyOffsets, BitSet filter, boolean full)
       {
          BitSet result = new BitSet();
          int dirtyOffset = dirtyOffsets.nextSetBit(0);
          do
          {
             if (filter.isEmpty() || (dirtyOffset >= 0 && !dirtyProps.get(dirtyOffset)))
             {
                // filter is set to filter ALL indices OR property at offset is not dirty, so there can be no
                // fully dirty indices containing the given property
                dirtyOffset = dirtyOffsets.nextSetBit(dirtyOffset + 1);
                continue;
             }

             BitSet dirty = null;

             for (int i = filter.nextSetBit(0); i >= 0; i = filter.nextSetBit(i + 1))
             {
                BitSet orig = indices[i];

                if (dirtyOffset >= 0 && !orig.get(dirtyOffset))
                {
                   continue;
                }

                BitSet copy = (BitSet) orig.clone();
                copy.and(dirtyProps);
                if (full ? copy.equals(orig) : !copy.isEmpty())
                {
                   if (dirty == null)
                   {
                      dirty = new BitSet();
                   }
                   dirty.set(i);
                }
             }
             if (dirty != null)
             {
                result.or(dirty);
             }
             dirtyOffset = dirtyOffsets.nextSetBit(dirtyOffset + 1);
          }
          while(dirtyOffset != -1);
          return result;
       }

       /**
        * A property's value has changed. Update the record's internal state accordingly and set up rollback
        * information if the record is undoable.
        *
        * @param   offset
        *          The datum's offset in the record's data array.
        * @param   datum
        *          The new value of the datum (may be {@code null}).
        */
       private void dataChanged(int offset, Object datum)
       throws PersistenceException
       {
          if (activeBuffer == null)
          {
             return;
          }

          // update the unvalidated properties bitset
          if (_recordMeta().validatable.get(offset))
          {
             unvalidated.set(offset);
          }

          // update the null property bitset
          nullProps.set(offset, datum == null);

          int flags = CHANGED;
          updateState(flags, true);

          int txLevel = prepareChangeSet(activeBuffer.getSession());
          if (txLevel >= 0)
          {
             changeSet.logStateChange(flags, txLevel);
             changeSet.logDataChange(txLevel, offset, datum);
          }
       }

       /**
        * Multiple property values have changed. Update the record's internal state accordingly and set up rollback
        * information if the record is undoable.
        *
        * @param   offsets
        *          A list of offsets for the changed values in record's data array.
        * @param   datums
        *          A list of new values for the changed datums.
        */
       private void bulkDataChanged(List<Integer> offsets, List<Object> datums)
       throws PersistenceException
       {
          if (activeBuffer == null)
          {
             return;
          }

          for (int i = 0; i < offsets.size(); i++)
          {
             int offset = offsets.get(i);
             Object datum = datums.get(i);

             // update the unvalidated properties bitset
             if (_recordMeta().validatable.get(offset))
             {
                unvalidated.set(offset);
             }

             // update the null property bitset
             nullProps.set(offset, datum == null);
          }

          int flags = CHANGED;
          updateState(flags, true);

          int txLevel = prepareChangeSet(activeBuffer.getSession());
          if (txLevel >= 0)
          {
             changeSet.logStateChange(flags, txLevel);
             for (int i = 0; i < offsets.size(); i++)
             {
                int offset = offsets.get(i);
                Object datum = datums.get(i);
                changeSet.logDataChange(txLevel, offset, datum);
             }
          }
       }

       /**
        * Updates the set of state flags for this record. Depending on {@code add} parameter, the
        * flags can be set (when {@code true}) or removed (when {@code false}. If one flag is already
        * set and {@code add == true} that flag remains unchanged. The same stands for un-setting a
        * not active flag.
        *
        * @param   flags
        *          A set of OR-ed {@code RecordState} flags to be updated.
        * @param   set
        *          If {@code true} the flags will be set, otherwise they will be unset.
        *
        * @see     RecordState
        */
       private void updateState(int flags, boolean set)
       {
          if (set)
          {
             state.state |= flags;
          }
          else
          {
             state.state &= ~flags;
          }
       }

       /**
        * If this is the first update to this record since an application transaction began, prepare a
        * change set to track updates made to the record during the transaction, to be used for full or
        * sub-transaction rollback, if needed.
        * <p>
        * For a change set to be prepared, the following conditions must be met:
        * <ul>
        * <li>a record buffer must be associated with the record, indicating that this call is being made
        *     in the context of a business logic call into the record;</li>
        * <li>the record must be associated with an undoable table;</li>
        * <li>an application-level transaction must currently be active.</li>
        * </ul>
        * <p>
        * If a change set for the current transaction already exists, another is not created.
        * <p>
        * A non-negative return value indicates a change set exists and is ready to track changes.
        *
        * @param   session
        *          Database session to be interrogated for transaction information.
        *
        * @return  The current transaction nesting level, or -1 if any of the above conditions are not met.
        */
       private int prepareChangeSet(Session session)
       throws PersistenceException
       {
          Long txNestingId;

          if ((state.state & NOUNDO) == NOUNDO ||
              (state.state & COPY) == COPY ||
              session == null ||
              (txNestingId = session.getTxNestingId()) == null)
          {
             // record is not in a valid state for tracking undoable changes
             return -1;
          }

          // high order 2 words of transaction nesting ID are nesting level, low order 2 words are
          // transaction ID
          int txLevel = (int) ((txNestingId & (0xFFFFFFFFL << 32)) >>> 32);
          int txId = (int) (txNestingId & (0xFFFFFFFFL));

          if (txLevel >= 0)
          {
             // create a change set if one with the same transaction ID does not already exist
             if (changeSet == null || !changeSet.verify(session, txId))
             {
                changeSet = new ChangeSet(session, txId, data, state.state, txLevel);
             }

             // register this undoable record with the savepoint manager at the current transaction nesting
             // level
             session.trackUndoable(this);
          }

          return txLevel;
       }

       /**
        * Obtain an exclusive lock for the current record on behalf of the active buffer, if any.
        *
        * @return  {@code true} if the lock is successfully obtained, or if it is unneeded due to system state,
        *          or if there is no active buffer. {@code false} if there was an error obtaining the lock (and
        *          we are in silent error mode).
        */
       private boolean lockForUpdate()
       {
          if (DatabaseManager.isInitializing())
          {
             // no locking needed at this time
             return true;
          }

          // TODO: in certain, unstudied cases [activeBuffer] remains null, causing a NPE to be thrown
          if (activeBuffer == null)
          {
             //Exception exc = new RuntimeException("BaseRecord.lockForUpdate(activeBuffer == null)");
             //LOG.log(Level.FINER, "", exc);
             return true;
          }

          try
          {
             // try to get an exclusive lock
             activeBuffer.upgradeLock();

             return true;
          }
          catch (PersistenceException exc)
          {
             ErrorManager.recordOrThrowError(
                142,
                "Unable to update " + _recordMeta().tables[0] + " Field",
                exc);

             return false;
          }
       }

       /**
        * Get the name of the primary table associated with this record.
        *
        * @return  Primary table name.
        */
       private String getTable()
       {
          String[] tables = _recordMeta().tables;

          return (tables != null && tables.length > 0) ? tables[0] : "";
       }

       /**
        * Get the size of the record.
        *
        * @return Size of the record in bytes.
        */
       public long getSize()
       {
          PropertyMeta[] pm = _recordMeta().getPropertyMeta(false);
          long size = 0;
          for (int i = 0; i < data.length; i++)
          {
             size += pm[i].getDataHandler().getFieldSizeInIndex(data[i]);
          }
          return size;
       }
    }