org.h2.schema.TriggerObject Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of h2-mvstore Show documentation
Show all versions of h2-mvstore Show documentation
Fork of h2database to maintain Java 8 compatibility
The newest version!
/*
* Copyright 2004-2023 H2 Group. Multiple-Licensed under the MPL 2.0,
* and the EPL 1.0 (https://h2database.com/html/license.html).
* Initial Developer: H2 Group
*/
package org.h2.schema;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Arrays;
import org.h2.api.ErrorCode;
import org.h2.api.Trigger;
import org.h2.engine.Constants;
import org.h2.engine.DbObject;
import org.h2.engine.SessionLocal;
import org.h2.jdbc.JdbcConnection;
import org.h2.jdbc.JdbcResultSet;
import org.h2.message.DbException;
import org.h2.message.Trace;
import org.h2.result.Row;
import org.h2.result.SimpleResult;
import org.h2.table.Column;
import org.h2.table.Table;
import org.h2.tools.TriggerAdapter;
import org.h2.util.JdbcUtils;
import org.h2.util.SourceCompiler;
import org.h2.util.StringUtils;
import org.h2.value.Value;
import org.h2.value.ValueToObjectConverter;
/**
*A trigger is created using the statement
* CREATE TRIGGER
*/
public final class TriggerObject extends SchemaObject {
/**
* The default queue size.
*/
public static final int DEFAULT_QUEUE_SIZE = 1024;
private boolean insteadOf;
private boolean before;
private int typeMask;
private boolean rowBased;
private boolean onRollback;
// TODO trigger: support queue and noWait = false as well
private int queueSize = DEFAULT_QUEUE_SIZE;
private boolean noWait;
private Table table;
private String triggerClassName;
private String triggerSource;
private Trigger triggerCallback;
public TriggerObject(Schema schema, int id, String name, Table table) {
super(schema, id, name, Trace.TRIGGER);
this.table = table;
setTemporary(table.isTemporary());
}
public void setBefore(boolean before) {
this.before = before;
}
public boolean isInsteadOf() {
return insteadOf;
}
public void setInsteadOf(boolean insteadOf) {
this.insteadOf = insteadOf;
}
private synchronized void load() {
if (triggerCallback != null) {
return;
}
try {
SessionLocal sysSession = database.getSystemSession();
Connection c2 = sysSession.createConnection(false);
Object obj;
if (triggerClassName != null) {
obj = JdbcUtils.loadUserClass(triggerClassName).getDeclaredConstructor().newInstance();
} else {
obj = loadFromSource();
}
triggerCallback = (Trigger) obj;
triggerCallback.init(c2, getSchema().getName(), getName(),
table.getName(), before, typeMask);
} catch (Throwable e) {
// try again later
triggerCallback = null;
throw DbException.get(ErrorCode.ERROR_CREATING_TRIGGER_OBJECT_3, e, getName(),
triggerClassName != null ? triggerClassName : "..source..", e.toString());
}
}
private Trigger loadFromSource() {
SourceCompiler compiler = database.getCompiler();
synchronized (compiler) {
String fullClassName = Constants.USER_PACKAGE + ".trigger." + getName();
compiler.setSource(fullClassName, triggerSource);
try {
if (SourceCompiler.isJavaxScriptSource(triggerSource)) {
return (Trigger) compiler.getCompiledScript(fullClassName).eval();
} else {
final Method m = compiler.getMethod(fullClassName);
if (m.getParameterTypes().length > 0) {
throw new IllegalStateException("No parameters are allowed for a trigger");
}
return (Trigger) m.invoke(null);
}
} catch (DbException e) {
throw e;
} catch (Exception e) {
throw DbException.get(ErrorCode.SYNTAX_ERROR_1, e, triggerSource);
}
}
}
/**
* Set the trigger class name and load the class if possible.
*
* @param triggerClassName the name of the trigger class
* @param force whether exceptions (due to missing class or access rights)
* should be ignored
*/
public void setTriggerClassName(String triggerClassName, boolean force) {
this.setTriggerAction(triggerClassName, null, force);
}
/**
* Set the trigger source code and compile it if possible.
*
* @param source the source code of a method returning a {@link Trigger}
* @param force whether exceptions (due to syntax error)
* should be ignored
*/
public void setTriggerSource(String source, boolean force) {
this.setTriggerAction(null, source, force);
}
private void setTriggerAction(String triggerClassName, String source, boolean force) {
this.triggerClassName = triggerClassName;
this.triggerSource = source;
try {
load();
} catch (DbException e) {
if (!force) {
throw e;
}
}
}
/**
* Call the trigger class if required. This method does nothing if the
* trigger is not defined for the given action. This method is called before
* or after any rows have been processed, once for each statement.
*
* @param session the session
* @param type the trigger type
* @param beforeAction if this method is called before applying the changes
*/
public void fire(SessionLocal session, int type, boolean beforeAction) {
if (rowBased || before != beforeAction || (typeMask & type) == 0) {
return;
}
load();
Connection c2 = session.createConnection(false);
boolean old = false;
if (type != Trigger.SELECT) {
old = session.setCommitOrRollbackDisabled(true);
}
Value identity = session.getLastIdentity();
try {
if (triggerCallback instanceof TriggerAdapter) {
((TriggerAdapter) triggerCallback).fire(c2, (ResultSet) null, (ResultSet) null);
} else {
triggerCallback.fire(c2, null, null);
}
} catch (Throwable e) {
throw getErrorExecutingTrigger(e);
} finally {
session.setLastIdentity(identity);
if (type != Trigger.SELECT) {
session.setCommitOrRollbackDisabled(old);
}
}
}
private static Object[] convertToObjectList(Row row, JdbcConnection conn) {
if (row == null) {
return null;
}
int len = row.getColumnCount();
Object[] list = new Object[len];
for (int i = 0; i < len; i++) {
list[i] = ValueToObjectConverter.valueToDefaultObject(row.getValue(i), conn, false);
}
return list;
}
/**
* Call the fire method of the user-defined trigger class if required. This
* method does nothing if the trigger is not defined for the given action.
* This method is called before or after a row is processed, possibly many
* times for each statement.
*
* @param session the session
* @param table the table
* @param oldRow the old row
* @param newRow the new row
* @param beforeAction true if this method is called before the operation is
* applied
* @param rollback when the operation occurred within a rollback
* @return true if no further action is required (for 'instead of' triggers)
*/
public boolean fireRow(SessionLocal session, Table table, Row oldRow, Row newRow,
boolean beforeAction, boolean rollback) {
if (!rowBased || before != beforeAction) {
return false;
}
if (rollback && !onRollback) {
return false;
}
load();
boolean fire = false;
if ((typeMask & Trigger.INSERT) != 0) {
if (oldRow == null && newRow != null) {
fire = true;
}
}
if ((typeMask & Trigger.UPDATE) != 0) {
if (oldRow != null && newRow != null) {
fire = true;
}
}
if ((typeMask & Trigger.DELETE) != 0) {
if (oldRow != null && newRow == null) {
fire = true;
}
}
if (!fire) {
return false;
}
JdbcConnection c2 = session.createConnection(false);
boolean old = session.getAutoCommit();
boolean oldDisabled = session.setCommitOrRollbackDisabled(true);
Value identity = session.getLastIdentity();
try {
session.setAutoCommit(false);
if (triggerCallback instanceof TriggerAdapter) {
JdbcResultSet oldResultSet = oldRow != null ? createResultSet(c2, table, oldRow, false) : null;
JdbcResultSet newResultSet = newRow != null ? createResultSet(c2, table, newRow, before) : null;
try {
((TriggerAdapter) triggerCallback).fire(c2, oldResultSet, newResultSet);
} catch (Throwable e) {
throw getErrorExecutingTrigger(e);
}
if (newResultSet != null) {
Value[] updatedList = newResultSet.getUpdateRow();
if (updatedList != null) {
boolean modified = false;
for (int i = 0, l = updatedList.length; i < l; i++) {
Value v = updatedList[i];
if (v != null) {
modified = true;
newRow.setValue(i, v);
}
}
if (modified) {
table.convertUpdateRow(session, newRow, true);
}
}
}
} else {
Object[] oldList = convertToObjectList(oldRow, c2);
Object[] newList = convertToObjectList(newRow, c2);
Object[] newListBackup = before && newList != null ? Arrays.copyOf(newList, newList.length) : null;
try {
triggerCallback.fire(c2, oldList, newList);
} catch (Throwable e) {
throw getErrorExecutingTrigger(e);
}
if (newListBackup != null) {
boolean modified = false;
for (int i = 0; i < newList.length; i++) {
Object o = newList[i];
if (o != newListBackup[i]) {
modified = true;
newRow.setValue(i, ValueToObjectConverter.objectToValue(session, o, Value.UNKNOWN));
}
}
if (modified) {
table.convertUpdateRow(session, newRow, true);
}
}
}
} catch (Exception e) {
if (onRollback) {
// ignore
} else {
throw DbException.convert(e);
}
} finally {
session.setLastIdentity(identity);
session.setCommitOrRollbackDisabled(oldDisabled);
session.setAutoCommit(old);
}
return insteadOf;
}
private static JdbcResultSet createResultSet(JdbcConnection conn, Table table, Row row, boolean updatable)
throws SQLException {
SimpleResult result = new SimpleResult(table.getSchema().getName(), table.getName());
for (Column c : table.getColumns()) {
result.addColumn(c.getName(), c.getType());
}
/*
* Old implementation works with and without next() invocation, so add
* the row twice for compatibility.
*/
result.addRow(row.getValueList());
result.addRow(row.getValueList());
JdbcResultSet resultSet = new JdbcResultSet(conn, null, null, result, -1, false, false, updatable);
resultSet.next();
return resultSet;
}
private DbException getErrorExecutingTrigger(Throwable e) {
if (e instanceof DbException) {
return (DbException) e;
}
if (e instanceof SQLException) {
return DbException.convert(e);
}
return DbException.get(ErrorCode.ERROR_EXECUTING_TRIGGER_3, e, getName(),
triggerClassName != null ? triggerClassName : "..source..", e.toString());
}
/**
* Returns the trigger type.
*
* @return the trigger type
*/
public int getTypeMask() {
return typeMask;
}
/**
* Set the trigger type.
*
* @param typeMask the type
*/
public void setTypeMask(int typeMask) {
this.typeMask = typeMask;
}
public void setRowBased(boolean rowBased) {
this.rowBased = rowBased;
}
public boolean isRowBased() {
return rowBased;
}
public void setQueueSize(int size) {
this.queueSize = size;
}
public int getQueueSize() {
return queueSize;
}
public void setNoWait(boolean noWait) {
this.noWait = noWait;
}
public boolean isNoWait() {
return noWait;
}
public void setOnRollback(boolean onRollback) {
this.onRollback = onRollback;
}
public boolean isOnRollback() {
return onRollback;
}
@Override
public String getCreateSQLForCopy(Table targetTable, String quotedName) {
StringBuilder builder = new StringBuilder("CREATE FORCE TRIGGER ");
builder.append(quotedName);
if (insteadOf) {
builder.append(" INSTEAD OF ");
} else if (before) {
builder.append(" BEFORE ");
} else {
builder.append(" AFTER ");
}
getTypeNameList(builder).append(" ON ");
targetTable.getSQL(builder, DEFAULT_SQL_FLAGS);
if (rowBased) {
builder.append(" FOR EACH ROW");
}
if (noWait) {
builder.append(" NOWAIT");
} else {
builder.append(" QUEUE ").append(queueSize);
}
if (triggerClassName != null) {
StringUtils.quoteStringSQL(builder.append(" CALL "), triggerClassName);
} else {
StringUtils.quoteStringSQL(builder.append(" AS "), triggerSource);
}
return builder.toString();
}
/**
* Append the trigger types to the given string builder.
*
* @param builder the builder
* @return the passed string builder
*/
public StringBuilder getTypeNameList(StringBuilder builder) {
boolean f = false;
if ((typeMask & Trigger.INSERT) != 0) {
f = true;
builder.append("INSERT");
}
if ((typeMask & Trigger.UPDATE) != 0) {
if (f) {
builder.append(", ");
}
f = true;
builder.append("UPDATE");
}
if ((typeMask & Trigger.DELETE) != 0) {
if (f) {
builder.append(", ");
}
f = true;
builder.append("DELETE");
}
if ((typeMask & Trigger.SELECT) != 0) {
if (f) {
builder.append(", ");
}
f = true;
builder.append("SELECT");
}
if (onRollback) {
if (f) {
builder.append(", ");
}
builder.append("ROLLBACK");
}
return builder;
}
@Override
public String getCreateSQL() {
return getCreateSQLForCopy(table, getSQL(DEFAULT_SQL_FLAGS));
}
@Override
public int getType() {
return DbObject.TRIGGER;
}
@Override
public void removeChildrenAndResources(SessionLocal session) {
table.removeTrigger(this);
database.removeMeta(session, getId());
if (triggerCallback != null) {
try {
triggerCallback.remove();
} catch (SQLException e) {
throw DbException.convert(e);
}
}
table = null;
triggerClassName = null;
triggerSource = null;
triggerCallback = null;
invalidate();
}
/**
* Get the table of this trigger.
*
* @return the table
*/
public Table getTable() {
return table;
}
/**
* Check if this is a before trigger.
*
* @return true if it is
*/
public boolean isBefore() {
return before;
}
/**
* Get the trigger class name.
*
* @return the class name
*/
public String getTriggerClassName() {
return triggerClassName;
}
public String getTriggerSource() {
return triggerSource;
}
/**
* Close the trigger.
* @throws SQLException on failure
*/
public void close() throws SQLException {
if (triggerCallback != null) {
triggerCallback.close();
}
}
/**
* Check whether this is a select trigger.
*
* @return true if it is
*/
public boolean isSelectTrigger() {
return (typeMask & Trigger.SELECT) != 0;
}
}