View Javadoc

1   /*
2    * $Source: /usr/cvsroot/melati/poem/src/main/java/org/melati/poem/Database.java,v $
3    * $Revision: 1.155 $
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.Vector;
56  
57  import org.melati.poem.dbms.Dbms;
58  import org.melati.poem.dbms.DbmsFactory;
59  import org.melati.poem.transaction.Transaction;
60  import org.melati.poem.transaction.TransactionPool;
61  import org.melati.poem.util.ArrayEnumeration;
62  import org.melati.poem.util.ArrayUtils;
63  import org.melati.poem.util.FlattenedEnumeration;
64  import org.melati.poem.util.MappedEnumeration;
65  import org.melati.poem.util.StringUtils;
66  
67  import EDU.oswego.cs.dl.util.concurrent.ReadWriteLock;
68  import EDU.oswego.cs.dl.util.concurrent.WriterPreferenceReadWriteLock;
69  
70  /**
71   * An RDBMS database.  Don't instantiate (or subclass) this class, but rather
72   * {@link PoemDatabase}, which includes the boilerplate code for the standard
73   * tables such as <TT>user</TT> and <TT>columninfo</TT> which all POEM
74   * databases must contain.  If the database is predefined by a Data Structure
75   * Definition <TT><I>Bar</I>.dsd</TT>, there will be an application-specialised
76   * subclass of <TT>PoemDatabase</TT> called <TT><I>Bar</I>Database</TT> which
77   * provides named methods for accessing application-specialised objects
78   * representing the predefined tables.
79   *
80   * @see PoemDatabase
81   */
82  
83  public abstract class Database implements TransactionPool {
84  
85    final Database _this = this;
86  
87    private Vector transactions = null; 
88    private Vector freeTransactions = null;
89  
90    private Connection committedConnection;
91    private final ReadWriteLock lock = new WriterPreferenceReadWriteLock();
92    private long structureSerial = 0L;
93  
94    private Vector tables = new Vector();
95    private Hashtable tablesByName = new Hashtable();
96    private Table[] displayTables = null;
97  
98    private String name;
99    private String displayName;
100   private Dbms dbms;
101   private boolean logSQL = false;
102   private boolean logCommits = false;
103   private int transactionsMax;
104 
105   private String connectionUrl;
106   
107   /**
108    * Used in testing to check caching.
109    */
110   private int queryCount = 0;
111   /** Used in tests to check caching etc. */
112   private String lastQuery = null;
113   
114 
115   //
116   // ================
117   //  Initialisation
118   // ================
119   //
120 
121   /**
122    * Don't subclass this, subclass <TT>PoemDatabase</TT>.
123    * @see PoemDatabase
124    */
125 
126   public Database() {
127   }
128 
129   private boolean initialised = false;
130 
131   /**
132    * Initialise each table.
133    */
134   private synchronized void init() {
135     if (!initialised) {
136       for (Enumeration t = this.tables.elements(); t.hasMoreElements();)
137         ((Table)t.nextElement()).init();
138       initialised = true;
139     }
140   }
141 
142   private final boolean[] connecting = new boolean[1];
143 
144  /**
145   * Thrown when a request is made whilst the connection to
146   * the underlying database is still in progress.
147   */
148   public class ConnectingException extends PoemException {
149     private static final long serialVersionUID = 1L;
150     /**
151      * {@inheritDoc}
152      */
153     public String getMessage() {
154       return "Connection to the database is currently in progress; " +
155              "please try again in a moment";
156     }
157   }
158 
159   /**
160    * Connect to an RDBMS database.  This should be called once when the
161    * application starts up; it will
162    *
163    * <UL>
164    *   <LI> Open <TT>this.transactionsMax()</TT> JDBC <TT>Connection</TT>s to
165    *        the database for subsequent `pooling'
166    *   </LI>
167    *   <LI> Unify (reconcile) the structural information about the database
168    *        given in
169    *
170    *        <OL>
171    *          <LI> the Database Structure Definition (<I>i.e.</I> embodied in
172    *               the boilerplate code generated from it), including the
173    *               POEM-standard tables defined in <TT>Poem.dsd</TT>;
174    *          <LI> the metadata tables <TT>tableinfo</TT> and
175    *               <TT>columninfo</TT>;
176    *          <LI> the actual JDBC metadata from the RDBMS.
177    *        </OL>
178    *
179    *        Any tables or columns defined in the DSD or the metadata tables,
180    *        but not present in the actual database, will be created.
181    *        <BR>
182    *        Conversely, entries will be created in the metadata tables for
183    *        tables and columns that don't have them.  If an inconsistency is
184    *        detected between any of the three information sources (such as a
185    *        fundamental type incompatibility, or a string field which is
186    *        narrower in the database than it was declared to be), an exception
187    *        will be thrown.  In that case the database will in theory be left
188    *        untouched, except that in Postgres (at least) all structural
189    *        updates happen immediately and irrevocably even if made from a
190    *        transaction subsequently rolled back.
191    *   </LI>
192    * </UL>
193    *
194    * @param dbmsclass   The Melati DBMS class (see org/melati/poem/dbms)
195    *                    to use, usually specified in
196    *                    org.melati.LogicalDatabase.properties.
197    *
198    * @param url         The JDBC URL for the database; for instance
199    *                    <TT>jdbc:postgresql:williamc</TT>.  It is the
200    *                    programmer's responsibility to make sure that an
201    *                    appropriate driver has been loaded.
202    *
203    * @param username    The username under which to establish JDBC connections
204    *                    to the database.  This has nothing to do with the
205    *                    user/group/capability authentication performed by
206    *                    Melati.
207    *
208    * @param password    The password to go with the username.
209    *
210    * @param transactionsMaxP
211    *                    The maximum number of concurrent Transactions allowed,
212    *                    usually specified in
213    *                    org.melati.LogicalDatabase.properties.
214    *
215    * @see #transactionsMax()
216    */
217   public void connect(String name, String dbmsclass, String url,
218                       String username, String password,
219                       int transactionsMaxP) throws PoemException {
220 
221     this.name = name;
222     this.connectionUrl = url;
223 
224     synchronized (connecting) {
225       if (connecting[0])
226         throw new ConnectingException();
227       connecting[0] = true;
228     }
229 
230     try {
231       setDbms(DbmsFactory.getDbms(dbmsclass));
232 
233       if (committedConnection != null)
234         throw new ReconnectionPoemException(this);
235 
236       setTransactionsMax(transactionsMaxP);
237       committedConnection = getDbms().getConnection(url, username, password);
238       transactions = new Vector();
239       for (int s = 0; s < transactionsMax(); ++s)
240         transactions.add(
241           new PoemTransaction(
242               this,
243               getDbms().getConnection(url, username, password),
244               s));
245 
246       freeTransactions = (Vector)transactions.clone();
247 
248       try {
249         // Perform any table specific initialisation, none by default
250         init();
251 
252         // Bootstrap: set up the tableinfo and columninfo tables
253         DatabaseMetaData m = committedConnection.getMetaData();
254         getTableInfoTable().unifyWithDB(
255             m.getColumns(null, dbms.getSchema(),
256                          dbms.unreservedName(getTableInfoTable().getName()), null));
257         getColumnInfoTable().unifyWithDB(
258             m.getColumns(null, dbms.getSchema(),
259                          dbms.unreservedName(getColumnInfoTable().getName()), null));
260         getTableCategoryTable().unifyWithDB(
261             m.getColumns(null, dbms.getSchema(),
262                          dbms.unreservedName(getTableCategoryTable().getName()), null));
263 
264         inSession(AccessToken.root,
265                   new PoemTask() {
266                     public void run() throws PoemException {
267                       try {
268                         _this.unifyWithDB();
269                       }
270                       catch (SQLException e) {
271                         throw new SQLPoemException(e);
272                       }
273                     }
274 
275                     public String toString() {
276                       return "Unifying with DB";
277                     }
278                   });
279       }
280       catch (SQLException e) {
281         if (committedConnection != null) disconnect();
282         throw new UnificationPoemException(e);
283       }
284     } 
285     catch (SQLPoemException e) { 
286       if (committedConnection != null) disconnect();
287       throw e;
288     }
289     finally {
290       synchronized (connecting) {
291         connecting[0] = false;
292       }
293     }
294   }
295 
296   /**
297    * Releases database connections.
298    */
299   public void disconnect() throws PoemException {
300     if (committedConnection == null)
301       throw new ReconnectionPoemException(this);
302 
303     try {
304       Enumeration iter = freeTransactions.elements();
305       while (iter.hasMoreElements()) {
306         PoemTransaction txn = (PoemTransaction)iter.nextElement();
307         txn.getConnection().close();
308       }
309       freeTransactions.removeAllElements();
310       
311       getDbms().shutdown(committedConnection);
312       committedConnection.close();
313     } catch (SQLException e) {
314       throw new SQLPoemException(e);
315     }
316     committedConnection = null;
317   }
318   
319   /**
320    * Don't call this.  Tables should be defined either in the DSD (in which
321    * case the boilerplate code generated by the preprocessor will call this
322    * method), or directly in the RDBMS (in which case the initialisation code
323    * will), or using <TT>addTableAndCommit</TT>.
324    *
325    * @see #addTableAndCommit
326    */
327   protected synchronized void defineTable(Table table)
328       throws DuplicateTableNamePoemException {
329     if (tablesByName.get(table.getName().toLowerCase()) != null)
330       throw new DuplicateTableNamePoemException(this, table.getName());
331     redefineTable(table);
332   }
333 
334   protected synchronized void redefineTable(Table table) {
335     if (table.getDatabase() != this)
336       throw new TableInUsePoemException(this, table);
337 
338     if (tablesByName.get(table.getName().toLowerCase()) == null) {
339       tablesByName.put(table.getName().toLowerCase(), table);
340       tables.addElement(table);
341     }
342     else
343       tables.setElementAt(table,
344                           tables.indexOf(
345                               tablesByName.put(table.getName().toLowerCase(), table)));
346     displayTables = null;
347   }
348 
349   private ResultSet columnsMetadata(DatabaseMetaData m, String tableName)
350       throws SQLException {
351     return m.getColumns(null, dbms.getSchema(), dbms.unreservedName(tableName), null);
352   }
353 
354   /**
355    * Add a Table to this Databse and commit the Transaction.
356    * @param info Table metadata object
357    * @param troidName name of troidColumn
358    * @return new minted {@link Table} 
359    */
360   public Table addTableAndCommit(TableInfo info, String troidName)
361       throws PoemException {
362 
363     // For permission control we rely on them having successfully created a
364     // TableInfo
365 
366     Table table = new JdbcTable(this, info.getName(),
367                             DefinitionSource.infoTables);
368     table.defineColumn(new ExtraColumn(table, troidName,
369                                        TroidPoemType.it,
370                                        DefinitionSource.infoTables,
371                                        table.getNextExtrasIndex()));
372     table.setTableInfo(info);
373     table.unifyWithColumnInfo();
374     table.unifyWithDB(null);
375 
376     PoemThread.commit();
377     defineTable(table);
378 
379     return table;
380   }
381   
382   /**
383    * @param info the tableInfo for the table to delete
384    */
385   public void deleteTableAndCommit(TableInfo info) { 
386     beginStructuralModification();
387     try {
388       Table table = info.actualTable();
389       info.delete(); // Ensure we have no references in metadata
390       table.dbModifyStructure(" DROP TABLE " + table.quotedName());
391       synchronized (tables) {
392         tables.remove(table);
393         tablesByName.remove(table.getName().toLowerCase());
394         displayTables = (Table[])ArrayUtils.removed(displayTables, table);
395         uncache();
396         table.invalidateTransactionStuffs();
397       }
398       PoemThread.commit();
399     }
400     finally {
401       endStructuralModification();
402     }
403   }
404 
405   private synchronized void unifyWithDB() throws PoemException, SQLException {
406     boolean debug = false;
407     // Check all tables defined in the tableInfo metadata table
408     // defining the ones that don't exist
409 
410     for (Enumeration ti = getTableInfoTable().selection();
411          ti.hasMoreElements();) {
412       TableInfo tableInfo = (TableInfo)ti.nextElement();
413       Table table = (Table)tablesByName.get(tableInfo.getName().toLowerCase());
414       if (table == null) {
415         if (debug) log("Defining table:" + tableInfo.getName());
416         table = new JdbcTable(this, tableInfo.getName(),
417                           DefinitionSource.infoTables);
418         defineTable(table);
419       }
420       table.setTableInfo(tableInfo);
421     }
422 
423     // Conversely, add tableInfo for the tables that do not have an entry in tableInfo
424 
425     for (Enumeration t = tables.elements(); t.hasMoreElements();)
426       ((Table)t.nextElement()).createTableInfo();
427 
428     // Check all tables against columnInfo
429 
430     for (Enumeration t = tables.elements(); t.hasMoreElements();)
431       ((Table)t.nextElement()).unifyWithColumnInfo();
432 
433     // Finally, check tables against the actual JDBC metadata
434 
435     String[] normalTables = { "TABLE" };
436 
437     DatabaseMetaData m = committedConnection.getMetaData();
438     ResultSet tableDescs = m.getTables(null, dbms.getSchema(), null,
439                                        normalTables);
440     while (tableDescs.next()) {
441       if (debug) log("Table:" + tableDescs.getString("TABLE_NAME") +
442                         " Type:" + tableDescs.getString("TABLE_TYPE"));
443       String tableName = dbms.melatiName(tableDescs.getString("TABLE_NAME"));
444       Table table = null;
445       if (tableName != null) { //dbms returning grotty table name (MSAccess)
446         table = (Table)tablesByName.get(tableName.toLowerCase());
447         if (table == null) {  // We do not know about this table
448           if (debug) log("Unknown to POEM, with JDBC name " + tableName);
449 
450           // but we only want to include them if they have a plausible troid:
451           ResultSet idCol = m.getColumns(null, dbms.getSchema(), dbms.unreservedName(tableName), 
452               dbms.getJdbcMetadataName(dbms.unreservedName("id")));
453           if (idCol.next()) { 
454             if (debug) log("Got an ID column for discovered jdbc table ");
455             if (dbms.canRepresent(
456                    defaultPoemTypeOfColumnMetaData(idCol), TroidPoemType.it) != null) {
457               try {
458                 table = new JdbcTable(this, tableName,
459                                   DefinitionSource.sqlMetaData);
460                 defineTable(table);
461               }
462               catch (DuplicateTableNamePoemException e) {
463                 throw new UnexpectedExceptionPoemException(e);
464               }
465               table.createTableInfo();
466             } else if (debug) log("Can represent failed");
467           }  else if (debug) log("Table " + dbms.unreservedName(tableName) + 
468                   " appears not to have a troid column called " + 
469                   dbms.getJdbcMetadataName(dbms.unreservedName("id")));
470           /*
471           // Try to promote the primary key to a troid
472           else {
473             ResultSet pKeys = m.getPrimaryKeys(null, dbms.getSchema(), tableName);
474             if (pKeys.next()) {
475               String keyName = pKeys.getString("COLUMN_NAME");
476               if (!pKeys.next()) {
477                 ResultSet keyCol = m.getColumns(null, dbms.getSchema(),
478                                                 tableName, keyName);
479                 if (keyCol.next() &&
480                   dbms.canRepresent(defaultPoemTypeOfColumnMetaData(keyCol),
481                                     TroidPoemType.it) != null) {
482                   if (debug) log("Got a unique primary key");
483                   try {
484                     defineTable(table = new Table(this, tableName,
485                                                   DefinitionSource.sqlMetaData));
486                   }
487                   catch (DuplicateTableNamePoemException e) {
488                     throw new UnexpectedExceptionPoemException(e);
489                   }
490                   table.createTableInfo();
491                 }
492               }
493             }
494           } */
495         } else if (debug) log("table not null:" + tableName);
496       }
497 
498       if (table != null) {
499          if (debug) log("table not null now:" + tableName);
500          if (debug) log("columnsMetadata(m, tableName):"
501                             + columnsMetadata(m, tableName));
502          // Create the table if it has no metadata
503          // unify with it either way
504         table.unifyWithDB(columnsMetadata(m, tableName));
505       } else if (debug) log("table still null, probably doesn't have a troid:" + tableName);
506 
507     }
508 
509     // ... and create any that simply don't exist
510 
511     for (Enumeration t = tables.elements(); t.hasMoreElements();) {
512       Table table = (Table)t.nextElement();
513       // bit yukky using getColumns ...
514       ResultSet colDescs = columnsMetadata(m,
515                                dbms.unreservedName(table.getName()));
516       if (!colDescs.next()) {
517         // System.err.println("Table has no columns in dbms:" +
518         //                    dbms.unreservedName(table.getName()));
519         table.unifyWithDB(null);
520       }
521     }
522 
523     for (Enumeration t = tables.elements(); t.hasMoreElements();)
524       ((JdbcTable)t.nextElement()).postInitialise();
525     
526   }
527 
528   /**
529    * Add database constraints.
530    * The only constraints POEM expects are uniqueness and nullability.
531    * POEM assumes that the db will exploit indexes where present.
532    * However if you wish to export the db to a more DB oriented 
533    * application or wish to use schema interrogation or visualisation tools 
534    * then constraints can be added.
535    * Whether constraints are added is controlled in 
536    * org.melati.LogicalDatabase.properties. 
537    */
538   public void addConstraints() {
539     inSession(AccessToken.root,
540         new PoemTask() {
541           public void run() throws PoemException {
542             PoemThread.commit();
543             beginStructuralModification();
544             try {
545               for (Enumeration t = tables.elements(); t.hasMoreElements();)
546                 ((Table)t.nextElement()).dbAddConstraints();
547               PoemThread.commit();
548             }
549             finally {
550               endStructuralModification();
551             }
552           }
553 
554           public String toString() {
555             return "Adding constraints to DB";
556           }
557         });
558   }
559 
560   //
561   // ==============
562   //  Transactions
563   // ==============
564   //
565 
566   /**
567    * The number of transactions available for concurrent use on the database.
568    * This is the number of JDBC <TT>Connection</TT>s opened when the database
569    * was <TT>connect</TT>ed, this can be set via LogicalDatabase.properties,
570    * but defaults to 8 if not set.
571    * 
572    * {@inheritDoc}
573    * @see org.melati.poem.transaction.TransactionPool#transactionsMax()
574    */
575   public final int transactionsMax() {
576     return transactionsMax;
577   }
578 
579   /**
580    * Set the maximum number of transactions.
581    * Note that this does not resize the transaction pool 
582    * so should be called before the db is connected to.
583    * 
584    * {@inheritDoc}
585    * @see org.melati.poem.transaction.TransactionPool#setTransactionsMax(int)
586    */
587   public final void setTransactionsMax(int t) {
588     transactionsMax = t;
589   }
590 
591   /**
592    * {@inheritDoc}
593    * @see org.melati.poem.transaction.TransactionPool#getTransactionsCount()
594    */
595   public int getTransactionsCount() {
596     return transactions.size();
597   }
598 
599   /**
600    * {@inheritDoc}
601    * @see org.melati.poem.transaction.TransactionPool#getFreeTransactionsCount()
602    */
603   public int getFreeTransactionsCount() {
604     return freeTransactions.size();
605   }
606 
607   //
608   // ----------------------------------
609   //  Keeping track of the Transactions
610   // ----------------------------------
611   //
612 
613   /**
614    * Get a transaction for exclusive use.  It's simply taken off the freelist,
615    * to be put back later.
616    */
617   private PoemTransaction openTransaction() {
618     synchronized (freeTransactions) {
619       if (freeTransactions.size() == 0)
620         throw new NoMoreTransactionsException("Database " + name + " has no free transactions remaining of " 
621             + transactions.size() + " transactions.");
622       PoemTransaction transaction =
623           (PoemTransaction)freeTransactions.lastElement();
624       freeTransactions.setSize(freeTransactions.size() - 1);
625       return transaction; }
626   }
627 
628   /**
629    * Finish using a transaction, put it back on the freelist.
630    */
631   void notifyClosed(PoemTransaction transaction) {
632     freeTransactions.addElement(transaction);
633   }
634 
635   /**
636    * Find a transaction by its index.
637    * <p>
638    * transaction(i).index() == i
639    * 
640    * @param index the index of the Transaction to return
641    * @return the Transaction with that index
642    */
643   public PoemTransaction poemTransaction(int index) {
644     return (PoemTransaction)transactions.elementAt(index);
645   }
646 
647   /**
648    * {@inheritDoc}
649    * @see org.melati.poem.transaction.TransactionPool#transaction(int)
650    */
651   public final Transaction transaction(int index) {
652     return poemTransaction(index);
653   }
654 
655   /**
656    * @param trans a PoemTransaction
657    * @return whether the Transaction is free
658    */
659   public boolean isFree(PoemTransaction trans) {
660     return freeTransactions.contains(trans);
661   }
662 
663   /**
664    * Aquire a lock on the database.
665    */
666   public void beginExclusiveLock() {
667     // FIXME yuk
668 
669     if (PoemThread.inSession())
670       lock.readLock().release();
671 
672     try {
673       lock.writeLock().acquire();
674     }
675     catch (InterruptedException e) {
676       throw new InterruptedPoemException(e);
677     }
678   }
679 
680   /**
681    * Release lock.
682    */
683   public void endExclusiveLock() {
684     lock.writeLock().release();
685 
686     // FIXME yuk, see above
687 
688     if (PoemThread.inSession())
689       try {
690         lock.readLock().acquire();
691       }
692       catch (InterruptedException e) {
693         throw new InterruptedPoemException(e);
694       }
695   }
696 
697   //
698   // ---------------
699   //  Starting them
700   // ---------------
701   //
702 
703   /**
704    * Perform a PoemTask.
705    * @param accessToken the AccessToken to run the task under
706    * @param task the PoemTask to perform
707    * @param useCommittedTransaction whether to use an insulated Transaction or the Committed one
708    */
709   private void perform(AccessToken accessToken, final PoemTask task,
710                        boolean useCommittedTransaction) throws PoemException {
711     try {
712       lock.readLock().acquire();
713     }
714     catch (InterruptedException e) {
715       throw new InterruptedPoemException(e);
716     }
717 
718     final PoemTransaction transaction =
719         useCommittedTransaction ? null : openTransaction();
720     try {
721       PoemThread.inSession(new PoemTask() {
722                              public void run() throws PoemException {
723                                  task.run();
724                                  if (transaction != null) 
725                                    transaction.close(true);
726                              }
727 
728                              public String toString() {
729                                return task.toString();
730                              }
731                            },
732                            accessToken,
733                            transaction);
734     }
735     finally {
736       try {
737         if (transaction != null && !isFree(transaction)) {
738           transaction.close(false);
739         }
740       } finally {
741 
742         lock.readLock().release();
743       }
744     }
745   }
746 
747   /**
748    * Perform a task with the database.  Every access to a POEM database must be
749    * made in the context of a `transaction' established using this method (note
750    * that Melati programmers don't have to worry about this, because the
751    * <TT>PoemServlet</TT> will have done this by the time they get control).
752    *
753    * @param accessToken    A token determining the <TT>Capability</TT>s
754    *                       available to the task, which in turn determine
755    *                       what data it can attempt to read and write
756    *                       without triggering an
757    *                       <TT>AccessPoemException</TT>.  Note that a
758    *                       <TT>User</TT> can be an <TT>AccessToken</TT>.
759    *
760    * @param task           What to do: its <TT>run()</TT> is invoked, in
761    *                       the current Java thread; until <TT>run()</TT>
762    *                       returns, all POEM accesses made by the thread
763    *                       are taken to be performed with the capabilities
764    *                       given by <TT>accessToken</TT>, and in a private
765    *                       transaction.  No changes made to the database
766    *                       by other transactions will be visible to it (in the
767    *                       sense that once it has seen a particular
768    *                       version of a record, it will always
769    *                       subsequently see the same one), and its own
770    *                       changes will not be made permanent until it
771    *                       completes successfully or performs an explicit
772    *                       <TT>PoemThread.commit()</TT>.  If it terminates
773    *                       with an exception or issues a
774    *                       <TT>PoemThread.rollback()</TT> its changes will
775    *                       be lost.  (The task is allowed to continue
776    *                       after either a <TT>commit()</TT> or a
777    *                       <TT>rollback()</TT>.)
778    *
779    * @see PoemThread
780    * @see PoemThread#commit
781    * @see PoemThread#rollback
782    * @see User
783    */
784   public void inSession(AccessToken accessToken, PoemTask task) {
785     perform(accessToken, task, false);
786   }
787   
788   /**
789    * @param task the task to run
790    */
791   public void inSessionAsRoot(PoemTask task) { 
792     perform(AccessToken.root, task, false);
793   }
794 
795   /**
796    * Start a db session.
797    * This is the very manual way of doing db work - not reccomended -
798    * use inSession.
799    */
800   public void beginSession(AccessToken accessToken) {
801     try {
802       lock.readLock().acquire();
803     }
804     catch (InterruptedException e) {
805       throw new InterruptedPoemException(e);
806     }
807     PoemTransaction transaction = openTransaction();
808     try { 
809       PoemThread.beginSession(accessToken,transaction);
810     } catch (AlreadyInSessionPoemException e) { 
811       notifyClosed(transaction);
812       lock.readLock().release();
813       throw e;
814     }
815   }
816 
817   /**
818    * End a db session.
819    * <p>
820    * This is the very manual way of doing db work - not recommended -
821    * use inSession.
822    */
823   public void endSession() {
824     PoemTransaction tx = PoemThread.sessionToken().transaction;
825     PoemThread.endSession();
826     tx.close(true);
827     lock.readLock().release();
828 
829   }
830 
831   /**
832    * Perform a task with the database, but not in an insulated transaction.
833    * The effect is the same as <TT>inSession</TT>, except that the task will
834    * see changes to the database made by other transactions as they are
835    * committed, and it is not allowed to make any changes of its own.
836    * <p>
837    * A modification will trigger a <code>WriteCommittedException</code>, 
838    * however a create operation will trigger a NullPointerException, 
839    * as we have no Transaction.
840    * </p>
841    * <p>
842    * Not recommended; why exactly do you want to sidestep the Transaction handling?
843    * </p>
844    * @see #inSession
845    */
846   public void inCommittedTransaction(AccessToken accessToken, PoemTask task) {
847     perform(accessToken, task, true);
848   }
849 
850   //
851   // ==================
852   //  Accessing tables
853   // ==================
854   //
855 
856   /**
857    * Retrieve the table with a given name.
858    *
859    * @param name        The name of the table to return, as in the RDBMS
860    *                    database.  It's case-sensitive, and some RDBMSs such as
861    *                    Postgres 6.4.2 (and perhaps other versions) treat upper
862    *                    case letters in identifiers inconsistently, so the 
863    *                    name is forced to lowercase.
864    *
865    * @return the Table of that name 
866    *
867    * @exception NoSuchTablePoemException
868    *             if no table with the given name exists in the RDBMS
869    */
870   public final Table getTable(String name) throws NoSuchTablePoemException {
871     Table table = (Table)tablesByName.get(name.toLowerCase());
872     if (table == null) throw new NoSuchTablePoemException(this, name);
873     return table;
874   }
875 
876   /**
877    * All the tables in the database.
878    * NOTE This will include any deleted tables
879    * 
880    * @return an <TT>Enumeration</TT> of <TT>Table</TT>s, in no particular
881    *         order.
882    */
883   public final Enumeration tables() {
884      
885     return tables.elements();
886   }
887 
888   /**
889    * All the tables in the database in DisplayOrder
890    * order, using current transaction if there is one.
891    *
892    * @return an <TT>Enumeration</TT> of <TT>Table</TT>s
893    */
894   public Enumeration getDisplayTables() {
895     return getDisplayTables(PoemThread.inSession() ? PoemThread.transaction() : null);
896   }
897   
898   /**
899    * Currently all the tables in the database in DisplayOrder
900    * order.
901    *
902    * @return an <TT>Enumeration</TT> of <TT>Table</TT>s
903    */
904   public Enumeration getDisplayTables(PoemTransaction transaction) {
905     Table[] displayTablesL = this.displayTables;
906 
907     if (displayTablesL == null) {
908       Enumeration tableIDs = getTableInfoTable().troidSelection(
909         (String)null /* "displayable" */,
910         quotedName("displayorder") + ", " + quotedName("name"),
911         false, transaction);
912 
913       Vector them = new Vector();
914       while (tableIDs.hasMoreElements()) {
915         Table table =
916             tableWithTableInfoID(((Integer)tableIDs.nextElement()).intValue());
917         if (table != null)
918           them.addElement(table);
919       }
920 
921       displayTablesL = new Table[them.size()];
922       them.copyInto(displayTablesL);
923       this.displayTables = displayTablesL;
924     }
925 
926     return new ArrayEnumeration(this.displayTables);
927   }
928 
929   /**
930    * The table with a given ID in the <TT>tableinfo</TT> table, or
931    * <TT>null</TT>.
932    *
933    * @see #getTableInfoTable
934    */
935   Table tableWithTableInfoID(int tableInfoID) {
936     for (Enumeration t = tables.elements(); t.hasMoreElements();) {
937       Table table = (Table)t.nextElement();
938       Integer id = table.tableInfoID();
939       if (id != null && id.intValue() == tableInfoID)
940         return table;
941     }
942 
943     return null;
944   }
945 
946  /**
947   * @return All the {@link Column}s in the whole {@link Database}
948   */
949   public Enumeration columns() {
950     return new FlattenedEnumeration(
951         new MappedEnumeration(tables()) {
952           public Object mapped(Object table) {
953             return ((Table)table).columns();
954           }
955         });
956   }
957    
958   /**    
959    * @param columnInfoID   
960    * @return the Column with the given troid   
961    */  
962   Column columnWithColumnInfoID(int columnInfoID) {  
963     for (Enumeration t = tables.elements(); t.hasMoreElements();) {  
964       Column column =  
965           ((Table)t.nextElement()).columnWithColumnInfoID(columnInfoID);   
966       if (column != null)  
967         return column;   
968     }  
969     return null;   
970   }  
971 
972   /**
973    * @return The metadata table with information about all tables in the database.
974    */
975   public abstract TableInfoTable getTableInfoTable();
976 
977   /**
978    * @return The Table Category Table.
979    */
980   public abstract TableCategoryTable getTableCategoryTable();
981 
982   /**
983    * @return The metadata table with information about all columns in all tables in the
984    * database.
985    */
986   public abstract ColumnInfoTable getColumnInfoTable();
987 
988   /**
989    * The table of capabilities (required for reading and/or writing records)
990    * defined for the database.  Users acquire capabilities in virtue of being
991    * members of groups.
992    *
993    * @return the CapabilityTable
994    * @see Persistent#getCanRead
995    * @see Persistent#getCanWrite
996    * @see Persistent#getCanDelete
997    * @see JdbcTable#getDefaultCanRead
998    * @see JdbcTable#getDefaultCanWrite
999    * @see User
1000    * @see #getUserTable
1001    * @see Group
1002    * @see #getGroupTable
1003    */
1004   public abstract CapabilityTable getCapabilityTable();
1005 
1006   /**
1007    * @return the table of known users of the database
1008    */
1009   public abstract UserTable getUserTable();
1010 
1011   /**
1012    * @return the table of defined user groups for the database
1013    */
1014   public abstract GroupTable getGroupTable();
1015 
1016   /**
1017    * A user is a member of a group iff there is a record in this table to say so.
1018    * @return the table containing group-membership records
1019    */
1020   public abstract GroupMembershipTable getGroupMembershipTable();
1021 
1022   /**
1023    * The table containing group-capability records.  A group has a certain
1024    * capability iff there is a record in this table to say so.
1025    * @return the GroupCapability table 
1026    */
1027   public abstract GroupCapabilityTable getGroupCapabilityTable();
1028 
1029   /**
1030    * @return the Setting Table.
1031    */
1032   public abstract SettingTable getSettingTable();
1033 
1034   //
1035   // ========================
1036   //  Running arbitrary SQL
1037   // ========================
1038   //
1039 
1040   /**
1041    * Run an arbitrary SQL query against the database.  This is a low-level
1042    * <TT>java.sql.Statement.executeQuery</TT>, intended for fiddly queries for
1043    * which the higher-level methods are too clunky or inflexible.  <B>Note</B>
1044    * that it bypasses the access control mechanism!
1045    *
1046    * @return the ResultSet resulting from running the query
1047    * @see Table#selection()
1048    * @see Table#selection(java.lang.String)
1049    * @see Column#selectionWhereEq(java.lang.Object)
1050    */
1051   public ResultSet sqlQuery(String sql) throws SQLPoemException {
1052     SessionToken token = PoemThread.sessionToken();
1053     token.transaction.writeDown();
1054     try {
1055       Statement s = token.transaction.getConnection().createStatement();
1056       token.toTidy().add(s);
1057       ResultSet rs = s.executeQuery(sql);
1058       token.toTidy().add(rs);
1059       if (logSQL())
1060         log(new SQLLogEvent(sql));
1061       incrementQueryCount(sql);
1062       return rs;
1063     }
1064     catch (SQLException e) {
1065       throw new ExecutingSQLPoemException(sql, e);
1066     }
1067   }
1068 
1069   /**
1070    * Run an arbitrary SQL update against the database.  This is a low-level
1071    * <TT>java.sql.Statement.executeUpdate</TT>, intended for fiddly updates for
1072    * which the higher-level methods are too clunky or inflexible.
1073    * <p>  
1074    * NOTE This bypasses the access control mechanism.  Furthermore, the cache
1075    * will be left out of sync with the database and must be cleared out
1076    * (explicitly, manually) after the current transaction has been committed
1077    * or completed.
1078    *
1079    * @return either the row count for <code>INSERT</code>, <code>UPDATE</code>
1080    * or <code>DELETE</code> statements, or <code>0</code> for SQL statements 
1081    * that return nothing
1082    * 
1083    * @see Table#selection()
1084    * @see Table#selection(java.lang.String)
1085    * @see Column#selectionWhereEq(java.lang.Object)
1086    * @see #uncache
1087    */
1088   public int sqlUpdate(String sql) throws SQLPoemException {
1089     SessionToken token = PoemThread.sessionToken();
1090     token.transaction.writeDown();
1091 
1092     try {
1093       Statement s = token.transaction.getConnection().createStatement();
1094       token.toTidy().add(s);
1095       int n = s.executeUpdate(sql);
1096       if (logSQL())
1097         log(new SQLLogEvent(sql));
1098       incrementQueryCount(sql);
1099       return n;
1100     }
1101     catch (SQLException e) {
1102       throw dbms.exceptionForUpdate(null, sql,
1103                                     sql.indexOf("INSERT") >= 0 ||
1104                                       sql.indexOf("insert") >= 0,
1105                                     e);
1106     }
1107   }
1108 
1109   //
1110   // =======
1111   //  Users
1112   // =======
1113   //
1114 
1115   private User guest = null;
1116   /**
1117    * @return the guest
1118    */
1119   public User guestUser() {
1120     if (guest == null)
1121       guest = getUserTable().guestUser();
1122     return guest;
1123   }
1124 
1125   private User administrator = null; 
1126   /**
1127    * @return the administrator
1128    */
1129   public User administratorUser() {
1130     if (administrator == null)
1131       administrator = getUserTable().administratorUser();
1132     return administrator;
1133   }
1134 
1135   
1136   /**
1137    * Get the raw SQL statement for this database's DBMS for Capability 
1138    * check for a User.
1139    * @param user
1140    * @param capability
1141    * @return the raw SQL appropriate for this db
1142    */
1143   public String givesCapabilitySQL(User user, Capability capability) {
1144     // NOTE Bootstrapping to troid or we get a stack overflow 
1145     return dbms.givesCapabilitySQL(user.troid(), capability.troid().toString());
1146   }
1147 
1148  /**
1149   * @todo Use a prepared statement to get Capabilities
1150   */
1151   private boolean dbGivesCapability(User user, Capability capability) {
1152 
1153     String sql = givesCapabilitySQL(user, capability);
1154     ResultSet rs = null;
1155     try {
1156       rs = sqlQuery(sql);
1157       return rs.next();
1158     }
1159     catch (SQLPoemException e) {
1160       throw new UnexpectedExceptionPoemException(e);
1161     }
1162     catch (SQLException e) {
1163       throw new SQLSeriousPoemException(e, sql);
1164     }
1165     finally {
1166       try { if (rs != null) rs.close(); } catch (Exception e) {
1167         System.err.println("Cannot close resultset after exception.");
1168       }
1169     }
1170   }
1171 
1172   private class UserCapabilityCache {
1173     private Hashtable userCapabilities = null;
1174     private long groupMembershipSerial;
1175     private long groupCapabilitySerial;
1176 
1177     boolean hasCapability(User user, Capability capability) {
1178       PoemTransaction transaction = PoemThread.transaction();
1179       long currentGroupMembershipSerial =
1180           getGroupMembershipTable().serial(transaction);
1181       long currentGroupCapabilitySerial =
1182           getGroupCapabilityTable().serial(transaction);
1183 
1184       if (userCapabilities == null ||
1185           groupMembershipSerial != currentGroupMembershipSerial ||
1186           groupCapabilitySerial != currentGroupCapabilitySerial) {
1187         userCapabilities = new Hashtable();
1188         groupMembershipSerial = currentGroupMembershipSerial;
1189         groupCapabilitySerial = currentGroupCapabilitySerial;
1190       }
1191 
1192       Long pair = new Long(
1193           (user.troid().longValue() << 32) | (capability.troid().longValue()));
1194       Boolean known = (Boolean)userCapabilities.get(pair);
1195 
1196       if (known != null)
1197         return known.booleanValue();
1198       else {
1199         boolean does = dbGivesCapability(user, capability);
1200         userCapabilities.put(pair, does ? Boolean.TRUE : Boolean.FALSE);
1201         return does;
1202       }
1203     }
1204   }
1205 
1206   private UserCapabilityCache capabilityCache = new UserCapabilityCache();
1207 
1208   /**
1209    * Check if a user has the specified Capability.
1210    * @param user the User to check
1211    * @param capability the Capability required
1212    * @return whether User has Capability
1213    */
1214   public boolean hasCapability(User user, Capability capability) {
1215     // no capability means that we always have access
1216     if (capability == null) return true;
1217     // otherwise, go to the cache
1218     return capabilityCache.hasCapability(user, capability);
1219   }
1220 
1221   /**
1222    * @return the guest token.
1223    */
1224   public AccessToken guestAccessToken() {
1225     return getUserTable().guestUser();
1226   }
1227 
1228   private Capability canAdminister = null;
1229   
1230   /**
1231    * @return the Capability required to administer this db.
1232    */
1233   public Capability administerCapability() {
1234     return getCapabilityTable().administer();
1235   }
1236 
1237  
1238   /**
1239    * By default, anyone can administer a database.
1240    *
1241    * @return the required {@link Capability} to administer the db
1242    * (<tt>null</tt> unless overridden)
1243    */
1244   public Capability getCanAdminister() {
1245     return canAdminister;
1246   }
1247   
1248   /**
1249    * Set administrator capability to default.
1250    * <p>
1251    * NOTE Once a database has had its <tt>canAdminister</tt> capability set 
1252    * there is no mechanism to set it back to null. 
1253    */
1254   public void setCanAdminister() {
1255     canAdminister = administerCapability();
1256   }
1257   /**
1258    * Set administrator capability to named Capability.
1259    * @param capabilityName name of Capability