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