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