/FileExtraction/Java_unseen/dlitz_resin/modules/quercus/src/com/caucho/quercus/lib/db/JdbcConnectionResource.java
/*
 * Copyright (c) 1998-2011 Caucho Technology -- all rights reserved
 *
 * This file is part of Resin(R) Open Source
 *
 * Each copy or derived work must preserve the copyright notice and this
 * notice unmodified.
 *
 * Resin Open Source is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * Resin Open Source is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, or any warranty
 * of NON-INFRINGEMENT.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Resin Open Source; if not, write to the
 *
 *   Free Software Foundation, Inc.
 *   59 Temple Place, Suite 330
 *   Boston, MA 02111-1307  USA
 *
 * @author Scott Ferguson
 */

package com.caucho.quercus.lib.db;

import com.caucho.quercus.QuercusModuleException;
import com.caucho.quercus.env.BooleanValue;
import com.caucho.quercus.env.ConnectionEntry;
import com.caucho.quercus.env.Env;
import com.caucho.quercus.env.EnvCleanup;
import com.caucho.quercus.env.StringValue;
import com.caucho.quercus.env.Value;
import com.caucho.util.L10N;
import com.caucho.util.LruCache;
import com.caucho.util.JdbcUtil;

import java.sql.*;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Represents a JDBC Connection value.
 */
public abstract class JdbcConnectionResource
    implements EnvCleanup
{
  private static final L10N L = new L10N(JdbcConnectionResource.class);
  private static final Logger log
    = Logger.getLogger(JdbcConnectionResource.class.getName());

  private static LruCache<TableKey,JdbcTableMetaData> _tableMetadataMap
    = new LruCache<TableKey,JdbcTableMetaData>(256);

  protected ConnectionEntry _conn;

  // cached statement
  private Statement _savedStmt;
  
  private Statement _freeStmt;

  private DatabaseMetaData _dmd;

  private JdbcResultResource _rs;
  private int _affectedRows;

  private String _errorMessage = null;
  private int _errorCode;
  private SQLWarning _warnings;

  private Env _env;
  protected String _host;
  protected int _port;
  private String _userName;
  private String _password;
  protected String _driver;
  protected String _url;
  protected int _flags;
  protected String _socket;

  private String _catalog;
  private boolean _isCatalogOptimEnabled = false;

  private boolean _isUsed;

  protected SqlParseToken _sqlParseToken = new SqlParseToken();
  
  // php/144b, php/1464, php/1465
  protected static final String ENCODING = "ISO8859_1";

  public JdbcConnectionResource(Env env)
  {
    _env = env;
    
    env.addCleanup(this);
  }

  /**
   * Returns the error string for the most recent function call.
   * This method is not invoked from PHP code.
   */
  public StringValue error(Env env)
  {
    if (isConnected())
      return env.createString(getErrorMessage());
    else
      return env.getEmptyString();
  }

  public boolean isConnected()
  {
    return _conn != null;
  }

  public Env getEnv()
  {
    return _env;
  }

  public String getHost()
  {
    return _host;
  }

  public String getUserName()
  {
    return _userName;
  }

  public String getPassword()
  {
    return _password;
  }

  public String getDbName()
  {
    return _catalog;
  }

  public int getPort()
  {
    return _port;
  }

  public String getDriver()
  {
    return _driver;
  }

  public String getUrl()
  {
    return _url;
  }

  /**
   * Set the current underlying connection and
   * corresponding information: host, port and
   * database name.
   *
   * @param host server host
   * @param port server port
   * @param dbname database name
   */
  final protected boolean connectInternal(Env env,
                                          String host,
                                          String userName,
                                          String password,
                                          String dbname,
                                          int port,
                                          String socket,
                                          int flags,
                                          String driver,
                                          String url,
                                          boolean isNewLink)
  {
    if (_conn != null)
      throw new IllegalStateException(
        getClass().getSimpleName() + " attempt to open multiple connections");
    
    _host = host;
    _userName = userName;
    _password = password;
    _port = port;
    _socket = socket;
    _flags = flags;
    _driver = driver;
    _url = url;

    if (dbname == null)
      dbname = "";
    
    _catalog = dbname;

    _conn = connectImpl(env, host, userName, password,
                        dbname, port, socket, flags, driver, url,
                        isNewLink);

    if (_conn != null) {
      try {
        if ("".equals(_catalog)) 
          _catalog = _conn.getConnection().getCatalog();
      } catch (SQLException e) {
        log.log(Level.FINE, e.toString(), e);
      }
    }

    return _conn != null && _conn.getConnection() != null;
  }

  /**
   * Connects to the underlying database.
   */
  protected abstract ConnectionEntry connectImpl(Env env,
                                                 String host,
                                                 String userName,
                                                 String password,
                                                 String dbname,
                                                 int port,
                                                 String socket,
                                                 int flags,
                                                 String driver,
                                                 String url,
                                                 boolean isNewLink);

  /**
   * Escape the given string for SQL statements.
   *
   * @param str a string
   * @return the string escaped for SQL statements
   */
  protected StringValue realEscapeString(StringValue str)
  {
    StringValue buf = _env.createUnicodeBuilder();

    final int strLength = str.length();

    for (int i = 0; i < strLength; i++) {
      char c = str.charAt(i);

      switch (c) {
      case '\u0000':
        buf.append('\\');
        buf.append(0);
        break;
      case '\n':
        buf.append('\\');
        buf.append('n');
        break;
      case '\r':
        buf.append('\\');
        buf.append('r');
        break;
      case '\\':
        buf.append('\\');
        buf.append('\\');
        break;
      case '\'':
        buf.append('\\');
        buf.append('\'');
        break;
      case '"':
        buf.append('\\');
        buf.append('\"');
        break;
      case '\032':
        buf.append('\\');
        buf.append('Z');
        break;
      default:
        buf.append(c);
        break;
      }
    }

    return buf;
  }

  /**
   * Returns the affected rows from the last query.
   */
  public int getAffectedRows()
  {
    return _affectedRows;
  }

  public void setAffectedRows(int i)
  {
    _affectedRows = i;
  }

  /**
   * @return _fieldCount
   */
  public int getFieldCount()
  {
    if (_rs == null) {
      return 0;
    } else {
      return _rs.getFieldCount();
    }
  }

  /**
   * Returns JdbcResultResource of available databases
   */
  protected JdbcResultResource getCatalogs()
  {
    clearErrors();

    try {
      if (_dmd == null)
        _dmd = _conn.getConnection().getMetaData();

      ResultSet rs = _dmd.getCatalogs();

      if (rs != null)
        return createResult(_env, _savedStmt, rs);
      else
        return null;
    } catch (SQLException e) {
      saveErrors(e);
      log.log(Level.FINEST, e.toString(), e);
      return null;
    }
  }

  /**
   * @return current catalog or false if error
   */
  protected Value getCatalog()
  {
    return _env.createString(_catalog);
  }

  /**
   * Returns the client encoding.
   *
   * XXX: stubbed out. has to be revised once we
   * figure out what to do with character encoding
   */
  public String getCharacterSetName()
  {
    return "latin1";
  }

  /**
   * Alias for getCharacterSetName
   */
  public String getClientEncoding()
  {
    return getCharacterSetName();
  }

  /**
   * Set encoding on the client side of the connection.
   * Return true if the encoding was set, otherwise false.
   */

  public boolean setClientEncoding(String encoding)
  {
    return true;
  }

  /**
   * Returns the client version
   * @deprecated
   */
  public String getClientInfo()
  {
    try {
      if (_dmd == null)
        _dmd = _conn.getConnection().getMetaData();

      return _dmd.getDatabaseProductVersion();
    } catch (SQLException e) {
      log.log(Level.FINE, e.toString(), e);
      return null;
    }
  }

  /**
   * Returns the connection
   */
  public final Connection getConnection(Env env)
  {
    _isUsed = true;

    Connection conn = null;
    
    if (_conn != null)
      conn = _conn.getConnection();

    if (conn != null)
      return conn;
    else if (_errorMessage != null) {
      env.warning(_errorMessage);
      return null;
    }
    else {
      env.warning(L.l("Connection is not available: {0}", _conn));
      
      _errorMessage = L.l("Connection is not available: {0}", _conn);
      
      return null;
    }
  }

  /**
   * Returns the unwrapped SQL connection
   * associated to this statement.
   */
  protected Connection getJavaConnection()
    throws SQLException
  {
    // XXX: jdbc for jdk 1.6 updates
    return _env.getQuercus().getConnection(_conn.getConnection());
  }

  /**
   * Returns the data source.
   */
  public String getURL()
  {
    // return getJavaConnection().getURL();
    return _url;
  }

  /**
   * Returns the last error code.
   */
  public int getErrorCode()
  {
    return _errorCode;
  }

  /**
   * Returns the last error message.
   */
  public String getErrorMessage()
  {
    return _errorMessage;
  }

  /**
   *
   * returns the URL string for the given connection
   * IE: jdbc:mysql://localhost:3306/test
   * XXX: PHP returns Localhost via UNIX socket
   */
  public String getHostInfo()
    throws SQLException
  {
    if (_dmd == null)
      _dmd = _conn.getConnection().getMetaData();

    return _dmd.getURL();
  }

  /**
   * returns the server version
   */
  public String getServerInfo()
    throws SQLException
  {
    return getMetaData().getDatabaseProductVersion();
  }

  /**
   * Returns the table metadata.
   */
  public JdbcTableMetaData getTableMetaData(Env env,
                                            String catalog,
                                            String schema,
                                            String table)
    throws SQLException
  {
    try {
      if (table == null || table.equals(""))
        return null;
    
      TableKey key = new TableKey(getURL(), catalog, schema, table);

      // XXX: needs invalidation on DROP or ALTER
      JdbcTableMetaData tableMd = _tableMetadataMap.get(key);
    
      if (tableMd != null && tableMd.isValid(env))
        return tableMd;
    
      tableMd = new JdbcTableMetaData(env,
                                      catalog,
                                      schema,
                                      table,
                                      getMetaData());

      _tableMetadataMap.put(key, tableMd);

      return tableMd;
    } catch (SQLException e) {
      log.log(Level.FINE, e.toString(), e);

      return null;
    }
  }

  private DatabaseMetaData getMetaData()
    throws SQLException
  {
    if (_dmd == null)
      _dmd = _conn.getConnection().getMetaData();

    return _dmd;
  }

  static int infoToVersion(String info)
  {
    String[] result = info.split("[.a-z-]");

    if (result.length < 3)
      return 0;

    return (Integer.parseInt(result[0]) * 10000
            + Integer.parseInt(result[1]) * 100
            + Integer.parseInt(result[2]));
  }

  public void closeStatement(Statement stmt)
  {
    if (stmt == null)
      return;

    if (_freeStmt == null && false)
      _freeStmt = stmt;
    else
      JdbcUtil.close(stmt);
  }

  /**
   * Closes the connection.
   */
  public boolean close(Env env)
  {
    // php/1418
    // cleanup();

    ConnectionEntry conn = _conn;
    _conn = null;

    if (conn != null)
      conn.phpClose();

    return true;
  }

  /**
   * Implements the EnvCleanup interface. This method
   * will deallocate resources associated with this
   * connection. This method can be invoked via a
   * call to close(), or it can be invoked when the
   * environment is being cleaned up after a quercus
   * request has been processed.
   */
  public void cleanup()
  {
    if (log.isLoggable(Level.FINER)) {
      log.finer(this +  " cleanup()");
    }

    try {
      Statement savedStmt = _savedStmt;
      _savedStmt = null;
      
      Statement freeStmt = _freeStmt;
      _freeStmt = null;

      if (savedStmt != null)
        savedStmt.close();

      if (freeStmt != null)
        freeStmt.close();
    } catch (Throwable e) {
      // must catch throwable to force the conn close to work
      
      log.log(Level.FINER, e.toString(), e);
    }

    ConnectionEntry conn = _conn;
    _conn = null;

    if (conn != null) {
      conn.phpClose();
    }
  }

  public JdbcConnectionResource validateConnection()
  {
    if (_conn == null) {
      throw _env.createErrorException(
        L.l("Connection is not properly initialized {0}\nDriver {1}",
            _url, _driver));
    }

    return this;
  }

  /**
   * Execute a single query.
   */
  protected Value realQuery(Env env, String sql)
  {
    clearErrors();

    _rs = null;

    Statement stmt = _freeStmt;
    _freeStmt = null;

    try {
      Connection conn = getConnection(env);
      
      if (conn == null)
        return BooleanValue.FALSE;

      if (checkSql(_conn, sql))
        return BooleanValue.TRUE;

      // statement reuse does not gain performance significantly (< 1%)
      // php/142v
      if (true || stmt == null) {
        // XXX: test for performance
        
        boolean isSeekable = isSeekable();
        if (isSeekable)
          stmt = conn.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE,
                          ResultSet.CONCUR_READ_ONLY);
        else
          stmt = conn.createStatement();
          
        stmt.setEscapeProcessing(false); // php/1406
      }

      if (stmt.execute(sql)) {
        // Statement.execute(String) returns true when SQL statement is a
        // SELECT statement that returns a result set.

        ResultSet rs = stmt.getResultSet();
        _rs = createResult(_env, stmt, rs);
        _affectedRows = 0;

        // XXX: if these are needed, get them lazily for performance
        // _warnings = stmt.getWarnings();
      } else {
        // Statement.execute(String) returns false when SQL statement does
        // not returns a result set (UPDATE, INSERT, DELETE, or REPLACE).

        // php/430a should return a result set
        // for update statements. It is always
        // null though. So keep the stmt for
        // future reference (PostgresModule.pg_last_oid)

        // php/1f33

        // This is overriden in Postgres.java
        keepResourceValues(stmt);

        _affectedRows = 0;
        _affectedRows = stmt.getUpdateCount();
        if (_rs != null)
          _rs.setAffectedRows(_affectedRows);

        // XXX: if these are needed, get them lazily for performance
        // _warnings = stmt.getWarnings();

        // for php/430a
        if (keepStatementOpen()) {
          _savedStmt = stmt;
        }
        else {
          // _warnings = stmt.getWarnings();
              _freeStmt = stmt;
        }
      }
    } catch (DataTruncation truncationError) {
      try {
        _affectedRows = stmt.getUpdateCount();
        //_warnings = stmt.getWarnings();
      } catch (SQLException e) {
        saveErrors(e);
        log.log(Level.FINEST, e.toString(), e);
        return BooleanValue.FALSE;
      }
    } catch (SQLException e) {
      saveErrors(e);

      // php/431h
      if (keepStatementOpen()) {
        keepResourceValues(stmt);
      } else {
        log.log(Level.FINEST, e.toString(), e);
        return BooleanValue.FALSE;
      }
    } catch (IllegalStateException e) {
      // #2184, some drivers return this on closed connection
      saveErrors(new SQLExceptionWrapper(e));

      return BooleanValue.FALSE;
    }

    if (_rs == null)
      return BooleanValue.TRUE;
    
    return env.wrapJava(_rs);
  }

  private boolean checkSql(ConnectionEntry connEntry, String sql)
  {
    SqlParseToken tok = parseSqlToken(sql, null);

    if (tok == null)
      return false;

    switch (tok.getFirstChar()) {
      case 'a': case 'A': {
        // drop/alter clears metadata cache
        _tableMetadataMap.clear();
        break;
      }
      case 'd': case 'D': {
        if (tok.matchesToken("DROP")) {
          // drop/alter clears metadata cache
          _tableMetadataMap.clear();

          // If DROP is dropping the current database, then clear
          // the cached database name in the driver.
          //
          // php/144a

          tok = parseSqlToken(sql, tok);

          if ((tok != null) && tok.matchesToken("DATABASE")) {
            tok = parseSqlToken(sql, tok);

            if (tok != null) {
              String dbname = tok.toUnquotedString();

              if (dbname.equals(_catalog)) {
                try {
                  setCatalog(null);
                } catch (SQLException e) {
                  log.log(Level.FINEST, e.toString(), e);
                }
              }
            }
          }
        }
        break;
      }
      case 'c': case 'C': {
        if (tok.matchesToken("CREATE")) {
          // don't pool connections that create tables, because of mysql
          // temporary tables
          connEntry.markForPoolRemoval();
        }
        /*
        else if (tok.matchesToken("COMMIT")) {
          commit();
          setAutoCommit(true);
          return true;
        }
        */
        break;
      }

        // reason for comment out?  no real perf gain?
        /*
      case 'b': case 'B': {
        if (tok.matchesToken("BEGIN")) {
          // Test for mediawiki performance
          setAutoCommit(false);
          return true;
        }
        break;
      }

      case 'r': case 'R': {
        if (tok.matchesToken("ROLLBACK")) {
          rollback();
          setAutoCommit(true);
          return true;
        }
        break;
      }
        */
    }

    return false;
  }

  /**
   * Parse a token from a string containing a SQL statement.
   * If the prevToken is null, then the first token in parsed.
   * If a SQL token can't be found in the string, then null
   * is returned. If a SQL token is found, data is captured in
   * the returned SqlParseToken result.
   */
  protected SqlParseToken parseSqlToken(String sql, SqlParseToken prevToken)
  {
    if (sql == null) {
      _sqlParseToken.init();
      return null;
    }

    final int len = sql.length();
    int i, start;

    // Start at index 0, or where we left off last time

    if (prevToken == null)
      i = 0;
    else
      i = prevToken._end;

    while (i < len && Character.isWhitespace(sql.charAt(i))) {
      i++;
    }

    // Must be at least 1 non-whitespace character

    if ((i + 1) >= len) {
      _sqlParseToken.init();
      return null;
    }

    start = i;

    while (i < len && !Character.isWhitespace(sql.charAt(i))) {
      i++;
    }

    _sqlParseToken.assign(sql, start, i);

    return _sqlParseToken;
  }

  /**
   * Creates a database-specific result.
   */
  protected JdbcResultResource createResult(Env env,
                                            Statement stmt,
                                            ResultSet rs)
  {
    return new JdbcResultResource(env, stmt, rs, this);
  }


  /**
   * sets auto-commmit to true or false
   */
  public boolean setAutoCommit(boolean mode)
  {
    clearErrors();

    try {
      _conn.getConnection().setAutoCommit(mode);
    } catch (SQLException e) {
      saveErrors(e);
      log.log(Level.FINEST, e.toString(), e);
      return false;
    }

    return true;
  }

  /**
   * commits the transaction of the current connection
   */
  public boolean commit()
  {
    clearErrors();

    try {
      _conn.getConnection().commit();
    } catch (SQLException e) {
      saveErrors(e);
      log.log(Level.FINEST, e.toString(), e);
      return false;
    }

    return true;
  }

  /**
   * rolls the current transaction back
   *
   * NOTE: quercus doesn't seem to support the idea
   * of savepoints
   */
  public boolean rollback()
  {
    clearErrors();

    try {
      _conn.getConnection().rollback();
    } catch (SQLException e) {
      saveErrors(e);
      log.log(Level.FINEST, e.toString(), e);
      return false;
    }

    return true;
  }
  /**
   * Sets the catalog
   */
  public void setCatalog(String name)
    throws SQLException
  {
    if (_catalog != null && _catalog.equals(name))
      return;

    clearErrors();
    
    // php/142v
    // mysql jdbc: can't reuse old statements after a USE query
    _savedStmt = null;
    _freeStmt = null;

    if (! _isUsed && _isCatalogOptimEnabled) {
      // The database is only connected, but not used, reopen with
      // a real catalog

      ConnectionEntry conn = _conn;
      _conn = null;

      if (conn != null)
        conn.phpClose();

      connectInternal(_env, _host, _userName, _password, name,
                      _port, _socket, _flags, _driver, _url, false);
    }
    else {
      _conn.setCatalog(name);
    }
    
    _catalog = name;
  }

  /**
   * Converts to an object.
   */
  public Object toObject()
  {
    return null;
  }

  /**
   * Converts to a string.
   */
  public String toString()
  {
    if (_conn != null)
      return getClass().getSimpleName() + "[" + _conn.getConnection() + "]";
    else
      return getClass().getSimpleName() + "[" + null + "]";
  }

  /**
   * This function is overriden in Postgres to keep
   * result set references for php/430a (see also php/1f33)
   */
  protected void keepResourceValues(Statement stmt)
  {
    return;
  }

  /**
   * This function is overriden in Postgres to keep
   * statement references for php/430a
   */
  protected boolean keepStatementOpen()
  {
    return false;
  }

  /**
   * Get the current result resource
   */
  protected JdbcResultResource getResultResource()
  {
    return _rs;
  }

  /**
   * Set the current result resource
   */
  protected void setResultResource(JdbcResultResource rs)
  {
    _rs = rs;
  }

  /**
   * This function was added for PostgreSQL pg_last_notice
   *
   * @return warning messages
   */
  protected SQLWarning getWarnings()
  {
    return _warnings;
  }

  /**
   * Pings the database
   */
  public boolean ping(Env env)
  {
    try {

      return isConnected() && ! getConnection(env).isClosed();

    } catch (SQLException e) {
      log.log(Level.FINE, e.toString(), e);
      env.warning(e.toString(), e);
      
      return false;
    }
  }

  /**
   * Set the current SQL warnings.
   *
   * @param warnings the new SQL warnings
   */
  protected void setWarnings(SQLWarning warnings)
  {
    _warnings = warnings;
  }

  protected void clearErrors()
  {
    _errorMessage = null;
    _errorCode = 0;
    _warnings = null;
  }

  protected void saveErrors(SQLException e)
  {
    _errorMessage = e.getMessage();
    if (_errorMessage == null || "".equals(_errorMessage))
      _errorMessage = e.toString();
    
    _errorCode = e.getErrorCode();
  }
  
  /**
   * Returns true if this connection supports TYPE_SCROLL_INSENSITIVE.
   * http://bugs.caucho.com/view.php?id=3746
   */
  protected boolean isSeekable()
  {
    return true;
  }

  static class TableKey {
    private final String _url;
    private final String _catalog;
    private final String _schema;
    private final String _table;

    TableKey(String url, String catalog, String schema, String table)
    {
      _url = url;
      _catalog = catalog;
      _schema = schema;
      _table = table;
    }

    public int hashCode()
    {
      int hash = 37;

      if (_url != null)
        hash = 65537 * hash + _url.hashCode();

      if (_catalog != null)
        hash = 65537 * hash + _catalog.hashCode();

      if (_schema != null)
        hash = 65537 * hash + _schema.hashCode();

      if (_table != null)
        hash = 65537 * hash + _table.hashCode();

      return hash;
    }

    public boolean equals(Object o)
    {
      if (this == o)
        return true;
      else if (! (o instanceof TableKey))
        return false;

      TableKey key = (TableKey) o;

      if ((_url == null) != (key._url == null))
        return false;
      else if (_url != null && ! _url.equals(key._url))
        return false;

      if ((_catalog == null) != (key._catalog == null))
        return false;
      else if (_catalog != null && ! _catalog.equals(key._catalog))
        return false;

      if ((_schema == null) != (key._schema == null))
        return false;
      else if (_schema != null && ! _schema.equals(key._schema))
        return false;

      if ((_table == null) != (key._table == null))
        return false;
      else if (_table != null && ! _table.equals(key._table))
        return false;

      return true;
    }
  }

  /*
   * This class enables efficient parsing of a SQL token from
   * a String. An SQL statement can be parsed one token at a
   * time. One can efficiently check that first letter of
   * the parse token via matchesFirstChar() without creating
   * a substring from the original.
   */

  protected static class SqlParseToken {
    private String _query;
    private String _token;
    private int _start;
    private int _end;
    private char _firstChar;

    public void init()
    {
      _query = null;
      _token = null;
      _start = -1;
      _end = -1;
      _firstChar = '\0';
    }

    public void assign(String query, int start, int end)
    {
      _query = query;
      _token = null;
      _start = start;
      _end = end;
      _firstChar = query.charAt(start);
    }

    public boolean matchesFirstChar(char upper, char lower)
    {
      return (_firstChar == upper) || (_firstChar == lower);
    }

    public char getFirstChar()
    {
      return _firstChar;
    }

    // Case insensitive compare of token string

    public boolean matchesToken(String token)
    {
      if (_token == null)
        _token = _query.substring(_start, _end);

      return _token.equalsIgnoreCase(token);
    }

    public String toString()
    {
      if (_token == null)
        _token = _query.substring(_start, _end);
      
      return _token;
    }

    /*
     * Return the SQL token as a string. If the token
     * is back quoted, then remove the back quotes and
     * return the string inside.
     */

    public String toUnquotedString()
    {
      String tok = toString();

      // Extract database name if back quoted : "DROP DATABASE `DBNAME`"

      if (tok.length() >= 2
          && tok.charAt(0) == '`'
          && tok.charAt(tok.length() - 1) == '`') {
        tok = tok.substring(1, tok.length() - 1);
      }

      return tok;
    }
  }
}

