# TriggerBlockLayer.java

    /*
    ** Module   : TriggerBlockLayer.java
    ** Abstract : A layer of triggers and their configuration corresponding to a block level.
    **
    ** Copyright (c) 2013-2023, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ---------------------------------------Description----------------------------------------
    ** 001 OM  20131107 Initial commit.
    ** 002 OM  20140711 Added missing header and javadoc.
    ** 003 GES 20161006 Updated javadoc to clarify the contract for a method.
    ** 004 CA  20201003 Use an identity HashSet where possible.
    ** 005 CA  20220201 Performance improvement - lazily create collections, use identity maps where possible.
    ** 006 OM  20230915 Added support for generic session triggers.
    ** 025 OM  20231125 Redesign generic session triggers.
    */

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

    /**
     * This class holds all triggers defined ar a certain level in the stack of triggers along with
     * information whether some groups of triggers are disabled. The class also provides methods
     * for installing and removing triggers into/from this layer and for obtaining the active
     * {@link DatabaseTrigger} for current layer.
     * <p>
     * This class and all his methods are package-protected, so it is not visible outside the trigger
     * package; in fact it can only be accessed by {@link DatabaseTriggerManager}.
     */
    class TriggerBlockLayer
    {
       /** The status of a base database trigger event on a block layer. */
       enum EnableStatus
       {
          /** unknown state, no disable triggers was specified on this block */
          UNKNOWN,

          /** the trigger and associated replication are enabled */
          ENABLED,

          /** the trigger is disabled, but the replication is enabled */
          REPLICATION_ONLY,

          /** both trigger and replication are disabled */
          DISABLED
       }

       /** A FIND <code>TriggerData</code> can be defined for each table on this layer.*/
       private Map<Class<? extends Buffer>, TriggerData> findTriggers = null;

       /** A CREATE <code>TriggerData</code> can be defined for each table on this layer.*/
       private Map<Class<? extends Buffer>, TriggerData> createTriggers = null;

       /** A WRITE <code>TriggerData</code> can be defined for each table on this layer.*/
       private Map<Class<? extends Buffer>, TriggerData> writeTriggers = null;

       /**
        * A DELETE <code>TriggerData</code> can be defined for each table on this layer.
        */
       private Map<Class<? extends Buffer>, TriggerData> deleteTriggers = null;

       /**
        * For each field (property) of each table an ASSIGN <code>TriggerData</code> can be on this
        * layer. The mapping order is rather uncommon but this help re-using the code from lookup
        * of other triggers by applying first the mapping from field name.
        */
       private Map<String, Map<Class<? extends Buffer>, TriggerData>> assignTriggers = null;

       /**
        * A REPLICATION-CREATE <code>TriggerData</code> can be defined for each table on this layer.
        */
       private Map<Class<? extends Buffer>, TriggerData> repCreateTriggers = null;

       /**
        * A REPLICATION-WRITE <code>TriggerData</code> can be defined for each table on this layer.
        */
       private Map<Class<? extends Buffer>, TriggerData> repWriteTriggers = null;

       /**
        * A REPLICATION-DELETE <code>TriggerData</code> can be defined for each table on this layer.
        */
       private Map<Class<? extends Buffer>, TriggerData> repDeleteTriggers = null;

       /** This is the set of tables that disabled their DUMP triggers (FIND). */
       private Set<Class<? extends Buffer>> disabledDumpTriggers = null;

       /** This is the set of tables that disabled their LOAD triggers (all except FIND). */
       private Set<Class<? extends Buffer>> disabledLoadTriggers = null;

       /**
        * This is the set of tables that disabled their REPLICATION triggers. (REPLICATION-CREATE,
        * REPLICATION-WRITE, REPLICATION-DELETE and DELETE!).
        */
       private Set<Class<? extends Buffer>> enabledReplicationTriggers = null;

       /**
        * Constructor with minimum implementation, all members will use lazy initialization for
        * two reasons:
        * <ul>
        *    <li>to not allocate unneeded memory
        *    <li>quick creation of the object.
        * </ul>
        */
       TriggerBlockLayer()
       {
       }

       /**
        * Obtain the container that hold information for instantiating a trigger object.
        *
        * @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 ASSIGN triggers the name of the <strong>property</strong> (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 of null if such session trigger has not been declared
        *          within the current layer.
        */
       TriggerData getTrigger(DatabaseEventType det, Class<? extends Buffer> bufferCls, String property)
       {
          Map<Class<? extends Buffer>, TriggerData> triggerMap = getBufferTriggerMap(det, property, false);
          if (triggerMap == null)
          {
             // no trigger of this kind was declared at this block level
             return null;
          }

          // if a trigger was set for this buffer return it, otherwise return null
          return triggerMap.get(bufferCls);
       }

       /**
        * Obtain the container that hold information for instantiating a trigger object for the
        * replication version of the <code>det</code> event. This call is only valid for schema-level
        * <code>TriggerBlockLayer</code> and only 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 trigger.
        */
       TriggerData getReplicationTrigger(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 getTrigger(det, bufferCls, null);
       }

       /**
        * Sets the current trigger for a {@code det} and a {@code buffer}. If another trigger was set for the same
        * event and buffer, it will be dropped.
        * Because this class uses lazy initialization the map of triggers is automatically created if there wasn't
        * already there for this trigger event.
        *
        * @param   triggerCls
        *          The trigger to be set.
        * @param   det
        *          The trigger event.
        * @param   bufferCls
        *          The buffer for which the trigger is set.
        * @param   assignProperty
        *          When setting an ASSIGN trigger the name of the field is needed, otherwise ignored.
        * @param   container
        *          The container object of the trigger.
        * @param   override
        *          If {@code true}, the schema trigger is blocked, otherwise, schema trigger will be executed just
        *          after the session trigger.
        * @param   hasOldBuffer
        *          {@code true} if the callback of the trigger has reference to old buffer.
        */
       void setTrigger(Class<? extends DatabaseTrigger<? extends Buffer>> triggerCls,
                       DatabaseEventType det,
                       Class<? extends Buffer> bufferCls,
                       String assignProperty,
                       Object container,
                       boolean override,
                       boolean hasOldBuffer)
       {
          // get the trigger map, creating it if needed
          Map<Class<? extends Buffer>, TriggerData> triggerMap = getBufferTriggerMap(det, assignProperty, true);
          assert (triggerMap != null);

          TriggerData triggerData = new TriggerData(triggerCls, container, override, hasOldBuffer, true, false);

          // drop the old trigger, if any
          triggerMap.put(bufferCls, triggerData);
       }

       /**
        * Removes a trigger from the associated map.
        *
        * @param   det
        *          The event type of the trigger to be reverted
        * @param   bufferCls
        *          The buffer for which the trigger is removed.
        * @param   property
        *          The property that is unregistered from ASSIGN trigger, otherwise ignored.
        */
       void removeTrigger(DatabaseEventType det, Class<? extends Buffer> bufferCls, String property)
       {
          // get the trigger map, possible null
          Map<Class<? extends Buffer>, TriggerData> triggerMap = getBufferTriggerMap(det, property, false);

          if (triggerMap != null)
          {
             // drop the old trigger
             triggerMap.remove(bufferCls);
          }

          // TODO: if the triggerMap is null or it does not contain a trigger for specified buffer
          //       then there might be an incorrect pairing of ON event / ON event REVERT in user code.
          //       should display some kind of message in this case ?
       }

       /**
        * Disable a set of trigger events. The disabled set of triggers will remain disabled until
        * the end of the scope of the current layer (the end of the current subroutine block).
        * Individual sets of triggers are disabled. For the set of trigger that are not explicitly
        * disabled the old status remain unchanged.
        *
        * @param   dump
        *          Disable the FIND triggers.
        * @param   load
        *          Disable the LOAD (ASSIGN, CREATE, WRITE) triggers.
        * @param   allowReplication
        *          Disable replication triggers.
        */
       void disableTriggers(Class<? extends Buffer> bufferCls,
                            boolean dump,
                            boolean load,
                            boolean allowReplication)
       {
          if (dump)
          {
             if (disabledDumpTriggers == null)
             {
                disabledDumpTriggers = Collections.newSetFromMap(new IdentityHashMap<>());
             }
             disabledDumpTriggers.add(bufferCls);
          }
          if (load)
          {
             if (disabledLoadTriggers == null)
             {
                disabledLoadTriggers = Collections.newSetFromMap(new IdentityHashMap<>());
             }
             disabledLoadTriggers.add(bufferCls);
          }
          if (allowReplication)
          {
             if (enabledReplicationTriggers == null)
             {
                enabledReplicationTriggers = Collections.newSetFromMap(new IdentityHashMap<>());
             }
             enabledReplicationTriggers.add(bufferCls);
          }
       }

       /**
        * Lookup the buffer-to-trigger map using the DatabaseEventType. If the last parameter is
        * true and respective map is null, then it is created on-the-fly, otherwise the current
        * value is returned.
        *
        * @param   det
        *          The event type that is looked up.
        * @param   assignField
        *          In the case of ASSIGN triggers, the property name, otherwise ignored.
        * @param   createIfNotExist
        *          Flag to create the specified map if the map has not already been created.
        *
        * @return  the map that holds <code>det</code> buffer-to-events, possible <code>null</code>
        *          or a new empty map if this was requested.
        *
        */
       private Map<Class<? extends Buffer>, TriggerData> getBufferTriggerMap(
             DatabaseEventType det, String assignField, boolean createIfNotExist)
       {
          switch (det)
          {
             case ASSIGN:
                if (assignTriggers == null)
                {
                   if (createIfNotExist)
                   {
                      assignTriggers = new HashMap<>();
                   }
                   else
                   {
                      // no ASSIGN trigger defined for this table
                      return null;
                   }
                }
                assert (assignTriggers != null); // evidently

                Map<Class<? extends Buffer>, TriggerData> triggersMap = assignTriggers.get(assignField);
                if (triggersMap == null && createIfNotExist)
                {
                   triggersMap = new IdentityHashMap<>();
                   assignTriggers.put(assignField, triggersMap);
                }
                return triggersMap;

             case CREATE:
                if (createTriggers == null && createIfNotExist)
                {
                   createTriggers = new IdentityHashMap<>();
                }
                return createTriggers;

             case DELETE:
                if (deleteTriggers == null && createIfNotExist)
                {
                   deleteTriggers = new IdentityHashMap<>();
                }
                return deleteTriggers;

             case FIND:
                if (findTriggers == null && createIfNotExist)
                {
                   findTriggers = new IdentityHashMap<>();
                }
                return findTriggers;

             case WRITE:
                if (writeTriggers == null && createIfNotExist)
                {
                   writeTriggers = new IdentityHashMap<>();
                }
                return writeTriggers;

             case REPLICATION_CREATE:
                if (repCreateTriggers == null && createIfNotExist)
                {
                   repCreateTriggers = new IdentityHashMap<>();
                }
                return repCreateTriggers;

             case REPLICATION_DELETE:
                if (repDeleteTriggers == null && createIfNotExist)
                {
                   repDeleteTriggers = new IdentityHashMap<>();
                }
                return repDeleteTriggers;

             case REPLICATION_WRITE:
                if (repWriteTriggers == null && createIfNotExist)
                {
                   repWriteTriggers = new IdentityHashMap<>();
                }
                return repWriteTriggers;
          }

          // this should never be reached
          return null;
       }

       /**
        * Check if a trigger is disabled for a buffer on this layer. The reason for this
        * implementation are the following observations:
        * <ul>
        *    <li>DELETE behaves like a replication trigger!
        *    <li>Multiple disables: if at least one 'allow-replication' then replication (including
        *        DELETE) is always allowed for this layer.
        *    <li>If in a subsequent layer (new upper scope) LOAD is disabled in a different manner,
        *        that layer takes precedence.
        * </ul>
        * <p>
        * For FIND/ASSIGN/CREATE/WRITE triggers, if they are not DISABLED, then UNKNOWN status will
        * be returned.  It is OK that ENABLED is not returned for these cases because the caller is
        * checking on the DISABLED status for this specific layer and should not be looking for an
        * affirmative result. Returning UNKNOWN means these triggers are not-DISABLED for this layer.
        * The caller is expected to handle the checking of multiple layers as needed to determine
        * the resulting status.
        *
        * @param   det
        *          Database event type.
        * @param   bufferCls
        *          The buffer.
        *
        * @return  The trigger status for specified buffer on this layer.
        */
       public EnableStatus getTriggerStatus(DatabaseEventType det, Class<? extends Buffer> bufferCls)
       {
          switch (det)
          {
             case FIND:
                if (disabledDumpTriggers != null && disabledDumpTriggers.contains(bufferCls))
                {
                   return EnableStatus.DISABLED;
                }
                break;

             case ASSIGN:
             case CREATE:
             case WRITE:
                if (disabledLoadTriggers != null && disabledLoadTriggers.contains(bufferCls))
                {
                   return enabledReplicationTriggers != null && enabledReplicationTriggers.contains(bufferCls)
                         ? EnableStatus.REPLICATION_ONLY
                         : EnableStatus.DISABLED;
                }
                break;

             case DELETE:
                if (disabledLoadTriggers != null && disabledLoadTriggers.contains(bufferCls))
                {
                   return enabledReplicationTriggers != null && enabledReplicationTriggers.contains(bufferCls)
                         ? EnableStatus.ENABLED
                         : EnableStatus.DISABLED;
                }
                break;

             case REPLICATION_DELETE:
             case REPLICATION_WRITE:
             case REPLICATION_CREATE:
                if (enabledReplicationTriggers != null && enabledReplicationTriggers.contains(bufferCls))
                {
                   // this is a little forced, normally replication events should
                   // not be received as params
                   return EnableStatus.ENABLED;
                }
                break;
          }

          // otherwise
          return EnableStatus.UNKNOWN;
       }
    }