JdbcPersistent.java

/*
 * $Source$
 * $Revision$
 *
 * Copyright (C) 2007 Tim Pizey
 *
 * Part of Melati (http://melati.org), a framework for the rapid
 * development of clean, maintainable web applications.
 *
 * Melati is free software; Permission is granted to copy, distribute
 * and/or modify this software under the terms either:
 *
 * a) the GNU General Public License as published by the Free Software
 *    Foundation; either version 2 of the License, or (at your option)
 *    any later version,
 *
 *    or
 *
 * b) any version of the Melati Software License, as published
 *    at http://melati.org
 *
 * You should have received a copy of the GNU General Public License and
 * the Melati Software License along with this program;
 * if not, write to the Free Software Foundation, Inc.,
 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA to obtain the
 * GNU General Public License and visit http://melati.org to obtain the
 * Melati Software License.
 *
 * Feel free to contact the Developers of Melati (http://melati.org),
 * if you would like to work out a different arrangement than the options
 * outlined here.  It is our intention to allow Melati to be used by as
 * wide an audience as possible.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * Contact details for copyright holder:
 *
 *     Tim Pizey <timp At paneris.org>
 *     http://paneris.org/~timp
 */

package org.melati.poem;

import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.text.DateFormat;
import java.util.Enumeration;
import java.util.Map;
import java.util.Vector;

import org.melati.poem.transaction.Transaction;
import org.melati.poem.transaction.Transactioned;
import org.melati.poem.util.FlattenedEnumeration;
import org.melati.poem.util.MappedEnumeration;

/**
 * The object representing a single table row; this is the <B>PO</B> in POEM!
 * <p>
 * Instances are also used to represent selection criteria.
 *
 * @author WilliamC At paneris.org
 */

public class JdbcPersistent extends Transactioned implements Persistent, Cloneable {
  private Table<?> table;
  private Integer troid;        // null if a floating object
  private AccessToken clearedToken;
  private boolean
      knownCanRead = false, knownCanWrite = false, knownCanDelete = false;

  /**
   * Might this object have as yet unsaved modifications?
   * <p>
   * This is set to true when a write lock is obtained and this
   * happens when a value is assigned to a column, except when an
   * "unsafe" method is used.
   * <p>
   * It is set to false when this is written to the database,
   * even if not yet committed.
   */
  private boolean dirty = false;

  private static final int NONEXISTENT = 0, EXISTENT = 1, DELETED = 2;
  private int status = NONEXISTENT;

  private Object[] extras = null;
  /**
   * Constructor.
   */
  public JdbcPersistent() {
  }

  /**
   * Constructor.
   * @param table the table of the Persistent
   * @param troid its Table Row Object Id
   */
  public JdbcPersistent(JdbcTable<?> table, Integer troid) {
    super(table.getDatabase());
    this.table = table;
    this.troid = troid;
  }

  /**
   * Constructor.
   * @param tableName String name of a table
   * @param troidString String integer representation
   */
  public JdbcPersistent(String tableName, String troidString) {
    super(PoemThread.database());
    this.table = PoemThread.database().getTable(tableName);
    this.troid = new Integer(troidString);
   }

  // 
  // --------
  //  Status
  // --------
  // 

  final void setStatusNonexistent() {
    status = NONEXISTENT;
  }

  final void setStatusExistent() {
    status = EXISTENT;
  }

  /** 
   * {@inheritDoc}
   * @see org.melati.poem.Persistent#statusNonexistent()
   */
  public final boolean statusNonexistent() {
    return status == NONEXISTENT;
  }

  /** 
   * {@inheritDoc}
   * @see org.melati.poem.Persistent#statusExistent()
   */
  public final boolean statusExistent() {
    return status == EXISTENT;
  }

  // 
  // ***************
  //  Transactioned
  // ***************
  // 

  /**
   * Throws an exception if this Persistent has a null troid.
   */
  private void assertNotFloating() {
    if (troid == null)
      throw new InvalidOperationOnFloatingPersistentPoemException(this);
  }

  /**
   * Throws <tt>RowDisappearedPoemException</tt> if this Persistent has a status of DELETED.
   */
  private void assertNotDeleted() {
    if (status == DELETED)
      throw new RowDisappearedPoemException(this);
  }

  /**
   * Called if not uptodate.
   * 
   * {@inheritDoc}
   * @see org.melati.poem.transaction.Transactioned#load(org.melati.poem.transaction.Transaction)
   */
  protected void load(Transaction transaction) {
    if (troid == null) // I cannot contrive a test to cover this case, but hey
      throw new InvalidOperationOnFloatingPersistentPoemException(this);

    table.load((PoemTransaction)transaction, this);
    // table will clear our dirty flag and set status
  }

  /**
   * Whether we are up to date with respect to current Transaction.
   * <p>
   * Return the inherited validity flag.
   * 
   * {@inheritDoc}
   * @see org.melati.poem.transaction.Transactioned#upToDate(org.melati.poem.transaction.Transaction)
   */
  protected boolean upToDate(Transaction transaction) {
    return valid;
  }

  /**
   * Write the persistent to the database if this might be necessary.
   * <p>
   * It may be necessary if field values have been set since we last
   * did a write i.e. this persistent is dirty.
   * It will not be necessary if this persistent is deleted.
   * An exception will occur if it does not exist in the database.
   */
  protected void writeDown(Transaction transaction) {
    if (status != DELETED) {
      assertNotFloating();
      table.writeDown((PoemTransaction)transaction, this);
      // table will clear our dirty flag
    }
  }

  /** 
   * {@inheritDoc}
   * @see org.melati.poem.transaction.Transactioned#writeLock(org.melati.poem.transaction.Transaction)
   */
  protected void writeLock(Transaction transaction) {
    if (troid != null) {
      super.writeLock(transaction);
      assertNotDeleted();
      dirty = true;
      table.notifyTouched((PoemTransaction)transaction, this);
    }
  }

  /**
   * This is just to make this method available to <TT>Table</TT>.
   */
  protected void readLock(Transaction transaction) {
    if (troid != null) {
      super.readLock(transaction);
      assertNotDeleted();
    }
  }

  /**
   * Previously deletion was treated as non-rollbackable, 
   * as deleteAndCommit was the only deletion mechanism. 
   * 
   * {@inheritDoc}
   * @see org.melati.poem.transaction.Transactioned#commit(org.melati.poem.transaction.Transaction)
   */
  protected void commit(Transaction transaction) {
    //if (status != DELETED) {
      assertNotFloating();
      super.commit(transaction);
    //}
  }

  protected void rollback(Transaction transaction) {
    //if (status != DELETED) {
    assertNotFloating();
    if (status == DELETED)
      status = EXISTENT;
    super.rollback(transaction);
    //}
  }

  // 
  // ************
  //  Persistent
  // ************
  // 

 /** 
 * {@inheritDoc}
 * @see org.melati.poem.Persistent#makePersistent()
 */
  public void makePersistent() {
    getTable().create(this);
  }
  
  /* New extra columns could have been added since we were created */
  synchronized Object[] extras() {
    if (extras == null)
      extras = new Object[table.extrasCount()];
    else if (extras.length < table.extrasCount() ) {
      Object[] newExtras = new Object[table.extrasCount()];
      System.arraycopy(extras, 0, newExtras, 0, extras.length);
      extras = newExtras;
    }
    return extras;
  }

 /** 
 * {@inheritDoc}
 * @see org.melati.poem.Persistent#getTable()
 */
  public final Table<?> getTable() {
    return table;
  }

  synchronized void setTable(JdbcTable<?> table, Integer troid) {
    setTransactionPool(table.getDatabase());
    this.table = table;
    this.troid = troid;
  }


 /** 
 * {@inheritDoc}
 * @see org.melati.poem.Persistent#getDatabase()
 */
  public final Database getDatabase() {
    return table.getDatabase();
  }

  /**
   * @return The Table Row Object Id for this Persistent.
   * 
   * FIXME This shouldn't be public because we don't in principle want people
   * to know even the troid of an object they aren't allowed to read.  However,
   * I think this information may leak out elsewhere.
   * To fix is not simple, as generated setters rely upon a lock-free read of the object to set. 
   * 
   * {@inheritDoc}
   * 
   * @see org.melati.poem.Persistable#troid()
   */
  public final Integer troid() {
    return troid;
  }

  /**
   * The object's troid.
   *
   * @return Every record (object) in a POEM database must have a
   *         troid (table row ID, or table-unique non-nullable integer primary
   *         key), often but not necessarily called <TT>id</TT>, so that it can
   *         be conveniently `named' for retrieval.
   *
   * @exception AccessPoemException
   *                if <TT>assertCanRead</TT> fails
   *
   * @see Table#getObject(java.lang.Integer)
   * @see #assertCanRead()
   */

  public final Integer getTroid() throws AccessPoemException {
    assertCanRead();
    return troid();
  }

  // 
  // ----------------
  //  Access control
  // ----------------
  // 

  protected void existenceLock(SessionToken sessionToken) {
    super.readLock(sessionToken.transaction);
  }

  protected void readLock(SessionToken sessionToken)
      throws AccessPoemException {
    assertCanRead(sessionToken.accessToken);
    readLock(sessionToken.transaction);
  }

  protected void writeLock(SessionToken sessionToken)
      throws AccessPoemException {
    if (troid != null)
      assertCanWrite(sessionToken.accessToken);
    writeLock(sessionToken.transaction);
  }

  protected void deleteLock(SessionToken sessionToken)
      throws AccessPoemException {
    if (troid != null)
      assertCanDelete(sessionToken.accessToken);
    writeLock(sessionToken.transaction);
  }

  /** 
   * {@inheritDoc}
   * @see org.melati.poem.Persistent#existenceLock()
   */
  public void existenceLock() {
    existenceLock(PoemThread.sessionToken());
  }

  /**
   * Check if we may read this object and then lock it.
   * @throws AccessPoemException if current AccessToken does not give read Capability
   */
  protected void readLock() throws AccessPoemException {
    readLock(PoemThread.sessionToken());
  }

  /**
   * Check if we may write to this object and then lock it.
   * @throws AccessPoemException if current AccessToken does not give write Capability
   */
  protected void writeLock() throws AccessPoemException {
    writeLock(PoemThread.sessionToken());
  }

  /**
   * The capability required for reading the object's field values.  This is
   * used by <TT>assertCanRead</TT> (unless that's been overridden) to obtain a
   * <TT>Capability</TT> for comparison against the caller's
   * <TT>AccessToken</TT>.
   * <p>
   * NOTE If a canRead column is defined then it will override this method.
   *
   * @return the capability specified by the record's <TT>canread</TT> field, 
   *         or <TT>null</TT> if it doesn't have one or its value is SQL
   *         <TT>NULL</TT>
   *
   * @see #assertCanRead
   */

  protected Capability getCanRead() {
    return null;
  }

  /** 
   * {@inheritDoc}
   * @see org.melati.poem.Persistent#assertCanRead(org.melati.poem.AccessToken)
   */

  public void assertCanRead(AccessToken token)
      throws AccessPoemException {
    // FIXME!!!! this is wrong because token could be stale ...
    if (!(clearedToken == token && knownCanRead) && troid != null) {
      Capability canRead = getCanRead();
      if (canRead == null)
        canRead = getTable().getDefaultCanRead();
      if (canRead != null) {
        if (!token.givesCapability(canRead))
          throw new ReadPersistentAccessPoemException(this, token, canRead);
        if (clearedToken != token) {
          knownCanWrite = false;
          knownCanDelete = false;
        }
        clearedToken = token;
        knownCanRead = true;
      }
    }
  }

  /** 
   * {@inheritDoc}
   * @see org.melati.poem.Persistent#assertCanRead()
   */
  public final void assertCanRead() throws AccessPoemException {
    assertCanRead(PoemThread.accessToken());
  }

  /** 
   * {@inheritDoc}
   * @see org.melati.poem.Persistent#getReadable()
   */

  public final boolean getReadable() {
    try {
      assertCanRead();
      return true;
    }
    catch (AccessPoemException e) {
      return false;
    }
  }

  /**
   * The capability required for writing the object's field values.  This is
   * used by <TT>assertCanWrite</TT> (unless that's been overridden) to obtain 
   * a <TT>Capability</TT> for comparison against the caller's
   * <TT>AccessToken</TT>.
   * <p>
   * NOTE If a canWrite column is defined then it will override this method.
   *
   * @return the capability specified by the record's <TT>canwrite</TT> field,
   *         or <TT>null</TT> if it doesn't have one or its value is SQL
   *         <TT>NULL</TT>
   *
   * @see #assertCanWrite
   */
  protected Capability getCanWrite() {
    return null;
  }

  /** 
   * {@inheritDoc}
   * @see org.melati.poem.Persistent#assertCanWrite(org.melati.poem.AccessToken)
   */

  public void assertCanWrite(AccessToken token)
      throws AccessPoemException {
    // FIXME!!!! this is wrong because token could be stale ...
    if (!(clearedToken == token && knownCanWrite) && troid != null) {
      Capability canWrite = getCanWrite();
      if (canWrite == null)
        canWrite = getTable().getDefaultCanWrite();
      if (canWrite != null) {
        if (!token.givesCapability(canWrite))
          throw new WritePersistentAccessPoemException(this, token, canWrite);
        if (clearedToken != token) {
          knownCanRead = false;
          knownCanDelete = false;
        }
        clearedToken = token;
        knownCanWrite = true;
      }
    }
  }

  /** 
   * {@inheritDoc}
   * @see org.melati.poem.Persistent#assertCanWrite()
   */
  public final void assertCanWrite() throws AccessPoemException {
    assertCanWrite(PoemThread.accessToken());
  }

  /**
   * The capability required for deleting the object.  This is
   * used by <TT>assertCanDelete</TT> (unless that's been overridden) 
   * to obtain a <TT>Capability</TT> for comparison against the caller's
   * <TT>AccessToken</TT>.
   * <p>
   * NOTE If a canDelete column is defined then it will override this method.
   *
   * @return the capability specified by the record's <TT>candelete</TT> field,
   *         or <TT>null</TT> if it doesn't have one or its value is SQL
   *         <TT>NULL</TT>
   *
   * @see #assertCanDelete
   */
  protected Capability getCanDelete() {
    return null;
  }

  /** 
   * {@inheritDoc}
   * @see org.melati.poem.Persistent#assertCanDelete(org.melati.poem.AccessToken)
   */

  public void assertCanDelete(AccessToken token)
      throws AccessPoemException {
    // FIXME!!!! this is wrong because token could be stale ...
    if (!(clearedToken == token && knownCanDelete) && troid != null) {
      Capability canDelete = getCanDelete();
      if (canDelete == null)
        canDelete = getTable().getDefaultCanDelete();
      if (canDelete != null) {
        if (!token.givesCapability(canDelete))
          throw new DeletePersistentAccessPoemException(this, token, canDelete);
        if (clearedToken != token) {
          knownCanRead = false;
          knownCanWrite = false;
        }
        clearedToken = token;
        knownCanDelete = true;
      }
    }
  }

  /** 
   * {@inheritDoc}
   * @see org.melati.poem.Persistent#assertCanDelete()
   */
  public final void assertCanDelete() throws AccessPoemException {
    assertCanDelete(PoemThread.accessToken());
  }

  /**
   * The capability required to select the object.
   * <p>
   * Any persistent which has a <tt>canSelect</tt> field will override this method. 
   * <p>
   * There is no <code>assertCanSelect()</code> yet because I don't understand
   * this stale token stuff!
   *
   * @return the capability the user needs to select this record
   * TODO document use-case or delete
   */
  protected Capability getCanSelect() {
    return null;
  }
  
  /** 
   * {@inheritDoc}
   * @see org.melati.poem.Persistent#assertCanCreate(org.melati.poem.AccessToken)
   */

  public void assertCanCreate(AccessToken token) {
    Capability canCreate = getTable().getCanCreate();
    if (canCreate != null && !token.givesCapability(canCreate))
      throw new CreationAccessPoemException(getTable(), token, canCreate);
  }

  /** 
   * {@inheritDoc}
   * @see org.melati.poem.Persistent#assertCanCreate()
   */
  public final void assertCanCreate() throws AccessPoemException {
    assertCanCreate(PoemThread.accessToken());
  }


  // 
  // ============================
  //  Reading and writing fields
  // ============================
  // 

  // 
  // ------
  //  Raws
  // ------
  // 

  /** 
   * {@inheritDoc}
   * @see org.melati.poem.Persistent#getRaw(java.lang.String)
   */
  public Object getRaw(String name)
      throws NoSuchColumnPoemException, AccessPoemException {
    return getTable().getColumn(name).getRaw(this);
  }

  /** 
   * {@inheritDoc}
   * @see org.melati.poem.Persistent#getRawString(java.lang.String)
   */

  public final String getRawString(String name)
      throws AccessPoemException, NoSuchColumnPoemException {
    Column<?> column = getTable().getColumn(name);
    return column.getType().stringOfRaw(column.getRaw(this));
  }

  /** 
   * {@inheritDoc}
   * @see org.melati.poem.Persistent#setRaw(java.lang.String, java.lang.Object)
   */

  public void setRaw(String name, Object raw)
      throws NoSuchColumnPoemException, AccessPoemException,
             ValidationPoemException {
    getTable().getColumn(name).setRaw(this, raw);
  }

  /** 
   * {@inheritDoc}
   * @see org.melati.poem.Persistent#setRawString(java.lang.String, java.lang.String)
   */

  public final void setRawString(String name, String string)
      throws NoSuchColumnPoemException, AccessPoemException,
             ParsingPoemException, ValidationPoemException {
    Column<?> column = getTable().getColumn(name);
    column.setRaw(this, column.getType().rawOfString(string));
  }

  // 
  // --------
  //  Values
  // --------
  // 

  /** 
   * {@inheritDoc}
   * @see org.melati.poem.Persistent#getCooked(java.lang.String)
   */

  public Object getCooked(String name)
      throws NoSuchColumnPoemException, AccessPoemException {
    return getTable().getColumn(name).getCooked(this);
  }

  /** 
   * {@inheritDoc}
   * @see org.melati.poem.Persistent#getCookedString(java.lang.String, org.melati.poem.PoemLocale, int)
   */

  public final String getCookedString(String name, PoemLocale locale,
                                     int style)
      throws NoSuchColumnPoemException, AccessPoemException {
    Column<?> column = getTable().getColumn(name);
    return column.getType().stringOfCooked(column.getCooked(this),
                                          locale, style);
  }

  /** 
   * {@inheritDoc}
   * @see org.melati.poem.Persistent#setCooked(java.lang.String, java.lang.Object)
   */

  public void setCooked(String name, Object cooked)
      throws NoSuchColumnPoemException, ValidationPoemException,
             AccessPoemException {
    getTable().getColumn(name).setCooked(this, cooked);
  }

  // 
  // --------
  //  Fields
  // --------
  // 

  /** 
   * {@inheritDoc}
   * @see org.melati.poem.Persistent#getField(java.lang.String)
   */
  public final Field<?> getField(String name)
      throws NoSuchColumnPoemException, AccessPoemException {
    return getTable().getColumn(name).asField(this);
  }

  /** 
   * {@inheritDoc}
   * @see org.melati.poem.Persistent#fieldsOfColumns(java.util.Enumeration)
   */
  public Enumeration<Field<?>> fieldsOfColumns(Enumeration<Column<?>> columns) {
    final JdbcPersistent _this = this;
    return
        new MappedEnumeration<Field<?>, Column<?>>(columns) {
          public Field<?> mapped(Column<?> column) {
            return column.asField(_this);
          }
        };
  }

  /** 
   * {@inheritDoc}
   * @see org.melati.poem.Persistent#getFields()
   */

  public Enumeration<Field<?>> getFields() {
    return fieldsOfColumns(getTable().columns());
  }

  /** 
   * {@inheritDoc}
   * @see org.melati.poem.Persistent#getRecordDisplayFields()
   */

  public Enumeration<Field<?>> getRecordDisplayFields() {
    return fieldsOfColumns(getTable().getRecordDisplayColumns());
  }

  /** 
   * {@inheritDoc}
   * @see org.melati.poem.Persistent#getDetailDisplayFields()
   */
  public Enumeration<Field<?>> getDetailDisplayFields() {
    return fieldsOfColumns(getTable().getDetailDisplayColumns());
  }

  /** 
   * {@inheritDoc}
   * @see org.melati.poem.Persistent#getSummaryDisplayFields()
   */
  public Enumeration<Field<?>> getSummaryDisplayFields() {
    return fieldsOfColumns(getTable().getSummaryDisplayColumns());
  }

  /** 
   * {@inheritDoc}
   * @see org.melati.poem.Persistent#getSearchCriterionFields()
   */
  public Enumeration<Field<?>> getSearchCriterionFields() {
    return fieldsOfColumns(getTable().getSearchCriterionColumns());
  }

  /** 
   * {@inheritDoc}
   * @see org.melati.poem.Persistent#getPrimaryDisplayField()
   */
  public Field<?> getPrimaryDisplayField() {
    return getTable().displayColumn().asField(this);
  }

  // 
  // ==================
  //  Other operations
  // ==================
  // 

  /** 
   * {@inheritDoc}
   * @see org.melati.poem.Persistent#delete(java.util.Map)
   */
  public void delete(Map<Column<?>, IntegrityFix> columnToIntegrityFix) {
    
    assertNotFloating();

    deleteLock(PoemThread.sessionToken());

    Enumeration<Column<?>> columns = getDatabase().referencesTo(getTable());
    Vector<Enumeration<Persistent>> refEnumerations = new Vector<Enumeration<Persistent>>();

    while (columns.hasMoreElements()) {
      Column<?> column = columns.nextElement();

      IntegrityFix fix;
      try {
        fix = columnToIntegrityFix == null ?
                null : columnToIntegrityFix.get(column);
      }
      catch (ClassCastException e) {
        throw new AppBugPoemException(
            "columnToIntegrityFix argument to Persistent.deleteAndCommit " +
                "is meant to be a Map from Column to IntegrityFix",
            e);
      }

      if (fix == null)
        fix = column.getIntegrityFix();

      if (column.getType() instanceof ReferencePoemType)
        refEnumerations.addElement(
          fix.referencesTo(this, column, column.selectionWhereEq(troid()),
                           columnToIntegrityFix));
      else if (column.getType() instanceof StringKeyReferencePoemType) {
        getDatabase().log("Found a StringKeyReferencePoemType " + getName());
        String keyName = ((StringKeyReferencePoemType)column.getType()).targetKeyName();
        String keyValue = (String)getRaw(keyName);
        if (keyValue != null)
          refEnumerations.addElement(
              fix.referencesTo(this, column, column.selectionWhereEq(keyValue),
                               columnToIntegrityFix));

      } else 
        throw new UnexpectedExceptionPoemException(
            "Only expecting Reference or StringKeyReferences");
    }

    Enumeration<Persistent> refs = new FlattenedEnumeration<Persistent>(refEnumerations.elements());

    if (refs.hasMoreElements())
      throw new DeletionIntegrityPoemException(this, refs);

    delete_unsafe();
  }

  /** 
   * {@inheritDoc}
   * @see org.melati.poem.Persistent#delete_unsafe()
   */
  public void delete_unsafe() {
    assertNotFloating();
    SessionToken sessionToken = PoemThread.sessionToken();
    deleteLock(sessionToken);
    try {
      status = DELETED;
      table.delete(troid(), sessionToken.transaction);
    } catch (PoemException e) {
      status = EXISTENT;
      throw e;
    }
  }

 
  /** 
   * {@inheritDoc}
   * @see org.melati.poem.Persistent#delete()
   */
  public final void delete() {
    delete(null);
  }

  /** 
   * {@inheritDoc}
   * @see org.melati.poem.Persistent#deleteAndCommit(java.util.Map)
   */
  public void deleteAndCommit(Map<Column<?>, IntegrityFix> integrityFixOfColumn)
      throws AccessPoemException, DeletionIntegrityPoemException {

    getDatabase().beginExclusiveLock();
    try {
      delete(integrityFixOfColumn);
      PoemThread.commit();
    }
    catch (RuntimeException e) {
      PoemThread.rollback();
      throw e;
    }
    finally {
      getDatabase().endExclusiveLock();
    }
  }

  /** 
   * {@inheritDoc}
   * @see org.melati.poem.Persistent#deleteAndCommit()
   */
  public final void deleteAndCommit()
      throws AccessPoemException, DeletionIntegrityPoemException {
    deleteAndCommit(null);
  }

  /** 
   * {@inheritDoc}
   * @see org.melati.poem.Persistent#duplicated()
   */
  public Persistent duplicated() throws AccessPoemException {
    assertNotFloating();
    assertNotDeleted();
    return (JdbcPersistent)clone();
  }

  /** 
   * {@inheritDoc}
   * @see org.melati.poem.Persistent#duplicatedFloating()
   */
  public Persistent duplicatedFloating() throws AccessPoemException {
    return (JdbcPersistent)clone();
  }

  // 
  // ===========
  //  Utilities
  // ===========
  // 

  /**
   * A string briefly describing the object for diagnostic purposes.  The name
   * of its table and its troid.
   * {@inheritDoc}
   * @see java.lang.Object#toString()
   */
  public String toString() {
    if (getTable() == null) {
       return "null/" + troid();      
    }
    return getTable().getName() + "/" + troid();
  }

  /** 
   * {@inheritDoc}
   * @see org.melati.poem.Persistent#displayString(org.melati.poem.PoemLocale, int)
   */
  public String displayString(PoemLocale locale, int style)
      throws AccessPoemException {
    Column<?> displayColumn = getTable().displayColumn();
    if (displayColumn.isTroidColumn() && this.troid == null)
      return "null";
    else
      return displayColumn.getType().stringOfCooked(displayColumn.getCooked(this),
                                                  locale, style);
  }

  /** 
   * {@inheritDoc}
   * @see org.melati.poem.Persistent#displayString(org.melati.poem.PoemLocale)
   */
  public String displayString(PoemLocale locale) 
      throws AccessPoemException {
    return displayString(locale, DateFormat.MEDIUM);
  }
  /** 
   * {@inheritDoc}
   * @see org.melati.poem.Persistent#displayString()
   */
  public String displayString() 
      throws AccessPoemException {
    return displayString(PoemLocale.HERE, DateFormat.MEDIUM);
  }

  // 
  // ===============
  //  Support stuff
  // ===============
  // 

  /**
   * {@inheritDoc}
   * @see java.lang.Object#hashCode()
   */
  public final int hashCode() {
    if (troid == null)
      throw new InvalidOperationOnFloatingPersistentPoemException(this);

    return getTable().hashCode() + troid().intValue();
  }

  /**
   * {@inheritDoc}
   * @see java.lang.Object#equals(java.lang.Object)
   */
  public final boolean equals(Object object) {
    if (object == null || !(object instanceof Persistent))
      return false;
    else {
      JdbcPersistent other = (JdbcPersistent)object;
      return other.troid() == troid() &&
             other.getTable() == getTable();
    }
  }

  /**
   * {@inheritDoc}
   * @see org.melati.poem.transaction.Transactioned#invalidate()
   */
  public synchronized void invalidate() {
    assertNotFloating();
    super.invalidate();
    extras = null;
  }

  /**
   * {@inheritDoc}
   * @see java.lang.Object#clone()
   */
  protected Object clone() {
    // to clone it you have to be able to read it
    assertCanRead();
    JdbcPersistent it;
    try {
      it = (JdbcPersistent)super.clone();
    }
    catch (CloneNotSupportedException e) {
      throw new UnexpectedExceptionPoemException(e, "Object no longer supports clone.");
    }

    it.extras = (Object[])extras().clone();
    it.reset();
    it.troid = null;
    it.status = NONEXISTENT;

    return it;
  }

  /** 
   * {@inheritDoc}
   * @see org.melati.poem.Persistent#dump()
   */
  public String dump() {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    PrintStream ps = new PrintStream(baos);
    dump(ps);
    return baos.toString();
  }

  /** 
   * {@inheritDoc}
   * @see org.melati.poem.Persistent#dump(java.io.PrintStream)
   */
  public void dump(PrintStream p) {
    p.println(getTable().getName() + "/" + troid());
    for (Enumeration<Field<?>> f = getRecordDisplayFields(); f.hasMoreElements();) {
      p.print("  ");
      ((Field<?>)f.nextElement()).dump(p);
      p.println();
    }
  }

  /** 
   * {@inheritDoc}
   * @see org.melati.poem.Persistent#postWrite()
   */
  public void postWrite() {
  }

  /** 
   * {@inheritDoc}
   * @see org.melati.poem.Persistent#postInsert()
   */
  public void postInsert() {
  }

  /** 
   * {@inheritDoc}
   * @see org.melati.poem.Persistent#postModify()
   */
  public void postModify() {
  }

  /** 
   * {@inheritDoc}
   * @see org.melati.poem.Persistent#preEdit()
   */
  public void preEdit() {
  }

  /** 
   * {@inheritDoc}
   * @see org.melati.poem.Persistent#postEdit(boolean)
   */
  public void postEdit(boolean creating) {
  }

  // 
  // =================================
  // Use to Represent Query Constructs
  // =================================
  //

  /**
   * Return a SELECT query to count rows matching criteria represented
   * by this object.
   *
   * @param includeDeleted whether to include soft deleted records
   * @param excludeUnselectable Whether to append unselectable exclusion SQL 
   * @return an SQL query string
   */
  protected String countMatchSQL(boolean includeDeleted,
                              boolean excludeUnselectable) {
    return getTable().countSQL(
      fromClause(),
      getTable().whereClause(this),
      includeDeleted, excludeUnselectable);
  }

  /**
   * Return an SQL FROM clause for use when selecting rows using criteria
   * represented by this object.
   * <p>
   * By default just the table name is returned, quoted as necessary for
   * the DBMS.
   * <p>
   * Subtypes must ensure the result is compatible with the
   * result of {@link Table #appendWhereClause(StringBuffer, JdbcPersistent)}.
   * @return an SQL snippet 
   */
  protected String fromClause() {
    return getTable().quotedName();
  }

  public Treeable[] getChildren() {
    Enumeration<Persistent> refs = getDatabase().referencesTo(this);
    Vector<Persistent> v = new Vector<Persistent>();
    while (refs.hasMoreElements())
      v.addElement(refs.nextElement());
    Treeable[] kids;
    synchronized (v) {
      kids = new Treeable[v.size()];
      v.copyInto(kids);
    }

    return kids;
  }

  /** 
   * NOTE This will be overridden if the persistent has a field called <tt>name</tt>. 
   * {@inheritDoc}
   * @see org.melati.poem.Persistent#getName()
   */
  public String getName() {
    return displayString();
  }

  /**
   * @return the dirty
   */
  public boolean isDirty() {
    return dirty;
  }

  /**
   * @param dirty the dirty to set
   */
  public void setDirty(boolean dirty) {
    this.dirty = dirty;
  }

}