/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.openjpa.jdbc.kernel;

import java.io.Serializable;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.concurrent.ConcurrentHashMap;

import jakarta.transaction.NotSupportedException;

import org.apache.openjpa.conf.OpenJPAConfiguration;
import org.apache.openjpa.jdbc.conf.JDBCConfiguration;
import org.apache.openjpa.jdbc.conf.JDBCConfigurationImpl;
import org.apache.openjpa.jdbc.identifier.DBIdentifier;
import org.apache.openjpa.jdbc.identifier.DBIdentifier.DBIdentifierType;
import org.apache.openjpa.jdbc.identifier.Normalizer;
import org.apache.openjpa.jdbc.identifier.QualifiedDBIdentifier;
import org.apache.openjpa.jdbc.meta.ClassMapping;
import org.apache.openjpa.jdbc.schema.Column;
import org.apache.openjpa.jdbc.schema.PrimaryKey;
import org.apache.openjpa.jdbc.schema.Schema;
import org.apache.openjpa.jdbc.schema.SchemaGroup;
import org.apache.openjpa.jdbc.schema.SchemaTool;
import org.apache.openjpa.jdbc.schema.Schemas;
import org.apache.openjpa.jdbc.schema.Table;
import org.apache.openjpa.jdbc.schema.Unique;
import org.apache.openjpa.jdbc.sql.DBDictionary;
import org.apache.openjpa.jdbc.sql.Row;
import org.apache.openjpa.jdbc.sql.SQLBuffer;
import org.apache.openjpa.lib.conf.Configurable;
import org.apache.openjpa.lib.conf.Configuration;
import org.apache.openjpa.lib.conf.Configurations;
import org.apache.openjpa.lib.identifier.IdentifierUtil;
import org.apache.openjpa.lib.log.Log;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.lib.util.Options;
import org.apache.openjpa.lib.util.StringUtil;
import org.apache.openjpa.meta.JavaTypes;
import org.apache.openjpa.util.InvalidStateException;
import org.apache.openjpa.util.UserException;


////////////////////////////////////////////////////////////
// NOTE: Do not change property names; see SequenceMetaData
// and SequenceMapping for standard property names.
////////////////////////////////////////////////////////////

/**
 * {@link JDBCSeq} implementation that uses a database table
 * for sequence number generation. This base implementation uses a single
 * row for a global sequence number.
 *
 * @author Abe White
 */
public class TableJDBCSeq extends AbstractJDBCSeq implements Configurable {

    public static final String ACTION_DROP = "drop";
    public static final String ACTION_ADD = "add";
    public static final String ACTION_GET = "get";
    public static final String ACTION_SET = "set";
    public static final String DEFAULT_TABLE = "OPENJPA_SEQUENCE_TABLE";

    private static final Localizer _loc = Localizer.forPackage
        (TableJDBCSeq.class);

    private transient JDBCConfiguration _conf = null;
    private transient Log _log = null;
    private int _alloc = 50;
    private int _intValue = 1;
    private final ConcurrentHashMap<ClassMapping, Status> _stat = new ConcurrentHashMap<>();

    private DBIdentifier _table = DBIdentifier.newTable(DEFAULT_TABLE);
    private DBIdentifier _seqColumnName = DBIdentifier.newColumn("SEQUENCE_VALUE");
    private DBIdentifier _pkColumnName = DBIdentifier.newColumn("ID");
    private DBIdentifier[] _uniqueColumnNames;
    private DBIdentifier _uniqueConstraintName = DBIdentifier.NULL;

    private Column _seqColumn = null;
    private Column _pkColumn = null;

    /**
     * The sequence table name. Defaults to <code>OPENJPA_SEQUENCE_TABLE</code>.
     * By default, the table will be placed in the first schema listed in your
     * <code>openjpa.jdbc.Schemas</code> property, or in the default schema if
     * the property is not given. If you specify a table name in the form
     * <code>&lt;schema&gt;.&lt;table&gt;</code>, then the given schema
     * will be used.
     */
    public String getTable() {
        return _table.getName();
    }

    /**
     * The sequence table name. Defaults to <code>OPENJPA_SEQUENCE_TABLE</code>.
     * By default, the table will be placed in the first schema listed in your
     * <code>openjpa.jdbc.Schemas</code> property, or in the default schema if
     * the property is not given. If you specify a table name in the form
     * <code>&lt;schema&gt;.&lt;table&gt;</code>, then the given schema
     * will be used.
     */
    public void setTable(String name) {
        // Split the name into its individual parts
        String[] names = Normalizer.splitName(name);
        // Join the name back together.  This will delimit as appropriate.
        _table = DBIdentifier.newTable(Normalizer.joinNames(names));
    }

    /**
     * @deprecated Use {@link #setTable}. Retained for
     * backwards-compatibility	with auto-configuration.
     */
    @Deprecated
    public void setTableName(String name) {
        setTable(name);
    }

    /**
     * The name of the column that holds the sequence value. Defaults
     * to <code>SEQUENCE_VALUE</code>.
     */
    public String getSequenceColumn() {
        return _seqColumnName.getName();
    }

    /**
     * The name of the column that holds the sequence value. Defaults
     * to <code>SEQUENCE_VALUE</code>.
     */
    public void setSequenceColumn(String sequenceColumn) {
        _seqColumnName = DBIdentifier.newColumn(sequenceColumn);
    }

    /**
     * The name of the table's primary key column. Defaults to
     * <code>ID</code>.
     */
    public String getPrimaryKeyColumn() {
        return _pkColumnName.getName();
    }

    public DBIdentifier getPrimaryKeyColumnIdentifier() {
        return _pkColumnName;
    }

    /**
     * The name of the table's primary key column. Defaults to
     * <code>ID</code>.
     */
    public void setPrimaryKeyColumn(String primaryKeyColumn) {
        _pkColumnName = DBIdentifier.newColumn(primaryKeyColumn, _conf.getDBDictionaryInstance().delimitAll());
    }

    /**
     * Return the number of sequences to allocate for each update of the
     * sequence table. Sequence numbers will be grabbed in blocks of this
     * value to reduce the number of transactions that must be performed on
     * the sequence table.
     */
    public int getAllocate() {
        return _alloc;
    }

    /**
     * Return the number of sequences to allocate for each update of the
     * sequence table. Sequence numbers will be grabbed in blocks of this
     * value to reduce the number of transactions that must be performed on
     * the sequence table.
     */
    public void setAllocate(int alloc) {
        _alloc = alloc;
    }

    /**
     * Return the number as the initial number for the
     * GeneratedValue.TABLE strategy to start with.
     * @return an initial number
     */
    public int getInitialValue() {
        return _intValue;
    }

    /**
     * Set the initial number in the table for the GeneratedValue.TABLE
     * strategy to use as initial number.
     * @param intValue. The initial number
     */
    public void setInitialValue(int intValue) {
        _intValue = intValue;
    }

    /**
     * Sets the names of the columns on which a unique constraint is set.
     * @param columnsNames are passed as a single String concatenated with
     * a '|' character. This method parses it back to array of Strings.
     */
    public void setUniqueColumns(String columnNames) {
    	_uniqueColumnNames = (StringUtil.isEmpty(columnNames))
    		? null : DBIdentifier.split(columnNames, DBIdentifierType.COLUMN, IdentifierUtil.BAR);
    }

    public String getUniqueColumns() {
    	return Normalizer.joinNames(DBIdentifier.toStringArray(_uniqueColumnNames), IdentifierUtil.BAR);
    }

    /**
     * @deprecated Use {@link #setAllocate}. Retained for backwards
     * compatibility of auto-configuration.
     */
    @Deprecated
    public void setIncrement(int inc) {
        setAllocate(inc);
    }

    @Override
    public JDBCConfiguration getConfiguration() {
        return _conf;
    }

    @Override
    public void setConfiguration(Configuration conf) {
        _conf = (JDBCConfiguration) conf;
        _log = _conf.getLog(OpenJPAConfiguration.LOG_RUNTIME);
    }

    @Override
    public void startConfiguration() {
    }

    @Override
    public void endConfiguration() {
        buildTable();
    }


    @Override
    public void addSchema(ClassMapping mapping, SchemaGroup group) {
        // Since the table is created by openjpa internally
        // we can create the table for each schema within the PU
        // in here.

        Schema[] schemas = group.getSchemas();
        for (Schema value : schemas) {
            QualifiedDBIdentifier path = QualifiedDBIdentifier.getPath(_table);
            DBIdentifier schemaName = path.getSchemaName();
            if (DBIdentifier.isEmpty(schemaName)) {
                schemaName = Schemas.getNewTableSchemaIdentifier(_conf);
            }
            if (DBIdentifier.isNull(schemaName)) {
                schemaName = value.getIdentifier();
            }

            // create table in this group
            Schema schema = group.getSchema(schemaName);
            if (schema == null) {
                schema = group.addSchema(schemaName);
            }

            Table copy = schema.importTable(_pkColumn.getTable());
            // importTable() does not import unique constraints
            Unique[] uniques = _pkColumn.getTable().getUniques();
            for (Unique u : uniques) {
                copy.importUnique(u);
            }
            // we need to reset the table name in the column with the
            // fully qualified name for matching the table name from the
            // Column.
            _pkColumn.resetTableIdentifier(QualifiedDBIdentifier.newPath(schemaName, _pkColumn.getTableIdentifier()));
            // some databases require to create an index for the sequence table
            _conf.getDBDictionaryInstance().createIndexIfNecessary(schema,
                    _table, _pkColumn);
        }
    }

    @Override
    protected Object nextInternal(JDBCStore store, ClassMapping mapping) throws Exception {
        // if needed, grab the next handful of ids
        Status stat = getStatus(mapping);
        if (stat == null)
            throw new InvalidStateException(_loc.get("bad-seq-type",
                getClass(), mapping));

        while (true) {
            synchronized (stat) {
                // make sure seq is at least 1, since autoassigned ids of 0 can
                // conflict with uninitialized values
                stat.seq = Math.max(stat.seq, 1);
                if (stat.seq < stat.max)
                    return stat.seq++;
                allocateSequence(store, mapping, stat, _alloc, true);
            }
        }
    }

    @Override
    protected Object currentInternal(JDBCStore store, ClassMapping mapping)
        throws Exception {
        if (current == null) {
            CurrentSequenceRunnable runnable =
                new CurrentSequenceRunnable(store, mapping);
            try {
                if (suspendInJTA()) {
                    // NotSupportedException is wrapped in a StoreException by
                    // the caller.
                    _conf.getManagedRuntimeInstance().doNonTransactionalWork(
                            runnable);
                } else {
                    runnable.run();
                }
            } catch (RuntimeException re) {
                throw (Exception) (re.getCause() == null ? re : re.getCause());
            }
        }
        return super.currentInternal(store, mapping);
    }

    @Override
    protected void allocateInternal(int count, JDBCStore store,
        ClassMapping mapping)
        throws SQLException {
        Status stat = getStatus(mapping);
        if (stat == null)
            return;

        while (true) {
            int available;
            synchronized (stat) {
                available = (int) (stat.max - stat.seq);
                if (available >= count)
                    return;
            }
            allocateSequence(store, mapping, stat, count - available, false);
        }
    }

    /**
     * Return the appropriate status object for the given class, or null
     * if cannot handle the given class. The mapping may be null.
     */
    protected Status getStatus(ClassMapping mapping) {
        Status status = _stat.get(mapping);
        if (status == null){
            status = new Status();
            Status tStatus = _stat.putIfAbsent(mapping, status);
            // This can happen if another thread calls .put(..) sometime after our call to get. Return
            // the value from the putIfAbsent call as that is truly in the map.
            if (tStatus != null) {
                return tStatus;
            }
        }
        return status;
    }

    /**
     * Add the primary key column to the given table and return it.
     */
    protected Column addPrimaryKeyColumn(Table table) {
        DBDictionary dict = _conf.getDBDictionaryInstance();
        Column pkColumn = table.addColumn(dict.getValidColumnName(getPrimaryKeyColumnIdentifier(), table));
        pkColumn.setType(dict.getPreferredType(Types.TINYINT));
        pkColumn.setJavaType(JavaTypes.INT);
        return pkColumn;
    }

    /**
     * Return the primary key value for the sequence table for the given class.
     */
    protected Object getPrimaryKey(ClassMapping mapping) {
        return 0;
    }

    /**
     * Creates the object-level representation of the sequence table.
     */
    private void buildTable() {
        DBIdentifier tableName = DBIdentifier.NULL;
        DBIdentifier schemaName = DBIdentifier.NULL;
        QualifiedDBIdentifier path = QualifiedDBIdentifier.getPath(_table);
        if (!DBIdentifier.isEmpty(path.getSchemaName())) {
            schemaName = path.getSchemaName();
            tableName = path.getUnqualifiedName();
        }
        else {
            tableName = _table;
        }

        if (DBIdentifier.isEmpty(schemaName)) {
            schemaName = Schemas.getNewTableSchemaIdentifier(_conf);
        }

        SchemaGroup group = new SchemaGroup();
        Schema schema = group.addSchema(schemaName);

        Table table = schema.addTable(tableName);
        _pkColumn = addPrimaryKeyColumn(table);
        PrimaryKey pk = table.addPrimaryKey();
        pk.addColumn(_pkColumn);

        DBDictionary dict = _conf.getDBDictionaryInstance();
        DBIdentifier _delimitedSeqColumnName = dict.delimitAll() ?
                DBIdentifier.newColumn(this._seqColumnName.getName(), true) : this._seqColumnName;
        _seqColumn = table.addColumn(dict.getValidColumnName
            (_delimitedSeqColumnName, table));
        _seqColumn.setType(dict.getPreferredType(Types.BIGINT));
        _seqColumn.setJavaType(JavaTypes.LONG);

        if (_uniqueColumnNames != null) {
            DBIdentifier uniqueName = _uniqueConstraintName;
            if (DBIdentifier.isEmpty(uniqueName)) {
                uniqueName = dict.getValidUniqueName(DBIdentifier.newConstraint("UNQ"), table);
            }
    		Unique u = table.addUnique(uniqueName);
    		for (DBIdentifier columnName : _uniqueColumnNames) {
    			if (!table.containsColumn(columnName, _conf.getDBDictionaryInstance()))
                    throw new UserException(_loc.get("unique-missing-column",
                            columnName, table.getIdentifier(),
                            table.getColumnNames()));
    			Column col = table.getColumn(columnName);
    			u.addColumn(col);
    		}
        }

    }

    /**
     * Updates the max available sequence value.
     */
    private void allocateSequence(JDBCStore store, ClassMapping mapping,
            Status stat, int alloc, boolean updateStatSeq) throws SQLException {
        Runnable runnable =
            new AllocateSequenceRunnable(
                    store, mapping, stat, alloc, updateStatSeq);
        try {
            if (suspendInJTA()) {
                // NotSupportedException is wrapped in a StoreException by
                // the caller.
                try {
                _conf.getManagedRuntimeInstance().doNonTransactionalWork(
                        runnable);
                }
                catch(NotSupportedException nse) {
                    SQLException sqlEx = new SQLException(
                            nse.getLocalizedMessage(), nse);
                    throw sqlEx;
                }
            } else {
                runnable.run();
            }
        } catch (RuntimeException re) {
            Throwable e = re.getCause();
            if(e instanceof SQLException )
                throw (SQLException) e;
            else
                throw re;
        }
    }

    /**
     * Inserts the initial sequence column into the database.
     *
     * @param mapping
     *            ClassMapping for the class whose sequence column will be
     *            updated
     * @param conn
     *            Connection used issue SQL statements.
     */
    private void insertSequence(ClassMapping mapping, Connection conn)
        throws SQLException {

        if (_log.isTraceEnabled())
            _log.trace(_loc.get("insert-seq"));

        Object pk = getPrimaryKey(mapping);
        if (pk == null)
            throw new InvalidStateException(_loc.get("bad-seq-type",
                getClass(), mapping));

        DBDictionary dict = _conf.getDBDictionaryInstance();
        DBIdentifier tableName = resolveTableIdentifier(mapping, _pkColumn.getTable());
        SQLBuffer insert = new SQLBuffer(dict).append("INSERT INTO ").
            append(tableName).append(" (").
            append(_pkColumn).append(", ").append(_seqColumn).
            append(") VALUES (").
            appendValue(pk, _pkColumn).append(", ").
            appendValue(_intValue, _seqColumn).append(")");

        boolean wasAuto = conn.getAutoCommit();
        if (!wasAuto && !suspendInJTA())
            conn.setAutoCommit(true);

        PreparedStatement stmnt = null;
        try {
            stmnt = prepareStatement(conn, insert);
            dict.setTimeouts(stmnt, _conf, true);
            executeUpdate(_conf, conn, stmnt, insert, Row.ACTION_INSERT);
        } finally {
            if (stmnt != null)
                try { stmnt.close(); } catch (SQLException se) {}
            if (!wasAuto && !suspendInJTA())
                conn.setAutoCommit(false);
        }
    }

    /**
     * Get the current sequence value.
     *
     * @param mapping
     *            ClassMapping of the entity whose sequence value will be
     *            obtained.
     * @param conn
     *            Connection used issue SQL statements.
     *
     * @return The current sequence value, or <code>SEQUENCE_NOT_FOUND</code>
     *         if the sequence could not be found.
     */
    protected long getSequence(ClassMapping mapping, Connection conn)
        throws SQLException {
        if (_log.isTraceEnabled())
            _log.trace(_loc.get("get-seq"));

        Object pk = getPrimaryKey(mapping);
        if (pk == null)
            return -1;

        DBDictionary dict = _conf.getDBDictionaryInstance();
        SQLBuffer sel = new SQLBuffer(dict).append(_seqColumn);
        SQLBuffer where = new SQLBuffer(dict).append(_pkColumn).append(" = ").
            appendValue(pk, _pkColumn);
        DBIdentifier tableName = resolveTableIdentifier(mapping, _seqColumn.getTable());
        SQLBuffer tables = new SQLBuffer(dict).append(tableName);

        SQLBuffer select = dict.toSelect(sel, null, tables, where, null, null,
                null, false, dict.supportsSelectForUpdate, 0, Long.MAX_VALUE,
                false, true);

        PreparedStatement stmnt = null;
        ResultSet rs = null;
        try {
            stmnt = prepareStatement(conn, select);
            dict.setTimeouts(stmnt, _conf, false);
            rs = executeQuery(_conf, conn, stmnt, select);
            return getSequence(rs, dict);
        } finally {
            if (rs != null)
                try { rs.close(); } catch (SQLException se) {}
            if (stmnt != null)
                try { stmnt.close(); } catch (SQLException se) {}
        }
    }

    /**
     * Grabs the next handful of sequence numbers.
     *
     * @return true if the sequence was updated, false if no sequence
     * row existed for this mapping
     */
    protected boolean setSequence(ClassMapping mapping, Status stat, int inc,
        boolean updateStatSeq, Connection conn)
        throws SQLException {
        if (_log.isTraceEnabled())
            _log.trace(_loc.get("update-seq"));

        Object pk = getPrimaryKey(mapping);
        if (pk == null)
            throw new InvalidStateException(_loc.get("bad-seq-type",
                getClass(), mapping));

        DBDictionary dict = _conf.getDBDictionaryInstance();
        SQLBuffer where = new SQLBuffer(dict).append(_pkColumn).append(" = ").
            appendValue(pk, _pkColumn);

        // loop until we have a successful atomic select/update sequence
        long cur = 0;
        PreparedStatement stmnt;
        ResultSet rs;
        SQLBuffer upd;
        for (int updates = 0; updates == 0;) {
            stmnt = null;
            rs = null;
            try {
                cur = getSequence(mapping, conn);
                if (cur == -1)
                    return false;

                // update the value
                upd = new SQLBuffer(dict);
                DBIdentifier tableName = resolveTableIdentifier(mapping,
                        _seqColumn.getTable());
                upd.append("UPDATE ").append(tableName).
                    append(" SET ").append(_seqColumn).append(" = ").
                    appendValue(cur + inc, _seqColumn).
                    append(" WHERE ").append(where).append(" AND ").
                    append(_seqColumn).append(" = ").
                    appendValue(cur, _seqColumn);

                stmnt = prepareStatement(conn, upd);
                dict.setTimeouts(stmnt, _conf, true);
                updates = executeUpdate(_conf, conn, stmnt, upd,
                        Row.ACTION_UPDATE);
            } finally {
                if (rs != null)
                    try { rs.close(); } catch (SQLException se) {}
                if (stmnt != null)
                    try { stmnt.close(); } catch (SQLException se) {}
            }
        }

        // setup new sequence range
        synchronized (stat) {
            if (updateStatSeq && stat.seq < cur)
                stat.seq = cur;
            if (stat.max < cur + inc)
                stat.max = cur + inc;
        }
        return true;
    }
    /**
     * Resolve a fully qualified table name
     *
     * @param class
     *            mapping to get the schema name
     * @deprecated
     */
    @Deprecated
    public String resolveTableName(ClassMapping mapping, Table table) {
        return resolveTableIdentifier(mapping, table).getName();
    }

    /**
     * Resolve a fully qualified table name
     *
     * @param class
     *            mapping to get the schema name
     */
    public DBIdentifier resolveTableIdentifier(ClassMapping mapping, Table table) {
        DBIdentifier sName = mapping.getTable().getSchemaIdentifier();
        DBIdentifier tableName = DBIdentifier.NULL;

        //OPENJPA-2650: Don't use a schema name if the user has requested,
        //via useSchemaName, to not use one.
        if (!_conf.getDBDictionaryInstance().useSchemaName){
            tableName = table.getIdentifier();
        } else if (DBIdentifier.isNull(sName)) {
            tableName = table.getFullIdentifier();
        } else if (!DBIdentifier.isNull(table.getSchemaIdentifier())) {
            tableName = table.getFullIdentifier();
        } else {
            tableName = QualifiedDBIdentifier.newPath(sName, table.getIdentifier());
        }
        return tableName;
    }


    /**
     * Creates the sequence table in the DB.
     */
    public void refreshTable()
        throws SQLException {
        if (_log.isInfoEnabled())
            _log.info(_loc.get("make-seq-table"));

        // create the table
        SchemaTool tool = new SchemaTool(_conf);
        tool.setIgnoreErrors(true);
        tool.createTable(_pkColumn.getTable());
    }

    /**
     * Drops the sequence table in the DB.
     */
    public void dropTable()
        throws SQLException {
        if (_log.isInfoEnabled())
            _log.info(_loc.get("drop-seq-table"));

        // drop the table
        SchemaTool tool = new SchemaTool(_conf);
        tool.setIgnoreErrors(true);
        tool.dropTable(_pkColumn.getTable());
    }

    /////////
    // Main
    /////////

    /**
     * Usage: java org.apache.openjpa.jdbc.schema.TableJDBCSequence [option]*
     * -action/-a &lt;add | drop | get | set&gt; [value]
     *  Where the following options are recognized.
     * <ul>
     * <li><i>-properties/-p &lt;properties file or resource&gt;</i>: The
     * path or resource name of a OpenJPA properties file containing
     * information such as the license key	and connection data as
     * outlined in {@link JDBCConfiguration}. Optional.</li>
     * <li><i>-&lt;property name&gt; &lt;property value&gt;</i>: All bean
     * properties of the OpenJPA {@link JDBCConfiguration} can be set by
     * using their	names and supplying a value. For example:
     * <code>-licenseKey adslfja83r3lkadf</code></li>
     * </ul>
     *  The various actions are as follows.
     * <ul>
     * <li><i>add</i>: Create the sequence table.</li>
     * <li><i>drop</i>: Drop the sequence table.</li>
     * <li><i>get</i>: Print the current sequence value.</li>
     * <li><i>set</i>: Set the sequence value.</li>
     * </ul>
     */
    public static void main(String[] args)
        throws Exception {
        Options opts = new Options();
        final String[] arguments = opts.setFromCmdLine(args);
        boolean ret = Configurations.runAgainstAllAnchors(opts,
            new Configurations.Runnable() {
            @Override
            public boolean run(Options opts) throws Exception {
                JDBCConfiguration conf = new JDBCConfigurationImpl();
                try {
                    return TableJDBCSeq.run(conf, arguments, opts);
                } finally {
                    conf.close();
                }
            }
        });
        if (!ret) {
            // START - ALLOW PRINT STATEMENTS
            System.out.println(_loc.get("seq-usage"));
            // STOP - ALLOW PRINT STATEMENTS
        }
    }

    /**
     * Run the tool. Returns false if invalid options were given.
     */
    public static boolean run(JDBCConfiguration conf, String[] args,
        Options opts)
        throws Exception {
        String action = opts.removeProperty("action", "a", null);
        Configurations.populateConfiguration(conf, opts);
        return run(conf, args, action);
    }

    /**
     * Run the tool. Return false if an invalid option was given.
     */
    public static boolean run(JDBCConfiguration conf, String[] args,
        String action)
        throws Exception {
        if (args.length > 1 || (args.length != 0
            && !ACTION_SET.equals(action)))
            return false;

        TableJDBCSeq seq = new TableJDBCSeq();
        String props = Configurations.getProperties(conf.getSequence());
        Configurations.configureInstance(seq, conf, props);

        if (ACTION_DROP.equals(action))
            seq.dropTable();
        else if (ACTION_ADD.equals(action))
            seq.refreshTable();
        else if (ACTION_GET.equals(action) || ACTION_SET.equals(action)) {
            Connection conn = conf.getDataSource2(null).getConnection();
            try {
                long cur = seq.getSequence(null, conn);
                if (ACTION_GET.equals(action)) {
                    // START - ALLOW PRINT STATEMENTS
                    System.out.println(cur);
                    // STOP - ALLOW PRINT STATEMENTS
                } else {
                    long set;
                    if (args.length > 0)
                        set = Long.parseLong(args[0]);
                    else
                        set = cur + seq.getAllocate();
                    if (set < cur)
                        set = cur;
                    else {
                        Status stat = seq.getStatus(null);
                        seq.setSequence(null, stat, (int) (set - cur), true,
                            conn);
                        set = stat.seq;
                    }
                    // START - ALLOW PRINT STATEMENTS
                    System.err.println(set);
                    // STOP - ALLOW PRINT STATEMENTS
                }
            }
            catch (NumberFormatException nfe) {
                return false;
            } finally {
                try { conn.close(); } catch (SQLException se) {}
            }
        } else
            return false;
        return true;
    }

    /**
     * Helper struct to hold status information.
     */
    protected static class Status implements Serializable {
        private static final long serialVersionUID = 1L;
        public long seq = 1L;
        public long max = 0L;
    }

    /**
     * This method is to provide override for non-JDBC or JDBC-like
     * implementation of preparing statement.
     */
    protected PreparedStatement prepareStatement(Connection conn, SQLBuffer buf)
        throws SQLException {
        return buf.prepareStatement(conn);
    }

    /**
     * This method is to provide override for non-JDBC or JDBC-like
     * implementation of executing update.
     */
    protected int executeUpdate(JDBCConfiguration conf, Connection conn,
        PreparedStatement stmnt, SQLBuffer buf, int opcode) throws SQLException
    {
        return stmnt.executeUpdate();
    }

    /**
     * This method is to provide override for non-JDBC or JDBC-like
     * implementation of executing query.
     */
    protected ResultSet executeQuery(JDBCConfiguration conf, Connection conn,
        PreparedStatement stmnt, SQLBuffer buf) throws SQLException {
        return stmnt.executeQuery();
    }

    /**
     * This method is to provide override for non-JDBC or JDBC-like
     * implementation of getting sequence from the result set.
     */
    protected long getSequence(ResultSet rs, DBDictionary dict)
            throws SQLException {
        if (rs == null || !rs.next())
            return -1;
        return dict.getLong(rs, 1);
    }

    public void setUniqueConstraintName(String uniqueConstraintName) {
        _uniqueConstraintName = DBIdentifier.newConstraint(uniqueConstraintName);
    }

    public void setUniqueConstraintName(DBIdentifier uniqueConstraintName) {
        _uniqueConstraintName = uniqueConstraintName;
    }

    public String getUniqueConstraintName() {
        return _uniqueConstraintName.getName();
    }

    public DBIdentifier getUniqueConstraintIdentifier() {
        return _uniqueConstraintName;
    }

    /**
     * AllocateSequenceRunnable is a runnable wrapper that will inserts the
     * initial sequence value into the database.
     */
    protected class AllocateSequenceRunnable implements Runnable {

        JDBCStore store = null;
        ClassMapping mapping = null;
        Status stat = null;
        int alloc;
        boolean updateStatSeq;

        AllocateSequenceRunnable(JDBCStore store, ClassMapping mapping,
                Status stat, int alloc, boolean updateStatSeq) {
            this.store = store;
            this.mapping = mapping;
            this.stat = stat;
            this.alloc = alloc;
            this.updateStatSeq = updateStatSeq;
        }

        /**
         * This method actually obtains the current sequence value.
         *
         * @throws RuntimeException
         *             any SQLExceptions that occur when obtaining the sequence
         *             value are wrapped in a runtime exception to avoid
         *             breaking the Runnable method signature. The caller can
         *             obtain the "real" exception by calling getCause().
         */
        @Override
        public void run() throws RuntimeException {
            Connection conn = null;
            SQLException err = null;
            try {
                // Try to use the store's connection.

                conn = getConnection(store);
                boolean sequenceSet =
                    setSequence(mapping, stat, alloc, updateStatSeq, conn);
                closeConnection(conn);

                if (!sequenceSet) {
                    // insert a new sequence column. Prefer connection2 / non-jta-data-source when inserting a
                    // sequence column regardless of Seq.type.
                    conn = _conf.getDataSource2(store.getContext()).getConnection();
                    try {
                        insertSequence(mapping, conn);
                    } catch (SQLException e) {
                        // it is possible another thread already got in and inserted this sequence. Try to keep going
                        if (_log.isTraceEnabled()) {
                            _log.trace(
                                "Caught an exception while trying to insert sequence. Will try to reselect the " +
                                "seqence. ", e);
                        }
                    }

                    conn.close();

                    // now we should be able to update using the connection per
                    // on the seq type.
                    conn = getConnection(store);
                    if (!setSequence(mapping, stat, alloc, updateStatSeq, conn))
                    {
                        throw (err != null) ? err : new SQLException(_loc.get(
                                "no-seq-row", mapping, _table).getMessage());
                    }
                    closeConnection(conn);
                }
            } catch (SQLException e) {
                if (conn != null) {
                    closeConnection(conn);
                }
                RuntimeException re = new RuntimeException(e.getMessage(), e);
                throw re;
            }
        }
    }

    /**
     * CurentSequenceRunnable is a runnable wrapper which obtains the current
     * sequence value from the database.
     */
    protected class CurrentSequenceRunnable implements Runnable {
        private JDBCStore _store;
        private ClassMapping _mapping;

        CurrentSequenceRunnable(JDBCStore store, ClassMapping mapping) {
            _store = store;
            _mapping = mapping;
        }

        /**
         * This method actually obtains the current sequence value.
         *
         * @throws RuntimeException
         *             any SQLExceptions that occur when obtaining the sequence
         *             value are wrapped in a runtime exception to avoid
         *             breaking the Runnable method signature. The caller can
         *             obtain the "real" exception by calling getCause().
         */
        @Override
        public void run() throws RuntimeException {
            Connection conn = null;
            try {
                conn = getConnection(_store);
                long cur = getSequence(_mapping, conn);
                if (cur != -1 ) // USE the constant
                    current = cur;
            } catch (SQLException sqle) {
                RuntimeException re = new RuntimeException(sqle.getMessage(), sqle);
                throw re;
            } finally {
                if (conn != null) {
                    closeConnection(conn);
                }
            }
        }
    }
}
