001: /**
002: * com.mckoi.database.ConnectionTriggerManager 13 Mar 2003
003: *
004: * Mckoi SQL Database ( http://www.mckoi.com/database )
005: * Copyright (C) 2000, 2001, 2002 Diehl and Associates, Inc.
006: *
007: * This program is free software; you can redistribute it and/or
008: * modify it under the terms of the GNU General Public License
009: * Version 2 as published by the Free Software Foundation.
010: *
011: * This program is distributed in the hope that it will be useful,
012: * but WITHOUT ANY WARRANTY; without even the implied warranty of
013: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
014: * GNU General Public License Version 2 for more details.
015: *
016: * You should have received a copy of the GNU General Public License
017: * Version 2 along with this program; if not, write to the Free Software
018: * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
019: *
020: * Change Log:
021: *
022: *
023: */package com.mckoi.database;
024:
025: import java.io.*;
026: import java.util.ArrayList;
027: import com.mckoi.debug.Lvl;
028: import com.mckoi.util.IntegerVector;
029: import com.mckoi.util.BigNumber;
030:
031: /**
032: * A trigger manager on a DatabaseConnection that maintains a list of all
033: * triggers set in the database, and the types of triggers they are. This
034: * object is closely tied to a DatabaseConnection.
035: * <p>
036: * The trigger manager actually uses a trigger itself to maintain a list of
037: * tables that have triggers, and the action to perform on the trigger.
038: *
039: * @author Tobias Downer
040: */
041:
042: public final class ConnectionTriggerManager {
043:
044: /**
045: * The DatabaseConnection.
046: */
047: private DatabaseConnection connection;
048:
049: /**
050: * The list of triggers currently in view.
051: * (TriggerInfo)
052: */
053: private ArrayList triggers_active;
054:
055: /**
056: * If this is false then the list is not validated and must be refreshed
057: * when we next access trigger information.
058: */
059: private boolean list_validated;
060:
061: /**
062: * True if the trigger table was modified during the last transaction.
063: */
064: private boolean trigger_modified;
065:
066: /**
067: * Constructs the manager.
068: */
069: ConnectionTriggerManager(DatabaseConnection connection) {
070: this .connection = connection;
071: this .triggers_active = new ArrayList();
072: this .list_validated = false;
073: this .trigger_modified = false;
074: // Attach a commit trigger listener
075: connection.attachTableBackedCache(new CTMBackedCache());
076: }
077:
078: /**
079: * Returns a Table object that contains the trigger information with the
080: * given name. Returns an empty table if no trigger found.
081: */
082: private Table findTrigger(QueryContext context, DataTable table,
083: String schema, String name) {
084: // Find all the trigger entries with this name
085: Operator EQUALS = Operator.get("=");
086:
087: Variable schemav = table.getResolvedVariable(0);
088: Variable namev = table.getResolvedVariable(1);
089:
090: Table t = table.simpleSelect(context, namev, EQUALS,
091: new Expression(TObject.stringVal(name)));
092: return t.exhaustiveSelect(context, Expression.simple(schemav,
093: EQUALS, TObject.stringVal(schema)));
094: }
095:
096: /**
097: * Creates a new trigger action on a stored procedure and makes the change
098: * to the transaction of this DatabaseConnection. If the connection is
099: * committed then the trigger is made a perminant change to the database.
100: *
101: * @param schema the schema name of the trigger.
102: * @param name the name of the trigger.
103: * @param type the type of trigger.
104: * @param procedure_name the name of the procedure to execute.
105: * @param params any constant parameters for the triggering procedure.
106: */
107: public void createTableTrigger(String schema, String name,
108: int type, TableName on_table, String procedure_name,
109: TObject[] params) throws DatabaseException {
110:
111: TableName trigger_table_name = new TableName(schema, name);
112:
113: // Check this name is not reserved
114: DatabaseConnection.checkAllowCreate(trigger_table_name);
115:
116: // Before adding the trigger, make sure this name doesn't already resolve
117: // to an object in the database with this schema/name.
118: if (!connection.tableExists(trigger_table_name)) {
119:
120: // Encode the parameters
121: ByteArrayOutputStream bout = new ByteArrayOutputStream();
122: try {
123: ObjectOutputStream ob_out = new ObjectOutputStream(bout);
124: ob_out.writeInt(1); // version
125: ob_out.writeObject(params);
126: ob_out.flush();
127: } catch (IOException e) {
128: throw new RuntimeException("IO Error: "
129: + e.getMessage());
130: }
131: byte[] encoded_params = bout.toByteArray();
132:
133: // Insert the entry into the trigger table,
134: DataTable table = connection
135: .getTable(Database.SYS_DATA_TRIGGER);
136: RowData row = new RowData(table);
137: row.setColumnDataFromTObject(0, TObject.stringVal(schema));
138: row.setColumnDataFromTObject(1, TObject.stringVal(name));
139: row.setColumnDataFromTObject(2, TObject.intVal(type));
140: row.setColumnDataFromTObject(3, TObject.stringVal("T:"
141: + on_table.toString()));
142: row.setColumnDataFromTObject(4, TObject
143: .stringVal(procedure_name));
144: row.setColumnDataFromTObject(5, TObject
145: .objectVal(encoded_params));
146: row.setColumnDataFromTObject(6, TObject
147: .stringVal(connection.getUser().getUserName()));
148: table.add(row);
149:
150: // Invalidate the list
151: invalidateTriggerList();
152:
153: // Notify that this database object has been successfully created.
154: connection.databaseObjectCreated(trigger_table_name);
155:
156: // Flag that this transaction modified the trigger table.
157: trigger_modified = true;
158: } else {
159: throw new RuntimeException("Trigger name '" + schema + "."
160: + name + "' already in use.");
161: }
162: }
163:
164: /**
165: * Drops a trigger that has previously been defined.
166: */
167: public void dropTrigger(String schema, String name)
168: throws DatabaseException {
169: QueryContext context = new DatabaseQueryContext(connection);
170: DataTable table = connection
171: .getTable(Database.SYS_DATA_TRIGGER);
172:
173: // Find the trigger
174: Table t = findTrigger(context, table, schema, name);
175:
176: if (t.getRowCount() == 0) {
177: throw new StatementException("Trigger '" + schema + "."
178: + name + "' not found.");
179: } else if (t.getRowCount() > 1) {
180: throw new RuntimeException(
181: "Assertion failed: multiple entries for the same trigger name.");
182: } else {
183: // Drop this trigger,
184: table.delete(t);
185:
186: // Notify that this database object has been successfully dropped.
187: connection
188: .databaseObjectDropped(new TableName(schema, name));
189:
190: // Flag that this transaction modified the trigger table.
191: trigger_modified = true;
192: }
193:
194: }
195:
196: /**
197: * Returns true if the trigger exists, false otherwise.
198: */
199: public boolean triggerExists(String schema, String name) {
200: QueryContext context = new DatabaseQueryContext(connection);
201: DataTable table = connection
202: .getTable(Database.SYS_DATA_TRIGGER);
203:
204: // Find the trigger
205: Table t = findTrigger(context, table, schema, name);
206:
207: if (t.getRowCount() == 0) {
208: // Trigger wasn't found
209: return false;
210: } else if (t.getRowCount() > 1) {
211: throw new RuntimeException(
212: "Assertion failed: multiple entries for the same trigger name.");
213: } else {
214: // Trigger found
215: return true;
216: }
217: }
218:
219: /**
220: * Invalidates the trigger list causing the list to rebuild when a potential
221: * triggering event next occurs.
222: * <p>
223: * NOTE: must only be called from the thread that owns the
224: * DatabaseConnection.
225: */
226: private void invalidateTriggerList() {
227: list_validated = false;
228: triggers_active.clear();
229: }
230:
231: /**
232: * Build the trigger list if it is not validated.
233: */
234: private void buildTriggerList() {
235: if (!list_validated) {
236: // Cache the trigger table
237: DataTable table = connection
238: .getTable(Database.SYS_DATA_TRIGGER);
239: RowEnumeration e = table.rowEnumeration();
240:
241: // For each row
242: while (e.hasMoreRows()) {
243: int row_index = e.nextRowIndex();
244:
245: TObject trig_schem = table
246: .getCellContents(0, row_index);
247: TObject trig_name = table.getCellContents(1, row_index);
248: TObject type = table.getCellContents(2, row_index);
249: TObject on_object = table.getCellContents(3, row_index);
250: TObject action = table.getCellContents(4, row_index);
251: TObject misc = table.getCellContents(5, row_index);
252:
253: TriggerInfo trigger_info = new TriggerInfo();
254: trigger_info.schema = trig_schem.getObject().toString();
255: trigger_info.name = trig_name.getObject().toString();
256: trigger_info.type = type.toBigNumber().intValue();
257: trigger_info.on_object = on_object.getObject()
258: .toString();
259: trigger_info.action = action.getObject().toString();
260: trigger_info.misc = misc;
261:
262: // Add to the list
263: triggers_active.add(trigger_info);
264: }
265:
266: list_validated = true;
267: }
268: }
269:
270: /**
271: * Performs any trigger action for this event. For example, if we have it
272: * setup so a trigger fires when there is an INSERT event on table x then
273: * we perform the triggering procedure right here.
274: */
275: void performTriggerAction(TableModificationEvent evt) {
276: // REINFORCED NOTE: The 'tableExists' call is REALLY important. First it
277: // makes sure the transaction on the connection is established (it should
278: // be anyway if a trigger is firing), and it also makes sure the trigger
279: // table exists - which it may not be during database init.
280: if (connection.tableExists(Database.SYS_DATA_TRIGGER)) {
281: // If the trigger list isn't built, then do so now
282: buildTriggerList();
283:
284: // On object value to test for,
285: TableName table_name = evt.getTableName();
286: String on_ob_test = "T:" + table_name.toString();
287:
288: // Search the triggers list for an event that matches this event
289: int sz = triggers_active.size();
290: for (int i = 0; i < sz; ++i) {
291: TriggerInfo t_info = (TriggerInfo) triggers_active
292: .get(i);
293: if (t_info.on_object.equals(on_ob_test)) {
294: // Table name matches
295: // Do the types match? eg. before/after match, and
296: // insert/delete/update is being listened to.
297: if (evt.listenedBy(t_info.type)) {
298: // Type matches this trigger, so we need to fire it
299: // Parse the action string
300: String action = t_info.action;
301: // Get the procedure name to fire (qualify it against the schema
302: // of the table being fired).
303: ProcedureName procedure_name = ProcedureName
304: .qualify(table_name.getSchema(), action);
305: // Set up OLD and NEW tables
306:
307: // Record the old table state
308: DatabaseConnection.OldNewTableState current_state = connection
309: .getOldNewTableState();
310:
311: // Set the new table state
312: // If an INSERT event then we setup NEW to be the row being inserted
313: // If an DELETE event then we setup OLD to be the row being deleted
314: // If an UPDATE event then we setup NEW to be the row after the
315: // update, and OLD to be the row before the update.
316: connection
317: .setOldNewTableState(new DatabaseConnection.OldNewTableState(
318: table_name, evt.getRowIndex(),
319: evt.getRowData(), evt
320: .isBefore()));
321:
322: try {
323: // Invoke the procedure (no arguments)
324: connection.getProcedureManager()
325: .invokeProcedure(procedure_name,
326: new TObject[0]);
327: } finally {
328: // Reset the OLD and NEW tables to previous values
329: connection
330: .setOldNewTableState(current_state);
331: }
332:
333: }
334:
335: }
336:
337: } // for each trigger
338:
339: }
340:
341: }
342:
343: /**
344: * Returns an InternalTableInfo object used to model the list of triggers
345: * that are accessible within the given Transaction object. This is used to
346: * model all triggers that have been defined as tables.
347: */
348: static InternalTableInfo createInternalTableInfo(
349: Transaction transaction) {
350: return new TriggerInternalTableInfo(transaction);
351: }
352:
353: // ---------- Inner classes ----------
354:
355: /**
356: * A TableBackedCache that manages the list of connection level triggers that
357: * are currently active on this connection.
358: */
359: private class CTMBackedCache extends TableBackedCache {
360:
361: /**
362: * Constructor.
363: */
364: public CTMBackedCache() {
365: super (Database.SYS_DATA_TRIGGER);
366: }
367:
368: public void purgeCacheOfInvalidatedEntries(
369: IntegerVector added_rows, IntegerVector removed_rows) {
370: // Note that this is called when a transaction is started or stopped.
371:
372: // If the trigger table was modified, we need to invalidate the trigger
373: // list. This covers the case when we rollback a trigger table change
374: if (trigger_modified) {
375: invalidateTriggerList();
376: trigger_modified = false;
377: }
378: // If any data has been committed removed then completely flush the
379: // cache.
380: else if ((removed_rows != null && removed_rows.size() > 0)
381: || (added_rows != null && added_rows.size() > 0)) {
382: invalidateTriggerList();
383: }
384: }
385:
386: }
387:
388: /**
389: * Container class for all trigger actions defined on the database.
390: */
391: private class TriggerInfo {
392: String schema;
393: String name;
394: int type;
395: String on_object;
396: String action;
397: TObject misc;
398: }
399:
400: /**
401: * An object that models the list of triggers as table objects in a
402: * transaction.
403: */
404: private static class TriggerInternalTableInfo extends
405: AbstractInternalTableInfo2 {
406:
407: TriggerInternalTableInfo(Transaction transaction) {
408: super (transaction, Database.SYS_DATA_TRIGGER);
409: }
410:
411: private static DataTableDef createDataTableDef(String schema,
412: String name) {
413: // Create the DataTableDef that describes this entry
414: DataTableDef def = new DataTableDef();
415: def.setTableName(new TableName(schema, name));
416:
417: // Add column definitions
418: def.addColumn(DataTableColumnDef
419: .createNumericColumn("type"));
420: def.addColumn(DataTableColumnDef
421: .createStringColumn("on_object"));
422: def.addColumn(DataTableColumnDef
423: .createStringColumn("procedure_name"));
424: def.addColumn(DataTableColumnDef
425: .createStringColumn("param_args"));
426: def.addColumn(DataTableColumnDef
427: .createStringColumn("owner"));
428:
429: // Set to immutable
430: def.setImmutable();
431:
432: // Return the data table def
433: return def;
434: }
435:
436: public String getTableType(int i) {
437: return "TRIGGER";
438: }
439:
440: public DataTableDef getDataTableDef(int i) {
441: TableName table_name = getTableName(i);
442: return createDataTableDef(table_name.getSchema(),
443: table_name.getName());
444: }
445:
446: public MutableTableDataSource createInternalTable(int index) {
447: MutableTableDataSource table = transaction
448: .getTable(Database.SYS_DATA_TRIGGER);
449: RowEnumeration row_e = table.rowEnumeration();
450: int p = 0;
451: int i;
452: int row_i = -1;
453: while (row_e.hasMoreRows()) {
454: i = row_e.nextRowIndex();
455: if (p == index) {
456: row_i = i;
457: } else {
458: ++p;
459: }
460: }
461: if (p == index) {
462: String schema = table.getCellContents(0, row_i)
463: .getObject().toString();
464: String name = table.getCellContents(1, row_i)
465: .getObject().toString();
466:
467: final DataTableDef table_def = createDataTableDef(
468: schema, name);
469: final TObject type = table.getCellContents(2, row_i);
470: final TObject on_object = table.getCellContents(3,
471: row_i);
472: final TObject procedure_name = table.getCellContents(4,
473: row_i);
474: final TObject param_args = table.getCellContents(5,
475: row_i);
476: final TObject owner = table.getCellContents(6, row_i);
477:
478: // Implementation of MutableTableDataSource that describes this
479: // trigger.
480: return new GTDataSource(transaction.getSystem()) {
481: public DataTableDef getDataTableDef() {
482: return table_def;
483: }
484:
485: public int getRowCount() {
486: return 1;
487: }
488:
489: public TObject getCellContents(int col, int row) {
490: switch (col) {
491: case 0:
492: return type;
493: case 1:
494: return on_object;
495: case 2:
496: return procedure_name;
497: case 3:
498: return param_args;
499: case 4:
500: return owner;
501: default:
502: throw new RuntimeException(
503: "Column out of bounds.");
504: }
505: }
506: };
507:
508: } else {
509: throw new RuntimeException("Index out of bounds.");
510: }
511:
512: }
513:
514: }
515:
516: }
|