/* Copyright (c) 2001-2009, 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.lib.HsqlDeque; import org.hsqldb.lib.OrderedHashSet; import org.hsqldb.lib.StringConverter; import org.hsqldb.rights.GrantConstants; import org.hsqldb.rights.Grantee; // 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 - /** * Represents an HSQLDB Trigger definition. <p> * * Provides services regarding HSLDB Trigger execution and metadata. <p> * * Development of the trigger implementation sponsored by Logicscope * Realisations Ltd * * @author Peter Hudson - Logicscope Realisations Ltd * @version 1.7.0 (1.0.0.3) * Revision History: 1.0.0.1 First release in hsqldb 1.61 * 1.0.0.2 'nowait' support to prevent deadlock 1.0.0.3 multiple row * queue for each trigger */ public class TriggerDef implements Runnable, SchemaObject { static final int OLD_ROW = 0; static final int NEW_ROW = 1; static final int OLD_TABLE = 2; static final int NEW_TABLE = 3; // static final int NUM_TRIGGER_OPS = 3; // {ins,del,upd} static final int NUM_TRIGS = NUM_TRIGGER_OPS * 2; // {b, a},{fer, fes} static final int defaultQueueSize = 1024; // static final TriggerDef[] emptyArray = new TriggerDef[]{}; Table[] transitions; RangeVariable[] rangeVars; Expression condition; boolean hasTransitionTables; boolean hasTransitionRanges; String conditionSQL; String procedureSQL; Statement[] statements = Statement.emptyArray; int[] updateColumns; // other variables HsqlName name; String actionTimingString; String eventTimingString; int operationPrivilegeType; 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; int vectorIndex; // index into TriggerDef[][] 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) * * @param name The trigger object's HsqlName * @param when the String representation of whether the trigger fires * before or after the triggering event * @param operation the String representation of 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 named 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, String when, String operation, boolean forEach, Table table, Table[] transitions, RangeVariable[] rangeVars, Expression condition, String conditionSQL, int[] updateColumns, String triggerClassName, boolean noWait, int queueSize) { this.name = name; this.actionTimingString = when; this.eventTimingString = operation; this.forEachRow = forEach; this.table = table; this.transitions = transitions; this.rangeVars = rangeVars; this.condition = condition == null ? Expression.EXPR_TRUE : condition; this.conditionSQL = conditionSQL; this.updateColumns = updateColumns; this.procedureSQL = procedureSQL; this.triggerClassName = triggerClassName; this.nowait = noWait; this.maxRowsQueued = queueSize; rowsQueued = 0; pendingQueue = new HsqlDeque(); setUpIndexesAndTypes(); Class cl; try { cl = Class.forName(triggerClassName); } catch (ClassNotFoundException e) { valid = false; cl = DefaultTrigger.class; } try { // dynamically instantiate it trigger = (Trigger) cl.newInstance(); } catch (Exception e) { valid = false; cl = DefaultTrigger.class; } } 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) {} /** * Retrieves the SQL character sequence required to (re)create the * trigger, as a StringBuffer * * @return the SQL character sequence required to (re)create the * trigger */ public String getSQL() { StringBuffer sb = new StringBuffer(256); sb.append(Tokens.T_CREATE).append(' '); sb.append(Tokens.T_TRIGGER).append(' '); sb.append(name.getSchemaQualifiedStatementName()).append(' '); sb.append(actionTimingString).append(' '); sb.append(eventTimingString).append(' '); sb.append(Tokens.T_ON).append(' '); sb.append(table.getName().getSchemaQualifiedStatementName()); sb.append(' '); if (forEachRow) { sb.append(Tokens.T_FOR).append(' '); sb.append(Tokens.T_EACH).append(' '); sb.append(Tokens.T_ROW).append(' '); } if (nowait) { sb.append(Tokens.T_NOWAIT).append(' '); } if (maxRowsQueued != defaultQueueSize) { sb.append(Tokens.T_QUEUE).append(' '); sb.append(maxRowsQueued).append(' '); } sb.append(Tokens.T_CALL).append(' '); sb.append(StringConverter.toQuotedString(triggerClassName, '"', false)); return sb.toString(); } public String getClassName() { return trigger.getClass().getName(); } public String getActionTimingString() { return actionTimingString; } public String getEventTypeString() { return eventTimingString; } public boolean isForEachRow() { return forEachRow; } public String getConditionSQL() { return conditionSQL; } public String getProcedureSQL() { return procedureSQL; } public int[] getUpdateColumnIndexes() { return updateColumns; } public boolean hasOldTable() { return false; } public boolean hasNewTable() { return false; } public String getOldTransitionRowName() { return transitions[OLD_ROW] == null ? null : transitions[OLD_ROW].getName() .name; } public String getNewTransitionRowName() { return transitions[NEW_ROW] == null ? null : transitions[NEW_ROW].getName() .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() { vectorIndex = 0; if (eventTimingString.equals(Tokens.T_INSERT)) { vectorIndex = Trigger.INSERT_AFTER; operationPrivilegeType = GrantConstants.INSERT; } else if (eventTimingString.equals(Tokens.T_DELETE)) { operationPrivilegeType = GrantConstants.DELETE; vectorIndex = Trigger.DELETE_AFTER; } else if (eventTimingString.equals(Tokens.T_UPDATE)) { operationPrivilegeType = GrantConstants.UPDATE; vectorIndex = Trigger.UPDATE_AFTER; } else { throw Error.runtimeError(ErrorCode.U_S0500, "TriggerDef"); } if (actionTimingString.equals(Tokens.T_BEFORE) || actionTimingString.equals(Tokens.T_INSERT)) { vectorIndex += NUM_TRIGGER_OPS; // number of operations } triggerType = vectorIndex; if (forEachRow) { triggerType += 2 * NUM_TRIGGER_OPS; } } public int getPrivilegeType() { return operationPrivilegeType; } /** * 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.vectorIndex, name.name, table.getName().name, triggerData.oldRow, triggerData.newRow); } } } } /** * 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 <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 caled * 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[] row1, Object[] row2) { if (maxRowsQueued == 0) { trigger.fire(triggerType, name.name, table.getName().name, row1, row2); 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, row1, row2)); 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 org.hsqldb.Trigger { public void fire(int i, String name, String table, Object[] row1, Object[] row2) { throw new RuntimeException("Missing Trigger class!"); } } }