View Javadoc

1   /*
2    * $Source: /usr/cvsroot/melati/poem/src/main/java/org/melati/poem/JdbcTable.java,v $
3    * $Revision: 1.14 $
4    *
5    * Copyright (C) 2008 Tim Pizey
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   *     Tim Pizey <timp At paneris.org>
42   *     http://paneris.org/~timp
43   */
44  
45  package org.melati.poem;
46  
47  import java.io.PrintStream;
48  import java.sql.Connection;
49  import java.sql.PreparedStatement;
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.transaction.Transactioned;
59  import org.melati.poem.transaction.TransactionedSerial;
60  import org.melati.poem.util.ArrayEnumeration;
61  import org.melati.poem.util.ArrayUtils;
62  import org.melati.poem.util.Cache;
63  import org.melati.poem.util.CachedIndexFactory;
64  import org.melati.poem.util.EnumUtils;
65  import org.melati.poem.util.MappedEnumeration;
66  import org.melati.poem.util.Procedure;
67  import org.melati.poem.util.FilteredEnumeration;
68  import org.melati.poem.util.FlattenedEnumeration;
69  import org.melati.poem.util.Order;
70  import org.melati.poem.util.SortUtils;
71  import org.melati.poem.util.StringUtils;
72  
73  /**
74   * A Table.
75   * @since 14 April 2008 
76   */
77  public class JdbcTable implements Selectable, Table {
78  
79    /** Default limit for row cache. */
80    private static final int CACHE_LIMIT_DEFAULT = 100;
81    private static final int DISPLAY_ORDER_DEFAULT = 100;
82  
83    private JdbcTable _this = this;
84  
85    private Database database;
86    private String name;
87    private String quotedName;
88    private DefinitionSource definitionSource;
89  
90    private TableInfo info = null;
91  
92    private TableListener[] listeners = {};
93  
94    private Column[] columns = {};
95    private Hashtable columnsByName = new Hashtable();
96  
97    private Column troidColumn = null;
98    private Column deletedColumn = null;
99    private Column canReadColumn = null;
100   private Column canSelectColumn = null;
101   private Column canWriteColumn = null;
102   private Column canDeleteColumn = null;
103   private Column displayColumn = null;
104   private Column searchColumn = null;
105 
106   private String defaultOrderByClause = null;
107 
108   private Column[][] displayColumns = new Column[DisplayLevel.count()][];
109   private Column[] searchColumns = null;
110 
111   private TransactionedSerial serial;
112 
113   private CachedSelection allTroids = null;
114   private Hashtable cachedSelections = new Hashtable();
115   private Hashtable cachedCounts = new Hashtable();
116   private Hashtable cachedExists = new Hashtable();
117 
118   private int mostRecentTroid = -1;
119   private int extrasIndex = 0;
120 
121   
122   /**
123    * Constructor.
124    */
125   public JdbcTable(Database database, String name,
126                DefinitionSource definitionSource) {
127     this.database = database;
128     this.name = name;
129     // Don't do this here as the database does not know about the dbms yet
130     // this.quotedName = database.quotedName(name);
131     // this is actually set the first time it is accessed in quotedName()
132     this.definitionSource = definitionSource;
133     serial = new TransactionedSerial(database);
134   }
135 
136   /**
137    * Do stuff immediately after table initialisation.
138    * <p>
139    * This base method clears the column info caches and adds a listener
140    * to the column info table to maintain the caches.
141    * <p>
142    * It may be overridden to perform other actions. For example to
143    * ensure required rows exist in tables that define numeric ID's for
144    * codes.
145    *
146    * @see #notifyColumnInfo(ColumnInfo)
147    * @see #clearColumnInfoCaches()
148    */
149   public void postInitialise() {
150     clearColumnInfoCaches();
151     database.getColumnInfoTable().addListener(
152         new TableListener() {
153           public void notifyTouched(PoemTransaction transaction, Table table,
154                                     Persistent persistent) {
155             _this.notifyColumnInfo((ColumnInfo)persistent);
156           }
157 
158           public void notifyUncached(Table table) {
159             _this.clearColumnInfoCaches();
160           }
161         });
162   }
163 
164   // 
165   // ===========
166   //  Accessors
167   // ===========
168   // 
169 
170   /**
171    * The database to which the table is attached.
172    * @return the db
173    */
174   public final Database getDatabase() {
175     return database;
176   }
177 
178   /** 
179    * The table's programmatic name.  Identical with its name in the DSD (if the
180    * table was defined there) and in its <TT>tableinfo</TT> entry.
181    * This will normally be the same as the name in the RDBMS itself, however that name 
182    * may be translated to avoid DBMS specific name clashes. 
183    *
184    * @return the table name, case as defined in the DSD
185    * @see org.melati.poem.dbms.Dbms#melatiName(String)
186    */
187   public final String getName() {
188     return name;
189   }
190 
191  /**
192   * @return table name quoted using the DBMS' specific quoting rules.
193   */
194   public final String quotedName() {
195     if (quotedName == null) quotedName = database.quotedName(name);
196     return quotedName;
197   }
198 
199  /**
200   * The human-readable name of the table.  POEM itself doesn't use this, but
201   * it's available to applications and Melati's generic admin system as a
202   * default label for the table and caption for its records.
203    * @return The human-readable name of the table
204    */
205   public final String getDisplayName() {
206     return info.getDisplayname();
207   }
208 
209  /**
210   * A brief description of the table's function.  POEM itself doesn't use
211   * this, but it's available to applications and Melati's generic admin system
212   * as a default label for the table and caption for its records.
213   * @return the brief description
214   */
215   public final String getDescription() {
216     return info.getDescription();
217   }
218 
219   /**
220    * The category of this table.  POEM itself doesn't use
221    * this, but it's available to applications and Melati's generic admin system
222    * as a default label for the table and caption for its records.
223    * 
224    * @return the category
225    */
226   public final TableCategory getCategory() {
227      return info.getCategory();
228   }
229 
230  /**
231   * @return the {@link TableInfo} for this table
232   */
233   public final TableInfo getInfo() {
234      return info;
235   }
236 
237  /**
238   * The troid (<TT>id</TT>) of the table's entry in the <TT>tableinfo</TT>
239   * table.  It will always have one (except during initialisation, which the
240   * application programmer will never see).
241   * 
242   * @return id in TableInfo metadata table
243   */
244   public final Integer tableInfoID() {
245     return info == null ? null : info.troid();
246   }
247 
248   /**
249    * The table's column with a given name.  If the table is defined in the DSD
250    * under the name <TT><I>foo</I></TT>, there will be an
251    * application-specialised <TT>Table</TT> subclass, called
252    * <TT><I>Foo</I>Table</TT> (and available as <TT>get<I>Foo</I>Table</TT>
253    * from the application-specialised <TT>Database</TT> subclass) which has
254    * extra named methods for accessing the table's predefined <TT>Column</TT>s.
255    *
256    * @param nameP name of column to get
257    * @return column of that name
258    * @throws NoSuchColumnPoemException if there is no column with that name
259    */
260   public final Column getColumn(String nameP) throws NoSuchColumnPoemException {
261     Column column = _getColumn(nameP); 
262     if (column == null)
263       throw new NoSuchColumnPoemException(this, nameP);
264     else
265       return column;
266   }
267   protected final Column _getColumn(String nameP) {
268     Column column = (Column)columnsByName.get(nameP.toLowerCase());    
269     return column;
270   }
271   
272   /**
273    * All the table's columns.
274    *
275    * @return an <TT>Enumeration</TT> of <TT>Column</TT>s
276    * @see Column
277    */
278   public final Enumeration columns() {
279     return new ArrayEnumeration(columns);
280   }
281 
282  /**
283   * @return the number of columns in this table.
284   */
285   public final int getColumnsCount() {
286     return columns.length;
287   }
288 
289   /**
290    * @param columnInfoID
291    * @return the Column with a TROID equal to columnInfoID
292    */
293   public Column columnWithColumnInfoID(int columnInfoID) {
294     for (Enumeration c = columns(); c.hasMoreElements();) {
295       Column column = (Column)c.nextElement();
296       Integer id = column.columnInfoID();
297       if (id != null && id.intValue() == columnInfoID)
298         return column;
299     }
300     return null; // Happens when columns exist but are not defined in DSD
301   }
302 
303   /**
304    * The table's troid column.  Every table in a POEM database must have a
305    * troid (table row ID, or table-unique non-nullable integer primary key),
306    * often but not necessarily called <TT>id</TT>, so that it can be
307    * conveniently `named'.
308    *
309    * @return the id column
310    * @see #getObject(java.lang.Integer)
311    */
312   public final Column troidColumn() {
313     return troidColumn;
314   }
315 
316   /**
317    * @return The table's deleted-flag column, if any.
318    */
319   public final Column deletedColumn() {
320     return deletedColumn;
321   }
322 
323   /**
324    * The table's primary display column, the Troid column if not set.  
325    * This is the column used to represent records from the table 
326    * concisely in reports or whatever.  It is determined 
327    * at initialisation time by examining the <TT>Column</TT>s
328    * <TT>getPrimaryDisplay()</TT> flags.
329    *
330    * @return the table's display column, or <TT>null</TT> if it hasn't got one
331    *
332    * see Column#setColumnInfo
333    * @see ReferencePoemType#_stringOfCooked
334    * @see DisplayLevel#primary
335    */
336   public final Column displayColumn() {
337     return displayColumn == null ? troidColumn : displayColumn;
338   }
339 
340   /**
341    * @param column the display column to set
342    */
343   public final void setDisplayColumn(Column column) {
344     displayColumn = column;
345   }
346 
347  /**
348   * In a similar manner to the primary display column, each table can have 
349   * one primary criterion column.
350   * <p>
351   * The Primary Criterion is the main grouping field of the table, 
352   * ie the most important non-unique type field.
353   * <p>
354   * For example the Primary Criterion for a User table might be Nationality.
355   *
356   * @return the search column, if any
357   * @see Searchability
358   */
359   public final Column primaryCriterionColumn() {
360     return searchColumn;
361   }
362 
363   /**
364    * @param column the search column to set
365    */
366   public void setSearchColumn(Column column) {
367     searchColumn = column;
368   }
369 
370   /**
371    * If the troidColumn has yet to be set then returns an empty string.
372    *  
373    * @return comma separated list of the columns to order by
374    */
375   public String defaultOrderByClause() {
376     String clause = defaultOrderByClause;
377 
378     if (clause == null) {
379       clause = EnumUtils.concatenated(
380           ", ",
381           new MappedEnumeration(new ArrayEnumeration(SortUtils.sorted(
382               new Order() {
383                 public boolean lessOrEqual(Object a, Object b) {
384                   return
385                       ((Column)a).getDisplayOrderPriority().intValue() <=
386                       ((Column)b).getDisplayOrderPriority().intValue();
387                 }
388               },
389               new FilteredEnumeration(columns()) {
390                 public boolean isIncluded(Object column) {
391                   return ((Column)column).getDisplayOrderPriority() != null;
392                 }
393               }))) {
394             public Object mapped(Object column) {
395               String sort = ((Column)column).fullQuotedName();
396               if (((Column)column).getSortDescending()) sort += " desc";
397               return sort;
398             }
399           });
400 
401       if (clause.equals("") && displayColumn() != null)
402         clause = displayColumn().fullQuotedName();
403 
404       defaultOrderByClause = clause;
405     }
406 
407     return clause;
408   }
409 
410   /**
411    * Clear caches.
412    */
413   public void clearColumnInfoCaches() {
414     defaultOrderByClause = null;
415     for (int i = 0; i < displayColumns.length; ++i)
416       displayColumns[i] = null;
417   }
418 
419   /**
420    * Clears columnInfo caches, normally a no-op.
421    * 
422    * @param infoP the possibly null ColumnInfo meta-data persistent
423    */
424   public void notifyColumnInfo(ColumnInfo infoP) {
425     // FIXME info == null means deleted: effect is too broad really
426     if (infoP == null || infoP.getTableinfo_unsafe().equals(tableInfoID()))
427       clearColumnInfoCaches();
428   }
429 
430   /**
431    * Get an Array of columns meeting the criteria of whereClause.
432    * 
433    * It is the programmer's responsibility to ensure that the where clause 
434    * is suitable for the target DBMS.
435    * 
436    * @param whereClause an SQL snippet
437    * @return an array of Columns
438    */
439   private Column[] columnsWhere(String whereClause) {
440     // get the col IDs from the committed session
441     Enumeration colIDs =
442         getDatabase().getColumnInfoTable().troidSelection(
443             database.quotedName("tableinfo") + " = " + tableInfoID() + 
444               " AND (" + whereClause + ")",
445             null, false, PoemThread.inSession() ? PoemThread.transaction() : null);
446 
447     Vector them = new Vector();
448     while (colIDs.hasMoreElements()) {
449       Column column =
450           columnWithColumnInfoID(((Integer)colIDs.nextElement()).intValue());
451       // null shouldn't happen but let's not gratuitously fail if it does
452       if (column != null)
453         them.addElement(column);
454     }
455 
456     Column[] columnsLocal = new Column[them.size()];
457     them.copyInto(columnsLocal);
458     return columnsLocal;
459   }
460 
461   /**
462    * Return columns at a display level in display order.
463    *
464    * @param level the {@link DisplayLevel} to select
465    * @return an Enumeration of columns at the given level
466    */ 
467   public final Enumeration displayColumns(DisplayLevel level) {
468     Column[] columnsLocal = displayColumns[level.getIndex().intValue()];
469 
470     if (columnsLocal == null) {
471       columnsLocal =
472         columnsWhere(database.quotedName("displaylevel") + " <= " + 
473                                                          level.getIndex());
474       displayColumns[level.getIndex().intValue()] = columnsLocal;
475     }
476     return new ArrayEnumeration(columnsLocal);
477   }
478 
479   /**
480    * @param level the {@link DisplayLevel} to select
481    * @return the number of columns at a display level.
482    */ 
483   public final int displayColumnsCount(DisplayLevel level) {
484     int l = level.getIndex().intValue();
485     if (displayColumns[l] == null)
486       // FIXME Race 
487       displayColumns(level);
488 
489     return displayColumns[l].length;
490   }
491 
492   /**
493    * The table's columns for detailed display in display order.
494    *
495    * @return an <TT>Enumeration</TT> of <TT>Column</TT>s
496    * @see Column
497    * @see #displayColumns(DisplayLevel)
498    * @see DisplayLevel#detail
499    */
500   public final Enumeration getDetailDisplayColumns() {
501     return displayColumns(DisplayLevel.detail);
502   }
503 
504   /**
505    * @return the number of columns at display level <tt>Detail</tt>
506    */ 
507   public final int getDetailDisplayColumnsCount() {
508     return displayColumnsCount(DisplayLevel.detail);
509   }
510 
511   /**
512    * The table's columns designated for display in a record, in display order.
513    *
514    * @return an <TT>Enumeration</TT> of <TT>Column</TT>s
515    * @see Column
516    * @see #displayColumns(DisplayLevel)
517    * @see DisplayLevel#record
518    */
519   public final Enumeration getRecordDisplayColumns() {
520     return displayColumns(DisplayLevel.record);
521   }
522 
523   /**
524    * @return the number of columns at display level <tt>Record</tt>
525    */ 
526   public final int getRecordDisplayColumnsCount() {
527     return displayColumnsCount(DisplayLevel.record);
528   }
529 
530   /**
531    * The table's columns designated for display in a record summary, in display
532    * order.
533    *
534    * @return an <TT>Enumeration</TT> of <TT>Column</TT>s
535    * @see Column
536    * @see #displayColumns(DisplayLevel)
537    * @see DisplayLevel#summary
538    */
539   public final Enumeration getSummaryDisplayColumns() {
540     return displayColumns(DisplayLevel.summary);
541   }
542   
543   /**
544    * @return the number of columns at display level <tt>Summary</tt>
545    */ 
546   public final int getSummaryDisplayColumnsCount() {
547     return displayColumnsCount(DisplayLevel.summary);
548   }
549 
550   /**
551    * The table's columns designated for use as search criteria, in display
552    * order.
553    *
554    * @return an <TT>Enumeration</TT> of <TT>Column</TT>s
555    * @see Column
556    */
557   public final Enumeration getSearchCriterionColumns() {
558     Column[] columnsLocal = searchColumns;
559 
560     if (columnsLocal == null) {
561       columnsLocal = 
562          columnsWhere(database.quotedName("searchability") + " <= " +
563                                           Searchability.yes.getIndex());
564       searchColumns = columnsLocal;
565     }
566     return new ArrayEnumeration(searchColumns);
567   }
568 
569   /**
570    * @return the number of columns which are searchable
571    */ 
572   public final int getSearchCriterionColumnsCount() {
573     if (searchColumns == null)
574       // FIXME Race 
575       getSearchCriterionColumns();
576       
577     return searchColumns.length;
578   }
579 
580   private Dbms dbms() {
581     return getDatabase().getDbms();
582   }
583 
584   // 
585   // =========================
586   //  Low-level DB operations
587   // =========================
588   // 
589 
590   // 
591   // -----------
592   //  Structure
593   // -----------
594   // 
595 
596   /**
597    * Use this for DDL statements, ie those which alter the structure of the db.
598    * Postgresql in particular does not like DDL statements being executed within a transaction.
599    * 
600    * @param sql the SQL DDL statement to execute
601    * @throws StructuralModificationFailedPoemException
602    */
603   public void dbModifyStructure(String sql)
604       throws StructuralModificationFailedPoemException {
605     
606     // We have to do this to avoid blocking
607     if (PoemThread.inSession())
608       PoemThread.commit();
609 
610     try {
611       if (database.logSQL()) database.log("about to execute:" + sql);
612 
613       Statement updateStatement = database.getCommittedConnection().createStatement();
614       updateStatement.executeUpdate(sql);
615       updateStatement.close();
616       database.getCommittedConnection().commit();
617       if (database.logCommits()) database.log(new CommitLogEvent(null));
618       if (database.logSQL()) database.log(new StructuralModificationLogEvent(sql));
619       database.incrementQueryCount(sql);
620     }
621     catch (SQLException e) {
622       throw new StructuralModificationFailedPoemException(sql, e);
623     }
624   }
625 
626   private void dbCreateTable() {
627     String createTableSql = dbms().createTableSql(this);
628     dbModifyStructure(createTableSql);
629     String tableSetup = database.getDbms().tableInitialisationSql(this); 
630     if (tableSetup != null) { 
631       dbModifyStructure(tableSetup);
632     }
633   }
634   
635     
636 
637   /**
638    * @return A type string eg "TEXT"
639    * @see {@link org.melati.poem.dbms.Hsqldb}
640    */
641   public String getDbmsTableType() {
642     return null;
643   }
644 
645   /**
646    * Constraints are not used in POEM, but you might want to use them if 
647    * exporting the db or using schema visualisation tools.
648    */
649   public void dbAddConstraints() {
650     StringBuffer sqb = new StringBuffer();
651     for (int c = 0; c < columns.length; ++c) {
652       if (columns[c].getSQLType() instanceof TroidPoemType){
653         sqb.append("ALTER TABLE " + quotedName());
654         sqb.append(dbms().getPrimaryKeyDefinition(
655             columns[c].getName()));
656         try {
657           dbModifyStructure(sqb.toString());
658         } catch (StructuralModificationFailedPoemException e) {
659           // It is more expensive to only add constaints 
660           // if they are missing than to ignore exceptions.  
661           e = null;
662         }
663       }
664     }
665     for (int c = 0; c < columns.length; ++c) {
666       if (columns[c].getSQLType() instanceof ReferencePoemType){
667         IntegrityFix fix = columns[c].getIntegrityFix();
668         sqb = new StringBuffer();
669         sqb.append("ALTER TABLE " + quotedName());
670         sqb.append(dbms().getForeignKeyDefinition(
671                       getName(),
672                       columns[c].getName(),
673                       ((ReferencePoemType)columns[c].getSQLType()).
674                           targetTable().getName(),
675                       ((ReferencePoemType)columns[c].getSQLType()).
676                           targetTable().troidColumn().getName(),
677                        fix.getName()));
678         try {
679           dbModifyStructure(sqb.toString());
680         } catch (StructuralModificationFailedPoemException e) {
681           // It is more expensive to only add constaints 
682           // if they are missing than to ignore exceptions.  
683           e = null;          
684         }
685       }
686     }
687 
688 
689   }
690 
691   private void dbAddColumn(Column column) {
692     if (column.getType().getNullable()) {
693       dbModifyStructure(
694           "ALTER TABLE " + quotedName() +
695           " ADD " + column.quotedName() +
696           " " + column.getSQLType().sqlDefinition(dbms()));
697     } else {
698       dbModifyStructure(
699           "ALTER TABLE " + quotedName() +
700           " ADD " + column.quotedName() +
701           " " + column.getSQLType().sqlTypeDefinition(dbms()));
702       dbModifyStructure(
703           "UPDATE " + quotedName() +
704           " SET " + column.quotedName() +
705           " = " + dbms().getQuotedValue(column.getSQLType(), 
706                       column.getSQLType().sqlDefaultValue(dbms())));
707       dbModifyStructure(
708           dbms().alterColumnNotNullableSQL(name, column));      
709     }
710   }
711 
712   
713   private void dbCreateIndex(Column column) {
714     if (column.getIndexed()) {
715       if (!dbms().canBeIndexed(column)) {
716         database.log(new UnindexableLogEvent(column));
717       } else {
718         dbModifyStructure(
719             "CREATE " + (column.getUnique() ? "UNIQUE " : "") + "INDEX " +
720             indexName(column) +
721             " ON " + quotedName() + " " +
722             "(" + column.quotedName() + 
723              dbms().getIndexLength(column) + ")");
724       }
725     }
726   }
727 
728   private String indexName(Column column) { 
729     return database.quotedName(
730             dbms().unreservedName(name) + "_" + 
731             dbms().unreservedName(column.getName()) + "_index");
732   }
733   // 
734   // -------------------------------
735   //  Standard `PreparedStatement's
736   // -------------------------------
737   // 
738 
739   /**
740    * 
741    * @param connection the connection the PreparedStatement is tied to
742    * @return a PreparedStatment to perform a simple INSERT
743    */
744   private PreparedStatement simpleInsert(Connection connection) {
745     StringBuffer sql = new StringBuffer();
746     sql.append("INSERT INTO " + quotedName() + " (");
747     for (int c = 0; c < columns.length; ++c) {
748       if (c > 0) sql.append(", ");
749       sql.append(columns[c].quotedName());
750     }
751     sql.append(") VALUES (");
752     for (int c = 0; c < columns.length; ++c) {
753       if (c > 0) sql.append(", ");
754       sql.append("?");
755     }
756 
757     sql.append(")");
758 
759     try {
760       return connection.prepareStatement(sql.toString());
761     }
762     catch (SQLException e) {
763       throw new SimplePrepareFailedPoemException(sql.toString(), e);
764     }
765   }
766 
767   private PreparedStatement simpleGet(Connection connection) {
768     StringBuffer sql = new StringBuffer();
769     sql.append("SELECT ");
770     for (int c = 0; c < columns.length; ++c) {
771       if (c > 0) sql.append(", ");
772       sql.append(columns[c].quotedName());
773     }
774     sql.append(" FROM " + quotedName() +
775                " WHERE " + troidColumn.quotedName() + " = ?");
776 
777     try {
778       return connection.prepareStatement(sql.toString());
779     }
780     catch (SQLException e) {
781       throw new SimplePrepareFailedPoemException(sql.toString(), e);
782     }
783   }
784 
785   private PreparedStatement simpleModify(Connection connection) {
786     // FIXME synchronize this too
787     StringBuffer sql = new StringBuffer();
788     sql.append("UPDATE " + quotedName() + " SET ");
789     for (int c = 0; c < columns.length; ++c) {
790       if (c > 0) sql.append(", ");
791       sql.append(columns[c].quotedName());
792       sql.append(" = ?");
793     }
794     sql.append(" WHERE " + troidColumn.quotedName() + " = ?");
795 
796     try {
797       return connection.prepareStatement(sql.toString());
798     }
799     catch (SQLException e) {
800       throw new SimplePrepareFailedPoemException(sql.toString(), e);
801     }
802   }
803 
804   // 
805   // -----------------------------
806   //  Transaction-specific things
807   // -----------------------------
808   // 
809 
810   private class TransactionStuff {
811     PreparedStatement insert, modify, get;
812     TransactionStuff(Connection connection) {
813       insert = _this.simpleInsert(connection);
814       modify = _this.simpleModify(connection);
815       get = _this.simpleGet(connection);
816     }
817   }
818 
819   private CachedIndexFactory transactionStuffs = new CachedIndexFactory() {
820     public Object reallyGet(int index) {
821       // "Table.this" is attempt to work around Dietmar's problem with JDK1.3.1
822       return new TransactionStuff(
823           JdbcTable.this.database.poemTransaction(index).getConnection());
824     }
825   };
826 
827   private TransactionStuff committedTransactionStuff = null;
828 
829   /**
830    * When deleting a table and used in tests.
831    */
832   public void invalidateTransactionStuffs() { 
833     transactionStuffs.invalidate();
834   }
835   /**
836    * Called when working outside a Transaction.
837    * @return the TransactionStuff for the committed transaction
838    * @see org.melati.poem.PoemDatabase#inCommittedTransaction(AccessToken, PoemTask)
839    */
840   private synchronized TransactionStuff getCommittedTransactionStuff() {
841     if (committedTransactionStuff == null)
842       committedTransactionStuff =
843           new TransactionStuff(database.getCommittedConnection());
844     return committedTransactionStuff;
845   }
846 
847   // 
848   // --------------------
849   //  Loading and saving
850   // --------------------
851   // 
852 
853   private void load(PreparedStatement select, Persistent p) {
854     JdbcPersistent persistent = (JdbcPersistent)p;
855     try {
856       synchronized (select) {
857         select.setInt(1, persistent.troid().intValue());
858         ResultSet rs = select.executeQuery();
859         if (database.logSQL())
860           database.log(new SQLLogEvent(select.toString()));
861         database.incrementQueryCount(select.toString());
862         try {
863           if (!rs.next())
864             persistent.setStatusNonexistent();
865           else {
866             persistent.setStatusExistent();
867             for (int c = 0; c < columns.length; ++c)
868               columns[c].load_unsafe(rs, c + 1, persistent);
869           }
870           persistent.setDirty(false);
871           persistent.markValid();
872           if (rs.next())
873             throw new DuplicateTroidPoemException(this, persistent.troid());
874         }
875         finally {
876           try { rs.close(); } catch (Exception e) {
877             System.err.println("Cannot close resultset after exception.");  
878           }
879         }
880       }
881     }
882     catch (SQLException e) {
883       throw new SimpleRetrievalFailedPoemException(e, select.toString());
884     }
885     catch (ValidationPoemException e) {
886       throw new UnexpectedValidationPoemException(e);
887     }
888   }
889 
890   /**
891    * @param transaction possibly null if working with the committed transaction
892    * @param persistent the Persistent to load
893    */
894   public void load(PoemTransaction transaction, Persistent persistent) {
895     load(transaction == null ?
896             getCommittedTransactionStuff().get :
897             ((TransactionStuff)transactionStuffs.get(transaction.index)).get,
898          persistent);
899   }
900 
901   private void modify(PoemTransaction transaction, Persistent persistent) {
902     PreparedStatement modify =
903         ((TransactionStuff)transactionStuffs.get(transaction.index)).modify;
904     synchronized (modify) {
905       for (int c = 0; c < columns.length; ++c)
906         columns[c].save_unsafe(persistent, modify, c + 1);
907 
908       try {
909         modify.setInt(columns.length + 1, persistent.troid().intValue());
910       }
911       catch (SQLException e) {
912         throw new SQLSeriousPoemException(e);
913       }
914 
915       try {
916         modify.executeUpdate();
917       }
918       catch (SQLException e) {
919         throw dbms().exceptionForUpdate(this, modify, false, e);
920       }
921       database.incrementQueryCount(modify.toString());
922 
923       if (database.logSQL())
924         database.log(new SQLLogEvent(modify.toString()));
925     }
926     persistent.postModify();
927   }
928 
929   private void insert(PoemTransaction transaction, Persistent persistent) {
930     
931     PreparedStatement insert =
932         ((TransactionStuff)transactionStuffs.get(transaction.index)).insert;
933     synchronized (insert) {
934       for (int c = 0; c < columns.length; ++c)
935         columns[c].save_unsafe(persistent, insert, c + 1);
936       try {
937         insert.executeUpdate();
938       }
939       catch (SQLException e) {
940         throw dbms().exceptionForUpdate(this, insert, true, e);
941       }
942       database.incrementQueryCount(insert.toString());
943       if (database.logSQL())
944         database.log(new SQLLogEvent(insert.toString()));
945     }
946     persistent.postInsert();
947   }
948 
949   /**
950    * The Transaction cannot be null, as this is trapped in 
951    * #deleteLock(SessionToken).
952    * @param troid id of row to delete
953    * @param transaction a non-null transaction
954    */
955   public void delete(Integer troid, PoemTransaction transaction) {
956     String sql =
957         "DELETE FROM " + quotedName() +
958         " WHERE " + troidColumn.quotedName() + " = " +
959         troid.toString();
960     try {
961       transaction.writeDown();
962       Connection connection = transaction.getConnection();
963 
964       Statement deleteStatement = connection.createStatement();
965       int deleted = deleteStatement.executeUpdate(sql);
966       if (deleted != 1) { 
967         throw new RowDisappearedPoemException(this,troid);
968       }
969       deleteStatement.close();
970       database.incrementQueryCount(sql);
971       if (database.logSQL())
972         database.log(new SQLLogEvent(sql));
973 
974       cache.delete(troid);
975     }
976     catch (SQLException e) {
977       throw new ExecutingSQLPoemException(sql, e);
978     }
979   }
980 
981   /**
982    * @param transaction our PoemTransaction 
983    * @param p the Persistent to write
984    */
985   public void writeDown(PoemTransaction transaction, Persistent p) {
986     JdbcPersistent persistent = (JdbcPersistent)p;
987     // NOTE No race, provided that the one-thread-per-transaction parity is
988     // maintained
989 
990     if (persistent.isDirty()) {
991       troidColumn.setRaw_unsafe(persistent, persistent.troid());
992 
993       if (persistent.statusExistent()) {
994         modify(transaction, persistent);
995       } else if (persistent.statusNonexistent()) {
996         insert(transaction, persistent);
997         persistent.setStatusExistent();
998       }
999 
1000       persistent.setDirty(false);
1001       persistent.postWrite();
1002     }
1003   }
1004 
1005   // 
1006   // ============
1007   //  Operations
1008   // ============
1009   // 
1010 
1011   // 
1012   // ----------
1013   //  Cacheing
1014   // ----------
1015   // 
1016 
1017   private Cache cache = new Cache(CACHE_LIMIT_DEFAULT);
1018 
1019   private static final Procedure invalidator =
1020       new Procedure() {
1021         public void apply(Object arg) {
1022           ((Transactioned)arg).invalidate();
1023         }
1024       };
1025 
1026   /**
1027    * Invalidate table cache.
1028    * 
1029    * NOTE Invalidated cache elements are reloaded when next read
1030    */
1031   public void uncache() {
1032     cache.iterate(invalidator);
1033     serial.invalidate();
1034     TableListener[] listenersLocal = this.listeners;
1035     for (int l = 0; l < listenersLocal.length; ++l)
1036       listenersLocal[l].notifyUncached(this);
1037   }
1038 
1039   /**
1040    * @param maxSize new maximum size
1041    */
1042   public void trimCache(int maxSize) {
1043     cache.trim(maxSize);
1044   }
1045 
1046   /**
1047    * @return the Cache Info object
1048    */ 
1049   public Cache.Info getCacheInfo() {
1050     return cache.getInfo();
1051   }
1052 
1053   /**
1054    * Add a {@link TableListener} to this Table.
1055    */ 
1056   public void addListener(TableListener listener) {
1057     listeners = (TableListener[])ArrayUtils.added(listeners, listener);
1058   }
1059 
1060   /**
1061    * Notify the table that one if its records is about to be changed in a
1062    * transaction.  You can (with care) use this to support cacheing of
1063    * frequently-used facts about the table's records.  
1064    *
1065    * @param transaction the transaction in which the change will be made
1066    * @param persistent  the record to be changed
1067    */
1068   public void notifyTouched(PoemTransaction transaction, Persistent persistent) {
1069     serial.increment(transaction);
1070 
1071     TableListener[] listenersLocal = this.listeners;
1072     for (int l = 0; l < listenersLocal.length; ++l)
1073       listenersLocal[l].notifyTouched(transaction, this, persistent);
1074   }
1075 
1076   /**
1077    * @return the Transaction serial 
1078    */ 
1079   public long serial(PoemTransaction transaction) {
1080     return serial.current(transaction);
1081   }
1082 
1083   /**
1084    * Lock this record.
1085    */ 
1086   public void readLock() {
1087     serial(PoemThread.transaction());
1088   }
1089 
1090   // 
1091   // ----------
1092   //  Fetching
1093   // ----------
1094   // 
1095 
1096   /**
1097    * The object from the table with a given troid.
1098    *
1099    * @param troid       Every record (object) in a POEM database must have a
1100    *                    troid (table row ID, or table-unique non-nullable
1101    *                    integer primary key), often but not necessarily called
1102    *                    <TT>id</TT>, so that it can be conveniently `named' for
1103    *                    retrieval by this method.
1104    *
1105    * @return A <TT>Persistent</TT> of the record with the given troid;
1106    *         or, if the table was defined in the DSD under the name
1107    *         <TT><I>foo</I></TT>, an application-specialised subclass
1108    *         <TT><I>Foo</I></TT> of <TT>Persistent</TT>.  In that case, there
1109    *         will also be an application-specialised <TT>Table</TT> subclass,
1110    *         called <TT><I>Foo</I>Table</TT> (and available as
1111    *         <TT>get<I>Foo</I>Table</TT> from the application-specialised
1112    *         <TT>Database</TT> subclass), which has a matching method
1113    *         <TT>get<I>Foo</I>Object</TT> for obtaining the specialised object
1114    *         under its own type.  Note that no access checks are done at this
1115    *         stage: you may not be able to do anything with the object handle
1116    *         returned from this method without provoking a
1117    *         <TT>PoemAccessException</TT>.
1118    *
1119    * @exception NoSuchRowPoemException
1120    *                if there is no row in the table with the given troid
1121    *
1122    * @see Persistent#getTroid()
1123    */
1124   public Persistent getObject(Integer troid) throws NoSuchRowPoemException {
1125     JdbcPersistent persistent = (JdbcPersistent)cache.get(troid);
1126 
1127     if (persistent == null) {
1128       persistent = (JdbcPersistent)newPersistent();
1129       claim(persistent, troid);
1130       load(PoemThread.transaction(), persistent);
1131       if (persistent.statusExistent())
1132         synchronized (cache) {
1133           JdbcPersistent tryAgain = (JdbcPersistent)cache.get(troid);
1134           if (tryAgain == null)
1135             cache.put(troid, persistent);
1136           else
1137             persistent = tryAgain;
1138         }
1139     }
1140 
1141     if (!persistent.statusExistent())
1142       throw new NoSuchRowPoemException(this, troid);
1143 
1144     persistent.existenceLock(PoemThread.sessionToken());
1145 
1146     return persistent;
1147   }
1148 
1149   /**
1150    * The object from the table with a given troid.  See previous.
1151    *
1152    * @param troid the table row id
1153    * @return the Persistent
1154    * @throws NoSuchRowPoemException if not found
1155    * @see #getObject(java.lang.Integer)
1156    */
1157   public Persistent getObject(int troid) throws NoSuchRowPoemException {
1158     return getObject(new Integer(troid));
1159   }
1160 
1161   // 
1162   // -----------
1163   //  Searching
1164   // -----------
1165   // 
1166 
1167   /**
1168    * The from clause has been added as an argument because it is
1169    * inextricably linked to the where clause, but the default is 
1170    * {@link #quotedName()}.
1171    *
1172    * It is the programmer's responsibility to ensure that the where clause 
1173    * is suitable for the target DBMS.
1174    * 
1175    * @param fromClause Comma separated list of table names or null for default.
1176    * @param whereClause SQL fragment
1177    * @param orderByClause Comma separated list
1178    * @param includeDeleted Flag as to whether to include soft deleted records
1179    * @param excludeUnselectable Whether to append unselectable exclusion SQL 
1180    * @todo Should work within some kind of limit
1181    * @return an SQL SELECT statement put together from the arguments and
1182    * default order by clause.
1183    */
1184   public String selectionSQL(String fromClause, String whereClause, 
1185                              String orderByClause, boolean includeDeleted, 
1186                              boolean excludeUnselectable) {
1187     return selectOrCountSQL(troidColumn().fullQuotedName(),
1188                             fromClause, whereClause, orderByClause,
1189                             includeDeleted, excludeUnselectable);
1190   }
1191 
1192   /**
1193    * It is the programmer's responsibility to ensure that the where clause 
1194    * is suitable for the target DBMS.
1195    * 
1196    * @param fromClause SQL fragment
1197    * @param whereClause SQL fragment
1198    * @param orderByClause comma separated list
1199    * @param includeDeleted flag as to whether to include soft deleted records
1200    * @param excludeUnselectable whether to append unselectable exclusion SQL 
1201    * @param transaction null now defaults to 
1202    *                    {@link PoemThread#transaction()} but
1203    *                    we do not rely on this much yet.
1204    * @return a ResultSet                     
1205    * @throws SQLPoemException if necessary
1206    */
1207   private ResultSet selectionResultSet(String fromClause, String whereClause,
1208                                        String orderByClause, 
1209                                        boolean includeDeleted, 
1210                                        boolean excludeUnselectable,
1211                                        PoemTransaction transaction)
1212       throws SQLPoemException {
1213 
1214     String sql = selectionSQL(fromClause, whereClause, orderByClause,
1215                               includeDeleted, excludeUnselectable);
1216 
1217 
1218     try {
1219       Connection connection;
1220       if (transaction == null) {
1221         connection = getDatabase().getCommittedConnection();
1222       } else {
1223         transaction.writeDown();
1224         connection = transaction.getConnection();
1225       }
1226 
1227       Statement selectionStatement = connection.createStatement();
1228       ResultSet rs = selectionStatement.executeQuery(sql);
1229       database.incrementQueryCount(sql);
1230 
1231       SessionToken token = PoemThread._sessionToken();
1232       if (token != null) {
1233         token.toTidy().add(rs);
1234         token.toTidy().add(selectionStatement);
1235       }
1236       if (database.logSQL())
1237         database.log(new SQLLogEvent(sql));
1238       return rs;
1239     }
1240     catch (SQLException e) {
1241       throw new ExecutingSQLPoemException(sql, e);
1242     }
1243   }
1244 
1245   /**
1246    * It is the programmer's responsibility to ensure that the where clause 
1247    * is suitable for the target DBMS.
1248    * 
1249    * @return an {@link Enumeration} of Troids satisfying the criteria.
1250    */ 
1251   public Enumeration troidSelection(String whereClause, String orderByClause,
1252                                     boolean includeDeleted, 
1253                                     PoemTransaction transaction) {
1254     return troidsFrom(selectionResultSet(null, whereClause, orderByClause,
1255                                          includeDeleted, true,
1256                                          transaction));
1257   }
1258 
1259   /**
1260    *
1261    * @see #troidSelection(String, String, boolean, PoemTransaction)
1262    * @param criteria Represents selection criteria possibly on joined tables
1263    * @param transaction A transaction or null for 
1264    *                    {@link PoemThread#transaction()}
1265    * @return a selection of troids given arguments specifying a query
1266    */
1267   public Enumeration troidSelection(Persistent criteria, String orderByClause,
1268                                     boolean includeDeleted, 
1269                                     boolean excludeUnselectable,
1270                                     PoemTransaction transaction) {
1271     return troidsFrom(selectionResultSet(((JdbcPersistent)criteria).fromClause(), 
1272                                          whereClause(