Database.java

/*
 * Copyright (C) 2000 William Chesters
 *
 * Part of Melati (http://melati.org), a framework for the rapid
 * development of clean, maintainable web applications.
 *
 * Melati is free software; Permission is granted to copy, distribute
 * and/or modify this software under the terms either:
 *
 * a) 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,
 *
 *    or
 *
 * b) any version of the Melati Software License, as published
 *    at http://melati.org
 *
 * You should have received a copy of the GNU General Public License and
 * the Melati Software License along with this program;
 * if not, write to the Free Software Foundation, Inc.,
 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA to obtain the
 * GNU General Public License and visit http://melati.org to obtain the
 * Melati Software License.
 *
 * Feel free to contact the Developers of Melati (http://melati.org),
 * if you would like to work out a different arrangement than the options
 * outlined here.  It is our intention to allow Melati to be used by as
 * wide an audience as possible.
 *
 * This program 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.  See the
 * GNU General Public License for more details.
 *
 * Contact details for copyright holder:
 *
 *     William Chesters <williamc At paneris.org>
 *     http://paneris.org/~williamc
 *     Obrechtstraat 114, 2517VX Den Haag, The Netherlands
 */

package org.melati.poem;

import org.melati.poem.dbms.Dbms;
import org.melati.poem.dbms.DbmsFactory;
import org.melati.poem.transaction.Transaction;
import org.melati.poem.transaction.TransactionPool;
import org.melati.poem.util.*;

import java.sql.*;
import java.util.*;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * An RDBMS database.  Don't instantiate (or subclass) this class, but rather
 * {@link PoemDatabase}, which includes the boilerplate code for the standard
 * tables such as <TT>user</TT> and <TT>columninfo</TT> which all POEM
 * databases must contain.  If the database is predefined by a Data Structure
 * Definition <TT><I>Bar</I>.dsd</TT>, there will be an application-specialised
 * subclass of <TT>PoemDatabase</TT> called <TT><I>Bar</I>Database</TT> which
 * provides named methods for accessing application-specialised objects
 * representing the predefined tables.
 *
 * @see PoemDatabase
 */

public abstract class Database implements TransactionPool {

  final Database _this = this;
  private final ReadWriteLock lock = new ReentrantReadWriteLock();
  private final boolean[] connecting = new boolean[1];
  private Vector<Transaction> transactions = null;
  private Vector<Transaction> freeTransactions = null;
  private Connection committedConnection;
  private long structureSerial = 0L;
  private Vector<Table<?>> tables = new Vector<Table<?>>();
  private Hashtable<String, Table<?>> tablesByName = new Hashtable<String, Table<?>>();
  private Table<?>[] displayTables = null;
  private String name;
  private String displayName;
  private Dbms dbms;
  private boolean logSQL = false;
  private boolean logCommits = false;
  private int transactionsMax;
  private String connectionUrl;
  /**
   * Used in testing to check caching.
   */
  private int queryCount = 0;
  

  //
  // ================
  //  Initialisation
  // ================
  //
  /**
   * Used in tests to check caching etc.
   */
  private String lastQuery = null;
  private boolean initialised = false;
  private User guest = null;
  private User administrator = null;
  private UserCapabilityCache capabilityCache = new UserCapabilityCache();
  private Capability canAdminister = null;

  /**
   * Don't subclass this, subclass <TT>PoemDatabase</TT>.
   * @see PoemDatabase
   */

  public Database() {
  }
  
  /**
   * Initialise each table.
   */
  private synchronized void init() {
    if (!initialised) {
      for (Table<?> t : this.tables)
        t.init();
      initialised = true;
    }
  }

  /**
   * Connect to an RDBMS database.  This should be called once when the
   * application starts up; it will
   *
   * <UL>
   *   <LI> Open <TT>this.transactionsMax()</TT> JDBC <TT>Connection</TT>s to
   *        the database for subsequent `pooling'
   *   </LI>
   *   <LI> Unify (reconcile) the structural information about the database
   *        given in
   *
   *        <OL>
   *          <LI> the Database Structure Definition (<I>i.e.</I> embodied in
   *               the boilerplate code generated from it), including the
   *               POEM-standard tables defined in <TT>Poem.dsd</TT>;
   *          <LI> the metadata tables <TT>tableinfo</TT> and
   *               <TT>columninfo</TT>;
   *          <LI> the actual JDBC metadata from the RDBMS.
   *        </OL>
   *
   *        Any tables or columns defined in the DSD or the metadata tables,
   *        but not present in the actual database, will be created.
   *        <BR>
   *        Conversely, entries will be created in the metadata tables for
   *        tables and columns that don't have them.  If an inconsistency is
   *        detected between any of the three information sources (such as a
   *        fundamental type incompatibility, or a string field which is
   *        narrower in the database than it was declared to be), an exception
   *        will be thrown.  In that case the database will in theory be left
   *        untouched, except that in Postgres (at least) all structural
   *        updates happen immediately and irrevocably even if made from a
   *        transaction subsequently rolled back.
   *   </LI>
   * </UL>
   *
   * @param dbmsclass   The Melati DBMS class (see org/melati/poem/dbms)
   *                    to use, usually specified in
   *                    org.melati.LogicalDatabase.properties.
   *
   * @param url         The JDBC URL for the database; for instance
   *                    <TT>jdbc:postgresql:williamc</TT>.  It is the
   *                    programmer's responsibility to make sure that an
   *                    appropriate driver has been loaded.
   *
   * @param username    The username under which to establish JDBC connections
   *                    to the database.  This has nothing to do with the
   *                    user/group/capability authentication performed by
   *                    Melati.
   *
   * @param password    The password to go with the username.
   *
   * @param transactionsMaxP
   *                    The maximum number of concurrent Transactions allowed,
   *                    usually specified in
   *                    org.melati.LogicalDatabase.properties.
   *
   * @see #transactionsMax()
   */
  @SuppressWarnings("unchecked")
  public void connect(String nameIn, String dbmsclass, String url,
                      String username, String password,
                      int transactionsMaxP) throws PoemException {

    this.name = nameIn;
    this.connectionUrl = url;

    synchronized (connecting) {
      if (connecting[0])
        throw new ConnectingException();
      connecting[0] = true;
    }

    if (committedConnection != null)
      throw new ReconnectionPoemException(this);

    setDbms(DbmsFactory.getDbms(dbmsclass));

    setTransactionsMax(transactionsMaxP);
    committedConnection = getDbms().getConnection(url, username, password);
    transactions = new Vector<Transaction>();
    for (int s = 0; s < transactionsMax(); ++s)
      transactions.add(
        new PoemTransaction(
            this,
            getDbms().getConnection(url, username, password),
            s));

    freeTransactions = (Vector<Transaction>)transactions.clone();

    try {
      // Perform any table specific initialisation, none by default
      init();

      // Bootstrap: set up the tableinfo and columninfo tables
      DatabaseMetaData m = committedConnection.getMetaData();
      getTableInfoTable().unifyWithDB(
          m.getColumns(null, getSchema(),
              unreservedName(getTableInfoTable().getName()), null), unreservedName("id"));
      getColumnInfoTable().unifyWithDB(
          m.getColumns(null, getSchema(),
              unreservedName(getColumnInfoTable().getName()), null), unreservedName("id"));
      getTableCategoryTable().unifyWithDB(
          m.getColumns(null, getSchema(),
              unreservedName(getTableCategoryTable().getName()), null), unreservedName("id"));

      inSession(AccessToken.root,
                new PoemTask() {
                  public void run() throws PoemException {
                    try {
                      _this.unifyWithDB();
                    }
                    catch (SQLException e) {
                      throw new SQLPoemException(e);
                    }
                  }

                  public String toString() {
                    return "Unifying with DB";
                  }
                });
    } catch (Exception e) {
        if (committedConnection != null) disconnect();
        throw new UnificationPoemException(e);
    } finally {
      synchronized (connecting) {
        connecting[0] = false;
      }
    }
  }

  /**
   * Releases database connections.
   */
  public void disconnect() throws PoemException {
    if (committedConnection == null)
      throw new ReconnectionPoemException(this);

    try {
      for (Transaction poemTransaction : freeTransactions){
        ((PoemTransaction)poemTransaction).getConnection().close();
      }
      freeTransactions.removeAllElements();

      getDbms().shutdown(committedConnection);
      committedConnection.close();
    } catch (SQLException e) {
      throw new SQLPoemException(e);
    }
    committedConnection = null;
  }

  /**
   * Don't call this.  Tables should be defined either in the DSD (in which
   * case the boilerplate code generated by the preprocessor will call this
   * method), or directly in the RDBMS (in which case the initialisation code
   * will), or using <TT>addTableAndCommit</TT>.
   *
   * @see #addTableAndCommit
   */
  protected synchronized void defineTable(Table<?> table)
      throws DuplicateTableNamePoemException {
    if (getTableIgnoringCase(table.getName()) != null)
      throw new DuplicateTableNamePoemException(this, table.getName());
    redefineTable(table);
  }
  
  protected synchronized void redefineTable(Table<?> table) {
    if (table.getDatabase() != this)
      throw new TableInUsePoemException(this, table);

    if (getTableIgnoringCase(table.getName()) == null) {
      tablesByName.put(table.getName().toLowerCase(), table);
      tables.addElement(table);
    }
    else
      tables.setElementAt(table,
                          tables.indexOf(
                              tablesByName.put(table.getName().toLowerCase(), table)));
    displayTables = null;
  }

  private ResultSet columnsMetadata(DatabaseMetaData m, String tableName)
      throws SQLException {
    return m.getColumns(null, getSchema(), unreservedName(tableName), null);
  }

  protected String getSchema() {
    return null;
  }

  protected String unreservedName(String name) {
    return dbms.unreservedName(name);
  }

  protected String getJdbcMetadataName(String unreservedName) {
    return dbms.getJdbcMetadataName(unreservedName);
  }

  protected String melatiName(String name) {
    return dbms.melatiName(name);
  }
  /**
   * Add a Table to this Database and commit the Transaction.
   * @param info Table metadata object
   * @param troidName name of troidColumn
   * @return new minted {@link Table}
   */
  @SuppressWarnings({ "unchecked", "rawtypes" })
  public Table<?> addTableAndCommit(TableInfo info, String troidName)
      throws PoemException {

    // For permission control we rely on them having successfully created a
    // TableInfo

    Table<?> table = new JdbcTable<Persistent>(this, info.getName(),
                            DefinitionSource.infoTables);
    table.defineColumn(new ExtraColumn(table, troidName,
                                       TroidPoemType.it,
                                       DefinitionSource.infoTables,
                                       table.getNextExtrasIndex()));
    table.setTableInfo(info);
    table.unifyWithColumnInfo();
    table.unifyWithDB(null,troidName);

    PoemThread.commit();
    defineTable(table);

    return table;
  }

  /**
   * @param info the tableInfo for the table to delete
   */
  public void deleteTableAndCommit(TableInfo info) {
    try {
      Table<?> table = info.actualTable();
      Enumeration<Column<?>> columns = table.columns();
      while (columns.hasMoreElements()){
        Column<?> c = columns.nextElement();
        table.deleteColumnAndCommit(c.getColumnInfo());
      }

      info.delete(); // Ensure we have no references in metadata
      beginStructuralModification();
      table.dbModifyStructure(" DROP TABLE " + table.quotedName());
      synchronized (tables) {
        tables.remove(table);
        tablesByName.remove(table.getName().toLowerCase());
        if (displayTables != null)
          displayTables = (Table[])ArrayUtils.removed(displayTables, table);
        uncache();
        table.invalidateTransactionStuffs();
      }
      PoemThread.commit();
    }
    finally {
      endStructuralModification();
    }
  }

  private String getTroidColumnName(DatabaseMetaData m, String tableName) throws SQLException {
    String troidColumnName = null;
    System.err.println("Looking for " + unreservedName(tableName));
    ResultSet tables = m.getTables(null, getSchema(), unreservedName(tableName), null);
    if (tables.next()) {
      ResultSet r = m.getPrimaryKeys(null, getSchema(), unreservedName(tableName));
      while (r.next())
        troidColumnName = r.getString("COLUMN_NAME");
      r.close();

      if (troidColumnName != null) {
        log(getJdbcMetadataName(unreservedName(troidColumnName)));
        ResultSet idCol = m.getColumns(null, getSchema(), unreservedName(tableName), getJdbcMetadataName(unreservedName(troidColumnName)));
        log("Discovered a primary key troid candidate column for jdbc table :" + tableName + ":" + troidColumnName);
        if (idCol.next()) {
          if (dbms.canRepresent(defaultPoemTypeOfColumnMetaData(idCol), TroidPoemType.it) == null)
            if (troidColumnName.equals("id")) // a non-numeric id column
                                              // deserves an exception
              throw new UnificationPoemException("Primary Key " + troidColumnName + " cannot represent a Troid");
            else {
              log("Column " + troidColumnName + " cannot represent troid as it has type " + defaultPoemTypeOfColumnMetaData(idCol));
              ResultSet u;
              try {
                u = m.getIndexInfo
                    (null, getSchema(), unreservedName(tableName), true, false);
              } catch (SQLException e) {
                throw new RuntimeException("IndexInfo not found for " + unreservedName(tableName), e);
              }
              String unusableKey = troidColumnName;
              troidColumnName = null;
              String uniqueKey = null;
              String foundKey = null;
              while (u.next()) {
                uniqueKey = u.getString("COLUMN_NAME");
                if (!uniqueKey.equals(unusableKey)) {
                  ResultSet idColNotPrimeKey = m.getColumns(null,
                      getSchema(),
                      unreservedName(tableName),
                      getJdbcMetadataName(unreservedName(uniqueKey)));
                  if (idColNotPrimeKey.next()) {
                    if (idColNotPrimeKey.getInt("NULLABLE") != DatabaseMetaData.columnNoNulls) {
                      idColNotPrimeKey.close();
                      break;
                    }
                    SQLPoemType<?> t = defaultPoemTypeOfColumnMetaData(idColNotPrimeKey);
                    if (dbms.canRepresent(t, TroidPoemType.it) == null) {
                      log("Unique Column " + uniqueKey + " cannot represent troid as it has type " + t);
                      uniqueKey = null;
                    }
                    if (uniqueKey != null) {
                      if (foundKey != null) {
                        idColNotPrimeKey.close();
                        throw new UnificationPoemException(
                            "Second unique, non-nullable numeric index found :" + uniqueKey
                                + " already found " + foundKey);
                      }
                      log("Unique Column " + uniqueKey + " can represent troid as it has type " + t);
                      foundKey = uniqueKey;
                    }
                    idColNotPrimeKey.close();
                  } else
                    throw new UnexpectedExceptionPoemException(
                        "Found a unique key but no corresponding column");

                  troidColumnName = uniqueKey;
                }
              }
              u.close();
            }
        } else
          throw new UnexpectedExceptionPoemException(
              "Found a primary key but no corresponding column");
        idCol.close();
      }
      tables.close();
    }
    return troidColumnName;
  }

  //
  // ==============
  //  Transactions
  // ==============
  //

  private synchronized void unifyWithDB() throws PoemException, SQLException {
    boolean debug = true;

    // Check all tables defined in the tableInfo metadata table
    // defining the ones that don't exist

    for (Enumeration<TableInfo> ti = getTableInfoTable().selection();
         ti.hasMoreElements();) {
      TableInfo tableInfo = ti.nextElement();
      Table<?> table = getTableIgnoringCase(tableInfo.getName());
      if (table == null) {
        if (debug) log("Defining table:" + tableInfo.getName());
        table = new JdbcTable<Persistent>(this, tableInfo.getName(),
                          DefinitionSource.infoTables);
        defineTable(table);
      }
      table.setTableInfo(tableInfo);
    }

    // Conversely, add tableInfo for the tables that do not have an entry in tableInfo

    for (Table<?> t : tables)
      t.createTableInfo();

    // Check all tables against columnInfo

    for (Table<?> t : tables)
      t.unifyWithColumnInfo();

    // Finally, check tables against the actual JDBC metadata

    DatabaseMetaData m = committedConnection.getMetaData();
    ResultSet tableTypes = m.getTableTypes();
    while (tableTypes.next()) {
      System.err.println("Type: " + tableTypes.getString(1));
    }
    List<HashMap<String, String>> tableDescs = getRelevantTables(m);
    int tableCount = 0;
    for (HashMap<String, String> tableDesc : tableDescs) {
      tableCount++;
      if (debug) log("Table:" + tableDesc.get("TABLE_NAME") +
          " Type:" + tableDesc.get("TABLE_TYPE"));
      String tableName = melatiName(tableDesc.get("TABLE_NAME"));
      if (debug) log("Melati Table name :" + tableName);
      Table<?> table = null;
      String troidColumnName = null;
      if (tableName != null) {
        table = getTableIgnoringCase(tableName);
        if (table == null) {  // POEM does not know about this table
          if (debug) log("Unknown to POEM, with JDBC name " + tableName);

          // but we only want to include them if they have a plausible troid:
          troidColumnName = getTroidColumnName(m, unreservedName(tableName));
          if(debug) log("Primary key:"+ troidColumnName);
          if (troidColumnName != null) {
            if (debug) log("Got a troid column for discovered jdbc table :" + tableName + ":" + troidColumnName);
              try {
                table = new JdbcTable<Persistent>(this, tableName,
                                  DefinitionSource.sqlMetaData);
                defineTable(table);
              }
              catch (DuplicateTableNamePoemException e) {
                throw new UnexpectedExceptionPoemException(e);
              }
              table.createTableInfo();
          } else log ("Ignoring table " + tableName + " as it has no plausible troid");
        } else if (debug) log("Table not null:" + tableName + " has name " + table.getName());
      }

      if (table != null) {
         if (debug) log("table not null now:" + tableName);
         if (debug) log("columnsMetadata(m, tableName):"
                            + columnsMetadata(m, tableName));
         // Create the table if it has no metadata
         // unify with it either way
        table.unifyWithDB(columnsMetadata(m, tableName), troidColumnName);
      } else if (debug) log("table still null, probably doesn't have a troid:" + tableName);

    }
    System.err.println("Table count:" + tableCount);

    // ... and create any tables that simply don't exist in the db

    for (Table<?> table : tables) {
      if (debug) log("Unifying:" + table.getName() + "(" + unreservedName(table.getName()) + ")");

      // bit yukky using getColumns to determine if this table exists in underlying db
      ResultSet colDescs = columnsMetadata(m,
          unreservedName(table.getName()));
      if (!colDescs.next()) {
        // System.err.println("Table has no columns in dbms:" +
        //                    unreservedName(table.getName()));
        table.unifyWithDB(null, getTroidColumnName(m, unreservedName(table.getName())));
      }
    }

    for (Table<?> table : tables)
      table.postInitialise();

  }

  protected List<HashMap<String, String>> getRelevantTables(DatabaseMetaData m) throws SQLException {
    List<HashMap<String, String>> tableMetaData = new ArrayList<HashMap<String, String>>();
    String[] normalTables = {"TABLE"};
    ResultSet tables = m.getTables(null, null, null, normalTables);
    while (tables.next()) {
      HashMap<String, String> t = new HashMap<String, String>();

      t.put("TABLE_TYPE", tables.getString("TABLE_TYPE"));
      t.put("TABLE_NAME", tables.getString("TABLE_NAME"));

      tableMetaData.add(t);
    }
    return tableMetaData;
  }

  /**
   * Add database constraints.
   * The only constraints POEM expects are uniqueness and nullability.
   * POEM assumes that the db will exploit indexes where present.
   * However if you wish to export the db to a more DB oriented
   * application or wish to use schema interrogation or visualisation tools
   * then constraints can be added.
   * Whether constraints are added is controlled in
   * org.melati.LogicalDatabase.properties.
   */
  public void addConstraints() {
    inSession(AccessToken.root,
        new PoemTask() {
          public void run() throws PoemException {
            PoemThread.commit();
            beginStructuralModification();
            try {
              for (Table<?> table : tables)
                table.dbAddConstraints();
              PoemThread.commit();
            }
            finally {
              endStructuralModification();
            }
          }

          public String toString() {
            return "Adding constraints to DB";
          }
        });
  }

  /**
   * The number of transactions available for concurrent use on the database.
   * This is the number of JDBC <TT>Connection</TT>s opened when the database
   * was <TT>connect</TT>ed, this can be set via LogicalDatabase.properties,
   * but defaults to 8 if not set.
   *
   * {@inheritDoc}
   * @see org.melati.poem.transaction.TransactionPool#transactionsMax()
   */
  public final int transactionsMax() {
    return transactionsMax;
  }

  //
  // ----------------------------------
  //  Keeping track of the Transactions
  // ----------------------------------
  //

  /**
   * Set the maximum number of transactions.
   * Note that this does not resize the transaction pool
   * so should be called before the db is connected to.
   *
   * {@inheritDoc}
   * @see org.melati.poem.transaction.TransactionPool#setTransactionsMax(int)
   */
  public final void setTransactionsMax(int t) {
    transactionsMax = t;
  }

  /**
   * {@inheritDoc}
   * @see org.melati.poem.transaction.TransactionPool#getTransactionsCount()
   */
  public int getTransactionsCount() {
    return transactions.size();
  }

  /**
   * {@inheritDoc}
   * @see org.melati.poem.transaction.TransactionPool#getFreeTransactionsCount()
   */
  public int getFreeTransactionsCount() {
    return freeTransactions.size();
  }

  /**
   * Get a transaction for exclusive use.  It's simply taken off the freelist,
   * to be put back later.
   */
  private PoemTransaction openTransaction() {
    synchronized (freeTransactions) {
      if (freeTransactions.size() == 0)
        throw new NoMoreTransactionsException("Database " + name + " has no free transactions remaining of "
            + transactions.size() + " transactions.");
      PoemTransaction transaction =
          (PoemTransaction)freeTransactions.lastElement();
      freeTransactions.setSize(freeTransactions.size() - 1);
      return transaction; }
  }

  /**
   * Finish using a transaction, put it back on the freelist.
   */
  void notifyClosed(PoemTransaction transaction) {
    freeTransactions.addElement(transaction);
  }

  /**
   * Find a transaction by its index.
   * <p>
   * transaction(i).index() == i
   *
   * @param index the index of the Transaction to return
   * @return the Transaction with that index
   */
  public PoemTransaction poemTransaction(int index) {
    return (PoemTransaction)transactions.elementAt(index);
  }

  /**
   * {@inheritDoc}
   * @see org.melati.poem.transaction.TransactionPool#transaction(int)
   */
  public final Transaction transaction(int index) {
    return poemTransaction(index);
  }

  //
  // ---------------
  //  Starting them
  // ---------------
  //

  /**
   * @param trans a PoemTransaction
   * @return whether the Transaction is free
   */
  public boolean isFree(PoemTransaction trans) {
    return freeTransactions.contains(trans);
  }

  /**
   * Acquire a lock on the database.
   */
  public void beginExclusiveLock() {
    // FIXME Yuk
    if (PoemThread.inSession()) {
      lock.readLock().unlock();

    }
    lock.writeLock().lock();
  }
  
  /**
   * Release lock.
   */
  public void endExclusiveLock() {
    lock.writeLock().unlock();

    // FIXME Yuk, see above

    if (PoemThread.inSession())
      lock.readLock().lock();
  }

  /**
   * Perform a PoemTask.
   * @param accessToken the AccessToken to run the task under
   * @param task the PoemTask to perform
   * @param useCommittedTransaction whether to use an insulated Transaction or the Committed one
   */
  private void perform(AccessToken accessToken, final PoemTask task,
                       boolean useCommittedTransaction) throws PoemException {

    lock.readLock().lock();

    final PoemTransaction transaction =
        useCommittedTransaction ? null : openTransaction();
    try {
      PoemThread.inSession(new PoemTask() {
                             public void run() throws PoemException {
                                 task.run();
                               if (transaction != null)
                                   transaction.close(true);
                             }

                             public String toString() {
                               return task.toString();
                             }
                           },
                           accessToken,
                           transaction);
    }
    finally {
      try {
        if (transaction != null && !isFree(transaction)) {
          transaction.close(false);
        }
      } finally {

        lock.readLock().unlock();
      }
    }
  }

  /**
   * Perform a task with the database.  Every access to a POEM database must be
   * made in the context of a `transaction' established using this method (note
   * that Melati programmers don't have to worry about this, because the
   * <TT>PoemServlet</TT> will have done this by the time they get control).
   *
   * @param accessToken    A token determining the <TT>Capability</TT>s
   *                       available to the task, which in turn determine
   *                       what data it can attempt to read and write
   *                       without triggering an
   *                       <TT>AccessPoemException</TT>.  Note that a
   *                       <TT>User</TT> can be an <TT>AccessToken</TT>.
   *
   * @param task           What to do: its <TT>run()</TT> is invoked, in
   *                       the current Java thread; until <TT>run()</TT>
   *                       returns, all POEM accesses made by the thread
   *                       are taken to be performed with the capabilities
   *                       given by <TT>accessToken</TT>, and in a private
   *                       transaction.  No changes made to the database
   *                       by other transactions will be visible to it (in the
   *                       sense that once it has seen a particular
   *                       version of a record, it will always
   *                       subsequently see the same one), and its own
   *                       changes will not be made permanent until it
   *                       completes successfully or performs an explicit
   *                       <TT>PoemThread.commit()</TT>.  If it terminates
   *                       with an exception or issues a
   *                       <TT>PoemThread.rollback()</TT> its changes will
   *                       be lost.  (The task is allowed to continue
   *                       after either a <TT>commit()</TT> or a
   *                       <TT>rollback()</TT>.)
   *
   * @see PoemThread
   * @see PoemThread#commit
   * @see PoemThread#rollback
   * @see User
   */
  public void inSession(AccessToken accessToken, PoemTask task) {
    perform(accessToken, task, false);
  }

  /**
   * @param task the task to run
   */
  public void inSessionAsRoot(PoemTask task) {
    perform(AccessToken.root, task, false);
  }

  //
  // ==================
  //  Accessing tables
  // ==================
  //

  /**
   * Start a db session.
   * This is the very manual way of doing db work - not reccomended -
   * use inSession.
   */
  public void beginSession(AccessToken accessToken) {
    lock.readLock().lock();
    PoemTransaction transaction = openTransaction();
    try {
      PoemThread.beginSession(accessToken,transaction);
    } catch (AlreadyInSessionPoemException e) {
      notifyClosed(transaction);
      lock.readLock().unlock();
      throw e;
    }
  }

  /**
   * End a db session.
   * <p>
   * This is the very manual way of doing db work - not recommended -
   * use inSession.
   */
  public void endSession() {
    PoemTransaction tx = PoemThread.sessionToken().transaction;
    PoemThread.endSession();
    tx.close(true);
    lock.readLock().unlock();
  }
  
  /**
   * Perform a task with the database, but not in an insulated transaction.
   * The effect is the same as <TT>inSession</TT>, except that the task will
   * see changes to the database made by other transactions as they are
   * committed, and it is not allowed to make any changes of its own.
   * <p>
   * A modification will trigger a <code>WriteCommittedException</code>,
   * however a create operation will trigger a NullPointerException,
   * as we have no Transaction.
   * </p>
   * <p>
   * Not recommended; why exactly do you want to sidestep the Transaction handling?
   * </p>
   * @see #inSession
   */
  public void inCommittedTransaction(AccessToken accessToken, PoemTask task) {
    perform(accessToken, task, true);
  }

  /**
   * Retrieve the table with a given name.
   *
   * @param tableName        The name of the table to return, as in the RDBMS
   *                    database.  It's case-sensitive, and some RDBMSs such as
   *                    Postgres 6.4.2 (and perhaps other versions) treat upper
   *                    case letters in identifiers inconsistently, so the
   *                    name is forced to lowercase.
   *
   * @return the Table of that name
   *
   * @exception NoSuchTablePoemException
   *             if no table with the given name exists in the RDBMS
   */
  public final Table<?> getTable(String tableName) throws NoSuchTablePoemException {
    Table<?> table = getTableIgnoringCase(tableName);
    if (table == null) throw new NoSuchTablePoemException(this, tableName);
    return table;
  }

  private Table<?> getTableIgnoringCase(String tableName) {
    return tablesByName.get(tableName.toLowerCase());
  }

  /**
   * All the tables in the database.
   * NOTE This will include any deleted tables
   *
   * @return an <TT>Enumeration</TT> of <TT>Table</TT>s, in no particular
   *         order.
   */
  public final Enumeration<Table<?>> tables() {
    return tables.elements();
  }
  
  /**
    * All the tables in the database.
    * NOTE This will include any deleted tables
   */
  public final List<Table<?>> getTables() {
    return tables;
  }

  /**
   * All the tables in the database in DisplayOrder
   * order, using current transaction if there is one.
   *
   * @return an <TT>Enumeration</TT> of <TT>Table</TT>s
   */
  public Enumeration<Table<?>> displayTables() {
    return displayTables(PoemThread.inSession() ? PoemThread.transaction() : null);
  }

  /** A convenience wrapper around displayTables() */
  public List<Table<?>> getDisplayTables() {
    return EnumUtils.list(displayTables());
  }

  /**
   * Currently all the tables in the database in DisplayOrder
   * order.
   *
   * @return an <TT>Enumeration</TT> of <TT>Table</TT>s
   */
  public Enumeration<Table<?>> displayTables(PoemTransaction transaction) {
    Table<?>[] displayTablesL = this.displayTables;

    if (displayTablesL == null) {
      Enumeration<Integer> tableIDs = getTableInfoTable().troidSelection(
        (String)null /* "displayable" */,
        quotedName("displayorder") + ", " + quotedName("name"),
        false, transaction);

      Vector<Table<?>> them = new Vector<Table<?>>();
      while (tableIDs.hasMoreElements()) {
        Table<?> table =
            tableWithTableInfoID(tableIDs.nextElement().intValue());
        if (table != null)
          them.addElement(table);
      }

      displayTablesL = new Table[them.size()];
      them.copyInto(displayTablesL);
      this.displayTables = displayTablesL;
    }

    return new ArrayEnumeration<Table<?>>(this.displayTables);
  }

  /**
   * The table with a given ID in the <TT>tableinfo</TT> table, or
   * <TT>null</TT>.
   *
   * @see #getTableInfoTable
   */
  Table<?> tableWithTableInfoID(int tableInfoID) {
    for (Table<?> table : tables) {
      Integer id = table.tableInfoID();
      if (id != null && id.intValue() == tableInfoID)
        return table;
    }
    return null;
  }

 /**
  * @return All the {@link Column}s in the whole {@link Database}
  */
  public Enumeration<Column<?>> columns() {
    return new FlattenedEnumeration<Column<?>>(
        new MappedEnumeration<Enumeration<Column<?>>, Table<?>>(tables()) {
          public Enumeration<Column<?>> mapped(Table<?> table) {
            return table.columns();
          }
        });
  }

  /** Wrapper around columns() */
  public List<Column<?>> getColumns() {
    return EnumUtils.list(columns());
  }

  public int tableCount() {
    return tables.size();
  }

  public int columnCount() {
    return getColumns().size();
  }

  public int recordCount() {
    Enumeration<Integer> counts = new MappedEnumeration<Integer, Table<?>>(tables()) {
      public Integer mapped(Table<?> table) {
        return new Integer(table.count());
      }
    };
    int total = 0;
    while(counts.hasMoreElements())
      total = total + counts.nextElement().intValue();

    return total;
  }

  /**
   * @param columnInfoID
   * @return the Column with the given troid
   */
  Column<?> columnWithColumnInfoID(int columnInfoID) {
    for (Table<?> table : tables) {
      Column<?> column = table.columnWithColumnInfoID(columnInfoID);
      if (column != null)
        return column;
    }
    return null;
  }

  /**
   * @return The metadata table with information about all tables in the database.
   */
  public abstract TableInfoTable<TableInfo> getTableInfoTable();

  /**
   * @return The Table Category Table.
   */
  public abstract TableCategoryTable<TableCategory> getTableCategoryTable();

  /**
   * @return The metadata table with information about all columns in all tables in the
   * database.
   */
  public abstract ColumnInfoTable<ColumnInfo> getColumnInfoTable();

  /**
   * The table of capabilities (required for reading and/or writing records)
   * defined for the database.  Users acquire capabilities in virtue of being
   * members of groups.
   *
   * @return the CapabilityTable
   * @see Persistent#assertCanRead()
   * @see Persistent#assertCanWrite()
   * @see Persistent#assertCanDelete()
   * @see JdbcTable#getDefaultCanRead
   * @see JdbcTable#getDefaultCanWrite
   * @see User
   * @see #getUserTable
   * @see Group
   * @see #getGroupTable
   */
  public abstract CapabilityTable<Capability> getCapabilityTable();

  /**
   * @return the table of known users of the database
   */
  public abstract UserTable<User> getUserTable();

  /**
   * @return the table of defined user groups for the database
   */
  public abstract GroupTable<Group> getGroupTable();

  //
  // ========================
  //  Running arbitrary SQL
  // ========================
  //

  /**
   * A user is a member of a group iff there is a record in this table to say so.
   * @return the table containing group-membership records
   */
  public abstract GroupMembershipTable<GroupMembership> getGroupMembershipTable();

  /**
   * The table containing group-capability records.  A group has a certain
   * capability iff there is a record in this table to say so.
   * @return the GroupCapability table
   */
  public abstract GroupCapabilityTable<GroupCapability> getGroupCapabilityTable();

  //
  // =======
  //  Users
  // =======
  //

  /**
   * @return the Setting Table.
   */
  public abstract SettingTable<Setting> getSettingTable();

  /**
   * Run an arbitrary SQL query against the database.  This is a low-level
   * <TT>java.sql.Statement.executeQuery</TT>, intended for fiddly queries for
   * which the higher-level methods are too clunky or inflexible.  <B>Note</B>
   * that it bypasses the access control mechanism!
   *
   * @return the ResultSet resulting from running the query
   * @see Table#selection()
   * @see Table#selection(java.lang.String)
   * @see Column#selectionWhereEq(java.lang.Object)
   */
  public ResultSet sqlQuery(String sql) throws SQLPoemException {
    SessionToken token = PoemThread.sessionToken();
    token.transaction.writeDown();
    try {
      Statement s = token.transaction.getConnection().createStatement();
      token.toTidy().add(s);
      ResultSet rs = s.executeQuery(sql);
      token.toTidy().add(rs);
      if (logSQL())
        log(new SQLLogEvent(sql));
      incrementQueryCount(sql);
      return rs;
    }
    catch (SQLException e) {
      throw new ExecutingSQLPoemException(sql, e);
    }
  }

  /**
   * Run an arbitrary SQL update against the database.  This is a low-level
   * <TT>java.sql.Statement.executeUpdate</TT>, intended for fiddly updates for
   * which the higher-level methods are too clunky or inflexible.
   * <p>
   * NOTE This bypasses the access control mechanism.  Furthermore, the cache
   * will be left out of sync with the database and must be cleared out
   * (explicitly, manually) after the current transaction has been committed
   * or completed.
   *
   * @return either the row count for <code>INSERT</code>, <code>UPDATE</code>
   * or <code>DELETE</code> statements, or <code>0</code> for SQL statements
   * that return nothing
   *
   * @see Table#selection()
   * @see Table#selection(java.lang.String)
   * @see Column#selectionWhereEq(java.lang.Object)
   * @see #uncache
   */
  public int sqlUpdate(String sql) throws SQLPoemException {
    SessionToken token = PoemThread.sessionToken();
    token.transaction.writeDown();

    try {
      Statement s = token.transaction.getConnection().createStatement();
      token.toTidy().add(s);
      int n = s.executeUpdate(sql);
      if (logSQL())
        log(new SQLLogEvent(sql));
      incrementQueryCount(sql);
      return n;
    }
    catch (SQLException e) {
      throw dbms.exceptionForUpdate(null, sql,
                                    sql.indexOf("INSERT") >= 0 ||
                                      sql.indexOf("insert") >= 0,
                                    e);
    }
  }

  /**
   * @return the guest
   */
  public User guestUser() {
    if (guest == null)
      guest = getUserTable().guestUser();
    return guest;
  }

  /**
   * @return the administrator
   */
  public User administratorUser() {
    if (administrator == null)
      administrator = getUserTable().administratorUser();
    return administrator;
  }

  /**
   * Get the raw SQL statement for this database's DBMS for Capability
   * check for a User.
   * @param user
   * @param capability
   * @return the raw SQL appropriate for this db
   */
  public String givesCapabilitySQL(User user, Capability capability) {
    // NOTE Bootstrapping to troid or we get a stack overflow
    return dbms.givesCapabilitySQL(user.troid(), capability.troid().toString());
  }

 /**
  * TODO Use a prepared statement to get Capabilities
  */
  private boolean dbGivesCapability(User user, Capability capability) {

    String sql = givesCapabilitySQL(user, capability);
    ResultSet rs = null;
    try {
      rs = sqlQuery(sql);
      return rs.next();
    }
    catch (SQLPoemException e) {
      throw new UnexpectedExceptionPoemException(e);
    }
    catch (SQLException e) {
      throw new SQLSeriousPoemException(e, sql);
    }
    finally {
      try { if (rs != null) rs.close(); } catch (Exception e) {
        System.err.println("Cannot close resultset after exception.");
      }
    }
  }

  /**
   * Check if a user has the specified Capability.
   * @param user the User to check
   * @param capability the Capability required
   * @return whether User has Capability
   */
  public boolean hasCapability(User user, Capability capability) {
    // no capability means that we always have access
    if (capability == null) return true;
    // otherwise, go to the cache
    return capabilityCache.hasCapability(user, capability);
  }

  /**
   * @return the guest token.
   */
  public AccessToken guestAccessToken() {
    return getUserTable().guestUser();
  }

  /**
   * @return the Capability required to administer this db.
   */
  public Capability administerCapability() {
    return getCapabilityTable().administer();
  }

  /**
   * By default, anyone can administer a database.
   *
   * @return the required {@link Capability} to administer the db
   * (<tt>null</tt> unless overridden)
   */
  public Capability getCanAdminister() {
    return canAdminister;
  }

  /**
   * Set administrator capability to named Capability.
   *
   * @param capabilityName name of Capability
   */
  public void setCanAdminister(String capabilityName) {
    canAdminister = getCapabilityTable().ensure(capabilityName);
  }

  /**
   * Set administrator capability to default.
   * <p>
   * NOTE Once a database has had its <tt>canAdminister</tt> capability set
   * there is no mechanism to set it back to null.
   */
  public void setCanAdminister() {
    canAdminister = administerCapability();
  }
  
  /**
   * Trim POEM's cache to a given size.
   *
   * @param maxSize     The data for all but this many records per table will
   *                    be dropped from POEM's cache, on a least-recently-used
   *                    basis.
   */
  public void trimCache(int maxSize) {
    for (Table<?> table : tables)
      table.trimCache(maxSize);
  }

  /**
   * Set the contents of the cache to empty.
   */
  public void uncache() {
    for (int t = 0; t < tables.size(); ++t)
      tables.elementAt(t).uncache();
  }

  //
  // ==========
  //  Cacheing
  // ==========
  //

  /**
   * Find all references to specified object in all tables.
   *
   * @param persistent the object being referred to
   * @return an Enumeration of {@link Persistent}s
   */
  @SuppressWarnings({ "rawtypes", "unchecked" })
  public <P extends Persistent> Enumeration<P> referencesTo(final Persistent persistent) {
    return new FlattenedEnumeration<P>(
        new MappedEnumeration(tables()) {
          @Override
          public Object mapped(Object table) {
            return ((Table<P>)table).referencesTo(persistent);
          }
        });
  }

  /**
   * Wrapper around referencesTo()
   * @return a List of Columns referring to given Table
   */
  public List<Persistent> getReferencesTo(final Persistent persistent) {
    return EnumUtils.list(referencesTo(persistent));
  }

  //
  // ===========
  //  Utilities
  // ===========
  //

  /**
   * @return An Enumeration of Columns referring to the specified Table.
   */
  public Enumeration<Column<?>> referencesTo(final Table<?> tableIn) {
    return new FlattenedEnumeration<Column<?>>(
        new MappedEnumeration<Enumeration<Column<?>>, Table<?>>(tables()) {
          public Enumeration<Column<?>> mapped(Table<?> table) {
            return table.referencesTo(tableIn);
          }
        });
  }

  /**
   * Wrapper around referencesTo()
   * @return a List of Columns referring to given Table
   */
  public List<Column<?>> getReferencesTo(final Table<?> table) {
    return EnumUtils.list(referencesTo(table));
  }

  /**
   * Print some diagnostic information about the contents and consistency of
   * POEM's cache to stderr.
   */
  public void dumpCacheAnalysis() {
    for (Table<?> table : tables)
      table.dumpCacheAnalysis();
  }
  
  /**
   * Print information about the structure of the database to stdout.
   */
  public void dump() {
    for (int t = 0; t < tables.size(); ++t) {
      System.out.println();
      tables.elementAt(t).dump();
    }

    System.err.println("there are " + getTransactionsCount() + " transactions " +
                       "of which " + getFreeTransactionsCount() + " are free");
  }

  /**
   * @return the Database Management System class of this db
   */
  public Dbms getDbms() {
      return dbms;
  }

  /**
   * Set the DBMS.
   *
   * @param aDbms
   */
  private void setDbms(Dbms aDbms) {
      dbms = aDbms;
  }

  //
  // =========================
  //  Database-specific stuff
  // =========================
  //

  /**
   * Quote a name in the DBMS' specific dialect.
   * @param nameIn
   * @return name quoted.
   */
  public final String quotedName(String nameIn) {
      return getDbms().getQuotedName(nameIn);
  }

  /**
   * The default {@link PoemType} corresponding to a ResultSet of JDBC column metadata.
   *
   * @param md A result set of the JDBC columns metadata
   * @return The appropriatePoemType
   */
  final SQLPoemType<?> defaultPoemTypeOfColumnMetaData(ResultSet md)
      throws SQLException {
    return getDbms().defaultPoemTypeOfColumnMetaData(md);
  }

  /**
   * Returns the connection url.
   * If you want a simple name see LogicalDatabase.
   * {@inheritDoc}
   * @see java.lang.Object#toString()
   */
  public String toString() {
    if (connectionUrl == null)
      return "unconnected database";
    else
      return connectionUrl;
  }

  /**
   * @return the non-transactioned jdbc Connection
   */
  public Connection getCommittedConnection() {
    return committedConnection;
  }

  //
  // =====================
  //  Technical utilities
  // =====================
  //

  /**
   * @return whether logging is switched on
   */
  public boolean logSQL() {
    return logSQL;
  }

  /**
   * Toggle logging.
   */
  public void setLogSQL(boolean value) {
    logSQL = value;
  }

  /**
   * @return whether database commits should be logged
   */
  public boolean logCommits() {
    return logCommits;
  }

  /**
   * Toggle commit logging.
   */
  public void setLogCommits(boolean value) {
    logCommits = value;
  }

  void log(PoemLogEvent e) {
    System.err.println("---\n" + e.toString());
  }

  void log(String s) {
    System.err.println(s);
  }

  protected void beginStructuralModification() {
    beginExclusiveLock();
  }

  /**
   * Uncache, increment serial and release exclusive lock.
   */
  protected void endStructuralModification() {
    for (int t = 0; t < tables.size(); ++t)
      tables.elementAt(t).uncache();
    ++structureSerial;
    endExclusiveLock();
  }

  /**
   * @return an id incremented for each change
   */
  long structureSerial() {
    return structureSerial;
  }

  /**
   * Used in testing to check if the cache is being used
   * or a new query is being issued.
   * @return Returns the queryCount.
   */
  public int getQueryCount() {
    return queryCount;
  }

  /**
   * Increment query count.
   */
  public void incrementQueryCount(String sql) {
    lastQuery = sql;
    queryCount++;
  }

  /**
   * @return the most recent query
   */
  public String getLastQuery() {
    return lastQuery;
  }

  /**
   * @return the name
   */
  public String getName() {
    return name;
  }

  /**
   * @return the displayName
   */
  public String getDisplayName() {
    if (displayName == null)
      return StringUtils.capitalised(getName());
    return displayName;
  }

  /**
   * @param displayName the displayName to set
   */
  public void setDisplayName(String displayName) {
    this.displayName = displayName;
  }

  /**
   * Use this for DDL statements, ie those which alter the structure of the db.
   * Postgresql in particular does not like DDL statements being executed within a transaction.
   *
   * @param sql the SQL DDL statement to execute
   */
  public void modifyStructure(String sql)
      throws StructuralModificationFailedPoemException {

    // We have to do this to avoid blocking
    if (PoemThread.inSession())
      PoemThread.commit();

    try {
      Statement updateStatement = getCommittedConnection().createStatement();
      updateStatement.executeUpdate(sql);
      updateStatement.close();
      getCommittedConnection().commit();
      if (logCommits()) log(new CommitLogEvent(null));
      if (logSQL()) log(new StructuralModificationLogEvent(sql));
      incrementQueryCount(sql);
    }
    catch (SQLException e) {
      throw new StructuralModificationFailedPoemException(sql, e);
    }
  }

  /**
   * Thrown when a request is made whilst the connection to
   * the underlying database is still in progress.
   */
  public class ConnectingException extends PoemException {
    private static final long serialVersionUID = 1L;

    /**
     * {@inheritDoc}
     */
    public String getMessage() {
      return "Connection to the database is currently in progress; " +
          "please try again in a moment";
    }
  }

  private class UserCapabilityCache {
    private Hashtable<Long, Boolean> userCapabilities = null;
    private long groupMembershipSerial;
    private long groupCapabilitySerial;

    boolean hasCapability(User user, Capability capability) {
      PoemTransaction transaction = PoemThread.transaction();
      long currentGroupMembershipSerial =
          getGroupMembershipTable().serial(transaction);
      long currentGroupCapabilitySerial =
          getGroupCapabilityTable().serial(transaction);

      if (userCapabilities == null ||
          groupMembershipSerial != currentGroupMembershipSerial ||
          groupCapabilitySerial != currentGroupCapabilitySerial) {
        userCapabilities = new Hashtable<Long, Boolean>();
        groupMembershipSerial = currentGroupMembershipSerial;
        groupCapabilitySerial = currentGroupCapabilitySerial;
      }

      Long pair = new Long(
          (user.troid().longValue() << 32) | (capability.troid().longValue()));
      Boolean known = userCapabilities.get(pair);

      if (known != null)
        return known.booleanValue();
      else {
        boolean does = dbGivesCapability(user, capability);
        userCapabilities.put(pair, does ? Boolean.TRUE : Boolean.FALSE);
        return does;
      }
    }
  }

}