/* Copyright (c) 2001-2019, The HSQL Development Group
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 *
 * Neither the name of the HSQL Development Group nor the names of its
 * contributors may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG,
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */


package org.hsqldb;

import org.hsqldb.HsqlNameManager.HsqlName;
import org.hsqldb.error.Error;
import org.hsqldb.error.ErrorCode;
import org.hsqldb.lib.HsqlDeque;
import org.hsqldb.lib.OrderedHashSet;
import org.hsqldb.lib.StringConverter;
import org.hsqldb.rights.Grantee;
import org.hsqldb.trigger.Trigger;

// peterhudson@users 20020130 - patch 478657 by peterhudson - triggers support
// fredt@users 20020130 - patch 1.7.0 by fredt
// added new class as jdk 1.1 does not allow use of LinkedList
// fredt@users 20030727 - signature and other alterations
// fredt@users 20040430 - changes by mattshaw@users to allow termination of the
// trigger thread -
// fredt@users - updated for v. 2.x

Represents an HSQLDB Trigger definition.

Provides services regarding HSQLDB Trigger execution and metadata.

Development of the trigger implementation sponsored by Logicscope Realisations Ltd

Author:Peter Hudson (peterhudson@users dot sourceforge.net)
Version: 2.0.1
Since:hsqldb 1.61
/** * Represents an HSQLDB Trigger definition. <p> * * Provides services regarding HSQLDB Trigger execution and metadata. <p> * * Development of the trigger implementation sponsored by Logicscope * Realisations Ltd * * @author Peter Hudson (peterhudson@users dot sourceforge.net) * @version 2.0.1 * @since hsqldb 1.61 */
public class TriggerDef implements Runnable, SchemaObject { static final int OLD_ROW = 0; static final int NEW_ROW = 1; static final int RANGE_COUNT = 2; static final int OLD_TABLE = 2; static final int NEW_TABLE = 3; static final int BEFORE = 4; static final int AFTER = 5; static final int INSTEAD = 6; // static final int NUM_TRIGGER_OPS = 3; // {ins,del,upd} static final int NUM_TRIGS = NUM_TRIGGER_OPS * 3; // {b}{fer}, {a},{fer, fes} // static final TriggerDef[] emptyArray = new TriggerDef[]{}; Table[] transitions; RangeVariable[] rangeVars; Expression condition; boolean hasTransitionTables; boolean hasTransitionRanges; String conditionSQL; Routine routine; int[] updateColumns; // other variables private HsqlName name; long changeTimestamp; int actionTiming; int operationType; boolean isSystem; boolean forEachRow; boolean nowait; // block or overwrite if queue full int maxRowsQueued; // max size of queue of pending triggers Table table; Trigger trigger; String triggerClassName; int triggerType; Thread thread; //protected boolean busy; // firing trigger in progress protected HsqlDeque pendingQueue; // row triggers pending protected int rowsQueued; // rows in pendingQueue protected boolean valid = true; // parsing valid protected volatile boolean keepGoing = true; TriggerDef() {}
Constructs a new TriggerDef object to represent an HSQLDB trigger declared in an SQL CREATE TRIGGER statement. Changes in 1.7.2 allow the queue size to be specified as 0. A zero queue size causes the Trigger.fire() code to run in the main thread of execution (fully inside the enclosing transaction). Otherwise, the code is run in the Trigger's own thread. (fredt@users)
Params:
  • name – The trigger object's HsqlName
  • when – whether the trigger fires before, after or instead of the triggering event
  • operation – the triggering operation; currently insert, update, or delete
  • forEach – indicates whether the trigger is fired for each row (true) or statement (false)
  • table – the Table object upon which the indicated operation fires the trigger
  • triggerClassName – the fully qualified name of the class implementing the org.hsqldb.Trigger (trigger body) interface
  • noWait – do not wait for available space on the pending queue; if the pending queue does not have fewer than nQueueSize queued items, then overwrite the current tail instead
  • queueSize – the length to which the pending queue may grow before further additions are either blocked or overwrite the tail entry, as determined by noWait
/** * Constructs a new TriggerDef object to represent an HSQLDB trigger * declared in an SQL CREATE TRIGGER statement. * * Changes in 1.7.2 allow the queue size to be specified as 0. A zero * queue size causes the Trigger.fire() code to run in the main thread of * execution (fully inside the enclosing transaction). Otherwise, the code * is run in the Trigger's own thread. * (fredt@users) * * @param name The trigger object's HsqlName * @param when whether the trigger fires * before, after or instead of the triggering event * @param operation the triggering operation; * currently insert, update, or delete * @param forEach indicates whether the trigger is fired for each row * (true) or statement (false) * @param table the Table object upon which the indicated operation * fires the trigger * @param triggerClassName the fully qualified name of the class implementing * the org.hsqldb.Trigger (trigger body) interface * @param noWait do not wait for available space on the pending queue; if * the pending queue does not have fewer than nQueueSize queued items, * then overwrite the current tail instead * @param queueSize the length to which the pending queue may grow before * further additions are either blocked or overwrite the tail entry, * as determined by noWait */
public TriggerDef(HsqlNameManager.HsqlName name, int when, int operation, boolean forEach, Table table, Table[] transitions, RangeVariable[] rangeVars, Expression condition, String conditionSQL, int[] updateColumns, String triggerClassName, boolean noWait, int queueSize) { this(name, when, operation, forEach, table, transitions, rangeVars, condition, conditionSQL, updateColumns); this.triggerClassName = triggerClassName; this.nowait = noWait; this.maxRowsQueued = queueSize; rowsQueued = 0; pendingQueue = new HsqlDeque(); Class<?> cl = null; try { cl = Class.forName(triggerClassName, true, Thread.currentThread().getContextClassLoader()); } catch (Throwable t1) { try { cl = Class.forName(triggerClassName); } catch (Throwable t) {} } if (cl == null) { valid = false; trigger = new DefaultTrigger(); } else { try { // dynamically instantiate it trigger = (Trigger) cl.getDeclaredConstructor().newInstance(); } catch (Throwable t1) { valid = false; trigger = new DefaultTrigger(); } } } public TriggerDef(HsqlNameManager.HsqlName name, int when, int operation, boolean forEachRow, Table table, Table[] transitions, RangeVariable[] rangeVars, Expression condition, String conditionSQL, int[] updateColumns) { this.name = name; this.actionTiming = when; this.operationType = operation; this.forEachRow = forEachRow; this.table = table; this.transitions = transitions; this.rangeVars = rangeVars; this.condition = condition == null ? Expression.EXPR_TRUE : condition; this.updateColumns = updateColumns; this.conditionSQL = conditionSQL; hasTransitionRanges = rangeVars[OLD_ROW] != null || rangeVars[NEW_ROW] != null; hasTransitionTables = transitions[OLD_TABLE] != null || transitions[NEW_TABLE] != null; setUpIndexesAndTypes(); } public boolean isValid() { return valid; } public int getType() { return SchemaObject.TRIGGER; } public HsqlName getName() { return name; } public HsqlName getCatalogName() { return name.schema.schema; } public HsqlName getSchemaName() { return name.schema; } public Grantee getOwner() { return name.schema.owner; } public OrderedHashSet getReferences() { return new OrderedHashSet(); } public OrderedHashSet getComponents() { return null; } public void compile(Session session, SchemaObject parentObject) {}
Retrieves the SQL character sequence required to (re)create the trigger, as a String
Returns:the SQL character sequence required to (re)create the trigger
/** * Retrieves the SQL character sequence required to (re)create the * trigger, as a String * * @return the SQL character sequence required to (re)create the * trigger */
public String getSQL() { StringBuilder sb = getSQLMain(); if (maxRowsQueued != 0) { sb.append(Tokens.T_QUEUE).append(' '); sb.append(maxRowsQueued).append(' '); if (nowait) { sb.append(Tokens.T_NOWAIT).append(' '); } } sb.append(Tokens.T_CALL).append(' '); sb.append(StringConverter.toQuotedString(triggerClassName, '"', false)); return sb.toString(); } public long getChangeTimestamp() { return changeTimestamp; } public StringBuilder getSQLMain() { StringBuilder sb = new StringBuilder(256); sb.append(Tokens.T_CREATE).append(' '); sb.append(Tokens.T_TRIGGER).append(' '); sb.append(name.getSchemaQualifiedStatementName()).append(' '); sb.append(getActionTimingString()).append(' '); sb.append(getEventTypeString()).append(' '); if (updateColumns != null) { sb.append(Tokens.T_OF).append(' '); for (int i = 0; i < updateColumns.length; i++) { if (i != 0) { sb.append(','); } HsqlName name = table.getColumn(updateColumns[i]).getName(); sb.append(name.statementName); } sb.append(' '); } sb.append(Tokens.T_ON).append(' '); sb.append(table.getName().getSchemaQualifiedStatementName()); sb.append(' '); if (hasTransitionRanges || hasTransitionTables) { sb.append(Tokens.T_REFERENCING).append(' '); if (rangeVars[OLD_ROW] != null) { sb.append(Tokens.T_OLD).append(' ').append(Tokens.T_ROW); sb.append(' ').append(Tokens.T_AS).append(' '); sb.append( rangeVars[OLD_ROW].getTableAlias().getStatementName()); sb.append(' '); } if (rangeVars[NEW_ROW] != null) { sb.append(Tokens.T_NEW).append(' ').append(Tokens.T_ROW); sb.append(' ').append(Tokens.T_AS).append(' '); sb.append( rangeVars[NEW_ROW].getTableAlias().getStatementName()); sb.append(' '); } if (transitions[OLD_TABLE] != null) { sb.append(Tokens.T_OLD).append(' ').append(Tokens.T_TABLE); sb.append(' ').append(Tokens.T_AS).append(' '); sb.append(transitions[OLD_TABLE].getName().statementName); sb.append(' '); } if (transitions[NEW_TABLE] != null) { sb.append(Tokens.T_OLD).append(' ').append(Tokens.T_TABLE); sb.append(' ').append(Tokens.T_AS).append(' '); sb.append(transitions[NEW_TABLE].getName().statementName); sb.append(' '); } } if (forEachRow) { sb.append(Tokens.T_FOR).append(' '); sb.append(Tokens.T_EACH).append(' '); sb.append(Tokens.T_ROW).append(' '); } if (condition != Expression.EXPR_TRUE) { sb.append(Tokens.T_WHEN).append(' '); sb.append(Tokens.T_OPENBRACKET).append(conditionSQL); sb.append(Tokens.T_CLOSEBRACKET).append(' '); } return sb; } public String getClassName() { return trigger.getClass().getName(); } public String getActionTimingString() { switch (this.actionTiming) { case TriggerDef.BEFORE : return Tokens.T_BEFORE; case TriggerDef.AFTER : return Tokens.T_AFTER; case TriggerDef.INSTEAD : return Tokens.T_INSTEAD + ' ' + Tokens.T_OF; default : throw Error.runtimeError(ErrorCode.U_S0500, "TriggerDef"); } } public String getEventTypeString() { switch (this.operationType) { case StatementTypes.INSERT : return Tokens.T_INSERT; case StatementTypes.DELETE_WHERE : return Tokens.T_DELETE; case StatementTypes.UPDATE_WHERE : return Tokens.T_UPDATE; default : throw Error.runtimeError(ErrorCode.U_S0500, "TriggerDef"); } } public boolean isSystem() { return isSystem; } public boolean isForEachRow() { return forEachRow; } public String getConditionSQL() { return conditionSQL; } public String getProcedureSQL() { return routine == null ? null : routine.getSQLBodyDefinition(); } public int[] getUpdateColumnIndexes() { return updateColumns; } public boolean hasOldTable() { return false; } public boolean hasNewTable() { return false; } public boolean hasOldRow() { return rangeVars[OLD_ROW] != null; } public boolean hasNewRow() { return rangeVars[NEW_ROW] != null; } public String getOldTransitionRowName() { return rangeVars[OLD_ROW] == null ? null : rangeVars[OLD_ROW].getTableAlias() .name; } public String getNewTransitionRowName() { return rangeVars[NEW_ROW] == null ? null : rangeVars[NEW_ROW].getTableAlias() .name; } public String getOldTransitionTableName() { return transitions[OLD_TABLE] == null ? null : transitions[OLD_TABLE] .getName().name; } public String getNewTransitionTableName() { return transitions[NEW_TABLE] == null ? null : transitions[NEW_TABLE] .getName().name; }
Given the SQL creating the trigger, set up the index to the HsqlArrayList[] and the associated GRANT type
/** * Given the SQL creating the trigger, set up the index to the * HsqlArrayList[] and the associated GRANT type */
void setUpIndexesAndTypes() { triggerType = 0; switch (operationType) { case StatementTypes.INSERT : triggerType = Trigger.INSERT_AFTER; break; case StatementTypes.DELETE_WHERE : triggerType = Trigger.DELETE_AFTER; break; case StatementTypes.UPDATE_WHERE : triggerType = Trigger.UPDATE_AFTER; break; default : throw Error.runtimeError(ErrorCode.U_S0500, "TriggerDef"); } if (forEachRow) { triggerType += NUM_TRIGGER_OPS; } if (actionTiming == TriggerDef.BEFORE || actionTiming == TriggerDef.INSTEAD) { triggerType += NUM_TRIGGER_OPS; } }
Return the type code for operation tokens
/** * Return the type code for operation tokens */
static int getOperationType(int token) { switch (token) { case Tokens.INSERT : return StatementTypes.INSERT; case Tokens.DELETE : return StatementTypes.DELETE_WHERE; case Tokens.UPDATE : return StatementTypes.UPDATE_WHERE; default : throw Error.runtimeError(ErrorCode.U_S0500, "TriggerDef"); } } static int getTiming(int token) { switch (token) { case Tokens.BEFORE : return TriggerDef.BEFORE; case Tokens.AFTER : return TriggerDef.AFTER; case Tokens.INSTEAD : return TriggerDef.INSTEAD; default : throw Error.runtimeError(ErrorCode.U_S0500, "TriggerDef"); } } public int getStatementType() { return operationType; }
run method declaration

the trigger JSP is run in its own thread here. Its job is simply to wait until it is told by the main thread that it should fire the trigger.

/** * run method declaration <P> * * the trigger JSP is run in its own thread here. Its job is simply to * wait until it is told by the main thread that it should fire the * trigger. */
public void run() { while (keepGoing) { TriggerData triggerData = popPair(); if (triggerData != null) { if (triggerData.username != null) { trigger.fire(this.triggerType, name.name, table.getName().name, triggerData.oldRow, triggerData.newRow); } } } try { thread.setContextClassLoader(null); } catch (Throwable t) {} }
start the thread if this is threaded
/** * start the thread if this is threaded */
public synchronized void start() { if (maxRowsQueued != 0) { thread = new Thread(this); thread.start(); } }
signal the thread to stop
/** * signal the thread to stop */
public synchronized void terminate() { keepGoing = false; notify(); }
pop2 method declaration

The consumer (trigger) thread waits for an event to be queued

Note: This push/pop pairing assumes a single producer thread and a single consumer thread _only_.

Returns: Description of the Return Value
/** * pop2 method declaration <P> * * The consumer (trigger) thread waits for an event to be queued <P> * * <B>Note: </B> This push/pop pairing assumes a single producer thread * and a single consumer thread _only_. * * @return Description of the Return Value */
synchronized TriggerData popPair() { if (rowsQueued == 0) { try { wait(); // this releases the lock monitor } catch (InterruptedException e) { /* ignore and resume */ } } rowsQueued--; notify(); // notify push's wait if (pendingQueue.size() == 0) { return null; } else { return (TriggerData) pendingQueue.removeFirst(); } }
The main thread tells the trigger thread to fire by this call. If this Trigger is not threaded then the fire method is called immediately and executed by the main thread. Otherwise, the row data objects are added to the queue to be used by the Trigger thread.
Params:
  • row1 –
  • row2 –
/** * The main thread tells the trigger thread to fire by this call. * If this Trigger is not threaded then the fire method is called * immediately and executed by the main thread. Otherwise, the row * data objects are added to the queue to be used by the Trigger thread. * * @param row1 * @param row2 */
synchronized void pushPair(Session session, Object[] oldData, Object[] newData) { if (maxRowsQueued == 0) { if (condition != Expression.EXPR_TRUE) { session.sessionContext.triggerArguments = new Object[][] { oldData, newData }; if (!condition.testCondition(session)) { return; } } session.getInternalConnection(); try { trigger.fire(triggerType, name.name, table.getName().name, oldData, newData); } finally { session.releaseInternalConnection(); } return; } if (rowsQueued >= maxRowsQueued) { if (nowait) { pendingQueue.removeLast(); // overwrite last } else { try { wait(); } catch (InterruptedException e) { /* ignore and resume */ } rowsQueued++; } } else { rowsQueued++; } pendingQueue.add(new TriggerData(session, oldData, newData)); notify(); // notify pop's wait } public boolean isBusy() { return rowsQueued != 0; } public Table getTable() { return table; } public String getActionOrientationString() { return forEachRow ? Tokens.T_ROW : Tokens.T_STATEMENT; }
Class to store the data used to fire a trigger. The username attribute is not used but it allows developers to change the signature of the fire method of the Trigger class and pass the user name to the Trigger.
/** * Class to store the data used to fire a trigger. The username attribute * is not used but it allows developers to change the signature of the * fire method of the Trigger class and pass the user name to the Trigger. */
static class TriggerData { public Object[] oldRow; public Object[] newRow; public String username; public TriggerData(Session session, Object[] oldRow, Object[] newRow) { this.oldRow = oldRow; this.newRow = newRow; this.username = session.getUsername(); } } static class DefaultTrigger implements Trigger { public void fire(int i, String name, String table, Object[] row1, Object[] row2) { // do nothing } } }