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