BasePoemType.java

/*
 * $Source$
 * $Revision$
 *
 * Copyright (C) 2000 William Chesters
 *
 * 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:
 *
 *     William Chesters <williamc At paneris.org>
 *     http://paneris.org/~williamc
 *     Obrechtstraat 114, 2517VX Den Haag, The Netherlands
 */

package org.melati.poem;

import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Enumeration;

import org.melati.poem.dbms.Dbms;
import org.melati.poem.util.ConsEnumeration;

/**
 * Base class of all fundamental types.
 */
public abstract class BasePoemType<T> implements SQLPoemType<T>, Cloneable {
  private int sqlTypeCode;
  protected boolean nullable;

  private Comparable<T> low = null, limit = null;

  BasePoemType(int sqlTypeCode, boolean nullable) {
    this.sqlTypeCode = sqlTypeCode;
    this.nullable = nullable;
  }

  /**
   * Set the limits, if applicable.
   * @param low included lower limit
   * @param limit excluded upper limit
   */
  public void setRawRange(Comparable<T> low, Comparable<T> limit) {
    this.low = low;
    this.limit = limit;
  }

  protected Comparable<T> getLowRaw() {
    return low;
  }

  protected Comparable<T> getLimitRaw() {
    return limit;
  }

  protected abstract void _assertValidRaw(Object raw)
      throws ValidationPoemException;

  @SuppressWarnings("unchecked")
  private void assertRawInRange(Object raw) {
    // Range check.  Since we can't do this with multiple inheritance, we
    // provide it as a facility even in types for which it is meaningless.

    T asComparable;
    try {
      // Note that in java5 this will not throw until 
      // the cast object is accessed
      asComparable = (T)raw;

      if ((low != null && low.compareTo(asComparable) > 0) ||
          (limit != null && limit.compareTo(asComparable) <= 0))
      throw new ValidationPoemException(
            this, raw, new OutsideRangePoemException(low, limit, raw));
    } catch (ClassCastException e) {
      throw new NotComparablePoemException(raw, this);
    }
  }

  /**
   * {@inheritDoc}
   * @see org.melati.poem.PoemType#assertValidRaw(java.lang.Object)
   */
  public final void assertValidRaw(Object raw)
      throws ValidationPoemException {
    if (raw == null) {
      if (!nullable)
        throw new NullTypeMismatchPoemException(this);
    }
    else {
      if (low != null || limit != null)
        assertRawInRange(raw);
      _assertValidRaw(raw);
    }
  }

  /**
   * Check if the raw value is valid, as expected.
   * @param raw an Object which should be of correct type
   */
  private void doubleCheckValidRaw(Object raw) {
    try {
      assertValidRaw(raw);
    }
    catch (ValidationPoemException e) {
      throw new UnexpectedValidationPoemException(e);
    }
  }

  protected abstract T _getRaw(ResultSet rs, int col)
      throws SQLException;

  /**
   * {@inheritDoc}
   * @see org.melati.poem.SQLType#getRaw(java.sql.ResultSet, int)
   */
  public final T getRaw(ResultSet rs, int col)
      throws ValidationPoemException {
    T o;
    try {
      o = (T) _getRaw(rs, col);
    }
    catch (SQLException e) {
      throw new SQLSeriousPoemException(e);
    }

    assertValidRaw(o);
    return o;
  }

  protected abstract void _setRaw(PreparedStatement ps, int col,
                                  Object raw)
      throws SQLException;

  /**
   * {@inheritDoc}
   * @see org.melati.poem.SQLType#setRaw(java.sql.PreparedStatement, int, java.lang.Object)
   */
  public final void setRaw(PreparedStatement ps, int col, Object raw) {
    doubleCheckValidRaw(raw);
    try {
      if (raw == null)
        ps.setNull(col, sqlTypeCode());
      else
        _setRaw(ps, col, raw);
    }
    catch (SQLException e) {
      throw new SQLSeriousPoemException(e);
    }
  }

  protected Enumeration<T> _possibleRaws() {
    return null;
  }
  
  /**
   * {@inheritDoc}
   * @see org.melati.poem.PoemType#possibleRaws()
   */
  public Enumeration<T> possibleRaws() {
    Enumeration<T> them = _possibleRaws();
    return them == null ? null :
                   getNullable() ? new ConsEnumeration<T>(null, them) :
                   them;
  }

  protected abstract String _stringOfRaw(Object raw);

  /**
   * This <B>doesn't</B> do an explicit <TT>assertValidRaw</TT>.
      * {@inheritDoc}
   * @see org.melati.poem.PoemType#stringOfRaw(java.lang.Object)
   */
  public final String stringOfRaw(Object raw)
      throws ValidationPoemException {
    return raw == null ? null : _stringOfRaw(raw);
  }

  /**
   * Converts a non-null string to an appropriate value 
   * for insertion into the underlying DBMS.
   * @param string the String to parse
   * @return a converted type
   */
  protected abstract T _rawOfString(String string)
      throws ParsingPoemException;

  /**
   * Converts a possibly null <code>String</code> to a low level
   * representation of a valid database column value.
   * <p>
   * Null values are not changed.
   * <p>
   * This result is validated with {@link #assertValidRaw(Object)}
   * whereas {@link #stringOfRaw(Object)} assumes this is not
   * required.
   * {@inheritDoc}
   * @see org.melati.poem.PoemType#rawOfString(java.lang.String)
   */
  public final T rawOfString(String string)
      throws ParsingPoemException, ValidationPoemException {
    T raw = string == null ? null : _rawOfString(string);
    assertValidRaw(raw);
    return raw;
  }

  protected abstract void _assertValidCooked(Object cooked)
      throws ValidationPoemException;

  /**
   * {@inheritDoc}
   * @see org.melati.poem.PoemType#assertValidCooked(java.lang.Object)
   */
  public final void assertValidCooked(Object cooked)
      throws ValidationPoemException {
    if (cooked == null) {
      if (!nullable)
        throw new NullTypeMismatchPoemException(this);
    }
    else {
      _assertValidCooked(cooked);
      if (low != null || limit != null)
        assertRawInRange(_rawOfCooked(cooked));
    }
  }

  /**
   * Check that object is valid, as expected.
   * NOTE If it isn't valid then it isn't cooked.
   * @param cooked the cooked object
   */
  final void doubleCheckValidCooked(Object cooked) {
    try {
      assertValidCooked(cooked);
    }
    catch (ValidationPoemException e) {
      throw new UnexpectedValidationPoemException(e);
    }
  }

  /**
   * Converts a non-null low-level representation of a database
   * column value to the appropriate object.
   * <p>
   * For the base object types, (String, Integer etc) this involves no change. 
   * <p>
   * For types with an integer id, such as Poem internal types and user defined types, 
   * then the appropriate instantiated type is returned from its Integer id.
   * @param raw the base object or Integer object id
   * @return the unchanged base object or an instantiated type
   */
  protected abstract Object _cookedOfRaw(Object raw) throws PoemException;

  /**
   * Converts a possibly null low-level representation of a database
   * column value to its canonical form. 
   * Types represented as integers in the database are converted to 
   * corresponding objects .
   * <p>
   * The raw value is checked to ensure it is valid.
   * {@inheritDoc}
   * @see org.melati.poem.PoemType#cookedOfRaw(java.lang.Object)
   */
  public final Object cookedOfRaw(Object raw) throws PoemException {
    doubleCheckValidRaw(raw);
    return raw == null ? null : _cookedOfRaw(raw);
  }

  protected abstract T _rawOfCooked(Object raw) throws PoemException;

  /**
   * {@inheritDoc}
   * @see org.melati.poem.PoemType#rawOfCooked(java.lang.Object)
   */
  public final T rawOfCooked(Object cooked) {
    doubleCheckValidCooked(cooked);
    return cooked == null ? null : _rawOfCooked(cooked);
  }

  protected abstract String _stringOfCooked(Object cooked,
                                           PoemLocale locale, int style)
      throws PoemException;

  /**
   * {@inheritDoc}
   * @see org.melati.poem.PoemType#stringOfCooked(java.lang.Object, 
   *          org.melati.poem.PoemLocale, int)
   */
  public final String stringOfCooked(Object cooked,
                                    PoemLocale locale, int style)
      throws PoemException {
    doubleCheckValidCooked(cooked);
    return cooked == null ? "" : _stringOfCooked(cooked, locale, style);
  }

  /**
   * {@inheritDoc}
   * @see org.melati.poem.PoemType#getNullable()
   */
  public final boolean getNullable() {
    return nullable;
  }

  /**
   * {@inheritDoc}
   * @see org.melati.poem.SQLType#sqlTypeCode()
   */
  public final int sqlTypeCode() {
    return sqlTypeCode;
  }

  protected abstract String _sqlDefinition(Dbms dbms);

  /**
   * See http://dev.mysql.com/doc/refman/5.0/en/timestamp.html
   * The MySQL default for nullability of timestamps is not null, so need to 
   * make all fields explicitly nullable.
   * 
   * {@inheritDoc}
   * @see org.melati.poem.SQLType#sqlDefinition(org.melati.poem.dbms.Dbms)
   */
  public String sqlDefinition(Dbms dbms) {
    return sqlTypeDefinition(dbms) + (nullable ? " NULL" : " NOT NULL");
  }
  /**
   * {@inheritDoc}
   * @see org.melati.poem.SQLType#sqlTypeDefinition(org.melati.poem.dbms.Dbms)
   */
  public String sqlTypeDefinition(Dbms dbms) {
    return _sqlDefinition(dbms);
  }
  protected abstract boolean _canRepresent(SQLPoemType<?> other);

  /**
   * {@inheritDoc}
   * @see org.melati.poem.PoemType#canRepresent(org.melati.poem.PoemType)
   */
  public <O>PoemType<O> canRepresent(PoemType<O> other) {
    // FIXME takes no account of range---need to decide on semantics for this,
    // is it subset (inclusion) or some other notion of storability?
    if (!(other instanceof SQLPoemType)) 
      // NOTE Never happens as currently all PoemTypes are SQLPoemTypes
      return null;
    else {
      SQLPoemType<O> q = (SQLPoemType<O>)other;
      return
          !(!nullable && q.getNullable()) && // Nullable may represent not nullable
          _canRepresent(q) ?
              q : null;
    }
  }

  /**
   * {@inheritDoc}
   * @see org.melati.poem.PoemType#withNullable(boolean)
   */
  @SuppressWarnings("unchecked")
  public final PoemType<T> withNullable(boolean nullableP) {
    if (this.nullable == nullableP)
      return this;
    else {
      BasePoemType<T> it = (BasePoemType<T>)clone();
      it.nullable = nullableP;
      return it;
    }
  }

  protected abstract void _saveColumnInfo(ColumnInfo info)
      throws AccessPoemException;

  /**
   * {@inheritDoc}
   * @see org.melati.poem.PoemType#saveColumnInfo(org.melati.poem.ColumnInfo)
   */
  public void saveColumnInfo(ColumnInfo info) throws AccessPoemException {
    info.setNullable(nullable);
    info.setSize(0);
    info.setRangelow_string(
        getLowRaw() == null ? null : stringOfRaw(getLowRaw()));
    // this _won't_ throw an OutsideRangePoemException since it doesn't check
    info.setRangelimit_string(
        getLimitRaw() == null ? null : stringOfRaw(getLimitRaw()));
    _saveColumnInfo(info);
  }

  protected abstract String _quotedRaw(Object raw);

  /**
   * {@inheritDoc}
   * @see org.melati.poem.SQLType#quotedRaw(java.lang.Object)
   */
  public String quotedRaw(Object raw) throws ValidationPoemException {
    assertValidRaw(raw);
    return raw == null ? "NULL" : _quotedRaw(raw);
  }

  protected abstract String _toString();

  // 
  // --------
  //  Object
  // --------
  // 

  /**
   * {@inheritDoc}
   * @see java.lang.Object#toString()
   */
  public String toString() {
    return (nullable ? "nullable " : "") + _toString() + 
    " (" + this.getClass().getName() + ")";
  }

  /**
   * {@inheritDoc}
   * @see java.lang.Object#clone()
   */
  protected Object clone() {
    try {
      return super.clone();
    }
    catch (CloneNotSupportedException e) {
      throw new PoemBugPoemException();
    }
  }
}