KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > com > mckoi > database > ConnectionTriggerManager


1 /**
2  * com.mckoi.database.ConnectionTriggerManager 13 Mar 2003
3  *
4  * Mckoi SQL Database ( http://www.mckoi.com/database )
5  * Copyright (C) 2000, 2001, 2002 Diehl and Associates, Inc.
6  *
7  * This program is free software; you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License
9  * Version 2 as published by the Free Software Foundation.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14  * GNU General Public License Version 2 for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * Version 2 along with this program; if not, write to the Free Software
18  * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
19  *
20  * Change Log:
21  *
22  *
23  */

24
25 package com.mckoi.database;
26
27 import java.io.*;
28 import java.util.ArrayList JavaDoc;
29 import com.mckoi.debug.Lvl;
30 import com.mckoi.util.IntegerVector;
31 import com.mckoi.util.BigNumber;
32
33 /**
34  * A trigger manager on a DatabaseConnection that maintains a list of all
35  * triggers set in the database, and the types of triggers they are. This
36  * object is closely tied to a DatabaseConnection.
37  * <p>
38  * The trigger manager actually uses a trigger itself to maintain a list of
39  * tables that have triggers, and the action to perform on the trigger.
40  *
41  * @author Tobias Downer
42  */

43
44 public final class ConnectionTriggerManager {
45
46   /**
47    * The DatabaseConnection.
48    */

49   private DatabaseConnection connection;
50
51   /**
52    * The list of triggers currently in view.
53    * (TriggerInfo)
54    */

55   private ArrayList JavaDoc triggers_active;
56
57   /**
58    * If this is false then the list is not validated and must be refreshed
59    * when we next access trigger information.
60    */

61   private boolean list_validated;
62   
63   /**
64    * True if the trigger table was modified during the last transaction.
65    */

66   private boolean trigger_modified;
67   
68   /**
69    * Constructs the manager.
70    */

71   ConnectionTriggerManager(DatabaseConnection connection) {
72     this.connection = connection;
73     this.triggers_active = new ArrayList JavaDoc();
74     this.list_validated = false;
75     this.trigger_modified = false;
76     // Attach a commit trigger listener
77
connection.attachTableBackedCache(new CTMBackedCache());
78   }
79
80   /**
81    * Returns a Table object that contains the trigger information with the
82    * given name. Returns an empty table if no trigger found.
83    */

84   private Table findTrigger(QueryContext context, DataTable table,
85                             String JavaDoc schema, String JavaDoc name) {
86     // Find all the trigger entries with this name
87
Operator EQUALS = Operator.get("=");
88
89     Variable schemav = table.getResolvedVariable(0);
90     Variable namev = table.getResolvedVariable(1);
91
92     Table t = table.simpleSelect(context, namev, EQUALS,
93                                  new Expression(TObject.stringVal(name)));
94     return t.exhaustiveSelect(context, Expression.simple(
95                               schemav, EQUALS, TObject.stringVal(schema)));
96   }
97
98   /**
99    * Creates a new trigger action on a stored procedure and makes the change
100    * to the transaction of this DatabaseConnection. If the connection is
101    * committed then the trigger is made a perminant change to the database.
102    *
103    * @param schema the schema name of the trigger.
104    * @param name the name of the trigger.
105    * @param type the type of trigger.
106    * @param procedure_name the name of the procedure to execute.
107    * @param params any constant parameters for the triggering procedure.
108    */

109   public void createTableTrigger(String JavaDoc schema, String JavaDoc name,
110                                  int type, TableName on_table,
111                                  String JavaDoc procedure_name, TObject[] params)
112                                                      throws DatabaseException {
113
114     TableName trigger_table_name = new TableName(schema, name);
115
116     // Check this name is not reserved
117
DatabaseConnection.checkAllowCreate(trigger_table_name);
118
119     // Before adding the trigger, make sure this name doesn't already resolve
120
// to an object in the database with this schema/name.
121
if (!connection.tableExists(trigger_table_name)) {
122
123       // Encode the parameters
124
ByteArrayOutputStream bout = new ByteArrayOutputStream();
125       try {
126         ObjectOutputStream ob_out = new ObjectOutputStream(bout);
127         ob_out.writeInt(1); // version
128
ob_out.writeObject(params);
129         ob_out.flush();
130       }
131       catch (IOException e) {
132         throw new RuntimeException JavaDoc("IO Error: " + e.getMessage());
133       }
134       byte[] encoded_params = bout.toByteArray();
135       
136       // Insert the entry into the trigger table,
137
DataTable table = connection.getTable(Database.SYS_DATA_TRIGGER);
138       RowData row = new RowData(table);
139       row.setColumnDataFromTObject(0, TObject.stringVal(schema));
140       row.setColumnDataFromTObject(1, TObject.stringVal(name));
141       row.setColumnDataFromTObject(2, TObject.intVal(type));
142       row.setColumnDataFromTObject(3,
143                                 TObject.stringVal("T:" + on_table.toString()));
144       row.setColumnDataFromTObject(4, TObject.stringVal(procedure_name));
145       row.setColumnDataFromTObject(5, TObject.objectVal(encoded_params));
146       row.setColumnDataFromTObject(6,
147                         TObject.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     }
159     else {
160       throw new RuntimeException JavaDoc("Trigger name '" + schema + "." + name +
161                                  "' already in use.");
162     }
163   }
164
165   /**
166    * Drops a trigger that has previously been defined.
167    */

168   public void dropTrigger(String JavaDoc schema, String JavaDoc name) throws DatabaseException {
169     QueryContext context = new DatabaseQueryContext(connection);
170     DataTable table = connection.getTable(Database.SYS_DATA_TRIGGER);
171
172     // Find the trigger
173
Table t = findTrigger(context, table, schema, name);
174     
175     if (t.getRowCount() == 0) {
176       throw new StatementException("Trigger '" + schema + "." + name +
177                                    "' not found.");
178     }
179     else if (t.getRowCount() > 1) {
180       throw new RuntimeException JavaDoc(
181              "Assertion failed: multiple entries for the same trigger name.");
182     }
183     else {
184       // Drop this trigger,
185
table.delete(t);
186
187       // Notify that this database object has been successfully dropped.
188
connection.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 JavaDoc schema, String JavaDoc name) {
200     QueryContext context = new DatabaseQueryContext(connection);
201     DataTable table = connection.getTable(Database.SYS_DATA_TRIGGER);
202
203     // Find the trigger
204
Table t = findTrigger(context, table, schema, name);
205     
206     if (t.getRowCount() == 0) {
207       // Trigger wasn't found
208
return false;
209     }
210     else if (t.getRowCount() > 1) {
211       throw new RuntimeException JavaDoc(
212              "Assertion failed: multiple entries for the same trigger name.");
213     }
214     else {
215       // Trigger found
216
return true;
217     }
218   }
219
220   /**
221    * Invalidates the trigger list causing the list to rebuild when a potential
222    * triggering event next occurs.
223    * <p>
224    * NOTE: must only be called from the thread that owns the
225    * DatabaseConnection.
226    */

227   private void invalidateTriggerList() {
228     list_validated = false;
229     triggers_active.clear();
230   }
231   
232   /**
233    * Build the trigger list if it is not validated.
234    */

235   private void buildTriggerList() {
236     if (!list_validated) {
237       // Cache the trigger table
238
DataTable table = connection.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.getCellContents(0, row_index);
246         TObject trig_name = table.getCellContents(1, row_index);
247         TObject type = table.getCellContents(2, row_index);
248         TObject on_object = table.getCellContents(3, row_index);
249         TObject action = table.getCellContents(4, row_index);
250         TObject misc = table.getCellContents(5, row_index);
251
252         TriggerInfo trigger_info = new TriggerInfo();
253         trigger_info.schema = trig_schem.getObject().toString();
254         trigger_info.name = trig_name.getObject().toString();
255         trigger_info.type = type.toBigNumber().intValue();
256         trigger_info.on_object = on_object.getObject().toString();
257         trigger_info.action = action.getObject().toString();
258         trigger_info.misc = misc;
259
260         // Add to the list
261
triggers_active.add(trigger_info);
262       }
263
264       list_validated = true;
265     }
266   }
267   
268   /**
269    * Performs any trigger action for this event. For example, if we have it
270    * setup so a trigger fires when there is an INSERT event on table x then
271    * we perform the triggering procedure right here.
272    */

273   void performTriggerAction(TableModificationEvent evt) {
274     // REINFORCED NOTE: The 'tableExists' call is REALLY important. First it
275
// makes sure the transaction on the connection is established (it should
276
// be anyway if a trigger is firing), and it also makes sure the trigger
277
// table exists - which it may not be during database init.
278
if (connection.tableExists(Database.SYS_DATA_TRIGGER)) {
279       // If the trigger list isn't built, then do so now
280
buildTriggerList();
281
282       // On object value to test for,
283
TableName table_name = evt.getTableName();
284       String JavaDoc on_ob_test = "T:" + table_name.toString();
285       
286       // Search the triggers list for an event that matches this event
287
int sz = triggers_active.size();
288       for (int i = 0; i < sz; ++i) {
289         TriggerInfo t_info = (TriggerInfo) triggers_active.get(i);
290         if (t_info.on_object.equals(on_ob_test)) {
291           // Table name matches
292
// Do the types match? eg. before/after match, and
293
// insert/delete/update is being listened to.
294
if (evt.listenedBy(t_info.type)) {
295             // Type matches this trigger, so we need to fire it
296
// Parse the action string
297
String JavaDoc action = t_info.action;
298             // Get the procedure name to fire (qualify it against the schema
299
// of the table being fired).
300
ProcedureName procedure_name =
301                      ProcedureName.qualify(table_name.getSchema(), action);
302             // Set up OLD and NEW tables
303

304             // Record the old table state
305
DatabaseConnection.OldNewTableState current_state =
306                                        connection.getOldNewTableState();
307
308             // Set the new table state
309
// If an INSERT event then we setup NEW to be the row being inserted
310
// If an DELETE event then we setup OLD to be the row being deleted
311
// If an UPDATE event then we setup NEW to be the row after the
312
// update, and OLD to be the row before the update.
313
connection.setOldNewTableState(
314                  new DatabaseConnection.OldNewTableState(table_name,
315                          evt.getRowIndex(), evt.getRowData(), evt.isBefore()));
316             
317             try {
318               // Invoke the procedure (no arguments)
319
connection.getProcedureManager().invokeProcedure(
320                                                procedure_name, new TObject[0]);
321             }
322             finally {
323               // Reset the OLD and NEW tables to previous values
324
connection.setOldNewTableState(current_state);
325             }
326
327           }
328
329         }
330
331       } // for each trigger
332

333     }
334
335   }
336
337   /**
338    * Returns an InternalTableInfo object used to model the list of triggers
339    * that are accessible within the given Transaction object. This is used to
340    * model all triggers that have been defined as tables.
341    */

342   static InternalTableInfo createInternalTableInfo(Transaction transaction) {
343     return new TriggerInternalTableInfo(transaction);
344   }
345   
346   // ---------- Inner classes ----------
347

348   /**
349    * A TableBackedCache that manages the list of connection level triggers that
350    * are currently active on this connection.
351    */

352   private class CTMBackedCache extends TableBackedCache {
353
354     /**
355      * Constructor.
356      */

357     public CTMBackedCache() {
358       super(Database.SYS_DATA_TRIGGER);
359     }
360
361     public void purgeCacheOfInvalidatedEntries(
362                         IntegerVector added_rows, IntegerVector removed_rows) {
363       // Note that this is called when a transaction is started or stopped.
364

365       // If the trigger table was modified, we need to invalidate the trigger
366
// list. This covers the case when we rollback a trigger table change
367
if (trigger_modified) {
368         invalidateTriggerList();
369         trigger_modified = false;
370       }
371       // If any data has been committed removed then completely flush the
372
// cache.
373
else if ((removed_rows != null && removed_rows.size() > 0) ||
374                (added_rows != null && added_rows.size() > 0)) {
375         invalidateTriggerList();
376       }
377     }
378
379   }
380
381   /**
382    * Container class for all trigger actions defined on the database.
383    */

384   private class TriggerInfo {
385     String JavaDoc schema;
386     String JavaDoc name;
387     int type;
388     String JavaDoc on_object;
389     String JavaDoc action;
390     TObject misc;
391   }
392   
393   /**
394    * An object that models the list of triggers as table objects in a
395    * transaction.
396    */

397   private static class TriggerInternalTableInfo
398                                        extends AbstractInternalTableInfo2 {
399
400     TriggerInternalTableInfo(Transaction transaction) {
401       super(transaction, Database.SYS_DATA_TRIGGER);
402     }
403
404     private static DataTableDef createDataTableDef(String JavaDoc schema, String JavaDoc name) {
405       // Create the DataTableDef that describes this entry
406
DataTableDef def = new DataTableDef();
407       def.setTableName(new TableName(schema, name));
408
409       // Add column definitions
410
def.addColumn(DataTableColumnDef.createNumericColumn("type"));
411       def.addColumn(DataTableColumnDef.createStringColumn("on_object"));
412       def.addColumn(DataTableColumnDef.createStringColumn("procedure_name"));
413       def.addColumn(DataTableColumnDef.createStringColumn("param_args"));
414       def.addColumn(DataTableColumnDef.createStringColumn("owner"));
415
416       // Set to immutable
417
def.setImmutable();
418
419       // Return the data table def
420
return def;
421     }
422
423     
424     public String JavaDoc getTableType(int i) {
425       return "TRIGGER";
426     }
427
428     public DataTableDef getDataTableDef(int i) {
429       TableName table_name = getTableName(i);
430       return createDataTableDef(table_name.getSchema(), table_name.getName());
431     }
432
433     public MutableTableDataSource createInternalTable(int index) {
434       MutableTableDataSource table =
435                                transaction.getTable(Database.SYS_DATA_TRIGGER);
436       RowEnumeration row_e = table.rowEnumeration();
437       int p = 0;
438       int i;
439       int row_i = -1;
440       while (row_e.hasMoreRows()) {
441         i = row_e.nextRowIndex();
442         if (p == index) {
443           row_i = i;
444         }
445         else {
446           ++p;
447         }
448       }
449       if (p == index) {
450         String JavaDoc schema = table.getCellContents(0, row_i).getObject().toString();
451         String JavaDoc name = table.getCellContents(1, row_i).getObject().toString();
452
453         final DataTableDef table_def = createDataTableDef(schema, name);
454         final TObject type = table.getCellContents(2, row_i);
455         final TObject on_object = table.getCellContents(3, row_i);
456         final TObject procedure_name = table.getCellContents(4, row_i);
457         final TObject param_args = table.getCellContents(5, row_i);
458         final TObject owner = table.getCellContents(6, row_i);
459
460         // Implementation of MutableTableDataSource that describes this
461
// trigger.
462
return new GTDataSource(transaction.getSystem()) {
463           public DataTableDef getDataTableDef() {
464             return table_def;
465           }
466           public int getRowCount() {
467             return 1;
468           }
469           public TObject getCellContents(int col, int row) {
470             switch (col) {
471               case 0:
472                 return type;
473               case 1:
474                 return on_object;
475               case 2:
476                 return procedure_name;
477               case 3:
478                 return param_args;
479               case 4:
480                 return owner;
481               default:
482                 throw new RuntimeException JavaDoc("Column out of bounds.");
483             }
484           }
485         };
486
487       }
488       else {
489         throw new RuntimeException JavaDoc("Index out of bounds.");
490       }
491
492     }
493
494   }
495   
496 }
497
498
Popular Tags