/*
  Copyright (C) 2006 Helge Hess

  This file is part of JOPE.

  JOPE is free software; you can redistribute it and/or modify it under
  the terms of the GNU Lesser General Public License as published by the
  Free Software Foundation; either version 2, or (at your option) any
  later version.

  JOPE 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 Lesser General Public
  License for more details.

  You should have received a copy of the GNU Lesser General Public
  License along with JOPE; see the file COPYING.  If not, write to the
  Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
  02111-1307, USA.
*/

package org.opengroupware.jope.eoaccess;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.opengroupware.jope.eocontrol.EOAndQualifier;
import org.opengroupware.jope.eocontrol.EOFetchSpecification;
import org.opengroupware.jope.eocontrol.EOQualifier;
import org.opengroupware.jope.foundation.NSDisposable;
import org.opengroupware.jope.foundation.NSException;
import org.opengroupware.jope.foundation.NSObject;

/*
 * EOAdaptorChannel
 * 
 * Wraps a JDBC SQL connection.
 * 
 * TODO: in EOF the channel is the JDBC statement and the context is the
 *       connection. Maybe we want to do this as well, don't know.
 * 
 * TODO: implement caching of prepared statements. we use a lot of identical
 *       queries when EODatabaseChannel is being used. Possibly this gives a
 *       major performance boost.
 * 
 * TODO: document more
 */
public class EOAdaptorChannel extends NSObject implements NSDisposable {
  protected static final Log log    = LogFactory.getLog("EOAdaptorChannel");
  protected static final Log sqllog = LogFactory.getLog("EOSQLRunLog");

  protected EOAdaptor  adaptor;
  protected Connection connection;
  protected long       startTimeInSeconds;
  protected Exception  lastException;
  
  public EOAdaptorChannel(EOAdaptor _adaptor, Connection _c) {
    this.adaptor    = _adaptor;
    this.connection = _c;
    this.startTimeInSeconds  = new Date().getTime() / 1000;
  }
  
  /* accessors */
  
  public Connection connection() {
    return this.connection;
  }
  
  public Exception consumeLastException() {
    Exception e = this.lastException;
    this.lastException = null;
    return e;
  }
  
  public long startTimeInSeconds() {
    return this.startTimeInSeconds;
  }
  
  public long ageInSeconds() {
    return (new Date().getTime() / 1000) - this.startTimeInSeconds;
  }
  
  /* EOSQLStatements */
  
  public List<Map<String, Object>> evaluateQueryExpression(EOSQLExpression _s) {
    if (_s == null) {
      log.error("performSQL caller gave us no SQL ...");
      return null;      
    }
    
    this.lastException = null;
    List<Map<String, Object>> binds = _s.bindVariableDictionaries();
    
    if (binds == null || binds.size() == 0)
      return this.performSQL(_s.statement());
    
    PreparedStatement stmt = 
      this._prepareStatementWithBinds(_s.statement(), binds);
    if (stmt == null) {
      log.error("could not create prepared statement for expression: "+_s);
      return null;
    }
    
    /* perform query */
    
    List<Map<String, Object>> records = null;
    ResultSet rs = null;
    try {
      sqllog.info(_s.statement());
      
      rs = stmt.executeQuery();
      ResultSetMetaData meta = rs.getMetaData();
      
      /* loop over results and convert them to records */
      records = new ArrayList<Map<String, Object>>(128);
      while (rs.next()) {
        Map<String, Object> record;
        
        // TODO: somehow add model information
        record = this.convertCurrentResultSetRowToMap(rs, meta, null /*attrs*/);
        if (record != null) records.add(record);
      }
    }
    catch (SQLException e) {
      // MySQL: when it encounters network-connectivity issues during the
      //        processing of a query it sets:
      //        SQLException.getSQLState() to '08S01'
      log.error("could not execute SQL expression: " + _s, e);
    }
    finally {
      // TODO: we might also want to close our channel if the tear down was not
      //       clean
      this._releaseResources(stmt, rs);
    }
    
    // TODO: compact array

    return records;
  }
  
  public int evaluateUpdateExpression(EOSQLExpression _s) {
    if (_s == null) {
      log.error("evaluateUpdateExpression caller gave us no expr ...");
      return -1;      
    }

    this.lastException = null;
    
    String sql = _s.statement();
    if (sql == null) {
      log.error("evaluateUpdateExpression param is invalid expr: " + _s);
      return -1;
    }
    
    /* we always prepare for updates to improve escaping behaviour */
    
    List<Map<String, Object>> binds = _s.bindVariableDictionaries();

    if (sqllog.isInfoEnabled()) {
      sqllog.info(sql);
      sqllog.info("binds: " + binds);
    }
    PreparedStatement stmt = 
      this._prepareStatementWithBinds(sql, binds);
    
    if (stmt == null) {
      log.error("could not create prepared statement for expression: "+_s);
      return -1;
    }

    /* perform update */
    
    int updateCount = 0;
    try {
      /* execute */
      updateCount = stmt.executeUpdate();
    }
    catch (SQLException e) {
      this.lastException = e;
      log.error("could not perform update expression: " + _s, e);
      return -1;
    }
    catch (NullPointerException e) {
      /* Note: this happens in the MySQL adaptor if the statement got closed in
       *       the meantime. (TODO: closed by whom?)
       */
      this.lastException = e;
      log.error("could not perform update statement (null ptr): "+
                     _s, e);
      return -1;
    }
    finally {
      this._releaseResources(stmt, null);
    }

    if (log.isDebugEnabled())
      log.debug("affected objects: " + updateCount);
    
    return updateCount;
  }
  
  protected PreparedStatement _prepareStatementWithBinds
    (String _sql, List<Map<String, Object>> _binds)
  {
    boolean isDebugOn = log.isDebugEnabled();
    if (_sql == null || _sql.length() == 0) return null;

    PreparedStatement stmt = this._createPreparedStatement(_sql);
    if (stmt == null)
      return null;
    if (_binds == null) {
      if (isDebugOn)
        log.debug("statement to prepare has no binds ..");
      return stmt; /* hm, statement has no binds */
    }
    
    /* fill in parameters */

    if (isDebugOn)
      log.debug("prepare binds: " + _binds);
    
    try {
      /* fill statement with values */
      for (int i = 0; i < _binds.size(); i++) {
        Map<String, Object> bind = _binds.get(i);
        
        EOAttribute attribute = 
          (EOAttribute)bind.get(EOSQLExpression.BindVariableAttributeKey);
        
        Object value = bind.get(EOSQLExpression.BindVariableValueKey);
        
        int sqlType = this.sqlTypeForValue(value, attribute);
        
        if (isDebugOn) {
          log.debug("  bind attribute: " + attribute);
          log.debug("           value: " + value + " / " + 
                         (value != null ? value.getClass() : "[NULL]"));
          log.debug("            type: " + sqlType);
        }
        
        if (value == null)
          stmt.setNull(i + 1, sqlType);
        else {
          switch (sqlType) {
            case java.sql.Types.NULL:
              stmt.setNull(i + 1, java.sql.Types.VARCHAR); // CRAP
              break;
              
            // TODO: customize value processing for types
            case java.sql.Types.VARCHAR:
            case java.sql.Types.TIMESTAMP:
            case java.sql.Types.DATE:
            case java.sql.Types.INTEGER:
            case java.sql.Types.BOOLEAN:
            default:
              if (value instanceof String)
                stmt.setString(i + 1, (String)value);
              else if (value instanceof Boolean)
                stmt.setBoolean(i + 1, (Boolean)value);
              else if (value instanceof Integer)
                stmt.setInt(i + 1, (Integer)value);
              else if (value instanceof java.util.Date) {
                stmt.setTimestamp(i + 1,
                  new java.sql.Timestamp(((Date)value).getTime()));
              }
              else if (value instanceof java.sql.Date) {
                /* Note: this is just the DATE component, no TIME */
                stmt.setDate(i + 1, (java.sql.Date)value);
              }
              else if (value instanceof byte[])
                stmt.setBytes(i + 1, (byte[])value);
              else {
                log.warn("using String column for value: " + value +
                              " (" + value.getClass() + ")");
              }
          }
        }
      }
    }
    catch (NullPointerException e) {
      this.lastException = e;
      log.error("could not apply binds to prepared statement (null ptr): "+
                     _sql, e);
      this._releaseResources(stmt, null);
      return null;
    }
    catch (SQLException e) {
      this.lastException = e;
      log.error("could not apply binds to prepared statement: " + _sql, e);
      this._releaseResources(stmt, null);
      return null;
    }
    
    return stmt;
  }
  
  protected int sqlTypeForValue(Object _o, EOAttribute _attr) {
    if (_attr != null) {
      int type = _attr.sqlType();
      if (type != java.sql.Types.NULL)
        return type; /* a specific type is set */
      
      type = this.sqlTypeForExternalType(_attr.externalType());
      if (type != java.sql.Types.NULL) {
        // TODO: maybe cache in attribute?
        return type; /* a specific type is set */
      }
      
      /* otherwise continue with object */
    }
    if (_o == null)                   return java.sql.Types.NULL;
    if (_o instanceof String)         return java.sql.Types.VARCHAR;
    if (_o instanceof java.util.Date) return java.sql.Types.TIMESTAMP;
    if (_o instanceof java.sql.Date)  return java.sql.Types.DATE;
    if (_o instanceof Integer)        return java.sql.Types.INTEGER;
    if (_o instanceof Boolean)        return java.sql.Types.BOOLEAN;
    return java.sql.Types.VARCHAR;
  }
  
  protected int sqlTypeForExternalType(String _type) {
    if (_type == null)
      return java.sql.Types.NULL;
    _type = _type.toUpperCase();

    /* somehow derive type from external type */
    if (_type.startsWith("VARCHAR"))   return java.sql.Types.VARCHAR;
    if (_type.startsWith("INT"))       return java.sql.Types.INTEGER;
    if (_type.startsWith("BOOL"))      return java.sql.Types.BOOLEAN;
    if (_type.startsWith("TIMESTAMP")) return java.sql.Types.TIMESTAMP;
    if (_type.startsWith("DATETIME"))  return java.sql.Types.TIMESTAMP;
    if (_type.startsWith("DATE"))      return java.sql.Types.DATE;
    if (_type.startsWith("TIME"))      return java.sql.Types.DATE;
    return java.sql.Types.NULL;
  }

  /* utility methods */
  
  // TODO: return some kind of indexed SQL (EOGenericRecord??) record which does
  //       KVC and stores the columns as an array.
  public List<Map<String, Object>> performSQL(String _sql) {
    if (_sql == null || _sql.length() == 0) {
      log.error("performSQL caller gave us no SQL ...");
      this.lastException = new Exception("got no SQL to perform!");
      return null;
    }
    this.lastException = null;
    
    /* acquire DB resources */
    
    Statement  stmt = this._createStatement();
    if (stmt == null) return null;
    
    /* perform query */
    
    List<Map<String, Object>> records = null;
    ResultSet rs = null;
    try {
      sqllog.info(_sql);
      
      rs = stmt.executeQuery(_sql);
      ResultSetMetaData meta = rs.getMetaData();
      
      /* loop over results and convert them to records */
      records = new ArrayList<Map<String, Object>>(128);
      while (rs.next()) {
        Map<String, Object> record;
        
        record = this.convertCurrentResultSetRowToMap(rs, meta, null /*attrs*/);
        if (record != null) records.add(record);
      }
    }
    catch (SQLException e) {
      /*
       * SQLState:
       * 42601 - PostgreSQL for invalid SQL, like "SELECT *"
       */
      log.error("could not execute SQL statement: " + _sql + 
                " " + e.getSQLState(), e);
    }
    finally {
      // TODO: we might also want to close our channel if the tear down was not
      //       clean
      this._releaseResources(stmt, rs);
    }
    
    // TODO: compact array

    return records;
  }
  
  public int performUpdateSQL(String _sql) {
    if (_sql == null || _sql.length() == 0) {
      log.error("performUpdateSQL caller gave us no SQL ...");
      this.lastException = new Exception("got no SQL to perform!");
      return -1;
    }
    this.lastException = null;
    
    /* acquire DB resources */
    
    Statement  stmt = this._createStatement();
    if (stmt == null) return -1;
    
    /* perform query */
    
    int updateCount = 0;
    try {
      sqllog.info(_sql);
      
      updateCount = stmt.executeUpdate(_sql);
    }
    catch (SQLException e) {
      this.lastException = e;
      log.error("could not execute SQL statement: " + _sql, e);
    }
    finally {
      // TODO: we might also want to close our channel if the tear down was not
      //       clean
      this._releaseResources(stmt, null /* resultset */);
    }
    
    return updateCount;
  }
   
  public boolean insertRow(String _table, Map<String, Object> _record) {
    // Note: this does not support insertion of NULLs
    if (_table == null || _record == null)
      return false;
    
    String columns[] = _record.keySet().toArray(new String[0]);
    Object values[]  = new Object[columns.length];
    int    types[]   = new int[columns.length];
    
    for (int i = 0; i < columns.length; i++) {
      values[i] = _record.get(columns[i]);
      types[i]  = this.sqlTypeForValue(values[i], null /* attribute */);
    }
    return this.insertRow(_table, columns, types, values);
  }
  
  public boolean insertRow
    (String _table, String _cols[], int _types[], Object _vals[])
  {
    if (_table == null || _cols == null)
      return false;
    
    /* generate SQL */
    
    EOSQLExpression e = this.adaptor.expressionFactory().createExpression(null);
    StringBuffer sql = new StringBuffer(255);
    
    sql.append("INSERT INTO ");
    sql.append(e.sqlStringForSchemaObjectName(_table));
    
    /* keys */
    
    sql.append(" (");
    
    for (int i = 0; i < _cols.length; i++) {
      if (i > 0) sql.append(", ");
      sql.append(e.sqlStringForSchemaObjectName(_cols[i]));
    }
    
    /* values */
    
    sql.append(" ) VALUES (");
    
    for (int i = 0; i < _cols.length; i++)
      sql.append(i > 0 ? ", ? " : " ? ");
    
    sql.append(")");
    
    /* acquire DB resources */
    
    PreparedStatement stmt = this._createPreparedStatement(sql.toString());
    if (stmt == null) return false;
    
    /* perform insert */
    
    int insertCount = 0;
    try {
      /* fill statement with values */
      for (int i = 0; i < _vals.length; i++)
        this._setStatementParameter(stmt, i + 1, _types[i], _vals[i]);
      
      /* execute */
      insertCount = stmt.executeUpdate();
    }
    catch (SQLException ex) {
      log.error("could not perform INSERT: " + sql.toString(), ex);
    }
    finally {
      // TODO: fix me
      this._releaseResources(stmt, null);
    }
    
    return insertCount == 1;
  }
  
  public boolean updateRow
    (String _table, String _pkey, Object _value, Map<String, Object> _record)
  {
    // Note: this does not support insertion of NULLs
    if (_table == null || _record == null)
      return false;
    
    String columns[] = _record.keySet().toArray(new String[0]);
    Object values[]  = new Object[columns.length];
    int    types[]   = new int[columns.length];
    
    for (int i = 0; i < columns.length; i++) {
      values[i] = _record.get(columns[i]);
      types[i]  = this.sqlTypeForValue(values[i], null /* attribute */);
    }
    return this.updateRow(_table, _pkey, _value, columns, types, values);
  }
  
  public boolean updateRow
    (String _table, String _pkey, Object _value, 
     String _cols[], int _types[], Object _vals[])
  {
    if (_table == null || _cols == null || _pkey == null || _value == null)
      return false;
    
    /* generate SQL */
    
    EOSQLExpression e = this.adaptor.expressionFactory().createExpression(null);
    StringBuffer sql = new StringBuffer(255);
    sql.append("UPDATE ");
    sql.append(e.sqlStringForSchemaObjectName(_table));
    sql.append(" SET ");
    
    /* keys / values */
    
    for (int i = 0; i < _cols.length; i++) {
      if (i > 0) sql.append(", ");
      sql.append(e.sqlStringForSchemaObjectName(_cols[i]));
      sql.append(" = ?");      
    }
    
    /* where */
    
    sql.append(" WHERE ");
    sql.append(e.sqlStringForSchemaObjectName(_pkey));
    sql.append(" = ?");
    
    /* acquire DB resources */
    
    PreparedStatement stmt = this._createPreparedStatement(sql.toString());
    if (stmt == null) return false;
    
    /* perform update */
    
    int updateCount = 0;
    try {
      /* fill statement with values */
      for (int i = 0; i < _vals.length; i++)
        this._setStatementParameter(stmt, i + 1, _types[i], _vals[i]);
      
      /* WHERE statement parameter */
      this._setStatementParameter(stmt, _vals.length + 1, 
                                  this.sqlTypeForValue(_value, null), _value);
      
      /* execute */
      updateCount = stmt.executeUpdate();
      if (updateCount > 1) {
        log.warn("update affected more than one record " +
                      _table + " (" + _pkey + " = " + _value + ")");
      }
    }
    catch (SQLException ex) {
      log.error("could not perform UPDATE: " + sql.toString(), ex);
    }
    finally {
      // TODO: fix me, check result, check connection
      this._releaseResources(stmt, null);
    }
    
    return updateCount > 0; /* well, yes, we consider 1+ updates OK */
  }

  public Integer nextNumberInSequence(String _sequence) {
    log.warn("this EOAdaptor does not implement sequence fetches ...");
    return null;
  }
  
  public boolean close() {
    if (this.connection == null)
      return true; /* consider closing a closed connection OK ... */
    
    try {
      this.connection.close();
      return true;
    }
    catch (SQLException e) {
      log.warn("failed to close connection.");
      return false;
    }
  }
  
  public void dispose() {
    this.close();
    this.adaptor = null;
  }
  
  /* adaptor operations */
  
  public int performAdaptorOperationN(EOAdaptorOperation _op) {
    /* returns the affected rows */
    
    if (_op == null) /* got nothing, should we raise? */
      return 0;
    
    int affectedRows = 0;
    switch (_op.adaptorOperator()) {
      case EOAdaptorOperation.AdaptorLockOperator: {
        this.lockRowComparingAttributes
          (_op.attributes(), _op.entity(), _op.qualifier(),_op.changedValues());
        affectedRows = 1; /* a bit hackish? */
        break;
      }
        
      case EOAdaptorOperation.AdaptorInsertOperator:
        // TODO: somehow we need to report autoincrement primary keys!
        if (this.insertRow(_op.changedValues(), _op.entity()))
          affectedRows = 1;
        else
          affectedRows = -1;
        break;
        
      case EOAdaptorOperation.AdaptorUpdateOperator:
        affectedRows = this.updateValuesInRowsDescribedByQualifier
          (_op.changedValues(), _op.qualifier(), _op.entity());
        break;
        
      case EOAdaptorOperation.AdaptorDeleteOperator:
        affectedRows =
          this.deleteRowsDescribedByQualifier(_op.qualifier(), _op.entity());
        break;
        
      default:
        // TODO: improve error handling
        log.error("unknown/unsupported adaptor operation: " + _op);
        this.lastException =
          new Exception("unknown/unsupported adaptor operation");
        _op.setException(this.consumeLastException());
        return -1;
    }

    return affectedRows;
  }
  public Exception performAdaptorOperation(EOAdaptorOperation _op) {
    if (_op == null) /* got nothing, should we raise? */
      return null;
    
    int affectedRows = this.performAdaptorOperationN(_op);
    if (affectedRows == 1)
      return null; /* everything OK */

    Exception error = this.consumeLastException();
    if (error == null)
      error = new Exception("operation did affect more/less than one row");
    _op.setException(error);
    return error; 
  }
  
  public Exception performAdaptorOperations(EOAdaptorOperation[] _ops) {
    if (_ops == null) /* got nothing, should we raise? */
      return null;
    
    // TODO: we should probably open a transaction if count > 1?
    
    // if the JDBC adaptor supports it, we could create update-batches for
    // changes which are the same.
    
    for (EOAdaptorOperation op: _ops) {
      Exception e = this.performAdaptorOperation(op);
      if (e != null) return e;
    }
    
    return null;
  }
  public Exception performAdaptorOperations(List<EOAdaptorOperation> _ops) {
    return this.performAdaptorOperations
      (_ops.toArray(new EOAdaptorOperation[_ops.size()]));
  }
  
  public boolean updateValuesInRowDescribedByQualifier
    (Map<String, Object> _values, EOQualifier _qualifier, EOEntity _entity)
  {
    return this.updateValuesInRowsDescribedByQualifier
      (_values, _qualifier, _entity) == 1;
  }
  public int updateValuesInRowsDescribedByQualifier
    (Map<String, Object> _values, EOQualifier _qualifier, EOEntity _entity)
  {
    if (_values == null || _values.size() == 0) {
      this.lastException = new NSException("got no value for update?!");
      return -1;
    }
    
    EOSQLExpression expr = this.adaptor.expressionFactory()
      .updateStatementForRow(_values, _qualifier, _entity);
    return this.evaluateUpdateExpression(expr);
  }
  
  public boolean deleteRowDescribedByQualifier(EOQualifier _q, EOEntity _e) {
    return this.deleteRowsDescribedByQualifier(_q, _e) == 1;
  }
  public int deleteRowsDescribedByQualifier(EOQualifier _q, EOEntity _entity) {
    EOSQLExpression expr = this.adaptor.expressionFactory()
      .deleteStatementWithQualifier(_q, _entity);
    return this.evaluateUpdateExpression(expr);
  }
  
  public boolean insertRow(Map<String, Object> _row, EOEntity _entity) {
    EOSQLExpression expr = this.adaptor.expressionFactory()
      .insertStatementForRow(_row, _entity);
    
    return this.evaluateUpdateExpression(expr) == 1;
  }
  
  public List<Map<String, Object>> selectAttributes
    (EOAttribute[] _attrs, EOFetchSpecification _fs, boolean _lock, EOEntity _e)
  {
    if (this.adaptor == null) {
      this.lastException = new Exception("missing adaptor!");
      return null;
    }
    
    /* build SQL */
    
    EOSQLExpression expr = this.adaptor.expressionFactory()
      .selectExpressionForAttributes(_attrs, _lock, _fs, _e);
    
    /* perform fetch */
    
    List<Map<String, Object>> rows = this.evaluateQueryExpression(expr);
    
    if (_fs != null && _fs.fetchesRawRows())
      return rows;
    if (rows == null || rows.size() == 0)
      return rows;
    
    //System.err.println("ROWS: " + rows);
    
    /* map row names */
    // TODO: this should be already done when converting the JDBC resultset */
    
    //System.err.println("ATTRS: " + Arrays.asList(_attrs));
    
    EOAttribute[] attributesToMap =
      this.attributesWhichRequireRowNameMapping(_attrs);
    
    if (attributesToMap != null) {
      for (int i = 0; i < rows.size(); i++) {
        Map<String, Object> row = rows.get(i);
        
        // hack: modification is in-place (has columnName==name issues)
        for (int j = 0; j < attributesToMap.length; j++) {
          Object v = row.remove(attributesToMap[j].columnName());
          row.put(attributesToMap[j].name(), v);
          
          //log.error("map attribute: " + attributesToMap[j]);
        }
      }
    }
    else
      log.debug("did not map any row attributes ...");
    //System.err.println("ROWS: " + rows);
    
    return rows;
  }
  
  public boolean lockRowComparingAttributes
    (EOAttribute[] _attrs, EOEntity _entity, EOQualifier _qualifier, 
     Map<String, Object> _snapshot)
  {
    EOQualifier q = EOQualifier.qualifierToMatchAllValues(_snapshot);
    if (_qualifier != null) {
      q = (q == null)
        ? _qualifier
        : new EOAndQualifier(new EOQualifier[] { _qualifier, q });
    }
    
    EOFetchSpecification fspec =
      new EOFetchSpecification(_entity != null ? _entity.name() : null,
                               q,
                               null /* sort orderings */);
    
    List<Map<String, Object>> results =
      this.selectAttributes(_attrs, fspec, true /* do lock */, _entity);
    
    if (results == null) /* SQL error */
      return false;
    if (results.size() != 1) /* more or less rows matched */
      return false;
    
    return true;
  }
  
  /* attribute name mapping */
  
  public EOAttribute[] attributesWhichRequireRowNameMapping(EOAttribute[] _s) {
    if (_s        == null) return null;
    if (_s.length == 0)    return null;
    
    List<EOAttribute> toBeMapped = null;
    for (int i = 0; i < _s.length; i++) {
      if (_s[i] == null) {
        log.warn("got a null attribute when scanning for mappings ...");
        continue;
      }
      
      String attrname = _s[i].name();
      if (attrname == null) continue; /* attrs w/o a name don't need mapping */
      
      String colname  = _s[i].columnName();
      if (colname == attrname || colname == null) continue; /* fast check */
      if (colname.equals(attrname)) continue;
      
      /* ok, has different names */
      if (toBeMapped == null)
        toBeMapped = new ArrayList<EOAttribute>(_s.length);
      toBeMapped.add(_s[i]);
    }
    
    return toBeMapped != null ? toBeMapped.toArray(new EOAttribute[0]) : null;
  }
  
  /* primitives */
  
  protected Statement _createStatement() {
    if (this.connection == null)
      return null;
    
    try {
      Statement stmt = this.connection.createStatement();
      return stmt;
    }
    catch (SQLException e) {
      log.error("could not create SQL statement", e);
      return null;
    }
  }
  protected PreparedStatement _createPreparedStatement(String _sql) {
    if (this.connection == null || _sql == null || _sql.length() == 0)
      return null;
    
    try {
      PreparedStatement stmt = this.connection.prepareStatement(_sql);
      return stmt;
    }
    catch (SQLException e) {
      log.error("could not prepare SQL statement", e);
      return null;
    }
  }
  
  protected boolean _releaseResources(Statement _s, ResultSet _rs) {
    boolean wasCleanRelease = true;
    
    if (_rs != null) {
      try {
        _rs.close();
      }
      catch (SQLException e) {
        log.error("failed to close SQL result set", e);
        wasCleanRelease = false;
      }
    }
    
    if (_s != null) {
      try {
        _s.close();
      }
      catch (SQLException e) {
        log.error("failed to close SQL statement", e);
        wasCleanRelease = false;
      }
    }
    
    return wasCleanRelease;
  }
  
  // TODO: this might be a nice utility function, but its better to convert
  //       to some SQL record which implements the Map interface plus some
  //       more.
  protected Map<String,Object> convertCurrentResultSetRowToMap
    (ResultSet _rs, ResultSetMetaData _meta, EOAttribute[] _attrs)
    throws SQLException
  {
    if (_rs   == null) return null;
    if (_meta == null) _meta = _rs.getMetaData();
    
    boolean isDebugOn   = log.isDebugEnabled();
    int     columnCount = _meta.getColumnCount();
    
    /* Note: we do store NULL columns as 'null' values! Thats possible with a
     * Java Map :-)
     */
    Map<String, Object> record = new HashMap<String,Object>(columnCount);
    
    if (isDebugOn)
      log.debug("map ResultSet to Map (" + columnCount + " columns):");
    
    for (int i = 1; i <= columnCount; i++) {
      String l       = _meta.getColumnName(i);
      int    coltype = _meta.getColumnType(i);
      
      Object v;
      
      /* Note: remember, _first_ get the value, _then_ check wasNull() .. */
      if (coltype == java.sql.Types.VARCHAR || coltype == java.sql.Types.CHAR) {
        String s = _rs.getString(i);
        
        if (_rs.wasNull())
          v = null;
        else
          v = s;
      }
      else if (coltype == java.sql.Types.TIMESTAMP) {
        
        try {
          v = _rs.getObject(i);
          if (_rs.wasNull()) v = null;
        }
        catch (SQLException e) {
          /* Note: we might get "Cannot convert value '0000-00-00 00:00:00'",
           *       in this case it doesn't help to attempt to get the string.
           */
          String s = e.getMessage();
          
          // TODO: hack for MySQL 4.1 JDBC
          if (s.indexOf("convert value \'0000-00-00 00:00:00\'") != -1)
            v = null; /* treat as NULL ... */
          else {
            log.error("issue with timestamp column[" + i + "]: " + s);
            continue;
          }
        }
      }
      else {
        try {
          v = _rs.getObject(i);
          if (_rs.wasNull()) v = null;
        }
        catch (SQLException e) {
          log.error("could not fetch column[" + i + "]: " +
                         e.getMessage());
          continue;
        }
      }
      
      if (isDebugOn)
        log.debug("  row[" + i + "] " + l + ": \t" + v);
      
      record.put(l, v);
    }
    return record;
  } 
  
  protected void _setStatementParameter
    (PreparedStatement _stmt, int _idx, int _type, Object _value)
    throws SQLException
  {
    if (_stmt == null)
      return;
    
    /* NULL */
    
    if (_value == null) {
      _stmt.setNull(_idx, _type);
      return;
    }
    
    /* values */
    
    switch (_type) {
      case java.sql.Types.NULL:
        _stmt.setNull(_idx, java.sql.Types.VARCHAR); // CRAP
        break;
        
      // TODO: customize value processing for types
      case java.sql.Types.VARCHAR:
      case java.sql.Types.TIMESTAMP:
      case java.sql.Types.DATE:
      case java.sql.Types.INTEGER:
      case java.sql.Types.BOOLEAN:
      default:
        if (_value instanceof String)
          _stmt.setString(_idx, (String)_value);
        else if (_value instanceof Boolean)
          _stmt.setBoolean(_idx, (Boolean)_value);
        else if (_value instanceof Integer)
          _stmt.setInt(_idx, (Integer)_value);
        else if (_value instanceof java.util.Date) {
          _stmt.setTimestamp(_idx,
            new java.sql.Timestamp(((Date)_value).getTime()));
        }
        else if (_value instanceof java.sql.Date) {
          /* Note: this is just the DATE component, no TIME */
          _stmt.setDate(_idx, (java.sql.Date)_value);
        }
        else if (_value instanceof byte[])
          _stmt.setBytes(_idx, (byte[])_value);
        else {
          log.warn("using String column for value: " + _value +
                        " (" + _value.getClass() + ")");
        }
    }
  }
  
  /* reflection */
  
  public String[] describeTableNames() {
    /* no generic way to retrieve tablenames via JDBC/SQL? */
    return null;
  }
  public String[] describeDatabaseNames(String _like) {
    /* no generic way to retrieve tablenames via JDBC/SQL? */
    return null;
  }
  
  public EOModel describeModelWithTableNames(String[] _tableNames) {
    if (_tableNames == null) return null;
    
    int count = _tableNames.length;
    EOEntity[] entities = new EOEntity[count];
    
    for (int i = 0; i < count; i++) {
      entities[i] = this.describeEntityWithTableName(_tableNames[i]);
      if (entities[i] == null) /* error */
        return null;
    }
    
    return new EOModel(entities);
  }
  
  public EOEntity describeEntityWithTableName(String _tableName) {
    return null;
  }

  /* name processing */
  
  protected String entityNameForTableName(String _tableName) {
    return _tableName;
  }
  
  protected String attributeNameForColumnName(String _colName) {
    return _colName;
  }
  
  /* utility */
  
  protected String[] fetchSingleStringRows(String _sql, String _columnName) {
    /* acquire DB resources */
    
    Statement  stmt = this._createStatement();
    if (stmt == null) return null;
    
    /* perform query */
    
    List<String> values = null;
    ResultSet rs = null;
    try {
      sqllog.info(_sql);
      
      rs = stmt.executeQuery(_sql);
      
      /* loop over results and convert them to records */
      values = new ArrayList<String>(64);
      while (rs.next()) {
        String s = _columnName != null
          ? rs.getString(_columnName) : rs.getString(1);
        if (s != null) values.add(s);
      }
    }
    catch (SQLException e) {
      log.error("could not execute retrieve table names", e);
      this.lastException = e;
    }
    finally {
      // TODO: we might also want to close our channel if the tear down was not
      //       clean
      this._releaseResources(stmt, rs);
    }
    
    if (values == null)
      return null;
    
    return values.toArray(new String[values.size()]);
  }
  
  
  /* description */

  public void appendAttributesToDescription(StringBuffer _d) {
    super.appendAttributesToDescription(_d);
    
    if (this.startTimeInSeconds != 0) {
      Date d = new Date(this.startTimeInSeconds * 1000);
      _d.append(" opened=" + d);
    }
    else
      _d.append(" no-starttime");
    
    if (this.connection == null)
      _d.append(" no-connection");
    
    if (this.lastException != null)
      _d.append(" last-error=" + this.lastException);
  }
}
