org.hsqldb.TriggerDef Maven / Gradle / Ivy
Show all versions of hsqldb Show documentation
/* Copyright (c) 2001-2024, 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.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.7.3
* @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;
/**
* 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(
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(
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 getSchemaName() {
return name.schema;
}
public HsqlName getCatalogName() {
return name.schema.schema;
}
public Grantee getOwner() {
return name.schema.owner;
}
/**
* 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(' ')
.append(maxRowsQueued)
.append(' ');
if (nowait) {
sb.append(Tokens.T_NOWAIT).append(' ');
}
}
sb.append(Tokens.T_CALL)
.append(' ')
.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(' ')
.append(Tokens.T_TRIGGER)
.append(' ')
.append(name.getSchemaQualifiedStatementName())
.append(' ')
.append(getActionTimingString())
.append(' ')
.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(' ')
.append(table.getName().getSchemaQualifiedStatementName())
.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)
.append(' ')
.append(Tokens.T_AS)
.append(' ')
.append(rangeVars[OLD_ROW].getTableAlias().getStatementName())
.append(' ');
}
if (rangeVars[NEW_ROW] != null) {
sb.append(Tokens.T_NEW)
.append(' ')
.append(Tokens.T_ROW)
.append(' ')
.append(Tokens.T_AS)
.append(' ')
.append(rangeVars[NEW_ROW].getTableAlias().getStatementName())
.append(' ');
}
if (transitions[OLD_TABLE] != null) {
sb.append(Tokens.T_OLD)
.append(' ')
.append(Tokens.T_TABLE)
.append(' ')
.append(Tokens.T_AS)
.append(' ')
.append(transitions[OLD_TABLE].getName().statementName)
.append(' ');
}
if (transitions[NEW_TABLE] != null) {
sb.append(Tokens.T_OLD)
.append(' ')
.append(Tokens.T_TABLE)
.append(' ')
.append(Tokens.T_AS)
.append(' ')
.append(transitions[NEW_TABLE].getName().statementName)
.append(' ');
}
}
if (forEachRow) {
sb.append(Tokens.T_FOR)
.append(' ')
.append(Tokens.T_EACH)
.append(' ')
.append(Tokens.T_ROW)
.append(' ');
}
if (condition != Expression.EXPR_TRUE) {
sb.append(Tokens.T_WHEN)
.append(' ')
.append(Tokens.T_OPENBRACKET)
.append(conditionSQL)
.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
*/
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
*/
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.
*/
public void run() {
while (keepGoing) {
TriggerData triggerData = popPair();
if (triggerData != null) {
if (triggerData.username != null) {
trigger.fire(
this.triggerType,
name.name,
table.getName().name,
table.getColumnLabels(),
triggerData.oldRow,
triggerData.newRow);
}
}
}
try {
thread.setContextClassLoader(null);
} catch (Throwable t) {}
}
/**
* 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
*/
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_.
*
* @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.isEmpty()) {
return null;
} else {
return 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.
*
* @param session the session
* @param oldData old row
* @param newData new row
*/
synchronized void pushPair(
Session session,
Object[] oldData,
Object[] newData) {
if (maxRowsQueued == 0) {
session.sessionContext.push();
session.sessionContext.triggerArguments = new Object[][] {
oldData, newData
};
session.getInternalConnection();
try {
if (condition.testCondition(session)) {
trigger.fire(
triggerType,
name.name,
table.getName().name,
table.getColumnLabels(),
oldData,
newData);
}
} finally {
session.releaseInternalConnection();
session.sessionContext.pop();
}
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.
*/
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
}
}
}