/*
 * The contents of this file are subject to the Mozilla Public License Version 1.1
 * (the "License"); you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at <http://www.mozilla.org/MPL/>.
 * 
 * Software distributed under the License is distributed on an "AS IS" basis, WITHOUT
 * WARRANTY OF ANY KIND, either express or implied. See the License for the specific
 * language governing rights and limitations under the License.
 * 
 * The Original Code is the Venice Web Communities System.
 * 
 * The Initial Developer of the Original Code is Eric J. Bowersox <erbo@silcom.com>,
 * for Silverwrist Design Studios.  Portions created by Eric J. Bowersox are
 * Copyright (C) 2001-02 Eric J. Bowersox/Silverwrist Design Studios.  All Rights Reserved.
 * 
 * Contributor(s): 
 */
package com.silverwrist.venice.core.impl;

import java.sql.*;
import java.util.*;
import org.apache.log4j.*;
import com.silverwrist.util.OptionSet;
import com.silverwrist.util.StringUtil;
import com.silverwrist.venice.core.*;
import com.silverwrist.venice.core.internals.*;
import com.silverwrist.venice.db.*;
import com.silverwrist.venice.except.*;
import com.silverwrist.venice.htmlcheck.*;
import com.silverwrist.venice.security.AuditRecord;

class TopicUserContextImpl implements TopicContext
{
  /*--------------------------------------------------------------------------------
   * Static data members
   *--------------------------------------------------------------------------------
   */

  private static Category logger = Category.getInstance(TopicUserContextImpl.class);

  /*--------------------------------------------------------------------------------
   * Attributes
   *--------------------------------------------------------------------------------
   */

  private EnvConference env;                // the enclosing environment
  private int topicid;
  private short topicnum;
  private int creator_uid;
  private int top_message;
  private boolean frozen;
  private boolean archived;
  private java.util.Date created;
  private java.util.Date lastupdate;
  private String name;
  private boolean hidden;
  private int unread;
  private boolean subscribed;
  private boolean deleted = false;
  private HashSet bozo_uids = null;

  /*--------------------------------------------------------------------------------
   * Constructors
   *--------------------------------------------------------------------------------
   */

  protected TopicUserContextImpl(EnvConference env, int topicid, short topicnum, int creator_uid,
				 int top_message, boolean frozen, boolean archived, java.util.Date created,
				 java.util.Date lastupdate, String name, boolean hidden, int unread,
				 boolean subscribed)
  {
    this.env = env;
    this.topicid = topicid;
    this.topicnum = topicnum;
    this.creator_uid = creator_uid;
    this.top_message = top_message;
    this.frozen = frozen;
    this.archived = archived;
    this.created = created;
    this.lastupdate = lastupdate;
    this.name = name;
    this.hidden = hidden;
    this.unread = (unread<0 ? 0 : unread);
    this.subscribed = subscribed;

  } // end constructor

  TopicUserContextImpl(EnvConference env, ReturnTopicInfo inf, String name)
  {
    this.env = env;
    this.topicid = inf.getTopicID();
    this.topicnum = inf.getTopicNum();
    this.creator_uid = env.getUserID();
    this.top_message = 0;
    this.frozen = false;
    this.archived = false;
    this.created = inf.getCreateDate();
    this.lastupdate = inf.getCreateDate();
    this.name = name;
    this.hidden = false;
    this.unread = 1;
    this.subscribed = false;
    this.bozo_uids = new HashSet();  // no bozos yet

  } // end constructor

  /*--------------------------------------------------------------------------------
   * Internal functions
   *--------------------------------------------------------------------------------
   */

  private static final ResultSet queryByTopic(Statement stmt, int topicid, int uid) throws SQLException
  {
    StringBuffer sql =
      new StringBuffer("SELECT topics.topicid, topics.num, topics.creator_uid, topics.top_message, "
		       + "topics.frozen, topics.archived, topics.createdate, topics.lastupdate, "
		       + "topics.name, IFNULL(topicsettings.hidden,0) AS hidden, "
		       + "(topics.top_message - IFNULL(topicsettings.last_message,-1)) AS unread, "
		       + "IFNULL(topicsettings.subscribe,0) AS subscribe "
		       + "FROM topics LEFT JOIN topicsettings ON topics.topicid = topicsettings.topicid "
		       + "AND topicsettings.uid = ");
    sql.append(uid).append(" WHERE topics.topicid = ").append(topicid).append(';');
    if (logger.isDebugEnabled())
      logger.debug("SQL: " + sql.toString());
    return stmt.executeQuery(sql.toString());

  } // end queryByTopic

  private final void makeDeleted()
  {
    top_message = -1;
    frozen = false;
    archived = false;
    created = null;
    lastupdate = null;
    name = null;
    hidden = false;
    unread = -1;
    subscribed = false;
    deleted = true;

  } // end if

  private final void refresh(Connection conn) throws SQLException
  {
    Statement stmt = null;

    try
    { // use database to refresh object
      stmt = conn.createStatement();
    
      // perform a requery of the database
      ResultSet rs = queryByTopic(stmt,topicid,env.getUserID());
      if (rs.next())
      { // update the fields that are capable of changing
	top_message = rs.getInt(4);
	frozen = rs.getBoolean(5);
	archived = rs.getBoolean(6);
	lastupdate = SQLUtil.getFullDateTime(rs,8);
	hidden = rs.getBoolean(10);
	unread = rs.getInt(11);
	if (unread<0)
	  unread = 0;
	subscribed = rs.getBoolean(12);

      } // end if
      else // this topic must have been deleted - fsck it
	makeDeleted();

    } // end try
    finally
    { // shut down the statement to conserve resources
      SQLUtil.shutdown(stmt);

    } // end finally

  } // end refresh

  private final void loadBozo(Connection conn) throws SQLException
  {
    Statement stmt = null;

    try
    { // load the bozo UIDs list
      stmt = conn.createStatement();
      StringBuffer sql = new StringBuffer("SELECT bozo_uid FROM topicbozo WHERE topicid = ");
      sql.append(topicid).append(" AND uid = ").append(env.getUserID()).append(';');
      ResultSet rs = stmt.executeQuery(sql.toString());
      bozo_uids = new HashSet();
      while (rs.next())
	bozo_uids.add(new Integer(rs.getInt(1)));

    } // end try
    finally
    { // shut down the statement to conserve resources
      SQLUtil.shutdown(stmt);

    } // end finally

  } // end loadBozo

  /*--------------------------------------------------------------------------------
   * Implementations from interface TopicContext
   *--------------------------------------------------------------------------------
   */

  public void refresh() throws DataException
  {
    if (logger.isDebugEnabled())
      logger.debug("refreshing topic ID " + String.valueOf(topicid));
    Connection conn = null;      // pooled database connection

    try
    { // get a database connection
      conn = env.getConnection();
      refresh(conn);

    } // end try
    catch (SQLException e)
    { // turn SQLException into data exception
      logger.error("DB error reading topic data: " + e.getMessage(),e);
      throw new DataException("unable to retrieve topic information: " + e.getMessage(),e);

    } // end catch
    finally
    { // make sure we release the connection before we go
      env.releaseConnection(conn);

    } // end finally

  } // end refresh

  public int getTopicID()
  {
    return topicid;

  } // end getTopicID

  public short getTopicNumber()
  {
    return topicnum;

  } // end getTopicNumber

  public String getName()
  {
    return name;

  } // end getName

  public int getUnreadMessages()
  {
    return unread;

  } // end getUnreadMessages

  public int getTotalMessages()
  {
    return top_message + 1;

  } // end getTotalMessages

  public java.util.Date getLastUpdateDate()
  {
    return lastupdate;

  } // end getLastUpdateDate

  public int getCreatorUID()
  {
    return creator_uid;

  } // end getCreatorUID

  public boolean isFrozen()
  {
    return frozen;

  } // end isFrozen

  public boolean isArchived()
  {
    return archived;

  } // end isArchived

  public java.util.Date getCreatedDate()
  {
    return created;

  } // end getCreatedDate

  public boolean isHidden()
  {
    return hidden;

  } // end isHidden

  public boolean isDeleted()
  {
    return deleted;

  } // end isDeleted

  public boolean canFreeze()
  {
    return env.getConference().userCanHide();

  } // end canFreeze

  public boolean canArchive()
  {
    return env.getConference().userCanHide();

  } // end canArchive

  public void setFrozen(boolean flag) throws DataException, AccessError
  {
    if ((frozen==flag) || deleted)
      return;  // no-op
    if (!(env.getConference().userCanHide()))
    { // you can't freeze the topic!
      logger.error("user cannot change frozen status of topic");
      throw new AccessError("You are not permitted to freeze or unfreeze this topic.");

    } // end if

    Connection conn = null;      // pooled database connection
    Statement stmt = null;
    AuditRecord ar = null;       // audit record indicating success

    try
    { // get a database connection
      conn = env.getConnection();
      stmt = conn.createStatement();

      // create the SQL statement to freeze or unfreeze the topic
      StringBuffer sql = new StringBuffer("UPDATE topics SET frozen = ");
      sql.append(flag ? '1' : '0').append(" WHERE topicid = ").append(topicid).append(';');
      if (stmt.executeUpdate(sql.toString())>0)
      { // success! save the flag and generate an audit record
	frozen = flag;
	ar = env.newAudit(AuditRecord.TOPIC_FREEZE,"conf=" + env.getConfID() + ",topic=" + topicid,
			  flag ? "freeze" : "unfreeze");

      } // end if
      else  // somebody else must have deleted this topic
	makeDeleted();

    } // end try
    catch (SQLException e)
    { // turn SQLException into data exception
      logger.error("DB error setting topic data: " + e.getMessage(),e);
      throw new DataException("unable to set topic frozen status: " + e.getMessage(),e);

    } // end catch
    finally
    { // make sure we release the connection before we go
      SQLUtil.shutdown(stmt);
      AuditRecord.store(conn,ar);
      env.releaseConnection(conn);

    } // end finally

  } // end setFrozen

  public void setArchived(boolean flag) throws DataException, AccessError
  {
    if ((archived==flag) || deleted)
      return;  // no-op
    if (!(env.getConference().userCanHide()))
    { // you can't archive the topic!
      logger.error("user cannot change archived status of topic");
      throw new AccessError("You are not permitted to archive or unarchive this topic.");

    } // end if

    Connection conn = null;      // pooled database connection
    Statement stmt = null;
    AuditRecord ar = null;       // audit record indicating success

    try
    { // get a database connection
      conn = env.getConnection();
      stmt = conn.createStatement();

      // create the SQL statement to freeze or unfreeze the topic
      StringBuffer sql = new StringBuffer("UPDATE topics SET archived = ");
      sql.append(flag ? '1' : '0').append(" WHERE topicid = ").append(topicid).append(';');
      if (stmt.executeUpdate(sql.toString())>0)
      { // success! save the flag and generate an audit record
	archived = flag;
	ar = env.newAudit(AuditRecord.TOPIC_ARCHIVE,"conf=" + env.getConfID() + ",topic=" + topicid,
			  flag ? "archive" : "unarchive");

      } // end if
      else  // somebody else must have deleted this topic
	makeDeleted();

    } // end try
    catch (SQLException e)
    { // turn SQLException into data exception
      logger.error("DB error setting topic data: " + e.getMessage(),e);
      throw new DataException("unable to set topic archived status: " + e.getMessage(),e);

    } // end catch
    finally
    { // make sure we release the connection before we go
      SQLUtil.shutdown(stmt);
      AuditRecord.store(conn,ar);
      env.releaseConnection(conn);

    } // end finally

  } // end setArchived

  public void setHidden(boolean flag) throws DataException
  {
    if ((hidden==flag) || deleted || env.getUser().userIsAnonymous())
      return;  // no-op

    Connection conn = null;      // pooled database connection
    Statement stmt = null;

    try
    { // get a database connection
      conn = env.getConnection();
      stmt = conn.createStatement();
      stmt.executeUpdate("LOCK TABLES topicsettings WRITE, topics READ;");

      try
      { // start by trying to see if we can update topicsettings directly
	StringBuffer sql = new StringBuffer("UPDATE topicsettings SET hidden = ");
	sql.append(flag ? '1' : '0').append(" WHERE topicid = ").append(topicid).append(" AND uid = ");
	sql.append(env.getUserID()).append(';');
	if (stmt.executeUpdate(sql.toString())>0)
	{ // that was all we needed - just save the flag and exit
	  hidden = flag;
	  return;

	} // end if

	// OK, check: Is the topic still there?!?
	sql.setLength(0);
	sql.append("SELECT topicid from topics WHERE topicid = ").append(topicid).append(';');
	ResultSet rs = stmt.executeQuery(sql.toString());
	if (!(rs.next()))
	{ // the topic's been deleted - bail out
	  makeDeleted();
	  return;

	} // end if

	// OK, just insert a new row into topicsettings, why dontcha...
	sql.setLength(0);
	sql.append("INSERT INTO topicsettings (topicid, uid, hidden) VALUES (").append(topicid).append(", ");
	sql.append(env.getUserID()).append(", ").append(flag ? '1' : '0').append(");");
	stmt.executeUpdate(sql.toString());
	hidden = flag;  // successful completion

      } // end try
      finally
      { // unlock the tables before we go
	SQLUtil.unlockTables(conn);

      } // end finally

    } // end try
    catch (SQLException e)
    { // turn SQLException into data exception
      logger.error("DB error setting topic data: " + e.getMessage(),e);
      throw new DataException("unable to set topic hidden status: " + e.getMessage(),e);

    } // end catch
    finally
    { // make sure we release the connection before we go
      SQLUtil.shutdown(stmt);
      env.releaseConnection(conn);

    } // end finally

  } // end setHidden

  public int getFirstUnreadMessage()
  {
    if (unread==0)
      return -1;
    else
      return top_message - (unread - 1);

  } // end getFirstUnreadMessage

  public void setUnreadMessages(int count) throws DataException
  {
    if (logger.isDebugEnabled())
      logger.debug("[raw] setUnreadMessages(" + count + ") entry");

    if (env.getUser().userIsAnonymous())
    { // this is effectively a no-op, but log it
      logger.debug("reject 1: anonymous user");
      return;

    } // end if
    if (count>(top_message+1))   // constrain count to [0, top_message+1]
      count = top_message + 1;
    else if (count<0)
      count = 0;
    if ((count==unread) || deleted)
    { // this is effectively a no-op, but log it
      if (logger.isDebugEnabled())
	logger.debug("reject 2: count=" + count + ", unread=" + unread + ", deleted=" + deleted);
      return;

    } // end if

    if (logger.isInfoEnabled())
      logger.info("[cooked] setUnreadMessages(" + count + ") entry");

    int last_msg = top_message - count;
    Connection conn = null;      // pooled database connection
    Statement stmt = null;

    try
    { // get a database connection
      conn = env.getConnection();
      stmt = conn.createStatement();
      stmt.executeUpdate("LOCK TABLES confsettings WRITE, topicsettings WRITE, topics READ;");

      try
      { // start by trying to see if we can update topicsettings directly
	StringBuffer sql = new StringBuffer("UPDATE topicsettings SET last_message = ");
	sql.append(last_msg).append(", last_read = '");
	java.util.Date now = new java.util.Date();
	sql.append(SQLUtil.encodeDate(now)).append("' WHERE topicid = ").append(topicid).append(" AND uid = ");
	sql.append(env.getUserID()).append(';');
	if (logger.isDebugEnabled())
	  logger.debug("SQL: " + sql.toString());
	if (stmt.executeUpdate(sql.toString())>0)
	{ // that was all we needed - just save the flag and exit
	  logger.debug("--> bailed out after update - done with setUnreadMessages{");
	  env.getConference().touchRead(conn);
	  unread = count;
	  return;

	} // end if

	// OK, check: Is the topic still there?!?
	sql.setLength(0);
	sql.append("SELECT topicid from topics WHERE topicid = ").append(topicid).append(';');
	if (logger.isDebugEnabled())
	  logger.debug("SQL: " + sql.toString());
	ResultSet rs = stmt.executeQuery(sql.toString());
	if (!(rs.next()))
	{ // the topic's been deleted - bail out
	  logger.debug("--> bailed out because topic is deleted - done with setUnreadMessages{");
	  makeDeleted();
	  return;

	} // end if

	SQLUtil.shutdown(rs);

	// OK, just insert a new row into topicsettings, why dontcha...
	sql.setLength(0);
	sql.append("INSERT INTO topicsettings (topicid, uid, last_message, last_read) VALUES (");
	sql.append(topicid).append(", ").append(env.getUserID()).append(", ").append(last_msg).append(", '");
	sql.append(SQLUtil.encodeDate(now)).append("');");
	if (logger.isDebugEnabled())
	  logger.debug("SQL: " + sql.toString());
	stmt.executeUpdate(sql.toString());
	env.getConference().touchRead(conn);
	unread = count;  // successful completion

      } // end try
      finally
      { // unlock the tables before we go
	SQLUtil.unlockTables(conn);

      } // end finally

    } // end try
    catch (SQLException e)
    { // turn SQLException into data exception
      logger.error("DB error setting topic user data: " + e.getMessage(),e);
      throw new DataException("unable to set unread messages: " + e.getMessage(),e);

    } // end catch
    finally
    { // make sure we release the connection before we go
      SQLUtil.shutdown(stmt);
      env.releaseConnection(conn);

    } // end finally

  } // end setUnreadMessages

  public void fixSeen() throws DataException
  {
    setUnreadMessages(0);

  } // end fixSeen

  public List getMessages(int low, int high) throws DataException, AccessError
  {
    if (!(env.getConference().userCanRead()))
    { // they can't read messages in this topic!
      logger.error("trying to read postings w/o permission!");
      throw new AccessError("You do not have permission to read messages in this conference.");

    } // end if

    // reorder parameters so they come in the correct order!
    if (low<=high)
      return TopicMessageUserContextImpl.loadMessageRange(env,topicid,low,high);
    else
      return TopicMessageUserContextImpl.loadMessageRange(env,topicid,high,low);

  } // end getMessages

  public TopicMessageContext getMessage(int number) throws DataException, AccessError
  {
    if (!(env.getConference().userCanRead()))
    { // they can't read messages in this topic!
      logger.error("trying to read postings w/o permission!");
      throw new AccessError("You do not have permission to read messages in this conference.");

    } // end if

    // pass down to one of our static functiions to return this
    return TopicMessageUserContextImpl.loadMessage(env,topicid,number);

  } // end getMessage

  public TopicMessageContext postNewMessage(long parent, String pseud, String text, int mode)
      throws DataException, AccessError
  {
    if (logger.isInfoEnabled())
      logger.info("postNewMessage(" + parent + ", '" + pseud + "',<text>," + mode + ") entry");

    if ((mode!=POST_MODE_NORMAL) && (mode!=POST_MODE_EMAIL))
      throw new IllegalArgumentException("invalid mode parameter");

    if (!(env.getConference().userCanPost()))
    { // they can't post in this topic!
      logger.error("trying to post w/o permission!");
      throw new AccessError("You do not have permission to post messages in this conference.");

    } // end if

    if (deleted)
    { // the topic has been deleted
      logger.error("can't post to a deleted topic!");
      throw new AccessError("You cannot post to a topic that has been deleted.");

    } // end if

    if (frozen && !(env.getConference().userCanHide()))
    { // can't post to a frozen topic!
      logger.error("can't post to a frozen topic!");
      throw new AccessError("The topic is frozen, and you do not have permission to post to it.");

    } // end if

    if (archived && !(env.getConference().userCanHide()))
    { // can't post to a frozen topic!
      logger.error("can't post to an archived topic!");
      throw new AccessError("The topic is archived, and you do not have permission to post to it.");

    } // end if

    // figure out which HTML checkers to use
    int pseud_ch_index = EngineBackend.HTMLC_POST_PSEUD;
    int body_ch_index = EngineBackend.HTMLC_POST_BODY;
    if (mode==POST_MODE_NORMAL)
    { // configure for normal posting
      pseud_ch_index = EngineBackend.HTMLC_POST_PSEUD;
      body_ch_index = EngineBackend.HTMLC_POST_BODY;

    } // end if
    else if (mode==POST_MODE_EMAIL)
    { // configure for E-mail posting
      pseud_ch_index = EngineBackend.HTMLC_POST_PSEUD;
      body_ch_index = EngineBackend.HTMLC_POST_BODY_EMAIL;

    } // end else if

    // preprocess the two arguments through HTML checkers
    HTMLChecker pseud_ch = env.getEngine().createCheckerObject(pseud_ch_index);
    HTMLChecker text_ch = env.getEngine().createCheckerObject(body_ch_index);
    text_ch.setContextValue("PostLinkDecoderContext",env.getConference().createDecoderContext(topicnum));
    try
    { // run both arguments through the HTML checker
      pseud_ch.append(pseud);
      pseud_ch.finish();
      text_ch.append(text);
      text_ch.finish();

    } // end try
    catch (AlreadyFinishedException e)
    { // this isn't right...
      throw new InternalStateError("HTMLChecker erroneously throwing AlreadyFinishedException",e);

    } // end catch

    String real_pseud, real_text;
    int text_linecount;
    try
    { // retrieve the processed values
      real_pseud = pseud_ch.getValue();
      real_text = text_ch.getValue();
      text_linecount = text_ch.getLines();
      if (logger.isDebugEnabled())
	logger.debug("new post has " + text_linecount + " lines");

    } // end try
    catch (NotYetFinishedException e)
    { // this isn't right either!
      throw new InternalStateError("HTMLChecker erroneously throwing NotYetFinishedException",e);

    } // end catch

    int new_post_num;
    long new_post_id;
    java.util.Date posted_date;
    Connection conn = null;
    Statement stmt = null;
    AuditRecord ar = null;
    ArrayList mailto_addrs = null;

    try
    { // get a database connection
      conn = env.getConnection();
      stmt = conn.createStatement();
      ArrayList mailto_uids = null;
      StringBuffer sql = new StringBuffer();
      ResultSet rs;

      // slap a lock on all the tables we need to touch
      stmt.executeUpdate("LOCK TABLES confs WRITE, topics WRITE, posts WRITE, postdata WRITE, "
			 + "confsettings WRITE, topicsettings WRITE;");

      try
      { // refresh our current status and recheck allowed status
	refresh(conn);
	if (deleted)
	{ // the topic has been deleted
	  logger.error("can't post to a deleted topic!");
	  throw new AccessError("You cannot post to a topic that has been deleted.");

	} // end if

	if (frozen && !(env.getConference().userCanHide()))
	{ // can't post to a frozen topic!
	  logger.error("can't post to a frozen topic!");
	  throw new AccessError("The topic is frozen, and you do not have permission to post to it.");

	} // end if

	if (archived && !(env.getConference().userCanHide()))
	{ // can't post to a frozen topic!
	  logger.error("can't post to an archived topic!");
	  throw new AccessError("The topic is archived, and you do not have permission to post to it.");

	} // end if

	// Determine what the new post number is.
	new_post_num = top_message + 1;
	if (logger.isDebugEnabled())
	  logger.debug("New post number: " + new_post_num);

	// Add the post "header" to the posts table.
	sql.append("INSERT INTO posts (parent, topicid, num, linecount, creator_uid, posted, pseud) VALUES (");
	sql.append(parent).append(", ").append(topicid).append(", ").append(new_post_num).append(", ");
	sql.append(text_linecount).append(", ").append(env.getUserID()).append(", '");
	posted_date = new java.util.Date();
	sql.append(SQLUtil.encodeDate(posted_date)).append("', '").append(real_pseud).append("');");
	if (logger.isDebugEnabled())
	  logger.debug("SQL: " + sql.toString());
	stmt.executeUpdate(sql.toString());

	// Retrieve the new post ID.
	rs = stmt.executeQuery("SELECT LAST_INSERT_ID();");
	if (!(rs.next()))
	  throw new InternalStateError("postNewMessage(): Unable to get new post ID!");
	new_post_id = rs.getLong(1);
	if (logger.isDebugEnabled())
	  logger.debug("New post ID: " + new_post_id);
	SQLUtil.shutdown(rs);
   
	// Touch the topic values to reflect the added post.
	sql.setLength(0);
	sql.append("UPDATE topics SET top_message = ").append(new_post_num).append(", lastupdate = '");
	sql.append(SQLUtil.encodeDate(posted_date)).append("' WHERE topicid = ").append(topicid).append(';');
	if (logger.isDebugEnabled())
	  logger.debug("SQL: " + sql.toString());
	stmt.executeUpdate(sql.toString());

	// insert the post data
	sql.setLength(0);
	sql.append("INSERT INTO postdata (postid, data) VALUES (").append(new_post_id).append(", '");
	sql.append(real_text).append("');");
	stmt.executeUpdate(sql.toString());

	// mark that we posted to the topic
	sql.setLength(0);
	sql.append("UPDATE topicsettings SET last_post = '").append(SQLUtil.encodeDate(posted_date));
	sql.append("' WHERE topicid = ").append(topicid).append(" AND uid = ").append(env.getUserID());
	sql.append(';');
	if (logger.isDebugEnabled())
	  logger.debug("SQL: " + sql.toString());
	if (stmt.executeUpdate(sql.toString())<1)
	{ // we had no topicsettings record, add one
	  sql.setLength(0);
	  sql.append("INSERT INTO topicsettings (topicid, uid, last_post) VALUES (").append(topicid);
	  sql.append(", ").append(env.getUserID()).append(", '").append(SQLUtil.encodeDate(posted_date));
	  sql.append("');");
	  if (logger.isDebugEnabled())
	    logger.debug("SQL: " + sql.toString());
	  stmt.executeUpdate(sql.toString());

	} // end if

	// mark that we posted to the conference
	env.getConference().touchUpdate(conn,posted_date);
	env.getConference().touchPost(conn,posted_date);

	// Who's subscribed to this conference?  Whoever it is, they need to get this post via E-mail.
	sql.setLength(0);
	sql.append("SELECT uid FROM topicsettings WHERE topicid = ").append(topicid);
	sql.append(" AND subscribe = 1;");
	if (logger.isDebugEnabled())
	  logger.debug("SQL: " + sql.toString());
	rs = stmt.executeQuery(sql.toString());
	while (rs.next())
	{ // load the UIDs here
	  if (mailto_uids==null)
	    mailto_uids = new ArrayList();
	  mailto_uids.add(new Integer(rs.getInt(1)));

	} // end while

	SQLUtil.shutdown(rs);

	// Fill in our own local variables to reflect the update.  This includes the recalculation
	// of "unread" based on the new value of "top_message".
        int tmp_last_msg = top_message - unread;
	top_message = new_post_num;
	lastupdate = posted_date;
	unread = top_message - tmp_last_msg;

	logger.debug("message post completed successfully");

      } // end try
      finally
      { // make sure we unlock the tables when we're done
	SQLUtil.unlockTables(conn);

      } // end finally

      // record what we did in an audit record
      ar = env.newAudit(AuditRecord.POST_MESSAGE,"conf=" + env.getConfID() + ",topic=" + topicid + ",post="
			+ new_post_id,"pseud=" + real_pseud);

      if (mailto_uids!=null)
      { // We need to translate the "mailto" UIDs to E-mail addresses while we still have the database open!
	mailto_addrs = new ArrayList(mailto_uids.size());
	sql.setLength(0);
	sql.append("SELECT c.email FROM users u, contacts c WHERE u.contactid = c.contactid AND u.uid ");
	if (mailto_uids.size()==1)
	  sql.append("= ").append(mailto_uids.get(0)).append(';');
	else
	  sql.append("IN (").append(StringUtil.join(mailto_uids,", ")).append(");");
	if (logger.isDebugEnabled())
	  logger.debug("SQL: " + sql.toString());
	rs = stmt.executeQuery(sql.toString());
	while (rs.next())
	  mailto_addrs.add(rs.getString(1));

      } // end if

    } // end try
    catch (SQLException e)
    { // turn SQLException into data exception
      logger.error("DB error posting message: " + e.getMessage(),e);
      throw new DataException("unable to post message: " + e.getMessage(),e);

    } // end catch
    finally
    { // make sure we release the connection before we go
      SQLUtil.shutdown(stmt);
      AuditRecord.store(conn,ar);
      env.releaseConnection(conn);

    } // end finally

    if (mailto_addrs!=null)
    { // a copy of the post needs to be delivered via E-mail to the specified addresses
      // use our PostDeliveryAgent to do it in the background
      PostDeliveryAgent agent = new PostDeliveryAgent(env,text,pseud,name,topicnum,mailto_addrs);
      agent.start();

    } // end if
    // else don't bother - it would be a waste of time

    // return the new message context
    return new TopicMessageUserContextImpl(env,new_post_id,parent,topicid,new_post_num,text_linecount,
					   env.getUserID(),posted_date,real_pseud);

  } // end postNewMessage

  public TopicMessageContext postNewMessage(long parent, String pseud, String text)
      throws DataException, AccessError
  {
    return this.postNewMessage(parent,pseud,text,POST_MODE_NORMAL);

  } // end postNewMessage

  public HTMLChecker getPreviewChecker()
  {
    HTMLChecker rc = env.getEngine().createCheckerObject(EngineBackend.HTMLC_PREVIEW_BODY);
    rc.setContextValue("PostLinkDecoderContext",env.getConference().createDecoderContext(topicnum));
    return rc;
    
  } // end getPreviewChecker

  public boolean canDelete()
  {
    return env.getConference().userCanNuke();

  } // end canDelete

  public void delete() throws DataException, AccessError
  {
    if (!(env.getConference().userCanNuke()))
    { // we can't delete the topic!
      logger.error("trying to delete w/o permission!");
      throw new AccessError("You do not have permission to delete this topic.");

    } // end if

    if (deleted)
      return;  // an exercise in futility...

    Connection conn = null;
    Statement stmt = null;
    AuditRecord ar = null;
    int post_count;
    long post_max;

    try
    { // get a database connection
      conn = env.getConnection();
      stmt = conn.createStatement();

      // lock some tables while we do the critical parts of the delete
      stmt.executeUpdate("LOCK TABLES confs WRITE, topics WRITE, topicsettings WRITE, topicbozo WRITE, "
			 + "posts READ;");
      try
      { // first delete the topic record itself
	StringBuffer sql = new StringBuffer("DELETE FROM topics WHERE topicid = ");
	sql.append(topicid).append(';');
	stmt.executeUpdate(sql.toString());

	// now delete all topicsettings records
	sql.setLength(0);
	sql.append("DELETE FROM topicsettings WHERE topicid = ").append(topicid).append(';');
	stmt.executeUpdate(sql.toString());

	// and all topicbozo records
	sql.setLength(0);
	sql.append("DELETE FROM topicbozo WHERE topicid = ").append(topicid).append(';');
	stmt.executeUpdate(sql.toString());

	// and indicate that we updated the conference
	env.getConference().touchUpdate(conn,new java.util.Date());

	// determine the number of posts in this topic, and the maximum post ID
	sql.setLength(0);
	sql.append("SELECT COUNT(*), MAX(postid) FROM posts WHERE topicid = ").append(topicid).append(';');
	ResultSet rs = stmt.executeQuery(sql.toString());
	if (!(rs.next()))
	  throw new InternalStateError("TopicContext.delete screwup on post SELECT");
	post_count = rs.getInt(1);
	post_max = rs.getLong(2);
	
	// and record that we're now deleted
	makeDeleted();
    
      } // end try
      finally
      { // make sure and unlock before we're done
	SQLUtil.unlockTables(conn);

      } // end finally

      // record what we did in an audit record
      ar = env.newAudit(AuditRecord.DELETE_TOPIC,"conf=" + env.getConfID() + ",topic=" + topicid);

    } // end try
    catch (SQLException e)
    { // turn SQLException into data exception
      logger.error("DB error deleting topic: " + e.getMessage(),e);
      throw new DataException("unable to delete topic: " + e.getMessage(),e);

    } // end catch
    finally
    { // make sure we release the connection before we go
      SQLUtil.shutdown(stmt);
      AuditRecord.store(conn,ar);
      env.releaseConnection(conn);

    } // end finally

    // Delete the rest of the gunk in the background; spin off another thread to handle it.
    BackgroundTopicPurge purger = new BackgroundTopicPurge(env,topicid,post_count,post_max);
    Thread thrd = new Thread(purger);
    thrd.setPriority(Thread.NORM_PRIORITY-1);
    thrd.start();

  } // end delete

  public void deleteTopic() throws DataException, AccessError
  {
    this.delete();

  } // end deleteTopic

  public List getActivePosters(int skip, int limit) throws DataException
  {
    Connection conn = null;
    Statement stmt = null;
    ArrayList rc = new ArrayList();

    try
    { // retrieve a connection
      conn = env.getConnection();
      stmt = conn.createStatement();

      // create the SQL statement to retrieve all posters
      StringBuffer sql =
          new StringBuffer("SELECT s.uid, u.username, s.last_read, s.last_post FROM topicsettings s, "
			   + "users u WHERE u.uid = s.uid AND s.topicid = ");
      sql.append(topicid).append(" AND u.is_anon = 0 AND ISNULL(s.last_post) = 0 ORDER BY s.last_post DESC");
      if ((skip>=0) && (limit>0))
	sql.append(" LIMIT ").append(skip).append(", ").append(limit);
      sql.append(';');

      // execute the statement
      ResultSet rs = stmt.executeQuery(sql.toString());

      while (rs.next())
      { // return all the records as ActiveUser data elements
	ActiveUser usr = new ActiveUserImpl(rs.getInt(1),rs.getString(2),SQLUtil.getFullDateTime(rs,3),
					    SQLUtil.getFullDateTime(rs,4));
	rc.add(usr);

      } // end while

    } // end try
    catch (SQLException e)
    { // this becomes a DataException
      logger.error("DB error getting active poster list: " + e.getMessage(),e);
      throw new DataException("unable to get active poster listing: " + e.getMessage(),e);

    } // end catch
    finally
    { // make sure we release the connection before we go
      SQLUtil.shutdown(stmt);
      env.releaseConnection(conn);

    } // end finally

    return Collections.unmodifiableList(rc);

  } // end getActivePosters

  public List getActivePosters(int limit) throws DataException, AccessError
  {
    return getActivePosters(0,limit);

  } // end getActivePosters

  public List getActivePosters() throws DataException, AccessError
  {
    return getActivePosters(-1,-1);

  } // end getActivePosters

  public List getActiveReaders(int skip, int limit) throws DataException, AccessError
  {
    Connection conn = null;
    Statement stmt = null;
    ArrayList rc = new ArrayList();

    try
    { // retrieve a connection
      conn = env.getConnection();
      stmt = conn.createStatement();

      // create the SQL statement to retrieve all readers
      StringBuffer sql =
          new StringBuffer("SELECT s.uid, u.username, s.last_read, s.last_post FROM topicsettings s, "
			   + "users u WHERE u.uid = s.uid AND s.topicid = ");
      sql.append(topicid).append(" AND u.is_anon = 0 AND ISNULL(s.last_read) = 0 ORDER BY s.last_read DESC");
      if ((skip>=0) && (limit>0))
	sql.append(" LIMIT ").append(skip).append(", ").append(limit);
      sql.append(';');

      // execute the statement
      ResultSet rs = stmt.executeQuery(sql.toString());

      while (rs.next())
      { // return all the records as ActiveUser data elements
	ActiveUser usr = new ActiveUserImpl(rs.getInt(1),rs.getString(2),SQLUtil.getFullDateTime(rs,3),
					    SQLUtil.getFullDateTime(rs,4));
	rc.add(usr);

      } // end while

    } // end try
    catch (SQLException e)
    { // this becomes a DataException
      logger.error("DB error getting active reader list: " + e.getMessage(),e);
      throw new DataException("unable to get active reader listing: " + e.getMessage(),e);

    } // end catch
    finally
    { // make sure we release the connection before we go
      SQLUtil.shutdown(stmt);
      env.releaseConnection(conn);

    } // end finally

    return Collections.unmodifiableList(rc);

  } // end getActiveReaders

  public List getActiveReaders(int limit) throws DataException, AccessError
  {
    return getActiveReaders(0,limit);

  } // end getActiveReaders

  public List getActiveReaders() throws DataException, AccessError
  {
    return getActiveReaders(-1,-1);

  } // end getActiveReaders

  public boolean isBozo(int other_uid) throws DataException
  {
    if (deleted || env.getUser().userIsAnonymous() || (other_uid==env.getUserID()))
      return false;  // no-op

    if (bozo_uids==null)
    { // the bozo UIDs need to be loaded!
      Connection conn = null;
      try
      { // load the bozo filter UIDs from the database
	conn = env.getConnection();
	loadBozo(conn);

      } // end try
      catch (SQLException e)
      { // this becomes a DataException
	logger.error("DB error getting bozo list: " + e.getMessage(),e);
	throw new DataException("unable to get bozo filter listing: " + e.getMessage(),e);

      } // end catch
      finally
      { // make sure we release the connection before we go
	env.releaseConnection(conn);

      } // end finally

    } // end if

    return bozo_uids.contains(new Integer(other_uid));

  } // end isBozo

  public void setBozo(int other_uid, boolean bozo) throws DataException
  {
    if (deleted || env.getUser().userIsAnonymous() || (other_uid==env.getUserID()))
      return;  // no-op

    Connection conn = null;
    Statement stmt = null;
    try
    { // figure out what to do here
      conn = env.getConnection();
      refresh(conn);
      if (deleted)
	return;          // one more check to make sure we're not deleted
      if (bozo_uids==null)
	loadBozo(conn);  // load the bozo filter if we don't already have it

      Integer uid_key = new Integer(other_uid);
      StringBuffer sql;
      if (bozo)
      { // this user is a bozo...
	if (!(bozo_uids.contains(uid_key)))
	{ // add them to the bozo list
	  sql = new StringBuffer("INSERT INTO topicbozo (topicid, uid, bozo_uid) VALUES (");
	  sql.append(topicid).append(", ").append(env.getUserID()).append(", ").append(other_uid).append(");");
	  stmt = conn.createStatement();
	  stmt.executeUpdate(sql.toString());
	  bozo_uids.add(uid_key);

	} // end if
	// else this is a no-op

      } // end if
      else
      { // this user is no longer a bozo
	if (bozo_uids.contains(uid_key))
	{ // remove them from the bozo list
	  sql = new StringBuffer("DELETE FROM topicbozo WHERE topicid = ");
	  sql.append(topicid).append(" AND uid = ").append(env.getUserID()).append(" AND bozo_uid = ");
	  sql.append(other_uid).append(';');
	  stmt = conn.createStatement();
	  stmt.executeUpdate(sql.toString());
	  bozo_uids.remove(uid_key);

	} // end if
	// else this is a no-op

      } // end else

    } // end try
    catch (SQLException e)
    { // this becomes a DataException
      logger.error("DB error updating bozo list: " + e.getMessage(),e);
      throw new DataException("unable to update bozo filter list: " + e.getMessage(),e);

    } // end catch
    finally
    { // make sure we release the connection before we go
      SQLUtil.shutdown(stmt);
      env.releaseConnection(conn);

    } // end finally

  } // end setBozo

  public boolean canSetBozo(int other_uid)
  {
    // 1. You can't set bozo filters on a deleted topic.
    // 2. You can't set bozo filters if you're the anonymous user.
    // 3. You can't bozo-filter yourself, silly.
    return !(deleted || env.getUser().userIsAnonymous() || (other_uid==env.getUserID()));

  } // end canSetBozoFilter

  public List getBozos() throws DataException
  {
    if (deleted || env.getUser().userIsAnonymous())
      return Collections.EMPTY_LIST;  // no-op

    if (bozo_uids==null)
    { // the bozo UIDs need to be loaded!
      Connection conn = null;
      try
      { // load the bozo filter UIDs from the database
	conn = env.getConnection();
	loadBozo(conn);

      } // end try
      catch (SQLException e)
      { // this becomes a DataException
	logger.error("DB error getting bozo list: " + e.getMessage(),e);
	throw new DataException("unable to get bozo filter listing: " + e.getMessage(),e);

      } // end catch
      finally
      { // make sure we release the connection before we go
	env.releaseConnection(conn);

      } // end finally

    } // end if

    return Collections.unmodifiableList(new ArrayList(bozo_uids));

  } // end getBozos

  public boolean isSubscribed()
  {
    return subscribed;

  } // end isSubscribed

  public void setSubscribed(boolean flag) throws DataException
  {
    if ((subscribed==flag) || deleted || env.getUser().userIsAnonymous())
      return;  // no-op

    Connection conn = null;      // pooled database connection
    Statement stmt = null;

    try
    { // get a database connection
      conn = env.getConnection();
      stmt = conn.createStatement();
      stmt.executeUpdate("LOCK TABLES topicsettings WRITE, topics READ;");

      try
      { // start by trying to see if we can update topicsettings directly
	StringBuffer sql = new StringBuffer("UPDATE topicsettings SET subscribe = ");
	sql.append(flag ? '1' : '0').append(" WHERE topicid = ").append(topicid).append(" AND uid = ");
	sql.append(env.getUserID()).append(';');
	if (stmt.executeUpdate(sql.toString())>0)
	{ // that was all we needed - just save the flag and exit
	  subscribed = flag;
	  return;

	} // end if

	// OK, check: Is the topic still there?!?
	sql.setLength(0);
	sql.append("SELECT topicid from topics WHERE topicid = ").append(topicid).append(';');
	ResultSet rs = stmt.executeQuery(sql.toString());
	if (!(rs.next()))
	{ // the topic's been deleted - bail out
	  makeDeleted();
	  return;

	} // end if

	SQLUtil.shutdown(rs);

	// OK, just insert a new row into topicsettings, why dontcha...
	sql.setLength(0);
	sql.append("INSERT INTO topicsettings (topicid, uid, subscribe) VALUES (").append(topicid);
	sql.append(", ").append(env.getUserID()).append(", ").append(flag ? '1' : '0').append(");");
	stmt.executeUpdate(sql.toString());
	subscribed = flag;  // successful completion

      } // end try
      finally
      { // unlock the tables before we go
	SQLUtil.unlockTables(conn);

      } // end finally

    } // end try
    catch (SQLException e)
    { // turn SQLException into data exception
      logger.error("DB error setting topic data: " + e.getMessage(),e);
      throw new DataException("unable to set topic subscribed status: " + e.getMessage(),e);

    } // end catch
    finally
    { // make sure we release the connection before we go
      SQLUtil.shutdown(stmt);
      env.releaseConnection(conn);

    } // end finally

  } // end setSubscribed

  public void sendInvitation(String address, String personal_message)
      throws AccessError, DataException, EmailException
  {
    if (!(env.getCommunity().selfCommunity().canSendInvitation()))
      throw new AccessError("You are not permitted to send a topic invitation.");

    // Format the added "conference name" message.
    HashMap vars = new HashMap();
    vars.put("community.name",env.getCommunityName());
    vars.put("conference.name",env.getConferenceName());
    vars.put("topic.name",name);
    String extra_msg = env.getEngine().getStockMessage("add-topic-invite");
    String msg = StringUtil.replaceAllVariables(extra_msg,vars);
    if (!StringUtil.isStringEmpty(personal_message))
      msg += ("\n\n" + personal_message);

    // now send it as if it were a community invitation
    env.getCommunity().selfCommunity().sendInvitation(address,msg);

  } // end sendInvitation

  public boolean canSendInvitation()
  {
    return env.getCommunity().selfCommunity().canSendInvitation();

  } // end canSendInvitation

  public void sendMailToParticipants(boolean posters, int day_limit, String subject, String text)
      throws AccessError, DataException
  {
    if (logger.isDebugEnabled())
      logger.debug("sendMailToParticipants(" + posters + "," + day_limit + ")");
    if (!(env.testPermission("Conference.EMailParticipants")))
      throw new AccessError("You are not permitted to send E-mail to conference participants.");

    if (day_limit==0)
      return;  // no-op
    java.util.Date stop_point = null;
    if (day_limit>0)
    { // compute stop point as a number of days back from this instant
      Calendar right_now = Calendar.getInstance();
      right_now.add(Calendar.DAY_OF_MONTH,-day_limit);
      stop_point = right_now.getTime();
      
    } // end if

    Connection conn = null;
    Statement stmt = null;
    ArrayList rc = new ArrayList();

    try
    { // retrieve a connection
      conn = env.getConnection();
      stmt = conn.createStatement();

      // Build the SQL statement.
      StringBuffer sql = new StringBuffer("SELECT c.email, ");
      if (posters)
	sql.append("s.last_post");
      else
	sql.append("s.last_read");
      sql.append(" FROM contacts c, users u, topicsettings s, propuser p WHERE c.contactid = u.contactid "
		 + "AND u.uid = s.uid AND s.topicid = ").append(topicid);
      sql.append(" AND u.is_anon = 0 AND u.uid = p.uid AND p.ndx = ").append(UserContextImpl.PROP_FLAGS);
      sql.append(" AND p.data NOT LIKE '%");
      sql.append(OptionSet.getOptionChar(UserContextImpl.BF_MASSMAIL_OPTOUT)).append("%' AND ");
      if (posters)
	sql.append("ISNULL(s.last_post) = 0 ORDER BY s.last_post DESC;");
      else
	sql.append("ISNULL(s.last_read) = 0 ORDER BY s.last_read DESC;");
      if (logger.isDebugEnabled())
	logger.debug("SQL: " + sql.toString());

      // Execute the query and start reading E-mail addresses.
      ResultSet rs = stmt.executeQuery(sql.toString());
      while (rs.next())
      { // accumulate E-mail addresses
	if (stop_point!=null)
	{ // see if we can stop the loop yet
	  java.util.Date x = SQLUtil.getFullDateTime(rs,2);
	  if (stop_point.compareTo(x)>0)
	    break;

	} // end if

	rc.add(rs.getString(1));  // save off the E-mail address

      } // end while

      if (logger.isDebugEnabled())
	logger.debug("Selected " + rc.size() + " address(es) for E-mailing");

    } // end try
    catch (SQLException e)
    { // this becomes a DataException
      logger.error("DB error getting active poster list: " + e.getMessage(),e);
      throw new DataException("unable to get active poster listing: " + e.getMessage(),e);

    } // end catch
    finally
    { // make sure we release the connection before we go
      SQLUtil.shutdown(stmt);
      env.releaseConnection(conn);

    } // end finally

    if (rc.size()>0)
    { // prepare the E-mail message text; append the disKlaimer at the end
      HashMap vars = new HashMap();
      vars.put("community.name",env.getCommunityName());
      vars.put("conference.name",env.getConferenceName());
      vars.put("topic.name",name);
      String disklaimer =
	  env.getStockMessage(posters ? "mass-topic-notice-poster" : "mass-topic-notice-reader");
      StringBuffer buf = new StringBuffer(text);
      buf.append("\n\n").append(StringUtil.replaceAllVariables(disklaimer,vars));
      
      // send the actual E-mail messages in the background
      MailerAgent agent = new MailerAgent(env,env.getUser().realUserName(),env.getUser().realEmailAddress(),
					  rc,subject,buf.toString());
      agent.start();

    } // end if

  } // end sendMailToParticipants

  public List searchPosts(String search_terms, int offset, int count) throws AccessError, DataException
  {
    if (logger.isDebugEnabled())
      logger.debug("searchPosts('" + search_terms + "', " + offset + ", " + count + ") entry");
    if (search_terms==null)
      throw new NullPointerException("invalid search terms");
    if (offset<0)
      throw new IllegalArgumentException("invalid offset parameter");
    if (count<=0)
      throw new IllegalArgumentException("invalid count parameter");
    if (!(env.getConference().userCanRead()))
      throw new AccessError("You are not permitted to search for posts within this topic.");

    Connection conn = null;
    Statement stmt = null;
    ArrayList rc = new ArrayList();

    try
    { // get a database connection
      conn = env.getConnection();
      TopicMessageFoundHelper helper = new TopicMessageFoundHelper(conn);
      stmt = conn.createStatement();

      // create the SQL statement
      StringBuffer sql =
	  new StringBuffer("SELECT p.postid, p.num, p.creator_uid, p.posted, p.linecount, d.data "
			   + "FROM posts p, postdata d WHERE p.topicid = ");
      sql.append(topicid);
      sql.append(" AND p.scribble_uid IS NULL AND d.postid = p.postid AND MATCH (d.data) AGAINST (");
      sql.append(SQLUtil.encodeStringArg(search_terms)).append(") LIMIT ").append(offset).append(", ");
      sql.append(count+1).append(';');

      // execute the query and load the results into the return arraylist
      ResultSet rs = stmt.executeQuery(sql.toString());
      while (rs.next())
	rc.add(new TopicMessageFoundImpl(helper,env.getCommunityID(),env.getConfID(),topicid,topicnum,
					 rs.getLong(1),rs.getInt(2),rs.getInt(3),
					 SQLUtil.getFullDateTime(rs,4),rs.getInt(5),rs.getString(6)));

    } // end try
    catch (SQLException e)
    { // this becomes a DataException
      logger.error("DB error searching for posts: " + e.getMessage(),e);
      throw new DataException("unable to search: " + e.getMessage(),e);

    } // end catch
    finally
    { // make sure we release the connection before we go
      SQLUtil.shutdown(stmt);
      env.releaseConnection(conn);

    } // end finally

    if (rc.isEmpty())
      return Collections.EMPTY_LIST;
    else
      return Collections.unmodifiableList(rc);

  } // end searchPosts

  public int getSearchPostCount(String search_terms) throws AccessError, DataException
  {
    if (logger.isDebugEnabled())
      logger.debug("getSearchPostCount('" + search_terms + ") entry");
    if (search_terms==null)
      throw new NullPointerException("invalid search terms");
    if (!(env.getConference().userCanRead()))
      throw new AccessError("You are not permitted to search for posts within this topic.");

    Connection conn = null;
    Statement stmt = null;

    try
    { // get a database connection
      conn = env.getConnection();
      stmt = conn.createStatement();

      // create the SQL statement
      StringBuffer sql =
	  new StringBuffer("SELECT COUNT(*) FROM posts p, postdata d WHERE p.topicid = ");
      sql.append(topicid);
      sql.append(" AND p.scribble_uid IS NULL AND d.postid = p.postid AND MATCH (d.data) AGAINST (");
      sql.append(SQLUtil.encodeStringArg(search_terms)).append(");");

      // execute the query and load the results into the return arraylist
      ResultSet rs = stmt.executeQuery(sql.toString());
      if (!(rs.next()))
	throw new InternalStateError("query failure in TopicUserContextImpl.getSearchPostCount!");
      return rs.getInt(1);

    } // end try
    catch (SQLException e)
    { // this becomes a DataException
      logger.error("DB error searching for posts: " + e.getMessage(),e);
      throw new DataException("unable to search: " + e.getMessage(),e);

    } // end catch
    finally
    { // make sure we release the connection before we go
      SQLUtil.shutdown(stmt);
      env.releaseConnection(conn);

    } // end finally

  } // end getSearchPostCount

  public ConferenceContext getEnclosingConference()
  {
    return env.getConference().selfConference();

  } // end getEnclosingConference

  public String getPostLink() throws DataException
  {
    if (deleted)
      return null;
    return env.getConference().selfConference().getPostLink() + "." + topicnum;

  } // end getPostLink

  /*--------------------------------------------------------------------------------
   * External operations usable only from within the package
   *--------------------------------------------------------------------------------
   */

  static final List getTopicList(EnvConference env, int get_option, int sort_option) throws DataException
  {
    if (logger.isDebugEnabled())
      logger.debug("getTopicList for conf # " + env.getConfID() + ", user #" + env.getUserID());
    ArrayList rc = new ArrayList();    // return from this function
    Connection conn = null;      // pooled database connection
    Statement stmt = null;

    try
    { // get a database connection
      conn = env.getConnection();
      stmt = conn.createStatement();

      // Figure out what the "where" clause of the SQL statement will be.  This is in addition to a
      // WHERE t.confid = (confid) that is always present.  (Some of these WHERE specifiers refer to
      // computed fields of the SELECT.)
      String where_clause;
      switch (get_option)
      {
	case ConferenceContext.GET_ALL:
	  where_clause = null;   // no filtering options
	  break;

	case ConferenceContext.DISPLAY_NEW:
          // only non-hidden topics w/unread messages
	  where_clause = "IFNULL(s.hidden,0) = 0 AND t.top_message > IFNULL(s.last_message,-1)";
	  break;

	case ConferenceContext.DISPLAY_ACTIVE:
	  where_clause = "t.archived = 0 AND IFNULL(s.hidden,0) = 0";  // only non-hidden, non-archived topics
	  break;

	case ConferenceContext.DISPLAY_ALL:
	  where_clause = "t.archived = 0 AND IFNULL(s.hidden,0) = 0";  // only non-hidden, non-archived topics
	  break;

	case ConferenceContext.DISPLAY_HIDDEN:
	  where_clause = "IFNULL(s.hidden,0) = 1";  // only hidden topics
	  break;

	case ConferenceContext.DISPLAY_ARCHIVED:
	  where_clause = "t.archived = 1 AND IFNULL(s.hidden,0) = 0";  // only non-hidden, archived topics
	  break;

	default:
	  logger.error("ACK! get_option (" + String.valueOf(get_option) + ") is bogus!");
	  throw new DataException("Invalid retrieval option specified.");

      } // end switch

      // Figure out what the "order by" clause of the SQL statement will be.  In the case of
      // DISPLAY_NEW mode, this will be in addition to an ordering clause that puts all topics
      // with unread messages first.
      boolean reverse = false;
      int real_sort_option = sort_option;
      if (sort_option<0)
      { // did we specify a "reverse" option?
	reverse = true;
	real_sort_option = -sort_option;

      } // end if

      String order_by_clause;
      switch (real_sort_option)
      {
	case ConferenceContext.SORT_TOPICID:
	  // no reverse possible here
	  order_by_clause = "t.topicid ASC";
	  break;

	case ConferenceContext.SORT_NUMBER:
	  if (reverse)
	    order_by_clause = "t.num DESC";
	  else
	    order_by_clause = "t.num ASC";
	  break;

	case ConferenceContext.SORT_NAME:
	  if (reverse)
	    order_by_clause = "t.name DESC, t.num DESC";
	  else
	    order_by_clause = "t.name ASC, t.num ASC";
	  break;

	case ConferenceContext.SORT_UNREAD:
	  if (reverse)
	    order_by_clause = "unread ASC, t.num DESC";
	  else
	    order_by_clause = "unread DESC, t.num ASC";
	  break;

	case ConferenceContext.SORT_TOTAL:
	  if (reverse)
	    order_by_clause = "t.top_message ASC, t.num DESC";
	  else
	    order_by_clause = "t.top_message DESC, t.num ASC";
	  break;

	case ConferenceContext.SORT_DATE:
	  if (reverse)
	    order_by_clause = "t.lastupdate ASC, t.num DESC";
	  else
	    order_by_clause = "t.lastupdate DESC, t.num ASC";
	  break;

	default:
	  logger.error("ACK! sort_option (" + String.valueOf(sort_option) + ") is bogus!");
	  throw new DataException("Invalid sort option specified.");

      } // end switch

      // Build the huge SQL statement to feed to the database.
      StringBuffer sql =
	new StringBuffer("SELECT t.topicid, t.num, t.creator_uid, t.top_message, t.frozen, t.archived, "
			 + "t.createdate, t.lastupdate, t.name, IFNULL(s.hidden,0) AS hidden, "
			 + "(t.top_message - IFNULL(s.last_message,-1)) AS unread, "
			 + "IFNULL(s.subscribe,0) AS subscribe");
      if (get_option==ConferenceContext.DISPLAY_ACTIVE)
	sql.append(", GREATEST(SIGN(t.top_message - IFNULL(s.last_message,-1)),0) AS newflag");
      sql.append(" FROM topics t LEFT JOIN topicsettings s ON t.topicid = s.topicid AND s.uid = ");
      sql.append(env.getUserID()).append(" WHERE t.confid = ").append(env.getConfID());
      if (where_clause!=null)
	sql.append(" AND ").append(where_clause);
      sql.append(" ORDER BY ");
      if (get_option==ConferenceContext.DISPLAY_ACTIVE)
	sql.append("newflag DESC, ");
      sql.append(order_by_clause).append(';');
      if (logger.isDebugEnabled())
	logger.debug("SQL: " + sql.toString());

      // Now pass this query off to the database!
      ResultSet rs = stmt.executeQuery(sql.toString());

      while (rs.next())
      { // create the returned objects and add them to the vector
	TopicContext top =
	    new TopicUserContextImpl(env,rs.getInt(1),rs.getShort(2),rs.getInt(3),rs.getInt(4),
				     rs.getBoolean(5),rs.getBoolean(6),SQLUtil.getFullDateTime(rs,7),
				     SQLUtil.getFullDateTime(rs,8),rs.getString(9),rs.getBoolean(10),
				     rs.getInt(11),rs.getBoolean(12));
	rc.add(top);

      } // end while

    } // end try
    catch (SQLException e)
    { // turn SQLException into data exception
      logger.error("DB error reading topic entries: " + e.getMessage(),e);
      throw new DataException("unable to retrieve topic information: " + e.getMessage(),e);

    } // end catch
    finally
    { // make sure we release the connection before we go
      SQLUtil.shutdown(stmt);
      env.releaseConnection(conn);

    } // end finally

    return Collections.unmodifiableList(rc);

  } // end getTopicList

  static final TopicContext getTopicByID(EnvConference env, int topicid) throws DataException
  {
    if (logger.isDebugEnabled())
      logger.debug("getTopicByID for topic # " + topicid + ", user #" + env.getUserID());

    Connection conn = null;      // pooled database connection
    Statement stmt = null;

    try
    { // get a database connection
      conn = env.getConnection();
      stmt = conn.createStatement();

      // Query the dtabase by topic ID.
      ResultSet rs = queryByTopic(stmt,topicid,env.getUserID());
      if (rs.next()) // found it!
	return new TopicUserContextImpl(env,topicid,rs.getShort(2),rs.getInt(3),rs.getInt(4),rs.getBoolean(5),
					rs.getBoolean(6),SQLUtil.getFullDateTime(rs,7),
					SQLUtil.getFullDateTime(rs,8),rs.getString(9),rs.getBoolean(10),
					rs.getInt(11),rs.getBoolean(12));
      // else fall out and return null

    } // end try
    catch (SQLException e)
    { // turn SQLException into data exception
      logger.error("DB error reading topic entries: " + e.getMessage(),e);
      throw new DataException("unable to retrieve topic information: " + e.getMessage(),e);

    } // end catch
    finally
    { // make sure we release the connection before we go
      SQLUtil.shutdown(stmt);
      env.releaseConnection(conn);

    } // end finally

    return null;  // default return

  } // end getTopicByID

  static final TopicContext getTopicByNumber(EnvConference env, short topicnum) throws DataException
  {
    if (logger.isDebugEnabled())
      logger.debug("getTopicByNumber for topic # " + topicnum + ", conf # " + env.getConfID() + ", user #"
		   + env.getUserID());

    Connection conn = null;      // pooled database connection
    Statement stmt = null;

    try
    { // get a database connection
      conn = env.getConnection();
      stmt = conn.createStatement();

      // Build the database query.
      StringBuffer sql =
	new StringBuffer("SELECT t.topicid, t.num, t.creator_uid, t.top_message, t.frozen, t.archived, "
			 + "t.createdate, t.lastupdate, t.name, IFNULL(s.hidden,0) AS hidden, "
			 + "(t.top_message - IFNULL(s.last_message,-1)) AS unread, "
			 + "IFNULL(s.subscribe,0) AS subscribe FROM topics t "
			 + "LEFT JOIN topicsettings s ON t.topicid = s.topicid AND s.uid = ");
      sql.append(env.getUserID()).append(" WHERE t.confid = ").append(env.getConfID());
      sql.append(" AND t.num = ").append(topicnum).append(';');
      if (logger.isDebugEnabled())
	logger.debug("SQL: " + sql.toString());

      // Now pass this query off to the database!
      ResultSet rs = stmt.executeQuery(sql.toString());
      if (rs.next()) // found it!
	return new TopicUserContextImpl(env,rs.getInt(1),topicnum,rs.getInt(3),rs.getInt(4),rs.getBoolean(5),
					rs.getBoolean(6),SQLUtil.getFullDateTime(rs,7),
					SQLUtil.getFullDateTime(rs,8),rs.getString(9),rs.getBoolean(10),
					rs.getInt(11),rs.getBoolean(12));
      // else fall out and return null

    } // end try
    catch (SQLException e)
    { // turn SQLException into data exception
      logger.error("DB error reading topic entries: " + e.getMessage(),e);
      throw new DataException("unable to retrieve topic information: " + e.getMessage(),e);

    } // end catch
    finally
    { // make sure we release the connection before we go
      SQLUtil.shutdown(stmt);
      env.releaseConnection(conn);

    } // end finally

    return null;  // default return

  } // end getTopicByNumber

} // end class TopicUserContextImpl
