# ChangeBroker.java

    /*
    ** Module   : ChangeBroker.java
    ** Abstract : Clearinghouse for DMO property change events
    **
    ** Copyright (c) 2004-2024, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- --JPRM-- ----------------------------Description----------------------------
    ** 001 ECF 20060719   @28114 Created initial version. Brokers notification
    **                           of DMO state changes to registered listeners.
    ** 002 ECF 20060722   @28134 Fixed listener registration defect. Listeners
    **                           registered at inner scopes were hiding those
    **                           registered for the same DMO types at outer
    **                           scopes.
    ** 003 ECF 20060802   @28348 Make addListener() tolerant of multiple calls
    **                           within the same scope. Listeners are now
    **                           stored in a HashSet, so duplicates are
    **                           prevented.
    ** 004 ECF 20060918   @29665 Added forcePendingFlush() method. Forces a
    **                           pending session flush for a particular DMO
    **                           type.
    ** 005 ECF 20060919   @29696 Fixed handling of pending flush state. The
    **                           pending flush state was being tracked
    **                           globally, across all databases, rather than
    **                           per database. This led to errors in proper
    **                           detection of whether a session flush was
    **                           needed, because the state was being cleared
    **                           too aggressively.
    ** 006 ECF 20061010   @30308 Made changes to accommodate auto-commit temp
    **                           tables. These were primarily side effects of
    **                           changes to the Persistence class to support
    **                           this feature.
    ** 007 ECF 20070306   @32286 Added findDirty() implementation to session
    **                           interceptor. This allows us to optimize dirty
    **                           checking in some cases by avoiding the
    **                           default dirty check comparisons which
    **                           Hibernate performs.
    ** 008 ECF 20070322   @32603 Added forcePendingFlush() variant. Allows
    **                           forcing a pending flush when a RecordBuffer
    **                           instance is not available.
    ** 009 ECF 20070416   @33019 Integrated user interrupt handling.
    ** 010 ECF 20080103   @36869 Minor change in stateChanged(). Don't force
    **                           a pending flush for a record that is still
    **                           transient. Also integrated generics.
    ** 011 ECF 20080227   @37222 Fixed SessionInterceptor.findDirty(). Was
    **                           improperly short-circuiting Hiberate dirty
    **                           check in non-legacy mode.
    ** 012 ECF 20080314   @37471 Added debug logic. Number of DMO dirty checks
    **                           performed since last flush are now tracked
    **                           and reported when trace level logging is
    **                           enabled.
    ** 013 ECF 20080403   @38138 Use entity name instead of DMO class to test
    **                           if flush is required.
    ** 014 ECF 20080822   @39554 Modified stateChanged() to accept an event
    **                           type parameter. Uses inner enum Type from
    **                           RecordChangeEvent.
    ** 015 CA  20080815   @39452 Support API change in DBUtils.
    ** 016 SVL 20081024   @40217 Added addListener(.., int depth) function.
    ** 017 ECF 20090212   @41294 Implemented eviction of unused DMOs after a
    **                           session flush. Temp table DMOs may be left
    **                           behind after a bulk delete, causing more
    **                           flushing effort than is necessary. We now
    **                           evict these objects after a flush, if they
    **                           are not in use.
    ** 018 ECF 20090610   @42971 Minor change to SessionInterceptor.postFlush().
    **                           Log database info in trace mode.
    ** 019 ECF 20090716   @43214 Minor change to minimize memory used by listener
    **                           collections.
    ** 020 CA  20131013          Added no-op scopeDeleted() method, required by the changes in
    **                           Scopeable interface.
    ** 021 CA  20140513          Added a weight for the context-local var, to ensure predetermined
    **                           order during context reset.
    ** 022 OM  20150112          Added support for storing listeners in the GLOBAl scope for
    **                           persistent RUN procedure.
    ** 023 OM  20150213          Prevented a NPE in removeFromGlobal() when listeners is empty.
    ** 024 ECF 20150906          Changed SessionInterceptor to match changes in BufferManager API.
    ** 025 ECF 20160607          Reimplemented listener registration and notification to be more
    **                           efficient. Reduced session flushing when processing state changes.
    ** 026 ECF 20160615          Added support needed for custom dirty entity strategy. Replaced
    **                           Apache commons logging with J2SE logging.
    ** 027 ECF 20160907          Improved session flush management for better performance.
    ** 028 CA  20161010          Fixed stateChanged - all listeners, from all scopes, must be notified
    **                           for a certain DMO interface.
    ** 029 ECF 20180311          Fixed listener registration. Cleanup interceptor state after
    **                           transactions on commit as well rollback.
    ** 030 ECF 20180709          Refactored pending eviction processing.
    ** 031 ECF 20190302          Fixed insertions of listeners into scopes between the global and the
    **                           current scope.
    ** 032 CA  20190326          Implemented ProcedureHelper and TransactionHelper instead of direct
    **                           usage of the static API.  This allows the elimination of context
    **                           local usage in ProcedureManager and TransactionManager.
    ** 033 OM  20190411          Removed compile-time ambiguity.
    ** 034 OM  20190624          Small javadoc update.
    ** 035 ECF 20200419          Removed Hibernate dependencies.
    **     CA  20200722          Performance improvements.
    ** 036 CA  20200924          Small performance optimization - do not use get in a map usage.
    **     CA  20200930          Use a lambda instead of an inner class for the scopeable factory.
    **     CA  20201003          Allow dictionaries with an identity map.
    **                           Use an identity HashSet where possible.
    **     CA  20220309          Added isQueried(), which checks if a temp-table has any listeners registered for
    **                           events which are a P2JQuery.
    **     CA  20220901          Refactored scope notification support: ScopeableFactory was removed, and the
    **                           registration is now specific to each type of scopeable.  For each case, the block
    **                           will be registered for scope support (for that particular scopeable) only when
    **                           the scopeable is 'active' (i.e. unnamed streams or accumulators are used).  This
    **                           allows a lazy registration of scopeables, to avoid the unnecessary overhead of
    **                           processing all the scopeables for each and every block.
    **     CA  20220906          Extracted all BufferManager state which requires notification for
    **                           'isImportantBlock' to an external TxWrapper class (including the private class
    **                           here), to allow the scopeable BufferManager state related only to buffers (and
    **                           not transaction related) to be registered for scope notifications in a lazy
    **                           manner, when buffers are accessed/used/opened.
    **     CA  20221010          Performance improvements - avoid the ProcedureData lookup, by keeping a parallel
    **                           stack of this data for THIS-PROCEDURE.  Refs #6826
    ** 037 GBB 20230512          Logging methods replaced by CentralLogger/ConversionStatus.
    ** 038 AL2 20230614          Do local context look-up for the bufferManager only once per ChangeBroker.
    ** 039 CA  20231026          A persistent procedure with a pending delete is no longer persistent, so
    **                           scopeDeleted needs to be executed.
    **     CA  20231031          Added 'BlockDefinition' parameter to 'scopeStart'.
    ** 040 CA  20240809          Avoid iterator usage, for performance improvement.
    ** 041 AB2 20250415          Changed stateChanged to notify all matching listeners from all scopes. See #9722.
    ** 042 RNC 20250423          Reverted commit with entry 041. See #9914.
    **     RNC 20250428          Assure local to global linking for fresh listener sets.
    */

    /*
    ** 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.ref.*;
    import java.util.*;
    import java.util.function.*;
    import java.util.logging.*;
    import com.goldencode.p2j.persist.event.*;
    import com.goldencode.p2j.security.*;
    import com.goldencode.p2j.util.*;
    import com.goldencode.p2j.util.logging.*;

    /**
     * A clearing house which notifies interested parties of changes made to
     * active records within the current client context.  These notifications
     * are used for various purposes:
     * <ul>
     *   <li>to enable {@link RecordBuffer} to defer creating a placeholder
     *       snapshot as long as possible;
     *   <li>to permit {@link Persistence} query methods to disable auto-flushing
     *       of the active Hibernate session when possible;
     *   <li>to allow {@link AdaptiveQuery} to invalidate its current result set
     *       when necessary.
     * </ul>
     * <p>
     * {@link com.goldencode.p2j.persist.event.RecordChangeListener
     * RecordChangeListener} objects register to receive notifications for one or
     * more DMO types, using the {@link #addListener} method.  A listener will
     * only receive notifications for changes to DMOs of the types for which they
     * registered.  Beyond this, no other filtering is performed.  Listeners are
     * maintained in a scoped dictionary, so there is never a need to remove a
     * listener explicitly;  when the scope in which the listener was registered
     * is closed, it is released automatically.
     * <p>
     * Change notifications are driven by the {@link #stateChanged} method.  It
     * is anticipated that this only will be invoked by <code>RecordBuffer</code>
     * objects, as user code calls setter methods on DMO proxies.  A state change
     * causes a {@link com.goldencode.p2j.persist.event.RecordChangeEvent
     * RecordChangeEvent} to be broadcast to all listeners registered against the
     * type of DMO which was modified.  "Type" in this sense corresponds to the
     * DMO interface.
     * <p>
     * Because it relies upon the <code>RecordBuffer</code> as the source for all
     * state change events, <code>ChangeBroker</code> is not useful in non-legacy
     * client contexts (i.e., where external code accesses the services of the
     * <code>Persistence</code> class directly).  For the most part, this class
     * is transparent for these contexts.  One exception is that the auto-flush
     * optimization technique described above would be harmful if allowed to
     * proceed naturally, because flushing would always be disabled since state
     * changes would never be reported, thus it would always seem flushing is not
     * required.  To prevent this problem, this class detects when it is not in
     * legacy mode and disables this optimization accordingly in such cases.
     */
    final class ChangeBroker
    implements Scopeable
    {
       /** Logger */
       private static final CentralLogger LOG = CentralLogger.get(ChangeBroker.class.getName());

       /** Context local instance of this class */
       private static final ContextContainer context = new ContextContainer();

       /** Sets of listeners which have registered interest in DMO property changes */
       private final ScopedDictionary<Class<?>, ListenerSet> listeners = new ScopedDictionary<>();

       /** Map of saved listeners, per persistent procedure referent. */
       private final Map<Object, Map<Class<?>, ListenerSet>> procListeners = new IdentityHashMap<>();

       /** Helper to use the ProcedureManager without any context local lookups */
       private ProcedureManager.ProcedureHelper pm;

       /** Helper to use the TM without any context local lookups */
       private TransactionManager.TransactionHelper tm;

       /** Helper to use the buffer manager without any context local lookups */
       private BufferManager bufferManager;

       /**
        * This class can only be constructed privately.
        */
       private ChangeBroker()
       {
          listeners.setIdentityKeys(true);
       }

       /**
        * Retrieve the context-local instance of this class, instantiating it
        * first if necessary.
        *
        * @return  Context-local instance of this class.
        */
       static ChangeBroker get()
       {
          return context.obtain();
       }

       /**
        * Add a scope to the listeners dictionary whenever a new runtime scope
        * opens.
        *
        * @param    block
        *           The explicit block definition which required this notification.
        */
       @Override
       public void scopeStart(BlockDefinition block)
       {
          listeners.addScope(null);
       }

       /**
        * Pop the current scope from the listeners dictionary whenever a runtime
        * scope is closed.
        */
       public void scopeFinished()
       {
          Object referent = null;
          boolean saveForProc = false;

          if (!tm.isGlobalBlock())
          {
             BlockType bt = tm.getBlockType();
             // if we have a persistent procedure, save the data
             if (bt == BlockType.EXTERNAL_PROC && pm.isThisProcedurePersistent())
             {
                referent = pm._thisProcedure();
                saveForProc = true;
             }
          }

          Map<Class<?>, ListenerSet> current = listeners.getDictionaryAtScope(0, false);
          if (current != null && !current.isEmpty())
          {
             current.forEach((cls, set) -> set.unlink());

             if (saveForProc)
             {
                procListeners.put(referent, current);
             }
          }

          listeners.deleteScope();
       }

       /**
        * Notification that the instantiating procedure where the scope was opened was deleted.
        * <p>
        * This implementation is a no-op.
        */
       @Override
       public void scopeDeleted()
       {
          Object referent = pm.getProcessedProcedure();

          if (referent == null || pm._isPersistentAndNotPendingDelete(referent))
          {
             // if no referent or procedure is still persistent, do not clean up yet
             return;
          }

          // this needs to be executed only when the procedure gets deleted.
          procListeners.remove(referent);
       }

       /**
        * Get the {@link ScopeId} for the instance.
        *
        * @return   {@link ScopeId#CHANGE_BROKER}.
        */
       @Override
       public ScopeId getScopeId()
       {
          return ScopeId.CHANGE_BROKER;
       }

       /**
        * Register a listener to receive DMO property change notifications.  The
        * listener will receive notifications of any changes to DMOs of the types
        * for which it is registered, as they occur in this scope or in scopes
        * nested within this scope.  The listener is deregistered automatically
        * when the scope ends.
        * <p>
        * This method is tolerant of being called multiple times for the same
        * listener in the same scope;  the listener will only be registered once,
        * and therefore will receive only one notification per distinct event.
        *
        * @param   listener
        *          Listener object to be added.
        */
       void addListener(RecordChangeListener listener)
       {
          BufferManager.registerScopeable(bufferManager);

          addListener(listener, listeners.size());
       }

       /**
        * Register a listener to receive DMO property change notifications. The
        * registration is performed in the current scope and the outer scopes,
        * which number is specified by the <code>depth</code> parameter. The
        * listener will receive notifications of any changes to DMOs of the types
        * for which it is registered, as they occur in the specified scopes or in
        * the nested ones.  The listener is deregistered automatically
        * when the scopes end.
        * <p>
        * When we call this function with a <code>depth</code> less than the
        * number of innermost scope, we actually want to register it in some outer
        * scope, but since registrations at inner scopes will hide those at outer
        * scopes during a lookup, we should register it in the several scopes
        * if needed.
        * <p>
        * This method is tolerant of being called multiple times for the same
        * listener in the same scope;  the listener will only be registered once,
        * and therefore will receive only one notification per distinct event.
        * <p>
        * If the buffer is opened in a procedure executed persistent, the buffer has to survive
        * when the procedure code is finished until the persistent procedure is deleted. In this
        * case the buffer is stored in the GLOBAL scope (depth = 0).
        *
        * @param   listener
        *          Listener object to be added.
        *
        * @param   depth
        *          1-based number of the most outer scope in which we want to register the listener
        *          (starting from the very outermost scope). Use 0 for GLOBAL scope when the buffer
        *          is opened in a persistent procedure.
        */
       void addListener(RecordChangeListener listener, int depth)
       {
          BufferManager.registerScopeable(bufferManager);

          boolean global = (depth == 0);

          Iterator<RecordBuffer> iter = listener.recordBuffers();
          while (iter.hasNext())
          {
             RecordBuffer buffer = iter.next();
             Class<? extends DataModelObject> dmoIface = buffer.getDMOInterface();
             ListenerSet set;

             if (global)
             {
                // insert to global scope
                set = listeners.getValueAtScope(dmoIface, -1);
                boolean createSet = set == null;

                if (createSet)
                {
                   set = new ListenerSet();

                   if (set.nextInner == null && listeners.size() > 1)
                   {
                      ListenerSet nextInner = listeners.reverseLookup(dmoIface, 1);
                      if (nextInner != null)
                      {
                         nextInner.link(set);
                      }
                   }

                   listeners.addEntry(true, dmoIface, set);
                }

                set.add(listener);
             }
             else
             {
                // add the buffer to the specified scope of listeners
                int scope = listeners.size() - depth;
                set = listeners.getValueAtScope(dmoIface, scope);
                boolean createSet = set == null;

                if (createSet)
                {
                   set = new ListenerSet();

                   if (set.nextOuter == null)
                   {
                      ListenerSet nextOuter = depth > 1 ? listeners.lookup(dmoIface, depth - 1)
                                                        : listeners.getValueAtScope(dmoIface, -1);
                      if (nextOuter != null)
                      {
                         set.link(nextOuter);
                      }
                   }

                   listeners.addEntryAt(depth - 1, dmoIface, set);
                }

                set.add(listener);
             }
          }
       }

       /**
        * Remove a listener from global scope. This is only called for listeners whose buffers belong
        * to a procedure that was executed as persistent. In this case, opening the buffer registered
        * it to the global scope. This buffer is available as long as the procedure survives. When the
        * procedure is removed / deleted the buffer has to be removed as well.
        * <p>
        * If this buffer was not open in global scope (from a persistent procedure) this is a no-op and
        * the returned value is <code>false</code>.
        *
        * @param   listener
        *          The listener to be removed.
        *
        * @return  <code>true</code> if the listener was successfully removed from global scope.
        */
       boolean removeFromGlobal(RecordChangeListener listener)
       {
          boolean ret = false;
          Iterator<RecordBuffer> iter = listener.recordBuffers();
          while (iter.hasNext())
          {
             RecordBuffer buffer = iter.next();
             Class<? extends DataModelObject> dmoIface = buffer.getDMOInterface();
             // get the global scope for the dmoIface:
             ListenerSet set = listeners.size() == 0 ? null : listeners.getValueAtScope(dmoIface, -1);
             if (set != null)
             {
                ret |= set.remove(listener);

                // do the cleanup:
                if (set.isEmpty())
                {
                   // if set of listeners is empty, remove from global scope
                   listeners.removeEntryAtScope(dmoIface, listeners.size() - 1);
                   set.unlink();
                }
             }
          }
          return ret;
       }

       /**
        * Remove the specified listener from all scopes.
        *
        * @param    listener
        *           The listener to be removed.
        */
       void removeListener(RecordChangeListener listener)
       {
          Iterator<RecordBuffer> iter = listener.recordBuffers();
          while (iter.hasNext())
          {
             RecordBuffer buffer = iter.next();
             Class<? extends DataModelObject> dmoIface = buffer.getDMOInterface();

             listeners.apply(dmoIface, (dictionary) ->
             {
                ListenerSet set = dictionary.get(dmoIface);

                if (set.remove(listener))
                {
                   if (set.isEmpty())
                   {
                      set.unlink();
                      dictionary.remove(dmoIface);
                   }

                   return true;
                }

                return false;
             });
          }
       }

       /**
        * Check if there are any listeners registered with a query, for this temp-table, identified by its DMO
        * interface and the multiplex ID.
        *
        * @param    dmoIface
        *           Data model object interface.
        * @param    multiplexID
        *           The multiplex ID for the temp-table reclaiming this key.
        *
        * @return   See above.
        */
       boolean isQueried(Class<? extends DataModelObject> dmoIface, Integer multiplexID)
       {
          Function<ListenerSet, Boolean> queried = (lset) ->
          {
             for (RecordChangeListener listener : lset)
             {
                if (!(listener instanceof P2JQuery))
                {
                   continue;
                }

                Iterator<RecordBuffer> iter = listener.recordBuffers();
                while (iter.hasNext())
                {
                   RecordBuffer buf = iter.next();
                   if (multiplexID.equals(buf.getMultiplexID()))
                   {
                      return true;
                   }
                }
             }

             return false;
          };

          ListenerSet set = listeners.lookup(dmoIface);
          if (set != null)
          {
             if (queried.apply(set))
             {
                return true;
             }
          }

          for (Map.Entry<Object, Map<Class<?>, ListenerSet>> entry : procListeners.entrySet())
          {
             Map<Class<?>, ListenerSet> procScope = entry.getValue();
             ListenerSet procSet = procScope.get(dmoIface);
             if (procSet != null && queried.apply(procSet))
             {
                return true;
             }
          }

          return false;
       }

       /**
        * Broadcast a notification that a DMO's state has changed.  All listeners
        * registered to receive events about DMOs of the type managed by
        * <code>buffer</code> will receive the event.
        *
        * @param   type
        *          Event type:  insert, update, or delete.
        * @param   buffer
        *          Record buffer which originated the event.
        * @param   dmo
        *          <b>Post-modification</b> state of the affected DMO (in the
        *          case where the modification is the deletion of the DMO from
        *          the database, this will be the object instance which was
        *          deleted).
        * @param   snapshot
        *          A deep copy of the DMO which reflects the state of the DMO as
        *          it was first set into the record buffer.  For a newly created
        *          record, this will contain the primary key assigned to the
        *          record.  All other properties will be set to their default
        *          values.
        * @param   properties
        *          A map whose keys are the names of those properties which have
        *          been modified and whose values are lists of the indexes at
        *          which those values are stored (indexed properties only).  For
        *          a simple property, the matching value will be
        *          <code>null</code>.
        *
        * @throws  PersistenceException
        *          if any error occurs within a listener while processing a
        *          change event.  Exceptions will be deferred until all listeners
        *          have had a chance to process the event.  If more than one
        *          listener throws an exception, only the first will be rethrown
        *          by this method;  however, all will be logged.
        */
       void stateChanged(RecordChangeEvent.Type type,
                         RecordBuffer buffer,
                         Record dmo,
                         Record snapshot,
                         Map<String, List<Integer>> properties)
       throws PersistenceException
       {
          RecordChangeEvent event = null;

          if (dmo != null)
          {
             event = new RecordChangeEvent(type, buffer, dmo, snapshot, properties);
          }

          Class<? extends DataModelObject> dmoIface = buffer.getDMOInterface();
          ListenerSet set = listeners.lookup(dmoIface);
          if (set == null)
          {
             return;
          }

          if (event == null)
          {
             event = new RecordChangeEvent(type, buffer, dmo, snapshot, properties);
          }
          Exception error = null;

          // Notify all registered listeners.
          for (RecordChangeListener listener : set)
          {
             try
             {
                listener.stateChanged(event);
             }
             catch (Exception exc)
             {
                DBUtils.handleException(buffer.getDatabase(), exc);

                // Can only throw the first exception caught, but we want to
                // give all listeners a chance to process the event.
                if (error == null)
                {
                   error = exc;
                }

                // Log it regardless.
                LOG.log(Level.SEVERE, exc.getMessage(), exc);
             }
          }

          // process all listeners saved for persistent procedures, if they have registered for this
          // DMO interface
          for (Map.Entry<Object, Map<Class<?>, ListenerSet>> entry : procListeners.entrySet())
          {
             Map<Class<?>, ListenerSet> procScope = entry.getValue();
             ListenerSet procSet = procScope.get(dmoIface);
             if (procSet != null)
             {
                for (RecordChangeListener listener : procSet)
                {
                   try
                   {
                      listener.stateChanged(event);
                   }
                   catch (Exception exc)
                   {
                      DBUtils.handleException(buffer.getDatabase(), exc);

                      // Can only throw the first exception caught, but we want to
                      // give all listeners a chance to process the event.
                      if (error == null)
                      {
                         error = exc;
                      }

                      // Log it regardless.
                      LOG.log(Level.SEVERE, exc.getMessage(), exc);
                   }
                }
             }
          }

          if (error != null)
          {
             throw new PersistenceException(error);
          }
       }

       /**
        * An ordered set of listeners (iterates in the order added to the set), which optionally
        * links to other sets of listeners up and down the stack of open scopes.
        * <p>
        * When the set is iterated, it first walks its links to other listener sets to the outermost
        * in scope and iterates those first. It then walks reverse (weak) links back to more deeply
        * nested scopes and iterates listeners in those sets, and so on. Duplicate listeners are
        * only visited once.
        */
       private static class ListenerSet
       implements Iterable<RecordChangeListener>
       {
          /** Entities which have registered interest in DMO property changes */
          private final LinkedHashSet<RecordChangeListener> interested = new LinkedHashSet<>();

          /** Next set of listeners registered for the same events in an outer scope */
          private ListenerSet nextOuter = null;

          /** Next set of listeners registered for the same events in an inner scope */
          private WeakReference<ListenerSet> nextInner = null;

          /**
           * Get an iterator which will iterate over outermost to innermost listeners in all outer
           * listener sets linked to and including this listener set.
           *
           * @return  Iterator as described above.
           */
          @Override
          public Iterator<RecordChangeListener> iterator()
          {
             return new Iter();
          }

          /**
           * Return a string representation of this object's state, primarily for debugging purposes.
           *
           * @return  Description of this object's state.
           */
          public String toString()
          {
             StringBuilder buf = new StringBuilder(superString());
             buf.append(";  inner:  ");
             buf.append(nextInner == null
                        ? "null"
                        : (nextInner.get() == null ? "null" : nextInner.get().superString()));
             buf.append(";  outer:  ");
             buf.append(nextOuter == null ? null : nextOuter.superString());

             return buf.toString();
          }

          /**
           * Add a listener.
           *
           * @param   listener
           *          Listener to be registered.
           *
           * @return  {@code true} if the listener was added; {@code false} if it was already present.
           */
          boolean add(RecordChangeListener listener)
          {
             return interested.add(listener);
          }

          /**
           * Remove a listener.
           *
           * @param   listener
           *          Listener to be deregistered.
           *
           * @return  {@code true} if the listener was removed; {@code false} if it could not be
           *          found.
           */
          boolean remove(RecordChangeListener listener)
          {
             return interested.remove(listener);
          }

          /**
           * Indicate whether the set of registered listeners is empty.
           *
           * @return  {@code true} if empty, else {@code false}
           */
          boolean isEmpty()
          {
             return interested.isEmpty();
          }

          /**
           * Link to this object the next related listener set in an outer scope. Listeners in that
           * set will receive notifications before listeners in this set.
           * <p>
           * A link from {@code nextOuter} is made back to this listener set, so iteration across
           * these sets can work its way back from the outer set to this one. The link in this
           * direction is made with a weak reference, so this set can be garbage collected if its
           * scope is popped.
           *
           * @param   nextOuter
           *          Outer listener set to be linked to this one.
           */
          void link(ListenerSet nextOuter)
          {
             ListenerSet outersInner = nextOuter.nextInner != null ? nextOuter.nextInner.get(): null;

             if (outersInner != null)
             {
                this.nextInner = new WeakReference<>(outersInner);
                outersInner.nextOuter = this;
             }

             this.nextOuter = nextOuter;
             nextOuter.nextInner = new WeakReference<>(this);
          }

          /**
           * Unlink this object from its next outer and next inner listener sets, if any, and link
           * those sets to each other instead.
           */
          void unlink()
          {
             ListenerSet nextOuter = this.nextOuter;
             WeakReference<ListenerSet> nextInner = this.nextInner;

             if (nextInner != null)
             {
                ListenerSet set = nextInner.get();
                if (set != null)
                {
                   // replace nextInner's reference to us with reference to our nextOuter
                   set.nextOuter = nextOuter;
                }
             }

             if (nextOuter != null)
             {
                // replace nextOuter's reference to us with reference to our nextInner
                nextOuter.nextInner = nextInner;
             }

             this.nextOuter = null;
             this.nextInner = null;
          }

          /**
           * Return the parent class' string representation of this object's state.
           *
           * @return  The return value of the parent class' {@code toString()} method.
           */
          private String superString()
          {
             return super.toString();
          }

          /**
           * An iterator which walks all record change listeners registered to receive notifications
           * for a DMO type. The walk starts at the outermost scope and works its way to inner scopes,
           * ending with the listeners in the enclosing listener set. If a listener is registered in
           * more than one scope, it is visited only once, in the outermost scope in which it is
           * registered. Listeners are visited in the order in which they were registered. If the
           * enclosing listener set has not been linked to any outer listener sets, only the current
           * set is walked.
           */
          private class Iter
          implements Iterator<RecordChangeListener>
          {
             /** Number of scopes to iterate */
             private int scopes = 0;

             /** Listener set currently being iterated */
             private ListenerSet currentSet;

             /** Current worker iterator */
             private Iterator<RecordChangeListener> iterator;

             /** Set of listeners already visited in this iteration */
             private Set<RecordChangeListener> visited = null;

             /** Next listener to be visited */
             private RecordChangeListener next = null;

             /**
              * Default constructor.
              */
             Iter()
             {
                // start iterating the listener set at the outermost scope
                ListenerSet set = ListenerSet.this;
                while (set.nextOuter != null)
                {
                   set = set.nextOuter;
                   scopes++;
                }
                currentSet = set;
                iterator = currentSet.interested.iterator();

                // track visited listeners if we have more than one listener set to iterate
                if (scopes > 0)
                {
                   visited = Collections.newSetFromMap(new IdentityHashMap<>());
                }
             }

             /**
              * Indicate whether there is a next listener to be visited.
              *
              * @return  {@code true} if there is a next listener, else {@code false}.
              */
             @Override
             public boolean hasNext()
             {
                if (next != null)
                {
                   return true;
                }

                boolean result;

                do
                {
                   // reset next listener
                   next = null;

                   // if current iterator has no more listeners to visit and currentSet has not been
                   // reset, try to advance to next inner listener set
                   while (!(result = iterator.hasNext()) && currentSet != null)
                   {
                      // only advance as many scopes as we counted during construction
                      if (--scopes >= 0)
                      {
                         WeakReference<ListenerSet> nextInner = currentSet.nextInner;
                         if (nextInner == null)
                         {
                            currentSet = null;

                            return false;
                         }
                         else
                         {
                            currentSet = nextInner.get();
                            if (currentSet != null)
                            {
                               iterator = currentSet.interested.iterator();
                            }
                            else
                            {
                               return false;
                            }
                         }
                      }
                      else
                      {
                         currentSet = null;

                         return false;
                      }
                   }

                   if (result)
                   {
                      next = iterator.next();
                   }

                } while (visited != null && next != null && visited.contains(next));

                return result;
             }

             /**
              * Get the next listener to be visited.
              *
              * @return  Next record change listener.
              */
             @Override
             public RecordChangeListener next()
             {
                if (!hasNext())
                {
                   throw new NoSuchElementException();
                }

                if (visited != null)
                {
                   visited.add(next);
                }

                RecordChangeListener l = next;
                next = null;

                return l;
             }

             /**
              * Implementation of listener removal which throws an exception, since removal through
              * this iterator type is not supported.
              *
              * @throws  UnsupportedOperationException
              *          always.
              */
             @Override
             public void remove()
             {
                throw new UnsupportedOperationException();
             }
          }
       }

       /**
        * Convenient implementation that allows the local context to be lazily initialized.
        * The change broker depends on the buffer manager, so does the buffer manager depend
        * on the change broker. To avoid infinite recursion, the change broker opts to be
        * lazily initialized.
        */
       static class ContextContainer
       extends ContextLocal<ChangeBroker>
       {
          /**
           * Get the weight of this context-local var.
           *
           * @return   Always {@link WeightFactor#LEVEL_6}.
           */
          @Override
          public WeightFactor getWeight()
          {
             return WeightFactor.LEVEL_6;
          }

          /**
           * Obtains the context-local instance of the contained global work
           * area.  The first time this method is invoked in a context, a new
           * {@code ChangeBroker} instance is lazily created.
           *
           * @return  The change broker associated with this context.
           */
          public ChangeBroker obtain()
          {
             ChangeBroker broker = get();

             if (broker == null)
             {
                broker = new ChangeBroker();

                set(broker);

                broker.bufferManager = BufferManager.get();
                broker.pm = ProcedureManager.getProcedureHelper();
                broker.tm = TransactionManager.getTransactionHelper();
             }

             return broker;
          }

          /**
           * Return {@code null} rather than providing a true initial value;
           * initial value is instead lazily created in {@link #obtain}.
           *
           * @return  {@code null}.
           */
          @Override
          protected ChangeBroker initialValue()
          {
             return null;
          }
       }
    }