001package io.ebean.config.dbplatform.h2;
002
003import org.h2.api.Trigger;
004import org.slf4j.Logger;
005import org.slf4j.LoggerFactory;
006
007import java.sql.*;
008import java.time.LocalDateTime;
009import java.util.Arrays;
010
011/**
012 * H2 database trigger used to populate history tables to support the @History feature.
013 */
014public class H2HistoryTrigger implements Trigger {
015
016  private static final Logger log = LoggerFactory.getLogger(H2HistoryTrigger.class);
017
018  /**
019   * Hardcoding the column and history table suffix for now. Not sure how to get that
020   * configuration into the trigger instance nicely as it is instantiated by H2.
021   */
022  private static final String SYS_PERIOD_START = "SYS_PERIOD_START";
023  private static final String SYS_PERIOD_END = "SYS_PERIOD_END";
024  private static final String HISTORY_SUFFIX = "_history";
025
026  /**
027   * SQL to insert into the history table.
028   */
029  private String insertHistorySql;
030
031  /**
032   * Position of SYS_PERIOD_START column in the Object[].
033   */
034  private int effectStartPosition;
035
036  /**
037   * Position of SYS_PERIOD_END column in the Object[].
038   */
039  private int effectEndPosition;
040
041  @Override
042  public void init(Connection conn, String schemaName, String triggerName, String tableName, boolean before, int type) throws SQLException {
043    // get the columns for the table
044    ResultSet rs = conn.getMetaData().getColumns(null, schemaName, tableName, null);
045
046    // build the insert into history table SQL
047    StringBuilder insertSql = new StringBuilder(150);
048    insertSql.append("insert into ").append(schemaName).append(".").append(tableName).append(HISTORY_SUFFIX).append(" (");
049
050    int count = 0;
051    while (rs.next()) {
052      if (++count > 1) {
053        insertSql.append(",");
054      }
055      String columnName = rs.getString("COLUMN_NAME");
056      if (columnName.equalsIgnoreCase(SYS_PERIOD_START)) {
057        this.effectStartPosition = count - 1;
058      } else if (columnName.equalsIgnoreCase(SYS_PERIOD_END)) {
059        this.effectEndPosition = count - 1;
060      }
061      insertSql.append(columnName);
062    }
063    insertSql.append(") values (");
064    for (int i = 0; i < count; i++) {
065      if (i > 0) {
066        insertSql.append(",");
067      }
068      insertSql.append("?");
069    }
070    insertSql.append(");");
071
072    this.insertHistorySql = insertSql.toString();
073    log.debug("History table insert sql: {}", insertHistorySql);
074  }
075
076  @Override
077  public void fire(Connection connection, Object[] oldRow, Object[] newRow) throws SQLException {
078    if (oldRow != null) {
079      // a delete or update event
080      LocalDateTime now = LocalDateTime.now();
081      oldRow[effectEndPosition] = now;
082      if (newRow != null) {
083        // update event. Set the effective start timestamp to now.
084        newRow[effectStartPosition] = now;
085      }
086      if (log.isTraceEnabled()) {
087        log.trace("History insert: {}", Arrays.toString(oldRow));
088      }
089      insertIntoHistory(connection, oldRow);
090    }
091  }
092
093  /**
094   * Insert the data into the history table.
095   */
096  private void insertIntoHistory(Connection connection, Object[] oldRow) throws SQLException {
097    try (PreparedStatement stmt = connection.prepareStatement(insertHistorySql)) {
098      for (int i = 0; i < oldRow.length; i++) {
099        stmt.setObject(i + 1, oldRow[i]);
100      }
101      stmt.executeUpdate();
102    }
103  }
104
105  @Override
106  public void close() throws SQLException {
107    // do nothing
108  }
109
110  @Override
111  public void remove() throws SQLException {
112    // do nothing
113  }
114}