/* Copyright (c) 2001-2010, 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.sample; import java.io.PrintWriter; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.sql.Statement; import org.hsqldb.error.Error; import org.hsqldb.error.ErrorCode; import org.hsqldb.Trigger; import org.hsqldb.lib.StringUtil; // peterhudson@users 20020130 - patch 478657 by peterhudson - new class // fredt@users 20030727 - signature altered // boucherb@users 20040315 - sample updated /** * <P>Sample code for use of triggers in hsqldb. * * This class org.hsqldb.sample package, but a typical implementation is in * users's class hierarchy. * * SQL to invoke is:<p> * CREATE TRIGGER triggerSample BEFORE|AFTER INSERT|UPDATE|DELETE * ON myTable [FOR EACH ROW] [QUEUE n] [NOWAIT] CALL "myPackage.trigClass"<br> * * This will create a thread that will wait for its firing event to occur; * when this happens, the trigger's thread runs the 'trigClass.fire' * Note that this is still in the same Java Virtual Machine as the * database, so make sure the fired method does not hang.<p> * * There is a queue of events waiting to be run by each trigger thread. * This is particularly useful for 'FOR EACH ROW' triggers, when a large * number of trigger events occur in rapid succession, without the trigger * thread getting a chance to run. If the queue becomes full, subsequent * additions to it cause the database engine to suspend awaiting space * in the queue. Take great care to avoid this situation if the trigger * action involves accessing the database, as deadlock will occur. * This can be avoided either by ensuring the QUEUE parameter makes a large * enough queue, or by using the NOWAIT parameter, which causes a new * trigger event to overwrite the most recent event in the queue. * The default queue size is 1024.<p> * * Ensure that "myPackage.trigClass" is present in the classpath which * you use to start hsql.<p> * * If the method wants to access the database, it must establish * a JDBC connection.<p> * * When the 'fire' method is called, it is passed the following arguments: <p> * * fire (int type, String trigName, String tabName, Object oldRow[], * Object[] newRow) <p> * * where 'type' is one of the values enumerated in the Trigger interface and * the 'oldRow'/'newRow' pair represents the rows acted on. The first * length - 1 array slots contain column values and the final slot contains * either null or the value of the internally assigned row identity, if * the concerned table has no primary key. The final slot must _never_ be * modified. <p> * * The mapping of row classes to database types is specified in * /doc/hsqlSyntax.html#Datatypes. <p> * * To be done:<p> * * <ol> * <li> Implement the "jdbc:default:connection: URL to provide transparent * and portable access to internal connections for use in triggers and * stored procedures. <p> * * <li> Implement declaritive column to trigger method argument * mapping, conditional execution (WHEN clause), etc. <p> * * <li> Investigate and refine synchronous and asynchronous trigger models. <p> * * Because certain combinations of trigger create parameters cause the * individual triggered actions of a multirow update to run in different * threads, it is possible for an 'after' trigger to run before its * corresponding 'before' trigger; the acceptability and implications * of this needs to be investigated, documented and the behaviour of * the engine fully specified. * * <li> Investigate and implement the SQL 200n specified execution stack under * arbitrary triggered action and SQL-invoked routine call graphs. * </ol> * * @author Peter Hudson * @author boucherb@users * @version 1.7.2 * @since 1.7.0 */ public class TriggerSample implements Trigger { static final PrintWriter out = new PrintWriter(System.out); static final String drv = "org.hsqldb.jdbc.JDBCDriver"; static final String url = "jdbc:hsqldb:mem:trigger-sample"; static final String usr = "SA"; static final String pwd = ""; static final String impl = TriggerSample.class.getName(); static final String tn = "trig_test"; static final String drop_test_table_stmt = "DROP TABLE " + tn + " IF EXISTS"; static final String create_test_table_stmt = "CREATE TABLE " + tn + "(id INTEGER PRIMARY KEY, value VARCHAR(20))"; static final String drop_audit_table_stmt = "DROP TABLE audit IF EXISTS"; static final String create_audit_table_stmt = "CREATE TABLE audit(" + "id INTEGER GENERATED BY DEFAULT AS IDENTITY(START WITH 1), " + "op VARCHAR(6), " + "tn VARCHAR(20), " + "ors LONGVARCHAR, " + "nrs LONGVARCHAR, " + "ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP)"; static final String audit_insert_stmt = "INSERT INTO audit(op, tn, ors, nrs) VALUES(?, ?, ?, ?)"; /** * A sample HSQLDB Trigger interface implementation. <p> * * This sample prints information about the firing trigger and records * actions in an audit table. <p> * * The techniques used here are simplified dramatically for demonstration * purposes and are in no way recommended as a model upon which to build * actual installations involving triggered actions. * * @param typ trigger type * @param trn trigger name * @param tn table name * @param or old row * @param nr new row */ public void fire(int typ, String trn, String tn, Object[] or, Object[] nr) { synchronized (TriggerSample.class) { String ors = or == null ? "null" : StringUtil.arrayToString(or); String nrs = nr == null ? "null" : StringUtil.arrayToString(nr); out.println("----------------------------------------"); out.println(getTriggerDescriptor(trn, typ, tn)); out.println("old row : " + ors); out.println("new row : " + nrs); out.flush(); if ("TRIG_TEST".equals(tn)) { switch (typ) { case INSERT_BEFORE_ROW : { // Business rule: ID shall be less than 11. // (Marti DiBergi, we love you ;-) // You can cast row[i] given your knowledge of what // the table format is: final int ID = ((Number) nr[0]).intValue(); doAssert(ID < 11, "ID < 11"); break; } case UPDATE_BEFORE_ROW : { // Business rule: ignore update of VALUE 'unchangable'. if ("unchangable".equals(or[1])) { nr[1] = or[1]; // short-circuit the update } // !!!Warning!!! // The engine does not check the class of substituted // values; it's up to you to use the correct class. // For example, this will cause database curruption: // nr[1] = new Integer(5); break; } } } doAuditStep(typ, tn, ors, nrs); } } private static void doAssert(boolean b, String msg) { if (b) { // do nothing } else { throw org.hsqldb.error.Error.error(ErrorCode.GENERAL_ERROR, msg); } } private static void doAuditStep(int typ, String tn, String ors, String nrs) { Connection conn; PreparedStatement stmt; switch (typ) { case INSERT_AFTER_ROW : case UPDATE_AFTER_ROW : case DELETE_AFTER_ROW : { try { conn = getConnection(); stmt = conn.prepareStatement(audit_insert_stmt); stmt.setString(1, getOperationSpec(typ)); stmt.setString(2, tn); stmt.setString(3, ors); stmt.setString(4, nrs); stmt.executeUpdate(); conn.close(); } catch (SQLException se) { se.printStackTrace(); } } } } public static String getWhenSpec(int type) { switch (type) { case INSERT_BEFORE_ROW : case UPDATE_BEFORE_ROW : case DELETE_BEFORE_ROW : { return "BEFORE"; } case INSERT_AFTER : case INSERT_AFTER_ROW : case UPDATE_AFTER : case UPDATE_AFTER_ROW : case DELETE_AFTER : case DELETE_AFTER_ROW : { return "AFTER"; } default : { return ""; } } } public static String getOperationSpec(int type) { switch (type) { case INSERT_AFTER : case INSERT_AFTER_ROW : case INSERT_BEFORE_ROW : { return "INSERT"; } case UPDATE_AFTER : case UPDATE_AFTER_ROW : case UPDATE_BEFORE_ROW : { return "UPDATE"; } case DELETE_AFTER : case DELETE_AFTER_ROW : case DELETE_BEFORE_ROW : { return "DELETE"; } default : { return ""; } } } public static String getQueueSpec(int qs) { return (qs < 0) ? "" : ("QUEUE " + qs); } public static String getForEachSpec(int type) { switch (type) { case INSERT_BEFORE_ROW : case INSERT_AFTER_ROW : case UPDATE_BEFORE_ROW : case UPDATE_AFTER_ROW : case DELETE_AFTER_ROW : case DELETE_BEFORE_ROW : { return "FOR EACH ROW"; } default : { return "FOR EACH STATEMENT"; } } } public static String getTriggerDDL(String trn, int typ, String tab, int qs, String impl) throws SQLException { StringBuffer sb = new StringBuffer(); sb.append("CREATE TRIGGER "); sb.append(trn); sb.append(' '); sb.append(getWhenSpec(typ)); sb.append(' '); sb.append(getOperationSpec(typ)); sb.append(" ON "); sb.append(tab); sb.append(' '); sb.append(getForEachSpec(typ)); sb.append(' '); sb.append(getQueueSpec(qs)); sb.append(" CALL \""); sb.append(impl); sb.append("\""); return sb.toString(); } public static String getTriggerDescriptor(String trn, int typ, String tab) { StringBuffer sb = new StringBuffer(); sb.append("TRIGGER : "); sb.append(trn); sb.append(' '); sb.append(getWhenSpec(typ)); sb.append(' '); sb.append(getOperationSpec(typ)); sb.append(" ON "); sb.append(tab); sb.append(' '); sb.append(getForEachSpec(typ)); return sb.toString(); } private static Connection getConnection() throws SQLException { try { Class.forName(drv).newInstance(); return DriverManager.getConnection(url, usr, pwd); } catch (SQLException se) { throw se; } catch (Exception e) { throw new SQLException(e.toString()); } } private static void createTrigger(Statement stmt, String trn, int typ) throws SQLException { stmt.execute(getTriggerDDL(trn, typ, tn, 0, impl)); } private static void setup() throws SQLException { Connection conn = getConnection(); Statement stmt = conn.createStatement(); stmt.execute(drop_test_table_stmt); stmt.execute(create_test_table_stmt); stmt.execute(drop_audit_table_stmt); stmt.execute(create_audit_table_stmt); createTrigger(stmt, "tibr_" + tn, INSERT_BEFORE_ROW); createTrigger(stmt, "tia_" + tn, INSERT_AFTER); createTrigger(stmt, "tiar_" + tn, INSERT_AFTER_ROW); createTrigger(stmt, "tubr_" + tn, UPDATE_BEFORE_ROW); createTrigger(stmt, "tua_" + tn, UPDATE_AFTER); createTrigger(stmt, "tuar_" + tn, UPDATE_AFTER_ROW); createTrigger(stmt, "tdbr_" + tn, DELETE_BEFORE_ROW); createTrigger(stmt, "tda_" + tn, DELETE_AFTER); createTrigger(stmt, "tdar_" + tn, DELETE_AFTER_ROW); stmt.close(); conn.close(); } private static void doSomeWork() throws SQLException { Connection conn = getConnection(); Statement stmt = conn.createStatement(); conn.setAutoCommit(false); stmt.execute("INSERT INTO trig_test VALUES (1, 'hello')"); stmt.execute("INSERT INTO trig_test VALUES (2, 'now what?')"); stmt.execute("INSERT INTO trig_test VALUES (3, 'unchangable')"); stmt.execute("INSERT INTO trig_test VALUES (4, 'goodbye')"); conn.commit(); dumpTable("trig_test"); stmt.execute("UPDATE trig_test SET value = 'all done'"); conn.commit(); dumpTable("trig_test"); stmt.execute("DELETE FROM trig_test"); conn.rollback(); dumpTable("trig_test"); try { stmt.execute("INSERT INTO trig_test VALUES(11, 'whatever')"); } catch (SQLException se) { se.printStackTrace(); } stmt.execute("INSERT INTO trig_test VALUES(10, 'whatever')"); conn.commit(); dumpTable("trig_test"); stmt.close(); conn.close(); } private static void dumpTable(String tn) throws SQLException { Connection conn = getConnection(); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery("select * from " + tn); ResultSetMetaData rsmd = rs.getMetaData(); int count = rsmd.getColumnCount(); out.println(); out.println("****************************************"); out.println("DUMP FOR TABLE: " + tn); out.println("****************************************"); out.flush(); while (rs.next()) { out.print("["); for (int i = 1; i <= count; i++) { out.print(rs.getString(i)); if (i < count) { out.print(" : "); } } out.println("]"); } out.println(); out.flush(); rs.close(); stmt.close(); conn.close(); } private static void runSample() throws SQLException { setup(); doSomeWork(); dumpTable("audit"); } public static void main(String[] args) throws SQLException { runSample(); } } /* test SQL CREATE CACHED TABLE trig_test (int_field integer) CREATE TRIGGER ins_before BEFORE INSERT ON trig_test CALL "org.hsqldb.sample.TriggerSample" CREATE TRIGGER ins_after AFTER INSERT ON trig_test CALL "org.hsqldb.sample.TriggerSample" CREATE TRIGGER upd_before BEFORE UPDATE ON trig_test CALL "org.hsqldb.sample.TriggerSample" CREATE TRIGGER upd_after AFTER UPDATE ON trig_test CALL "org.hsqldb.sample.TriggerSample" CREATE TRIGGER upd_before_row BEFORE UPDATE ON trig_test FOR EACH ROW CALL "org.hsqldb.sample.TriggerSample" CREATE TRIGGER upd_after_row AFTER UPDATE ON trig_test FOR EACH ROW CALL "org.hsqldb.sample.TriggerSample" CREATE TRIGGER del_before BEFORE DELETE ON trig_test CALL "org.hsqldb.sample.TriggerSample" CREATE TRIGGER del_after AFTER DELETE ON trig_test CALL "org.hsqldb.sample.TriggerSample" CREATE TRIGGER del_before_row BEFORE DELETE ON trig_test FOR EACH ROW CALL "org.hsqldb.sample.TriggerSample" CREATE TRIGGER del_after_row AFTER DELETE ON trig_test FOR EACH ROW CALL "org.hsqldb.sample.TriggerSample" INSERT INTO trig_test VALUES (1) INSERT INTO trig_test VALUES (2) INSERT INTO trig_test VALUES (3) UPDATE trig_test SET int_field = int_field + 3 DELETE FROM trig_test */