1 | /* |
2 | * Copyright 2004-2014 H2 Group. Multiple-Licensed under the MPL 2.0, |
3 | * and the EPL 1.0 (http://h2database.com/html/license.html). |
4 | * Initial Developer: H2 Group |
5 | */ |
6 | package org.h2.schema; |
7 | |
8 | import java.lang.reflect.Method; |
9 | import java.sql.Connection; |
10 | import java.sql.SQLException; |
11 | |
12 | import org.h2.api.ErrorCode; |
13 | import org.h2.api.Trigger; |
14 | import org.h2.command.Parser; |
15 | import org.h2.engine.Constants; |
16 | import org.h2.engine.DbObject; |
17 | import org.h2.engine.Session; |
18 | import org.h2.message.DbException; |
19 | import org.h2.message.Trace; |
20 | import org.h2.result.Row; |
21 | import org.h2.table.Table; |
22 | import org.h2.util.JdbcUtils; |
23 | import org.h2.util.SourceCompiler; |
24 | import org.h2.util.StatementBuilder; |
25 | import org.h2.util.StringUtils; |
26 | import org.h2.value.DataType; |
27 | import org.h2.value.Value; |
28 | |
29 | /** |
30 | *A trigger is created using the statement |
31 | * CREATE TRIGGER |
32 | */ |
33 | public class TriggerObject extends SchemaObjectBase { |
34 | |
35 | /** |
36 | * The default queue size. |
37 | */ |
38 | public static final int DEFAULT_QUEUE_SIZE = 1024; |
39 | |
40 | private boolean insteadOf; |
41 | private boolean before; |
42 | private int typeMask; |
43 | private boolean rowBased; |
44 | private boolean onRollback; |
45 | // TODO trigger: support queue and noWait = false as well |
46 | private int queueSize = DEFAULT_QUEUE_SIZE; |
47 | private boolean noWait; |
48 | private Table table; |
49 | private String triggerClassName; |
50 | private String triggerSource; |
51 | private Trigger triggerCallback; |
52 | |
53 | public TriggerObject(Schema schema, int id, String name, Table table) { |
54 | initSchemaObjectBase(schema, id, name, Trace.TRIGGER); |
55 | this.table = table; |
56 | setTemporary(table.isTemporary()); |
57 | } |
58 | |
59 | public void setBefore(boolean before) { |
60 | this.before = before; |
61 | } |
62 | |
63 | public void setInsteadOf(boolean insteadOf) { |
64 | this.insteadOf = insteadOf; |
65 | } |
66 | |
67 | private synchronized void load() { |
68 | if (triggerCallback != null) { |
69 | return; |
70 | } |
71 | try { |
72 | Session sysSession = database.getSystemSession(); |
73 | Connection c2 = sysSession.createConnection(false); |
74 | Object obj; |
75 | if (triggerClassName != null) { |
76 | obj = JdbcUtils.loadUserClass(triggerClassName).newInstance(); |
77 | } else { |
78 | obj = loadFromSource(); |
79 | } |
80 | triggerCallback = (Trigger) obj; |
81 | triggerCallback.init(c2, getSchema().getName(), getName(), |
82 | table.getName(), before, typeMask); |
83 | } catch (Throwable e) { |
84 | // try again later |
85 | triggerCallback = null; |
86 | throw DbException.get(ErrorCode.ERROR_CREATING_TRIGGER_OBJECT_3, e, getName(), |
87 | triggerClassName != null ? triggerClassName : "..source..", e.toString()); |
88 | } |
89 | } |
90 | |
91 | private Trigger loadFromSource() { |
92 | SourceCompiler compiler = database.getCompiler(); |
93 | synchronized (compiler) { |
94 | String fullClassName = Constants.USER_PACKAGE + ".trigger." + getName(); |
95 | compiler.setSource(fullClassName, triggerSource); |
96 | try { |
97 | Method m = compiler.getMethod(fullClassName); |
98 | if (m.getParameterTypes().length > 0) { |
99 | throw new IllegalStateException("No parameters are allowed for a trigger"); |
100 | } |
101 | return (Trigger) m.invoke(null); |
102 | } catch (DbException e) { |
103 | throw e; |
104 | } catch (Exception e) { |
105 | throw DbException.get(ErrorCode.SYNTAX_ERROR_1, e, triggerSource); |
106 | } |
107 | } |
108 | } |
109 | |
110 | /** |
111 | * Set the trigger class name and load the class if possible. |
112 | * |
113 | * @param triggerClassName the name of the trigger class |
114 | * @param force whether exceptions (due to missing class or access rights) |
115 | * should be ignored |
116 | */ |
117 | public void setTriggerClassName(String triggerClassName, boolean force) { |
118 | this.setTriggerAction(triggerClassName, null, force); |
119 | } |
120 | |
121 | /** |
122 | * Set the trigger source code and compile it if possible. |
123 | * |
124 | * @param source the source code of a method returning a {@link Trigger} |
125 | * @param force whether exceptions (due to syntax error) |
126 | * should be ignored |
127 | */ |
128 | public void setTriggerSource(String source, boolean force) { |
129 | this.setTriggerAction(null, source, force); |
130 | } |
131 | |
132 | private void setTriggerAction(String triggerClassName, String source, boolean force) { |
133 | this.triggerClassName = triggerClassName; |
134 | this.triggerSource = source; |
135 | try { |
136 | load(); |
137 | } catch (DbException e) { |
138 | if (!force) { |
139 | throw e; |
140 | } |
141 | } |
142 | } |
143 | |
144 | /** |
145 | * Call the trigger class if required. This method does nothing if the |
146 | * trigger is not defined for the given action. This method is called before |
147 | * or after any rows have been processed, once for each statement. |
148 | * |
149 | * @param session the session |
150 | * @param type the trigger type |
151 | * @param beforeAction if this method is called before applying the changes |
152 | */ |
153 | public void fire(Session session, int type, boolean beforeAction) { |
154 | if (rowBased || before != beforeAction || (typeMask & type) == 0) { |
155 | return; |
156 | } |
157 | load(); |
158 | Connection c2 = session.createConnection(false); |
159 | boolean old = false; |
160 | if (type != Trigger.SELECT) { |
161 | old = session.setCommitOrRollbackDisabled(true); |
162 | } |
163 | Value identity = session.getLastScopeIdentity(); |
164 | try { |
165 | triggerCallback.fire(c2, null, null); |
166 | } catch (Throwable e) { |
167 | throw DbException.get(ErrorCode.ERROR_EXECUTING_TRIGGER_3, e, getName(), |
168 | triggerClassName != null ? triggerClassName : "..source..", e.toString()); |
169 | } finally { |
170 | session.setLastScopeIdentity(identity); |
171 | if (type != Trigger.SELECT) { |
172 | session.setCommitOrRollbackDisabled(old); |
173 | } |
174 | } |
175 | } |
176 | |
177 | private static Object[] convertToObjectList(Row row) { |
178 | if (row == null) { |
179 | return null; |
180 | } |
181 | int len = row.getColumnCount(); |
182 | Object[] list = new Object[len]; |
183 | for (int i = 0; i < len; i++) { |
184 | list[i] = row.getValue(i).getObject(); |
185 | } |
186 | return list; |
187 | } |
188 | |
189 | /** |
190 | * Call the fire method of the user-defined trigger class if required. This |
191 | * method does nothing if the trigger is not defined for the given action. |
192 | * This method is called before or after a row is processed, possibly many |
193 | * times for each statement. |
194 | * |
195 | * @param session the session |
196 | * @param oldRow the old row |
197 | * @param newRow the new row |
198 | * @param beforeAction true if this method is called before the operation is |
199 | * applied |
200 | * @param rollback when the operation occurred within a rollback |
201 | * @return true if no further action is required (for 'instead of' triggers) |
202 | */ |
203 | public boolean fireRow(Session session, Row oldRow, Row newRow, |
204 | boolean beforeAction, boolean rollback) { |
205 | if (!rowBased || before != beforeAction) { |
206 | return false; |
207 | } |
208 | if (rollback && !onRollback) { |
209 | return false; |
210 | } |
211 | load(); |
212 | Object[] oldList; |
213 | Object[] newList; |
214 | boolean fire = false; |
215 | if ((typeMask & Trigger.INSERT) != 0) { |
216 | if (oldRow == null && newRow != null) { |
217 | fire = true; |
218 | } |
219 | } |
220 | if ((typeMask & Trigger.UPDATE) != 0) { |
221 | if (oldRow != null && newRow != null) { |
222 | fire = true; |
223 | } |
224 | } |
225 | if ((typeMask & Trigger.DELETE) != 0) { |
226 | if (oldRow != null && newRow == null) { |
227 | fire = true; |
228 | } |
229 | } |
230 | if (!fire) { |
231 | return false; |
232 | } |
233 | oldList = convertToObjectList(oldRow); |
234 | newList = convertToObjectList(newRow); |
235 | Object[] newListBackup; |
236 | if (before && newList != null) { |
237 | newListBackup = new Object[newList.length]; |
238 | System.arraycopy(newList, 0, newListBackup, 0, newList.length); |
239 | } else { |
240 | newListBackup = null; |
241 | } |
242 | Connection c2 = session.createConnection(false); |
243 | boolean old = session.getAutoCommit(); |
244 | boolean oldDisabled = session.setCommitOrRollbackDisabled(true); |
245 | Value identity = session.getLastScopeIdentity(); |
246 | try { |
247 | session.setAutoCommit(false); |
248 | triggerCallback.fire(c2, oldList, newList); |
249 | if (newListBackup != null) { |
250 | for (int i = 0; i < newList.length; i++) { |
251 | Object o = newList[i]; |
252 | if (o != newListBackup[i]) { |
253 | Value v = DataType.convertToValue(session, o, Value.UNKNOWN); |
254 | newRow.setValue(i, v); |
255 | } |
256 | } |
257 | } |
258 | } catch (Exception e) { |
259 | if (onRollback) { |
260 | // ignore |
261 | } else { |
262 | throw DbException.convert(e); |
263 | } |
264 | } finally { |
265 | session.setLastScopeIdentity(identity); |
266 | session.setCommitOrRollbackDisabled(oldDisabled); |
267 | session.setAutoCommit(old); |
268 | } |
269 | return insteadOf; |
270 | } |
271 | |
272 | /** |
273 | * Set the trigger type. |
274 | * |
275 | * @param typeMask the type |
276 | */ |
277 | public void setTypeMask(int typeMask) { |
278 | this.typeMask = typeMask; |
279 | } |
280 | |
281 | public void setRowBased(boolean rowBased) { |
282 | this.rowBased = rowBased; |
283 | } |
284 | |
285 | public void setQueueSize(int size) { |
286 | this.queueSize = size; |
287 | } |
288 | |
289 | public int getQueueSize() { |
290 | return queueSize; |
291 | } |
292 | |
293 | public void setNoWait(boolean noWait) { |
294 | this.noWait = noWait; |
295 | } |
296 | |
297 | public boolean isNoWait() { |
298 | return noWait; |
299 | } |
300 | |
301 | public void setOnRollback(boolean onRollback) { |
302 | this.onRollback = onRollback; |
303 | } |
304 | |
305 | @Override |
306 | public String getDropSQL() { |
307 | return null; |
308 | } |
309 | |
310 | @Override |
311 | public String getCreateSQLForCopy(Table targetTable, String quotedName) { |
312 | StringBuilder buff = new StringBuilder("CREATE FORCE TRIGGER "); |
313 | buff.append(quotedName); |
314 | if (insteadOf) { |
315 | buff.append(" INSTEAD OF "); |
316 | } else if (before) { |
317 | buff.append(" BEFORE "); |
318 | } else { |
319 | buff.append(" AFTER "); |
320 | } |
321 | buff.append(getTypeNameList()); |
322 | buff.append(" ON ").append(targetTable.getSQL()); |
323 | if (rowBased) { |
324 | buff.append(" FOR EACH ROW"); |
325 | } |
326 | if (noWait) { |
327 | buff.append(" NOWAIT"); |
328 | } else { |
329 | buff.append(" QUEUE ").append(queueSize); |
330 | } |
331 | if (triggerClassName != null) { |
332 | buff.append(" CALL ").append(Parser.quoteIdentifier(triggerClassName)); |
333 | } else { |
334 | buff.append(" AS ").append(StringUtils.quoteStringSQL(triggerSource)); |
335 | } |
336 | return buff.toString(); |
337 | } |
338 | |
339 | public String getTypeNameList() { |
340 | StatementBuilder buff = new StatementBuilder(); |
341 | if ((typeMask & Trigger.INSERT) != 0) { |
342 | buff.appendExceptFirst(", "); |
343 | buff.append("INSERT"); |
344 | } |
345 | if ((typeMask & Trigger.UPDATE) != 0) { |
346 | buff.appendExceptFirst(", "); |
347 | buff.append("UPDATE"); |
348 | } |
349 | if ((typeMask & Trigger.DELETE) != 0) { |
350 | buff.appendExceptFirst(", "); |
351 | buff.append("DELETE"); |
352 | } |
353 | if ((typeMask & Trigger.SELECT) != 0) { |
354 | buff.appendExceptFirst(", "); |
355 | buff.append("SELECT"); |
356 | } |
357 | if (onRollback) { |
358 | buff.appendExceptFirst(", "); |
359 | buff.append("ROLLBACK"); |
360 | } |
361 | return buff.toString(); |
362 | } |
363 | |
364 | @Override |
365 | public String getCreateSQL() { |
366 | return getCreateSQLForCopy(table, getSQL()); |
367 | } |
368 | |
369 | @Override |
370 | public int getType() { |
371 | return DbObject.TRIGGER; |
372 | } |
373 | |
374 | @Override |
375 | public void removeChildrenAndResources(Session session) { |
376 | table.removeTrigger(this); |
377 | database.removeMeta(session, getId()); |
378 | if (triggerCallback != null) { |
379 | try { |
380 | triggerCallback.remove(); |
381 | } catch (SQLException e) { |
382 | throw DbException.convert(e); |
383 | } |
384 | } |
385 | table = null; |
386 | triggerClassName = null; |
387 | triggerSource = null; |
388 | triggerCallback = null; |
389 | invalidate(); |
390 | } |
391 | |
392 | @Override |
393 | public void checkRename() { |
394 | // nothing to do |
395 | } |
396 | |
397 | /** |
398 | * Get the table of this trigger. |
399 | * |
400 | * @return the table |
401 | */ |
402 | public Table getTable() { |
403 | return table; |
404 | } |
405 | |
406 | /** |
407 | * Check if this is a before trigger. |
408 | * |
409 | * @return true if it is |
410 | */ |
411 | public boolean isBefore() { |
412 | return before; |
413 | } |
414 | |
415 | /** |
416 | * Get the trigger class name. |
417 | * |
418 | * @return the class name |
419 | */ |
420 | public String getTriggerClassName() { |
421 | return triggerClassName; |
422 | } |
423 | |
424 | public String getTriggerSource() { |
425 | return triggerSource; |
426 | } |
427 | |
428 | /** |
429 | * Close the trigger. |
430 | */ |
431 | public void close() throws SQLException { |
432 | if (triggerCallback != null) { |
433 | triggerCallback.close(); |
434 | } |
435 | } |
436 | |
437 | /** |
438 | * Check whether this is a select trigger. |
439 | * |
440 | * @return true if it is |
441 | */ |
442 | public boolean isSelectTrigger() { |
443 | return (typeMask & Trigger.SELECT) != 0; |
444 | } |
445 | |
446 | } |