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;
      }
   }
}