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