# ForeignNuller.java

    /*
    ** Module   : ForeignNuller.java
    ** Abstract : Helper object which cleans up orphaned foreign key joins
    **
    ** Copyright (c) 2004-2024, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
    ** 001 ECF 20061105   @31092 Created initial version. Helper object which
    **                           cleans up orphaned foreign key joins when a
    **                           referenced record is deleted.
    ** 002 ECF 20061128   @31450 Fixed NPE. Return empty iterator from
    **                           localDMOs() if foreign DMO's local getter
    **                           method returns null.
    ** 003 ECF 20070326   @32604 Force a pending flush from breakLinkage().
    **                           Required to ensure foreign key update is
    **                           flushed to the database before a delete of
    **                           the foreign referent record. Otherwise, a
    **                           foreign key constraint violation would occur.
    ** 004 ECF 20070412   @32979 Retrofit to use RecordLockContext for record
    **                           locking.
    ** 005 ECF 20070416   @33023 Integrated user interrupt handling.
    ** 006 ECF 20070704   @34359 Considerable rewrite to fix a latent defect.
    **                           The old implementation relied upon the
    **                           inverse association from referent DMO to
    **                           refering DMO(s) to be up to date in real
    **                           time. However, this logic was removed from
    **                           RecordBuffer and the AssociationSyncher
    **                           hierarchy long ago as too expensive. It is
    **                           thus necessary to execute a database query to
    **                           collect the set of refering DMOs. This is the
    **                           basis of the new implementation.
    ** 007 ECF 20070706   @34413 Minor optimization. Expose instances() method
    **                           as package private so that the caller can
    **                           cache instances of this class and pass them
    **                           the static breakLinkages() method as needed,
    **                           rather than looking them up at each call to
    **                           breakLinkages().
    ** 008 ECF 20070716   @34545 Fixed regression. Context must be specific to
    **                           each ForeignNuller instance, but it was
    **                           static.
    ** 009 ECF 20070911   @35287 Replaced database name string with Database
    **                           information object. Required for non-local
    **                           database connections.
    ** 010 ECF 20080425   @38390 Adjust for foreign key runtime flag. Static
    **                           method instances() returns an empty list in
    **                           the event foreign key support is disabled.
    ** 011 CA  20080815   @39459 Support API change in DBUtils.
    ** 012 GES 20090421   @41829 Matched utility class package change.
    ** 013 ECF 20131029          Import change.
    ** 014 SVL 20140210          Use HQLExpression instead of HQL string.
    ** 015 ECF 20190620          EmptyIterator API change.
    ** 016 ECF 20200906          New ORM implementation.
    ** 017 OM  20200924          Small optimization.
    **     ECF 20210506          Use RecordBuffer.getDmoInfo() getter instead of direct field access to
    **                           prevent NPE in proxy case.
    **     OM  20221103          New class names for FQLPreprocessor, FQLExpression, FQLBundle, and FQLCache.
    ** 018 SR  20230510          Matched persistence.list() new signature.
    ** 019 OM  20241128          Multi-tenant runtime support: selected the proper persistence context, eventually
    **                           based on [sharedDb] 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;

    import java.lang.reflect.*;
    import java.util.*;
    import com.goldencode.p2j.persist.lock.*;
    import com.goldencode.p2j.persist.orm.*;
    import com.goldencode.p2j.security.*;
    import com.goldencode.util.*;

    /**
     * A class whose primary purpose is to null out foreign key references to a
     * DMO from related records, when the DMO is about to be deleted.  A single,
     * static method provides the primary access point for this class: {@link
     * #breakLinkages} -- nulls out all foreign key references to a given DMO.
     * <p>
     * The nullification of these foreign references is necessary to prevent
     * database errors caused by foreign key constraints when a record is deleted
     * without first deleting dependent records related to the primary record by
     * a foreign key.  This may happen for one of two reasons:
     * <ul>
     *   <li>the deletes occur out of order, such that the referent record is
     *       deleted before its dependent records;  or
     *   <li>the dependent records are never deleted at all;  perhaps because the
     *       application requires that they survive the delete of the referent
     *       record, for subsequent reporting or other purposes.
     * </ul>
     * <p>
     * Both of the above situations is possible and natural in Progress, due to
     * the lack of database-level, referential integrity enforced by foreign key
     * constraints.  In a converted application running in the P2J environment,
     * the introduction of foreign key relations between tables necessitates the
     * special delete-time handling provided by this class.
     * <p>
     * Construction of an instance of this class is expensive, since it involves
     * multiple method lookups using reflection.  For this reason, instances of
     * this class are cached across client contexts.  Context-local state for
     * each instance is managed within an instance of an inner class.
     * <p>
     * The actual breaking of linkages is also expensive in that it requires a
     * database query to find all records which refer to a particular record by
     * foreign key.  However, the query uses the refering record's foreign key
     * column, which is indexed, to make this lookup as fast as possible.
     * Nevertheless, a tight loop of many deletes requiring this service will be
     * quite expensive.  An alternative to this expense would be to use the
     * inverse association between the referent record and all of its refering
     * records.  This would require only a method call at linkage breakage time to
     * collect refering DMOs.  However, for this option to work, the DMO graph
     * which holds set of associated records (one-to-many) or a single associated
     * record (one-to-one) would have to be kept up to date in real time.  This
     * would require the record buffer to update these inverse associations each
     * time a legacy foreign key property in the referent DMO is updated.  This
     * would probably add a more pervasive (and noticable) cost to the overall
     * system.  So,the lazy approach of looking up refering records at referent
     * delete time was chosen instead.
     *
     * @see  RecordBuffer#delete
     */
    final class ForeignNuller
    {
       /** Cache of nuller objects, keyed by RelationInfo instances */
       private static final Map<RelationInfo, ForeignNuller> cache = new IdentityHashMap<>();

       /** Context local list of states managed by this object */
       private final ContextLocal<Context> context = new ContextLocal<Context>()
       {
          protected Context initialValue() { return (new Context()); }
       };

       /** Persistence service object which performs record locking/unlocking */
       private final Persistence persistence;

       /** Name of table for which a lock must be acquired */
       private final String lockTarget;

       /** HQL statement used to retrieve refering DMOs given a referent */
       private final String hql;

       /** Method of foreign DMO which sets refering (local) DMO */
       private final Method localSetter;

       /** Method of local DMO which gets referent (foreign) DMO */
       private final Method foreignGetter;

       /** Method of local DMO which sets referent (foreign) DMO */
       private final Method foreignSetter;

       /** Is relation uniquely constrained at the local table? */
       private final boolean unique;

       /** In case of multi-tenant environment: is the buffer shared or private? */
       private final boolean sharedDb;

       /**
        * Private constructor which is only accessed by the {@link #instances}
        * factory method.
        *
        * @param   buffer
        *          RecordBuffer which defines the persistence service object and
        *          database for this object.
        * @param   info
        *          Descriptor of the relation between the local and foreign DMOs.
        *
        * @throws  PersistenceException
        *          if a method cannot be found using reflection.
        */
       private ForeignNuller(RecordBuffer buffer, RelationInfo info)
       throws PersistenceException
       {
          this.persistence = buffer.getPersistence();
          this.sharedDb = !buffer.getDmoInfo().multiTenant;

          // generate HQL to retrieve DMOs which refer to referent via foreign keys.
          hql = generateHQL(info);

          // determine whether local table is uniquely constrained.
          unique = info.isUnique();

          // store name of table for which a lock must be acquired before nulling out foreign reference.
          Class<? extends Record> localClass = info.getLocalClass();
          lockTarget = DatabaseManager.getBackingTableName(localClass);

          try
          {
             Class<?> foreignIface = info.getForeignInterface();
             Class<?> localIface = info.getLocalInterface();

             // Store local setter method of foreign DMO.
             StringBuilder buf = new StringBuilder();
             buf.append("set");
             buf.append(info.getLocalInterfaceName());
             buf.append(unique ? "Record" : "Set");
             String name = buf.toString();
             localSetter = foreignIface.getDeclaredMethod(name, (unique ? localIface : Set.class));

             // Store foreign getter method of local DMO.
             buf.setLength(0);
             buf.append("get");
             buf.append(info.getForeignInterfaceName());
             buf.append("Record");
             name = buf.toString();
             foreignGetter = localIface.getDeclaredMethod(name);

             // Store foreign setter method of local DMO.
             buf.setLength(0);
             buf.append("set");
             buf.append(info.getForeignInterfaceName());
             buf.append("Record");
             name = buf.toString();
             foreignSetter = localIface.getDeclaredMethod(name, foreignIface);
          }
          catch (NoSuchMethodException exc)
          {
             throw new PersistenceException("Error initializing foreign key nuller", exc);
          }
       }

       /**
        * Retrieve a list of <code>ForeignNuller</code> instances, one for each
        * inverse relation between the DMO represented by the business interface
        * <code>dmoIface</code> and another DMO which references this record via
        * a foreign key.  Instances are first retrieved from a cross-context
        * cache, if present in the cache.  Otherwise, they are lazily created and
        * stored in the cache.
        *
        * @param   buffer
        *          Record buffer which holds the DMO representing a database
        *          record which is about to be deleted, which may be referenced
        *          via foreign key by other records.
        *
        * @return  List of <code>ForeignNuller</code> instances.  May be empty,
        *          but will not be <code>null</code>.
        *
        * @throws  PersistenceException
        *          if there is an error creating a <code>ForeignNuller</code>
        *          instance.
        */
       static List<ForeignNuller> instances(RecordBuffer buffer)
       throws PersistenceException
       {
          if (!Persistence.isForeignKeysEnabled())
          {
             return Collections.emptyList();
          }

          ArrayList<ForeignNuller> instances = new ArrayList<>();
          Iterator<RelationInfo> iter = buffer.getDmoInfo().inverseRelations();
          while (iter.hasNext())
          {
             RelationInfo info = iter.next();

             synchronized (cache)
             {
                ForeignNuller nuller = cache.get(info);
                if (nuller == null)
                {
                   nuller = new ForeignNuller(buffer, info);
                   cache.put(info, nuller);
                }
                instances.add(nuller);
             }
          }

          instances.trimToSize();

          return instances;
       }

       /**
        * Set to <code>null</code> all foreign references to <code>referent</code>
        * from related records.  If there are no foreign key references to
        * <code>referent</code>, this method returns immediately.
        * <p>
        * All records which are related to <code>referent</code> are collected,
        * locked exclusively, and updated to null out their foreign reference back
        * to the record represented by <code>referent</code>.  Locks on these
        * methods are restored to their previous state when this operation is
        * complete.
        * <p>
        * No change is made unless <em>all</em> refering records can be locked.
        * This method will attempt to obtain the locks repeatedly, until either
        * all locks are obtained, or the user aborts locking.
        *
        * @param   instances
        *          <code>ForeignNuller</code> instances which will be used to
        *          break foreign key references to <code>referent</code>.
        * @param   referent
        *          DMO representing a database record which is about to be
        *          deleted, which may be referenced via foreign key by other
        *          records.
        *
        * @throws  PersistenceException
        *          if any reflection error occurs getting dependent records or
        *          setting foreign references;
        *          if there was an error locating the refering DMOs.
        */
       static void breakLinkages(List<ForeignNuller> instances, Record referent)
       throws PersistenceException
       {
          Iterator<ForeignNuller> iter;
          ForeignNuller nuller = null;

          outer:
          while (true)
          {
             try
             {
                // Acquire exclusive locks for all local DMOs.
                iter = instances.iterator();
                while (iter.hasNext())
                {
                   nuller = iter.next();
                   if (!nuller.acquireLocalLocks(referent))
                   {
                      continue outer;
                   }
                }

                // Break the foreign key reference from each local DMO to the
                // foreign record.
                iter = instances.iterator();
                while (iter.hasNext())
                {
                   nuller = iter.next();
                   nuller.breakLinkages(referent);
                }

                break outer;
             }
             catch (IllegalAccessException exc)
             {
                throw new PersistenceException(
                   "Error invoking method while breaking foreign reference",
                   exc);
             }
             catch (InvocationTargetException exc)
             {
                DBUtils.handleException(nuller.persistence.getDatabase(Persistence.PRIVATE_CTX), exc);

                throw new PersistenceException(
                   "Error invoking method while breaking foreign reference",
                   exc);
             }
             finally
             {
                // Reset locks to their previous states.
                iter = instances.iterator();
                while (iter.hasNext())
                {
                   nuller = iter.next();
                   nuller.resetLocalLocks();
                }
             }
          }
       }

       /**
        * Generate the HQL statement which will be used for the resolution of
        * refering records to referent record (via foreign key).  Uses the
        * information stored in the relation descriptor to compose the query.
        *
        * @param   info
        *          Relation descriptor.
        *
        * @return  HQL query statement.
        */
       private static String generateHQL(RelationInfo info)
       {
          String locAlias = info.getLocalAlias();
          String fgnAlias = info.getForeignAlias();
          String locImpl = info.getLocalClassName();

          FQLExpression buf = new FQLExpression("from ");
          buf.append(locImpl);
          buf.append(" as ");
          buf.append(locAlias);
          buf.append(" where ");
          buf.append(locAlias);
          buf.append(".");
          buf.append(fgnAlias);
          buf.append("Record = ", true);

          return buf.toFinalExpression();
       }

       /**
        * Acquire an exclusive record lock for each DMO which references
        * <code>referent</code> by a foreign key.  Each local DMO which so
        * references <code>referent</code> is retrieved by invoking a getter
        * method for that record (in a one-to-one relation) or for a collection
        * of records (in a one-to-many relation).
        * <p>
        * A context-local list of state objects is stored for this
        * <code>ForeignNuller</code> object (because it can be used across client
        * contexts).  One {@link ForeignNuller.State} object is added to this
        * list for each local DMO successfully locked.  This context local list
        * will be removed when the current scope is closed.
        *
        * @param   referent
        *          DMO referenced by one or more refering DMOs via foreign key.
        *
        * @return  <code>true</code> if all required locks were acquired properly;
        *          else <code>false</code>.
        *
        * @throws  PersistenceException
        *          if there was an error locating the refering DMOs.
        * @throws  IllegalAccessException
        *          if the referent DMO's getter method for local DMO(s) is
        *          inaccessible.
        * @throws  InvocationTargetException
        *          if the referent DMO's getter method for local DMO(s) throws an
        *          exception.
        */
       private boolean acquireLocalLocks(Record referent)
       throws PersistenceException,
              IllegalAccessException,
              InvocationTargetException
       {
          Context ctx = context.get();
          int expectedSize = 0;

          Iterator<Record> iter = referers(referent, ctx);
          while (iter.hasNext())
          {
             Record referer = iter.next();
             expectedSize++;
             acquireLock(referer, referent, ctx);
          }

          return (expectedSize == ctx.states.size());
       }

       /**
        * Acquire an exclusive lock on <code>referer</code>, then optionally
        * confirm that <code>referer</code> still has a foreign reference to
        * <code>referent</code>.  Create a new instance of the inner class
        * {@link ForeignNuller.State}, which remembers the local DMO and the type
        * of lock held before acquiring the exclusive lock, and add it to the
        * <code>states</code> list.
        *
        * @param   referer
        *          DMO to be locked exclusively.
        * @param   referent
        *          DMO which should be referenced by <code>referer</code> after
        *          it is locked.
        * @param   ctx
        *          Current context for this object.  Contains a list to which a
        *          new <code>State</code> instance will be added after the locking
        *          and reference confirmation take place.
        *
        * @return  <code>true</code> if the lock was acquired AND the confirmation
        *          of the referer to referent relationship was successful or
        *          unnecessary;  <code>false</code> if the lock was acquired but
        *          the confirmation failed.
        *
        * @throws  IllegalAccessException
        *          if the local DMO's getter method for the foreign DMO is
        *          inaccessible.
        * @throws  InvocationTargetException
        *          if the local DMO's getter method for the foreign DMO throws an
        *          exception.
        */
       private boolean acquireLock(Record referer, Record referent, Context ctx)
       throws IllegalAccessException,
              InvocationTargetException
       {
          try
          {
             Long id = referer.primaryKey();
             RecordIdentifier<String> ident = new RecordIdentifier<>(lockTarget, id);
             LockType oldLockType = persistence.queryLock(ident, sharedDb);
             if (!oldLockType.isExclusive())
             {
                // Acquire an exclusive lock temporarily.
                RecordLockContext lockCtx = persistence.getRecordLockContext(sharedDb);
                lockCtx.lock(ident, LockType.EXCLUSIVE);
             }
             else
             {
                // If record was locked coming into this method, there is no
                // chance that the local DMO could not already reference the
                // appropriate foreign DMO, so we can skip the subsequent
                // confirmation check in this case.
                ctx.states.add(new State(referer, oldLockType));

                return true;
             }

             // Now that we've got the lock, check to make sure the locked, local
             // record still refers back to the proper foreign DMO.
             Record fgnDMO = (Record) foreignGetter.invoke(referer);
             if (fgnDMO == null     ||
                 referent == fgnDMO ||
                 referent.primaryKey().equals(fgnDMO.primaryKey()))
             {
                // Only add a new state to the list if the reference is still
                // correct.
                ctx.states.add(new State(referer, oldLockType));

                return true;
             }
          }
          catch (LockUnavailableException exc)
          {
             // Not thrown but Persistence.lock() method signature requires we
             // catch or declare.
          }

          return false;
       }

       /**
        * Reset all refering DMOs' record locks to their previous states (before
        * an exclusive lock was acquired).  This method relies on the presence of
        * a context local list of <code>State</code> objects which indicate what
        * the pre-lock status was.  If in a transaction, the lock may be
        * downgraded if appropriate to the previous state, but it will not be
        * released until the end of the transaction.
        */
       private void resetLocalLocks()
       {
          Context ctx = context.get();

          try
          {
             Iterator<State> iter = ctx.states.iterator();
             while (iter.hasNext())
             {
                State next = iter.next();
                if (!next.oldLockType.isExclusive())
                {
                   // Downgrade (or remove) the temporary, exclusive lock.
                   Long id = next.referer.primaryKey();
                   RecordIdentifier<String> ident = new RecordIdentifier<>(lockTarget, id);
                   RecordLockContext lockCtx = persistence.getRecordLockContext(sharedDb);
                   lockCtx.lock(ident, next.oldLockType);
                }
             }
          }
          catch (LockUnavailableException exc)
          {
             // Not thrown but Persistence.lock() method signature requires we
             // catch or declare.
          }
          finally
          {
             ctx.referers = null;
             ctx.states.clear();
          }
       }

       /**
        * Update the foreign key references from all DMOs currently refering to
        * <code>referent</code>, such that they will instead refer to
        * <code>null</code>.  At the same time, remove the inverse association
        * from the referent record to the refering DMO, to maintain the correct
        * state on the other end of the bidirectional association (if indeed there
        * was such an inverse reference at all).
        *
        * @param   referent
        *          DMO being deleted.
        *
        * @throws  PersistenceException
        *          if there was an error locating the refering DMOs.
        * @throws  IllegalAccessException
        *          if the refering DMO's setter method for the referent DMO is
        *          inaccessible.
        * @throws  InvocationTargetException
        *          if the refering DMO's setter method for the referent DMO throws
        *          an exception.
        */
       private void breakLinkages(Record referent)
       throws PersistenceException,
              IllegalAccessException,
              InvocationTargetException
       {
          Iterator<Record> iter = referers(referent, context.get());
          while (iter.hasNext())
          {
             Record referer = iter.next();

             // Update the foreign key.
             foreignSetter.invoke(referer);
             iter.remove();
          }
       }

       /**
        * Compose a list of DMOs which refer to the <code>referent</code> DMO via
        * foreign key.  This list is cached within a context local state object
        * for the life of the process which breaks these foreign key linkages, so
        * that it does not need to be retrieved separately for each stage of the
        * linkage break process (lock acquisition, DMO updates, lock reset).  Only
        * the first call to this method during this cycle triggers the actual
        * database access;  subsequent calls use the cached list.
        *
        * @param   referent
        *          DMO to which zero or more refering DMOs have a foreign key
        *          link.
        * @param   ctx
        *          Context local state object which stores the list of found DMOs.
        *
        * @throws  PersistenceException
        *          if an error occurs looking up the refering DMOs in the
        *          database.
        */
       private void getReferers(Record referent, Context ctx)
       throws PersistenceException
       {
          if (ctx.referers != null)
          {
             return;
          }

          Object[] args = new Object[] { referent };
          List<Record> list = persistence.list(hql, args, 0, 0);

          if (list == null)
          {
             list = Collections.emptyList();
          }

          ctx.referers = list;
       }

       /**
        * Return an iterator on all of the local DMOs related to the given,
        * foreign DMO.  If the association is one-to-one in nature, the iterator
        * will return only a single record.  If the association is one-to-many in
        * nature, the iterator will return multiple records.  The optional,
        * remove operation will invoke the appropriate method to remove the
        * inverse reference from the foreign DMO.
        *
        * @param   referent
        *          DMO on the inverse end of the foreign key association.
        * @param   ctx
        *          Current context for this object, which maintains the list of
        *          found referers.
        *
        * @return  Iterator on all DMOs which refer to <code>referent</code> via
        *          foreign key.
        *
        * @throws  PersistenceException
        *          if there was any error locating the refering DMOs.
        */
       private Iterator<Record> referers(final Record referent, final Context ctx)
       throws PersistenceException
       {
          getReferers(referent, ctx);

          if (ctx.referers.isEmpty())
          {
             return EmptyIterator.get();
          }

          if (unique)
          {
             // An iterator on a single record.
             return new Iterator<Record>()
             {
                Record dmo = ctx.referers.get(0);

                public boolean hasNext()
                {
                   return (dmo != null);
                }

                public Record next()
                {
                   if (dmo == null)
                   {
                      throw new NoSuchElementException();
                   }

                   Record p = dmo;
                   dmo = null;

                   return p;
                }

                public void remove()
                {
                   try
                   {
                      // Dereference the local DMO from the foreign DMO.
                      localSetter.invoke(referent, new Object[] { null });
                   }
                   catch (Exception exc)
                   {
                      DBUtils.handleException(persistence.getDatabase(Persistence.PRIVATE_CTX), exc);

                      throw new RuntimeException(exc);
                   }
                }
             };
          }
          else
          {
             // An iterator on a set of records.
             return new Iterator<Record>()
             {
                private Iterator<Record> iter = ctx.referers.iterator();

                public boolean hasNext()
                {
                   return iter.hasNext();
                }

                public Record next()
                {
                   return iter.next();
                }

                public void remove()
                {
                   iter.remove();

                   // After last element is removed, dereference the collection.
                   if (!iter.hasNext())
                   {
                      try
                      {
                         localSetter.invoke(referent);
                      }
                      catch (Exception exc)
                      {
                         DBUtils.handleException(persistence.getDatabase(Persistence.PRIVATE_CTX), exc);

                         throw new RuntimeException(exc);
                      }
                   }
                }
             };
          }
       }

       /**
        * Context local state maintained for the enclosing class.  Maintains a
        * list of DMOs which refer to the DMO to which linkages must be broken,
        * as well as a list of previous lock states for those DMOs.
        */
       private static class Context
       {
          /** List of pre-linkage break lock states for referer DMOs */
          private final List<State> states = new ArrayList<>();

          /** Transient list of DMOs which refer to current referent DMO */
          private List<Record> referers = null;
       }

       /**
        * Object which stores a reference to a local DMO and its lock status
        * before its foreign reference is changed.  One or more instances of this
        * class are stored while the enclosing class is changing the foreign key
        * reference of a target DMO.
        */
       private static class State
       {
          /** Local DMO whose foreign reference is being changed */
          private Record referer = null;

          /** Previous lock type of local DMO;  must be reset after operation */
          private LockType oldLockType = null;

          /**
           * Convenience constructor.
           *
           * @param   referer
           *          Local DMO whose foreign reference is being changed.
           * @param   oldLockType
           *          Previous lock type of local DMO;  must be reset after
           *          operation.
           */
          State(Record referer, LockType oldLockType)
          {
             this.referer = referer;
             this.oldLockType = oldLockType;
          }
       }
    }