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