Coverage Report - org.melati.poem.Database
 
Classes in this File Line Coverage Branch Coverage Complexity
Database
83%
385/461
69%
117/168
2.236
Database$1
57%
4/7
N/A
2.236
Database$2
90%
9/10
100%
2/2
2.236
Database$3
100%
6/6
100%
2/2
2.236
Database$4
100%
2/2
N/A
2.236
Database$5
0%
0/2
N/A
2.236
Database$6
100%
2/2
N/A
2.236
Database$7
100%
2/2
N/A
2.236
Database$UserCapabilityCache
100%
19/19
90%
9/10
2.236
 
 1  
 /*
 2  
  * $Source$
 3  
  * $Revision$
 4  
  *
 5  
  * Copyright (C) 2000 William Chesters
 6  
  *
 7  
  * Part of Melati (http://melati.org), a framework for the rapid
 8  
  * development of clean, maintainable web applications.
 9  
  *
 10  
  * Melati is free software; Permission is granted to copy, distribute
 11  
  * and/or modify this software under the terms either:
 12  
  *
 13  
  * a) the GNU General Public License as published by the Free Software
 14  
  *    Foundation; either version 2 of the License, or (at your option)
 15  
  *    any later version,
 16  
  *
 17  
  *    or
 18  
  *
 19  
  * b) any version of the Melati Software License, as published
 20  
  *    at http://melati.org
 21  
  *
 22  
  * You should have received a copy of the GNU General Public License and
 23  
  * the Melati Software License along with this program;
 24  
  * if not, write to the Free Software Foundation, Inc.,
 25  
  * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA to obtain the
 26  
  * GNU General Public License and visit http://melati.org to obtain the
 27  
  * Melati Software License.
 28  
  *
 29  
  * Feel free to contact the Developers of Melati (http://melati.org),
 30  
  * if you would like to work out a different arrangement than the options
 31  
  * outlined here.  It is our intention to allow Melati to be used by as
 32  
  * wide an audience as possible.
 33  
  *
 34  
  * This program is distributed in the hope that it will be useful,
 35  
  * but WITHOUT ANY WARRANTY; without even the implied warranty of
 36  
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 37  
  * GNU General Public License for more details.
 38  
  *
 39  
  * Contact details for copyright holder:
 40  
  *
 41  
  *     William Chesters <williamc At paneris.org>
 42  
  *     http://paneris.org/~williamc
 43  
  *     Obrechtstraat 114, 2517VX Den Haag, The Netherlands
 44  
  */
 45  
 
 46  
 package org.melati.poem;
 47  
 
 48  
 import java.sql.Connection;
 49  
 import java.sql.DatabaseMetaData;
 50  
 import java.sql.ResultSet;
 51  
 import java.sql.SQLException;
 52  
 import java.sql.Statement;
 53  
 import java.util.Enumeration;
 54  
 import java.util.Hashtable;
 55  
 import java.util.List;
 56  
 import java.util.Vector;
 57  
 
 58  
 import org.melati.poem.dbms.Dbms;
 59  
 import org.melati.poem.dbms.DbmsFactory;
 60  
 import org.melati.poem.transaction.Transaction;
 61  
 import org.melati.poem.transaction.TransactionPool;
 62  
 import org.melati.poem.util.ArrayEnumeration;
 63  
 import org.melati.poem.util.ArrayUtils;
 64  
 import org.melati.poem.util.EnumUtils;
 65  
 import org.melati.poem.util.FlattenedEnumeration;
 66  
 import org.melati.poem.util.MappedEnumeration;
 67  
 import org.melati.poem.util.StringUtils;
 68  
 
 69  
 import java.util.concurrent.locks.ReadWriteLock;
 70  
 import java.util.concurrent.locks.ReentrantReadWriteLock;
 71  
 
 72  
 /**
 73  
  * An RDBMS database.  Don't instantiate (or subclass) this class, but rather
 74  
  * {@link PoemDatabase}, which includes the boilerplate code for the standard
 75  
  * tables such as <TT>user</TT> and <TT>columninfo</TT> which all POEM
 76  
  * databases must contain.  If the database is predefined by a Data Structure
 77  
  * Definition <TT><I>Bar</I>.dsd</TT>, there will be an application-specialised
 78  
  * subclass of <TT>PoemDatabase</TT> called <TT><I>Bar</I>Database</TT> which
 79  
  * provides named methods for accessing application-specialised objects
 80  
  * representing the predefined tables.
 81  
  *
 82  
  * @see PoemDatabase
 83  
  */
 84  
 
 85  70
 public abstract class Database implements TransactionPool {
 86  
 
 87  43
   final Database _this = this;
 88  
 
 89  43
   private Vector<Transaction> transactions = null; 
 90  43
   private Vector<Transaction> freeTransactions = null;
 91  
 
 92  
   private Connection committedConnection;
 93  43
   private final ReadWriteLock lock = new ReentrantReadWriteLock();
 94  43
   private long structureSerial = 0L;
 95  
 
 96  43
   private Vector<Table<?>> tables = new Vector<Table<?>>();
 97  43
   private Hashtable<String, Table<?>> tablesByName = new Hashtable<String, Table<?>>();
 98  43
   private Table<?>[] displayTables = null;
 99  
 
 100  
   private String name;
 101  
   private String displayName;
 102  
   private Dbms dbms;
 103  43
   private boolean logSQL = false;
 104  43
   private boolean logCommits = false;
 105  
   private int transactionsMax;
 106  
 
 107  
   private String connectionUrl;
 108  
   
 109  
   /**
 110  
    * Used in testing to check caching.
 111  
    */
 112  43
   private int queryCount = 0;
 113  
   /** Used in tests to check caching etc. */
 114  43
   private String lastQuery = null;
 115  
   
 116  
 
 117  
   //
 118  
   // ================
 119  
   //  Initialisation
 120  
   // ================
 121  
   //
 122  
 
 123  
   /**
 124  
    * Don't subclass this, subclass <TT>PoemDatabase</TT>.
 125  
    * @see PoemDatabase
 126  
    */
 127  
 
 128  43
   public Database() {
 129  43
   }
 130  
 
 131  43
   private boolean initialised = false;
 132  
 
 133  
   /**
 134  
    * Initialise each table.
 135  
    */
 136  
   private synchronized void init() {
 137  41
     if (!initialised) {
 138  41
       for (Table<?> t : this.tables)
 139  705
         t.init();
 140  41
       initialised = true;
 141  
     }
 142  41
   }
 143  
 
 144  43
   private final boolean[] connecting = new boolean[1];
 145  
 
 146  
  /**
 147  
   * Thrown when a request is made whilst the connection to
 148  
   * the underlying database is still in progress.
 149  
   */
 150  
   public class ConnectingException extends PoemException {
 151  
     private static final long serialVersionUID = 1L;
 152  
     /**
 153  
      * {@inheritDoc}
 154  
      */
 155  
     public String getMessage() {
 156  
       return "Connection to the database is currently in progress; " +
 157  
              "please try again in a moment";
 158  
     }
 159  
   }
 160  
 
 161  
   /**
 162  
    * Connect to an RDBMS database.  This should be called once when the
 163  
    * application starts up; it will
 164  
    *
 165  
    * <UL>
 166  
    *   <LI> Open <TT>this.transactionsMax()</TT> JDBC <TT>Connection</TT>s to
 167  
    *        the database for subsequent `pooling'
 168  
    *   </LI>
 169  
    *   <LI> Unify (reconcile) the structural information about the database
 170  
    *        given in
 171  
    *
 172  
    *        <OL>
 173  
    *          <LI> the Database Structure Definition (<I>i.e.</I> embodied in
 174  
    *               the boilerplate code generated from it), including the
 175  
    *               POEM-standard tables defined in <TT>Poem.dsd</TT>;
 176  
    *          <LI> the metadata tables <TT>tableinfo</TT> and
 177  
    *               <TT>columninfo</TT>;
 178  
    *          <LI> the actual JDBC metadata from the RDBMS.
 179  
    *        </OL>
 180  
    *
 181  
    *        Any tables or columns defined in the DSD or the metadata tables,
 182  
    *        but not present in the actual database, will be created.
 183  
    *        <BR>
 184  
    *        Conversely, entries will be created in the metadata tables for
 185  
    *        tables and columns that don't have them.  If an inconsistency is
 186  
    *        detected between any of the three information sources (such as a
 187  
    *        fundamental type incompatibility, or a string field which is
 188  
    *        narrower in the database than it was declared to be), an exception
 189  
    *        will be thrown.  In that case the database will in theory be left
 190  
    *        untouched, except that in Postgres (at least) all structural
 191  
    *        updates happen immediately and irrevocably even if made from a
 192  
    *        transaction subsequently rolled back.
 193  
    *   </LI>
 194  
    * </UL>
 195  
    *
 196  
    * @param dbmsclass   The Melati DBMS class (see org/melati/poem/dbms)
 197  
    *                    to use, usually specified in
 198  
    *                    org.melati.LogicalDatabase.properties.
 199  
    *
 200  
    * @param url         The JDBC URL for the database; for instance
 201  
    *                    <TT>jdbc:postgresql:williamc</TT>.  It is the
 202  
    *                    programmer's responsibility to make sure that an
 203  
    *                    appropriate driver has been loaded.
 204  
    *
 205  
    * @param username    The username under which to establish JDBC connections
 206  
    *                    to the database.  This has nothing to do with the
 207  
    *                    user/group/capability authentication performed by
 208  
    *                    Melati.
 209  
    *
 210  
    * @param password    The password to go with the username.
 211  
    *
 212  
    * @param transactionsMaxP
 213  
    *                    The maximum number of concurrent Transactions allowed,
 214  
    *                    usually specified in
 215  
    *                    org.melati.LogicalDatabase.properties.
 216  
    *
 217  
    * @see #transactionsMax()
 218  
    */
 219  
   @SuppressWarnings("unchecked")
 220  
   public void connect(String nameIn, String dbmsclass, String url,
 221  
                       String username, String password,
 222  
                       int transactionsMaxP) throws PoemException {
 223  
 
 224  42
     this.name = nameIn;
 225  42
     this.connectionUrl = url;
 226  
 
 227  42
     synchronized (connecting) {
 228  42
       if (connecting[0])
 229  0
         throw new ConnectingException();
 230  42
       connecting[0] = true;
 231  42
     }
 232  
 
 233  42
     if (committedConnection != null)
 234  1
       throw new ReconnectionPoemException(this);
 235  
 
 236  41
     setDbms(DbmsFactory.getDbms(dbmsclass));
 237  
 
 238  41
     setTransactionsMax(transactionsMaxP);
 239  41
     committedConnection = getDbms().getConnection(url, username, password);
 240  41
     transactions = new Vector<Transaction>();
 241  397
     for (int s = 0; s < transactionsMax(); ++s)
 242  712
       transactions.add(
 243  
         new PoemTransaction(
 244  
             this,
 245  356
             getDbms().getConnection(url, username, password),
 246  
             s));
 247  
 
 248  41
     freeTransactions = (Vector<Transaction>)transactions.clone();
 249  
 
 250  
     try {
 251  
       // Perform any table specific initialisation, none by default
 252  41
       init();
 253  
 
 254  
       // Bootstrap: set up the tableinfo and columninfo tables
 255  41
       DatabaseMetaData m = committedConnection.getMetaData();
 256  82
       getTableInfoTable().unifyWithDB(
 257  82
           m.getColumns(null, dbms.getSchema(),
 258  82
                        dbms.unreservedName(getTableInfoTable().getName()), null), dbms.unreservedName("id"));
 259  82
       getColumnInfoTable().unifyWithDB(
 260  82
           m.getColumns(null, dbms.getSchema(),
 261  82
                        dbms.unreservedName(getColumnInfoTable().getName()), null), dbms.unreservedName("id"));
 262  82
       getTableCategoryTable().unifyWithDB(
 263  82
           m.getColumns(null, dbms.getSchema(),
 264  82
                        dbms.unreservedName(getTableCategoryTable().getName()), null), dbms.unreservedName("id"));
 265  
 
 266  41
       inSession(AccessToken.root,
 267  41
                 new PoemTask() {
 268  
                   public void run() throws PoemException {
 269  
                     try {
 270  41
                       _this.unifyWithDB();
 271  
                     }
 272  0
                     catch (SQLException e) {
 273  0
                       throw new SQLPoemException(e);
 274  41
                     }
 275  41
                   }
 276  
 
 277  
                   public String toString() {
 278  0
                     return "Unifying with DB";
 279  
                   }
 280  
                 });
 281  0
     } catch (Exception e) {
 282  0
         if (committedConnection != null) disconnect();
 283  0
         throw new UnificationPoemException(e);
 284  
     } finally {
 285  41
       synchronized (connecting) {
 286  41
         connecting[0] = false;
 287  41
       }
 288  41
     }
 289  41
   }
 290  
 
 291  
   /**
 292  
    * Releases database connections.
 293  
    */
 294  
   public void disconnect() throws PoemException {
 295  18
     if (committedConnection == null)
 296  0
       throw new ReconnectionPoemException(this);
 297  
 
 298  
     try {
 299  18
       for (Transaction poemTransaction : freeTransactions){ 
 300  144
         ((PoemTransaction)poemTransaction).getConnection().close();
 301  144
       }
 302  18
       freeTransactions.removeAllElements();
 303  
       
 304  18
       getDbms().shutdown(committedConnection);
 305  18
       committedConnection.close();
 306  0
     } catch (SQLException e) {
 307  0
       throw new SQLPoemException(e);
 308  18
     }
 309  18
     committedConnection = null;
 310  18
   }
 311  
   
 312  
   /**
 313  
    * Don't call this.  Tables should be defined either in the DSD (in which
 314  
    * case the boilerplate code generated by the preprocessor will call this
 315  
    * method), or directly in the RDBMS (in which case the initialisation code
 316  
    * will), or using <TT>addTableAndCommit</TT>.
 317  
    *
 318  
    * @see #addTableAndCommit
 319  
    */
 320  
   protected synchronized void defineTable(Table<?> table)
 321  
       throws DuplicateTableNamePoemException {
 322  20
     if (getTableIgnoringCase(table.getName()) != null)
 323  1
       throw new DuplicateTableNamePoemException(this, table.getName());
 324  19
     redefineTable(table);
 325  19
   }
 326  
 
 327  
   protected synchronized void redefineTable(Table<?> table) {
 328  931
     if (table.getDatabase() != this)
 329  0
       throw new TableInUsePoemException(this, table);
 330  
 
 331  931
     if (getTableIgnoringCase(table.getName()) == null) {
 332  742
       tablesByName.put(table.getName().toLowerCase(), table);
 333  742
       tables.addElement(table);
 334  
     }
 335  
     else
 336  378
       tables.setElementAt(table,
 337  189
                           tables.indexOf(
 338  189
                               tablesByName.put(table.getName().toLowerCase(), table)));
 339  931
     displayTables = null;
 340  931
   }
 341  
 
 342  
   private ResultSet columnsMetadata(DatabaseMetaData m, String tableName)
 343  
       throws SQLException {
 344  1260
     return m.getColumns(null, dbms.getSchema(), dbms.unreservedName(tableName), null);
 345  
   }
 346  
 
 347  
 
 348  
   /**
 349  
    * Add a Table to this Database and commit the Transaction.
 350  
    * @param info Table metadata object
 351  
    * @param troidName name of troidColumn
 352  
    * @return new minted {@link Table} 
 353  
    */
 354  
   @SuppressWarnings({ "unchecked", "rawtypes" })
 355  
   public Table<?> addTableAndCommit(TableInfo info, String troidName)
 356  
       throws PoemException {
 357  
 
 358  
     // For permission control we rely on them having successfully created a
 359  
     // TableInfo
 360  
 
 361  6
     Table<?> table = new JdbcTable<Persistent>(this, info.getName(),
 362  
                             DefinitionSource.infoTables);
 363  10
     table.defineColumn(new ExtraColumn(table, troidName,
 364  
                                        TroidPoemType.it,
 365  
                                        DefinitionSource.infoTables,
 366  5
                                        table.getNextExtrasIndex()));
 367  5
     table.setTableInfo(info);
 368  5
     table.unifyWithColumnInfo();
 369  5
     table.unifyWithDB(null,troidName);
 370  
 
 371  5
     PoemThread.commit();
 372  5
     defineTable(table);
 373  
 
 374  4
     return table;
 375  
   }
 376  
   
 377  
   /**
 378  
    * @param info the tableInfo for the table to delete
 379  
    */
 380  
   public void deleteTableAndCommit(TableInfo info) { 
 381  
     try {
 382  1
       Table<?> table = info.actualTable();
 383  1
       Enumeration<Column<?>> columns = table.columns();
 384  2
       while (columns.hasMoreElements()){ 
 385  1
         Column<?> c = columns.nextElement();
 386  1
         table.deleteColumnAndCommit(c.getColumnInfo());
 387  1
       }
 388  
         
 389  1
       info.delete(); // Ensure we have no references in metadata
 390  1
       beginStructuralModification();
 391  1
       table.dbModifyStructure(" DROP TABLE " + table.quotedName());
 392  1
       synchronized (tables) {
 393  1
         tables.remove(table);
 394  1
         tablesByName.remove(table.getName().toLowerCase());
 395  1
         if (displayTables != null)
 396  0
           displayTables = (Table[])ArrayUtils.removed(displayTables, table);
 397  1
         uncache();
 398  1
         table.invalidateTransactionStuffs();
 399  1
       }
 400  1
       PoemThread.commit();
 401  
     }
 402  
     finally {
 403  1
       endStructuralModification();
 404  1
     }
 405  1
   }
 406  
 
 407  
   private String getTroidColumnName(DatabaseMetaData m, String tableName) throws SQLException {
 408  198
     String troidColumnName = null;
 409  198
     ResultSet tables = m.getTables(null, dbms.getSchema(), dbms.unreservedName(tableName), null);
 410  198
     if (tables.next()) {
 411  42
       ResultSet r = m.getPrimaryKeys(null, dbms.getSchema(), dbms.unreservedName(tableName));
 412  44
       while (r.next())
 413  2
         troidColumnName = r.getString("COLUMN_NAME");
 414  42
       r.close();
 415  
 
 416  42
       if (troidColumnName != null) {
 417  2
         log(dbms.getJdbcMetadataName(dbms.unreservedName(troidColumnName)));
 418  2
         ResultSet idCol = m.getColumns(null, dbms.getSchema(), dbms.unreservedName(tableName), dbms.getJdbcMetadataName(dbms.unreservedName(troidColumnName)));
 419  2
         log("Discovered a primary key troid candidate column for jdbc table :" + tableName + ":" + troidColumnName);
 420  2
         if (idCol.next()) {
 421  2
           if (dbms.canRepresent(defaultPoemTypeOfColumnMetaData(idCol), TroidPoemType.it) == null)
 422  0
             if (troidColumnName.equals("id")) // a non-numeric id column
 423  
                                               // deserves an exception
 424  0
               throw new UnificationPoemException("Primary Key " + troidColumnName + " cannot represent a Troid");
 425  
             else {
 426  0
               log("Column " + troidColumnName + " cannot represent troid as it has type " + defaultPoemTypeOfColumnMetaData(idCol));
 427  0
               ResultSet u = m.getIndexInfo
 428  0
                   (null, dbms.getSchema(), dbms.unreservedName(tableName), true, false);
 429  0
               String unusableKey = troidColumnName;
 430  0
               troidColumnName = null;
 431  0
               String uniqueKey = null;
 432  0
               String foundKey = null;
 433  0
               while (u.next()) {
 434  0
                 uniqueKey = u.getString("COLUMN_NAME");
 435  0
                 if (!uniqueKey.equals(unusableKey)) {
 436  0
                   ResultSet idColNotPrimeKey = m.getColumns(null,
 437  0
                       dbms.getSchema(),
 438  0
                       dbms.unreservedName(tableName),
 439  0
                       dbms.getJdbcMetadataName(dbms.unreservedName(uniqueKey)));
 440  0
                   if (idColNotPrimeKey.next()) {
 441  0
                     if (idColNotPrimeKey.getInt("NULLABLE") != DatabaseMetaData.columnNoNulls) {
 442  0
                       idColNotPrimeKey.close();
 443  0
                       break;
 444  
                     }
 445  0
                     SQLPoemType<?> t = defaultPoemTypeOfColumnMetaData(idColNotPrimeKey);
 446  0
                     if (dbms.canRepresent(t, TroidPoemType.it) == null) {
 447  0
                       log("Unique Column " + uniqueKey + " cannot represent troid as it has type " + t);
 448  0
                       uniqueKey = null;
 449  
                     }
 450  0
                     if (uniqueKey != null) {
 451  0
                       if (foundKey != null) {
 452  0
                         idColNotPrimeKey.close();
 453  0
                         throw new UnificationPoemException(
 454  
                             "Second unique, non-nullable numeric index found :" + uniqueKey
 455  
                                 + " already found " + foundKey);
 456  
                       }
 457  0
                       log("Unique Column " + uniqueKey + " can represent troid as it has type " + t);
 458  0
                       foundKey = uniqueKey;
 459  
                     }
 460  0
                     idColNotPrimeKey.close();
 461  0
                   } else
 462  0
                     throw new UnexpectedExceptionPoemException(
 463  
                         "Found a unique key but no corresponding column");
 464  
 
 465  0
                   troidColumnName = uniqueKey;
 466  0
                 }
 467  
               }
 468  0
               u.close();
 469  0
             }
 470  
         } else
 471  0
           throw new UnexpectedExceptionPoemException(
 472  
               "Found a primary key but no corresponding column");
 473  2
         idCol.close();
 474  
       }
 475  42
       tables.close();
 476  
     }
 477  198
     return troidColumnName;
 478  
   }
 479  
 
 480  
   private synchronized void unifyWithDB() throws PoemException, SQLException {
 481  41
     boolean debug = false;
 482  
     
 483  
     // Check all tables defined in the tableInfo metadata table
 484  
     // defining the ones that don't exist
 485  
 
 486  41
     for (Enumeration<TableInfo> ti = getTableInfoTable().selection();
 487  51
          ti.hasMoreElements();) {
 488  10
       TableInfo tableInfo = ti.nextElement();
 489  10
       Table<?> table = getTableIgnoringCase(tableInfo.getName());
 490  10
       if (table == null) {
 491  1
         if (debug) log("Defining table:" + tableInfo.getName());
 492  1
         table = new JdbcTable<Persistent>(this, tableInfo.getName(),
 493  
                           DefinitionSource.infoTables);
 494  1
         defineTable(table);
 495  
       }
 496  10
       table.setTableInfo(tableInfo);
 497  10
     }
 498  
 
 499  
     // Conversely, add tableInfo for the tables that do not have an entry in tableInfo
 500  
 
 501  41
     for (Table<?> t : tables)
 502  706
       t.createTableInfo();
 503  
 
 504  
     // Check all tables against columnInfo
 505  
 
 506  41
     for (Table<?> t : tables)
 507  706
       t.unifyWithColumnInfo();
 508  
 
 509  
     // Finally, check tables against the actual JDBC metadata
 510  
 
 511  41
     String[] normalTables = { "TABLE" };
 512  
 
 513  41
     DatabaseMetaData m = committedConnection.getMetaData();
 514  41
     ResultSet tableDescs = m.getTables(null, dbms.getSchema(), null,
 515  
                                        normalTables);
 516  633
     while (tableDescs.next()) {
 517  592
       if (debug) log("Table:" + tableDescs.getString("TABLE_NAME") +
 518  0
           " Type:" + tableDescs.getString("TABLE_TYPE"));
 519  592
       String tableName = dbms.melatiName(tableDescs.getString("TABLE_NAME"));
 520  592
       if (debug) log("Melati Table name :" + tableName);
 521  592
       Table<?> table = null;
 522  592
       String troidColumnName = null;
 523  592
       if (tableName != null) { 
 524  592
         table = getTableIgnoringCase(tableName);
 525  592
         if (table == null) {  // POEM does not know about this table
 526  42
           if (debug) log("Unknown to POEM, with JDBC name " + tableName);
 527  
 
 528  
           // but we only want to include them if they have a plausible troid:
 529  42
           troidColumnName = getTroidColumnName(m,dbms.unreservedName(tableName));
 530  42
           if(debug) log("Primary key:"+ troidColumnName);
 531  42
           if (troidColumnName != null) { 
 532  2
             if (debug) log("Got a troid column for discovered jdbc table :" + tableName + ":" + troidColumnName);
 533  
               try {
 534  2
                 table = new JdbcTable<Persistent>(this, tableName,
 535  
                                   DefinitionSource.sqlMetaData);
 536  2
                 defineTable(table);
 537  
               }
 538  0
               catch (DuplicateTableNamePoemException e) {
 539  0
                 throw new UnexpectedExceptionPoemException(e);
 540  2
               }
 541  2
               table.createTableInfo();
 542  40
           } else log ("Ignoring table " + tableName + " as it has no plausible troid");
 543  550
         } else if (debug) log("Table not null:" + tableName + " has name " + table.getName());
 544  
       }
 545  
 
 546  592
       if (table != null) {
 547  552
          if (debug) log("table not null now:" + tableName);
 548  552
          if (debug) log("columnsMetadata(m, tableName):"
 549  0
                             + columnsMetadata(m, tableName));
 550  
          // Create the table if it has no metadata
 551  
          // unify with it either way
 552  552
         table.unifyWithDB(columnsMetadata(m, tableName), troidColumnName);
 553  40
       } else if (debug) log("table still null, probably doesn't have a troid:" + tableName);
 554  
 
 555  592
     }
 556  
 
 557  
     // ... and create any tables that simply don't exist in the db
 558  
 
 559  41
     for (Table<?> table : tables) {
 560  708
       if (debug) log("Unifying:" + table.getName() + "(" + dbms.unreservedName(table.getName()) + ")");
 561  
 
 562  
       // bit yukky using getColumns to determine if this table exists in underlying db
 563  1416
       ResultSet colDescs = columnsMetadata(m,
 564  708
                                dbms.unreservedName(table.getName()));
 565  708
       if (!colDescs.next()) {
 566  
         // System.err.println("Table has no columns in dbms:" +
 567  
         //                    dbms.unreservedName(table.getName()));
 568  156
         table.unifyWithDB(null, getTroidColumnName(m,dbms.unreservedName(table.getName())));
 569  
       }
 570  708
     }
 571  
 
 572  41
     for (Table<?> table : tables)
 573  708
       table.postInitialise();
 574  
     
 575  41
   }
 576  
 
 577  
   /**
 578  
    * Add database constraints.
 579  
    * The only constraints POEM expects are uniqueness and nullability.
 580  
    * POEM assumes that the db will exploit indexes where present.
 581  
    * However if you wish to export the db to a more DB oriented 
 582  
    * application or wish to use schema interrogation or visualisation tools 
 583  
    * then constraints can be added.
 584  
    * Whether constraints are added is controlled in 
 585  
    * org.melati.LogicalDatabase.properties. 
 586  
    */
 587  
   public void addConstraints() {
 588  17
     inSession(AccessToken.root,
 589  17
         new PoemTask() {
 590  
           public void run() throws PoemException {
 591  17
             PoemThread.commit();
 592  17
             beginStructuralModification();
 593  
             try {
 594  17
               for (Table<?> table : tables)
 595  153
                 table.dbAddConstraints();
 596  17
               PoemThread.commit();
 597  
             }
 598  
             finally {
 599  17
               endStructuralModification();
 600  17
             }
 601  17
           }
 602  
 
 603  
           public String toString() {
 604  0
             return "Adding constraints to DB";
 605  
           }
 606  
         });
 607  17
   }
 608  
 
 609  
   //
 610  
   // ==============
 611  
   //  Transactions
 612  
   // ==============
 613  
   //
 614  
 
 615  
   /**
 616  
    * The number of transactions available for concurrent use on the database.
 617  
    * This is the number of JDBC <TT>Connection</TT>s opened when the database
 618  
    * was <TT>connect</TT>ed, this can be set via LogicalDatabase.properties,
 619  
    * but defaults to 8 if not set.
 620  
    * 
 621  
    * {@inheritDoc}
 622  
    * @see org.melati.poem.transaction.TransactionPool#transactionsMax()
 623  
    */
 624  
   public final int transactionsMax() {
 625  759
     return transactionsMax;
 626  
   }
 627  
 
 628  
   /**
 629  
    * Set the maximum number of transactions.
 630  
    * Note that this does not resize the transaction pool 
 631  
    * so should be called before the db is connected to.
 632  
    * 
 633  
    * {@inheritDoc}
 634  
    * @see org.melati.poem.transaction.TransactionPool#setTransactionsMax(int)
 635  
    */
 636  
   public final void setTransactionsMax(int t) {
 637  43
     transactionsMax = t;
 638  43
   }
 639  
 
 640  
   /**
 641  
    * {@inheritDoc}
 642  
    * @see org.melati.poem.transaction.TransactionPool#getTransactionsCount()
 643  
    */
 644  
   public int getTransactionsCount() {
 645  3
     return transactions.size();
 646  
   }
 647  
 
 648  
   /**
 649  
    * {@inheritDoc}
 650  
    * @see org.melati.poem.transaction.TransactionPool#getFreeTransactionsCount()
 651  
    */
 652  
   public int getFreeTransactionsCount() {
 653  2936
     return freeTransactions.size();
 654  
   }
 655  
 
 656  
   //
 657  
   // ----------------------------------
 658  
   //  Keeping track of the Transactions
 659  
   // ----------------------------------
 660  
   //
 661  
 
 662  
   /**
 663  
    * Get a transaction for exclusive use.  It's simply taken off the freelist,
 664  
    * to be put back later.
 665  
    */
 666  
   private PoemTransaction openTransaction() {
 667  3437
     synchronized (freeTransactions) {
 668  3437
       if (freeTransactions.size() == 0)
 669  0
         throw new NoMoreTransactionsException("Database " + name + " has no free transactions remaining of " 
 670  0
             + transactions.size() + " transactions.");
 671  3437
       PoemTransaction transaction =
 672  3437
           (PoemTransaction)freeTransactions.lastElement();
 673  3437
       freeTransactions.setSize(freeTransactions.size() - 1);
 674  3437
       return transaction; }
 675  
   }
 676  
 
 677  
   /**
 678  
    * Finish using a transaction, put it back on the freelist.
 679  
    */
 680  
   void notifyClosed(PoemTransaction transaction) {
 681  3437
     freeTransactions.addElement(transaction);
 682  3437
   }
 683  
 
 684  
   /**
 685  
    * Find a transaction by its index.
 686  
    * <p>
 687  
    * transaction(i).index() == i
 688  
    * 
 689  
    * @param index the index of the Transaction to return
 690  
    * @return the Transaction with that index
 691  
    */
 692  
   public PoemTransaction poemTransaction(int index) {
 693  697
     return (PoemTransaction)transactions.elementAt(index);
 694  
   }
 695  
 
 696  
   /**
 697  
    * {@inheritDoc}
 698  
    * @see org.melati.poem.transaction.TransactionPool#transaction(int)
 699  
    */
 700  
   public final Transaction transaction(int index) {
 701  1
     return poemTransaction(index);
 702  
   }
 703  
 
 704  
   /**
 705  
    * @param trans a PoemTransaction
 706  
    * @return whether the Transaction is free
 707  
    */
 708  
   public boolean isFree(PoemTransaction trans) {
 709  3442
     return freeTransactions.contains(trans);
 710  
   }
 711  
 
 712  
   /**
 713  
    * Acquire a lock on the database.
 714  
    */
 715  
   public void beginExclusiveLock() {
 716  
     // FIXME Yuk
 717  116
     if (PoemThread.inSession()) { 
 718  116
       lock.readLock().unlock();
 719  
       
 720  
     }
 721  116
     lock.writeLock().lock();
 722  116
   }
 723  
 
 724  
   /**
 725  
    * Release lock.
 726  
    */
 727  
   public void endExclusiveLock() {
 728  116
     lock.writeLock().unlock();
 729  
 
 730  
     // FIXME Yuk, see above
 731  
 
 732  116
     if (PoemThread.inSession())
 733  116
       lock.readLock().lock();
 734  116
   }
 735  
 
 736  
   //
 737  
   // ---------------
 738  
   //  Starting them
 739  
   // ---------------
 740  
   //
 741  
 
 742  
   /**
 743  
    * Perform a PoemTask.
 744  
    * @param accessToken the AccessToken to run the task under
 745  
    * @param task the PoemTask to perform
 746  
    * @param useCommittedTransaction whether to use an insulated Transaction or the Committed one
 747  
    */
 748  
   private void perform(AccessToken accessToken, final PoemTask task,
 749  
                        boolean useCommittedTransaction) throws PoemException {
 750  
 
 751  3439
     lock.readLock().lock();
 752  
 
 753  3440
     final PoemTransaction transaction =
 754  3434
         useCommittedTransaction ? null : openTransaction();
 755  
     try {
 756  3440
       PoemThread.inSession(new PoemTask() {
 757  
                              public void run() throws PoemException {
 758  3440
                                  task.run();
 759  3440
                                  if (transaction != null) 
 760  3434
                                    transaction.close(true);
 761  3440
                              }
 762  
 
 763  
                              public String toString() {
 764  1
                                return task.toString();
 765  
                              }
 766  
                            },
 767  
                            accessToken,
 768  
                            transaction);
 769  
     }
 770  
     finally {
 771  0
       try {
 772  3440
         if (transaction != null && !isFree(transaction)) {
 773  0
           transaction.close(false);
 774  
         }
 775  
       } finally {
 776  
 
 777  3440
         lock.readLock().unlock();
 778  3440
       }
 779  3440
     }
 780  3440
   }
 781  
 
 782  
   /**
 783  
    * Perform a task with the database.  Every access to a POEM database must be
 784  
    * made in the context of a `transaction' established using this method (note
 785  
    * that Melati programmers don't have to worry about this, because the
 786  
    * <TT>PoemServlet</TT> will have done this by the time they get control).
 787  
    *
 788  
    * @param accessToken    A token determining the <TT>Capability</TT>s
 789  
    *                       available to the task, which in turn determine
 790  
    *                       what data it can attempt to read and write
 791  
    *                       without triggering an
 792  
    *                       <TT>AccessPoemException</TT>.  Note that a
 793  
    *                       <TT>User</TT> can be an <TT>AccessToken</TT>.
 794  
    *
 795  
    * @param task           What to do: its <TT>run()</TT> is invoked, in
 796  
    *                       the current Java thread; until <TT>run()</TT>
 797  
    *                       returns, all POEM accesses made by the thread
 798  
    *                       are taken to be performed with the capabilities
 799  
    *                       given by <TT>accessToken</TT>, and in a private
 800  
    *                       transaction.  No changes made to the database
 801  
    *                       by other transactions will be visible to it (in the
 802  
    *                       sense that once it has seen a particular
 803  
    *                       version of a record, it will always
 804  
    *                       subsequently see the same one), and its own
 805  
    *                       changes will not be made permanent until it
 806  
    *                       completes successfully or performs an explicit
 807  
    *                       <TT>PoemThread.commit()</TT>.  If it terminates
 808  
    *                       with an exception or issues a
 809  
    *                       <TT>PoemThread.rollback()</TT> its changes will
 810  
    *                       be lost.  (The task is allowed to continue
 811  
    *                       after either a <TT>commit()</TT> or a
 812  
    *                       <TT>rollback()</TT>.)
 813  
    *
 814  
    * @see PoemThread
 815  
    * @see PoemThread#commit
 816  
    * @see PoemThread#rollback
 817  
    * @see User
 818  
    */
 819  
   public void inSession(AccessToken accessToken, PoemTask task) {
 820  3433
     perform(accessToken, task, false);
 821  3434
   }
 822  
   
 823  
   /**
 824  
    * @param task the task to run
 825  
    */
 826  
   public void inSessionAsRoot(PoemTask task) { 
 827  0
     perform(AccessToken.root, task, false);
 828  0
   }
 829  
 
 830  
   /**
 831  
    * Start a db session.
 832  
    * This is the very manual way of doing db work - not reccomended -
 833  
    * use inSession.
 834  
    */
 835  
   public void beginSession(AccessToken accessToken) {
 836  3
     lock.readLock().lock();
 837  3
     PoemTransaction transaction = openTransaction();
 838  
     try { 
 839  3
       PoemThread.beginSession(accessToken,transaction);
 840  1
     } catch (AlreadyInSessionPoemException e) { 
 841  1
       notifyClosed(transaction);
 842  1
       lock.readLock().unlock();
 843  1
       throw e;
 844  2
     }
 845  2
   }
 846  
 
 847  
   /**
 848  
    * End a db session.
 849  
    * <p>
 850  
    * This is the very manual way of doing db work - not recommended -
 851  
    * use inSession.
 852  
    */
 853  
   public void endSession() {
 854  2
     PoemTransaction tx = PoemThread.sessionToken().transaction;
 855  2
     PoemThread.endSession();
 856  2
     tx.close(true);
 857  2
     lock.readLock().unlock();
 858  2
   }
 859  
 
 860  
   /**
 861  
    * Perform a task with the database, but not in an insulated transaction.
 862  
    * The effect is the same as <TT>inSession</TT>, except that the task will
 863  
    * see changes to the database made by other transactions as they are
 864  
    * committed, and it is not allowed to make any changes of its own.
 865  
    * <p>
 866  
    * A modification will trigger a <code>WriteCommittedException</code>, 
 867  
    * however a create operation will trigger a NullPointerException, 
 868  
    * as we have no Transaction.
 869  
    * </p>
 870  
    * <p>
 871  
    * Not recommended; why exactly do you want to sidestep the Transaction handling?
 872  
    * </p>
 873  
    * @see #inSession
 874  
    */
 875  
   public void inCommittedTransaction(AccessToken accessToken, PoemTask task) {
 876  6
     perform(accessToken, task, true);
 877  6
   }
 878  
 
 879  
   //
 880  
   // ==================
 881  
   //  Accessing tables
 882  
   // ==================
 883  
   //
 884  
 
 885  
   /**
 886  
    * Retrieve the table with a given name.
 887  
    *
 888  
    * @param tableName        The name of the table to return, as in the RDBMS
 889  
    *                    database.  It's case-sensitive, and some RDBMSs such as
 890  
    *                    Postgres 6.4.2 (and perhaps other versions) treat upper
 891  
    *                    case letters in identifiers inconsistently, so the 
 892  
    *                    name is forced to lowercase.
 893  
    *
 894  
    * @return the Table of that name 
 895  
    *
 896  
    * @exception NoSuchTablePoemException
 897  
    *             if no table with the given name exists in the RDBMS
 898  
    */
 899  
   public final Table<?> getTable(String tableName) throws NoSuchTablePoemException {
 900  176
     Table<?> table = getTableIgnoringCase(tableName);
 901  176
     if (table == null) throw new NoSuchTablePoemException(this, tableName);
 902  107
     return table;
 903  
   }
 904  
   private Table<?> getTableIgnoringCase(String tableName) { 
 905  1729
     return tablesByName.get(tableName.toLowerCase());
 906  
   }
 907  
   
 908  
   /**
 909  
    * All the tables in the database.
 910  
    * NOTE This will include any deleted tables
 911  
    * 
 912  
    * @return an <TT>Enumeration</TT> of <TT>Table</TT>s, in no particular
 913  
    *         order.
 914  
    */
 915  
   public final Enumeration<Table<?>> tables() {
 916  2860
     return tables.elements();
 917  
   }
 918  
 
 919  
   /**
 920  
     * All the tables in the database.
 921  
     * NOTE This will include any deleted tables
 922  
    */
 923  
   public final List<Table<?>> getTables() { 
 924  1
     return tables;
 925  
   }
 926  
   
 927  
   /**
 928  
    * All the tables in the database in DisplayOrder
 929  
    * order, using current transaction if there is one.
 930  
    *
 931  
    * @return an <TT>Enumeration</TT> of <TT>Table</TT>s
 932  
    */
 933  
   public Enumeration<Table<?>> displayTables() {
 934  2
     return displayTables(PoemThread.inSession() ? PoemThread.transaction() : null);
 935  
   }
 936  
   /** A convenience wrapper around displayTables() */
 937  
   public List<Table<?>> getDisplayTables() {
 938  2
     return EnumUtils.list(displayTables());
 939  
   }
 940  
   
 941  
   /**
 942  
    * Currently all the tables in the database in DisplayOrder
 943  
    * order.
 944  
    *
 945  
    * @return an <TT>Enumeration</TT> of <TT>Table</TT>s
 946  
    */
 947  
   public Enumeration<Table<?>> displayTables(PoemTransaction transaction) {
 948  2
     Table<?>[] displayTablesL = this.displayTables;
 949  
 
 950  2
     if (displayTablesL == null) {
 951  4
       Enumeration<Integer> tableIDs = getTableInfoTable().troidSelection(
 952  
         (String)null /* "displayable" */,
 953  2
         quotedName("displayorder") + ", " + quotedName("name"),
 954  
         false, transaction);
 955  
 
 956  2
       Vector<Table<?>> them = new Vector<Table<?>>();
 957  36
       while (tableIDs.hasMoreElements()) {
 958  34
         Table<?> table =
 959  34
             tableWithTableInfoID(tableIDs.nextElement().intValue());
 960  34
         if (table != null)
 961  34
           them.addElement(table);
 962  34
       }
 963  
 
 964  2
       displayTablesL = new Table[them.size()];
 965  2
       them.copyInto(displayTablesL);
 966  2
       this.displayTables = displayTablesL;
 967  
     }
 968  
 
 969  2
     return new ArrayEnumeration<Table<?>>(this.displayTables);
 970  
   }
 971  
 
 972  
   /**
 973  
    * The table with a given ID in the <TT>tableinfo</TT> table, or
 974  
    * <TT>null</TT>.
 975  
    *
 976  
    * @see #getTableInfoTable
 977  
    */
 978  
   Table<?> tableWithTableInfoID(int tableInfoID) {
 979  76
     for (Table<?> table : tables) {
 980  915
       Integer id = table.tableInfoID();
 981  915
       if (id != null && id.intValue() == tableInfoID)
 982  76
         return table;
 983  839
     }
 984  0
     return null;
 985  
   }
 986  
 
 987  
  /**
 988  
   * @return All the {@link Column}s in the whole {@link Database}
 989  
   */
 990  
   public Enumeration<Column<?>> columns() {
 991  1296
     return new FlattenedEnumeration<Column<?>>(
 992  12960
         new MappedEnumeration<Enumeration<Column<?>>, Table<?>>(tables()) {
 993  
           public Enumeration<Column<?>> mapped(Table<?> table) {
 994  11664
             return table.columns();
 995  
           }
 996  
         });
 997  
   }
 998  
   /** Wrapper around columns() */
 999  
   public List<Column<?>> getColumns() { 
 1000  1
     return EnumUtils.list(columns());
 1001  
   }
 1002  
   public int tableCount() { 
 1003  0
     return tables.size();
 1004  
   }
 1005  
   public int columnCount() { 
 1006  0
     return getColumns().size();
 1007  
   }
 1008  
   public int recordCount() { 
 1009  0
     Enumeration<Integer> counts = new MappedEnumeration<Integer, Table<?>>(tables()) {
 1010  
       public Integer mapped(Table<?> table) {
 1011  0
         return new Integer(table.count());
 1012  
       }
 1013  
     };
 1014  0
     int total = 0;
 1015  0
     while(counts.hasMoreElements())
 1016  0
       total = total + counts.nextElement().intValue();
 1017  
     
 1018  0
     return total;
 1019  
   }
 1020  
   
 1021  
   /**    
 1022  
    * @param columnInfoID   
 1023  
    * @return the Column with the given troid   
 1024  
    */  
 1025  
   Column<?> columnWithColumnInfoID(int columnInfoID) {  
 1026  6
     for (Table<?> table : tables) {  
 1027  23
       Column<?> column = table.columnWithColumnInfoID(columnInfoID);   
 1028  23
       if (column != null)  
 1029  5
         return column;   
 1030  18
     }  
 1031  1
     return null;   
 1032  
   }
 1033  
   
 1034  
 
 1035  
   /**
 1036  
    * @return The metadata table with information about all tables in the database.
 1037  
    */
 1038  
   public abstract TableInfoTable<TableInfo> getTableInfoTable();
 1039  
 
 1040  
   /**
 1041  
    * @return The Table Category Table.
 1042  
    */
 1043  
   public abstract TableCategoryTable<TableCategory> getTableCategoryTable();
 1044  
 
 1045  
   /**
 1046  
    * @return The metadata table with information about all columns in all tables in the
 1047  
    * database.
 1048  
    */
 1049  
   public abstract ColumnInfoTable<ColumnInfo> getColumnInfoTable();
 1050  
 
 1051  
   /**
 1052  
    * The table of capabilities (required for reading and/or writing records)
 1053  
    * defined for the database.  Users acquire capabilities in virtue of being
 1054  
    * members of groups.
 1055  
    *
 1056  
    * @return the CapabilityTable
 1057  
    * @see Persistent#assertCanRead()
 1058  
    * @see Persistent#assertCanWrite()
 1059  
    * @see Persistent#assertCanDelete()
 1060  
    * @see JdbcTable#getDefaultCanRead
 1061  
    * @see JdbcTable#getDefaultCanWrite
 1062  
    * @see User
 1063  
    * @see #getUserTable
 1064  
    * @see Group
 1065  
    * @see #getGroupTable
 1066  
    */
 1067  
   public abstract CapabilityTable<Capability> getCapabilityTable();
 1068  
 
 1069  
   /**
 1070  
    * @return the table of known users of the database
 1071  
    */
 1072  
   public abstract UserTable<User> getUserTable();
 1073  
 
 1074  
   /**
 1075  
    * @return the table of defined user groups for the database
 1076  
    */
 1077  
   public abstract GroupTable<Group> getGroupTable();
 1078  
 
 1079  
   /**
 1080  
    * A user is a member of a group iff there is a record in this table to say so.
 1081  
    * @return the table containing group-membership records
 1082  
    */
 1083  
   public abstract GroupMembershipTable<GroupMembership> getGroupMembershipTable();
 1084  
 
 1085  
   /**
 1086  
    * The table containing group-capability records.  A group has a certain
 1087  
    * capability iff there is a record in this table to say so.
 1088  
    * @return the GroupCapability table 
 1089  
    */
 1090  
   public abstract GroupCapabilityTable<GroupCapability> getGroupCapabilityTable();
 1091  
 
 1092  
   /**
 1093  
    * @return the Setting Table.
 1094  
    */
 1095  
   public abstract SettingTable<Setting> getSettingTable();
 1096  
 
 1097  
   //
 1098  
   // ========================
 1099  
   //  Running arbitrary SQL
 1100  
   // ========================
 1101  
   //
 1102  
 
 1103  
   /**
 1104  
    * Run an arbitrary SQL query against the database.  This is a low-level
 1105  
    * <TT>java.sql.Statement.executeQuery</TT>, intended for fiddly queries for
 1106  
    * which the higher-level methods are too clunky or inflexible.  <B>Note</B>
 1107  
    * that it bypasses the access control mechanism!
 1108  
    *
 1109  
    * @return the ResultSet resulting from running the query
 1110  
    * @see Table#selection()
 1111  
    * @see Table#selection(java.lang.String)
 1112  
    * @see Column#selectionWhereEq(java.lang.Object)
 1113  
    */
 1114  
   public ResultSet sqlQuery(String sql) throws SQLPoemException {
 1115  23
     SessionToken token = PoemThread.sessionToken();
 1116  23
     token.transaction.writeDown();
 1117  
     try {
 1118  23
       Statement s = token.transaction.getConnection().createStatement();
 1119  23
       token.toTidy().add(s);
 1120  23
       ResultSet rs = s.executeQuery(sql);
 1121  22
       token.toTidy().add(rs);
 1122  22
       if (logSQL())
 1123  14
         log(new SQLLogEvent(sql));
 1124  22
       incrementQueryCount(sql);
 1125  22
       return rs;
 1126  
     }
 1127  1
     catch (SQLException e) {
 1128  1
       throw new ExecutingSQLPoemException(sql, e);
 1129  
     }
 1130  
   }
 1131  
 
 1132  
   /**
 1133  
    * Run an arbitrary SQL update against the database.  This is a low-level
 1134  
    * <TT>java.sql.Statement.executeUpdate</TT>, intended for fiddly updates for
 1135  
    * which the higher-level methods are too clunky or inflexible.
 1136  
    * <p>  
 1137  
    * NOTE This bypasses the access control mechanism.  Furthermore, the cache
 1138  
    * will be left out of sync with the database and must be cleared out
 1139  
    * (explicitly, manually) after the current transaction has been committed
 1140  
    * or completed.
 1141  
    *
 1142  
    * @return either the row count for <code>INSERT</code>, <code>UPDATE</code>
 1143  
    * or <code>DELETE</code> statements, or <code>0</code> for SQL statements 
 1144  
    * that return nothing
 1145  
    * 
 1146  
    * @see Table#selection()
 1147  
    * @see Table#selection(java.lang.String)
 1148  
    * @see Column#selectionWhereEq(java.lang.Object)
 1149  
    * @see #uncache
 1150  
    */
 1151  
   public int sqlUpdate(String sql) throws SQLPoemException {
 1152  5
     SessionToken token = PoemThread.sessionToken();
 1153  5
     token.transaction.writeDown();
 1154  
 
 1155  
     try {
 1156  5
       Statement s = token.transaction.getConnection().createStatement();
 1157  5
       token.toTidy().add(s);
 1158  5
       int n = s.executeUpdate(sql);
 1159  3
       if (logSQL())
 1160  2
         log(new SQLLogEvent(sql));
 1161  3
       incrementQueryCount(sql);
 1162  3
       return n;
 1163  
     }
 1164  2
     catch (SQLException e) {
 1165  4
       throw dbms.exceptionForUpdate(null, sql,
 1166  2
                                     sql.indexOf("INSERT") >= 0 ||
 1167  1
                                       sql.indexOf("insert") >= 0,
 1168  
                                     e);
 1169  
     }
 1170  
   }
 1171  
 
 1172  
   //
 1173  
   // =======
 1174  
   //  Users
 1175  
   // =======
 1176  
   //
 1177  
 
 1178  43
   private User guest = null;
 1179  
   /**
 1180  
    * @return the guest
 1181  
    */
 1182  
   public User guestUser() {
 1183  21
     if (guest == null)
 1184  3
       guest = getUserTable().guestUser();
 1185  21
     return guest;
 1186  
   }
 1187  
 
 1188  43
   private User administrator = null; 
 1189  
   /**
 1190  
    * @return the administrator
 1191  
    */
 1192  
   public User administratorUser() {
 1193  3
     if (administrator == null)
 1194  2
       administrator = getUserTable().administratorUser();
 1195  3
     return administrator;
 1196  
   }
 1197  
 
 1198  
   
 1199  
   /**
 1200  
    * Get the raw SQL statement for this database's DBMS for Capability 
 1201  
    * check for a User.
 1202  
    * @param user
 1203  
    * @param capability
 1204  
    * @return the raw SQL appropriate for this db
 1205  
    */
 1206  
   public String givesCapabilitySQL(User user, Capability capability) {
 1207  
     // NOTE Bootstrapping to troid or we get a stack overflow 
 1208  13
     return dbms.givesCapabilitySQL(user.troid(), capability.troid().toString());
 1209  
   }
 1210  
 
 1211  
  /**
 1212  
   * TODO Use a prepared statement to get Capabilities
 1213  
   */
 1214  
   private boolean dbGivesCapability(User user, Capability capability) {
 1215  
 
 1216  12
     String sql = givesCapabilitySQL(user, capability);
 1217  12
     ResultSet rs = null;
 1218  
     try {
 1219  12
       rs = sqlQuery(sql);
 1220  12
       return rs.next();
 1221  
     }
 1222  0
     catch (SQLPoemException e) {
 1223  0
       throw new UnexpectedExceptionPoemException(e);
 1224  
     }
 1225  0
     catch (SQLException e) {
 1226  0
       throw new SQLSeriousPoemException(e, sql);
 1227  
     }
 1228  
     finally {
 1229  12
       try { if (rs != null) rs.close(); } catch (Exception e) {
 1230  0
         System.err.println("Cannot close resultset after exception.");
 1231  24
       }
 1232  
     }
 1233  
   }
 1234  
 
 1235  86
   private class UserCapabilityCache {
 1236  43
     private Hashtable<Long,Boolean> userCapabilities = null;
 1237  
     private long groupMembershipSerial;
 1238  
     private long groupCapabilitySerial;
 1239  
 
 1240  
     boolean hasCapability(User user, Capability capability) {
 1241  33
       PoemTransaction transaction = PoemThread.transaction();
 1242  33
       long currentGroupMembershipSerial =
 1243  33
           getGroupMembershipTable().serial(transaction);
 1244  33
       long currentGroupCapabilitySerial =
 1245  33
           getGroupCapabilityTable().serial(transaction);
 1246  
 
 1247  33
       if (userCapabilities == null ||
 1248  
           groupMembershipSerial != currentGroupMembershipSerial ||
 1249  
           groupCapabilitySerial != currentGroupCapabilitySerial) {
 1250  8
         userCapabilities = new Hashtable<Long,Boolean>();
 1251  8
         groupMembershipSerial = currentGroupMembershipSerial;
 1252  8
         groupCapabilitySerial = currentGroupCapabilitySerial;
 1253  
       }
 1254  
 
 1255  33
       Long pair = new Long(
 1256  33
           (user.troid().longValue() << 32) | (capability.troid().longValue()));
 1257  33
       Boolean known = userCapabilities.get(pair);
 1258  
 
 1259  33
       if (known != null)
 1260  21
         return known.booleanValue();
 1261  
       else {
 1262  12
         boolean does = dbGivesCapability(user, capability);
 1263  12
         userCapabilities.put(pair, does ? Boolean.TRUE : Boolean.FALSE);
 1264  12
         return does;
 1265  
       }
 1266  
     }
 1267  
   }
 1268  
 
 1269  43
   private UserCapabilityCache capabilityCache = new UserCapabilityCache();
 1270  
 
 1271  
   /**
 1272  
    * Check if a user has the specified Capability.
 1273  
    * @param user the User to check
 1274  
    * @param capability the Capability required
 1275  
    * @return whether User has Capability
 1276  
    */
 1277  
   public boolean hasCapability(User user, Capability capability) {
 1278  
     // no capability means that we always have access
 1279  35
     if (capability == null) return true;
 1280  
     // otherwise, go to the cache
 1281  33
     return capabilityCache.hasCapability(user, capability);
 1282  
   }
 1283  
 
 1284  
   /**
 1285  
    * @return the guest token.
 1286  
    */
 1287  
   public AccessToken guestAccessToken() {
 1288  9
     return getUserTable().guestUser();
 1289  
   }
 1290  
 
 1291  43
   private Capability canAdminister = null;
 1292  
   
 1293  
   /**
 1294  
    * @return the Capability required to administer this db.
 1295  
    */
 1296  
   public Capability administerCapability() {
 1297  1156
     return getCapabilityTable().administer();
 1298  
   }
 1299  
 
 1300  
  
 1301  
   /**
 1302  
    * By default, anyone can administer a database.
 1303  
    *
 1304  
    * @return the required {@link Capability} to administer the db
 1305  
    * (<tt>null</tt> unless overridden)
 1306  
    */
 1307  
   public Capability getCanAdminister() {
 1308  5
     return canAdminister;
 1309  
   }
 1310  
   
 1311  
   /**
 1312  
    * Set administrator capability to default.
 1313  
    * <p>
 1314  
    * NOTE Once a database has had its <tt>canAdminister</tt> capability set 
 1315  
    * there is no mechanism to set it back to null. 
 1316  
    */
 1317  
   public void setCanAdminister() {
 1318  3
     canAdminister = administerCapability();
 1319  3
   }
 1320  
   /**
 1321  
    * Set administrator capability to named Capability.
 1322  
    * @param capabilityName name of Capability
 1323  
    */
 1324  
   public void setCanAdminister(String capabilityName) {
 1325  1
     canAdminister = getCapabilityTable().ensure(capabilityName);
 1326  1
   }
 1327  
 
 1328  
   //
 1329  
   // ==========
 1330  
   //  Cacheing
 1331  
   // ==========
 1332  
   //
 1333  
 
 1334  
   /**
 1335  
    * Trim POEM's cache to a given size.
 1336  
    *
 1337  
    * @param maxSize     The data for all but this many records per table will
 1338  
    *                    be dropped from POEM's cache, on a least-recently-used
 1339  
    *                    basis.
 1340  
    */
 1341  
   public void trimCache(int maxSize) {
 1342  1
     for (Table<?> table : tables)
 1343  9
       table.trimCache(maxSize);
 1344  1
   }
 1345  
 
 1346  
   /**
 1347  
    * Set the contents of the cache to empty.
 1348  
    */
 1349  
   public void uncache() {
 1350  258
     for (int t = 0; t < tables.size(); ++t)
 1351  245
       tables.elementAt(t).uncache();
 1352  13
   }
 1353  
 
 1354  
   //
 1355  
   // ===========
 1356  
   //  Utilities
 1357  
   // ===========
 1358  
   //
 1359  
 
 1360  
   /**
 1361  
    * Find all references to specified object in all tables. 
 1362  
    * 
 1363  
    * @param persistent the object being referred to 
 1364  
    * @return an Enumeration of {@link Persistent}s
 1365  
    */
 1366  
   @SuppressWarnings({ "rawtypes", "unchecked" })
 1367  
   public <P extends Persistent> Enumeration<P> referencesTo(final Persistent persistent) {
 1368  2
     return new FlattenedEnumeration<P>(
 1369  2
         new MappedEnumeration(tables()) {
 1370  
           @Override
 1371  
           public Object mapped(Object table) {
 1372  18
             return ((Table<P>)table).referencesTo(persistent);
 1373  
           }
 1374  
         });
 1375  
   }
 1376  
 
 1377  
   /** 
 1378  
    * Wrapper around referencesTo() 
 1379  
    * @return a List of Columns referring to given Table 
 1380  
    */
 1381  
   public List<Persistent> getReferencesTo(final Persistent persistent) { 
 1382  1
     return EnumUtils.list(referencesTo(persistent));
 1383  
   }
 1384  
 
 1385  
   /**
 1386  
    * @return An Enumeration of Columns referring to the specified Table. 
 1387  
    */
 1388  
   public Enumeration<Column<?>> referencesTo(final Table<?> tableIn) {
 1389  116
     return new FlattenedEnumeration<Column<?>>(
 1390  2338
         new MappedEnumeration<Enumeration<Column<?>>, Table<?>>(tables()) {
 1391  
           public Enumeration<Column<?>> mapped(Table<?> table) {
 1392  2222
             return table.referencesTo(tableIn);
 1393  
           }
 1394  
         });
 1395  
   }
 1396  
   
 1397  
   /** 
 1398  
    * Wrapper around referencesTo() 
 1399  
    * @return a List of Columns referring to given Table 
 1400  
    */
 1401  
   public List<Column<?>> getReferencesTo(final Table<?> table) { 
 1402  0
     return EnumUtils.list(referencesTo(table));
 1403  
   }
 1404  
 
 1405  
   /**
 1406  
    * Print some diagnostic information about the contents and consistency of
 1407  
    * POEM's cache to stderr.
 1408  
    */
 1409  
   public void dumpCacheAnalysis() {
 1410  1
     for (Table<?> table : tables)
 1411  9
       table.dumpCacheAnalysis();
 1412  1
   }
 1413  
 
 1414  
   /**
 1415  
    * Print information about the structure of the database to stdout.
 1416  
    */
 1417  
   public void dump() {
 1418  10
     for (int t = 0; t < tables.size(); ++t) {
 1419  9
       System.out.println();
 1420  9
       tables.elementAt(t).dump();
 1421  
     }
 1422  
 
 1423  2
     System.err.println("there are " + getTransactionsCount() + " transactions " +
 1424  1
                        "of which " + getFreeTransactionsCount() + " are free");
 1425  1
   }
 1426  
 
 1427  
   //
 1428  
   // =========================
 1429  
   //  Database-specific stuff
 1430  
   // =========================
 1431  
   //
 1432  
 
 1433  
   /**
 1434  
    * @return the Database Management System class of this db
 1435  
    */
 1436  
   public Dbms getDbms() {
 1437  31914
       return dbms;
 1438  
   }
 1439  
 
 1440  
   /**
 1441  
    * Set the DBMS.
 1442  
    * 
 1443  
    * @param aDbms
 1444  
    */
 1445  
   private void setDbms(Dbms aDbms) {
 1446  41
       dbms = aDbms;
 1447  41
   }
 1448  
 
 1449  
   /**
 1450  
    * Quote a name in the DBMS' specific dialect.
 1451  
    * @param nameIn
 1452  
    * @return name quoted.
 1453  
    */
 1454  
   public final String quotedName(String nameIn) {
 1455  6809
       return getDbms().getQuotedName(nameIn);
 1456  
   }
 1457  
 
 1458  
   /**
 1459  
    * The default {@link PoemType} corresponding to a ResultSet of JDBC column metadata.
 1460  
    *  
 1461  
    * @param md A result set of the JDBC columns metadata
 1462  
    * @return The appropriatePoemType
 1463  
    */
 1464  
   final SQLPoemType<?> defaultPoemTypeOfColumnMetaData(ResultSet md)
 1465  
       throws SQLException {
 1466  4091
     return getDbms().defaultPoemTypeOfColumnMetaData(md);
 1467  
   }
 1468  
 
 1469  
   //
 1470  
   // =====================
 1471  
   //  Technical utilities
 1472  
   // =====================
 1473  
   //
 1474  
 
 1475  
 
 1476  
   /**
 1477  
    * Returns the connection url.
 1478  
    * If you want a simple name see LogicalDatabase.
 1479  
    * {@inheritDoc}
 1480  
    * @see java.lang.Object#toString()
 1481  
    */
 1482  
   public String toString() {
 1483  15
     if (connectionUrl == null)
 1484  1
       return "unconnected database";
 1485  
     else
 1486  14
       return connectionUrl;
 1487  
   }
 1488  
 
 1489  
   /**
 1490  
    * @return the non-transactioned jdbc Connection
 1491  
    */
 1492  
   public Connection getCommittedConnection() {
 1493  4290
     return committedConnection;
 1494  
   }
 1495  
 
 1496  
   /**
 1497  
    * @return whether logging is switched on
 1498  
    */
 1499  
   public boolean logSQL() {
 1500  25913
     return logSQL;
 1501  
   }
 1502  
 
 1503  
   /**
 1504  
    * Toggle logging.
 1505  
    */
 1506  
   public void setLogSQL(boolean value) {
 1507  110
     logSQL = value;
 1508  110
   }
 1509  
 
 1510  
   /**
 1511  
    * @return whether database commits should be logged
 1512  
    */
 1513  
   public boolean logCommits() {
 1514  6084
     return logCommits;
 1515  
   }
 1516  
 
 1517  
   /**
 1518  
    * Toggle commit logging.
 1519  
    */
 1520  
   public void setLogCommits(boolean value) {
 1521  105
     logCommits = value;
 1522  105
   }
 1523  
 
 1524  
   void log(PoemLogEvent e) {
 1525  10061
     System.err.println("---\n" + e.toString());
 1526  10061
   }
 1527  
   void log(String s) {
 1528  168
     System.err.println(s);
 1529  168
   }
 1530  
 
 1531  
   protected void beginStructuralModification() {
 1532  103
     beginExclusiveLock();
 1533  103
   }
 1534  
 
 1535  
   /**
 1536  
    * Uncache, increment serial and release exclusive lock.
 1537  
    */
 1538  
   protected void endStructuralModification() {
 1539  1444
     for (int t = 0; t < tables.size(); ++t)
 1540  1341
       tables.elementAt(t).uncache();
 1541  103
     ++structureSerial;
 1542  103
     endExclusiveLock();
 1543  103
   }
 1544  
 
 1545  
   /**
 1546  
    * @return an id incremented for each change
 1547  
    */
 1548  
   long structureSerial() {
 1549  3091
     return structureSerial;
 1550  
   }
 1551  
 
 1552  
   /**
 1553  
    * Used in testing to check if the cache is being used 
 1554  
    * or a new query is being issued. 
 1555  
    * @return Returns the queryCount.
 1556  
    */
 1557  
   public int getQueryCount() {
 1558  76
     return queryCount;
 1559  
   }
 1560  
   /**
 1561  
    * Increment query count.
 1562  
    */
 1563  
   public void incrementQueryCount(String sql) {
 1564  25909
     lastQuery = sql;
 1565  25909
     queryCount++; 
 1566  25909
   }
 1567  
 
 1568  
   /**
 1569  
    * @return the most recent query
 1570  
    */
 1571  
   public String getLastQuery() { 
 1572  3
     return lastQuery;
 1573  
   }
 1574  
   /**
 1575  
    * @return the name
 1576  
    */
 1577  
   public String getName() {
 1578  4
     return name;
 1579  
   }
 1580  
 
 1581  
   /**
 1582  
    * @return the displayName
 1583  
    */
 1584  
   public String getDisplayName() {
 1585  1
     if (displayName == null) 
 1586  1
       return StringUtils.capitalised(getName());
 1587  0
     return displayName;
 1588  
   }
 1589  
 
 1590  
   /**
 1591  
    * @param displayName the displayName to set
 1592  
    */
 1593  
   public void setDisplayName(String displayName) {
 1594  0
     this.displayName = displayName;
 1595  0
   }
 1596  
 
 1597  
   /**
 1598  
    * Use this for DDL statements, ie those which alter the structure of the db.
 1599  
    * Postgresql in particular does not like DDL statements being executed within a transaction.
 1600  
    * 
 1601  
    * @param sql the SQL DDL statement to execute
 1602  
    */
 1603  
   public void modifyStructure(String sql)
 1604  
       throws StructuralModificationFailedPoemException {
 1605  
     
 1606  
     // We have to do this to avoid blocking
 1607  1269
     if (PoemThread.inSession())
 1608  1149
       PoemThread.commit();
 1609  
   
 1610  
     try {
 1611  1269
       Statement updateStatement = getCommittedConnection().createStatement();
 1612  1269
       updateStatement.executeUpdate(sql);
 1613  1269
       updateStatement.close();
 1614  1269
       getCommittedConnection().commit();
 1615  1269
       if (logCommits()) log(new CommitLogEvent(null));
 1616  1269
       if (logSQL()) log(new StructuralModificationLogEvent(sql));
 1617  1269
       incrementQueryCount(sql);
 1618  
     }
 1619  0
     catch (SQLException e) {
 1620  0
       throw new StructuralModificationFailedPoemException(sql, e);
 1621  1269
     }
 1622  1269
   }
 1623  
 
 1624  
 }
 1625