/*
 * 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@ricochet.com>,
 * for Silverwrist Design Studios.  Portions created by Eric J. Bowersox are
 * Copyright (C) 2001-2004 Eric J. Bowersox/Silverwrist Design Studios.  All Rights Reserved.
 * 
 * Contributor(s): 
 */
package com.silverwrist.venice.core.impl;

import java.io.*;
import java.sql.*;
import java.util.*;
import org.apache.log4j.*;
import com.silverwrist.util.*;
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.Role;
import com.silverwrist.venice.util.MailSend;

class ConferenceUserContextImpl implements ConferenceContext, ConferenceBackend
{
  /*--------------------------------------------------------------------------------
   * Inner class used to cache locally-important data
   *--------------------------------------------------------------------------------
   */

  class ConfCache
  {
    String name;
    String description;
    java.util.Date created;
    int grants_level;

    public ConfCache(String name, String description, java.util.Date created, int grants_level)
    {
      this.name = name;
      this.description = description;
      this.created = created;
      this.grants_level = grants_level;

    } // end constructor

    public final String getName()
    {
      return name;

    } // end getName

    public final String getDescription()
    {
      return description;

    } // end getDescription

    public final java.util.Date getCreated()
    {
      return created;

    } // end getCreated

    public final int getGrantsLevel()
    {
      return grants_level;

    } // end getGrantsLevel

  } // end class ConfCache

  /*--------------------------------------------------------------------------------
   * Internal class used in the implementation of fixSeen()
   *--------------------------------------------------------------------------------
   */

  static class FixSeenHelper
  {
    private int topicid;
    private int top_message;
    private boolean do_insert;

    public FixSeenHelper(int topicid, int top_message, boolean do_insert)
    {
      this.topicid = topicid;
      this.top_message = top_message;
      this.do_insert = do_insert;

    } // end constructor

    public void doFix(Statement stmt, int uid, java.util.Date date) throws SQLException
    {
      StringBuffer sql = new StringBuffer();
      if (do_insert)
      { // construct an SQL INSERT statement
	sql.append("INSERT INTO topicsettings (topicid, uid, last_message, last_read) VALUES (");
	sql.append(topicid).append(", ").append(uid).append(", ").append(top_message).append(", '");
	sql.append(SQLUtil.encodeDate(date)).append("');");

      } // end if
      else
      { // construct an SQL UPDATE statement
	sql.append("UPDATE topicsettings SET last_message = ").append(top_message).append(", last_read = '");
	sql.append(SQLUtil.encodeDate(date)).append("' WHERE topicid = ").append(topicid);
	sql.append(" AND uid = ").append(uid).append(';');

      } // end else

      // now execute the update
      stmt.executeUpdate(sql.toString());

    } // end doFix

  } // end class FixSeenHelper

  /*--------------------------------------------------------------------------------
   * Static class used to return hot list entries
   *--------------------------------------------------------------------------------
   */

  static class MyHotlist implements ConferenceHotlistEntry
  {
    private ConferenceContext conf;
    private int seq;

    MyHotlist(ConferenceContext conf, int seq)
    {
      this.conf = conf;
      this.seq = seq;

    } // end constructor

    public ConferenceContext getConference()
    {
      return conf;

    } // end getConference

    public int getSequence()
    {
      return seq;

    } // end getSequence

  } // end class MyHotlist

  /*--------------------------------------------------------------------------------
   * Static data members
   *--------------------------------------------------------------------------------
   */

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

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

  private EnvConference env;          // the environment
  private int confid;                 // the conference ID
  private int level;                  // access level I have within this conference
  private ConfCache cache;            // cached internal data for the conference
  private ConferenceCommunityContext confdata = null;  // the internal subobject
  private boolean settings_loaded = false;  // did we load the settings from the database?
  private String pseud;               // default pseud to use in this conference
  private java.util.Date last_read;   // last date we read this conference
  private java.util.Date last_post;   // last date we posted to this conference
  private boolean deleted = false;    // has conference been deleted?

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

  protected ConferenceUserContextImpl(EnvCommunity env, int confid, String name, String description,
				      java.util.Date created, int comm_level, int user_level,
				      Connection conn) throws DataException
  {
    this.env = new EnvConference(env,this);
    this.confid = confid;
    this.cache = new ConfCache(name,description,created,comm_level);
    recalcLevel(user_level);
    tryLoadSettings(conn);

  } // end constructor

  ConferenceUserContextImpl(EnvCommunity env, ConferenceCommunityContext cdata) throws DataException
  {
    EnvConference new_env = new EnvConference(env,this);
    this.env = new_env;
    this.confid = cdata.getConfID();
    this.cache = null;
    this.confdata = cdata;
    recalcLevel(new_env.getDefaultRole("Conference.Creator").getLevel());
    this.pseud = env.getUserProps().propGet("default.pseud");
    this.last_read = null;
    this.last_post = null;

  } // end constructor

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

  private final ConferenceCommunityContext getConferenceData() throws DataException
  {
    if (confdata==null)
    { // attempt to load the ConferenceCommunityContext
      if (deleted)
	throw new DataException("This conference has been deleted.");
      confdata = env.getCommunity().getConferenceDataObject(confid);

      // clear cache when we get the real confdata
      cache = null;

    } // end if

    return confdata;

  } // end getConferenceData

  private final ConferenceCommunityContext getConferenceDataNE()
  {
    if (confdata==null)
    { // we need to load the ConferenceCommunityContext...
      if (deleted)
	return null;  // no return on deletion

      try
      { // attempt to load the ConferenceCommunityContext
	confdata = env.getCommunity().getConferenceDataObject(confid);

      } // end try
      catch (DataException e)
      { // there was a data failure loading the conference
	logger.error("** Got DataException in getConferenceDataNE: " + e.getMessage(),e);
	return null;

      } // end catch

      // clear cache when we get the real confdata
      cache = null;

    } // end if

    return confdata;

  } // end getConferenceDataNE

  private final void recalcLevel(int new_level)
  {
    level = env.getCommunity().realCommunityLevel();

    int comm_level = -1;
    if (cache!=null)
      comm_level = cache.getGrantsLevel();
    else
    { // look at the ConferenceCommunityContext to get the community level
      ConferenceCommunityContext c = getConferenceDataNE();
      if (c!=null)
	comm_level = c.getCommunityGrantedLevel();

    } // end else

    if (comm_level>level)
      level = comm_level;
    if (new_level>level)
      level = new_level;

  } // end recalcLevel

  private final void tryLoadSettings(Connection conn) throws DataException
  {
    if (deleted)
      return;  // this is a no-op

    Statement stmt = null;

    try
    { // look up the conference settings for this user
      stmt = conn.createStatement();
      StringBuffer sql = new StringBuffer("SELECT * FROM confsettings WHERE confid = ");
      sql.append(confid).append(" AND uid = ").append(env.getUserID()).append(';');
      ResultSet rs = stmt.executeQuery(sql.toString());

      if (rs.next())
      { // load in the settings
	settings_loaded = true;
	pseud = rs.getString("default_pseud");
	last_read = SQLUtil.getFullDateTime(rs,"last_read");
	last_post = SQLUtil.getFullDateTime(rs,"last_post");

      } // end if
      else
      { // just default everything
	settings_loaded = false;
	pseud = env.getUserProps().propGet("default.pseud");
	last_read = null;
	last_post = null;

      } // end else

    } // end try
    catch (SQLException e)
    { // handle a SQL error as usual
      logger.error("Error loading conference settings: " + e.getMessage(),e);
      throw new DataException("Unable to load conference settings: " + e.getMessage(),e);

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

    } // end finally

  } // end tryLoadSettings

  /*--------------------------------------------------------------------------------
   * Implementations from interface ConferenceContext
   *--------------------------------------------------------------------------------
   */

  public int getConfID()
  {
    return confid;

  } // end getConfID

  public String getName()
  {
    if (cache!=null)
      return cache.getName();
    ConferenceCommunityContext c = getConferenceDataNE();
    if (c!=null)
      return c.getName();
    else
      return null;

  } // end getName
 
  public String getDescription()
  {
    if (cache!=null)
      return cache.getDescription();
    ConferenceCommunityContext c = getConferenceDataNE();
    if (c!=null)
      return c.getDescription();
    else
      return null;

  } // end getDescription

  public java.util.Date getCreationDate()
  {
    if (cache!=null)
      return cache.getCreated();
    ConferenceCommunityContext c = getConferenceDataNE();
    if (c!=null)
      return c.getCreationDate();
    else
      return null;

  } // end getCreationDate

  public java.util.Date getLastUpdateDate()
  {
    ConferenceCommunityContext c = getConferenceDataNE();
    if (c!=null)
      return c.getLastUpdateDate();
    else
      return null;

  } // end getLastUpdateDate

  public List getAliases() throws DataException
  {
    return getConferenceData().getAliases();

  } // end getAliases

  public List getHosts() throws DataException
  {
    return getConferenceData().getHosts();

  } // end getHosts

  public boolean canReadConference()
  {
    ConferenceCommunityContext c = getConferenceDataNE();
    if (c==null)
      return false;
    return c.canReadConference(level);

  } // end canReadConference

  public boolean canPostToConference()
  {
    ConferenceCommunityContext c = getConferenceDataNE();
    if (c==null)
      return false;
    return c.canPostToConference(level);

  } // end canPostToConference

  public boolean canCreateTopic()
  {
    ConferenceCommunityContext c = getConferenceDataNE();
    if (c==null)
      return false;
    return c.canCreateTopic(level);

  } // end canCreateTopic

  public boolean canChangeConference()
  {
    ConferenceCommunityContext c = getConferenceDataNE();
    if (c==null)
      return false;
    return c.canChangeConference(level);

  } // end canChangeConference

  public int getReadLevel() throws DataException, AccessError
  {
    if (!(getConferenceData().canChangeConference(level)))
    { // this user can't modify the conference security
      logger.error("getReadLevel():  user not permitted to retrieve security info");
      throw new AccessError("You are not permitted to retrieve security information for this conference.");

    } // end if

    return getConferenceData().getReadLevel();

  } // end getReadLevel

  public int getPostLevel() throws DataException, AccessError
  {
    if (!(getConferenceData().canChangeConference(level)))
    { // this user can't modify the conference security
      logger.error("getPostLevel(): user not permitted to retrieve security info");
      throw new AccessError("You are not permitted to retrieve security information for this conference.");

    } // end if

    return getConferenceData().getPostLevel();

  } // end getPostLevel

  public int getCreateLevel() throws DataException, AccessError
  {
    if (!(getConferenceData().canChangeConference(level)))
    { // this user can't modify the conference security
      logger.error("getCreateLevel(): user not permitted to retrieve security info");
      throw new AccessError("You are not permitted to retrieve security information for this conference.");

    } // end if

    return getConferenceData().getCreateLevel();

  } // end getCreateLevel

  public int getHideLevel() throws DataException, AccessError
  {
    if (!(getConferenceData().canChangeConference(level)))
    { // this user can't modify the conference security
      logger.error("getHideLevel(): user not permitted to retrieve security info");
      throw new AccessError("You are not permitted to retrieve security information for this conference.");

    } // end if

    return getConferenceData().getHideLevel();

  } // end getHideLevel

  public int getNukeLevel() throws DataException, AccessError
  {
    if (!(getConferenceData().canChangeConference(level)))
    { // this user can't modify the conference security
      logger.error("getNukeLevel(): user not permitted to retrieve security info");
      throw new AccessError("You are not permitted to retrieve security information for this conference.");

    } // end if

    return getConferenceData().getNukeLevel();

  } // end getNukeLevel

  public int getChangeLevel() throws DataException, AccessError
  {
    if (!(getConferenceData().canChangeConference(level)))
    { // this user can't modify the conference security
      logger.error("getChangeLevel(): user not permitted to retrieve security info");
      throw new AccessError("You are not permitted to retrieve security information for this conference.");

    } // end if

    return getConferenceData().getChangeLevel();

  } // end getChangeLevel

  public int getDeleteLevel() throws DataException, AccessError
  {
    if (!(getConferenceData().canChangeConference(level)))
    { // this user can't modify the conference security
      logger.error("getDeleteLevel(): user not permitted to retrieve security info");
      throw new AccessError("You are not permitted to retrieve security information for this conference.");

    } // end if

    return getConferenceData().getDeleteLevel();

  } // end getDeleteLevel

  public void setSecurityLevels(int read, int post, int create, int hide, int nuke, int change, int delete)
      throws DataException, AccessError
  {
    if (!(getConferenceData().canChangeConference(level)))
    { // this user can't modify the conference security
      logger.error("setSecurityLevels(): user not permitted to change security info");
      throw new AccessError("You are not permitted to change security information for this conference.");

    } // end if

    // "Correct" the security levels specified.
    if (post>create)
    { // Post level should be no greater than Create
      if (logger.isDebugEnabled())
	logger.debug("Post level (" + post + ") was greater than create level (" + create + "), correcting");
      post = create;

    } // end if

    if (read>post)
    { // Read level should be no greater than Post
      if (logger.isDebugEnabled())
	logger.debug("Read level (" + read + ") was greater than post level (" + post + "), correcting");
      read = post;

    } // end if

    if (change>delete)
    { // Change level should be no greater than Delete
      if (logger.isDebugEnabled())
	logger.debug("Change level (" + change + ") was greater than delete level (" + delete
		     + "), correcting");
      change = delete;

    } // end if

    if (nuke>delete)
    { // Nuke level should be no greater than Delete
      if (logger.isDebugEnabled())
	logger.debug("Nuke level (" + nuke + ") was greater than delete level (" + delete + "), correcting");
      nuke = delete;

    } // end if

    if (hide>nuke)
    { // Hide level should be no greater than Nuke
      if (logger.isDebugEnabled())
	logger.debug("Hide level (" + hide + ") was greater than nuke level (" + nuke + "), correcting");
      hide = nuke;

    } // end if

    // Now call through to set the security levels.
    getConferenceData().setSecurityLevels(env,read,post,create,hide,nuke,change,delete);

  } // end setSecurityLevels

  public void setName(String val) throws DataException, AccessError
  {
    if (!(getConferenceData().canChangeConference(level)))
    { // this user can't modify the conference security
      logger.error("setName(): user not permitted to change name");
      throw new AccessError("You are not permitted to change the name of this conference.");

    } // end if

    // Call through to set the name.
    getConferenceData().setName(env,val);

  } // end setName

  public void setDescription(String val) throws DataException, AccessError
  {
    if (!(getConferenceData().canChangeConference(level)))
    { // this user can't modify the conference security
      logger.error("setDescription(): user not permitted to change description");
      throw new AccessError("You are not permitted to change the description of this conference.");

    } // end if

    // Call through to set the name.
    getConferenceData().setDescription(val);

  } // end setDescription

  public void addAlias(String alias) throws DataException, AccessError
  {
    if (!(getConferenceData().canChangeConference(level)))
    { // this user can't modify the conference security
      logger.error("addAlias(): user not permitted to change aliases");
      throw new AccessError("You are not permitted to change the aliases of this conference.");

    } // end if

    // Call through to add the alias.
    getConferenceData().addAlias(env,alias);

  } // end addAlias

  public void removeAlias(String alias) throws DataException, AccessError
  {
    if (!(getConferenceData().canChangeConference(level)))
    { // this user can't modify the conference security
      logger.error("removeAlias(): user not permitted to change aliases");
      throw new AccessError("You are not permitted to change the aliases of this conference.");

    } // end if

    // Call through to remove the alias.
    getConferenceData().removeAlias(env,alias);

  } // end removeAlias

  public void setMembership(int uid, int grant_level) throws DataException, AccessError
  {
    if (!(getConferenceData().canChangeConference(level)))
    { // this user can't modify the conference security
      logger.error("setMembership(): user not permitted to change membership");
      throw new AccessError("You are not permitted to change the membership of this conference.");

    } // end if

    // call down to set the level
    getConferenceData().setMembership(env,uid,grant_level);

    // If we adjusted our OWN level, reflect that change.
    if (uid==env.getUserID())
      recalcLevel(grant_level);

  } // end setMembership

  public void addMember(int uid, boolean as_host) throws DataException, AccessError
  {
    Role r;
    if (as_host)
      r = env.getDefaultRole("Conference.NewHost");
    else
      r = env.getDefaultRole("Conference.NewUser");
    setMembership(uid,r.getLevel());

  } // end addMember

  public void removeMember(int uid) throws DataException, AccessError
  {
    setMembership(uid,-1);

  } // end removeMember

  public int getCommunityGrantedLevel() throws DataException, AccessError
  {
    ConferenceCommunityContext c = getConferenceData();
    if (!(env.getCommunity().userCanCreateSubobjects()))
    { // this user can't modify the conference security
      logger.error("getCommunityGrantedLevel(): user not permitted to retrieve security info");
      throw new AccessError("You are not permitted to retrieve security information for this conference.");

    } // end if

    return c.getCommunityGrantedLevel();

  } // end getCommunityGrantedLevel

  public void setCommunityGrantedLevel(int new_level) throws DataException, AccessError
  {
    ConferenceCommunityContext c = getConferenceData();
    if (!(env.getCommunity().userCanCreateSubobjects()))
    { // this user can't modify the conference security
      logger.error("setCommunityGrantedLevel(): user not permitted to change security info");
      throw new AccessError("You are not permitted to change security information for this conference.");

    } // end if

    c.setCommunityGrantedLevel(env,new_level);

  } // end setCommunityGrantedLevel

  public short getSequence() throws DataException, AccessError
  {
    ConferenceCommunityContext c = getConferenceData();
    if (!(env.getCommunity().userCanCreateSubobjects()))
    { // this user can't modify the conference security
      logger.error("getSequence(): user not permitted to retrieve layout info");
      throw new AccessError("You are not permitted to retrieve layout information for this conference.");

    } // end if

    return c.getSequence();

  } // end getSequence

  public void setSequence(short seq) throws DataException, AccessError
  {
    ConferenceCommunityContext c = getConferenceData();
    if (!(env.getCommunity().userCanCreateSubobjects()))
    { // this user can't modify the conference security
      logger.error("setSequence(): user not permitted to set layout info");
      throw new AccessError("You are not permitted to set layout information for this conference.");

    } // end if

    c.setSequence(seq);

  } // end setSequence

  public boolean getHideList() throws DataException, AccessError
  {
    ConferenceCommunityContext c = getConferenceData();
    if (!(env.getCommunity().userCanCreateSubobjects()))
    { // this user can't modify the conference security
      logger.error("getHideList(): user not permitted to retrieve security info");
      throw new AccessError("You are not permitted to retrieve security information for this conference.");

    } // end if

    return c.getHideList();

  } // end getHideList

  public void setHideList(boolean flag) throws DataException, AccessError
  {
    ConferenceCommunityContext c = getConferenceData();
    if (!(env.getCommunity().userCanCreateSubobjects()))
    { // this user can't modify the conference security
      logger.error("setHideList(): user not permitted to change security info");
      throw new AccessError("You are not permitted to change security information for this conference.");

    } // end if

    c.setHideList(env,flag);

  } // end setHideList

  public boolean canSetHideList()
  {
    return env.getCommunity().userCanCreateSubobjects();

  } // end canSetHideList

  public String getDefaultPseud()
  {
    return pseud;

  } // end getDefaultPseud

  public void setDefaultPseud(String val) throws DataException
  {
    if (env.isAnonymous())
      return;  // anonymous user can't change pseud
    if (deleted)
      throw new DataException("This conference has been deleted.");

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

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

      if (settings_loaded)
      { // generate an update statement
	sql.append("UPDATE confsettings SET default_pseud = '").append(SQLUtil.encodeString(val));
	sql.append("' WHERE confid = ").append(confid).append(" AND uid = ").append(env.getUserID());
	sql.append(';');

      } // end if
      else
      { // need to insert a confsettings row
	sql.append("INSERT INTO confsettings (confid, uid, default_pseud) VALUES (").append(confid);
	sql.append(", ").append(env.getUserID()).append(", '").append(SQLUtil.encodeString(val)).append("');");

      } // end else

      // execute the statement
      stmt.executeUpdate(sql.toString());

      // save off the values to our local fields
      pseud = val;
      settings_loaded = true;

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

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

    } // end finally

  } // end setDefaultPseud

  public boolean anyUnread()
  {
    if (deleted || env.isAnonymous())
      return false;
    Connection conn = null;      // pooled database connection
    Statement stmt = null;

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

      // Build a query.  The idea here is that we want to see the topic IDs of all topics within the
      // conference which (a) are not archived, (b) are not hidden and (c) have one or more messages unread
      // by this user. The need for the user's settings for both (b) and (c) necessitates the use of the LEFT
      // JOIN to topicsettings, which in turn necessitates the IFNULL guards on references to topicsettings
      // columns in the WHERE clause (as there's only a topicsettings row if the user has read anything
      // in this topic or otherwise set it).
      stmt = conn.createStatement();
      StringBuffer sql = new StringBuffer("SELECT t.topicid FROM topics t LEFT JOIN topicsettings s "
					  + "ON t.topicid = s.topicid AND s.uid = ");
      sql.append(env.getUserID()).append(" WHERE t.confid = ").append(confid);
      sql.append(" AND t.archived = 0 AND IFNULL(s.hidden,0) = 0 "
		 + "AND (t.top_message - IFNULL(s.last_message,-1)) > 0 LIMIT 1;");
      if (logger.isDebugEnabled())
	logger.debug("SQL: " + sql.toString());

      // run the query
      ResultSet rs = stmt.executeQuery(sql.toString());
      return rs.next();  // will return 0 or 1 rows

    } // end try
    catch (SQLException e)
    { // turn SQLException into data exception
      logger.error("DB error getting unread flag: " + e.getMessage(),e);

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

    } // end finally

    return false;  // default return

  } // end anyUnread

  public List getTopicList(int get_option, int sort_option, boolean ignore_sticky) throws DataException, AccessError
  {
    if (!(getConferenceData().canReadConference(level)))
    { // the luser can't even read the conference...
      logger.error("getTopicList(): user not permitted to get topics");
      throw new AccessError("You are not permitted to read this conference.");

    } // end if
    
    // palm it all off on the big static function
    return TopicUserContextImpl.getTopicList(env,get_option,sort_option,ignore_sticky);

  } // end getTopicList

  public List getTopicList(int get_option, int sort_option) throws DataException, AccessError
  {
    return getTopicList(get_option,sort_option,false);

  } // end getTopicList

  public TopicContext getTopic(short number) throws DataException, AccessError
  {
    if (!(getConferenceData().canReadConference(level)))
    { // the luser can't even read the conference...
      logger.error("getTopic(): user not permitted to read");
      throw new AccessError("You are not permitted to read this conference.");

    } // end if

    // call down to the TopicUserContextImpl implementation
    return TopicUserContextImpl.getTopicByNumber(env,number);

  } // end getTopic

  public TopicContext addTopic(String title, String zp_pseud, String zp_text)
      throws DataException, AccessError
  {
    if (logger.isDebugEnabled())
      logger.debug("addTopic(\"" + title + "\") entry");
    if (!(getConferenceData().canCreateTopic(level)))
    { // not allowed to create a topic - bail out
      logger.error("addTopic(): user not permitted to create new topic");
      throw new AccessError("You are not permitted to create a topic in this conference.");

    } // end if

    // preprocess the pseud and title arguments through HTML checkers
    HTMLChecker title_ch = env.getHTMLChecker("post-pseud");
    HTMLChecker zp_pseud_ch = env.getHTMLChecker("post-pseud");

    try
    { // run arguments through the HTML checker
      title_ch.append(title);
      title_ch.finish();
      zp_pseud_ch.append(zp_pseud);
      zp_pseud_ch.finish();

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

    } // end catch

    // Create the actual topic in the database.
    String real_title;
    ReturnTopicInfo new_topic_inf;
    try
    { // call down to create the new topic!
      real_title = title_ch.getValue();
      new_topic_inf = getConferenceData().createNewTopic(env,real_title,zp_pseud_ch.getValue(),zp_text);
      if (logger.isDebugEnabled())
	logger.debug("Created new topic ID #" + new_topic_inf.getTopicID());

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

    } // end catch

    // now we need to reset our last post date
    Connection conn = null;
    PreparedStatement stmt = null;

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

      // create a new record in topicsettings (we WERE the first to post in the topic after all!)
      stmt = conn.prepareStatement("INSERT INTO topicsettings (topicid, uid, last_post) VALUES (?, ?, ?);");
      stmt.setInt(1,new_topic_inf.getTopicID());
      stmt.setInt(2,env.getUserID());
      SQLUtil.setFullDateTime(stmt,3,new_topic_inf.getCreateDate());
      stmt.executeUpdate();

      // update the conference last-post information
      touchPost(conn,new_topic_inf.getCreateDate());

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

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

    } // end finally

    // create the topic context to return to the user
    return new TopicUserContextImpl(env,new_topic_inf,real_title);

  } // end addTopic

  public TopicMessageContext getMessageByPostID(long postid) throws DataException, AccessError
  {
    if (!(getConferenceData().canReadConference(level)))
    { // the luser can't even read the conference...
      logger.error("getMessageByPostID(): user not permitted to get messages");
      throw new AccessError("You are not permitted to read this conference.");

    } // end if

    // call down to the static function level
    return TopicMessageUserContextImpl.getMessage(env,postid);

  } // end getMessageByPostID

  public HTMLChecker getNewTopicPreviewChecker()
  {
    ConferenceCommunityContext c = getConferenceDataNE();
    short new_topic;
    if (c==null)
      new_topic = 0;
    else
      new_topic = (short)(c.getTopTopic() + 1);
    HTMLChecker rc = env.getHTMLChecker("preview");
    rc.setContextValue("PostLinkDecoderContext",createDecoderContext(new_topic));
    return rc;
    
  } // end getNewTopicPreviewChecker

  public void fixSeen() throws DataException, AccessError
  {
    if (!(getConferenceData().canReadConference(level)))
    { // the luser can't even read the conference...
      logger.error("fixSeen(): user not permitted to set unread messages");
      throw new AccessError("You are not permitted to read this conference.");

    } // end if

    if (env.isAnonymous())
      return;  // anonymous user can't fixseen

    Connection conn = null;
    Statement stmt = null;

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

      // lock the tables that we need
      stmt.executeUpdate("LOCK TABLES confsettings WRITE, topicsettings WRITE, topics READ;");

      try
      { // get the existing topic list and its top messages list
	StringBuffer sql =
          new StringBuffer("SELECT topics.topicid, topics.top_message, ISNULL(topicsettings.last_message) "
			   + "FROM topics LEFT JOIN topicsettings ON topics.topicid = topicsettings.topicid "
			   + "AND topicsettings.uid = ");
	sql.append(env.getUserID()).append(" WHERE topics.confid = ").append(confid).append(';');
	ResultSet rs = stmt.executeQuery(sql.toString());

	// use the results to build up a list of FixSeenHelpers
	ArrayList tmp = new ArrayList();
	while (rs.next())
	  tmp.add(new FixSeenHelper(rs.getInt(1),rs.getInt(2),rs.getBoolean(3)));
	SQLUtil.shutdown(rs);

	// now iterate over the list and call doFix on each one
	Iterator it = tmp.iterator();
	java.util.Date now = new java.util.Date();
	while (it.hasNext())
	{ // just hit each one in turn
	  FixSeenHelper fsh = (FixSeenHelper)(it.next());
	  fsh.doFix(stmt,env.getUserID(),now);

	} // end while

	// update our last-read indicator, too
	touchRead(conn);

      } // end try
      finally
      { // make sure we unlock everything before we go
	SQLUtil.unlockTables(conn);

      } // end finally

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

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

    } // end finally

  } // end fixSeen

  public List getActivePosters(int skip, int limit) throws DataException, AccessError
  {
    if (!(getConferenceData().canReadConference(level)))
    { // the luser can't even read the conference...
      logger.error("getActivePosters(): user not permitted to get posters list");
      throw new AccessError("You are not permitted to read this conference.");

    } // end if

    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 confsettings s, "
			   + "users u WHERE u.uid = s.uid AND s.confid = ");
      sql.append(confid).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);
      SQLUtil.shutdown(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
  {
    if (!(getConferenceData().canReadConference(level)))
    { // the luser can't even read the conference...
      logger.error("getActiveReaders(): user not permitted to get readers list");
      throw new AccessError("You are not permitted to read this conference.");

    } // end if

    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 confsettings s, "
			   + "users u WHERE u.uid = s.uid AND s.confid = ");
      sql.append(confid).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);
      SQLUtil.shutdown(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 List getMemberList() throws DataException, AccessError
  {
    if (!(getConferenceData().canReadConference(level)))
    { // the luser can't even read the conference...
      logger.error("getMemberList(): user not permitted to get member list");
      throw new AccessError("You are not permitted to read this conference.");

    } // end if

    return getConferenceData().getMemberList();

  } // end getMemberList

  public int getMemberLevel(int uid) throws DataException, AccessError
  {
    if (!(getConferenceData().canReadConference(level)))
    { // the luser can't even read the conference...
      logger.error("getMemberLevel(): user not permitted to get member data");
      throw new AccessError("You are not permitted to read this conference.");

    } // end if

    return getConferenceData().getMemberLevel(uid);

  } // end getMemberLevel

  public void delete() throws DataException, AccessError
  {
    ConferenceCommunityContext ctxt = getConferenceData();
    if (!(ctxt.canDeleteConference(level)) || !(env.getCommunity().userCanDeleteSubobjects()))
    { // no way can we delete this conference, not NO way!
      logger.error("delete(): user not permitted to delete conference");
      throw new AccessError("You are not permitted to delete this conference.");

    } // end if

    // call the methods required to delete the conference
    ctxt.delete(env);
    env.getCommunity().detachConferenceDataObject(confid);

    // flag that we've been deleted
    cache = null;
    settings_loaded = true;
    pseud = null;
    last_read = null;
    last_post = null;
    deleted = true;

    // detach our references from the lower-level object
    confdata = null;

  } // end delete

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

  } // end deleteConference

  public boolean canDeleteConference()
  {
    ConferenceCommunityContext c = getConferenceDataNE();
    if (c==null)
      return false;
    return (c.canDeleteConference(level) && env.getCommunity().userCanDeleteSubobjects());

  } // end canDeleteConference

  public boolean isInHotlist()
  {
    Connection conn = null;
    Statement stmt = null;

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

      // do a quickie query
      StringBuffer sql = new StringBuffer("SELECT sequence FROM confhotlist WHERE uid = ");
      sql.append(env.getUserID()).append(" AND sigid = ").append(env.getCommunityID());
      sql.append(" AND confid = ").append(confid).append(';');
      ResultSet rs = stmt.executeQuery(sql.toString());
      return rs.next();

    } // end try
    catch (SQLException e)
    { // this becomes a DataException
      logger.error("DB error checking hotlist: " + e.getMessage(),e);
      return false;

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

    } // end finally

  } // end isInHotlist

  public void addToHotlist() throws DataException
  {
    Connection conn = null;
    Statement stmt = null;

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

      stmt.executeUpdate("LOCK TABLES confhotlist WRITE;");
      try
      { // do a quickie query to see if we're already in the hotlist
	StringBuffer sql = new StringBuffer("SELECT sequence FROM confhotlist WHERE uid = ");
	sql.append(env.getUserID()).append(" AND sigid = ").append(env.getCommunityID());
	sql.append(" AND confid = ").append(confid).append(';');
	ResultSet rs = stmt.executeQuery(sql.toString());
	if (rs.next())
	  return;  // already in hotlist - this is a no-op

	// find a sequence number for the new entry
	sql.setLength(0);
	sql.append("SELECT MAX(sequence) FROM confhotlist WHERE uid = ").append(env.getUserID()).append(';');
	rs = stmt.executeQuery(sql.toString());
	if (!(rs.next()))
	  throw new InternalStateError("bogus query result on addToHotlist");
	int new_sequence = rs.getInt(1) + 100;
	SQLUtil.shutdown(rs);

	// add the new record
	sql.setLength(0);
	sql.append("INSERT INTO confhotlist (uid, sequence, sigid, confid) VALUES (").append(env.getUserID());
	sql.append(", ").append(new_sequence).append(", ").append(env.getCommunityID()).append(", ");
	sql.append(confid).append(");");
	stmt.executeUpdate(sql.toString());

      } // end try
      finally
      { // make sure the table is unlocked before we go
	SQLUtil.unlockTables(conn);

      } // end finally

    } // end try
    catch (SQLException e)
    { // this becomes a DataException
      logger.error("DB error adding to hotlist: " + e.getMessage(),e);
      throw new DataException("error adding to hotlist: " + e.getMessage(),e);

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

    } // end finally

  } // end addToHotList

  public boolean canAddToHotlist()
  {
    if (env.isAnonymous())
      return false;
    return !(isInHotlist());

  } // end canAddToHotlist

  public CommunityContext getEnclosingCommunity()
  {
    return env.getCommunity().selfCommunity();

  } // end getEnclosingCommunity

  public void removeFromHotlist() throws DataException
  {
    Connection conn = null;
    Statement stmt = null;

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

      // create the DELETE statement and just execute it blind (if this conference is not in the hotlist,
      // the DELETE is a no-op).
      StringBuffer sql = new StringBuffer("DELETE FROM confhotlist WHERE uid = ");
      sql.append(env.getUserID()).append(" AND sigid = ").append(env.getCommunityID());
      sql.append(" AND confid = ").append(confid).append(';');
      stmt.executeUpdate(sql.toString());

    } // end try
    catch (SQLException e)
    { // this becomes a DataException
      logger.error("DB error removing from hotlist: " + e.getMessage(),e);
      throw new DataException("error removing from hotlist: " + e.getMessage(),e);

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

    } // end finally

  } // end removeFromHotlist

  public void setHotlistSequence(int seq) throws DataException
  {
    Connection conn = null;
    Statement stmt = null;

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

      // create the UPDATE statement and just execute it blind (if this conference is not in the hotlist,
      // the UPDATE is a no-op).
      StringBuffer sql = new StringBuffer("UPDATE confhotlist SET sequence = ");
      sql.append(seq).append(" WHERE uid = ").append(env.getUserID()).append(" AND sigid = ");
      sql.append(env.getCommunityID()).append(" AND confid = ").append(confid).append(';');
      stmt.executeUpdate(sql.toString());

    } // end try
    catch (SQLException e)
    { // this becomes a DataException
      logger.error("DB error setting hotlist sequence: " + e.getMessage(),e);
      throw new DataException("error setting hotlist sequence: " + e.getMessage(),e);

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

    } // end finally

  } // end setHotlistSequence

  public boolean displayPostPictures()
  {
    ConferenceCommunityContext c = getConferenceDataNE();
    if (c==null)
      return false;
    return c.displayPostPictures();

  } // end displayPostPictures

  public ConferenceProperties getProperties() throws DataException, AccessError
  {
    if (!(getConferenceData().canChangeConference(level)))
    { // this user can't modify the conference properties
      logger.error("getProperties(): user not permitted to change properties");
      throw new AccessError("You are not permitted to change the properties of this conference.");

    } // end if

    // call down to get the properties
    return getConferenceData().getProperties();

  } // end getProperties

  public void setProperties(ConferenceProperties props) throws DataException, AccessError
  {
    if (!(getConferenceData().canChangeConference(level)))
    { // this user can't modify the conference properties
      logger.error("setProperties(): user not permitted to change properties");
      throw new AccessError("You are not permitted to change the properties of this conference.");

    } // end if

    // call down to set the properties
    getConferenceData().setProperties(props);

  } // end setProperties

  public SecurityInfo getSecurityInfo()
  {
    return env.getSecurityInfo();

  } // end getSecurityInfo

  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 conference invitation.");

    // Format the added "conference name" message.
    HashMap vars = new HashMap();
    vars.put("community.name",env.getCommunityName());
    vars.put("conference.name",getName());
    String extra_msg = env.getStockMessage("add-conf-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, confsettings s, propuser p WHERE c.contactid = u.contactid "
		 + "AND u.uid = s.uid AND s.confid = ").append(confid);
      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);
      SQLUtil.shutdown(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());
      String disklaimer =
	  env.getStockMessage(posters ? "mass-conference-notice-poster" : "mass-conference-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
      MailSend msend = new PersonalMailSend(env.getMailSender(),env.getGlobalSite(),env.getUserProps());
      msend.setSubject(subject);
      msend.setText(buf.toString());
      env.getGlobalSite().queueTask(new MailerAgent(msend,rc));

    } // end if

  } // end if

  public String getCustomBlock(int selector) throws DataException
  {
    return getConferenceData().getCustomBlock(selector);

  } // end getCustomBlock

  public void setCustomBlock(int selector, String data) throws AccessError, DataException
  {
    if (!(getConferenceData().canChangeConference(level)))
    { // this user can't modify the conference properties
      logger.error("setCustomBlock(): user not permitted to change custom blocks");
      throw new AccessError("You are not permitted to change the custom blocks for this conference.");

    } // end if

    // call down to set the custom block
    getConferenceData().setCustomBlock(selector,data);

  } // end setCustomBlock

  public void removeCustomBlocks() throws AccessError, DataException
  {
    if (!(getConferenceData().canChangeConference(level)))
    { // this user can't modify the conference properties
      logger.error("setCustomBlock(): user not permitted to change custom blocks");
      throw new AccessError("You are not permitted to change the custom blocks for this conference.");

    } // end if

    // call down to remove the custom blocks
    getConferenceData().removeCustomBlocks();

  } // end removeCustomBlocks

  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 (!(getConferenceData().canReadConference(level)))
      throw new AccessError("You are not permitted to search for posts within this conference.");

    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 t.topicid, t.num, p.postid, p.num, p.creator_uid, p.posted, p.linecount, "
			   + "d.data FROM topics t, posts p, postdata d WHERE t.confid = ");
      sql.append(confid);
      sql.append(" AND t.topicid = p.topicid AND p.scribble_uid IS NULL AND p.postid = d.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(),confid,rs.getInt(1),rs.getInt(2),
					 rs.getLong(3),rs.getInt(4),rs.getInt(5),
					 SQLUtil.getFullDateTime(rs,6),rs.getInt(7),rs.getString(8)));

    } // 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);
      SQLUtil.shutdown(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 (!(getConferenceData().canReadConference(level)))
      throw new AccessError("You are not permitted to search for posts within this conference.");

    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 topics t, posts p, postdata d WHERE t.confid = ");
      sql.append(confid);
      sql.append(" AND t.topicid = p.topicid AND p.scribble_uid IS NULL AND p.postid = d.postid "
		 + "AND MATCH(d.data) AGAINST(");
      sql.append(SQLUtil.encodeStringArg(search_terms)).append(");");

      // execute the query and load the results
      ResultSet rs = stmt.executeQuery(sql.toString());
      if (!(rs.next()))
	throw new InternalStateError("query failure in ConferenceUserContextImpl.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);
      SQLUtil.shutdown(conn);

    } // end finally

  } // end getSearchPostCount

  public String getPostLink() throws DataException
  {
    if (deleted)
      return null;
    
    String v = getConferenceData().getAnAlias();
    return env.getCommunityAlias() + "!" + v;

  } // end getPostLink

  public String exportTopics(Set select_topics) throws AccessError, DataException, IOException
  {
    if (logger.isDebugEnabled())
      logger.debug("exportTopics(" + select_topics.toString() + ") entry");
    if (!(getConferenceData().canChangeConference(level)))
      throw new AccessError("You are not permitted to export posts from this conference.");
    if (deleted)
      throw new DataException("Cannot export posts from a deleted conference.");

    // "Normalize" the topic selection set.
    HashSet norm_select_topics = null;
    if (select_topics!=null)
    { // build the normalized set
      norm_select_topics = new HashSet();
      for (Iterator it=select_topics.iterator(); it.hasNext(); )
      { // get each object out in turn
	Object o = it.next();
	if (o==null)
	  continue;
	if (o instanceof Integer)
	{ // it's already an integer
	  norm_select_topics.add(o);
	  continue;

	} // end if

	if (o instanceof Number)
	{ // convert to Integer and add it
	  norm_select_topics.add(new Integer(((Number)o).intValue()));
	  continue;

	} // end if

	try
	{ // append integer equivalent to set
	  norm_select_topics.add(new Integer(o.toString()));

	} // end try
	catch (NumberFormatException e)
	{ // do nothing
	} // end catch

      } // end for

    } // end if

    ConferencingExporter xprt = new ConferencingExporter();
    xprt.setTopics(norm_select_topics);
    return xprt.exportConference(this);

  } // end exportTopics

  public List importMessages(InputStream xmlstream, int match_method, boolean create_new)
      throws AccessError, DataException
  {
    if (xmlstream==null)
      throw new NullPointerException("bad input stream");
    if ((match_method!=IMPORT_MATCH_NUM) && (match_method!=IMPORT_MATCH_NAME))
      throw new IndexOutOfBoundsException("invalid match method specified");

    if (!(getConferenceData().canChangeConference(level)))
      throw new AccessError("You are not permitted to import posts to this conference.");
    if (deleted)
      throw new DataException("Cannot import messages to a deleted conference.");

    Connection conn = null;
    try
    { // get a connection and create the worker
      conn = env.getConnection();
      ConferencingImporter worker = new ConferencingImporter(env.getUserID(),confid,match_method,create_new,conn,
							     env.getEngine());

      // run it!
      return worker.importMessages(xmlstream);

    } // end try
    catch (SQLException e)
    { // translate SQLException to DataException
      throw new DataException("Error getting database connection: " + e.getMessage(),e);

    } // end catch
    finally
    { // shut down the connection
      SQLUtil.shutdown(conn);

    } // end finally

  } // end importMessages

  /*--------------------------------------------------------------------------------
   * Implementations from interface ConferenceBackend
   *--------------------------------------------------------------------------------
   */

  public ConferenceContext selfConference()
  {
    return this;

  } // end selfConference

  public int realConfID()
  {
    return confid;

  } // end realConfID

  public boolean userCanHide()
  {
    ConferenceCommunityContext c = getConferenceDataNE();
    if (c==null)
      return false;
    return c.canHideTopics(level);

  } // end userCanHide

  public void touchRead(Connection conn) throws SQLException
  {
    if (deleted || env.isAnonymous())
      return;  // anonymous user can't update squat

    Statement stmt = null;

    try
    { // create the SQL statement
      stmt = conn.createStatement();
      StringBuffer sql = new StringBuffer();

      java.util.Date now = new java.util.Date();
      if (settings_loaded)
      { // generate an update statement
	sql.append("UPDATE confsettings SET last_read = '").append(SQLUtil.encodeDate(now));
	sql.append("' WHERE confid = ").append(confid).append(" AND uid = ").append(env.getUserID());
	sql.append(';');

      } // end if
      else
      { // need to insert a confsettings row
	sql.append("INSERT INTO confsettings (confid, uid, default_pseud, last_read) VALUES (").append(confid);
	sql.append(", ").append(env.getUserID()).append(", '").append(SQLUtil.encodeString(pseud));
	sql.append("', '").append(SQLUtil.encodeDate(now)).append("');");

      } // end else

      // execute the statement
      stmt.executeUpdate(sql.toString());

      // save off the values to our local fields
      last_read = now;
      settings_loaded = true;

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

    } // end finally

  } // end touchRead

  public void touchPost(Connection conn, java.util.Date post_date) throws SQLException
  {
    if (deleted || env.isAnonymous())
      return;  // anonymous user can't update squat

    Statement stmt = null;

    try
    { // create the statement
      stmt = conn.createStatement();
      StringBuffer sql = new StringBuffer();

      if (settings_loaded)
      { // generate an update statement
	sql.append("UPDATE confsettings SET last_post = '").append(SQLUtil.encodeDate(post_date));
	sql.append("' WHERE confid = ").append(confid).append(" AND uid = ").append(env.getUserID());
	sql.append(';');

      } // end if
      else
      { // need to insert a confsettings row
	sql.append("INSERT INTO confsettings (confid, uid, default_pseud, last_read) VALUES (").append(confid);
	sql.append(", ").append(env.getUserID()).append(", '").append(SQLUtil.encodeString(pseud));
	sql.append("', '").append(SQLUtil.encodeDate(post_date)).append("');");

      } // end else

      // execute the statement
      stmt.executeUpdate(sql.toString());

      // save off the values to our local fields
      last_post = post_date;
      settings_loaded = true;

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

    } // end finally

  } // end touchRead

  public String realConfAlias()
  {
    if (deleted)
      return null;

    try
    { // just pull it from the lower-level object
      return getConferenceData().getAnAlias();

    } // end try
    catch (DataException de)
    { // whoops - this wasn't supposed to happen!
      logger.error("DataException in realConfAlias(): " + de.getMessage(),de);
      return null;

    } // end catch

  } // end realConfAlias

  public boolean userCanScribble()
  {
    ConferenceCommunityContext c = getConferenceDataNE();
    if (c==null)
      return false;
    return c.canScribblePosts(level);

  } // end userCanScribble

  public boolean userCanNuke()
  {
    ConferenceCommunityContext c = getConferenceDataNE();
    if (c==null)
      return false;
    return c.canNukePosts(level);

  } // end userCanNuke

  public boolean userCanRead()
  {
    return canReadConference();

  } // end userCanRead

  public boolean userCanPost()
  {
    return canPostToConference();

  } // end userCanPost

  public void touchUpdate(Connection conn, java.util.Date date) throws DataException
  {
    getConferenceData().touchUpdate(conn,date);

  } // end getConferenceData

  public PostLinkDecoderContext createDecoderContext(short topicid)
  {
    return new PostLinkDecoderContext(env.getCommunity().realCommunityAlias(),realConfAlias(),topicid);

  } // end createDecoderContext

  public int env_getConfLevel()
  {
    return level;

  } // end env_getConfLevel

  public String env_getConfName()
  {
    return this.getName();

  } // end env_getConfName

  /*--------------------------------------------------------------------------------
   * Static functions usable only from within the package
   *--------------------------------------------------------------------------------
   */

  static final List getCommunityConferences(EnvCommunity env) throws DataException
  {
    if (logger.isDebugEnabled())
      logger.debug("getCommunityConferences for community # " + env.getCommunityID() + ", 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();

      // Build a monster query!  The idea is that we get the basic info of all conferences linked to the
      // community, as well as the user level (within the conference) that gets granted to all members of the
      // linked community.  (Commonly this is 0; the standard user level within the community will then take
      // precedence.) If the user has an entry within the confmember table for that conference, the level
      // granted by that entry is also reported.  The highest of these two levels and the level of the user
      // within the community is then taken to be the user's access level within the conference.
      StringBuffer sql =
	  new StringBuffer("SELECT c.confid, c.createdate, c.name, c.descr, s.granted_lvl, m.granted_lvl "
			   + "FROM sigtoconf s, confs c LEFT JOIN confmember m ON c.confid = m.confid "
			   + "AND m.uid = ");
      sql.append(env.getUserID()).append(" WHERE s.confid = c.confid AND s.sigid = ");
      sql.append(env.getCommunityID());
      if (env.getCommunity().userHideHiddenConferences())
	sql.append(" AND s.hide_list = 0");
      sql.append(" ORDER BY s.sequence, c.name;");
      if (logger.isDebugEnabled())
	logger.debug("SQL: " + sql.toString());

      // Run that monster query!
      ResultSet rs = stmt.executeQuery(sql.toString());

      while (rs.next())
      { // create the appropriate objects to return
	ConferenceContext cc = new ConferenceUserContextImpl(env,rs.getInt(1),rs.getString(3),rs.getString(4),
							     SQLUtil.getFullDateTime(rs,2),rs.getInt(5),
							     rs.getInt(6),conn);
	rc.add(cc);

      } // end while

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

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

    } // end finally

    return Collections.unmodifiableList(rc);

  } // end getCommunityConferences

  static final ConferenceContext getConference(EnvCommunity env, int confid) throws DataException
  {
    if (logger.isDebugEnabled())
      logger.debug("getConference(#" + confid + ") for community # " + env.getCommunityID() + ", user #"
		   + env.getUserID());

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

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

      // Build a monster query!  This is like the query in getCommunityConferences, except that we add the
      // conference ID as a search term (thus we can return at most one row).
      StringBuffer sql =
	  new StringBuffer("SELECT c.createdate, c.name, c.descr, s.granted_lvl, m.granted_lvl "
			   + "FROM sigtoconf s, confs c LEFT JOIN confmember m ON c.confid = m.confid "
			   + "AND m.uid = ");
      sql.append(env.getUserID()).append(" WHERE s.confid = c.confid AND s.sigid = ");
      sql.append(env.getCommunityID()).append(" AND c.confid = ").append(confid).append(';');
      if (logger.isDebugEnabled())
	logger.debug("SQL: " + sql.toString());

      // Run that monster query!
      ResultSet rs = stmt.executeQuery(sql.toString());
      if (!(rs.next()))
	throw new DataException("conference ID#" + confid + " not found in community#"
				+ env.getCommunityID());

      // pass back the new object
      return new ConferenceUserContextImpl(env,confid,rs.getString(2),rs.getString(3),
					   SQLUtil.getFullDateTime(rs,1),rs.getInt(4),rs.getInt(5),conn);

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

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

    } // end finally

  } // end getConference

  static final ConferenceContext getConference(EnvCommunity env, String alias) throws DataException
  {
    if (logger.isDebugEnabled())
      logger.debug("getConference(\"" + alias + "\") for community # " + env.getCommunityID()
		   + ", user #" + env.getUserID());

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

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

      // Build a monster query!  This is similar to the other getConference query, except that now we
      // have an extra table in the query (confalias) and we match on that.  Notice that pretty much
      // everything is joined on the conference ID...
      StringBuffer sql =
	  new StringBuffer("SELECT c.confid, c.createdate, c.name, c.descr, s.granted_lvl, m.granted_lvl "
			   + "FROM sigtoconf s, confalias a, confs c LEFT JOIN confmember m ON "
			   + "c.confid = m.confid AND m.uid = ");
      sql.append(env.getUserID()).append(" WHERE s.confid = c.confid AND a.confid = c.confid AND s.sigid = ");
      sql.append(env.getCommunityID()).append(" AND a.alias = '").append(SQLUtil.encodeString(alias));
      sql.append("';");
      if (logger.isDebugEnabled())
	logger.debug("SQL: " + sql.toString());

      // Run that monster query!
      ResultSet rs = stmt.executeQuery(sql.toString());
      if (!(rs.next()))
	throw new DataException("conference \"" + alias + "\" not found in community#"
				+ env.getCommunityID());

      // pass back the new object
      return new ConferenceUserContextImpl(env,rs.getInt(1),rs.getString(3),rs.getString(4),
					   SQLUtil.getFullDateTime(rs,2),rs.getInt(5),rs.getInt(6),conn);

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

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

    } // end finally

  } // end getConference

  static final List getUserHotlist(EnvUser env) throws DataException
  {
    if (logger.isDebugEnabled())
      logger.debug("getUserHotlist for user #" + env.getUserID());

    Connection conn = null;      // pooled database connection
    Statement stmt = null;
    ArrayList rc = new ArrayList();    // return from this function

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

      // Build a monster query!  We pull the CommunityID and ConfID entries from the confhotlist table,
      // but we need to pull in "confs" and "confmember" (left joined) to get enough data to create
      // a ConferenceUserContextImpl object.
      StringBuffer sql =
	new StringBuffer("SELECT h.sigid, h.confid, c.name, c.descr, c.createdate, s.granted_lvl, "
			 + "f.granted_lvl, h.sequence FROM confhotlist h, sigtoconf s, confs c "
			 + "LEFT JOIN confmember f ON h.confid = f.confid AND h.uid = f.uid "
			 + "WHERE h.confid = s.confid AND c.confid = h.confid AND h.uid = ");
      sql.append(env.getUserID()).append(" ORDER BY h.sequence ASC;");
      if (logger.isDebugEnabled())
	logger.debug("SQL: " + sql.toString());

      // Run that puppy!
      ResultSet rs = stmt.executeQuery(sql.toString());

      // Create some temporary data structures to assist us in building the return list.
      HashMap env_cache = new HashMap();

      while (rs.next())
      { // retrieve the community id from the resultset first
	Integer cid = new Integer(rs.getInt(1));

	// we need a community environment for our conference, so make sure we have one
	EnvCommunity ecomm = (EnvCommunity)(env_cache.get(cid));
	if (ecomm==null)
	{ // get the backend for this community and cache it
	  CommunityBackend cb = CommunityUserContextImpl.getCommunityBackend(env,conn,cid.intValue());
	  ecomm = new EnvCommunity(env,cb);
	  env_cache.put(cid,ecomm);
	  
	} // end if

	// make the new ConferenceContext
	ConferenceContext conf =
	    new ConferenceUserContextImpl(ecomm,rs.getInt(2),rs.getString(3),rs.getString(4),
					  SQLUtil.getFullDateTime(rs,5),rs.getInt(6),rs.getInt(7),conn);

	// and create the actual return value
	ConferenceHotlistEntry hle = new MyHotlist(conf,rs.getInt(8));
	rc.add(hle);

      } // end while

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

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

    } // end finally

    return Collections.unmodifiableList(rc);

  } // end getUserHotlist

} // end class ConferenceUserContextImpl
