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