1 /* Copyright (c) 2001-2016, The HSQL Development Group
2  * All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions are met:
6  *
7  * Redistributions of source code must retain the above copyright notice, this
8  * list of conditions and the following disclaimer.
9  *
10  * Redistributions in binary form must reproduce the above copyright notice,
11  * this list of conditions and the following disclaimer in the documentation
12  * and/or other materials provided with the distribution.
13  *
14  * Neither the name of the HSQL Development Group nor the names of its
15  * contributors may be used to endorse or promote products derived from this
16  * software without specific prior written permission.
17  *
18  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21  * ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG,
22  * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
23  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
24  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
26  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
28  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29  */
30 
31 
32 package org.hsqldb;
33 
34 import org.hsqldb.HsqlNameManager.HsqlName;
35 import org.hsqldb.error.Error;
36 import org.hsqldb.error.ErrorCode;
37 import org.hsqldb.lib.HsqlDeque;
38 import org.hsqldb.lib.OrderedHashSet;
39 import org.hsqldb.lib.StringConverter;
40 import org.hsqldb.rights.Grantee;
41 
42 // peterhudson@users 20020130 - patch 478657 by peterhudson - triggers support
43 // fredt@users 20020130 - patch 1.7.0 by fredt
44 // added new class as jdk 1.1 does not allow use of LinkedList
45 // fredt@users 20030727 - signature and other alterations
46 // fredt@users 20040430 - changes by mattshaw@users to allow termination of the
47 // trigger thread -
48 // fredt@users - updated for v. 2.x
49 
50 /**
51  *  Represents an HSQLDB Trigger definition. <p>
52  *
53  *  Provides services regarding HSQLDB Trigger execution and metadata. <p>
54  *
55  *  Development of the trigger implementation sponsored by Logicscope
56  *  Realisations Ltd
57  *
58  * @author Peter Hudson (peterhudson@users dot sourceforge.net)
59  * @version  2.0.1
60  * @since hsqldb 1.61
61  */
62 public class TriggerDef implements Runnable, SchemaObject {
63 
64     static final int OLD_ROW     = 0;
65     static final int NEW_ROW     = 1;
66     static final int RANGE_COUNT = 2;
67     static final int OLD_TABLE   = 2;
68     static final int NEW_TABLE   = 3;
69     static final int BEFORE      = 4;
70     static final int AFTER       = 5;
71     static final int INSTEAD     = 6;
72 
73     //
74     static final int NUM_TRIGGER_OPS = 3;                      // {ins,del,upd}
75     static final int NUM_TRIGS       = NUM_TRIGGER_OPS * 3;    // {b}{fer}, {a},{fer, fes}
76 
77     //
78     static final TriggerDef[] emptyArray = new TriggerDef[]{};
79     Table[]                   transitions;
80     RangeVariable[]           rangeVars;
81     Expression                condition;
82     boolean                   hasTransitionTables;
83     boolean                   hasTransitionRanges;
84     String                    conditionSQL;
85     Routine                   routine;
86     int[]                     updateColumns;
87 
88     // other variables
89     private HsqlName name;
90     long             changeTimestamp;
91     int              actionTiming;
92     int              operationType;
93     boolean          isSystem;
94     boolean          forEachRow;
95     boolean          nowait;                                   // block or overwrite if queue full
96     int              maxRowsQueued;                            // max size of queue of pending triggers
97     Table            table;
98     Trigger          trigger;
99     String           triggerClassName;
100     int              triggerType;
101     Thread           thread;
102 
103     //protected boolean busy;               // firing trigger in progress
104     protected HsqlDeque        pendingQueue;                   // row triggers pending
105     protected int              rowsQueued;                     // rows in pendingQueue
106     protected boolean          valid     = true;               // parsing valid
107     protected volatile boolean keepGoing = true;
108 
TriggerDef()109     TriggerDef() {}
110 
111     /**
112      *  Constructs a new TriggerDef object to represent an HSQLDB trigger
113      *  declared in an SQL CREATE TRIGGER statement.
114      *
115      *  Changes in 1.7.2 allow the queue size to be specified as 0. A zero
116      *  queue size causes the Trigger.fire() code to run in the main thread of
117      *  execution (fully inside the enclosing transaction). Otherwise, the code
118      *  is run in the Trigger's own thread.
119      *  (fredt@users)
120      *
121      * @param  name The trigger object's HsqlName
122      * @param  when whether the trigger fires
123      *      before, after or instead of the triggering event
124      * @param  operation the triggering operation;
125      *      currently insert, update, or delete
126      * @param  forEach indicates whether the trigger is fired for each row
127      *      (true) or statement (false)
128      * @param  table the Table object upon which the indicated operation
129      *      fires the trigger
130      * @param  triggerClassName the fully qualified named of the class implementing
131      *      the org.hsqldb.Trigger (trigger body) interface
132      * @param  noWait do not wait for available space on the pending queue; if
133      *      the pending queue does not have fewer than nQueueSize queued items,
134      *      then overwrite the current tail instead
135      * @param  queueSize the length to which the pending queue may grow before
136      *      further additions are either blocked or overwrite the tail entry,
137      *      as determined by noWait
138      */
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)139     public TriggerDef(HsqlNameManager.HsqlName name, int when, int operation,
140                       boolean forEach, Table table, Table[] transitions,
141                       RangeVariable[] rangeVars, Expression condition,
142                       String conditionSQL, int[] updateColumns,
143                       String triggerClassName, boolean noWait, int queueSize) {
144 
145         this(name, when, operation, forEach, table, transitions, rangeVars,
146              condition, conditionSQL, updateColumns);
147 
148         this.triggerClassName = triggerClassName;
149         this.nowait           = noWait;
150         this.maxRowsQueued    = queueSize;
151         rowsQueued            = 0;
152         pendingQueue          = new HsqlDeque();
153 
154         Class cl = null;
155 
156         try {
157             cl = Class.forName(triggerClassName, true,
158                                Thread.currentThread().getContextClassLoader());
159         } catch (Throwable t1) {
160             try {
161                 cl = Class.forName(triggerClassName);
162             } catch (Throwable t) {}
163         }
164 
165         if (cl == null) {
166             valid   = false;
167             trigger = new DefaultTrigger();
168         } else {
169             try {
170 
171                 // dynamically instantiate it
172                 trigger = (Trigger) cl.newInstance();
173             } catch (Throwable t1) {
174                 valid   = false;
175                 trigger = new DefaultTrigger();
176             }
177         }
178     }
179 
TriggerDef(HsqlNameManager.HsqlName name, int when, int operation, boolean forEachRow, Table table, Table[] transitions, RangeVariable[] rangeVars, Expression condition, String conditionSQL, int[] updateColumns)180     public TriggerDef(HsqlNameManager.HsqlName name, int when, int operation,
181                       boolean forEachRow, Table table, Table[] transitions,
182                       RangeVariable[] rangeVars, Expression condition,
183                       String conditionSQL, int[] updateColumns) {
184 
185         this.name          = name;
186         this.actionTiming  = when;
187         this.operationType = operation;
188         this.forEachRow    = forEachRow;
189         this.table         = table;
190         this.transitions   = transitions;
191         this.rangeVars     = rangeVars;
192         this.condition     = condition == null ? Expression.EXPR_TRUE
193                                                : condition;
194         this.updateColumns = updateColumns;
195         this.conditionSQL  = conditionSQL;
196         hasTransitionRanges = rangeVars[OLD_ROW] != null
197                               || rangeVars[NEW_ROW] != null;
198         hasTransitionTables = transitions[OLD_TABLE] != null
199                               || transitions[NEW_TABLE] != null;
200 
201         setUpIndexesAndTypes();
202     }
203 
isValid()204     public boolean isValid() {
205         return valid;
206     }
207 
getType()208     public int getType() {
209         return SchemaObject.TRIGGER;
210     }
211 
getName()212     public HsqlName getName() {
213         return name;
214     }
215 
getCatalogName()216     public HsqlName getCatalogName() {
217         return name.schema.schema;
218     }
219 
getSchemaName()220     public HsqlName getSchemaName() {
221         return name.schema;
222     }
223 
getOwner()224     public Grantee getOwner() {
225         return name.schema.owner;
226     }
227 
getReferences()228     public OrderedHashSet getReferences() {
229         return new OrderedHashSet();
230     }
231 
getComponents()232     public OrderedHashSet getComponents() {
233         return null;
234     }
235 
compile(Session session, SchemaObject parentObject)236     public void compile(Session session, SchemaObject parentObject) {}
237 
238     /**
239      *  Retrieves the SQL character sequence required to (re)create the
240      *  trigger, as a StringBuffer
241      *
242      * @return the SQL character sequence required to (re)create the
243      *  trigger
244      */
getSQL()245     public String getSQL() {
246 
247         StringBuffer sb = getSQLMain();
248 
249         if (maxRowsQueued != 0) {
250             sb.append(Tokens.T_QUEUE).append(' ');
251             sb.append(maxRowsQueued).append(' ');
252 
253             if (nowait) {
254                 sb.append(Tokens.T_NOWAIT).append(' ');
255             }
256         }
257 
258         sb.append(Tokens.T_CALL).append(' ');
259         sb.append(StringConverter.toQuotedString(triggerClassName, '"',
260                 false));
261 
262         return sb.toString();
263     }
264 
getChangeTimestamp()265     public long getChangeTimestamp() {
266         return changeTimestamp;
267     }
268 
getSQLMain()269     public StringBuffer getSQLMain() {
270 
271         StringBuffer sb = new StringBuffer(256);
272 
273         sb.append(Tokens.T_CREATE).append(' ');
274         sb.append(Tokens.T_TRIGGER).append(' ');
275         sb.append(name.getSchemaQualifiedStatementName()).append(' ');
276         sb.append(getActionTimingString()).append(' ');
277         sb.append(getEventTypeString()).append(' ');
278 
279         if (updateColumns != null) {
280             sb.append(Tokens.T_OF).append(' ');
281 
282             for (int i = 0; i < updateColumns.length; i++) {
283                 if (i != 0) {
284                     sb.append(',');
285                 }
286 
287                 HsqlName name = table.getColumn(updateColumns[i]).getName();
288 
289                 sb.append(name.statementName);
290             }
291 
292             sb.append(' ');
293         }
294 
295         sb.append(Tokens.T_ON).append(' ');
296         sb.append(table.getName().getSchemaQualifiedStatementName());
297         sb.append(' ');
298 
299         if (hasTransitionRanges || hasTransitionTables) {
300             sb.append(Tokens.T_REFERENCING).append(' ');
301 
302             if (rangeVars[OLD_ROW] != null) {
303                 sb.append(Tokens.T_OLD).append(' ').append(Tokens.T_ROW);
304                 sb.append(' ').append(Tokens.T_AS).append(' ');
305                 sb.append(
306                     rangeVars[OLD_ROW].getTableAlias().getStatementName());
307                 sb.append(' ');
308             }
309 
310             if (rangeVars[NEW_ROW] != null) {
311                 sb.append(Tokens.T_NEW).append(' ').append(Tokens.T_ROW);
312                 sb.append(' ').append(Tokens.T_AS).append(' ');
313                 sb.append(
314                     rangeVars[NEW_ROW].getTableAlias().getStatementName());
315                 sb.append(' ');
316             }
317 
318             if (transitions[OLD_TABLE] != null) {
319                 sb.append(Tokens.T_OLD).append(' ').append(Tokens.T_TABLE);
320                 sb.append(' ').append(Tokens.T_AS).append(' ');
321                 sb.append(transitions[OLD_TABLE].getName().statementName);
322                 sb.append(' ');
323             }
324 
325             if (transitions[NEW_TABLE] != null) {
326                 sb.append(Tokens.T_OLD).append(' ').append(Tokens.T_TABLE);
327                 sb.append(' ').append(Tokens.T_AS).append(' ');
328                 sb.append(transitions[NEW_TABLE].getName().statementName);
329                 sb.append(' ');
330             }
331         }
332 
333         if (forEachRow) {
334             sb.append(Tokens.T_FOR).append(' ');
335             sb.append(Tokens.T_EACH).append(' ');
336             sb.append(Tokens.T_ROW).append(' ');
337         }
338 
339         if (condition != Expression.EXPR_TRUE) {
340             sb.append(Tokens.T_WHEN).append(' ');
341             sb.append(Tokens.T_OPENBRACKET).append(conditionSQL);
342             sb.append(Tokens.T_CLOSEBRACKET).append(' ');
343         }
344 
345         return sb;
346     }
347 
getClassName()348     public String getClassName() {
349         return trigger.getClass().getName();
350     }
351 
getActionTimingString()352     public String getActionTimingString() {
353 
354         switch (this.actionTiming) {
355 
356             case TriggerDef.BEFORE :
357                 return Tokens.T_BEFORE;
358 
359             case TriggerDef.AFTER :
360                 return Tokens.T_AFTER;
361 
362             case TriggerDef.INSTEAD :
363                 return Tokens.T_INSTEAD + ' ' + Tokens.T_OF;
364 
365             default :
366                 throw Error.runtimeError(ErrorCode.U_S0500, "TriggerDef");
367         }
368     }
369 
getEventTypeString()370     public String getEventTypeString() {
371 
372         switch (this.operationType) {
373 
374             case StatementTypes.INSERT :
375                 return Tokens.T_INSERT;
376 
377             case StatementTypes.DELETE_WHERE :
378                 return Tokens.T_DELETE;
379 
380             case StatementTypes.UPDATE_WHERE :
381                 return Tokens.T_UPDATE;
382 
383             default :
384                 throw Error.runtimeError(ErrorCode.U_S0500, "TriggerDef");
385         }
386     }
387 
isSystem()388     public boolean isSystem() {
389         return isSystem;
390     }
391 
isForEachRow()392     public boolean isForEachRow() {
393         return forEachRow;
394     }
395 
getConditionSQL()396     public String getConditionSQL() {
397         return conditionSQL;
398     }
399 
getProcedureSQL()400     public String getProcedureSQL() {
401         return routine == null ? null
402                                : routine.getSQLBodyDefinition();
403     }
404 
getUpdateColumnIndexes()405     public int[] getUpdateColumnIndexes() {
406         return updateColumns;
407     }
408 
hasOldTable()409     public boolean hasOldTable() {
410         return false;
411     }
412 
hasNewTable()413     public boolean hasNewTable() {
414         return false;
415     }
416 
hasOldRow()417     public boolean hasOldRow() {
418         return rangeVars[OLD_ROW] != null;
419     }
420 
hasNewRow()421     public boolean hasNewRow() {
422         return rangeVars[NEW_ROW] != null;
423     }
424 
getOldTransitionRowName()425     public String getOldTransitionRowName() {
426 
427         return rangeVars[OLD_ROW] == null ? null
428                                           : rangeVars[OLD_ROW].getTableAlias()
429                                               .name;
430     }
431 
getNewTransitionRowName()432     public String getNewTransitionRowName() {
433 
434         return rangeVars[NEW_ROW] == null ? null
435                                           : rangeVars[NEW_ROW].getTableAlias()
436                                               .name;
437     }
438 
getOldTransitionTableName()439     public String getOldTransitionTableName() {
440 
441         return transitions[OLD_TABLE] == null ? null
442                                               : transitions[OLD_TABLE]
443                                               .getName().name;
444     }
445 
getNewTransitionTableName()446     public String getNewTransitionTableName() {
447 
448         return transitions[NEW_TABLE] == null ? null
449                                               : transitions[NEW_TABLE]
450                                               .getName().name;
451     }
452 
453     /**
454      *  Given the SQL creating the trigger, set up the index to the
455      *  HsqlArrayList[] and the associated GRANT type
456      */
setUpIndexesAndTypes()457     void setUpIndexesAndTypes() {
458 
459         triggerType = 0;
460 
461         switch (operationType) {
462 
463             case StatementTypes.INSERT :
464                 triggerType = Trigger.INSERT_AFTER;
465                 break;
466 
467             case StatementTypes.DELETE_WHERE :
468                 triggerType = Trigger.DELETE_AFTER;
469                 break;
470 
471             case StatementTypes.UPDATE_WHERE :
472                 triggerType = Trigger.UPDATE_AFTER;
473                 break;
474 
475             default :
476                 throw Error.runtimeError(ErrorCode.U_S0500, "TriggerDef");
477         }
478 
479         if (forEachRow) {
480             triggerType += NUM_TRIGGER_OPS;
481         }
482 
483         if (actionTiming == TriggerDef.BEFORE
484                 || actionTiming == TriggerDef.INSTEAD) {
485             triggerType += NUM_TRIGGER_OPS;
486         }
487     }
488 
489     /**
490      *  Return the type code for operation tokens
491      */
getOperationType(int token)492     static int getOperationType(int token) {
493 
494         switch (token) {
495 
496             case Tokens.INSERT :
497                 return StatementTypes.INSERT;
498 
499             case Tokens.DELETE :
500                 return StatementTypes.DELETE_WHERE;
501 
502             case Tokens.UPDATE :
503                 return StatementTypes.UPDATE_WHERE;
504 
505             default :
506                 throw Error.runtimeError(ErrorCode.U_S0500, "TriggerDef");
507         }
508     }
509 
getTiming(int token)510     static int getTiming(int token) {
511 
512         switch (token) {
513 
514             case Tokens.BEFORE :
515                 return TriggerDef.BEFORE;
516 
517             case Tokens.AFTER :
518                 return TriggerDef.AFTER;
519 
520             case Tokens.INSTEAD :
521                 return TriggerDef.INSTEAD;
522 
523             default :
524                 throw Error.runtimeError(ErrorCode.U_S0500, "TriggerDef");
525         }
526     }
527 
getStatementType()528     public int getStatementType() {
529         return operationType;
530     }
531 
532     /**
533      *  run method declaration <P>
534      *
535      *  the trigger JSP is run in its own thread here. Its job is simply to
536      *  wait until it is told by the main thread that it should fire the
537      *  trigger.
538      */
run()539     public void run() {
540 
541         while (keepGoing) {
542             TriggerData triggerData = popPair();
543 
544             if (triggerData != null) {
545                 if (triggerData.username != null) {
546                     trigger.fire(this.triggerType, name.name,
547                                  table.getName().name, triggerData.oldRow,
548                                  triggerData.newRow);
549                 }
550             }
551         }
552 
553         try {
554             thread.setContextClassLoader(null);
555         } catch (Throwable t) {}
556     }
557 
558     /**
559      * start the thread if this is threaded
560      */
start()561     public synchronized void start() {
562 
563         if (maxRowsQueued != 0) {
564             thread = new Thread(this);
565 
566             thread.start();
567         }
568     }
569 
570     /**
571      * signal the thread to stop
572      */
terminate()573     public synchronized void terminate() {
574 
575         keepGoing = false;
576 
577         notify();
578     }
579 
580     /**
581      *  pop2 method declaration <P>
582      *
583      *  The consumer (trigger) thread waits for an event to be queued <P>
584      *
585      *  <B>Note: </B> This push/pop pairing assumes a single producer thread
586      *  and a single consumer thread _only_.
587      *
588      * @return  Description of the Return Value
589      */
popPair()590     synchronized TriggerData popPair() {
591 
592         if (rowsQueued == 0) {
593             try {
594                 wait();    // this releases the lock monitor
595             } catch (InterruptedException e) {
596 
597                 /* ignore and resume */
598             }
599         }
600 
601         rowsQueued--;
602 
603         notify();    // notify push's wait
604 
605         if (pendingQueue.size() == 0) {
606             return null;
607         } else {
608             return (TriggerData) pendingQueue.removeFirst();
609         }
610     }
611 
612     /**
613      *  The main thread tells the trigger thread to fire by this call.
614      *  If this Trigger is not threaded then the fire method is called
615      *  immediately and executed by the main thread. Otherwise, the row
616      *  data objects are added to the queue to be used by the Trigger thread.
617      *
618      * @param  row1
619      * @param  row2
620      */
pushPair(Session session, Object[] row1, Object[] row2)621     synchronized void pushPair(Session session, Object[] row1, Object[] row2) {
622 
623         if (maxRowsQueued == 0) {
624             session.getInternalConnection();
625 
626             try {
627                 trigger.fire(triggerType, name.name, table.getName().name,
628                              row1, row2);
629             } finally {
630                 session.releaseInternalConnection();
631             }
632 
633             return;
634         }
635 
636         if (rowsQueued >= maxRowsQueued) {
637             if (nowait) {
638                 pendingQueue.removeLast();    // overwrite last
639             } else {
640                 try {
641                     wait();
642                 } catch (InterruptedException e) {
643 
644                     /* ignore and resume */
645                 }
646 
647                 rowsQueued++;
648             }
649         } else {
650             rowsQueued++;
651         }
652 
653         pendingQueue.add(new TriggerData(session, row1, row2));
654         notify();    // notify pop's wait
655     }
656 
isBusy()657     public boolean isBusy() {
658         return rowsQueued != 0;
659     }
660 
getTable()661     public Table getTable() {
662         return table;
663     }
664 
getActionOrientationString()665     public String getActionOrientationString() {
666         return forEachRow ? Tokens.T_ROW
667                           : Tokens.T_STATEMENT;
668     }
669 
670     /**
671      * Class to store the data used to fire a trigger. The username attribute
672      * is not used but it allows developers to change the signature of the
673      * fire method of the Trigger class and pass the user name to the Trigger.
674      */
675     static class TriggerData {
676 
677         public Object[] oldRow;
678         public Object[] newRow;
679         public String   username;
680 
TriggerData(Session session, Object[] oldRow, Object[] newRow)681         public TriggerData(Session session, Object[] oldRow, Object[] newRow) {
682 
683             this.oldRow   = oldRow;
684             this.newRow   = newRow;
685             this.username = session.getUsername();
686         }
687     }
688 
689     static class DefaultTrigger implements Trigger {
690 
fire(int i, String name, String table, Object[] row1, Object[] row2)691         public void fire(int i, String name, String table, Object[] row1,
692                          Object[] row2) {
693 
694             // do nothing
695         }
696     }
697 }
698