# DatabaseTriggerManager.java

    /*
    ** Module   : DatabaseTriggerManager.java
    ** Abstract : Database trigger manager.
    **
    ** Copyright (c) 2009-2024, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
    ** 001 SVL 20091019   @44152 Created initial version. Supports assign and delete schema triggers.
    ** 002 CA  20100527   @44879 Added support for CREATE and "no record is
    **                           available triggers". The CREATE, DELETE and
    **                           ASSIGN triggers will execute on undo too, but the
    **                           passed DMO will not be the buffer proxy; instead,
    **                           it will be the actual record instance.
    ** 003 SVL 20130213          Added (de)registerDatabaseTrigger and disable*Triggers functions.
    ** 004 OM  20131107          Rewritten entire class.
    ** 005 OM  20140107          Removed openReadOnlyScope, added error message for FIND attempts on
    **                           read-only buffers created by WRITE events.
    ** 006 OM  20140711          Rewrote the initialization. The trigger list is read from xml file
    **                           instead of scanning for SchemaTrigger annotation.
    ** 007 CA  20140729          Fixed problem when invoking a trigger: the caller info (i.e.
    **                           trigger's THIS-PROCEDURE) must be pushed/popped, the same way as
    **                           UI triggers are invoked.
    ** 008 ECF 20140730          Applied fix similar to 007 to fireAssignTrigger. Added methods to
    **                           test the availability and enable status of triggers at the current
    **                           block level. Changed logging level to reduce log noise.
    ** 009 OM  20160428          Added support for multi-project scenario.
    ** 010 GES 20161006          Updated javadoc to clarify the behavior for a method.
    ** 011 OM  20180307          Added support for TriggerTracker context local awareness of other
    **                           executing trigger for same record.
    ** 012 SBI 20180518          Renamed file-system/pkgroot into legacy-system/pkgroot.
    ** 013 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.
    ** 014 AIL 20190722          Removed initialization method. Replaced schemaTrigger map and
    **                           st TriggerBlockLayer with getSchemaTrigger and
    **                           getSchemaReplicationTrigger.
    **                           Trigger searching is now made at the runtime.
    ** 015 ECF 20190725          Mark the read-only "old" buffer created for a write trigger as a
    **                           "fake" buffer, so we do not process it like an active buffer.
    ** 016 AIL 20190805          Replaced getSchemaTrigger WARNINGS into proper errors. Reworked
    **                           cases in which errors arise.
    **         20190821          Added support for assign triggers that do not have a reference
    **                           to a buffer, basically declared with NEW VALUE.
    ** 017 CA  20190908          When checking a trigger's table, the comparison must be
    **                           case-insensitive.
    ** 018 CA  20191119          Track the trigger class, to be able to register any buffer to its
    **                           source definition class.
    ** 019 ECF 20200906          New ORM implementation.
    ** 020 ECF 20200916          Use DmoMeta to get schema name instead of DatabaseManager.getSchemaByInterface.
    **     CA  20200927          Use IdentityHashMap instead of plain map when the key is a Class.
    **     OM  20210512          trigger() return false if the trigger procedure vetoed the event.
    **                           Dropped vestigial code related to schema-triggers.xml.
    **     OM  20210603          Implemented vetoing mechanism for triggers.
    **     OM  20220506          Cleanup execution trigger stack.
    **     TW  20220803          Error-status:error must be set for return error in method, while return error
    **                           in old-style ABL function must remain the same. Refs: #6567.
    **     TW  20220817          Error-status:error must be set for return error in static method.
    **     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.
    **     TJD 20220504          Java 11 compatibility minor changes
    **     CA  20221206          Fixed session triggers created in persistent programs: these are saved and the
    **                           lookup is done in a LIFO order (top of stack down).
    **     CA  20230110          Cache the helper for ProcedureManager, ObjectOps and others (as needed), to
    **                           reduce context-local lookup.
    ** 021 SVL 20230317          Pause registration of off-end queries while we're in a database trigger.
    ** 022 GBB 20230512          Logging methods replaced by CentralLogger/ConversionStatus.
    ** 023 OM  20230915          Added support for generic session triggers.
    ** 024 CA  20231026          A persistent procedure with a pending delete is no longer persistent, so
    **                           scopeDeleted needs to be executed.
    ** 025 OM  20231125          Redesign generic session triggers.
    ** 026 AD  20240125          Added support for the 4GL parameter '-trig' when searching for trigger classes.
    ** 027 DDF 20240423          Modified how the error handling for a missing trigger is done.
    **     DDF 20240520          isTriggerEnabled() should not rely on getSchemaTrigger() because it should
    **                           only check if it exists in the TableMapper (LegacyTriggerInfo).
    ** 028 TJD 20240123          Java 17 compatibility updates
    ** 029 EAB 20240611          Moved a block of code in DmoMetadataManager (the getDmoIface method) to avoid
    **                           duplication.
    ** 030 AL2 20240628          Open scope of buffer for generic triggers to force its clear-up.
    **     AL2 20240701          Reworked solution to open scope only if not already opened by the trigger.
    **     AL2 20240701          Slow down logging generic trigger warnings to avoid log spam.
    */

    /*
    ** 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.trigger;

    import com.goldencode.p2j.persist.*;
    import com.goldencode.p2j.persist.Record;
    import com.goldencode.p2j.persist.orm.*;
    import com.goldencode.p2j.security.*;
    import com.goldencode.p2j.util.*;
    import com.goldencode.p2j.util.logging.*;
    import java.lang.reflect.*;
    import java.util.*;
    import java.util.logging.Level;

    /**
     * This class handles the management of trigger system for a database. For session triggers, it maintains a
     * stack of scopes (a layer of triggers for each subroutine block).
     * <br>
     * An additional independent virtual level for generic triggers (which are executed first, non-vetoable) is
     * composed of the {@code genericXXXXTriggers} maps. These can be {@code null} if no such triggers were
     * registered.
     * <br>
     * There is a last layer formed by the schema triggers. If defined, they are executed last, and only if not
     * overridden by the session trigger. They are not stored here but the code to be executed is dynamically
     * computed based on annotation declared in DMO and current PROPATH.
     * <p>
     * One instance of this class is associated with a user context and, because this class implements
     * {@link Scopeable} it is initialized in {@code StandardServer.invoke()}, before the
     * topmost "startup" scope is pushed.
     * The {@code DatabaseTriggerManager} is notified when the execution enters and leaves
     * subroutine blocks, so it can do the management of session trigger layers.
     */
    public class DatabaseTriggerManager
    implements Scopeable
    {
       /** Logger */
       private static final CentralLogger LOG = CentralLogger.get(DatabaseTriggerManager.class);

       /** Context local map of instances of this class, mapped by theirs database name . */
       private static final ContextLocal<DatabaseTriggerManager> context =
       new ContextLocal<DatabaseTriggerManager>()
       {
          protected DatabaseTriggerManager initialValue()
          {
             DatabaseTriggerManager local = new DatabaseTriggerManager();
             local.computeTriggerSearchPaths();
             if (local.trigPath.isPresent() && !local.isTrigAbsolutePath)
             {
                EnvironmentOps.addSourcePathListener((path) -> {
                   local.triggerSearchPaths = null;
                   local.proceduresClassMap.clear();
                });
             }

             return local;
          }
       };

       /**
        * The number of times we attempted to write the log on generic trigger old-buffer
        * not being handled properly
        */
       private static int logCluteringProtection = 0;

       /**
        * A rate at which we attempt to slow-down the logs on generic trigger old-buffer
        * not being handled properly. This is increasing periodically to reduce the number of logs.
        */
       private static int logCluteringThreshold = 1;

       /** Helper to use the ProcedureManager without any context local lookups. */
       private final ProcedureManager.ProcedureHelper pm = ProcedureManager.getProcedureHelper();

       /** Helper to use the TM without any context local lookups. */
       private final TransactionManager.TransactionHelper tm = TransactionManager.getTransactionHelper();

       /** The stack of all layer of triggers handled by this manager. */
       private final Deque<TriggerBlockLayer> stack = new ArrayDeque<>();

       /** The mapping of registered triggers for a persistent procedure. */
       private final Map<Object, TriggerBlockLayer> persistentTriggers = new IdentityHashMap<>();

       /** The order of the {@link #persistentTriggers}. */
       private final Deque<Object> programs = new ArrayDeque<>();

       /** The stack of currently executing triggers in this context for each record id. */
       private final Map<Class<? extends Buffer>,
                         HashMap<Long, Deque<DatabaseEventType>>> executing = new IdentityHashMap<>();

       /**
        * The paths to search for the trigger procedures, either an absolute path given at startup,
        * or the relative trigger path concatenated to the propath.
        */
       private String[] triggerSearchPaths = null;

       /** The trig parameter path specified by the client, with absolute prefix removed if necessary. */
       private Optional<String> trigPath = null;

       /** Indicates whether the client trigger path is an absolute path or not. */
       private boolean isTrigAbsolutePath;

       /** Procedure name to java class map. */
       private final Map<String, Class<?>> proceduresClassMap = new HashMap<>();

       /** The data for FIND generic triggers mapped by database. */
       private Map<String, TriggerData> genericFindTriggers = null;

       /** The data for CREATE generic triggers mapped by database. */
       private Map<String, TriggerData> genericCreateTriggers = null;

       /** The data for WRITE generic triggers mapped by database. */
       private Map<String, TriggerData> genericWriteTriggers = null;

       /** The data for DELETE generic triggers mapped by database. */
       private Map<String, TriggerData> genericDeleteTriggers = null;

       /** The data for ASSIGN generic triggers mapped by database and then by property name. */
       private Map<String, Map<String, TriggerData>> genericAssignTriggers = null;

       /**
        * Private constructor. An instance of this class can be obtained only using {@code get()} method.
        * Registers this object with TransactionManager as a Scopeable instance.
        */
       private DatabaseTriggerManager()
       {
          if (LOG.isLoggable(Level.FINE))
          {
             LOG.log(Level.FINE, "DatabaseTriggerManager registered with TransactionManager.");
          }
       }
       /**
        * Compute the locations where the search for the trigger procedures will happen depending on
        * the trig path value. If the trig path is an absolute path, the search happens only in it.
        * If the trig path is relative, it is concatenated to the end of every propath path, so the
        * search is done in this modified propath.
        */
       private void computeTriggerSearchPaths()
       {
          if (triggerSearchPaths != null || (trigPath != null && !trigPath.isPresent()))
          {
             return;
          }

          if (trigPath == null)
          {
             String trigLocation = SessionUtils._startupParameters().getTriggerLocation();
             trigPath = Optional.ofNullable(trigLocation);
             if (!trigPath.isPresent())
             {
                return;
             }
          }

          String absolutePrefix = SourceNameMapper.getAbsolutePrefix(trigPath.get());
          isTrigAbsolutePath = absolutePrefix != null;
          // use the trig parameter path as an absolute path
          if (isTrigAbsolutePath)
          {
             trigPath = Optional.ofNullable(SourceNameMapper.removeAbsolutePrefix(trigPath.get(), absolutePrefix));
             if (!trigPath.isPresent())
             {
                return;
             }

             String token = Utils.getProjectToken();
             if (token != null)
             {
                triggerSearchPaths = new String[2];
                triggerSearchPaths[1] = token + SourceNameMapper.getFileSep() + trigPath;
             }
             else
             {
                triggerSearchPaths = new String[1];
             }
             triggerSearchPaths[0] = trigPath.get();
          }
          else
          {
             String[] propath = SourceNameMapper.getPropath();
             triggerSearchPaths = new String[propath.length + 1];
             triggerSearchPaths[0] = "." + SourceNameMapper.getFileSep() + trigPath;
             for (int i = 1; i < triggerSearchPaths.length; i++)
             {
                triggerSearchPaths[i] = propath[i - 1] + SourceNameMapper.getFileSep() + trigPath;
             }
          }
       }

       /**
        * Obtain the container that holds information for instantiating a schema trigger object
        * responsible for a {@code det} event. If such trigger does not exist, or the procedure
        * defined is invalid, a null object is returned.
        *
        * @param   det
        *          The database event to which the trigger responds to.
        * @param   bufferCls
        *          The class that defines the table for which the trigger applies to.
        * @param   property
        *          Optionally, for {@code ASSIGN} triggers the name of the property (field) for which the
        *          trigger was declared. Otherwise, it is ignored.
        *
        * @return  The container that holds information of how to create an instance of the specified
        *          trigger and how to apply it or null if such schema trigger has not been declared.
        */
       static TriggerData getSchemaTrigger(DatabaseEventType det,
                                           Class<? extends Buffer> bufferCls,
                                           String property)
       {
          // search for the DMO interface of the bufferCls
          Class<? extends DataModelObject> dmoIface = DmoMetadataManager.getDmoIface(bufferCls);

          if (dmoIface == null)
          {
             // no suitable DMO interface was found for the given bufferCls
             return null;
          }

          // identify the trigger procedure meant to be executed
          String procedure = TableMapper.getLegacyTriggerProcedure(dmoIface, det.toString(), property);
          if (procedure == null)
          {
             // no procedure for the det trigger at the DMO's LegacyTrigger annotation
             return null;
          }

          DatabaseTriggerManager local = get();
          Class<?> cls = local.proceduresClassMap.get(procedure);

          if (cls == null)
          {
             String javaClassName = null;

             if (SourceNameMapper.getAbsolutePrefix(procedure) != null)
             {
                javaClassName = SourceNameMapper.convertNameToClass(procedure, new String[]{"."});
             }

             if (javaClassName == null)
             {
                // in 4GL, even if -trig is specified, if the 'procedure' file path starts with one of these
                // prefixes, then is 'like' an absolute path and -trig parameter is ignored in this case.
                // however, if the procedure path contains one of the prefixes but does not start with it,
                // -trig parameter will be used
                String[] prefixes = new String[] {"./", ".\\", "../", "..\\"};
                for (int i = 0; i < prefixes.length; i++)
                {
                   if (procedure.startsWith(prefixes[i]))
                   {
                      javaClassName = SourceNameMapper.convertNameToClass(procedure, new String[]{"."});
                      break;
                   }
                }
             }

             if (javaClassName == null)
             {
                if (local.triggerSearchPaths == null)
                {
                   local.computeTriggerSearchPaths();
                }

                if (local.triggerSearchPaths != null)
                {
                   javaClassName = SourceNameMapper.convertNameToClass(procedure, local.triggerSearchPaths);
                }
                else
                {
                   javaClassName = SourceNameMapper.convertNameToClass(procedure, false);
                }
             }

             if (javaClassName == null)
             {
                String pathNotFound = (local.trigPath == null || !local.trigPath.isPresent()) ? procedure :
                   local.trigPath + SourceNameMapper.getFileSep() + procedure;

                // there is no conversion for the file provided.
                ErrorManager.showErrorAndAbend(293,
                                               String.format("\"%s\" was not found", pathNotFound),
                                               true);
                return null;
             }

             try
             {
                cls = Class.forName(javaClassName);
                local.proceduresClassMap.put(procedure, cls);
             }
             catch (ClassNotFoundException e)
             {
                // this should not happen
                // the SourceNameMapper.convertNameToClass returns a valid Java Class name
                if (LOG.isLoggable(Level.SEVERE))
                {
                   LOG.log(Level.SEVERE,
                      String.format("The name of the Java Class for %s is not valid", procedure));
                }
                return null;
             }
          }

          if (!(cls.getSuperclass().equals(DatabaseTrigger.class)))
          {
             // the target procedure is not a trigger procedure
             if (det.equals(DatabaseEventType.ASSIGN))
             {
                ErrorManager.recordOrThrowError(
                      3248,
                      String.format("Cannot run %s for a FIND, DELETE, or CREATE trigger", procedure),
                      false);
             }
             else
             {
                ErrorManager.recordOrThrowError(
                      3250,
                      String.format("Cannot run %s for an ASSIGN trigger", procedure),
                      false);
             }
             return null;
          }

          @SuppressWarnings({ "unchecked" /*, "rawtypes"*/ })
          Class<? extends DatabaseTrigger<Buffer>> triggerCls = (Class<? extends DatabaseTrigger<Buffer>>) cls;
          SchemaTrigger schemaTriggerIface = triggerCls.getAnnotation(SchemaTrigger.class);

          if (schemaTriggerIface == null)
          {
             // this should not happen; the conversion should annotate a DatabaseTrigger with SchemaTrigger
             if (LOG.isLoggable(Level.SEVERE))
             {
                LOG.log(Level.SEVERE,
                        "%s should be converted in a DatabaseTrigger with SchemaTrigger annotation",
                        procedure);
             }
             return null;
          }

          boolean hasNewBuffer = !schemaTriggerIface.buffer().equals(EmptyBuffer.class);

          String tableTrg = TableMapper.getLegacySchemaName(dmoIface);
          if (!schemaTriggerIface.table().equalsIgnoreCase(tableTrg) && hasNewBuffer)
          {
             // the schema trigger event is not for the same table as the trigger procedure
             ErrorManager.recordOrThrowError(
                   3240,
                   String.format("Trigger for table %s executing %s which is FOR %s table",
                                 tableTrg,
                                 procedure,
                                 schemaTriggerIface.table()),
                   false);
             return null;
          }

          // the event type should be almost the same: either both of them are FIND/CREATE/DELETE
          // triggers or both of them are ASSIGN triggers.
          if (!schemaTriggerIface.event().equals(DatabaseEventType.ASSIGN) &&
              det.equals(DatabaseEventType.ASSIGN))
          {
             ErrorManager.recordOrThrowError(
                new int[] {3245, 3250},
                new String[] {
                   String.format("%s is a FIND, DELETE, or CREATE trigger procedure", procedure),
                   String.format("Cannot run %s for an ASSIGN trigger", procedure)
                },
                false);
             return null;
          }

          if (schemaTriggerIface.event().equals(DatabaseEventType.ASSIGN) &&
              !det.equals(DatabaseEventType.ASSIGN))
          {
             ErrorManager.recordOrThrowError(
                   new int[] {3247, 3248},
                   new String[] {
                      String.format("%s is an ASSIGN trigger procedure", procedure),
                      String.format("Cannot run %s for a FIND, DELETE, or CREATE trigger", procedure)
                   },
                   false);
             return null;
          }

          boolean overrideable = TableMapper.isLegacyTriggerOverrideable(dmoIface, det.toString(), property);
          boolean hasOldBuffer = schemaTriggerIface.hasOldBuffer();
          return new TriggerData(triggerCls, null, overrideable, hasOldBuffer, hasNewBuffer, false);
       }

       /**
        * Obtain the container that hold information for instantiating a schema trigger object for
        * the replication version of the {@code det} event. This call is only valid for events
        * that have replication equivalent. If these conditions are not met or no such replication
        * trigger was defined, then a null object is returned.
        *
        * @param   det
        *          The database event whose replication equivalent trigger responds to.
        * @param   bufferCls
        *          The buffer class for the dmo that fired the trigger.
        *
        * @return  The container that holds information of how to create an instance of the
        *          replication schema trigger.
       */
       static TriggerData getReplicationSchemaTrigger(DatabaseEventType det, Class<? extends Buffer> bufferCls)
       {
          // check if the requested det has a replication event, otherwise return null
          switch (det)
          {
             case CREATE:
                det = DatabaseEventType.REPLICATION_CREATE;
                break;
             case DELETE:
                det = DatabaseEventType.REPLICATION_DELETE;
                break;
             case WRITE:
                det = DatabaseEventType.REPLICATION_WRITE;
                break;
             default:
                return null;
          }

          // now det should be the replication event we need to look for
          return getSchemaTrigger(det, bufferCls, null);
       }

       /**
        * Obtain the context-local DatabaseTriggerManager object.
        *
        * @return  the DatabaseTriggerManager for current context.
        */
       public static DatabaseTriggerManager get()
       {
          return context.get();
       }

       /**
        * Register a session database trigger within the current scope. Any previously
        * registered session trigger on current block is silently removed. This overloaded method is
        * always not overriding the schema trigger and is not declaring an {@code OLD [BUFFER]}.
        *
        * @param    event
        *           Database event type.
        * @param    bufferCls
        *           The class of the buffer to which the event is applied.
        * @param    triggerCls
        *           The class of the trigger.
        * @param    container
        *           The containing object (all triggers are implemented as non-static inner classes,
        *           so they must have a reference to the containing class).
        */
       public static void registerDatabaseTrigger(DatabaseEventType event,
                                                  Class<? extends Buffer> bufferCls,
                                                  Class<? extends DatabaseTrigger<? extends Buffer>> triggerCls,
                                                  Object container)
       {
          registerDatabaseTrigger(event, bufferCls, triggerCls, container, false, false);
       }

       /**
        * Register a session database trigger within the current scope. Any previously
        * registered session trigger on current block is silently removed. This overloaded method
        * not declaring an {@code OLD [BUFFER]}.
        *
        * @param    event
        *           Database event type.
        * @param    bufferCls
        *           The class of the buffer to which the event is applied.
        * @param    triggerCls
        *           The class of the trigger.
        * @param    container
        *           The containing object (all triggers are implemented as non-static inner classes,
        *           so they must have a reference to the containing class).
        * @param    override
        *           {@code true} if {@code OVERRIDE} keyword is specified.
        */
       public static void registerDatabaseTrigger(DatabaseEventType event,
                                                  Class<? extends Buffer> bufferCls,
                                                  Class<? extends DatabaseTrigger<? extends Buffer>> triggerCls,
                                                  Object container,
                                                  boolean override)
       {
          registerDatabaseTrigger(event, bufferCls, triggerCls, container, override, false);
       }

       /**
        * Register a session database trigger within the current scope. Any previously
        * registered session trigger on current block is silently removed.
        *
        * @param   event
        *          Database event type.
        * @param   bufferCls
        *          The class of the buffer to which the event is applied.
        * @param   triggerCls
        *          The class of the trigger.
        * @param   container
        *          The containing object (all triggers are implemented as non-static inner classes,
        *          so they must have a reference to the containing class).
        * @param   override
        *          {@code true} if {@code OVERRIDE} keyword is specified.
        * @param   hasOldBuffer
        *          {@code true} if an {@code OLD [BUFFER]} keyword is specified.
        */
       public static void registerDatabaseTrigger(DatabaseEventType event,
                                                  Class<? extends Buffer> bufferCls,
                                                  Class<? extends DatabaseTrigger<? extends Buffer>> triggerCls,
                                                  Object container,
                                                  boolean override,
                                                  boolean hasOldBuffer)
       {
          DatabaseTriggerManager manager = get();
          manager.tm.registerTopLevelScopeable(manager);

          if (manager.stack.isEmpty())
          {
             // sanity check failed
             return;
          }

          // check if this event is overrideable
          if (override && !isSchemaTriggerOverrideable(event, bufferCls))
          {
             ErrorManager.recordOrThrowError(3226,
                "Attempt to OVERRIDE a schema " + event.toString().toLowerCase() + " trigger");
             return;
          }

          TriggerBlockLayer tbl = manager.stack.peekFirst();
          tbl.setTrigger(triggerCls, event, bufferCls, null, container, override, hasOldBuffer);
       }

       /**
        * Register an {@code ASSIGN} session database trigger within the current scope. Any
        * previously registered session trigger on current block is silently removed. This overloaded
        * method is always not overriding the schema trigger and is not declaring an {@code OLD [VALUE]}.
        *
        * @param    event
        *           Database event type.
        * @param    bufferCls
        *           The class of the buffer to which the event is applied.
        * @param    triggerCls
        *           The class of the trigger.
        * @param    container
        *           The containing object (all triggers are implemented as non-static inner classes,
        *           so they must have a reference to the containing class).
        */
       public static void registerDatabaseTrigger(DatabaseEventType event, // always ASSIGN
                                                  Class<? extends Buffer> bufferCls,
                                                  String property,
                                                  Class<? extends DatabaseTrigger<? extends Buffer>> triggerCls,
                                                  Object container)
       {
          registerDatabaseTrigger(event, bufferCls, property, triggerCls, container, false, false);
       }

       /**
        * Register an {@code ASSIGN} session database trigger within the current scope. Any
        * previously registered session trigger on current block is silently removed. This overloaded
        * method is not declaring an {@code OLD [VALUE]}.
        *
        * @param    event
        *           Database event type.
        * @param    bufferCls
        *           The class of the buffer to which the event is applied.
        * @param    triggerCls
        *           The class of the trigger.
        * @param    container
        *           The containing object (all triggers are implemented as non-static inner classes,
        *           so they must have a reference to the containing class).
        * @param    override
        *           {@code true} if {@code OVERRIDE} keyword is specified.
        */
       public static void registerDatabaseTrigger(DatabaseEventType event, // always ASSIGN
                                                  Class<? extends Buffer> bufferCls,
                                                  String property,
                                                  Class<? extends DatabaseTrigger<? extends Buffer>> triggerCls,
                                                  Object container,
                                                  boolean override)
       {
          registerDatabaseTrigger(event, bufferCls, property, triggerCls, container, override, false);
       }

       /**
        * Register an {@code ASSIGN} session database trigger within the current scope. Any previously
        * registered session trigger on current block is silently removed.
        *
        * @param    event
        *           Database event type.
        * @param    bufferCls
        *           The class of the buffer to which the event is applied.
        * @param    triggerCls
        *           The class of the trigger.
        * @param    container
        *           The containing object (all triggers are implemented as non-static inner classes,
        *           so they must have a reference to the containing class).
        * @param    override
        *           {@code true} if {@code OVERRIDE} keyword is specified.
        */
       public static void registerDatabaseTrigger(DatabaseEventType event, // always ASSIGN
                                                  Class<? extends Buffer> bufferCls,
                                                  String property,
                                                  Class<? extends DatabaseTrigger<? extends Buffer>> triggerCls,
                                                  Object container,
                                                  boolean override,
                                                  boolean hasOldValue)
       {
          // the first parameter is always DatabaseEventType.ASSIGN, so it can be dropped from the
          // list of parameters for this kind of event, but we keep it to have consistency
          // with invocation for other events.
          if (event != DatabaseEventType.ASSIGN)
          {
             throw new IllegalArgumentException("Use this form of register only for ASSIGN triggers.");
          }
          DatabaseTriggerManager manager = get();
          manager.tm.registerTopLevelScopeable(manager);

          if (manager.stack.isEmpty())
          {
             // sanity check failed
             return;
          }

          // check if this event is overrideable
          if (override && !isSchemaTriggerOverrideable(DatabaseEventType.ASSIGN, bufferCls))
          {
             ErrorManager.recordOrThrowError(3226, "Attempt to OVERRIDE a schema assign trigger");

             return;
          }

          TriggerBlockLayer tbl = manager.stack.peekFirst();
          tbl.setTrigger(triggerCls, event, bufferCls, property, container, override, hasOldValue);
       }

       /**
        * Unregister a session database trigger for the given event type and buffer class.
        * This is only called when a {@code ON Assign OF <table.field> REVERT}.
        *
        * @param   event
        *          Database event type.
        * @param   bufferCls
        *          The class of the buffer to which the event is applied.
        * @param   property
        *          The property that is deregistered from {@code ASSIGN} trigger.
        */
       public static void deregisterDatabaseTrigger(DatabaseEventType event,
                                                    Class<? extends Buffer> bufferCls,
                                                    String property)
       {
          // the first parameter is always DatabaseEventType.ASSIGN, so it can be dropped from the
          // list of parameters for this kind of event, but we keep it to have consistency
          // with invocation for other events.

          if (event != DatabaseEventType.ASSIGN)
          {
             throw new IllegalArgumentException("Use this form of register only for ASSIGN triggers.");
          }

          DatabaseTriggerManager manager = get();
          manager.tm.registerTopLevelScopeable(manager);

          if (manager.stack.isEmpty())
          {
             // sanity check failed
             return;
          }

          TriggerBlockLayer tbl = manager.stack.peekFirst();
          tbl.removeTrigger(event, bufferCls, property);
       }

       /**
        * Unregister a session database trigger for the given event type and buffer class.
        * This is only called when a {@code ON <event> OF <table> REVERT}.
        *
        * @param    event
        *           Database event type.
        * @param    bufferCls
        *           The class of the buffer to which the event is applied.
        */
       public static void deregisterDatabaseTrigger(DatabaseEventType event,
                                                    Class<? extends Buffer> bufferCls)
       {
          // the first parameter is always DatabaseEventType.ASSIGN, so it can be dropped from the
          // list of parameters for this kind of event, but we keep it to have consistency
          // with invocation for other events.

          if (event == DatabaseEventType.ASSIGN)
          {
             throw new IllegalArgumentException("Not executing ASSIGN with this method.");
          }

          DatabaseTriggerManager manager = get();
          manager.tm.registerTopLevelScopeable(manager);

          if (manager.stack.isEmpty())
          {
             // sanity check failed
             return;
          }

          TriggerBlockLayer tbl = manager.stack.peekFirst();
          tbl.removeTrigger(event, bufferCls, null);
       }

       /**
        * Disables dump triggers ({@code FIND} events) for the current block and any block of a
        * callee from this block (function, internal/external procedure).
        *
        * @param   bufferCls
        *          The buffer for which to disable dump triggers.
        *
        * @return  {@code true} if the filter was successfully applied. If {@code false}
        *          is returned then the internal stack is broken because of bad block management.
        */
       public static boolean disableDumpTriggers(Class<? extends Buffer> bufferCls)
       {
          DatabaseTriggerManager manager = get();
          manager.tm.registerTopLevelScopeable(manager);

          if (manager.stack.isEmpty())
          {
             return false;
          }

          TriggerBlockLayer tbl = manager.stack.peekFirst();
          tbl.disableTriggers(bufferCls, true, false, false);
          return true;
       }

       /**
        * Disables load triggers ({@code CREATE},{@code ASSIGN}, {@code WRITE} events)
        * for the current block. The replication triggers are also disabled.
        *
        * @param   bufferCls
        *          The buffer for which to disable load triggers.
        *
        * @return  {@code true} if the filter was successfully applied. If {@code false}
        *          is returned then the internal stack is broken because of bad block management.
        */
       public static boolean disableLoadTriggers(Class<? extends Buffer> bufferCls)
       {
          return disableLoadTriggers(bufferCls, false);
       }

       /**
        * Disables load triggers ({@code CREATE}, {@code ASSIGN}, {@code WRITE} events) for the current block.
        *
        * @param   bufferCls
        *          The buffer for which to disable load triggers.
        * @param   allowReplication
        *          If {@code true} the replication triggers will not be disabled for this block.
        *
        * @return  {@code true} if the filter was successfully applied. If {@code false}
        *          is returned then the internal stack is broken because of bad block management.
        */
       public static boolean disableLoadTriggers(Class<? extends Buffer> bufferCls,
                                                 boolean allowReplication)
       {
          DatabaseTriggerManager manager = get();
          manager.tm.registerTopLevelScopeable(manager);

          if (manager.stack.isEmpty())
          {
             return false;
          }

          TriggerBlockLayer tbl = manager.stack.peekFirst();
          tbl.disableTriggers(bufferCls, false, true, allowReplication);

          return true;
       }

       /**
        * Executes trigger suite for a base event (except for {@code ASSIGN} triggers). The suite includes the
        * session, simple schema and replication triggers that have been defined/assigned for the {@code det} base
        * event. The order in which they execute depends on {@code det}: session triggers are always executed
        * first and replication triggers last, except for the {@code FIND} triggers, where schema triggers
        * execute before session triggers and there is no replication trigger to execute.
        *
        * @param   det
        *          The trigger type to be executed.
        * @param   bufferCls
        *          The buffer class for the dmo that fired the trigger.
        * @param   newVal
        *          The buffer that fired the trigger.
        * @param   oldDmoValue
        *          When the (WRITE) trigger declares an 'old' buffer value, this is passed to trigger procedure as
        *          r/o buffer.
        * @param   database
        *          The database to apply to. In future revisions, this parameter might be replaced by a table
        *          pattern.
        *
        * @return  {@code true} if no trigger was executed, or they executed with success. The method will return
        *          {@code false} if the trigger vetoed the operation (by returning error).
        */
       public static boolean trigger(DatabaseEventType det,
                                     Class<? extends Buffer> bufferCls,
                                     Buffer newVal,
                                     Record oldDmoValue,
                                     String database)
       {
          // TODO: Persistence.load():2029: Object o = query.uniqueResult();
          //       can trigger an uncaught WRITE database event

          DatabaseTriggerManager manager = get();
          TriggerBlockLayer.EnableStatus triggerStatus = manager.getTriggerStatus(det, bufferCls);
          if (triggerStatus == TriggerBlockLayer.EnableStatus.DISABLED)
          {
             // triggers are disabled for this kind of event / table
             return true;
          }

          // look for generic trigger first and execute it, if found
          TriggerData genericTrigger = manager.getGenericTrigger(det, bufferCls, database, null);
          if (genericTrigger != null)
          {
             boolean sessOldBuffer = oldDmoValue != null && genericTrigger.hasOldBuffer();
             Buffer oldBuffer = sessOldBuffer
                   ? createWriteOldBuffer(genericTrigger.getTriggerClass(),
                                          bufferCls,
                                          oldDmoValue)
                   : null;

             guardedExec(() -> fireTrigger(det, genericTrigger, newVal, oldBuffer));
             // NOTE: do not return false if this trigger fails
             if (oldBuffer != null && ((BufferImpl) oldBuffer).buffer().isBufferScopeLeaking())
             {
                RecordBuffer.openScope((DataModelObject) oldBuffer);

                logCluteringProtection++;
                if (logCluteringProtection % logCluteringThreshold == 0 && LOG.isLoggable(Level.WARNING))
                {
                   String msg = "Old buffer lifecycle is not properly managed by the generic trigger: " +
                            genericTrigger.getTriggerClass() +
                            ". Consider opening the scope of the old buffer inside the generic " +
                            "trigger to properly manage its lifecycle.";
                   LOG.log(Level.WARNING, msg);

                   if (logCluteringProtection == logCluteringThreshold * 10)
                   {
                      logCluteringThreshold *= 10;
                      LOG.log(Level.WARNING, "To avoid log spam, 'generic trigger' logs will be slowed down!");
                   }
                }
             }
          }

          if (triggerStatus == TriggerBlockLayer.EnableStatus.ENABLED)
          {
             // look for the triggers for this event / bufferCls
             TriggerData sessionTrigger = manager.peekTrigger(det, bufferCls, null);
             TriggerData schemaTrigger = getSchemaTrigger(det, bufferCls, null);

             boolean overrideSchemaTriggers = sessionTrigger != null && sessionTrigger.isOverride();

             // except for the FIND case, the session trigger executes first (if it ever does)
             if (sessionTrigger != null && det != DatabaseEventType.FIND)
             {
                // each trigger invocation is done using a different instance of the OLD, r/o buffer
                boolean sessOldBuffer = oldDmoValue != null && sessionTrigger.hasOldBuffer();
                Buffer oldBuffer = sessOldBuffer
                                     ? createWriteOldBuffer(sessionTrigger.getTriggerClass(),
                                                            bufferCls,
                                                            oldDmoValue)
                                     : null;
                if (!guardedExec(() -> fireTrigger(det, sessionTrigger, newVal, oldBuffer)))
                {
                   return false;
                }
             }

             if (schemaTrigger != null && !overrideSchemaTriggers)
             {
                // each trigger invocation is done using a different instance of the OLD, r/o buffer
                boolean schOldBuffer = oldDmoValue != null && schemaTrigger.hasOldBuffer();
                Buffer oldBuffer = schOldBuffer
                                     ? createWriteOldBuffer(schemaTrigger.getTriggerClass(),
                                                            bufferCls,
                                                            oldDmoValue)
                                     : null;
                if (!guardedExec(() -> fireTrigger(det, schemaTrigger, newVal, oldBuffer)))
                {
                   return false;
                }
             }

             // in the FIND case, the session trigger executes second (if it ever does)
             if (sessionTrigger != null && det == DatabaseEventType.FIND)
             {
                // FIND triggers do not have an old buffer
                if (!guardedExec(() -> fireTrigger(det, sessionTrigger, newVal, null)))
                {
                   return false;
                }
             }
          }

          // replication triggers ignores the overrideable attribute, they always trigger
          TriggerData replicationTrigger = getReplicationSchemaTrigger(det, bufferCls);
          DatabaseEventType rDet = getReplicationEvent(det);
          if (replicationTrigger != null && rDet != null)
          {
             boolean replOldBuffer = oldDmoValue != null && replicationTrigger.hasOldBuffer();
             Buffer oldBuffer = replOldBuffer
                                  ? createWriteOldBuffer(replicationTrigger.getTriggerClass(),
                                                         bufferCls,
                                                         oldDmoValue)
                                  : null;

             return guardedExec(() -> fireTrigger(rDet, replicationTrigger, newVal, oldBuffer));
          }

          // everything is fine
          return true;
       }

       /**
        * Convert a base database event into the replication event. If they are already replication events, they
        * are returned. If they do not have a replication event associated, {@code null} is returned.
        *
        * @param   det
        *          The base trigger event.
        *
        * @return  The replication event of {@code det}. For {@code FIND} and {@code ASSIGN}, {@code null} is
        *          returned.
        */
       public static DatabaseEventType getReplicationEvent(DatabaseEventType det)
       {
          switch (det)
          {
             case CREATE:
             case REPLICATION_CREATE:
                return DatabaseEventType.REPLICATION_CREATE;
             case DELETE:
             case REPLICATION_DELETE:
                return DatabaseEventType.REPLICATION_DELETE;
             case WRITE:
             case REPLICATION_WRITE:
                return DatabaseEventType.REPLICATION_WRITE;
             default:
                return null;
          }
       }

       /**
        * Executes an ASSIGN trigger.
        *
        * @param   bufferCls
        *          The buffer class for the dmo that fired the trigger.
        * @param   newVal
        *          The buffer that contains the field that fired the trigger.
        * @param   oldValue
        *          When the ASSIGN trigger declares an 'old' value, this is passed to trigger
        *          procedure. The procedure code is allowed to modify this value without generating
        *          any error, however, they are lost as only a duplicate is passed on.
        * @param   ldb
        *          The database where the buffer reside.
        *
        * @return  {@code true} if no trigger was executed, or they executed with success. The method will return
        *          {@code false} if the trigger vetoed the operation (by returning error).
        */
       public static boolean triggerAssign(Class<? extends Buffer> bufferCls,
                                           Buffer newVal,
                                           String property,
                                           BaseDataType oldValue,
                                           String ldb)
       {
          DatabaseTriggerManager manager = get();
          if (manager.getTriggerStatus(DatabaseEventType.ASSIGN, bufferCls) !=
              TriggerBlockLayer.EnableStatus.ENABLED)
          {
             // triggers are disabled for this kind of event / buffer
             // there are no replication triggers for ASSIGN event
             return true;
          }

          // look for generic trigger first and execute it, if found
          TriggerData genericTrigger = manager.getGenericTrigger(DatabaseEventType.ASSIGN, bufferCls, ldb, property);
          if (genericTrigger != null)
          {
             fireAssignTrigger(genericTrigger, newVal, property, oldValue.duplicate());
             // NOTE: do not return false if this trigger fails
          }

          // look for the trigger
          TriggerData sessionTrigger = manager.peekTrigger(DatabaseEventType.ASSIGN, bufferCls, property);

          // execute session trigger second
          if (sessionTrigger != null)
          {
             boolean res = fireAssignTrigger(sessionTrigger, newVal, property, oldValue.duplicate());

             if (!res)
             {
                return false;
             }
             if (sessionTrigger.isOverride())
             {
                // if session trigger overrides then we return now without executing schema trigger
                return true;
             }
          }

          // the schema trigger executes last
          TriggerData schemaTrigger = getSchemaTrigger(DatabaseEventType.ASSIGN, bufferCls, property);
          if (schemaTrigger != null)
          {
             return fireAssignTrigger(schemaTrigger, newVal, property, oldValue.duplicate());
             // if trigger object failed an exception have already been printed to console
          }

          return true; // no trigger fired
       }

       /**
        * Provides a notification that a new scope is about to be entered. The manager only acts on
        * top-level scope. Each time a new scope is entered a fresh new layer of triggers is added
        * to internal stack.
        *
        * @param    block
        *           The explicit block definition which required this notification.
        */
       @Override
       public void scopeStart(BlockDefinition block)
       {
          // triggers get notifications at all top level blocks
          stack.addFirst(new TriggerBlockLayer()); // push();
       }

       /**
        * Provides a notification that an external scope is about to be deleted.
        */
       @Override
       public void scopeDeleted()
       {
          Object referent = pm.getProcessedProcedure();
          if (referent == null           ||
              pm._isPersistentAndNotPendingDelete(referent) ||
              (referent == pm._thisProcedure() && !tm.isExternalBlock()))
          {
             // if no referent or procedure is still persistent, do not clean up yet
             return;
          }

          programs.remove(referent);
          persistentTriggers.remove(referent);
       }

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

       /**
        * Provides a notification that a scope is about to be exited. The manager only acts on
        * top-level scope. Each time a scope is exited the last layer of triggers is removed from internal stack.
        */
       @Override
       public void scopeFinished()
       {
          TriggerBlockLayer tbl = null;

          // triggers get notifications at all top level blocks
          if (stack.isEmpty())
          {
             LOG.log(Level.WARNING, "Invalid scope finished.");
          }
          else
          {
             tbl = stack.removeFirst(); // pop();
          }

          if (tbl != null)
          {
             Object referent = pm._thisProcedure();
             if (pm._isPersistentAndNotPendingDelete(referent) && tm.isExternalBlock())
             {
                programs.push(referent);
                persistentTriggers.put(referent, tbl);
             }
          }
       }

       /**
        * Register a generic trigger for a specific event. When such trigger is registered, it will be fired for
        * all events of that type, of all tables from specified database before any other configured session or
        * schema trigger. The execution of the latter two is unaffected of the presence or execution of the
        * generic trigger.
        *
        * @param   eventName
        *          The event as case-insensitive string.
        * @param   database
        *          The database to apply to. In future revisions, this parameter might be replaced by a table
        *          pattern.
        * @param   triggerClassName
        *          The trigger class which implements the method for handling the respective event in a generic
        *          manner.
        *
        * @return  {@code null} if the operation is successful and a short description of the problem otherwise.
        */
       String registerGenericTrigger(String eventName, String database, String triggerClassName)
       {
          if (eventName == null || database == null || triggerClassName == null)
          {
             // never throw NullPointerException, maybe log the cause
             return "Null parameter (" +
                    (eventName == null ? "eventName" : (database == null ? "database" : "triggerClassName")) +
                    ").";
          }

          DatabaseEventType event;
          switch (eventName.toUpperCase())
          {
             case "CREATE": event = DatabaseEventType.CREATE; break;
             case "DELETE": event = DatabaseEventType.DELETE; break;
             case "FIND":   event = DatabaseEventType.FIND;   break;
             case "WRITE":  event = DatabaseEventType.WRITE;  break;
             default: return "Event name unrecognized (" + eventName + ").";
          }

          Class<? extends DatabaseTrigger<Buffer>> cls;
          try
          {
             Class<?> triggerClass = Class.forName(triggerClassName);

             if (!GenericDatabaseTrigger.class.isAssignableFrom(triggerClass))
             {
                return "Invalid class for '" + triggerClassName +
                       "'. It must extend 'com.goldencode.p2j.util.GenericDatabaseTrigger'.";
             }
             cls = (Class<? extends DatabaseTrigger<Buffer>>) triggerClass;
          }
          catch (ClassNotFoundException e)
          {
             return "Class '" + triggerClassName + "' is not found / accessible. Please add it to classpath.";
          }

          Map<String, TriggerData> genericMap;
          switch (event)
          {
             case CREATE:
                if (genericCreateTriggers == null)
                {
                   genericCreateTriggers = new HashMap<>();
                }
                genericMap = genericCreateTriggers;
                break;

             case DELETE:
                if (genericDeleteTriggers == null)
                {
                   genericDeleteTriggers = new HashMap<>();
                }
                genericMap = genericDeleteTriggers;
                break;

             case FIND:
                if (genericFindTriggers == null)
                {
                   genericFindTriggers = new HashMap<>();
                }
                genericMap = genericFindTriggers;
                break;

             case WRITE:
                if (genericWriteTriggers == null)
                {
                   genericWriteTriggers = new HashMap<>();
                }
                genericMap = genericWriteTriggers;
                break;

             default:
                return "Invalid event.";
          }

          genericMap.put(database, new TriggerData(cls, null, false, true, true, true));

          return null; // all OK
       }

       /**
        * Unregister a previously set generic trigger given the event type and database.
        * This is the equivalent of {@code ON <event> OF <table> REVERT} but for generic triggers.
        *
        * @param   eventName
        *          The event as case-insensitive string.
        * @param   database
        *          The database to apply to. In future revisions, this parameter might be replaced by a table
        *          pattern.
        *
        * @return  {@code null} if the operation is successful and a short description of the problem otherwise.
        */
       String deregisterGenericTrigger(String eventName, String database)
       {
          if (eventName == null || database == null)
          {
             // never throw NullPointerException, maybe log the cause
             return "Null parameter (" + (eventName == null ? "eventName" : "database") + ").";
          }

          DatabaseEventType event;
          switch (eventName.toUpperCase())
          {
             case "CREATE": event = DatabaseEventType.CREATE; break;
             case "DELETE": event = DatabaseEventType.DELETE; break;
             case "FIND":   event = DatabaseEventType.FIND;   break;
             case "WRITE":  event = DatabaseEventType.WRITE;  break;
             default: return "Event name unrecognized (" + eventName + ").";
          }

          removeGenericTrigger(event, database, null);
          return null;
       }

       /**
        * Register a generic trigger for a ASSIGN event. When such trigger is registered, it will be fired for all
        * ASSIGN events, of all tables from specified database BEFORE any other configured session or schema
        * triggers. The execution of the latter two is unaffected of the presence or execution of the generic
        * trigger.
        *
        * @param   eventName
        *          The event as case-insensitive string.
        * @param   database
        *          The database to apply to. In future revisions, this parameter might be replaced by a table
        *          pattern.
        * @param   triggerClassName
        *          The trigger class which implements the method for handling the respective event in a generic
        *          manner.
        * @param   property
        *          The <strong>property</strong> name of the affected field.
        *
        * @return  {@code null} if the operation is successful and a short description of the problem otherwise.
        */
       String registerGenericTrigger(String eventName,
                                     String database,
                                     String triggerClassName,
                                     String property)
       {
          if (eventName == null || database == null || triggerClassName == null)
          {
             // never throw NullPointerException, maybe log the cause
             return "Null parameter (" +
                    (eventName == null ? "eventName" : (database == null ? "database" : "triggerClassName")) +
                    ").";
          }

          if (!"ASSIGN".equalsIgnoreCase(eventName))
          {
             return "This variant of the method only handles ASSIGN triggers.";
          }

          Class<? extends DatabaseTrigger<Buffer>> cls;
          try
          {
             Class<?> triggerClass = Class.forName(triggerClassName);

             if (!GenericDatabaseTrigger.class.isAssignableFrom(triggerClass))
             {
                return "Invalid class for '" + triggerClassName +
                       "'. It must extend 'com.goldencode.p2j.util.GenericDatabaseTrigger'.";
             }
             cls = (Class<? extends DatabaseTrigger<Buffer>>) triggerClass;
          }
          catch (ClassNotFoundException e)
          {
             return "Class '" + triggerClassName + "' is not found / accessible. Please add it to classpath.";
          }

          if (genericAssignTriggers == null)
          {
             genericAssignTriggers = new HashMap<>();
          }
          Map<String, TriggerData> propMap = genericAssignTriggers.computeIfAbsent(database, k -> new HashMap<>());

          propMap.put(property, new TriggerData(cls, null, false, true, true, true));
          return null;
       }

       /**
        * Unregister a session database trigger for the given event type and buffer class.
        * This is the equivalent of {@code ON Assign OF <table> REVERT} but for generic triggers.
        *
        * @param   database
        *          The database to apply to. In future revisions, this parameter might be replaced by a table
        *          pattern.
        * @param   property
        *          The property that is deregistered from {@code ASSIGN} trigger.
        *
        * @return  {@code null} if the operation is successful and a short description of the problem otherwise.
        */
       String deregisterGenericAssignTrigger(String database, String property)
       {
          removeGenericTrigger(DatabaseEventType.ASSIGN, database, property);
          return null;
       }

       /**
        * Tests if a schema trigger is overrideable. In case the trigger is declared as overrideable,
        * {@code true} is returned. Otherwise, 3223 code is {@code recordOrThrowError()}.
        * If no error is acting {@code false} will be returned.
        *
        * @param   event
        *          The database event of the trigger to be tested.
        * @param   bufferCls
        *          The class buf for the table the trigger is defined.
        *
        * @return  {@code true} if respective trigger can be overridden by a session trigger.
        */
       private static boolean isSchemaTriggerOverrideable(DatabaseEventType event,
                                                          Class<? extends Buffer> bufferCls)
       {
          // check if this event is overrideable
          TriggerData schemaTrigger = getSchemaTrigger(event, bufferCls, null);
          if (schemaTrigger != null && !schemaTrigger.isOverride())
          {
             // detect table name
             String tableName = "?";
             for (Class<?> next : bufferCls.getInterfaces())
             {
                if (DataModelObject.class.isAssignableFrom(next))
                {
                   tableName = next.getSimpleName();
                   break;
                }
             }

             String msg = String.format("Attempt to OVERRIDE a schema %s trigger for %s", event, tableName);
             // this is a non overrideable schema trigger:
             ErrorManager.recordOrThrowError(3223, msg);
             return false;
          }
          return true;
       }

       /**
        * Create a trigger object instance based on information from a {@code TriggerData} container.
        *
        * @param   triggerData
        *          The container that holds information about the trigger to be built.
        *
        * @return  An instance of the trigger as configured in {@code triggerData}.
        */
       @SuppressWarnings("unchecked")
       private static DatabaseTrigger<Buffer> instantiateTrigger(TriggerData triggerData) {
          try
          {
             if (triggerData.getTriggerContainer() == null)
             {
                // if the container is null then this is (probably) a schema trigger
                // being a top-level public class, it does not need a container for constructing
                return triggerData.getTriggerClass().getDeclaredConstructor().newInstance();
             }

             Class<?>[] ctorParmlist  = new Class[] { triggerData.getTriggerContainer().getClass() };
             Constructor<? extends DatabaseTrigger> ctor =
                   triggerData.getTriggerClass().getConstructor(ctorParmlist);
             return ctor.newInstance(triggerData.getTriggerContainer());
          }
          catch (InstantiationException | IllegalAccessException |
                NoSuchMethodException | InvocationTargetException e)
          {
             LOG.severe("", e);
             // could not instantiate the trigger class
             return null;
          }
       }

       /**
        * Extract only the DMO schema-specific interface (i.e., exclude the Buffer interface)
        *
        * @param   def
        *          The source-definition class for the trigger.
        * @param   bufferCls
        *          Interface which defines DMO's public API plus all {@link Buffer} methods.
        * @param   oldDmoValue
        *          The read-only value.
        *
        * @return  A temporary read-only buffer that can be used as the OLD 'parameter' in a WRITE trigger.
        */
       private static Buffer createWriteOldBuffer(Class<?> def,
                                                  Class<? extends Buffer> bufferCls,
                                                  Record oldDmoValue)
       {
          Class<? extends DataModelObject> dmoIface = null;
          Class<?>[] superIfaces = bufferCls.getInterfaces();
          for (Class<?> next : superIfaces)
          {
             if (DataModelObject.class.isAssignableFrom(next))
             {
                dmoIface = (Class<? extends DataModelObject>) next;
                break;
             }
          }

          if (dmoIface == null)
          {
             LOG.log(Level.WARNING, "Could not create old buffer for WRITE trigger.");
             return null;
          }

          String schema = DmoMetadataManager.getDmoInfo(dmoIface).getSchema();
          return RecordBuffer.defineReadOnly(def, bufferCls, schema, oldDmoValue, true);
       }

       /**
        * Instantiates an object of the ASSIGN trigger and fires it.
        *
        * @param   triggerData
        *          Information about how the trigger will be built.
        * @param   newRecord
        *          The buffer that contains the field that fired the trigger.
        * @param   property
        *          Optionally, for ASSIGN triggers the name of the property (field) for which the
        *          trigger was declared. Otherwise, it is ignored.
        * @param   oldValue
        *          The old value of the field. May be null if the trigger did not declare it.
        *
        * @return  {@code true} if no trigger was executed, or they executed with success. The method will return
        *          {@code false} if the trigger vetoed the operation (by returning error).
        */
       private static boolean fireAssignTrigger(TriggerData triggerData,
                                                Buffer newRecord,
                                                String property,
                                                BaseDataType oldValue)
       {
          DatabaseTrigger<Buffer> trigger = instantiateTrigger(triggerData);
          if (trigger == null)
          {
             return true;
          }

          boolean ret = true;
          DatabaseTriggerManager dtm = get();
          try
          {
             Class<?> def = triggerData.getTriggerClass();
             Object target = triggerData.getTriggerContainer();
             String iename = "";
             String pname = dtm.pm.getRelativeName(target);

             dtm.pm.pushCalleeInfo(def, false, target, target, iename, pname, false, false);

             // the trigger is not related to any buffer
             // this should happen only for assign triggers which are defined with NEW VALUE
             if (!triggerData.hasNewBuffer())
             {
                if (property == null)
                {
                   // this should not happen; the TriggerData should always know the property
                   if (LOG.isLoggable(Level.SEVERE))
                   {
                      LOG.log(Level.SEVERE,
                              String.format(
                                    "The property of the assign trigger couldn't be identified for %s.",
                                    triggerData.getTriggerClass().toString()));
                   }
                   return false;
                }
                FieldReference fieldRef = new FieldReference((DataModelObject)newRecord, property);
                Class<? extends BaseDataType> type = (Class<? extends BaseDataType>) fieldRef.getType();
                Undoable field = OutputParameter.wrap((DataModelObject) newRecord, property, type, true);

                ErrorManager.ErrorHelper eh = ErrorManager.getErrorHelper();
                boolean wm = eh.isWarningMode();
                if (!wm)
                {
                   // this is mostly for APPEND mode, the other ones will not encounter issues here
                   eh.setWarningMode(true);
                }

                if (!guardedExec(() -> {
                   if (triggerData.isGeneric())
                   {
                      GenericDatabaseTrigger gdt = (GenericDatabaseTrigger) trigger;
                      Property prop = ((BufferImpl) newRecord).buffer().getDmoInfo().getFieldInfo(property);
                      gdt.assign(newRecord, oldValue, new character(prop.legacy));
                   }
                   else if (triggerData.hasOldBuffer())
                   {
                      trigger.assign(field, oldValue);
                   }
                   else
                   {
                      trigger.assign(field);
                   }
                }))
                {
                   return false;
                }
             }
             else
             {
                if (!guardedExec(() -> {
                   if (triggerData.isGeneric())
                   {
                      GenericDatabaseTrigger gdt = (GenericDatabaseTrigger) trigger;
                      Property prop = ((BufferImpl) newRecord).buffer().getDmoInfo().getFieldInfo(property);
                      gdt.assign(newRecord, oldValue, new character(prop.legacy));
                   }
                   else if (triggerData.hasOldBuffer())
                   {
                      trigger.assign(newRecord, oldValue);
                   }
                   else
                   {
                      trigger.assign(newRecord);
                   }
                })) {
                   return false;
                }
             }
          }
          finally
          {
             dtm.pm.popCalleeInfo();
          }

          // if trigger object failed an exception have already been printed to console
          return ret;
       }

       /**
        * Instantiates a trigger object associated with the specified database event type and fires it.
        *
        * @param   det
        *          The database event type to be fired.
        * @param   triggerData
        *          Information about how the trigger will be built.
        * @param   buffer
        *          The buffer that fired the trigger.
        * @param   oldValue
        *          The old value of the buffer. May be {@code null} if the trigger did not declare it.
        */
       private static void fireTrigger(DatabaseEventType det,
                                       TriggerData triggerData,
                                       Buffer buffer,
                                       Buffer oldValue)
       {
          final DatabaseTrigger<Buffer> trigger = instantiateTrigger(triggerData);
          if (trigger == null)
          {
             // if trigger object failed an exception have already been printed to console
             return;
          }

          DatabaseTriggerManager dtm = get();
          try
          {
             TransactionManager.pauseOffEndRegistration();
             Class<?> def = triggerData.getTriggerClass();
             Object target = triggerData.getTriggerContainer();
             String iename = "";
             String pname = dtm.pm.getRelativeName(target);

             dtm.pm.pushCalleeInfo(def, false, target, target, iename, pname, false, false);

             switch (det)
             {
                case ASSIGN:
                   throw new IllegalArgumentException("Not executing ASSIGN with this method.");
                   // break;
                case CREATE:
                   trigger.create(buffer);
                   break;
                case DELETE:
                   trigger.delete(buffer);
                   break;
                case FIND:
                   trigger.find(buffer);
                   break;
                case WRITE:
                   if (triggerData.isGeneric() || triggerData.hasOldBuffer() /* && oldValue != null ? */)
                   {
                      trigger.write(buffer, oldValue);
                   }
                   else
                   {
                      trigger.write(buffer);
                   }
                   break;
                case REPLICATION_CREATE:
                   trigger.replicationCreate(buffer);
                   break;
                case REPLICATION_DELETE:
                   trigger.replicationDelete(buffer);
                   break;
                case REPLICATION_WRITE:
                   if (triggerData.hasOldBuffer() /* && oldValue != null ? */)
                   {
                      trigger.replicationWrite(buffer, oldValue);
                   }
                   else
                   {
                      trigger.replicationWrite(buffer);
                   }
                   break;
             }
          }
          finally
          {
             dtm.pm.popCalleeInfo();
             TransactionManager.resumeOffEndRegistration();
          }
       }

       /**
        * Handle trigger- related errors. The error is displayed in status bar then the execution of
        * the procedure is abruptly interrupted because all trigger errors are fatal errors.
        *
        * @param   errId
        *          The id of the error.
        * @param   det
        *          The database event that caused the error.
        * @param   tableOrBufferName
        *          The name of the table (or buffer in some cases) that caused the error
        * @param   fieldName
        *          In the case of {@code ASSIGN} trigger the field that caused the error.
        */
       public static void handleError(int errId,
                                      DatabaseEventType det,
                                      String tableOrBufferName,
                                      String fieldName)
       {
          String msg = null;
          boolean displayOnly = false;

          switch (errId)
          {
             case 3168:
                msg = "Attempt to CREATE a " + tableOrBufferName + "record in " + det + "-TRIGGER " +
                      ProcedureManager.lastProcedure() + " while a trigger is executing";
                break;

             case 3169:
                msg = "Attempt to DELETE a " + tableOrBufferName + "record in " + det + "-TRIGGER " +
                      ProcedureManager.lastProcedure() + " while a trigger is executing";
                break;

             case 2873:
                assert (det == DatabaseEventType.FIND);
                msg = "Cannot change cursor of buffer " + tableOrBufferName +
                      "when executing a trigger";
                displayOnly = true;
                break;

             case 2872:
                assert (det == DatabaseEventType.WRITE);
                msg = "Attempt to FIND old trigger buffer during WRITE trigger for " +
                      tableOrBufferName;
                break;

             case 1003:
                assert (det == DatabaseEventType.ASSIGN);
                msg = "Cycle in procedure calls detected in 'ASSIGN-TRIGGER " +
                      ProcedureManager.lastProcedure() + "' while executing ASSIGN trigger for " +
                      tableOrBufferName + "." + fieldName;
                break;

             case 2665:
                // this is handled in RecordBuffer
                msg = "Attempt to update a Read-Only buffer for table " + tableOrBufferName;
                break;

             case 3129:
                // TODO: I found this error when (assign) trigger is fired after buffer RELEASE (if this is possible ?)
                assert (det == DatabaseEventType.ASSIGN);
                int[] errCodes = {91, 3129};
                String[] errMsgs = {"** No " + tableOrBufferName + " record is available",
                                    "Could not extract value from " + tableOrBufferName +
                                          " (rpos 5) for an ASSIGN trigger" };
                ErrorManager.recordOrThrowError(errCodes, errMsgs, false, true);
                return;
          }

          if (displayOnly)
          {
             ErrorManager.displayError(errId, msg);
          }
          else
          {
             ErrorManager.showErrorAndAbend(errId, msg);
          }
       }

       /**
        * Indicate whether any trigger (session or schema) of the specified type is registered for the
        * given buffer class at the current block level.
        *
        * @param   det
        *          Trigger type.
        * @param   bufferCls
        *          Buffer class.
        * @param   database
        *          The database where the buffer reside. If {@code null} try to autodetect using the buffer DMO.
        *
        * @return  {@code true} if a registered trigger is found, else {@code false}.
        */
       boolean isTriggerEnabled(DatabaseEventType det, Class<? extends Buffer> bufferCls, String database)
       {
          TriggerData genericTrigger = getGenericTrigger(det, bufferCls, database, null);
          if (genericTrigger != null)
          {
             return true;
          }

          TriggerBlockLayer.EnableStatus triggerStatus = getTriggerStatus(det, bufferCls);
          if (triggerStatus == TriggerBlockLayer.EnableStatus.ENABLED)
          {
             if (peekTrigger(det, bufferCls, null) != null)
             {
                return true;
             }

             // search for the DMO interface of the bufferCls
             Class<? extends DataModelObject> dmoIface = null;
             Class<?>[] superIfaces = bufferCls.getInterfaces();
             int len = superIfaces.length;
             for (int i = 0; i < len; i++)
             {
                Class<?> next = superIfaces[i];
                if (DataModelObject.class.isAssignableFrom(next))
                {
                   dmoIface = (Class<? extends DataModelObject>) next;
                   break;
                }
             }

             if (dmoIface == null)
             {
                // no suitable DMO interface was found for the given bufferCls
                return false;
             }

             // identify the trigger procedure meant to be executed
             String procedure = TableMapper.getLegacyTriggerProcedure(dmoIface, det.toString(), null);
             if (procedure == null)
             {
                // no procedure for the det trigger at the DMO's LegacyTrigger annotation
                return false;
             }

             return true;
          }

          return false;
       }

       /**
        * Indicate whether assign triggers are enabled for the given buffer class at this block level.
        *
        * @param   bufferCls
        *          Buffer class.
        *
        * @return  See above.
        */
       boolean areAssignTriggersEnabled(Class<? extends Buffer> bufferCls)
       {
          return getTriggerStatus(DatabaseEventType.ASSIGN, bufferCls) == TriggerBlockLayer.EnableStatus.ENABLED;
       }

       /**
        * Indicate whether an assign trigger (session or schema) is registered for the given buffer
        * class and DMO property at the current block level.
        *
        * @param   bufferCls
        *          Buffer class.
        * @param   property
        *          DMO property name.
        * @param   database
        *          The database where the buffer reside. If {@code null} try to autodetect using the buffer DMO.
        *
        * @return  {@code true} if a registered trigger is found, else {@code false}.
        */
       boolean isAssignTrigger(Class<? extends Buffer> bufferCls, String property, String database)
       {
          return getGenericTrigger(DatabaseEventType.ASSIGN, bufferCls, database, property) != null ||
                 peekTrigger(DatabaseEventType.ASSIGN, bufferCls, property) != null                 ||
                 getSchemaTrigger(DatabaseEventType.ASSIGN, bufferCls, property) != null;
       }

       /**
        * Get the set of triggers that are currently executed for a specified record.
        *
        * @param   bufferClass
        *          The dmo buffer interface.
        * @param   rowId
        *          The id of the record.
        * @param   create
        *          If the specified stack does not exist, make sure it is created.
        *
        * @return  The stack of execution triggers. May be {@code null} if stack does not exist and
        *          creation of a new one was not requested.
        */
       Deque<DatabaseEventType> getExecutingTriggerStack(Class<? extends Buffer> bufferClass,
                                                         Long rowId,
                                                         boolean create)
       {
          HashMap<Long, Deque<DatabaseEventType>> dmoClassMap = executing.get(bufferClass);
          if (dmoClassMap == null)
          {
             if (create)
             {
                // the map for this kind of dmo has not been found, we need to create it now
                dmoClassMap = new HashMap<>();
                executing.put(bufferClass, dmoClassMap);
             }
             else
             {
                // no executing triggers for this kind of DMO and no requested to create one
                return null;
             }
          }

          Deque<DatabaseEventType> stack = dmoClassMap.get(rowId);
          if (stack == null && create)
          {
             // if there is no stack of executing triggers for this id, create it now
             stack = new LinkedList<>();
             dmoClassMap.put(rowId, stack);
          }

          return stack;
       }

       /**
        * Cleanup. Removes a trigger stack for a specified record. The stack must exist and be empty,
        * otherwise there the pop/push are not balanced.
        *
        * @param   bufferClass
        *          The dmo buffer interface.
        * @param   rowId
        *          The id of the record.
        */
       void removeExecutingTriggerStack(Class<? extends Buffer> bufferClass, Long rowId)
       {
          HashMap<Long, Deque<DatabaseEventType>> dmoClassMap = executing.get(bufferClass);
          if (dmoClassMap == null)
          {
             // unexpected, might indicate a programming error (unbalanced pop/push)
             return;
          }

          Deque<DatabaseEventType> toRemove = dmoClassMap.remove(rowId);

          assert toRemove != null && toRemove.isEmpty();
       }

       /**
        * Looks into the stack of layers and check if a trigger is enabled for a database event.
        * If the trigger was not disabled at all, {@code EnableStatus.ENABLED} is returned. If
        * the trigger was disabled, the {@code EnableStatus.DISABLED} is returned unless the
        * events has a replication pair, and it was not disable using {@code ALLOW-REPLICATION},
        * when {@code EnableStatus.REPLICATION_ONLY} is returned.
        * <p>
        * FIND/ASSIGN/CREATE/WRITE events won't return ENABLED here, they will return either DISABLED
        * or UNKNOWN. Thus, we check the entire stack until DISABLED is found and if DISABLED is never
        * returned, then we are implicitly ENABLED.
        *
        * @param   det
        *          The database (base) event.
        * @param   bufferCls
        *          The buffer.
        *
        * @return  the status of the trigger on the buffer.
        */
       private TriggerBlockLayer.EnableStatus getTriggerStatus(DatabaseEventType det,
                                                               Class<? extends Buffer> bufferCls)
       {
          for (TriggerBlockLayer tbl : stack)
          {
             TriggerBlockLayer.EnableStatus status = tbl.getTriggerStatus(det, bufferCls);

             if (status != TriggerBlockLayer.EnableStatus.UNKNOWN)
             {
                return status;
             }
          }

          // if no disable-triggers were found in the whole stack then the trigger is enabled
          return TriggerBlockLayer.EnableStatus.ENABLED;
       }

       /**
        * Implementation of the trigger search algorithm in the stack of triggers. Each layer of the
        * stack is inspected for {@code det} type of trigger associated with {@code buff}.
        * If found it is returned, otherwise the search continue with the next level from the stack.
        * If no such trigger can be found, {@code null} is returned.
        * This method takes into account if respective trigger event was disabled and, in this case,
        * a special singleton object is returned.
        *
        * @param   det
        *          The database trigger type.
        * @param   bufferCls
        *          The buffer that raised the trigger event.
        * @param   property
        *          In the case of ASSIGN trigger, the name of the property that fired the trigger
        *
        * @return  The top-level instance of the trigger data of the {@code det} type associated
        *          with {@code buff}.
        */
       private TriggerData peekTrigger(DatabaseEventType det, Class<? extends Buffer> bufferCls, String property)
       {
          for (TriggerBlockLayer blockLayer : stack)
          // the proper order is guaranteed by deque.iterator() method
          {
             TriggerData triggerData = blockLayer.getTrigger(det, bufferCls, property);
             if (triggerData == null)
             {
                // no trigger found at this level, search deeper
                continue;
             }

             // if disabledTrigger was returned either a trigger was found or not, it does not
             // matter as long as nothing will be executed (disabledTrigger methods are empty and
             // also isOverride() return true so schema trigger is also disabled).
             // if a normal trigger is found it will be executed along with schema trigger
             return triggerData;
          }

          // if not found, look into the global session triggers, from the top of stack down
          Iterator<Object> iter = programs.iterator();
          while (iter.hasNext())
          {
             Object referent = iter.next();
             TriggerBlockLayer blockLayer = persistentTriggers.get(referent);
             if (blockLayer != null)
             {
                TriggerData triggerData = blockLayer.getTrigger(det, bufferCls, property);
                if (triggerData != null)
                {
                   return triggerData;
                }
             }
          }

          // search failed, no trigger of type det was defined for buff
          return null;
       }

       /**
        * Removes a generic trigger from the associated map.
        *
        * @param   det
        *          The event type of the trigger to be reverted
        * @param   database
        *          The database to apply to. In future revisions, this parameter might be replaced by a table
        *          pattern.
        * @param   property
        *          The property that is unregistered from ASSIGN trigger, otherwise ignored.
        */
       private void removeGenericTrigger(DatabaseEventType det, String database, String property)
       {
          switch (det)
          {
             case ASSIGN:
                if (genericAssignTriggers == null)
                {
                   return;
                }

                Map<String, TriggerData> triggersMap = genericAssignTriggers.get(database);
                if (triggersMap != null)
                {
                   triggersMap.remove(property);
                   // if (triggersMap.isEmpty()) genericAssignTriggers.remove(database);
                   // if (genericAssignTriggers.isEmpty()) genericAssignTriggers = null;
                }
                break;

             case CREATE:
                if (genericCreateTriggers != null)
                {
                   genericCreateTriggers.remove(database);
                }
                break;

             case DELETE:
                if (genericDeleteTriggers != null)
                {
                   genericDeleteTriggers.remove(database);
                }
                break;

             case FIND:
                if (genericFindTriggers != null)
                {
                   genericFindTriggers.remove(database);
                }
                break;

             case WRITE:
                if (genericWriteTriggers != null)
                {
                   genericWriteTriggers.remove(database);
                }
                break;
          }
       }

       /**
        * Look for a specific generic trigger and return it if one was registered.
        *
        * @param   det
        *          The trigger type to be executed.
        * @param   bufferCls
        *          The buffer class for the dmo that fired the trigger. Currently, unused. It will be used when
        *          the buffer pattern will be added to generic trigger registration.
        * @param   database
        *          The database to apply to. In future revisions, this parameter might be replaced by a table
        *          pattern.
        * @param   property
        *          The modified property, in case of ASSIGN triggers.
        *
        * @return  the trigger data necessary for firing the specified trigger, or {@code null} if none was found.
        */
       private TriggerData getGenericTrigger(DatabaseEventType det,
                                             Class<? extends Buffer> bufferCls,
                                             String database,
                                             String property)
       {
          if (det == DatabaseEventType.ASSIGN)
          {
             if (genericAssignTriggers != null)
             {
                Map<String, TriggerData> fieldMap = genericAssignTriggers.get(database);
                if (fieldMap != null)
                {
                   return fieldMap.get(property);
                }
             }
             return null;
          }

          // for the other triggers:
          Map<String, TriggerData> genericMap = null;
          switch (det)
          {
             case CREATE: genericMap = genericCreateTriggers; break;
             case DELETE: genericMap = genericDeleteTriggers; break;
             case FIND:   genericMap = genericFindTriggers;   break;
             case WRITE:  genericMap = genericWriteTriggers;  break;
          }
          if (genericMap != null)
          {
             return genericMap.get(database);
          }

          return null;
       }

       /**
        * Execute a piece of code, checking whether it raised an ERROR. In the event this happens, {@code false}
        * is returned if already in silent mode, otherwise the exception will propagate to the caller.
        *
        * @param   code
        *          The code to be executed.
        *
        * @return  {@code true} if the code executed normally, and {@code false} if it raised an error while in
        *          silent mode.
        *
        * @throws  ErrorConditionException
        *          if an error condition was raised while not in silent mode
        */
       private static boolean guardedExec(Runnable code)
       {
          // if not in silent mode, just execute the code. If an error condition is raised it will propagate
          // back to caller and the return value will be ignored.
          if (!ErrorManager.isSilent())
          {
             code.run();
             return true;
          }

          if (ErrorManager.isPendingError())
          {
             return false; // nothing to do, already in error mode
          }

          // at this moment we are in silent mode, with no pending error. We can run the code knowingly that there
          // will be no exceptions thrown because of error conditions but, if one happens, the 'pending' error
          // flag was set
          code.run();
          return !ErrorManager.isPendingError();
       }
    }