# DmoVersioning.java

    /*
    ** Module   : DmoVersioning.java
    ** Abstract : Manages the versions of database records to enable stale record detection
    **
    ** Copyright (c) 2020-2025, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
    ** 001 ECF 20200903 Created initial version.
    ** 002 CA  20250303 Added 'increment', used to invalidate cross-session cached records, when they are modified
    **                  via SQL executed against the database.
    */

    /*
    ** 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.concurrent.*;
    import java.util.concurrent.atomic.*;
    import com.goldencode.p2j.persist.*;

    /**
     * Provides version tracking services for data model objects (DMOs), to allow a user context to verify that
     * a record it has in its session cache represents the most recent version of that record in the database.
     * One instance of this class is created for each persistent database. Temp-table records are not versioned,
     * since they are local to a particular user context.
     * <p>
     * <strong>Note:</strong> This class keeps track of changes made within the FWD application server only.
     * No version information is stored within the database, as that would involve an intrusive schema change.
     * So, if a record is modified externally, this tracker will be unaware of the change.
     */
    public class DmoVersioning
    {
       /** DMO version value which indicates the DMO is not currently being tracked for versioning */
       static final int UNVERSIONED = -1;

       /** Index of current version in atomic integer array */
       static final int VERSION = 0;

       /** Index of reference count in atomic integer array */
       private static final int REFCOUNT = 1;

       /** Map of record identifier to current version information for tracked records */
       private ConcurrentMap<RecordIdentifier<String>, AtomicIntegerArray> versions = new ConcurrentHashMap<>();

       /**
        * Default constructor.
        */
       public DmoVersioning()
       {
       }

       /**
        * Increment the version of the record with the specified identifier.
        *
        * @param    ident
        *           Record identifier unique to the associated database.
        */
       public void increment(RecordIdentifier<String> ident)
       {
          AtomicIntegerArray v = versions.get(ident);
          if (v != null)
          {
             int version = UNVERSIONED;
             do
             {
                version = v.incrementAndGet(VERSION);
             }
             while (version == UNVERSIONED);
          }
       }

       /**
        * Increment the version of all the records from the specified table.
        *
        * @param    dmoImplName
        *           The DMO implementation name.
        */
       public void increment(String dmoImplName)
       {
          versions.forEach((recordIdent, v) ->
          {
             if (recordIdent.getTable().equals(dmoImplName))
             {
                int version = UNVERSIONED;
                do
                {
                   version = v.incrementAndGet(VERSION);
                }
                while (version == UNVERSIONED);
             }
          });
       }

       /**
        * Begin tracking the record represented by the given identifier. If it already is being tracked, its
        * reference count is incremented.
        * <p>
        * Since the data structure uses reference counting to determine whether any user contexts are interested
        * in this record, it is essential that this method and {@link #deregister(RecordIdentifier)} are called
        * in matching pairs for any record. Otherwise the reference count could get out of balance, creating a
        * memory leak, or releasing an entry too soon.
        *
        * @param   ident
        *          Record identifier unique to the associated database.
        *
        * @return  The atomic integer array which can be used to get and update version and reference count
        *          information for the given key's corresponding record.
        */
       public AtomicIntegerArray register(RecordIdentifier<String> ident)
       {
          // mutable flag accessible to mapping function to ensure atomic integer array values are only
          // modified once, if mapping function is executed multiple times
          boolean[] modified = new boolean[] { false };

          AtomicIntegerArray info = versions.compute(ident, (k, v) ->
          {
             if (v == null)
             {
                v = new AtomicIntegerArray(2);
             }

             if (!modified[0])
             {
                // update flag to ensure atomic integer array is only modified once per track request
                modified[0] = true;

                // value of atomic integer at element 1 in the array is the reference count of sessions who
                // are interested in the version of this DMO
                v.incrementAndGet(REFCOUNT);
             }

             return v;
          });

          return info;
       }

       /**
        * Stop tracking the
        * <p>
        * Since the data structure uses reference counting to determine whether any user contexts are interested
        * in this record, it is essential that this method and {@link #register(RecordIdentifier)} are called
        * in matching pairs for any record. Otherwise the reference count could get out of balance, creating a
        * memory leak, or releasing an entry too soon.
        *
        * @param   ident
        *          Record identifier unique to the associated database.
        */
       public void deregister(RecordIdentifier<String> ident)
       {
          // mutable flag accessible to mapping function to ensure atomic integer array values are only
          // modified once, if mapping function is executed multiple times
          boolean[] modified = new boolean[] { false };

          versions.computeIfPresent(ident, (k, v) ->
          {
             if (!modified[0])
             {
                // update flag to ensure atomic integer array is only modified once per track request
                modified[0] = true;

                // value of atomic integer at element 1 in the array is the reference count of sessions who
                // are interested in the version of this DMO
                if (v.decrementAndGet(REFCOUNT) == 0)
                {
                   // no one is interested in this record's version any longer; remove the entry
                   v = null;
                }
             }

             return v;
          });
       }
    }